feat: bulk insurance booking by applicant
This commit is contained in:
@@ -181,6 +181,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
'pickupInbound',
|
||||
'parking',
|
||||
'licensePlate',
|
||||
'bulkInsuranceBooking',
|
||||
'insurance',
|
||||
];
|
||||
|
||||
@@ -216,7 +217,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
$dynamicFields = [
|
||||
'assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'rentalInsurance',
|
||||
'skiPass', 'transportationOutbound', 'transportationInbound', 'pickupOutbound', 'pickupInbound', 'parking',
|
||||
'licensePlate', 'insurance',
|
||||
'licensePlate', 'bulkInsuranceBooking', 'insurance',
|
||||
];
|
||||
foreach ($dynamicFields as $fieldName) {
|
||||
if ($form->has($fieldName)) {
|
||||
@@ -248,6 +249,7 @@ class BookingCreateParticipantType extends AbstractType
|
||||
'pickupInbound' => ChoiceType::class,
|
||||
'parking' => CheckboxType::class,
|
||||
'licensePlate' => TextType::class,
|
||||
'bulkInsuranceBooking' => CheckboxType::class,
|
||||
'insurance' => ChoiceType::class,
|
||||
];
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ class ParticipantDto
|
||||
// Selected insurance for this participant (individual insurance selection per participant)
|
||||
public ?Insurance $insurance = null;
|
||||
|
||||
// Bulk insurance booking flag (applicant only: when checked, assigns same insurance type to all participants)
|
||||
public bool $bulkInsuranceBooking = false;
|
||||
|
||||
public static function fromPersonalData(PersonalData $personalData): static
|
||||
{
|
||||
$instance = new static();
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that evaluates whether bulk insurance booking is active for dependent participants.
|
||||
*
|
||||
* This condition determines if the applicant (first participant) has enabled bulk insurance
|
||||
* booking for all participants. When active, dependent participants (index > 0) will have
|
||||
* their insurance field replaced with a "wie Anmelder" (same as applicant) message, and the
|
||||
* applicant's insurance selection (or lack thereof) will be automatically applied to all
|
||||
* participants based on their individual travel prices and age constraints.
|
||||
*
|
||||
* The condition is satisfied when:
|
||||
* 1. Evaluating a dependent participant (not the applicant)
|
||||
* 2. The applicant has bulkInsuranceBooking flag set to true
|
||||
*/
|
||||
class BulkInsuranceBookingCondition implements FieldConditionInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates if bulk insurance booking is active for the given participant.
|
||||
*
|
||||
* For the applicant (index 0), always returns false since they control the bulk booking.
|
||||
* For dependent participants, returns true if the applicant has bulk insurance booking enabled,
|
||||
* regardless of whether an insurance is selected (applies to "no insurance" as well).
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if bulk insurance booking is active for this dependent participant
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// Applicant is never affected by bulk insurance booking (they control it)
|
||||
if (0 === $participantIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get applicant data
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
if (null === $applicant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if applicant has bulk insurance booking enabled
|
||||
// No need to check for insurance selection - bulk applies even when no insurance is selected
|
||||
return $this->getBulkInsuranceBookingValue($formData, $applicant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns field names that this condition depends on.
|
||||
*
|
||||
* This condition depends on the applicant's bulkInsuranceBooking flag.
|
||||
* Any changes to this field should trigger re-evaluation of dependent participant field states.
|
||||
*
|
||||
* @return string[] Array containing field names this condition depends on
|
||||
*/
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return ['bulkInsuranceBooking'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of this condition.
|
||||
*
|
||||
* @return string Description of the bulk insurance booking condition
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Applicant has bulk insurance booking enabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bulk insurance booking flag value from form data or participant DTO.
|
||||
*/
|
||||
private function getBulkInsuranceBookingValue(array $formData, object $applicant): bool
|
||||
{
|
||||
// First check form data (for fresh submissions)
|
||||
if (isset($formData['participants'][0]['bulkInsuranceBooking'])) {
|
||||
return (bool) $formData['participants'][0]['bulkInsuranceBooking'];
|
||||
}
|
||||
|
||||
// Fall back to participant DTO
|
||||
if (property_exists($applicant, 'bulkInsuranceBooking')) {
|
||||
return (bool) $applicant->bulkInsuranceBooking;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||
use App\Form\Service\Condition\BulkInsuranceBookingCondition;
|
||||
use App\Form\Service\Condition\CompositeCondition;
|
||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||
use App\Form\Service\Condition\FieldValueCondition;
|
||||
@@ -87,9 +88,38 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
];
|
||||
|
||||
// Hide insurance field until date of birth is provided
|
||||
// Bulk insurance booking conditions
|
||||
$bulkInsuranceBookingCondition = new BulkInsuranceBookingCondition();
|
||||
|
||||
// Show bulk insurance booking checkbox ONLY for applicant (index 0) and when insurance field is visible
|
||||
$this->fieldStateConditions['bulkInsuranceBooking'] = [
|
||||
'hidden' => CompositeCondition::or(
|
||||
CompositeCondition::not($dateOfBirthProvidedCondition), // Hide until date of birth provided
|
||||
new class() implements \App\Form\Service\Contract\FieldConditionInterface {
|
||||
public function evaluate(\App\Form\Model\BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return $participantIndex > 0; // Hide for all participants except applicant
|
||||
}
|
||||
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Participant is not the applicant';
|
||||
}
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
// Hide insurance field until date of birth is provided OR when bulk insurance booking is active for dependent participants
|
||||
$this->fieldStateConditions['insurance'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => CompositeCondition::or(
|
||||
CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
$bulkInsuranceBookingCondition
|
||||
),
|
||||
];
|
||||
|
||||
// Room-specific field conditions
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
use App\Service\InsuranceMatchingService;
|
||||
|
||||
/**
|
||||
* Handles bulk insurance booking for the applicant (first participant).
|
||||
*
|
||||
* When the applicant enables bulk insurance booking, their selected insurance type
|
||||
* (subType + familyInsurance) is automatically assigned to all participants with
|
||||
* automatic price tier adjustment based on each participant's individual travel price.
|
||||
*
|
||||
* This handler processes the bulkInsuranceBooking checkbox state and triggers
|
||||
* insurance assignment to dependent participants when enabled.
|
||||
*
|
||||
* Dependencies: insurance (applicant must have insurance selected before enabling bulk)
|
||||
*/
|
||||
class ParticipantBulkInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'bulkInsuranceBooking';
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
// Depends on insurance field to ensure insurance is selected before bulk assignment
|
||||
return ['insurance'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this handler should process the field.
|
||||
*
|
||||
* Only process for the applicant (index 0). Dependent participants don't have
|
||||
* the bulkInsuranceBooking checkbox - their insurance is controlled by the handler.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*
|
||||
* @return bool True if this is the applicant, false otherwise
|
||||
*/
|
||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||
{
|
||||
return 0 === $participantIndex; // Only process for applicant
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the bulk insurance booking checkbox for the applicant.
|
||||
*
|
||||
* When bulk insurance is enabled and applicant has insurance selected,
|
||||
* assigns the same insurance type to all participants based on their
|
||||
* individual pricing and eligibility criteria.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update
|
||||
* @param int $participantIndex The index of the participant (must be 0)
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant || !$bookingDto instanceof BookingCreateDto) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get checkbox value from submitted data
|
||||
$bulkInsuranceBooking = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
$isBulkEnabled = (bool) $bulkInsuranceBooking;
|
||||
|
||||
// Store checkbox state
|
||||
$participant->bulkInsuranceBooking = $isBulkEnabled;
|
||||
|
||||
// If bulk insurance is enabled and applicant has insurance, assign to all participants
|
||||
if (true === $isBulkEnabled && null !== $participant->insurance) {
|
||||
$this->applyBulkInsuranceToAllParticipants($bookingDto, $participant->insurance);
|
||||
}
|
||||
|
||||
// If bulk insurance is disabled, clear dependent participants' insurances
|
||||
if (false === $isBulkEnabled) {
|
||||
$this->clearDependentParticipantsInsurance($bookingDto);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the applicant's insurance type to all participants with automatic price tier adjustment.
|
||||
*
|
||||
* Uses InsuranceMatchingService to find the appropriate price tier for each participant
|
||||
* based on their individual travel price and eligibility criteria.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking DTO with all participants
|
||||
* @param object $applicantInsurance The insurance selected by the applicant
|
||||
*/
|
||||
private function applyBulkInsuranceToAllParticipants(BookingCreateDto $bookingDto, object $applicantInsurance): void
|
||||
{
|
||||
$availableInsurances = $bookingDto->travel->insurances ?? [];
|
||||
|
||||
// Get insurance assignments for all participants
|
||||
$assignments = $this->insuranceMatchingService->batchAssignInsuranceToParticipants(
|
||||
$availableInsurances,
|
||||
$applicantInsurance,
|
||||
$bookingDto
|
||||
);
|
||||
|
||||
// Apply assignments to participants (skip applicant index 0, they keep their selection)
|
||||
foreach ($assignments as $index => $insurance) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
}
|
||||
|
||||
$participant = $bookingDto->getParticipant($index);
|
||||
if (null !== $participant) {
|
||||
$participant->insurance = $insurance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears insurance selections for dependent participants when bulk booking is disabled.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking DTO with all participants
|
||||
*/
|
||||
private function clearDependentParticipantsInsurance(BookingCreateDto $bookingDto): void
|
||||
{
|
||||
foreach ($bookingDto->getParticipants() as $index => $participant) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
}
|
||||
|
||||
$participant->insurance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,6 +405,17 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
// Bulk insurance booking checkbox (applicant only - controls insurance assignment for all participants)
|
||||
$this->fieldOptionProviders['bulkInsuranceBooking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Für alle Teilnehmer buchen',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'hx-post' => $this->urlGenerator->generate('app_booking_create_step_2_refresh'),
|
||||
'hx-swap' => 'none',
|
||||
'hx-trigger' => 'change',
|
||||
],
|
||||
];
|
||||
|
||||
// Insurance field provider - provides age and eligibility filtered insurances for participants
|
||||
$this->fieldOptionProviders['insurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Reiseversicherung',
|
||||
|
||||
@@ -82,13 +82,22 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
* where the selection is cleared (field not present in data). This ensures
|
||||
* the participant DTO is updated with null when no insurance is selected.
|
||||
*
|
||||
* However, when bulk insurance booking is active for a dependent participant,
|
||||
* we skip processing to prevent overwriting the insurance assigned by the
|
||||
* bulk insurance handler.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*
|
||||
* @return bool Always returns true for insurance selection fields
|
||||
* @return bool True if processing should occur, false if bulk insurance handles it
|
||||
*/
|
||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
||||
{
|
||||
// Skip processing for dependent participants when bulk insurance booking is active
|
||||
if ($participantIndex > 0 && $this->isBulkInsuranceBookingActive($submittedData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // Always process to handle deselection cases
|
||||
}
|
||||
|
||||
@@ -258,4 +267,35 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
return (string) $currentInsurance->id === (string) $selectedInsuranceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if bulk insurance booking is active for dependent participants.
|
||||
*
|
||||
* Bulk insurance is active when the applicant has enabled the bulkInsuranceBooking
|
||||
* flag and has selected an insurance.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted form data
|
||||
*
|
||||
* @return bool True if bulk insurance booking is active
|
||||
*/
|
||||
private function isBulkInsuranceBookingActive(array $submittedData): bool
|
||||
{
|
||||
// Check if applicant data exists
|
||||
if (!isset($submittedData['participants'][0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$applicantData = $submittedData['participants'][0];
|
||||
|
||||
// Check if bulk insurance booking is enabled
|
||||
$bulkInsuranceBooking = $applicantData['bulkInsuranceBooking'] ?? false;
|
||||
if (false === (bool) $bulkInsuranceBooking) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if applicant has selected an insurance
|
||||
$applicantInsurance = $applicantData['insurance'] ?? null;
|
||||
|
||||
return null !== $applicantInsurance;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user