additionalServicesMutable = false; $travel->transportationServicesMutable = false; $travel->pickupsMutable = false; $workingParticipant = new ParticipantDto(); $workingParticipant->index = 0; $workingParticipant->additionalServices = [$this->createService(99)]; $workingParticipant->veg = $this->createService(88); $workingParticipant->transportationOutbound = $this->createService(88); $workingParticipant->pickup = $this->createPickup(77); $workingDto = new BookingDto($travel, 1); $workingDto->participants = [$workingParticipant]; $baselineParticipant = new ParticipantDto(); $baselineParticipant->index = 0; $baselineParticipant->additionalServices = [$this->createService(10)]; $baselineParticipant->veg = $this->createService(11); $baselineParticipant->transportationOutbound = $this->createService(20); $baselineParticipant->pickup = $this->createPickup(30); $baselineDto = new BookingDto($travel, 1); $baselineDto->participants = [$baselineParticipant]; $processor = $this->createMock(BookingDataProcessor::class); $processor->expects($this->once()) ->method('createBookingDtoFromBooking') ->willReturn($baselineDto); $service = new BookingEditSubmitGuard($processor, new ServiceAvailabilityCalculator()); $changed = $service->reconcileImmutableCategories($workingDto, new Booking()); $this->assertTrue($changed); $this->assertSame([10], array_map(static fn (Service $s) => $s->id, $workingParticipant->additionalServices)); $this->assertSame(11, $workingParticipant->veg?->id); $this->assertSame(20, $workingParticipant->transportationOutbound?->id); $this->assertSame(30, $workingParticipant->pickup?->id); } public function testReconcileImmutableCategoriesDoesNotChangeMutableCategories(): void { $travel = new Travel(); $travel->additionalServicesMutable = true; $travel->transportationServicesMutable = true; $travel->pickupsMutable = true; $workingParticipant = new ParticipantDto(); $workingParticipant->index = 0; $workingParticipant->additionalServices = [$this->createService(99)]; $workingDto = new BookingDto($travel, 1); $workingDto->participants = [$workingParticipant]; $baselineParticipant = new ParticipantDto(); $baselineParticipant->index = 0; $baselineParticipant->additionalServices = [$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 BookingEditSubmitGuard($processor, new ServiceAvailabilityCalculator()); $changed = $service->reconcileImmutableCategories($workingDto, new Booking()); $this->assertFalse($changed); $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 BookingEditSubmitGuard($processor, new ServiceAvailabilityCalculator()); $changed = $service->reconcileImmutableCategories($workingDto, new Booking()); $this->assertTrue($changed); $this->assertSame([10], array_map(static fn (Service $s) => $s->id, $workingParticipant->rentals)); } public function testRevertUnbookableServiceAdditionsDropsOnRequestAddition(): void { $onRequest = $this->createService(99); $onRequest->label = 'Bus-Hinfahrt'; $onRequest->subType = Constants::TOKEN_SKI_PASS; $onRequest->status = Constants::STATUS_ON_REQUEST; $travel = new Travel(); $travel->additionalServices = [99 => $onRequest]; $workingParticipant = new ParticipantDto(); $workingParticipant->index = 0; $workingParticipant->skiPass = $onRequest; $freshBooking = new Booking(); $workingDto = new BookingDto($travel, 1); $workingDto->participants = [$workingParticipant]; $workingDto->booking = $freshBooking; $baselineParticipant = new ParticipantDto(); $baselineParticipant->index = 0; $baselineDto = new BookingDto($travel, 1); $baselineDto->participants = [$baselineParticipant]; $processor = $this->createStub(BookingDataProcessor::class); $processor->method('createBookingDtoFromBooking')->willReturn($baselineDto); $guard = new BookingEditSubmitGuard($processor, new ServiceAvailabilityCalculator()); $reverted = $guard->revertUnbookableServiceAdditions($workingDto, $freshBooking); $this->assertSame(['Bus-Hinfahrt'], $reverted); $this->assertNull($workingParticipant->skiPass); } public function testRevertUnbookableServiceAdditionsKeepsAServiceTheParticipantAlreadyHolds(): void { $onRequest = $this->createService(99); $onRequest->label = 'Bus-Hinfahrt'; $onRequest->subType = Constants::TOKEN_SKI_PASS; $onRequest->status = Constants::STATUS_ON_REQUEST; $travel = new Travel(); $travel->additionalServices = [99 => $onRequest]; $held = clone $onRequest; $held->mapping = [0]; $freshBooking = new Booking(); $freshBooking->additionalServices = [99 => $held]; $workingParticipant = new ParticipantDto(); $workingParticipant->index = 0; $workingParticipant->skiPass = $onRequest; $workingDto = new BookingDto($travel, 1); $workingDto->participants = [$workingParticipant]; $workingDto->booking = $freshBooking; $baselineParticipant = new ParticipantDto(); $baselineParticipant->index = 0; $baselineParticipant->skiPass = $held; $baselineDto = new BookingDto($travel, 1); $baselineDto->participants = [$baselineParticipant]; $processor = $this->createStub(BookingDataProcessor::class); $processor->method('createBookingDtoFromBooking')->willReturn($baselineDto); $guard = new BookingEditSubmitGuard($processor, new ServiceAvailabilityCalculator()); $reverted = $guard->revertUnbookableServiceAdditions($workingDto, $freshBooking); $this->assertSame([], $reverted); $this->assertSame($onRequest, $workingParticipant->skiPass); } private function createService(int $id): Service { $service = new Service(); $service->id = $id; return $service; } private function createPickup(int $id): Pickup { $pickup = new Pickup(); $pickup->id = $id; return $pickup; } }