feat: indicate family insurance eligibility

This commit is contained in:
Björn Fromme committed 2026-08-05 15:11:58 +02:00
1 parent 0b642dcfe4
commit 2cfa87f44b
12 files changed
+327 -234

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ class BookingCreateContext
public readonly ?array $cardsData = null,
public readonly bool $isSubmitted = false,
public readonly ?array $participantPrices = null,
public readonly bool $familyInsuranceUpgradeAvailable = false,
) {
}
}
+4 -10
View File
@@ -70,18 +70,12 @@ class BookingDto
*/
public ?string $originalFingerprint = null;
/**
* Whether the applicant has already been notified that family insurance became
* available after they picked a non-family insurance while dependents were incomplete.
* Prevents re-showing the same hint on every subsequent participant card submission.
*/
public bool $familyInsuranceHintShown = false;
/**
* Whether the applicant's current insurance choice was made while family insurance
* was not yet eligible (i.e. before all dependents' dates of birth were known).
* Set at selection time; used to distinguish "family insurance just became
* available" from "it was available all along and the applicant chose otherwise".
* was not yet eligible (i.e. before the dependents' dates of birth were known).
* Set at selection time; used by FamilyInsuranceAvailabilityChecker to distinguish
* "the applicant never had the chance" from "it was available all along and the
* applicant chose otherwise".
*/
public bool $applicantInsuranceChosenWhileFamilyIneligible = false;
@@ -6,9 +6,9 @@ namespace App\Form\Service;
use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
use App\Service\BookingPriceCalculator;
use App\Service\FamilyInsuranceAvailabilityChecker;
use App\Service\InsuranceManager;
/**
@@ -38,6 +38,7 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
public function __construct(
private readonly InsuranceManager $insuranceService,
private readonly BookingPriceCalculator $priceCalculatorService,
private readonly FamilyInsuranceAvailabilityChecker $familyInsuranceService,
) {
}
@@ -125,19 +126,6 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
* @param int $participantIndex The index of the participant being processed
*/
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
{
$this->applyInsuranceSelection($submittedData, $bookingDto, $participantIndex);
// Runs after the selection above is fully resolved, so a submission that itself
// switches the applicant to family insurance doesn't trigger a stale "please
// recheck" notification about the choice it just made.
$this->notifyApplicantIfFamilyInsuranceNewlyAvailable($bookingDto);
}
/**
* @param array<string, mixed> $submittedData The submitted participant form data
*/
private function applyInsuranceSelection(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
{
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
@@ -283,8 +271,9 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
/**
* Records whether the applicant's insurance choice was made while family insurance
* was NOT yet eligible, so the notification below can tell "just became available"
* apart from "was available all along and the applicant chose non-family anyway".
* was NOT yet eligible, so the standing hint rendered in step 2 can tell "never had
* the chance" apart from "was available all along and the applicant chose non-family
* anyway".
*
* Only called when the applicant (index 0) makes a genuine new selection - not on
* mere resubmission/price-tier reassignment of an already-standing choice.
@@ -295,79 +284,11 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
return;
}
$applicant = $bookingDto->getParticipant(0);
if (null === $applicant) {
if (null === $bookingDto->getParticipant(0)) {
return;
}
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible = !$this->hasEligibleFamilyInsurance($bookingDto, $applicant);
}
/**
* Checks whether a family insurance is actually eligible right now - not just whether
* the participant composition qualifies as a family booking, but whether a selectable
* family insurance product actually matches the current total booking price.
*
* Used by both the selection-time recording above and the notification below, so they
* can never disagree on what "family insurance is eligible" means.
*/
private function hasEligibleFamilyInsurance(BookingDto $bookingDto, ParticipantDto $applicant): bool
{
if (!$bookingDto->isFamilyBooking()) {
return false;
}
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
$familyInsurances = array_values(array_filter($selectableInsurances, static fn (Insurance $i) => true === $i->familyInsurance));
if ([] === $familyInsurances) {
return false;
}
$totalPrice = $this->priceCalculatorService->calculateTotalBookingPriceExcludingInsurance($bookingDto);
return [] !== $this->insuranceService->getEligibleInsurances($familyInsurances, $applicant, $bookingDto, $totalPrice);
}
/**
* Notifies the applicant when family insurance becomes available after the fact.
*
* Participants are entered one card at a time, in any order, so the applicant is
* likely to pick a non-family insurance before any dependent's date of birth makes
* the booking eligible for family insurance. Once that happens, nothing prompts the
* applicant to revisit their earlier choice. This surfaces a one-time hint via the
* existing notification/toast pipeline (App\Form\Model\ParticipantDto::addNotification()),
* mirroring the cross-participant side effect already used by
* ParticipantAssignedRoomFieldHandler::resolveRoomCapacityConflict() for room conflicts.
*
* Requires `applicantInsuranceChosenWhileFamilyIneligible` to be true - i.e. the
* choice was actually made before family insurance was possible - not just that
* family insurance happens to be eligible now (it may have been all along).
*/
private function notifyApplicantIfFamilyInsuranceNewlyAvailable(BookingDto $bookingDto): void
{
if ($bookingDto->familyInsuranceHintShown) {
return;
}
if (!$bookingDto->applicantInsuranceChosenWhileFamilyIneligible) {
return;
}
$applicant = $bookingDto->getParticipant(0);
if (null === $applicant || null === $applicant->insurance || true === $applicant->insurance->familyInsurance) {
return;
}
if (!$this->hasEligibleFamilyInsurance($bookingDto, $applicant)) {
return;
}
$applicant->addNotification(
'info',
'Für eure Konstellation ist auch eine Familienversicherung verfügbar und kann über die anmeldende Person gebucht werden.'
);
$bookingDto->familyInsuranceHintShown = true;
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible = !$this->familyInsuranceService->hasEligibleFamilyInsurance($bookingDto);
}
/**