wip: improved insurance selection
This commit is contained in:
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Traits\SortByPriceTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
@@ -18,6 +19,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
*/
|
||||
class Travel
|
||||
{
|
||||
use SortByPriceTrait;
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $id = null;
|
||||
|
||||
@@ -98,7 +100,7 @@ class Travel
|
||||
* Filters additional services based on the provided subtype(s), availability,
|
||||
* and optionally whether their date range overlaps with the travel dates.
|
||||
* Services with null dates are considered always available when date filtering is enabled.
|
||||
* Services are sorted alphabetically by label.
|
||||
* Services are sorted by price in ascending order.
|
||||
*
|
||||
* @param mixed $subTypes The service subtype(s) to filter by
|
||||
* @param bool $filterAvailable Whether to include only available services
|
||||
@@ -135,11 +137,7 @@ class Travel
|
||||
return true;
|
||||
});
|
||||
|
||||
usort($services, function (Service $a, Service $b) {
|
||||
return $a->label <=> $b->label;
|
||||
});
|
||||
|
||||
return $services;
|
||||
return $this->sortByPrice($services);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Traits;
|
||||
|
||||
/**
|
||||
* Provides generic price-based sorting functionality for services and insurances.
|
||||
*
|
||||
* This trait implements consistent ascending price sorting across the application
|
||||
* for any objects that have a price property. Null prices are treated as zero,
|
||||
* ensuring items without pricing information appear first in sorted results.
|
||||
*
|
||||
* Usage:
|
||||
* - Services (skipasses, courses, rentals, board options, etc.)
|
||||
* - Insurances (individual and package insurances)
|
||||
*
|
||||
* Excluded from price sorting:
|
||||
* - Transportation services (sorted by subType)
|
||||
* - Pickup locations (no specific sorting)
|
||||
*/
|
||||
trait SortByPriceTrait
|
||||
{
|
||||
/**
|
||||
* Sorts an array of objects by their price property in ascending order.
|
||||
*
|
||||
* This method provides a consistent sorting strategy across all price-based
|
||||
* collections in the application. Items are sorted from lowest to highest price,
|
||||
* with null prices being treated as zero (appearing first).
|
||||
*
|
||||
* @param array $items Array of objects with a price property to sort
|
||||
*
|
||||
* @return array The sorted array of objects (ascending by price)
|
||||
*/
|
||||
protected function sortByPrice(array $items): array
|
||||
{
|
||||
usort($items, function (object $a, object $b) {
|
||||
$priceA = $a->price ?? 0;
|
||||
$priceB = $b->price ?? 0;
|
||||
|
||||
return $priceA <=> $priceB;
|
||||
});
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -34,15 +34,16 @@ class InsuranceParser extends AbstractParser
|
||||
$xmlContent->filterXPath('//versicherungen/versicherung')
|
||||
->each(function (Crawler $node) use (&$individualInsurances, &$insurances, $referencedIds) {
|
||||
$id = (int) $node->attr('idbuspro'); // Individual insurances have int IDs
|
||||
$isAdditional = $this->getBoolAttributeValue($node->attr('zusatzversicherung'));
|
||||
$isComplementary = $this->getBoolAttributeValue($node->attr('zusatzversicherung'));
|
||||
|
||||
// Parse all individual insurances for reference lookup
|
||||
$insurance = $this->parseInsuranceNode($node, false);
|
||||
if (null !== $insurance && null !== $insurance->id) {
|
||||
$individualInsurances[$insurance->id] = $insurance;
|
||||
|
||||
// Include if: not additional insurance OR referenced by package
|
||||
if (!$isAdditional || in_array($id, $referencedIds, true)) {
|
||||
// Include only if it's NOT a complementary insurance (zusatzversicherung)
|
||||
// Complementary insurances are only available as part of packages, never standalone
|
||||
if (!$isComplementary) {
|
||||
$insurances[$insurance->id] = $insurance;
|
||||
}
|
||||
}
|
||||
@@ -101,7 +102,7 @@ class InsuranceParser extends AbstractParser
|
||||
$idValue = $node->attr('idbuspro');
|
||||
$insurance->id = $isPackage ? $idValue : (int) $idValue;
|
||||
$insurance->code = $node->attr('code');
|
||||
$insurance->label = $node->attr('bezeichnung');
|
||||
$insurance->label = $this->normalizeInsuranceLabel($node->attr('bezeichnung'));
|
||||
$insurance->subType = $node->attr('unterart');
|
||||
$insurance->price = $node->attr('preis') ?
|
||||
$this->stringToFloat($node->attr('preis')) : null;
|
||||
@@ -183,6 +184,36 @@ class InsuranceParser extends AbstractParser
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes insurance labels by removing redundant phrases and replacing abbreviations.
|
||||
*
|
||||
* Transformations:
|
||||
* - Removes "Auto/Bahn/Bus (Europa)" - redundant travel mode specification
|
||||
* - Replaces "FAM" with "Familie" - clearer German terminology
|
||||
*
|
||||
* @param string|null $label The original insurance label from XML
|
||||
*
|
||||
* @return string|null The normalized label, or null if input was null
|
||||
*/
|
||||
private function normalizeInsuranceLabel(?string $label): ?string
|
||||
{
|
||||
if (null === $label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove redundant travel mode specification
|
||||
$label = str_replace('Auto/Bahn/Bus (Europa)', '', $label);
|
||||
|
||||
// Replace abbreviation with full German word
|
||||
$label = str_replace('FAM', 'Familie', $label);
|
||||
|
||||
// Clean up extra spaces that may result from removals
|
||||
$label = preg_replace('/\s+/', ' ', $label);
|
||||
$label = trim($label);
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
private function getBoolAttributeValue(?string $value): bool
|
||||
{
|
||||
return !empty($value) && $this->stringToBool($value);
|
||||
|
||||
@@ -762,11 +762,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
$label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.'));
|
||||
}
|
||||
|
||||
// Add type information if available using centralized mapping
|
||||
if (null !== $insurance->subType && isset(Constants::INSURANCE_LABELS[$insurance->subType])) {
|
||||
$label .= sprintf(' (%s)', Constants::INSURANCE_LABELS[$insurance->subType]);
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Traits\SortByPriceTrait;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Model\InsuranceEligibilityCriteria;
|
||||
@@ -19,6 +20,7 @@ use Carbon\Carbon;
|
||||
*/
|
||||
class InsuranceMatchingService
|
||||
{
|
||||
use SortByPriceTrait;
|
||||
public function __construct(
|
||||
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||
) {
|
||||
@@ -41,10 +43,12 @@ class InsuranceMatchingService
|
||||
return []; // Cannot match insurances without travel dates
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
$eligibleInsurances = array_filter(
|
||||
$insurances,
|
||||
fn (Insurance $insurance) => $this->isInsuranceEligible($insurance, $criteria)
|
||||
);
|
||||
|
||||
return $this->sortByPrice($eligibleInsurances);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,7 +81,7 @@ class ParticipantEligibilityService
|
||||
|
||||
$travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom);
|
||||
$birthDate = CarbonImmutable::instance($participant->dateOfBirth);
|
||||
$ageAtTravelStart = $travelStartDate->diffInYears($birthDate);
|
||||
$ageAtTravelStart = $birthDate->diffInYears($travelStartDate);
|
||||
$birthYear = (int) $birthDate->format('Y');
|
||||
|
||||
$constraintType = $service->ageConstraintType ?? 'absolute_age';
|
||||
|
||||
Reference in New Issue
Block a user