From a5dce776dea29014ba18c596d0dc85d6de532da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 30 Sep 2025 14:37:49 +0200 Subject: [PATCH] wip: insurance booking --- src/BusProNet/Constants.php | 20 ++ src/BusProNet/XmlLoader/InsuranceLoader.php | 27 -- .../ParticipantFieldHandlerRegistry.php | 6 + .../ParticipantFieldOptionsProvider.php | 17 +- .../ParticipantInsuranceFieldHandler.php | 81 ++++-- src/Service/BookingPriceCalculatorService.php | 23 +- src/Service/InsuranceMatchingService.php | 14 +- src/Service/InsuranceTypeResolver.php | 135 --------- templates/booking/create_step_2.html.twig | 14 +- .../Service/InsuranceMatchingServiceTest.php | 12 +- tests/Service/InsuranceTypeResolverTest.php | 257 ------------------ 11 files changed, 131 insertions(+), 475 deletions(-) delete mode 100644 src/Service/InsuranceTypeResolver.php delete mode 100644 tests/Service/InsuranceTypeResolverTest.php diff --git a/src/BusProNet/Constants.php b/src/BusProNet/Constants.php index 25b365a..022556c 100644 --- a/src/BusProNet/Constants.php +++ b/src/BusProNet/Constants.php @@ -19,11 +19,31 @@ final class Constants public const TOKEN_INSURANCES = ['RRV', 'PAK', 'OHN', 'PKG']; public const TOKEN_PARKING = 'PAR'; + // Insurance subtype labels for display + public const INSURANCE_LABELS = [ + 'RRV' => 'Reiserücktrittsversicherung', + 'PAK' => 'Reiseschutz', + 'OHN' => 'Selbstbehalt', + ]; + // Normalized service group keys for pricing display public const GROUP_TRANSPORTATION = 'transportation'; public const GROUP_RENTALS = 'rentals'; public const GROUP_INSURANCE = 'insurance'; + // Service type display labels (German) - used in pricing calculator + public const SERVICE_LABELS = [ + self::TOKEN_COURSES => 'Kurse', + self::TOKEN_SKI_PASS => 'Skipässe', + self::TOKEN_ADDITIONAL => 'Zusatzleistungen', + self::TOKEN_BOARD => 'Verpflegung', + self::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung', + self::TOKEN_PARKING => 'Parkplatz', + self::GROUP_TRANSPORTATION => 'Beförderung', + self::GROUP_RENTALS => 'Leihmaterial', + self::GROUP_INSURANCE => 'Reiseversicherungen', + ]; + public const STATUS_AVAILABLE = 'Frei'; public const STATUS_BLOCKED = 'Buchungsstop'; public const STATUS_ON_REQUEST = 'Anfrage'; diff --git a/src/BusProNet/XmlLoader/InsuranceLoader.php b/src/BusProNet/XmlLoader/InsuranceLoader.php index 7f4c394..2006f97 100644 --- a/src/BusProNet/XmlLoader/InsuranceLoader.php +++ b/src/BusProNet/XmlLoader/InsuranceLoader.php @@ -44,33 +44,6 @@ class InsuranceLoader extends AbstractLoader return $insurances[$id] ?? null; } - /** - * Loads all insurances with resolved package relationships. - * - * @param string|null $filename The XML filename to load from - * @return array Array of Insurance objects with resolved relationships - */ - public function loadAllWithRelationships(?string $filename = 'versicherungen.xml'): array - { - $insurances = $this->loadAll($filename); - - // Resolve package relationships - foreach ($insurances as $insurance) { - if ($insurance->package && !empty($insurance->containedInsuranceIds)) { - foreach ($insurance->containedInsuranceIds as $containedId) { - if (isset($insurances[$containedId])) { - $insurance->containedInsurances[] = $insurances[$containedId]; - } else { - // Log missing insurance reference for debugging - error_log("Insurance package {$insurance->id} references missing insurance {$containedId}"); - } - } - } - } - - return $insurances; - } - private function loadXml(?string $filename = 'versicherungen.xml'): Crawler { $xml = $this->xmlExport->read($filename); diff --git a/src/Form/Service/ParticipantFieldHandlerRegistry.php b/src/Form/Service/ParticipantFieldHandlerRegistry.php index b85fd93..86da8dc 100644 --- a/src/Form/Service/ParticipantFieldHandlerRegistry.php +++ b/src/Form/Service/ParticipantFieldHandlerRegistry.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Form\Service; +use App\BusProNet\Model\Insurance; use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Service; use App\Form\Model\BookingDtoInterface; @@ -345,6 +346,11 @@ class ParticipantFieldHandlerRegistry return $value->id; } + // Handle Insurance objects -> convert to ID + if ($value instanceof Insurance) { + return $value->id; + } + // Handle DateTimeInterface -> convert to string format if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d'); diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index 4bfbfa3..e8e84bb 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -13,7 +13,6 @@ use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface; use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; -use App\Form\Service\ServiceAgeEvaluator; use App\Service\InsuranceMatchingService; use App\Service\ServiceAvailabilityCalculator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -591,7 +590,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider private function getParkingCheckboxLabel(array $parkingServices): string { if (empty($parkingServices)) { - return 'Parkplatz'; + return Constants::SERVICE_LABELS[Constants::TOKEN_PARKING]; } $parkingService = reset($parkingServices); // Get the first (and only) parking service @@ -681,7 +680,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider private function getRentalInsuranceCheckboxLabel(array $rentalInsuranceServices): string { if (empty($rentalInsuranceServices)) { - return 'Leihmaterial-Versicherung'; + return Constants::SERVICE_LABELS[Constants::TOKEN_RENTAL_INSURANCE]; } $rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service @@ -751,15 +750,9 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider $label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.')); } - // Add type information if available - if (null !== $insurance->subType) { - $typeLabel = match ($insurance->subType) { - 'RRV' => 'Reiserücktrittsversicherung', - 'PAK' => 'Reiseschutz', - 'OHN' => 'Selbstbehalt', - default => $insurance->subType, - }; - $label .= sprintf(' (%s)', $typeLabel); + // 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; diff --git a/src/Form/Service/ParticipantInsuranceFieldHandler.php b/src/Form/Service/ParticipantInsuranceFieldHandler.php index 99b339d..b615c6d 100644 --- a/src/Form/Service/ParticipantInsuranceFieldHandler.php +++ b/src/Form/Service/ParticipantInsuranceFieldHandler.php @@ -35,7 +35,7 @@ use App\Service\InsuranceMatchingService; class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler { public function __construct( - private readonly InsuranceMatchingService $insuranceMatchingService + private readonly InsuranceMatchingService $insuranceMatchingService, ) { } @@ -52,13 +52,27 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler /** * Returns the field dependencies for proper processing order. * - * This handler depends on dateOfBirth for age evaluation and insurance eligibility. + * This handler must run AFTER all fields that affect travel price, since insurance + * reassignment logic depends on accurate price calculations. It also depends on + * dateOfBirth for age evaluation and insurance eligibility. * - * @return string[] Array containing 'dateOfBirth' dependency + * @return string[] Array of field dependencies */ public function getDependencies(): array { - return ['dateOfBirth']; + return [ + 'dateOfBirth', // Required for age-based eligibility + 'skiPass', // Affects travel price + 'rentals', // Affects travel price + 'courses', // Affects travel price + 'additionalServices', // Affects travel price + 'board', // Affects travel price + 'transportationOutbound', // Affects travel price + 'transportationInbound', // Affects travel price + 'pickupOutbound', // Affects travel price + 'pickupInbound', // Affects travel price + 'parking', // Affects travel price + ]; } /** @@ -82,8 +96,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler * Processes the insurance field for a specific participant. * * This method handles both explicit insurance selection from form data and automatic - * reassignment when the participant's travel price changes. It maintains the same - * insurance type but adjusts to the appropriate price tier when needed. + * reassignment when the participant's travel price changes. It distinguishes between: + * 1. New selection: User explicitly changed their insurance choice + * 2. Resubmission: Form resubmitted with existing insurance (e.g., after adding rentals) + * + * For resubmissions, it checks if the current insurance is still eligible with updated + * participant data and automatically reassigns to the correct price tier if needed. * * @param array $submittedData The submitted participant form data * @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit) @@ -100,59 +118,63 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler $currentInsurance = $participant->insurance; $availableInsurances = $bookingDto->travel->insurances ?? []; - // Handle explicit insurance selection from form - if (null !== $selectedInsuranceId) { + // Determine if this is a new user selection or just form resubmission + $isNewSelection = null !== $selectedInsuranceId + && (null === $currentInsurance || !$this->isSameInsurance($selectedInsuranceId, $currentInsurance)); + + // Handle explicit new insurance selection from user + if ($isNewSelection) { $selectedInsurance = $this->findInsuranceById($availableInsurances, $selectedInsuranceId); - // Check if the selected insurance is still eligible for this participant if (null !== $selectedInsurance) { $eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto); $isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances); if ($isSelectedInsuranceEligible) { - // Insurance is still eligible - use it directly + // New selection is eligible - use it $participant->insurance = $selectedInsurance; - return; } else { - // Insurance is no longer eligible - try to reassign to same type with new price tier + // New selection is not eligible - try to find alternative in same type $reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange( $availableInsurances, $selectedInsurance, $participant, $bookingDto ); - $participant->insurance = $reassignedInsurance; - return; } } else { // Insurance not found - clear selection $participant->insurance = null; - return; } + + return; } - // Handle automatic reassignment if participant had an insurance but it's no longer eligible - if (null !== $currentInsurance) { - // Check if current insurance is still eligible + // Handle form resubmission with existing insurance (automatic reassignment check) + if (null !== $currentInsurance && null !== $selectedInsuranceId) { + // Check if current insurance is still eligible with updated participant data $eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto); $isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances); if (!$isCurrentInsuranceStillEligible) { - // Try to reassign to same insurance type with appropriate price tier + // Current insurance no longer eligible - try to reassign to same type with new price tier $reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange( $availableInsurances, $currentInsurance, $participant, $bookingDto ); - $participant->insurance = $reassignedInsurance; // null if no suitable match found - return; } + + return; } - // No insurance selected or reassignment needed - keep current state (may be null) + // Handle explicit deselection (user removed insurance) + if (null === $selectedInsuranceId) { + $participant->insurance = null; + } } /** @@ -221,4 +243,19 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler return false; } + /** + * Checks if the submitted insurance ID matches the current insurance. + * + * This is used to distinguish between a new user selection and a form resubmission + * with the existing insurance selection (e.g., when user adds rentals that change travel price). + * + * @param string|int $selectedInsuranceId The insurance ID from form submission + * @param Insurance $currentInsurance The currently assigned insurance from DTO + * + * @return bool True if they represent the same insurance + */ + private function isSameInsurance(string|int $selectedInsuranceId, Insurance $currentInsurance): bool + { + return (string) $currentInsurance->id === (string) $selectedInsuranceId; + } } diff --git a/src/Service/BookingPriceCalculatorService.php b/src/Service/BookingPriceCalculatorService.php index 87eb7c5..cdfab25 100644 --- a/src/Service/BookingPriceCalculatorService.php +++ b/src/Service/BookingPriceCalculatorService.php @@ -359,7 +359,7 @@ class BookingPriceCalculatorService if ($parkingTotal > 0) { $transportationItems['transportation_parking'] = [ 'serviceId' => 'transportation_parking', - 'label' => 'Parkplatz', + 'label' => Constants::SERVICE_LABELS[Constants::TOKEN_PARKING], 'unitPrice' => null, 'participantCount' => $parkingParticipants, 'totalPrice' => $parkingTotal, @@ -444,23 +444,12 @@ class BookingPriceCalculatorService */ private function getGroupNameForSubtype(string $subType): string { - $groupMapping = [ - Constants::TOKEN_COURSES => 'Kurse', - Constants::TOKEN_SKI_PASS => 'Skipässe', - Constants::TOKEN_ADDITIONAL => 'Zusatzleistungen', - Constants::TOKEN_BOARD => 'Verpflegung', - Constants::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung', - Constants::GROUP_TRANSPORTATION => 'Beförderung', - Constants::GROUP_RENTALS => 'Leihmaterial', // Normalized rental subtype - Constants::GROUP_INSURANCE => 'Reiseversicherungen', // Normalized insurance subtype - ]; - // Handle rentals array (keep for backward compatibility with non-normalized subtypes) if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { - return 'Leihmaterial'; + return Constants::SERVICE_LABELS[Constants::GROUP_RENTALS]; } - return $groupMapping[$subType] ?? 'Sonstige Leistungen'; + return Constants::SERVICE_LABELS[$subType] ?? 'Sonstige Leistungen'; } /** @@ -525,8 +514,8 @@ class BookingPriceCalculatorService /** * Calculates the total service cost for a single participant. * - * @param ParticipantDto $participant The participant to calculate services for - * @param bool $includeInsurance Whether to include insurance pricing (default: true) + * @param ParticipantDto $participant The participant to calculate services for + * @param bool $includeInsurance Whether to include insurance pricing (default: true) * * @return float The total service cost for this participant */ @@ -612,7 +601,7 @@ class BookingPriceCalculatorService * * @param array $serviceAggregation The service aggregation array to update * @param Insurance $insurance The insurance to add - * @param int $quantity The quantity of the insurance + * @param int $quantity The quantity of the insurance */ private function addInsuranceToServiceAggregation(array &$serviceAggregation, Insurance $insurance, int $quantity): void { diff --git a/src/Service/InsuranceMatchingService.php b/src/Service/InsuranceMatchingService.php index 19d8b12..b3a283c 100644 --- a/src/Service/InsuranceMatchingService.php +++ b/src/Service/InsuranceMatchingService.php @@ -8,8 +8,6 @@ use App\BusProNet\Model\Insurance; use App\Form\Model\BookingCreateDto; use App\Form\Model\ParticipantDto; use App\Model\InsuranceEligibilityCriteria; -use App\Service\BookingPriceCalculatorService; -use App\Service\InsuranceTypeResolver; use Carbon\Carbon; /** @@ -23,9 +21,9 @@ class InsuranceMatchingService { public function __construct( private readonly BookingPriceCalculatorService $priceCalculatorService, - private readonly InsuranceTypeResolver $insuranceTypeResolver ) { } + /** * Filters insurances based on participant and booking criteria. * @@ -78,6 +76,10 @@ class InsuranceMatchingService /** * Batch-assigns insurances of the same type to all participants based on individual pricing. * + * FUTURE FEATURE: This method will be used when implementing the "applicant assigns + * insurance to all participants" feature. The applicant's selection will be propagated + * to all participants with automatic price tier adjustment based on individual prices. + * * This method takes the applicant's insurance selection and assigns the same insurance type * (subType + familyInsurance) to all participants, but selects the appropriate price tier * based on each participant's individual travel price. @@ -87,6 +89,8 @@ class InsuranceMatchingService * @param BookingCreateDto $booking The booking with all participants * * @return array Array indexed by participant index with assigned insurances + * + * @internal Reserved for future feature implementation */ public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingCreateDto $booking): array { @@ -106,9 +110,13 @@ class InsuranceMatchingService /** * Creates eligibility criteria from participant and booking data. + * + * This method performs early validation before creating the criteria object + * to avoid unnecessary object instantiation when criteria cannot be satisfied. */ private function createEligibilityCriteria(ParticipantDto $participant, BookingCreateDto $booking): ?InsuranceEligibilityCriteria { + // Early return if travel dates are missing - cannot evaluate any criteria $travelStartDate = $booking->travel->dateFrom; $travelEndDate = $booking->travel->dateTo; diff --git a/src/Service/InsuranceTypeResolver.php b/src/Service/InsuranceTypeResolver.php deleted file mode 100644 index 8e28829..0000000 --- a/src/Service/InsuranceTypeResolver.php +++ /dev/null @@ -1,135 +0,0 @@ -package) { - return $this->mapSubtypeToType($insurance->subType, $insurance->familyInsurance); - } - - return $this->resolvePackageType($insurance); - } - - /** - * Resolves the type for an insurance package by analyzing contained insurances. - * - * @param Insurance $package The package to analyze - * - * @return string|null The resolved type or null if no primary type found - */ - private function resolvePackageType(Insurance $package): ?string - { - // Get primary (non-deductible) insurance type from contained insurances - $primaryType = null; - foreach ($package->containedInsurances as $contained) { - $baseType = $this->mapSubtypeToBaseType($contained->subType); - if ($baseType && self::DEDUCTIBLE !== $baseType) { - $primaryType = $baseType; - break; - } - } - - // Apply family suffix using package's familyInsurance flag - return $primaryType ? $primaryType.($package->familyInsurance ? '_FAMILY' : '') : null; - } - - /** - * Maps a subtype to a full type including family variant. - * - * @param string|null $subtype The subtype to map - * @param bool $isFamily Whether this is a family insurance - * - * @return string|null The mapped type or null if unmappable - */ - private function mapSubtypeToType(?string $subtype, bool $isFamily): ?string - { - $baseType = $this->mapSubtypeToBaseType($subtype); - - return $baseType ? $baseType.($isFamily ? '_FAMILY' : '') : null; - } - - /** - * Maps a subtype to its base type constant. - * - * @param string|null $subtype The subtype to map - * - * @return string|null The base type constant or null if unmappable - */ - private function mapSubtypeToBaseType(?string $subtype): ?string - { - return match ($subtype) { - 'RRV' => self::TRAVEL_CANCELLATION, - 'PAK' => self::TRAVEL_PROTECTION, - 'OHN' => self::DEDUCTIBLE, - default => null, - }; - } - - /** - * Gets all available insurance type constants. - * - * @return array Array of all type constants - */ - public function getAllTypes(): array - { - return [ - self::TRAVEL_CANCELLATION, - self::TRAVEL_CANCELLATION_FAMILY, - self::TRAVEL_PROTECTION, - self::TRAVEL_PROTECTION_FAMILY, - self::DEDUCTIBLE, - ]; - } - - /** - * Checks if a type is a family variant. - * - * @param string $type The type to check - * - * @return bool True if the type is a family variant - */ - public function isFamilyType(string $type): bool - { - return str_ends_with($type, '_FAMILY'); - } - - /** - * Gets the base type from a family type. - * - * @param string $type The type to get the base for - * - * @return string The base type (without _FAMILY suffix) - */ - public function getBaseType(string $type): string - { - return str_replace('_FAMILY', '', $type); - } -} diff --git a/templates/booking/create_step_2.html.twig b/templates/booking/create_step_2.html.twig index 0d97b9d..7b4891e 100644 --- a/templates/booking/create_step_2.html.twig +++ b/templates/booking/create_step_2.html.twig @@ -95,7 +95,7 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when skipass not available #} +
{# Empty div to maintain grid layout when not available #} {% endif %} {% if participant.courses is defined %} {{ form_row(participant.courses, { @@ -105,6 +105,8 @@ 'hx-swap': 'none' } }) }} + {% else %} +
{# Empty div to maintain grid layout when not available #} {% endif %} {% if participant.additionalServices is defined %} {{ form_row(participant.additionalServices, { @@ -114,6 +116,8 @@ 'hx-swap': 'none' } }) }} + {% else %} +
{# Empty div to maintain grid layout when not available #} {% endif %} {% if participant.rentals is defined %} {{ form_row(participant.rentals, { @@ -123,6 +127,8 @@ 'hx-swap': 'none' } }) }} + {% else %} +
{# Empty div to maintain grid layout when not available #} {% endif %} {% if participant.rentalInsurance is defined %} {{ form_row(participant.rentalInsurance, { @@ -132,6 +138,8 @@ 'hx-swap': 'none' } }) }} + {% else %} +
{# Empty div to maintain grid layout when not available #} {% endif %} {% if participant.board is defined %} {{ form_row(participant.board, { @@ -141,6 +149,8 @@ 'hx-swap': 'none' } }) }} + {% else %} +
{# Empty div to maintain grid layout when not available #} {% endif %} {% if participant.insurance is defined %} {{ form_row(participant.insurance, { @@ -150,6 +160,8 @@ 'hx-swap': 'none' } }) }} + {% else %} +
{# Empty div to maintain grid layout when not available #} {% endif %} diff --git a/tests/Service/InsuranceMatchingServiceTest.php b/tests/Service/InsuranceMatchingServiceTest.php index 54cb1ee..9b0c747 100644 --- a/tests/Service/InsuranceMatchingServiceTest.php +++ b/tests/Service/InsuranceMatchingServiceTest.php @@ -7,6 +7,7 @@ namespace App\Tests\Service; use App\BusProNet\Model\Insurance; use App\Form\Model\BookingCreateDto; use App\Form\Model\ParticipantDto; +use App\Service\BookingPriceCalculatorService; use App\Service\InsuranceMatchingService; use Carbon\Carbon; use PHPUnit\Framework\TestCase; @@ -14,10 +15,17 @@ use PHPUnit\Framework\TestCase; class InsuranceMatchingServiceTest extends TestCase { private InsuranceMatchingService $service; + private BookingPriceCalculatorService $priceCalculator; protected function setUp(): void { - $this->service = new InsuranceMatchingService(); + // Mock the price calculator service + $this->priceCalculator = $this->createMock(BookingPriceCalculatorService::class); + $this->priceCalculator + ->method('calculateIndividualParticipantPriceExcludingInsurance') + ->willReturn(500.0); // Default test price + + $this->service = new InsuranceMatchingService($this->priceCalculator); // Set a fixed test date for consistent test results Carbon::setTestNow('2024-06-01 12:00:00'); @@ -164,6 +172,7 @@ class InsuranceMatchingServiceTest extends TestCase public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void { $participant = new ParticipantDto(); + $participant->index = 0; // Set participant index for price calculations $participant->dateOfBirth = null; $booking = $this->createBooking('2024-08-01', '2024-08-08'); @@ -239,6 +248,7 @@ class InsuranceMatchingServiceTest extends TestCase private function createParticipant(string $dateOfBirth): ParticipantDto { $participant = new ParticipantDto(); + $participant->index = 0; // Set participant index for price calculations $participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth); return $participant; diff --git a/tests/Service/InsuranceTypeResolverTest.php b/tests/Service/InsuranceTypeResolverTest.php deleted file mode 100644 index 75c4733..0000000 --- a/tests/Service/InsuranceTypeResolverTest.php +++ /dev/null @@ -1,257 +0,0 @@ -resolver = new InsuranceTypeResolver(); - } - - public function testResolveTypeForIndividualTravelCancellationInsurance(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = 'RRV'; - $insurance->familyInsurance = false; - - $result = $this->resolver->resolveType($insurance); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION, $result); - } - - public function testResolveTypeForIndividualFamilyTravelCancellationInsurance(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = 'RRV'; - $insurance->familyInsurance = true; - - $result = $this->resolver->resolveType($insurance); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY, $result); - } - - public function testResolveTypeForIndividualTravelProtectionInsurance(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = 'PAK'; - $insurance->familyInsurance = false; - - $result = $this->resolver->resolveType($insurance); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result); - } - - public function testResolveTypeForIndividualFamilyTravelProtectionInsurance(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = 'PAK'; - $insurance->familyInsurance = true; - - $result = $this->resolver->resolveType($insurance); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, $result); - } - - public function testResolveTypeForIndividualDeductibleInsurance(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = 'OHN'; - $insurance->familyInsurance = false; - - $result = $this->resolver->resolveType($insurance); - - $this->assertEquals(InsuranceTypeResolver::DEDUCTIBLE, $result); - } - - public function testResolveTypeForIndividualUnknownSubtype(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = 'UNKNOWN'; - $insurance->familyInsurance = false; - - $result = $this->resolver->resolveType($insurance); - - $this->assertNull($result); - } - - public function testResolveTypeForIndividualNullSubtype(): void - { - $insurance = new Insurance(); - $insurance->package = false; - $insurance->subType = null; - $insurance->familyInsurance = false; - - $result = $this->resolver->resolveType($insurance); - - $this->assertNull($result); - } - - public function testResolveTypeForPackageWithTravelProtectionContent(): void - { - // Create contained insurances - $protectionInsurance = new Insurance(); - $protectionInsurance->subType = 'PAK'; - - $deductibleInsurance = new Insurance(); - $deductibleInsurance->subType = 'OHN'; - - // Create package - $package = new Insurance(); - $package->package = true; - $package->familyInsurance = false; - $package->containedInsurances = [$protectionInsurance, $deductibleInsurance]; - - $result = $this->resolver->resolveType($package); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result); - } - - public function testResolveTypeForFamilyPackageWithTravelProtectionContent(): void - { - // Create contained insurances - $protectionInsurance = new Insurance(); - $protectionInsurance->subType = 'PAK'; - - $deductibleInsurance = new Insurance(); - $deductibleInsurance->subType = 'OHN'; - - // Create family package - $package = new Insurance(); - $package->package = true; - $package->familyInsurance = true; - $package->containedInsurances = [$protectionInsurance, $deductibleInsurance]; - - $result = $this->resolver->resolveType($package); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, $result); - } - - public function testResolveTypeForPackageWithTravelCancellationContent(): void - { - // Create contained insurances - $cancellationInsurance = new Insurance(); - $cancellationInsurance->subType = 'RRV'; - - $deductibleInsurance = new Insurance(); - $deductibleInsurance->subType = 'OHN'; - - // Create package - $package = new Insurance(); - $package->package = true; - $package->familyInsurance = false; - $package->containedInsurances = [$cancellationInsurance, $deductibleInsurance]; - - $result = $this->resolver->resolveType($package); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION, $result); - } - - public function testResolveTypeForPackageWithOnlyDeductibleContent(): void - { - // Create contained insurance (only deductible) - $deductibleInsurance = new Insurance(); - $deductibleInsurance->subType = 'OHN'; - - // Create package - $package = new Insurance(); - $package->package = true; - $package->familyInsurance = false; - $package->containedInsurances = [$deductibleInsurance]; - - $result = $this->resolver->resolveType($package); - - $this->assertNull($result); - } - - public function testResolveTypeForPackageWithNoContainedInsurances(): void - { - $package = new Insurance(); - $package->package = true; - $package->familyInsurance = false; - $package->containedInsurances = []; - - $result = $this->resolver->resolveType($package); - - $this->assertNull($result); - } - - public function testResolveTypeForPackageWithMultiplePrimaryTypes(): void - { - // Create contained insurances with multiple primary types - $protectionInsurance = new Insurance(); - $protectionInsurance->subType = 'PAK'; - - $cancellationInsurance = new Insurance(); - $cancellationInsurance->subType = 'RRV'; - - // Create package - should return first non-deductible type found - $package = new Insurance(); - $package->package = true; - $package->familyInsurance = false; - $package->containedInsurances = [$protectionInsurance, $cancellationInsurance]; - - $result = $this->resolver->resolveType($package); - - $this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result); - } - - public function testGetAllTypes(): void - { - $types = $this->resolver->getAllTypes(); - - $expectedTypes = [ - InsuranceTypeResolver::TRAVEL_CANCELLATION, - InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY, - InsuranceTypeResolver::TRAVEL_PROTECTION, - InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, - InsuranceTypeResolver::DEDUCTIBLE, - ]; - - $this->assertEquals($expectedTypes, $types); - $this->assertCount(5, $types); - } - - public function testIsFamilyType(): void - { - $this->assertTrue($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY)); - $this->assertTrue($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY)); - $this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_CANCELLATION)); - $this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_PROTECTION)); - $this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::DEDUCTIBLE)); - } - - public function testGetBaseType(): void - { - $this->assertEquals( - InsuranceTypeResolver::TRAVEL_CANCELLATION, - $this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY) - ); - $this->assertEquals( - InsuranceTypeResolver::TRAVEL_PROTECTION, - $this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY) - ); - $this->assertEquals( - InsuranceTypeResolver::TRAVEL_CANCELLATION, - $this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_CANCELLATION) - ); - $this->assertEquals( - InsuranceTypeResolver::DEDUCTIBLE, - $this->resolver->getBaseType(InsuranceTypeResolver::DEDUCTIBLE) - ); - } -}