wip: insurance booking

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent 29bdf794f3
commit e6eebc00a3
4 changed files with 76 additions and 40 deletions
@@ -91,10 +91,18 @@ class ParticipantFieldHandlerRegistry
/** /**
* Processes all participant fields from submitted form data using registered handlers. * Processes all participant fields from submitted form data using registered handlers.
* *
* This is the main entry point for field processing. It iterates through all * This is the main entry point for field processing. It processes handlers in dependency
* participants in the submitted data and applies the appropriate handlers in * order, applying each handler to ALL participants before moving to the next handler.
* dependency order. Each handler determines whether it should process the * This ensures that cross-participant logic (like family booking detection) has access
* participant's data and updates the booking DTO accordingly. * 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<string, mixed> $submittedData The submitted form data containing participants array * @param array<string, mixed> $submittedData The submitted form data containing participants array
* @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values (create or edit) * @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) // Get handlers sorted by dependency order (uses cache if available)
$sortedHandlerNames = $this->getSortedHandlers(); $sortedHandlerNames = $this->getSortedHandlers();
// Process each participant's data // Process each handler across all participants before moving to the next handler
foreach ($submittedData['participants'] as $participantIndex => $participantData) { // This ensures booking-level state (like family booking detection) is accurate
// Skip invalid participant data foreach ($sortedHandlerNames as $handlerName) {
if (false === is_array($participantData)) { $handler = $this->handlers[$handlerName];
continue;
}
// Apply each handler in dependency order // Apply this handler to all participants
foreach ($sortedHandlerNames as $handlerName) { foreach ($submittedData['participants'] as $participantIndex => $participantData) {
$handler = $this->handlers[$handlerName]; // Skip invalid participant data
if (false === is_array($participantData)) {
continue;
}
// Let each handler decide if it should process this participant's data // Let each handler decide if it should process this participant's data
if ($handler->shouldProcess($participantData, (int) $participantIndex)) { if ($handler->shouldProcess($participantData, (int) $participantIndex)) {
@@ -416,7 +416,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
), ),
'choice_label' => fn (?Insurance $insurance) => $this->formatInsuranceLabel($insurance), 'choice_label' => fn (?Insurance $insurance) => $this->formatInsuranceLabel($insurance),
'choice_value' => 'id', 'choice_value' => 'id',
'help' => 'Wählen Sie eine passende Reiseversicherung für diese Person aus.',
'attr' => [ 'attr' => [
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'), 'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
'hx-swap' => 'none', 'hx-swap' => 'none',
+21 -12
View File
@@ -35,6 +35,10 @@ class ServiceAgeEvaluator
* constraint requirements. Returns false if the participant doesn't * constraint requirements. Returns false if the participant doesn't
* meet the age requirements or if no birth date is provided. * 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 Service $service The service to evaluate
* @param BookingDtoInterface $bookingDto The booking containing participant data * @param BookingDtoInterface $bookingDto The booking containing participant data
* @param int $participantIndex The index of the participant to evaluate * @param int $participantIndex The index of the participant to evaluate
@@ -49,26 +53,30 @@ class ServiceAgeEvaluator
return false; // Cannot evaluate without birth date 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) { 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), '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), && $this->evaluateBirthYear($service, $participant->dateOfBirth),
default => true, // No constraints or unknown type 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 Service $service The service with age constraints
* @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 (travel start date)
* *
* @return bool True if the participant meets the absolute age requirements * @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) { if (null !== $service->ageFrom && $age < $service->ageFrom) {
return false; 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 * Uses DateTimeImmutable to ensure accurate age calculations accounting
* for leap years and exact birth date anniversaries. * 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 * @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;
} }
/** /**
+33 -14
View File
@@ -95,7 +95,9 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Skipass nicht wählbar
</div>
{% endif %} {% endif %}
{% if participant.courses is defined %} {% if participant.courses is defined %}
{{ form_row(participant.courses, { {{ form_row(participant.courses, {
@@ -106,7 +108,9 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Kurse nicht wählbar
</div>
{% endif %} {% endif %}
{% if participant.additionalServices is defined %} {% if participant.additionalServices is defined %}
{{ form_row(participant.additionalServices, { {{ form_row(participant.additionalServices, {
@@ -117,7 +121,9 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Zusatzleistungen nicht wählbar
</div>
{% endif %} {% endif %}
{% if participant.rentals is defined %} {% if participant.rentals is defined %}
{{ form_row(participant.rentals, { {{ form_row(participant.rentals, {
@@ -128,18 +134,27 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Leihmaterial nicht wählbar
</div>
{% endif %} {% endif %}
{% if participant.rentalInsurance is defined %} {% if participant.rentalInsurance is defined %}
{{ form_row(participant.rentalInsurance, { <fieldset>
'attr': { <legend class="font-semibold mb-1">
'hx-trigger': 'change', Leihmaterial-Versicherung
'hx-post': path('app_booking_create_step_2_refresh'), </legend>
'hx-swap': 'none' {{ form_row(participant.rentalInsurance, {
} 'attr': {
}) }} 'hx-trigger': 'change',
'hx-post': path('app_booking_create_step_2_refresh'),
'hx-swap': 'none'
}
}) }}
</fieldset>
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Leihmat.-Versicherung nicht wählbar
</div>
{% endif %} {% endif %}
{% if participant.board is defined %} {% if participant.board is defined %}
{{ form_row(participant.board, { {{ form_row(participant.board, {
@@ -150,7 +165,9 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Verpflegung nicht wählbar
</div>
{% endif %} {% endif %}
{% if participant.insurance is defined %} {% if participant.insurance is defined %}
{{ form_row(participant.insurance, { {{ form_row(participant.insurance, {
@@ -161,7 +178,9 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when not available #} <div>
Versicherung nicht wählbar
</div>
{% endif %} {% endif %}
</div> </div>