From e6eebc00a317ad7e5bf6dce39cb753fbd7e60c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 30 Sep 2025 15:13:20 +0200 Subject: [PATCH] wip: insurance booking --- .../ParticipantFieldHandlerRegistry.php | 35 +++++++++----- .../ParticipantFieldOptionsProvider.php | 1 - src/Form/Service/ServiceAgeEvaluator.php | 33 ++++++++----- templates/booking/create_step_2.html.twig | 47 +++++++++++++------ 4 files changed, 76 insertions(+), 40 deletions(-) diff --git a/src/Form/Service/ParticipantFieldHandlerRegistry.php b/src/Form/Service/ParticipantFieldHandlerRegistry.php index 86da8dc..1935aec 100644 --- a/src/Form/Service/ParticipantFieldHandlerRegistry.php +++ b/src/Form/Service/ParticipantFieldHandlerRegistry.php @@ -91,10 +91,18 @@ class ParticipantFieldHandlerRegistry /** * Processes all participant fields from submitted form data using registered handlers. * - * This is the main entry point for field processing. It iterates through all - * participants in the submitted data and applies the appropriate handlers in - * dependency order. Each handler determines whether it should process the - * participant's data and updates the booking DTO accordingly. + * This is the main entry point for field processing. It processes handlers in dependency + * order, applying each handler to ALL participants before moving to the next handler. + * This ensures that cross-participant logic (like family booking detection) has access + * to complete data from all participants. + * + * Processing order: handler-first, then participants + * - Process handler A for all participants + * - Process handler B for all participants + * - etc. + * + * This is critical for handlers that depend on booking-level state (like insurance + * family detection which needs all participants' ages to be processed first). * * @param array $submittedData The submitted form data containing participants array * @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values (create or edit) @@ -109,16 +117,17 @@ class ParticipantFieldHandlerRegistry // Get handlers sorted by dependency order (uses cache if available) $sortedHandlerNames = $this->getSortedHandlers(); - // Process each participant's data - foreach ($submittedData['participants'] as $participantIndex => $participantData) { - // Skip invalid participant data - if (false === is_array($participantData)) { - continue; - } + // Process each handler across all participants before moving to the next handler + // This ensures booking-level state (like family booking detection) is accurate + foreach ($sortedHandlerNames as $handlerName) { + $handler = $this->handlers[$handlerName]; - // Apply each handler in dependency order - foreach ($sortedHandlerNames as $handlerName) { - $handler = $this->handlers[$handlerName]; + // Apply this handler to all participants + foreach ($submittedData['participants'] as $participantIndex => $participantData) { + // Skip invalid participant data + if (false === is_array($participantData)) { + continue; + } // Let each handler decide if it should process this participant's data if ($handler->shouldProcess($participantData, (int) $participantIndex)) { diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index e8e84bb..e62376e 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -416,7 +416,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider ), 'choice_label' => fn (?Insurance $insurance) => $this->formatInsuranceLabel($insurance), 'choice_value' => 'id', - 'help' => 'Wählen Sie eine passende Reiseversicherung für diese Person aus.', 'attr' => [ 'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'), 'hx-swap' => 'none', diff --git a/src/Form/Service/ServiceAgeEvaluator.php b/src/Form/Service/ServiceAgeEvaluator.php index 8b16f19..8087168 100644 --- a/src/Form/Service/ServiceAgeEvaluator.php +++ b/src/Form/Service/ServiceAgeEvaluator.php @@ -35,6 +35,10 @@ class ServiceAgeEvaluator * constraint requirements. Returns false if the participant doesn't * meet the age requirements or if no birth date is provided. * + * Age constraints are evaluated at the travel start date to ensure + * that services are appropriately filtered based on the participant's + * age at the time of travel, not their current age. + * * @param Service $service The service to evaluate * @param BookingDtoInterface $bookingDto The booking containing participant data * @param int $participantIndex The index of the participant to evaluate @@ -49,26 +53,30 @@ class ServiceAgeEvaluator return false; // Cannot evaluate without birth date } + // Use travel start date as reference for absolute age calculations + $travelStartDate = $bookingDto->travel->dateFrom; + return match ($service->ageConstraintType) { - 'absolute_age' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth), + 'absolute_age' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth, $travelStartDate), 'birth_year' => $this->evaluateBirthYear($service, $participant->dateOfBirth), - 'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth) + 'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth, $travelStartDate) && $this->evaluateBirthYear($service, $participant->dateOfBirth), default => true, // No constraints or unknown type }; } /** - * Evaluates absolute age constraints against participant's current age. + * Evaluates absolute age constraints against participant's age at travel date. * - * @param Service $service The service with age constraints - * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * @param Service $service The service with age constraints + * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * @param \DateTimeImmutable|null $referenceDate The reference date for age calculation (travel start date) * * @return bool True if the participant meets the absolute age requirements */ - private function evaluateAbsoluteAge(Service $service, \DateTimeImmutable $dateOfBirth): bool + private function evaluateAbsoluteAge(Service $service, \DateTimeImmutable $dateOfBirth, ?\DateTimeImmutable $referenceDate = null): bool { - $age = $this->calculateAge($dateOfBirth); + $age = $this->calculateAge($dateOfBirth, $referenceDate); if (null !== $service->ageFrom && $age < $service->ageFrom) { return false; @@ -105,20 +113,21 @@ class ServiceAgeEvaluator } /** - * Calculates age in years from a date of birth. + * Calculates age in years from a date of birth at a reference date. * * Uses DateTimeImmutable to ensure accurate age calculations accounting * for leap years and exact birth date anniversaries. * - * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * @param \DateTimeImmutable|null $referenceDate The reference date for age calculation (defaults to today) * * @return int The calculated age in complete years */ - private function calculateAge(\DateTimeImmutable $dateOfBirth): int + private function calculateAge(\DateTimeImmutable $dateOfBirth, ?\DateTimeImmutable $referenceDate = null): int { - $today = new \DateTimeImmutable(); + $referenceDate = $referenceDate ?? new \DateTimeImmutable(); - return $dateOfBirth->diff($today)->y; + return $dateOfBirth->diff($referenceDate)->y; } /** diff --git a/templates/booking/create_step_2.html.twig b/templates/booking/create_step_2.html.twig index 7b4891e..bdb1c15 100644 --- a/templates/booking/create_step_2.html.twig +++ b/templates/booking/create_step_2.html.twig @@ -95,7 +95,9 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Skipass nicht wählbar +
{% endif %} {% if participant.courses is defined %} {{ form_row(participant.courses, { @@ -106,7 +108,9 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Kurse nicht wählbar +
{% endif %} {% if participant.additionalServices is defined %} {{ form_row(participant.additionalServices, { @@ -117,7 +121,9 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Zusatzleistungen nicht wählbar +
{% endif %} {% if participant.rentals is defined %} {{ form_row(participant.rentals, { @@ -128,18 +134,27 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Leihmaterial nicht wählbar +
{% endif %} {% if participant.rentalInsurance is defined %} - {{ form_row(participant.rentalInsurance, { - 'attr': { - 'hx-trigger': 'change', - 'hx-post': path('app_booking_create_step_2_refresh'), - 'hx-swap': 'none' - } - }) }} +
+ + Leihmaterial-Versicherung + + {{ form_row(participant.rentalInsurance, { + 'attr': { + 'hx-trigger': 'change', + 'hx-post': path('app_booking_create_step_2_refresh'), + 'hx-swap': 'none' + } + }) }} +
{% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Leihmat.-Versicherung nicht wählbar +
{% endif %} {% if participant.board is defined %} {{ form_row(participant.board, { @@ -150,7 +165,9 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Verpflegung nicht wählbar +
{% endif %} {% if participant.insurance is defined %} {{ form_row(participant.insurance, { @@ -161,7 +178,9 @@ } }) }} {% else %} -
{# Empty div to maintain grid layout when not available #} +
+ Versicherung nicht wählbar +
{% endif %}