Files
myep/src/Form/Service/ParticipantInsuranceFieldHandler.php
T

347 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingDto;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
use App\Service\BookingPriceCalculator;
use App\Service\InsuranceManager;
/**
* Handles processing of the insurance field for booking participants.
*
* This handler manages insurance selections for individual participants in the booking
* creation process. It processes insurance field from form submissions, validates
* selections against participant eligibility criteria, and provides automatic
* reassignment when participant pricing changes.
*
* Key Features:
* - Validates submitted insurance selections against eligibility criteria
* - Automatically reassigns insurances when travel price changes make current selection invalid
* - Preserves user intent by maintaining same insurance type (subType + familyInsurance)
* - Uses InsuranceMatchingService for eligibility filtering and reassignment logic
*
* Auto-Reassignment Logic:
* When a participant's individual travel price changes (e.g., by adding services),
* their current insurance may no longer be eligible for the new price range.
* Instead of clearing the selection, this handler automatically reassigns
* to the same insurance type with the appropriate price tier.
*
* Dependencies: dateOfBirth (for age evaluation and insurance eligibility)
*/
class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
{
public function __construct(
private readonly InsuranceManager $insuranceService,
private readonly BookingPriceCalculator $priceCalculatorService,
) {
}
/**
* Returns the form field name this handler processes.
*
* @return string The field name 'insurance'
*/
public function getFieldName(): string
{
return 'insurance';
}
/**
* Returns the field dependencies for proper processing order.
*
* 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 of field dependencies
*/
public function getDependencies(): array
{
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
'pickup', // Affects travel price
'parking', // Affects travel price
];
}
/**
* Determines if this handler should process the field based on submitted data.
*
* Insurance handler should NOT procefss in edit mode because the BPN API does not
* return insurance data. In edit mode, insurance data must be preserved as-is
* and passed through to the update endpoint unchanged.
*
* In create mode, we always need to process to handle cases where the selection
* is cleared (field not present in data). 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 string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
* @param int $participantIndex The index of the participant being processed
*
* @return bool True if processing should occur, false otherwise
*/
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
{
// In edit mode, insurance data is not available from API - skip processing entirely
if (BookingDto::MODE_EDIT === $mode) {
return false;
}
// Skip processing for dependent participants when bulk insurance booking is active
if ($participantIndex > 0 && $this->isBulkInsuranceBookingActive($submittedData)) {
return false;
}
return true; // Always process in create mode to handle deselection cases
}
/**
* 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 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<string, mixed> $submittedData The submitted participant form data
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
* @param int $participantIndex The index of the participant being processed
*/
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
{
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return;
}
$selectedInsuranceId = $this->getFieldValue($submittedData, $this->getFieldName());
$currentInsurance = $participant->insurance;
// Get selectable (non-complementary) insurances with caching
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
// Calculate travel price for eligibility checks
$travelPrice = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participantIndex);
// 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) {
// Handle synthetic "no insurance" option
if (Insurance::NO_INSURANCE_ID === $selectedInsuranceId) {
// Create synthetic insurance object to satisfy validation
// This will be excluded from BPN XML transmission
$noInsurance = new Insurance();
$noInsurance->id = Insurance::NO_INSURANCE_ID;
$noInsurance->label = 'keine Versicherung gewünscht';
$noInsurance->price = 0.0;
$participant->insurance = $noInsurance;
return; // Skip all other processing for "no insurance"
}
$selectedInsurance = $this->findInsuranceById($selectableInsurances, $selectedInsuranceId);
if (null !== $selectedInsurance) {
$eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice);
$isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances);
if ($isSelectedInsuranceEligible) {
// New selection is eligible - use it
$participant->insurance = $selectedInsurance;
} else {
// New selection is not eligible - try to find alternative in same type
$reassignedInsurance = $this->insuranceService->reassignInsuranceForPriceChange(
$selectableInsurances,
$selectedInsurance,
$participant,
$bookingDto,
$travelPrice
);
$participant->insurance = $reassignedInsurance;
if (null !== $reassignedInsurance) {
$participant->addNotification(
'info',
sprintf('Versicherung automatisch angepasst: %s', $reassignedInsurance->label)
);
}
}
} else {
// Insurance not found - clear selection
$participant->insurance = null;
}
}
// Handle form resubmission with existing insurance (automatic reassignment check)
if (null !== $currentInsurance && null !== $selectedInsuranceId) {
// Preserve "no insurance" selection during resubmission
if ($currentInsurance->isNoInsurance()) {
return;
}
// Check if current insurance is still eligible with updated participant data
$eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice);
$isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances);
if (!$isCurrentInsuranceStillEligible) {
// Current insurance no longer eligible - try to reassign to same type with new price tier
$reassignedInsurance = $this->insuranceService->reassignInsuranceForPriceChange(
$selectableInsurances,
$currentInsurance,
$participant,
$bookingDto,
$travelPrice
);
if (null !== $reassignedInsurance && $reassignedInsurance->id !== $currentInsurance->id) {
$participant->addNotification(
'info',
sprintf('Versicherung automatisch angepasst an neuen Preis: %s', $reassignedInsurance->label)
);
}
$participant->insurance = $reassignedInsurance; // null if no suitable match found
}
return;
}
// Handle explicit deselection (user removed insurance)
if (null === $selectedInsuranceId) {
$participant->insurance = null;
}
}
/**
* Returns field state modifications that should be applied after processing.
*
* Currently no field state modifications are needed for insurance selection.
*
* @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingDto $bookingDto The booking DTO (potentially modified by processing)
* @param int $participantIndex The participant index being processed
*
* @return array<string, array<string, mixed>> Empty array - no field state modifications
*/
public function getFieldStateModifications(array $submittedData, BookingDto $bookingDto, int $participantIndex): array
{
return [];
}
/**
* Returns field names whose state is affected by this handler's processing.
*
* Currently no other fields are affected by insurance selection.
*
* @return string[] Empty array - no other fields are affected
*/
public function getAffectedFieldNames(): array
{
return [];
}
/**
* Finds an insurance by ID from the available insurances array.
*
* @param array<Insurance> $insurances Array of available insurances
* @param string $insuranceId The insurance ID to find
*
* @return Insurance|null The found insurance or null if not found
*/
private function findInsuranceById(array $insurances, string $insuranceId): ?Insurance
{
foreach ($insurances as $insurance) {
if ($insurance->id === $insuranceId) {
return $insurance;
}
}
return null;
}
/**
* Checks if a specific insurance exists in a list of insurances.
*
* @param Insurance $targetInsurance The insurance to find
* @param array<Insurance> $insuranceList The list to search in
*
* @return bool True if the insurance is found in the list
*/
private function isInsuranceInList(Insurance $targetInsurance, array $insuranceList): bool
{
foreach ($insuranceList as $insurance) {
if ($insurance->id === $targetInsurance->id) {
return true;
}
}
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 $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 $selectedInsuranceId, Insurance $currentInsurance): bool
{
return $currentInsurance->id === $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 === $this->normalizeCheckboxValue($bulkInsuranceBooking)) {
return false;
}
// Check if applicant has selected an insurance
$applicantInsurance = $applicantData['insurance'] ?? null;
return null !== $applicantInsurance;
}
}