fix: reconcile assigned family insurance rate on updated participant
This commit is contained in:
@@ -728,6 +728,54 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->assertCount(1, $insuranceViolations);
|
||||
}
|
||||
|
||||
public function testDependentWithoutInsurancePassesWhenApplicantHasFamilyInsurance(): void
|
||||
{
|
||||
$applicant = $this->createAdultParticipant('[email protected]');
|
||||
$applicant->insurance = $this->createMockInsurance(familyInsurance: true);
|
||||
|
||||
$dependent = $this->createAdultParticipant('[email protected]');
|
||||
$dependent->insurance = null;
|
||||
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([$applicant, $dependent]);
|
||||
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
$violations = $this->validator->validate($wrapper, null, ['strict_required']);
|
||||
$insuranceViolations = array_filter(
|
||||
iterator_to_array($violations),
|
||||
fn ($v) => 'participant.insurance' === $v->getPropertyPath()
|
||||
);
|
||||
|
||||
$this->assertCount(0, $insuranceViolations);
|
||||
}
|
||||
|
||||
public function testDependentWithoutInsuranceFailsWhenApplicantHasNonFamilyInsurance(): void
|
||||
{
|
||||
$applicant = $this->createAdultParticipant('[email protected]');
|
||||
$applicant->insurance = $this->createMockInsurance(familyInsurance: false);
|
||||
|
||||
$dependent = $this->createAdultParticipant('[email protected]');
|
||||
$dependent->insurance = null;
|
||||
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([$applicant, $dependent]);
|
||||
|
||||
$wrapper = new ParticipantEditDto(
|
||||
participant: $bookingDto->participants[1],
|
||||
bookingContext: $bookingDto,
|
||||
);
|
||||
|
||||
$violations = $this->validator->validate($wrapper, null, ['strict_required']);
|
||||
$insuranceViolations = array_filter(
|
||||
iterator_to_array($violations),
|
||||
fn ($v) => 'participant.insurance' === $v->getPropertyPath()
|
||||
);
|
||||
|
||||
$this->assertCount(1, $insuranceViolations);
|
||||
}
|
||||
|
||||
private function createBookingDtoWithParticipants(array $participants): BookingDto
|
||||
{
|
||||
$travel = new Travel();
|
||||
@@ -821,12 +869,13 @@ class ParticipantEditDtoTest extends TestCase
|
||||
return $service;
|
||||
}
|
||||
|
||||
private function createMockInsurance(): Insurance
|
||||
private function createMockInsurance(bool $familyInsurance = false): Insurance
|
||||
{
|
||||
$insurance = new Insurance();
|
||||
$insurance->id = '1';
|
||||
$insurance->label = 'Test Insurance';
|
||||
$insurance->price = 10.0;
|
||||
$insurance->familyInsurance = $familyInsurance;
|
||||
|
||||
return $insurance;
|
||||
}
|
||||
|
||||
@@ -198,6 +198,36 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
||||
$this->assertNull($participant->insurance);
|
||||
}
|
||||
|
||||
public function testProcessFieldSwitchingToNewInsuranceIsNotOverwrittenByStaleResubmissionCheck(): void
|
||||
{
|
||||
// Regression: switching from one already-selected insurance to a different one must
|
||||
// not be re-validated (and potentially overwritten) against the OLD, now-stale
|
||||
// insurance by the "form resubmission" block afterward.
|
||||
$oldInsurance = $this->createInsurance('1');
|
||||
$newInsurance = $this->createInsurance('2');
|
||||
|
||||
$participant = new ParticipantDto();
|
||||
$participant->insurance = $oldInsurance;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->insurances = [$oldInsurance, $newInsurance];
|
||||
|
||||
$bookingDto = $this->createMockBookingDto();
|
||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||
$bookingDto->travel = $travel;
|
||||
|
||||
// Only the NEW insurance is ever eligible - the old one would fail re-validation
|
||||
// if the (buggy) resubmission block ran a second time after this new selection.
|
||||
$this->insuranceService
|
||||
->expects($this->once())
|
||||
->method('getEligibleInsurances')
|
||||
->willReturn([$newInsurance]);
|
||||
|
||||
$this->handler->processField(['insurance' => '2'], $bookingDto, 0);
|
||||
|
||||
$this->assertSame($newInsurance, $participant->insurance);
|
||||
}
|
||||
|
||||
public function testProcessFieldWorksWithStringAndIntegerIds(): void
|
||||
{
|
||||
$insurance = $this->createInsurance(123); // Integer ID
|
||||
@@ -249,6 +279,250 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
||||
$this->assertNull($participant->insurance);
|
||||
}
|
||||
|
||||
public function testProcessFieldNotifiesApplicantWhenFamilyInsuranceNewlyAvailable(): void
|
||||
{
|
||||
$bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance();
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
|
||||
$this->priceCalculatorService->method('calculateTotalBookingPriceExcludingInsurance')->willReturn(500.0);
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturn([$this->createInsurance('2', 'Reise-Rücktritt Familie')]);
|
||||
|
||||
// Dependent's own card is submitted (e.g. just their date of birth) - no insurance selection
|
||||
$this->handler->processField([], $bookingDto, 1);
|
||||
|
||||
$this->assertNotEmpty($applicant->notifications, 'Applicant should be notified that family insurance is now available');
|
||||
$this->assertTrue($bookingDto->familyInsuranceHintShown);
|
||||
}
|
||||
|
||||
public function testProcessFieldRecordsIneligibilityWhenApplicantSelectsInsuranceBeforeFamilyEligible(): void
|
||||
{
|
||||
$nonFamilyInsurance = $this->createInsurance('1');
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
$travel->insurances = [$nonFamilyInsurance];
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||
|
||||
// No dependents yet - not (and cannot be) a family booking at selection time
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->participants = [$applicant];
|
||||
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturn([$nonFamilyInsurance]);
|
||||
|
||||
$this->handler->processField(['insurance' => '1'], $bookingDto, 0);
|
||||
|
||||
$this->assertTrue(
|
||||
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible,
|
||||
'Choosing non-family insurance while ineligible must be recorded so a later hint is justified'
|
||||
);
|
||||
}
|
||||
|
||||
public function testProcessFieldDoesNotRecordIneligibilityWhenApplicantSelectsInsuranceWhileAlreadyFamilyEligible(): void
|
||||
{
|
||||
$nonFamilyInsurance = $this->createInsurance('1');
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
|
||||
$familyInsurance = $this->createInsurance('2');
|
||||
$familyInsurance->familyInsurance = true;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
$travel->insurances = [$nonFamilyInsurance, $familyInsurance];
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||
|
||||
$child = new ParticipantDto();
|
||||
$child->index = 1;
|
||||
$child->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
// Dependent's date of birth is already known, and a family insurance product is
|
||||
// actually eligible at the current price - family insurance was genuinely
|
||||
// available at the moment the applicant deliberately chose a non-family insurance.
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->participants = [$applicant, $child];
|
||||
|
||||
$this->priceCalculatorService->method('calculateTotalBookingPriceExcludingInsurance')->willReturn(500.0);
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturnCallback(
|
||||
fn (array $insurances) => $insurances
|
||||
);
|
||||
|
||||
$this->handler->processField(['insurance' => '1'], $bookingDto, 0);
|
||||
|
||||
$this->assertFalse(
|
||||
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible,
|
||||
'Must not claim family insurance "just became available" when it was already eligible at selection time'
|
||||
);
|
||||
}
|
||||
|
||||
public function testProcessFieldRecordsIneligibilityWhenFamilyCompositionExistsButNoPriceTierMatches(): void
|
||||
{
|
||||
// Regression: family composition (adult + child) alone isn't enough - a family
|
||||
// insurance product must actually match the current total price for it to count
|
||||
// as "eligible". If it doesn't, the applicant's choice was genuinely made while
|
||||
// family insurance was NOT selectable, even though isFamilyBooking() is already true.
|
||||
$nonFamilyInsurance = $this->createInsurance('1');
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
|
||||
$familyInsurance = $this->createInsurance('2');
|
||||
$familyInsurance->familyInsurance = true;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
$travel->insurances = [$nonFamilyInsurance, $familyInsurance];
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||
|
||||
$child = new ParticipantDto();
|
||||
$child->index = 1;
|
||||
$child->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->participants = [$applicant, $child];
|
||||
|
||||
$this->priceCalculatorService->method('calculateTotalBookingPriceExcludingInsurance')->willReturn(500.0);
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturnCallback(
|
||||
// No family insurance matches the current price tier, even though the
|
||||
// participant composition already qualifies as a family booking.
|
||||
fn (array $insurances) => array_values(array_filter($insurances, fn ($i) => false === $i->familyInsurance))
|
||||
);
|
||||
|
||||
$this->handler->processField(['insurance' => '1'], $bookingDto, 0);
|
||||
|
||||
$this->assertTrue(
|
||||
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible,
|
||||
'Family composition alone must not count as eligible - no family insurance actually matched the price'
|
||||
);
|
||||
}
|
||||
|
||||
public function testProcessFieldDoesNotNotifyWhenApplicantSwitchesToFamilyInsuranceInSameSubmission(): void
|
||||
{
|
||||
$familyInsurance = $this->createInsurance('2');
|
||||
$familyInsurance->familyInsurance = true;
|
||||
|
||||
$nonFamilyInsurance = $this->createInsurance('1');
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
$travel->insurances = [$nonFamilyInsurance, $familyInsurance];
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||
$applicant->insurance = $nonFamilyInsurance;
|
||||
|
||||
$child = new ParticipantDto();
|
||||
$child->index = 1;
|
||||
$child->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->participants = [$applicant, $child];
|
||||
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible = true;
|
||||
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturn([$familyInsurance]);
|
||||
|
||||
// Applicant's own submission switches them to family insurance in this same request
|
||||
$this->handler->processField(['insurance' => '2'], $bookingDto, 0);
|
||||
|
||||
$this->assertSame($familyInsurance, $applicant->insurance);
|
||||
$this->assertEmpty($applicant->notifications, 'Must not show a stale "please recheck" hint for a choice this submission already made');
|
||||
}
|
||||
|
||||
public function testProcessFieldDoesNotNotifyWhenHintAlreadyShown(): void
|
||||
{
|
||||
$bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance();
|
||||
$bookingDto->familyInsuranceHintShown = true;
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
|
||||
$this->priceCalculatorService->method('calculateTotalBookingPriceExcludingInsurance')->willReturn(500.0);
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturn([$this->createInsurance('2')]);
|
||||
|
||||
$this->handler->processField([], $bookingDto, 1);
|
||||
|
||||
$this->assertEmpty($applicant->notifications, 'Hint must not repeat once already shown');
|
||||
}
|
||||
|
||||
public function testProcessFieldDoesNotNotifyWhenApplicantHasNoInsurance(): void
|
||||
{
|
||||
$bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance();
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
$applicant->insurance = null;
|
||||
|
||||
$this->handler->processField([], $bookingDto, 1);
|
||||
|
||||
$this->assertEmpty($applicant->notifications, 'Nothing to alert about when applicant has not chosen insurance yet');
|
||||
$this->assertFalse($bookingDto->familyInsuranceHintShown);
|
||||
}
|
||||
|
||||
public function testProcessFieldDoesNotNotifyWhenApplicantAlreadyHasFamilyInsurance(): void
|
||||
{
|
||||
$bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance();
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
$familyInsurance = $this->createInsurance('2');
|
||||
$familyInsurance->familyInsurance = true;
|
||||
$applicant->insurance = $familyInsurance;
|
||||
|
||||
$this->handler->processField([], $bookingDto, 1);
|
||||
|
||||
$this->assertEmpty($applicant->notifications, 'Nothing to alert about when applicant already has family insurance');
|
||||
$this->assertFalse($bookingDto->familyInsuranceHintShown);
|
||||
}
|
||||
|
||||
public function testProcessFieldDoesNotNotifyWhenNoEligibleFamilyInsuranceExists(): void
|
||||
{
|
||||
$bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance();
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
|
||||
$this->priceCalculatorService->method('calculateTotalBookingPriceExcludingInsurance')->willReturn(500.0);
|
||||
$this->insuranceService->method('getEligibleInsurances')->willReturn([]); // No eligible family insurance
|
||||
|
||||
$this->handler->processField([], $bookingDto, 1);
|
||||
|
||||
$this->assertEmpty($applicant->notifications, 'Nothing to alert about when no family insurance is actually eligible');
|
||||
$this->assertFalse($bookingDto->familyInsuranceHintShown);
|
||||
}
|
||||
|
||||
private function createFamilyBookingDtoWithNonFamilyApplicantInsurance(): BookingDto
|
||||
{
|
||||
$nonFamilyInsurance = $this->createInsurance('1');
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
|
||||
$familyInsurance = $this->createInsurance('2');
|
||||
$familyInsurance->familyInsurance = true;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
$travel->insurances = [$nonFamilyInsurance, $familyInsurance];
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||
$applicant->insurance = $nonFamilyInsurance;
|
||||
|
||||
$child = new ParticipantDto();
|
||||
$child->index = 1;
|
||||
$child->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->participants = [$applicant, $child];
|
||||
$bookingDto->applicantInsuranceChosenWhileFamilyIneligible = true;
|
||||
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
public function testGetFieldStateModificationsReturnsEmptyArray(): void
|
||||
{
|
||||
$bookingDto = $this->createMockBookingDto();
|
||||
|
||||
Reference in New Issue
Block a user