fix: stop mutating booking state on edit-mode page views
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Tests\Controller\Booking\Edit;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Controller\Booking\Edit\ParticipantController;
|
||||
use App\Entity\User;
|
||||
@@ -16,7 +17,7 @@ use App\Form\Model\BookingSummaryDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\ParticipantEditDto;
|
||||
use App\Security\Crypt;
|
||||
use App\Service\ApplicantInsuranceCascade;
|
||||
use App\Service\BookingChangeTracker;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
@@ -134,7 +135,6 @@ class ParticipantControllerTest extends TestCase
|
||||
$bookingSessionService,
|
||||
$prepopulationService,
|
||||
$participantFormSupportService,
|
||||
$this->createMock(ApplicantInsuranceCascade::class),
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
@@ -243,7 +243,6 @@ class ParticipantControllerTest extends TestCase
|
||||
$bookingSessionService,
|
||||
$prepopulationService,
|
||||
$participantFormSupportService,
|
||||
$this->createMock(ApplicantInsuranceCascade::class),
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
@@ -260,6 +259,92 @@ class ParticipantControllerTest extends TestCase
|
||||
return new User('[email protected]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: the edit flow must not mutate the BookingDto on a page view.
|
||||
*
|
||||
* BookingChangeTracker hashes each participant's insurance into the fingerprint that
|
||||
* drives unsaved-changes detection, and the baseline is taken from un-cascaded API data
|
||||
* in BookingEditDataLoader before the user reaches this route. An earlier revision called
|
||||
* ApplicantInsuranceCascade here, which cleared dependents' BPN-loaded insurance and
|
||||
* re-tiered the applicant's. That made the booking permanently "dirty": the overview only
|
||||
* re-baselines while still clean, and the reload-from-BusPro path is gated on the same
|
||||
* flag, so BusPro-side changes stopped surfacing for the rest of the session.
|
||||
*/
|
||||
public function testRefreshDoesNotDirtyFingerprintForFamilyInsuranceBooking(): void
|
||||
{
|
||||
$request = Request::create('/bookings/42/edit/participants/0/refresh', 'POST');
|
||||
$user = $this->createUser();
|
||||
$bookingDto = $this->createFamilyInsuranceBookingDto();
|
||||
$bookingData = $this->createBooking();
|
||||
$bookingData->participantsStatus = ['F', 'F'];
|
||||
$participant = $bookingDto->participants[0];
|
||||
|
||||
$applicantInsurance = $bookingDto->participants[0]->insurance;
|
||||
$dependentInsurance = $bookingDto->participants[1]->insurance;
|
||||
|
||||
$changeTracker = new BookingChangeTracker();
|
||||
$bookingDto->originalFingerprint = $changeTracker->generateFingerprint($bookingDto);
|
||||
$this->assertFalse($changeTracker->isDirty($bookingDto), 'Baseline must start clean');
|
||||
|
||||
$form = $this->createMock(FormInterface::class);
|
||||
$form->method('handleRequest')->willReturnSelf();
|
||||
$form->method('createView')->willReturn(new FormView());
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->method('fetchBookingData')->willReturn($bookingData);
|
||||
|
||||
$bookingSessionService = $this->createMock(BookingSessionManager::class);
|
||||
$bookingSessionService->method('getBookingDto')->willReturn($bookingDto);
|
||||
|
||||
$participantFormSupportService = $this->createMock(ParticipantFormSupport::class);
|
||||
$participantFormSupportService->method('ensureParticipantExists')->willReturn($participant);
|
||||
$participantFormSupportService->method('createParticipantEditDto')
|
||||
->willReturn(new ParticipantEditDto($participant, $bookingDto));
|
||||
$participantFormSupportService->method('getParticipantFormOptions')->willReturn([
|
||||
'booking_context' => $bookingDto,
|
||||
'validation_groups' => false,
|
||||
]);
|
||||
$participantFormSupportService->method('collectAndClearNotifications')->willReturn([]);
|
||||
|
||||
$summaryData = $this->createMock(BookingSummaryDto::class);
|
||||
$contextFactory = $this->createMock(BookingEditContextFactory::class);
|
||||
$contextFactory->method('createParticipantContext')
|
||||
->willReturn(new BookingEditContext($bookingDto, $bookingData, null, $summaryData));
|
||||
|
||||
$controller = new TestableParticipantController(
|
||||
$dataLoader,
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$contextFactory,
|
||||
$this->createMock(BookingConfigurator::class),
|
||||
$bookingSessionService,
|
||||
new ParticipantDataPrefiller(
|
||||
$this->createMock(ApiClient::class),
|
||||
$this->createMock(Crypt::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
),
|
||||
$participantFormSupportService,
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
|
||||
$controller->refreshParticipantForm(42, 0, $request);
|
||||
|
||||
$this->assertFalse(
|
||||
$changeTracker->isDirty($bookingDto),
|
||||
'Viewing a participant must not mark an unchanged booking as dirty'
|
||||
);
|
||||
$this->assertSame(
|
||||
$applicantInsurance,
|
||||
$bookingDto->participants[0]->insurance,
|
||||
"Applicant's insurance must not be re-tiered on a page view"
|
||||
);
|
||||
$this->assertSame(
|
||||
$dependentInsurance,
|
||||
$bookingDto->participants[1]->insurance,
|
||||
"Dependent's BusPro-loaded insurance must survive a page view"
|
||||
);
|
||||
}
|
||||
|
||||
private function createBookingDto(): BookingDto
|
||||
{
|
||||
$travel = new Travel();
|
||||
@@ -274,6 +359,36 @@ class ParticipantControllerTest extends TestCase
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
private function createFamilyInsuranceBookingDto(): BookingDto
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
|
||||
$familyInsurance = new Insurance();
|
||||
$familyInsurance->id = 'family-1';
|
||||
$familyInsurance->label = 'Familienversicherung';
|
||||
$familyInsurance->price = 120.0;
|
||||
$familyInsurance->subType = 'RRV';
|
||||
$familyInsurance->familyInsurance = true;
|
||||
|
||||
// BusPro can map one policy to several participants via <zuordnung>, so a dependent
|
||||
// may legitimately load holding it too.
|
||||
$dependentInsurance = new Insurance();
|
||||
$dependentInsurance->id = 'dependent-own';
|
||||
$dependentInsurance->label = 'Reise-Rucktritt';
|
||||
$dependentInsurance->price = 50.0;
|
||||
$dependentInsurance->subType = 'RRV';
|
||||
$dependentInsurance->familyInsurance = false;
|
||||
|
||||
$bookingDto->participants[0]->insurance = $familyInsurance;
|
||||
|
||||
$dependent = new ParticipantDto();
|
||||
$dependent->index = 1;
|
||||
$dependent->insurance = $dependentInsurance;
|
||||
$bookingDto->participants[] = $dependent;
|
||||
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
private function createBooking(): Booking
|
||||
{
|
||||
$booking = new Booking();
|
||||
@@ -320,7 +435,6 @@ final class TestableParticipantController extends ParticipantController
|
||||
BookingSessionManager $bookingSessionService,
|
||||
ParticipantDataPrefiller $prepopulationService,
|
||||
ParticipantFormSupport $participantFormSupportService,
|
||||
ApplicantInsuranceCascade $applicantInsuranceCascade,
|
||||
private readonly User $user,
|
||||
FormInterface $form,
|
||||
) {
|
||||
@@ -334,7 +448,6 @@ final class TestableParticipantController extends ParticipantController
|
||||
$bookingSessionService,
|
||||
$prepopulationService,
|
||||
$participantFormSupportService,
|
||||
$applicantInsuranceCascade,
|
||||
new NullLogger(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user