fix: incorrect rules around family insurances
addresses #869dr80qj
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Service\Condition;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\Condition\FamilyInsuranceActiveCondition;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class FamilyInsuranceActiveConditionTest extends TestCase
|
||||
{
|
||||
private FamilyInsuranceActiveCondition $condition;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->condition = new FamilyInsuranceActiveCondition();
|
||||
}
|
||||
|
||||
public function testAlwaysFalseForApplicant(): void
|
||||
{
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->insurance = $this->createFamilyInsurance();
|
||||
|
||||
$bookingDto = new BookingDto(new Travel(), 1);
|
||||
$bookingDto->participants = [$applicant];
|
||||
|
||||
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||
|
||||
$this->assertFalse($result, 'Applicant is never affected by their own family insurance selection');
|
||||
}
|
||||
|
||||
public function testTrueForDependentWhenApplicantHasFamilyInsurance(): void
|
||||
{
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->insurance = $this->createFamilyInsurance();
|
||||
|
||||
$dependent = new ParticipantDto();
|
||||
$dependent->index = 1;
|
||||
|
||||
$bookingDto = new BookingDto(new Travel(), 1);
|
||||
$bookingDto->participants = [$applicant, $dependent];
|
||||
|
||||
$result = $this->condition->evaluate($bookingDto, 1, []);
|
||||
|
||||
$this->assertTrue($result, 'Dependent insurance field must be hidden when applicant has family insurance');
|
||||
}
|
||||
|
||||
public function testFalseForDependentWhenApplicantHasNonFamilyInsurance(): void
|
||||
{
|
||||
$nonFamilyInsurance = new Insurance();
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
$applicant->insurance = $nonFamilyInsurance;
|
||||
|
||||
$dependent = new ParticipantDto();
|
||||
$dependent->index = 1;
|
||||
|
||||
$bookingDto = new BookingDto(new Travel(), 1);
|
||||
$bookingDto->participants = [$applicant, $dependent];
|
||||
|
||||
$result = $this->condition->evaluate($bookingDto, 1, []);
|
||||
|
||||
$this->assertFalse($result, 'Dependent should keep their own insurance choice for non-family insurance');
|
||||
}
|
||||
|
||||
public function testFalseForDependentWhenApplicantHasNoInsurance(): void
|
||||
{
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
|
||||
$dependent = new ParticipantDto();
|
||||
$dependent->index = 1;
|
||||
|
||||
$bookingDto = new BookingDto(new Travel(), 1);
|
||||
$bookingDto->participants = [$applicant, $dependent];
|
||||
|
||||
$result = $this->condition->evaluate($bookingDto, 1, []);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testGetDependentFieldsReturnsInsurance(): void
|
||||
{
|
||||
$result = $this->condition->getDependentFields();
|
||||
|
||||
$this->assertSame(['insurance'], $result);
|
||||
}
|
||||
|
||||
public function testGetDescriptionReturnsString(): void
|
||||
{
|
||||
$result = $this->condition->getDescription();
|
||||
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringContainsString('family insurance', $result);
|
||||
}
|
||||
|
||||
private function createFamilyInsurance(): Insurance
|
||||
{
|
||||
$insurance = new Insurance();
|
||||
$insurance->familyInsurance = true;
|
||||
|
||||
return $insurance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\CreateFieldStateProvider;
|
||||
use App\Service\ParticipantEligibilityChecker;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration-level coverage for cross-participant field-state re-evaluation: proves the
|
||||
* dependent's 'insurance' field state actually flips when the applicant's insurance
|
||||
* selection changes, wiring FamilyInsuranceActiveCondition through the real
|
||||
* CreateFieldStateProvider composition rather than testing the condition in isolation.
|
||||
*/
|
||||
class CreateFieldStateProviderTest extends TestCase
|
||||
{
|
||||
private CreateFieldStateProvider $provider;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->provider = new CreateFieldStateProvider(new ParticipantEligibilityChecker());
|
||||
}
|
||||
|
||||
public function testDependentInsuranceFieldHidesWhenApplicantSelectsFamilyInsuranceThenReappearsAfterSwitch(): void
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('+30 days');
|
||||
$travel->dateTo = new \DateTimeImmutable('+37 days');
|
||||
|
||||
$applicant = new ParticipantDto();
|
||||
$applicant->index = 0;
|
||||
// Baby age (0-2 years at travel date) is always eligible regardless of skipass availability
|
||||
$applicant->dateOfBirth = $travel->dateFrom->modify('-1 year');
|
||||
|
||||
$dependent = new ParticipantDto();
|
||||
$dependent->index = 1;
|
||||
$dependent->dateOfBirth = $travel->dateFrom->modify('-1 year');
|
||||
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->participants = [$applicant, $dependent];
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->shouldIncludeField('insurance', $bookingDto, 1),
|
||||
'Dependent should see their own insurance field before the applicant selects family insurance'
|
||||
);
|
||||
|
||||
$familyInsurance = new Insurance();
|
||||
$familyInsurance->familyInsurance = true;
|
||||
$applicant->insurance = $familyInsurance;
|
||||
|
||||
$this->assertFalse(
|
||||
$this->provider->shouldIncludeField('insurance', $bookingDto, 1),
|
||||
'Dependent insurance field must be hidden once the applicant selects family insurance'
|
||||
);
|
||||
|
||||
$nonFamilyInsurance = new Insurance();
|
||||
$nonFamilyInsurance->familyInsurance = false;
|
||||
$applicant->insurance = $nonFamilyInsurance;
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->shouldIncludeField('insurance', $bookingDto, 1),
|
||||
'Dependent insurance field must reappear once the applicant switches away from family insurance'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\ParticipantFieldOptionsProvider;
|
||||
use App\Service\BookingPriceCalculator;
|
||||
use App\Service\InsuranceManager;
|
||||
use App\Service\ServiceAvailabilityCalculator;
|
||||
use App\Service\ServiceLabelFormatter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Regression tests for the family-insurance choices-list bug: the applicant selects a
|
||||
* family insurance, it's correctly reassigned to the total-price tier on submit, but
|
||||
* the re-rendered form showed no selection because the choices list was still filtered
|
||||
* by the applicant's individual price instead of the total booking price.
|
||||
*/
|
||||
class ParticipantFieldOptionsProviderInsuranceTest extends TestCase
|
||||
{
|
||||
private ParticipantFieldOptionsProvider $provider;
|
||||
private BookingPriceCalculator $priceCalculatorService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
|
||||
// InsuranceManager is stateless - use the real implementation so actual
|
||||
// eligibility/price-tier filtering runs, not a stubbed-out mock.
|
||||
$insuranceService = new InsuranceManager();
|
||||
$this->priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
|
||||
$serviceLabelFormatter = new ServiceLabelFormatter();
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturnCallback(fn (string $message) => $message);
|
||||
|
||||
$this->provider = new ParticipantFieldOptionsProvider(
|
||||
$serviceAvailabilityCalculator,
|
||||
$insuranceService,
|
||||
$this->priceCalculatorService,
|
||||
$serviceLabelFormatter,
|
||||
$translator
|
||||
);
|
||||
}
|
||||
|
||||
public function testFamilyInsuranceCorrectTierAppearsInApplicantChoicesUsingTotalPrice(): void
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
|
||||
// Low tier only matches the applicant's individual price (300), not the total (450)
|
||||
$lowTierFamilyInsurance = new Insurance();
|
||||
$lowTierFamilyInsurance->id = '1';
|
||||
$lowTierFamilyInsurance->label = 'Reise-Rücktritt Familie';
|
||||
$lowTierFamilyInsurance->familyInsurance = true;
|
||||
$lowTierFamilyInsurance->travelPriceFrom = 0.0;
|
||||
$lowTierFamilyInsurance->travelPriceTo = 400.0;
|
||||
|
||||
// Correct tier matches the total booking price (450), not the applicant's individual price (300)
|
||||
$correctTierFamilyInsurance = new Insurance();
|
||||
$correctTierFamilyInsurance->id = '2';
|
||||
$correctTierFamilyInsurance->label = 'Reise-Rücktritt Familie';
|
||||
$correctTierFamilyInsurance->familyInsurance = true;
|
||||
$correctTierFamilyInsurance->travelPriceFrom = 400.01;
|
||||
$correctTierFamilyInsurance->travelPriceTo = 600.0;
|
||||
|
||||
$travel->insurances = [$lowTierFamilyInsurance, $correctTierFamilyInsurance];
|
||||
|
||||
$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('calculateIndividualParticipantPriceExcludingInsurance')
|
||||
->willReturn(300.0);
|
||||
$this->priceCalculatorService
|
||||
->method('calculateTotalBookingPriceExcludingInsurance')
|
||||
->willReturn(450.0);
|
||||
|
||||
$options = $this->provider->getFieldOptions('insurance', $bookingDto, 0);
|
||||
|
||||
$choiceIds = array_map(fn (Insurance $insurance) => $insurance->id, $options['choices']);
|
||||
|
||||
$this->assertContains('2', $choiceIds, 'Family insurance tier matching the total booking price must be selectable');
|
||||
$this->assertNotContains('1', $choiceIds, 'Family insurance tier that only matches the individual price must not be selectable');
|
||||
}
|
||||
|
||||
public function testFamilyInsuranceNeverAppearsInDependentChoices(): void
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-08-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-08-08');
|
||||
|
||||
$familyInsurance = new Insurance();
|
||||
$familyInsurance->id = '1';
|
||||
$familyInsurance->label = 'Reise-Rücktritt Familie';
|
||||
$familyInsurance->familyInsurance = true;
|
||||
|
||||
$travel->insurances = [$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('calculateIndividualParticipantPriceExcludingInsurance')
|
||||
->willReturn(0.0);
|
||||
$this->priceCalculatorService
|
||||
->method('calculateTotalBookingPriceExcludingInsurance')
|
||||
->willReturn(450.0);
|
||||
|
||||
$options = $this->provider->getFieldOptions('insurance', $bookingDto, 1);
|
||||
|
||||
$choiceIds = array_map(fn (Insurance $insurance) => $insurance->id, $options['choices']);
|
||||
|
||||
$this->assertNotContains('1', $choiceIds, 'Family insurance must never be selectable for a non-applicant participant');
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
||||
// Mock price calculator to return a default price
|
||||
$this->priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')
|
||||
->willReturn(500.0);
|
||||
$this->priceCalculatorService->method('resolveInsuranceTravelPrice')
|
||||
->willReturn(500.0);
|
||||
|
||||
$this->handler = new ParticipantInsuranceFieldHandler($this->insuranceService, $this->priceCalculatorService);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user