feat: implement mutability cutoff for rentals
addresses #869cca42x
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Service\Contract\FieldConditionInterface;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition that becomes true once a configurable day-based cutoff is reached.
|
||||||
|
*
|
||||||
|
* Cutoff semantics are inclusive and day-based:
|
||||||
|
* now(startOfDay) >= travelStart(startOfDay) - N days.
|
||||||
|
*/
|
||||||
|
class TravelStartCutoffReachedCondition implements FieldConditionInterface
|
||||||
|
{
|
||||||
|
public const int DEFAULT_DAYS_BEFORE_START = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $daysBeforeStart Number of days before travel start when the cutoff is reached
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly int $daysBeforeStart,
|
||||||
|
) {
|
||||||
|
if ($daysBeforeStart < 0) {
|
||||||
|
throw new \InvalidArgumentException('daysBeforeStart must be greater than or equal to 0.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||||
|
{
|
||||||
|
if (null === $bookingDto->travel->dateFrom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom)->startOfDay();
|
||||||
|
$cutoffDate = $travelStartDate->subDays($this->daysBeforeStart);
|
||||||
|
$today = CarbonImmutable::now()->startOfDay();
|
||||||
|
|
||||||
|
return $today->greaterThanOrEqualTo($cutoffDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDependentFields(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Cutoff reached from %d day(s) before travel start (inclusive)',
|
||||||
|
$this->daysBeforeStart
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ use App\Form\Service\Condition\RoomSelectionCondition;
|
|||||||
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||||
use App\Form\Service\Condition\SingleRoomTypeCondition;
|
use App\Form\Service\Condition\SingleRoomTypeCondition;
|
||||||
use App\Form\Service\Condition\SkiPassSelectionCondition;
|
use App\Form\Service\Condition\SkiPassSelectionCondition;
|
||||||
|
use App\Form\Service\Condition\TravelStartCutoffReachedCondition;
|
||||||
use App\Service\ParticipantEligibilityService;
|
use App\Service\ParticipantEligibilityService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,6 +80,10 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
|
|
||||||
$rentalCondition = new RentalSelectionCondition();
|
$rentalCondition = new RentalSelectionCondition();
|
||||||
$skiPassCondition = new SkiPassSelectionCondition();
|
$skiPassCondition = new SkiPassSelectionCondition();
|
||||||
|
// Booking-level D-4 cutoff used to lock mutable service selections close to departure.
|
||||||
|
$serviceCutoffReachedCondition = new TravelStartCutoffReachedCondition(
|
||||||
|
TravelStartCutoffReachedCondition::DEFAULT_DAYS_BEFORE_START
|
||||||
|
);
|
||||||
|
|
||||||
// First participant read-only condition
|
// First participant read-only condition
|
||||||
// Render personal data fields as static text for first participant in non-internal agency bookings
|
// Render personal data fields as static text for first participant in non-internal agency bookings
|
||||||
@@ -177,13 +182,15 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Show rentals only when both date of birth is provided AND skipass is selected AND participant is eligible
|
// Show rentals only when both date of birth is provided AND skipass is selected AND participant is eligible.
|
||||||
|
// From D-4 onward, keep rentals visible but make them read-only.
|
||||||
$this->fieldStateConditions['rentals'] = [
|
$this->fieldStateConditions['rentals'] = [
|
||||||
'hidden' => CompositeCondition::or(
|
'hidden' => CompositeCondition::or(
|
||||||
CompositeCondition::not($dateOfBirthProvidedCondition),
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||||
CompositeCondition::not($skiPassCondition),
|
CompositeCondition::not($skiPassCondition),
|
||||||
$bookingEligibilityCondition
|
$bookingEligibilityCondition
|
||||||
),
|
),
|
||||||
|
'readonly' => $serviceCutoffReachedCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->fieldStateConditions['board'] = [
|
$this->fieldStateConditions['board'] = [
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use App\Form\Service\Condition\RentalSelectionCondition;
|
|||||||
use App\Form\Service\Condition\RoomSelectionCondition;
|
use App\Form\Service\Condition\RoomSelectionCondition;
|
||||||
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||||
use App\Form\Service\Condition\SkiPassSelectionCondition;
|
use App\Form\Service\Condition\SkiPassSelectionCondition;
|
||||||
|
use App\Form\Service\Condition\TravelStartCutoffReachedCondition;
|
||||||
use App\Form\Service\Condition\TransportationServicesMutabilityCondition;
|
use App\Form\Service\Condition\TransportationServicesMutabilityCondition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +49,10 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
$additionalServicesMutabilityCondition = new AdditionalServicesMutabilityCondition();
|
$additionalServicesMutabilityCondition = new AdditionalServicesMutabilityCondition();
|
||||||
$transportationServicesMutabilityCondition = new TransportationServicesMutabilityCondition();
|
$transportationServicesMutabilityCondition = new TransportationServicesMutabilityCondition();
|
||||||
$pickupsMutabilityCondition = new PickupsMutabilityCondition();
|
$pickupsMutabilityCondition = new PickupsMutabilityCondition();
|
||||||
|
// Booking-level D-4 cutoff used in addition to BPN mutability flags.
|
||||||
|
$serviceCutoffReachedCondition = new TravelStartCutoffReachedCondition(
|
||||||
|
TravelStartCutoffReachedCondition::DEFAULT_DAYS_BEFORE_START
|
||||||
|
);
|
||||||
|
|
||||||
// Personal data protection in edit mode:
|
// Personal data protection in edit mode:
|
||||||
// 1. First participant in non-internal agency booking - always read-only (first participant = applicant)
|
// 1. First participant in non-internal agency booking - always read-only (first participant = applicant)
|
||||||
@@ -155,14 +160,18 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'readonly' => $additionalServicesMutabilityCondition,
|
'readonly' => $additionalServicesMutabilityCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Rentals - shown only when skipass selected, readonly if services not mutable
|
// Rentals - shown only when skipass selected.
|
||||||
|
// Read-only when either BPN additional services are immutable OR the D-4 cutoff is reached.
|
||||||
// For first participant: only check skipass, not DOB
|
// For first participant: only check skipass, not DOB
|
||||||
$this->fieldStateConditions['rentals'] = [
|
$this->fieldStateConditions['rentals'] = [
|
||||||
'hidden' => CompositeCondition::or(
|
'hidden' => CompositeCondition::or(
|
||||||
$hideUntilDobCondition,
|
$hideUntilDobCondition,
|
||||||
CompositeCondition::not($skiPassCondition)
|
CompositeCondition::not($skiPassCondition)
|
||||||
),
|
),
|
||||||
'readonly' => $additionalServicesMutabilityCondition,
|
'readonly' => CompositeCondition::or(
|
||||||
|
$additionalServicesMutabilityCondition,
|
||||||
|
$serviceCutoffReachedCondition
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Rental insurance - shown only when rentals selected AND LVS services exist, readonly if services not mutable
|
// Rental insurance - shown only when rentals selected AND LVS services exist, readonly if services not mutable
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\BusProNet\Model\Pickup;
|
|||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use App\Form\Service\Condition\TravelStartCutoffReachedCondition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces immutable edit categories before submitting booking updates.
|
* Enforces immutable edit categories before submitting booking updates.
|
||||||
@@ -34,6 +35,14 @@ class BookingEditSubmitGuardService
|
|||||||
public function reconcileImmutableCategories(BookingDto $workingDto, Booking $freshBooking): bool
|
public function reconcileImmutableCategories(BookingDto $workingDto, Booking $freshBooking): bool
|
||||||
{
|
{
|
||||||
$baselineDto = $this->bookingDataProcessor->createBookingDtoFromBooking($freshBooking, $workingDto->travel);
|
$baselineDto = $this->bookingDataProcessor->createBookingDtoFromBooking($freshBooking, $workingDto->travel);
|
||||||
|
// 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;
|
$changed = false;
|
||||||
|
|
||||||
@@ -47,6 +56,12 @@ class BookingEditSubmitGuardService
|
|||||||
$changed = $this->reconcileAdditionalServices($participant, $baseline) || $changed;
|
$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) {
|
if (false === $workingDto->travel->transportationServicesMutable) {
|
||||||
$changed = $this->reconcileTransportationServices($participant, $baseline) || $changed;
|
$changed = $this->reconcileTransportationServices($participant, $baseline) || $changed;
|
||||||
}
|
}
|
||||||
@@ -78,11 +93,6 @@ class BookingEditSubmitGuardService
|
|||||||
$changed = true;
|
$changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->areServiceListsEqual($participant->rentals, $baseline->rentals)) {
|
|
||||||
$participant->rentals = $baseline->rentals;
|
|
||||||
$changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (false === $this->isSameService($participant->skiPass, $baseline->skiPass)) {
|
if (false === $this->isSameService($participant->skiPass, $baseline->skiPass)) {
|
||||||
$participant->skiPass = $baseline->skiPass;
|
$participant->skiPass = $baseline->skiPass;
|
||||||
$changed = true;
|
$changed = true;
|
||||||
@@ -101,6 +111,17 @@ class BookingEditSubmitGuardService
|
|||||||
return $changed;
|
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
|
private function reconcileTransportationServices(ParticipantDto $participant, ParticipantDto $baseline): bool
|
||||||
{
|
{
|
||||||
$changed = false;
|
$changed = false;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class AppExtension extends AbstractExtension
|
|||||||
new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]),
|
new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]),
|
||||||
new TwigFunction('is_static_text', [AppRuntime::class, 'isStaticText']),
|
new TwigFunction('is_static_text', [AppRuntime::class, 'isStaticText']),
|
||||||
new TwigFunction('is_hidden', [AppRuntime::class, 'isHidden']),
|
new TwigFunction('is_hidden', [AppRuntime::class, 'isHidden']),
|
||||||
|
new TwigFunction('is_travel_start_cutoff_reached', [AppRuntime::class, 'isTravelStartCutoffReached']),
|
||||||
new TwigFunction('collect_invalid_field_labels', [AppRuntime::class, 'collectInvalidFieldLabels']),
|
new TwigFunction('collect_invalid_field_labels', [AppRuntime::class, 'collectInvalidFieldLabels']),
|
||||||
new TwigFunction('booking_theme', [AppRuntime::class, 'getBookingTheme']),
|
new TwigFunction('booking_theme', [AppRuntime::class, 'getBookingTheme']),
|
||||||
new TwigFunction('gtm_id', [AppRuntime::class, 'getGtmId']),
|
new TwigFunction('gtm_id', [AppRuntime::class, 'getGtmId']),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\Twig;
|
|||||||
use App\BusProNet\DataProvider\CountryDataProvider;
|
use App\BusProNet\DataProvider\CountryDataProvider;
|
||||||
use App\EventListener\DomainThemeListener;
|
use App\EventListener\DomainThemeListener;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Service\Condition\TravelStartCutoffReachedCondition;
|
||||||
use App\Form\Service\CreateFieldStateProvider;
|
use App\Form\Service\CreateFieldStateProvider;
|
||||||
use App\Form\Service\EditFieldStateProvider;
|
use App\Form\Service\EditFieldStateProvider;
|
||||||
use App\Model\DomainConfig;
|
use App\Model\DomainConfig;
|
||||||
@@ -232,6 +233,23 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
return $labels;
|
return $labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a travel start cutoff window has been reached.
|
||||||
|
*
|
||||||
|
* Uses inclusive day semantics: cutoff is reached when current day is on or
|
||||||
|
* after (travel start day - $daysBeforeStart).
|
||||||
|
*/
|
||||||
|
public function isTravelStartCutoffReached(BookingDto $bookingDto, int $daysBeforeStart = 4): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$condition = new TravelStartCutoffReachedCondition($daysBeforeStart);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition->evaluate($bookingDto, 0, []);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current booking theme from request attribute.
|
* Returns the current booking theme from request attribute.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -417,6 +417,14 @@
|
|||||||
{% if form.rentals is defined %}
|
{% if form.rentals is defined %}
|
||||||
{{ form_errors(form.rentals) }}
|
{{ form_errors(form.rentals) }}
|
||||||
{{ form_help(form.rentals) }}
|
{{ form_help(form.rentals) }}
|
||||||
|
{% if is_travel_start_cutoff_reached(bookingDto) %}
|
||||||
|
<div class="text-sm mt-2 px-2 flex items-start space-x-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 flex-shrink-0">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||||
|
</svg>
|
||||||
|
<span>Online nicht mehr buchbar. Bitte direkt über die <strong>Hausleitung vor Ort</strong> anfragen (vorbehaltlich Verfügbarkeit)</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Booking;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Service\Condition\TravelStartCutoffReachedCondition;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class TravelStartCutoffReachedConditionTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEvaluateReturnsFalseBeforeCutoff(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow('2026-01-05 10:00:00');
|
||||||
|
$condition = new TravelStartCutoffReachedCondition(4);
|
||||||
|
|
||||||
|
$result = $condition->evaluate($this->createCreateDto('2026-01-10 12:00:00'), 0, []);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEvaluateReturnsTrueAtInclusiveCutoffBoundary(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow('2026-01-06 01:00:00');
|
||||||
|
$condition = new TravelStartCutoffReachedCondition(4);
|
||||||
|
|
||||||
|
$result = $condition->evaluate($this->createCreateDto('2026-01-10 12:00:00'), 0, []);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEvaluateReturnsTrueAfterCutoff(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow('2026-01-08 09:00:00');
|
||||||
|
$condition = new TravelStartCutoffReachedCondition(4);
|
||||||
|
|
||||||
|
$result = $condition->evaluate($this->createCreateDto('2026-01-10 12:00:00'), 0, []);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEvaluateReturnsTrueInEditModeAfterCutoff(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow('2026-01-08 09:00:00');
|
||||||
|
$condition = new TravelStartCutoffReachedCondition(4);
|
||||||
|
|
||||||
|
$this->assertTrue($condition->evaluate($this->createEditDto('2026-01-10 12:00:00'), 0, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorThrowsForNegativeDays(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
|
||||||
|
new TravelStartCutoffReachedCondition(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createCreateDto(string $travelStartDate): BookingDto
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->dateFrom = new \DateTimeImmutable($travelStartDate);
|
||||||
|
|
||||||
|
return new BookingDto($travel, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createEditDto(string $travelStartDate): BookingDto
|
||||||
|
{
|
||||||
|
$dto = $this->createCreateDto($travelStartDate);
|
||||||
|
$dto->booking = new Booking();
|
||||||
|
|
||||||
|
return $dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,16 @@ use App\BusProNet\Model\Travel;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Service\BookingEditSubmitGuardService;
|
use App\Service\BookingEditSubmitGuardService;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class BookingEditSubmitGuardServiceTest extends TestCase
|
class BookingEditSubmitGuardServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow();
|
||||||
|
}
|
||||||
|
|
||||||
public function testReconcileImmutableCategoriesRestoresLockedServiceData(): void
|
public function testReconcileImmutableCategoriesRestoresLockedServiceData(): void
|
||||||
{
|
{
|
||||||
$travel = new Travel();
|
$travel = new Travel();
|
||||||
@@ -90,6 +96,43 @@ class BookingEditSubmitGuardServiceTest extends TestCase
|
|||||||
$this->assertSame([99], array_map(static fn (Service $s) => $s->id, $workingParticipant->additionalServices));
|
$this->assertSame([99], array_map(static fn (Service $s) => $s->id, $workingParticipant->additionalServices));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReconcileImmutableCategoriesLocksRentalsFromFourDaysBeforeTravelStart(): void
|
||||||
|
{
|
||||||
|
CarbonImmutable::setTestNow('2026-01-08 12:00:00');
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->dateFrom = new \DateTimeImmutable('2026-01-10 09:00:00');
|
||||||
|
$travel->additionalServicesMutable = true;
|
||||||
|
$travel->transportationServicesMutable = true;
|
||||||
|
$travel->pickupsMutable = true;
|
||||||
|
|
||||||
|
$workingParticipant = new ParticipantDto();
|
||||||
|
$workingParticipant->index = 0;
|
||||||
|
$workingParticipant->rentals = [$this->createService(99)];
|
||||||
|
|
||||||
|
$workingDto = new BookingDto($travel, 1);
|
||||||
|
$workingDto->participants = [$workingParticipant];
|
||||||
|
|
||||||
|
$baselineParticipant = new ParticipantDto();
|
||||||
|
$baselineParticipant->index = 0;
|
||||||
|
$baselineParticipant->rentals = [$this->createService(10)];
|
||||||
|
|
||||||
|
$baselineDto = new BookingDto($travel, 1);
|
||||||
|
$baselineDto->participants = [$baselineParticipant];
|
||||||
|
|
||||||
|
$processor = $this->createMock(BookingDataProcessor::class);
|
||||||
|
$processor->expects($this->once())
|
||||||
|
->method('createBookingDtoFromBooking')
|
||||||
|
->willReturn($baselineDto);
|
||||||
|
|
||||||
|
$service = new BookingEditSubmitGuardService($processor);
|
||||||
|
|
||||||
|
$changed = $service->reconcileImmutableCategories($workingDto, new Booking());
|
||||||
|
|
||||||
|
$this->assertTrue($changed);
|
||||||
|
$this->assertSame([10], array_map(static fn (Service $s) => $s->id, $workingParticipant->rentals));
|
||||||
|
}
|
||||||
|
|
||||||
private function createService(int $id): Service
|
private function createService(int $id): Service
|
||||||
{
|
{
|
||||||
$service = new Service();
|
$service = new Service();
|
||||||
|
|||||||
Reference in New Issue
Block a user