Files
myep/tests/Service/ApplicantInsuranceCascadeTest.php
T

251 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\ApplicantInsuranceCascade;
use App\Service\BookingPriceCalculator;
use App\Service\InsuranceManager;
use PHPUnit\Framework\TestCase;
class ApplicantInsuranceCascadeTest extends TestCase
{
/**
* Regression for the production report: an adult and two children each picked their own
* insurance, then the applicant switched to the family insurance that had become available.
* The dependents' own selections used to survive in the session DTO until submit, which is
* what left the sidebar summary listing two superseded policies.
*/
public function testFamilyInsuranceClearsDependentsOwnSelections(): void
{
$familyInsurance = $this->createInsurance('family-1', true);
$childInsuranceA = $this->createInsurance('child-a', false);
$childInsuranceB = $this->createInsurance('child-b', false);
$bookingDto = $this->createBooking([
$this->createParticipant(0, $familyInsurance),
$this->createParticipant(1, $childInsuranceA),
$this->createParticipant(2, $childInsuranceB),
]);
$cascade = $this->createCascade([
0 => $familyInsurance,
1 => null,
2 => null,
], [$familyInsurance, $childInsuranceA, $childInsuranceB]);
$cascade->apply($bookingDto);
$this->assertSame($familyInsurance, $bookingDto->participants[0]->insurance);
$this->assertNull($bookingDto->participants[1]->insurance, 'First child must lose their own policy');
$this->assertNull($bookingDto->participants[2]->insurance, 'Second child must lose their own policy');
}
/**
* The cascade runs on every request, so applying it repeatedly must converge rather than
* drift - otherwise a simple page refresh could change the booking.
*/
public function testApplyingTwiceIsIdempotent(): void
{
$familyInsurance = $this->createInsurance('family-1', true);
$childInsurance = $this->createInsurance('child-a', false);
$bookingDto = $this->createBooking([
$this->createParticipant(0, $familyInsurance),
$this->createParticipant(1, $childInsurance),
]);
$cascade = $this->createCascade([
0 => $familyInsurance,
1 => null,
], [$familyInsurance, $childInsurance]);
$cascade->apply($bookingDto);
$cascade->apply($bookingDto);
$this->assertSame($familyInsurance, $bookingDto->participants[0]->insurance);
$this->assertNull($bookingDto->participants[1]->insurance);
}
/**
* Family insurance propagates on its own merit - the bulk checkbox is irrelevant to it.
*/
public function testFamilyInsurancePropagatesWithoutBulkCheckbox(): void
{
$familyInsurance = $this->createInsurance('family-1', true);
$childInsurance = $this->createInsurance('child-a', false);
$applicant = $this->createParticipant(0, $familyInsurance);
$applicant->bulkInsuranceBooking = false;
$bookingDto = $this->createBooking([$applicant, $this->createParticipant(1, $childInsurance)]);
$cascade = $this->createCascade([0 => $familyInsurance, 1 => null], [$familyInsurance, $childInsurance]);
$cascade->apply($bookingDto);
$this->assertNull($bookingDto->participants[1]->insurance);
}
/**
* Bulk booking assigns the applicant's insurance type to dependents, re-tiered per
* participant by InsuranceManager.
*/
public function testBulkInsuranceAssignsTierToDependents(): void
{
$applicantInsurance = $this->createInsurance('tier-high', false);
$dependentTier = $this->createInsurance('tier-low', false);
$applicant = $this->createParticipant(0, $applicantInsurance);
$applicant->bulkInsuranceBooking = true;
$dependent = $this->createParticipant(1, null);
$bookingDto = $this->createBooking([$applicant, $dependent]);
$cascade = $this->createCascade([
0 => $applicantInsurance,
1 => $dependentTier,
], [$applicantInsurance, $dependentTier]);
$cascade->apply($bookingDto);
$this->assertSame($applicantInsurance, $bookingDto->participants[0]->insurance, 'Applicant keeps their own choice');
$this->assertSame($dependentTier, $bookingDto->participants[1]->insurance);
}
/**
* Neither bulk nor family: every participant keeps whatever they chose individually.
*/
public function testNoPropagationWithoutBulkOrFamily(): void
{
$applicantInsurance = $this->createInsurance('own-a', false);
$dependentInsurance = $this->createInsurance('own-b', false);
$bookingDto = $this->createBooking([
$this->createParticipant(0, $applicantInsurance),
$this->createParticipant(1, $dependentInsurance),
]);
$insuranceService = $this->createMock(InsuranceManager::class);
$insuranceService->expects($this->never())->method('batchAssignInsuranceToParticipants');
$cascade = new ApplicantInsuranceCascade(
$insuranceService,
$this->createMock(BookingPriceCalculator::class)
);
$cascade->apply($bookingDto);
$this->assertSame($applicantInsurance, $bookingDto->participants[0]->insurance);
$this->assertSame($dependentInsurance, $bookingDto->participants[1]->insurance);
}
/**
* In edit mode a dependent's individually selected, locked-in insurance must survive a
* non-family bulk assignment.
*/
public function testEditModeKeepsIndividuallySelectedInsuranceForNonFamilyBulk(): void
{
$applicantInsurance = $this->createInsurance('tier-high', false);
$dependentOwn = $this->createInsurance('dependent-own', false);
$wouldBeAssigned = $this->createInsurance('tier-low', false);
$applicant = $this->createParticipant(0, $applicantInsurance);
$applicant->bulkInsuranceBooking = true;
$bookingDto = $this->createBooking([$applicant, $this->createParticipant(1, $dependentOwn)]);
$bookingDto->booking = new Booking(); // BookingDto derives MODE_EDIT from a present booking
$cascade = $this->createCascade([
0 => $applicantInsurance,
1 => $wouldBeAssigned,
], [$applicantInsurance, $dependentOwn, $wouldBeAssigned]);
$cascade->apply($bookingDto);
$this->assertSame($dependentOwn, $bookingDto->participants[1]->insurance, 'Locked individual selection must survive');
}
/**
* Family insurance is exempt from the edit-mode protection: a dependent can never
* legitimately hold their own policy alongside it.
*/
public function testEditModeFamilyInsuranceStillClearsDependents(): void
{
$familyInsurance = $this->createInsurance('family-1', true);
$dependentOwn = $this->createInsurance('dependent-own', false);
$bookingDto = $this->createBooking([
$this->createParticipant(0, $familyInsurance),
$this->createParticipant(1, $dependentOwn),
]);
$bookingDto->booking = new Booking(); // BookingDto derives MODE_EDIT from a present booking
$cascade = $this->createCascade([0 => $familyInsurance, 1 => null], [$familyInsurance, $dependentOwn]);
$cascade->apply($bookingDto);
$this->assertNull($bookingDto->participants[1]->insurance);
}
// Helpers
/**
* @param array<int, Insurance|null> $assignments
* @param array<Insurance> $selectable
*/
private function createCascade(array $assignments, array $selectable): ApplicantInsuranceCascade
{
$insuranceService = $this->createMock(InsuranceManager::class);
$insuranceService->method('getSelectableInsurances')->willReturn($selectable);
$insuranceService->method('batchAssignInsuranceToParticipants')->willReturn($assignments);
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
$priceCalculator->method('calculateIndividualParticipantPriceExcludingInsurance')->willReturn(0.0);
return new ApplicantInsuranceCascade($insuranceService, $priceCalculator);
}
private function createInsurance(string $id, bool $familyInsurance): Insurance
{
$insurance = new Insurance();
$insurance->id = $id;
$insurance->label = 'Insurance '.$id;
$insurance->price = 50.0;
$insurance->subType = 'RRV';
$insurance->package = false;
$insurance->complementary = false;
$insurance->familyInsurance = $familyInsurance;
return $insurance;
}
private function createParticipant(int $index, ?Insurance $insurance): ParticipantDto
{
$participant = new ParticipantDto();
$participant->index = $index;
$participant->insurance = $insurance;
return $participant;
}
/** @param array<ParticipantDto> $participants */
private function createBooking(array $participants): BookingDto
{
$travel = new Travel();
$travel->id = 1;
$travel->dateFrom = new \DateTimeImmutable('2026-02-01');
$travel->dateTo = new \DateTimeImmutable('2026-02-08');
$bookingDto = new BookingDto($travel, count($participants));
$bookingDto->participants = $participants;
return $bookingDto;
}
}