216 lines
7.4 KiB
PHP
216 lines
7.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\Pickup;
|
|
use App\BusProNet\Model\Service;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\ParticipantDto;
|
|
use App\Form\Service\Condition\TravelStartCutoffReachedCondition;
|
|
|
|
/**
|
|
* Enforces immutable edit categories before submitting booking updates.
|
|
*
|
|
* This guard reconciles the in-session edit DTO with a fresh booking snapshot.
|
|
* If a mutability cutoff passed during an edit session, stale user changes in
|
|
* immutable categories are reverted to the current booking state so update
|
|
* payloads cannot include blocked changes.
|
|
*/
|
|
class BookingEditSubmitGuard
|
|
{
|
|
public function __construct(
|
|
private readonly BookingDataProcessor $bookingDataProcessor,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Reverts immutable category changes to fresh booking values.
|
|
*
|
|
* @return bool True when at least one immutable field was reverted
|
|
*/
|
|
public function reconcileImmutableCategories(BookingDto $workingDto, Booking $freshBooking): bool
|
|
{
|
|
$baselineDto = $this->bookingDataProcessor->createBookingDtoFromBooking($freshBooking, $workingDto->travel, $workingDto->isInternalAgencyBooking());
|
|
// Booking-level D-4 cutoff used as submit-time safety net for rentals.
|
|
$serviceCutoffReachedCondition = new TravelStartCutoffReachedCondition(
|
|
TravelStartCutoffReachedCondition::DEFAULT_DAYS_BEFORE_START
|
|
);
|
|
// TravelStartCutoffReachedCondition is booking-level (travel date only).
|
|
// The interface requires a participant index, but this condition does not use it,
|
|
// so we pass 0 as a neutral placeholder.
|
|
$rentalsLockedByCutoff = $serviceCutoffReachedCondition->evaluate($workingDto, 0, []);
|
|
|
|
$changed = false;
|
|
|
|
foreach ($workingDto->participants as $index => $participant) {
|
|
$baseline = $baselineDto->participants[$index] ?? null;
|
|
if (null === $baseline) {
|
|
continue;
|
|
}
|
|
|
|
if (false === $workingDto->travel->additionalServicesMutable) {
|
|
$changed = $this->reconcileAdditionalServices($participant, $baseline) || $changed;
|
|
}
|
|
|
|
// Reconcile rentals either when BPN additional services are immutable
|
|
// or when our D-4 rule has been reached.
|
|
if (false === $workingDto->travel->additionalServicesMutable || $rentalsLockedByCutoff) {
|
|
$changed = $this->reconcileRentals($participant, $baseline) || $changed;
|
|
}
|
|
|
|
if (false === $workingDto->travel->transportationServicesMutable) {
|
|
$changed = $this->reconcileTransportationServices($participant, $baseline) || $changed;
|
|
}
|
|
|
|
if (false === $workingDto->travel->pickupsMutable) {
|
|
$changed = $this->reconcilePickups($participant, $baseline) || $changed;
|
|
}
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
private function reconcileAdditionalServices(ParticipantDto $participant, ParticipantDto $baseline): bool
|
|
{
|
|
$changed = false;
|
|
|
|
if (false === $this->areServiceListsEqual($participant->courses, $baseline->courses)) {
|
|
$participant->courses = $baseline->courses;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->areServiceListsEqual($participant->additionalServices, $baseline->additionalServices)) {
|
|
$participant->additionalServices = $baseline->additionalServices;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->areServiceListsEqual($participant->board, $baseline->board)) {
|
|
$participant->board = $baseline->board;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->isSameService($participant->veg, $baseline->veg)) {
|
|
$participant->veg = $baseline->veg;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->isSameService($participant->skiPass, $baseline->skiPass)) {
|
|
$participant->skiPass = $baseline->skiPass;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->isSameService($participant->rentalInsurance, $baseline->rentalInsurance)) {
|
|
$participant->rentalInsurance = $baseline->rentalInsurance;
|
|
$changed = true;
|
|
}
|
|
|
|
if ($participant->rentalInsuranceSelected !== $baseline->rentalInsuranceSelected) {
|
|
$participant->rentalInsuranceSelected = $baseline->rentalInsuranceSelected;
|
|
$changed = true;
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
private function reconcileRentals(ParticipantDto $participant, ParticipantDto $baseline): bool
|
|
{
|
|
if (false === $this->areServiceListsEqual($participant->rentals, $baseline->rentals)) {
|
|
$participant->rentals = $baseline->rentals;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function reconcileTransportationServices(ParticipantDto $participant, ParticipantDto $baseline): bool
|
|
{
|
|
$changed = false;
|
|
|
|
if (false === $this->isSameService($participant->transportationOutbound, $baseline->transportationOutbound)) {
|
|
$participant->transportationOutbound = $baseline->transportationOutbound;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->isSameService($participant->transportationInbound, $baseline->transportationInbound)) {
|
|
$participant->transportationInbound = $baseline->transportationInbound;
|
|
$changed = true;
|
|
}
|
|
|
|
if ($participant->parking !== $baseline->parking) {
|
|
$participant->parking = $baseline->parking;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->isSameService($participant->parkingService, $baseline->parkingService)) {
|
|
$participant->parkingService = $baseline->parkingService;
|
|
$changed = true;
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
private function reconcilePickups(ParticipantDto $participant, ParticipantDto $baseline): bool
|
|
{
|
|
$changed = false;
|
|
|
|
if (false === $this->isSamePickup($participant->pickup, $baseline->pickup)) {
|
|
$participant->pickup = $baseline->pickup;
|
|
$changed = true;
|
|
}
|
|
|
|
if (false === $this->isSamePickup($participant->dropOff, $baseline->dropOff)) {
|
|
$participant->dropOff = $baseline->dropOff;
|
|
$changed = true;
|
|
}
|
|
|
|
if ($participant->differentDropOff !== $baseline->differentDropOff) {
|
|
$participant->differentDropOff = $baseline->differentDropOff;
|
|
$changed = true;
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
/**
|
|
* @param list<Service> $left
|
|
* @param list<Service> $right
|
|
*/
|
|
private function areServiceListsEqual(array $left, array $right): bool
|
|
{
|
|
return $this->normalizeServiceIds($left) === $this->normalizeServiceIds($right);
|
|
}
|
|
|
|
/**
|
|
* @param Service[] $services
|
|
*
|
|
* @return int[]
|
|
*/
|
|
private function normalizeServiceIds(array $services): array
|
|
{
|
|
$ids = [];
|
|
foreach ($services as $service) {
|
|
$ids[] = $service->id;
|
|
}
|
|
|
|
$ids = array_values(array_unique($ids));
|
|
sort($ids);
|
|
|
|
return $ids;
|
|
}
|
|
|
|
private function isSameService(?Service $left, ?Service $right): bool
|
|
{
|
|
return $left?->id === $right?->id;
|
|
}
|
|
|
|
private function isSamePickup(?Pickup $left, ?Pickup $right): bool
|
|
{
|
|
return $left?->id === $right?->id;
|
|
}
|
|
}
|