calculator = new ServiceAvailabilityCalculator(); } public function testParkingServiceIsUnavailableWhenSoldOut(): void { $parkingService = $this->createParkingService(id: 1, available: 0); $bookingDto = $this->createBookingDtoWithServices([$parkingService]); $this->assertTrue($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testParkingServiceIsAvailableWhenQuotaRemains(): void { $parkingService = $this->createParkingService(id: 1, available: 5); $bookingDto = $this->createBookingDtoWithServices([$parkingService]); $this->assertFalse($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testParkingSelectedByOtherParticipantCountsTowardUsage(): void { $parkingService = $this->createParkingService(id: 1, available: 1); $bookingDto = $this->createBookingDtoWithServices([$parkingService]); $otherParticipant = new ParticipantDto(); $otherParticipant->parkingService = $parkingService; $bookingDto->participants[1] = $otherParticipant; // Only one parking slot remains, and another participant already claimed it, // so it must show unavailable for the current participant (index 0) $this->assertTrue($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testServiceOnBookingStopIsUnavailableDespiteUnlimitedQuota(): void { $skiPass = $this->createSkiPass(id: 1, available: null, status: Constants::STATUS_BLOCKED); $bookingDto = $this->createBookingDtoWithServices([$skiPass]); $this->assertTrue($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testServiceOnBookingStopIsUnavailableDespiteRemainingQuota(): void { $skiPass = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_BLOCKED); $bookingDto = $this->createBookingDtoWithServices([$skiPass]); $this->assertTrue($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testFilterAvailableServicesRemovesSoldOutAndBlockedServices(): void { $bookable = $this->createSkiPass(id: 1, available: 5); $soldOut = $this->createSkiPass(id: 2, available: 0); $blocked = $this->createSkiPass(id: 3, available: null, status: Constants::STATUS_BLOCKED); $unlimited = $this->createSkiPass(id: 4, available: null); $bookingDto = $this->createBookingDtoWithServices([$bookable, $soldOut, $blocked, $unlimited]); $filtered = $this->calculator->filterAvailableServices( $bookingDto->travel->additionalServices, $bookingDto, 0 ); $this->assertSame([1, 4], array_keys($filtered)); } public function testOnRequestServiceStaysAvailableInCreateMode(): void { $skiPass = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_ON_REQUEST); $bookingDto = $this->createBookingDtoWithServices([$skiPass]); // Create may still move the whole booking to status 'A', so on request is bookable there $this->assertFalse($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testOnRequestServiceIsUnavailableInEditMode(): void { $skiPass = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_ON_REQUEST); $bookingDto = $this->createEditBookingDtoWithServices([$skiPass]); $this->assertTrue($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testOnRequestServiceAlreadyHeldStaysAvailableInEditMode(): void { $skiPass = $this->createSkiPass(id: 1, available: 0, status: Constants::STATUS_ON_REQUEST); $bookingDto = $this->createEditBookingDtoWithServices([$skiPass], heldServiceIds: [1]); // Withdrawing a booked service breaks the Leistung/Teilnehmer counts, so it must be keepable $this->assertFalse($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testAutoBookedOnRequestServiceStaysAvailableInEditMode(): void { $service = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_ON_REQUEST); $service->autoBook = true; $bookingDto = $this->createEditBookingDtoWithServices([$service]); $this->assertFalse($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testMandatoryNonTransportOnRequestServiceStaysAvailableInEditMode(): void { $service = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_ON_REQUEST); $service->mandatory = true; $service->category = Constants::CATEGORY_ADDITIONAL; $bookingDto = $this->createEditBookingDtoWithServices([$service]); // Blocking a service the mandatory-services validator requires makes the form unsatisfiable $this->assertFalse($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testMandatoryTransportOnRequestServiceIsStillUnavailableInEditMode(): void { $service = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_ON_REQUEST); $service->mandatory = true; $service->category = Constants::CATEGORY_TRANSPORTATION; $bookingDto = $this->createEditBookingDtoWithServices([$service]); // pflicht on a transport leg means "pick one of this group", not "compulsory" $this->assertTrue($this->calculator->isServiceUnavailable(1, $bookingDto, 0)); } public function testFilterAvailableServicesNeverEmptiesAGroupOnTheOnRequestRuleAlone(): void { $first = $this->createSkiPass(id: 1, available: 10, status: Constants::STATUS_ON_REQUEST); $second = $this->createSkiPass(id: 2, available: 10, status: Constants::STATUS_ON_REQUEST); $bookingDto = $this->createEditBookingDtoWithServices([$first, $second]); $filtered = $this->calculator->filterAvailableServices( $bookingDto->travel->additionalServices, $bookingDto, 0 ); $this->assertSame([1, 2], array_keys($filtered)); } public function testFilterAvailableServicesStillEmptiesAGroupThatIsSoldOutOrBlocked(): void { $soldOut = $this->createSkiPass(id: 1, available: 0, status: Constants::STATUS_ON_REQUEST); $blocked = $this->createSkiPass(id: 2, available: null, status: Constants::STATUS_BLOCKED); $bookingDto = $this->createEditBookingDtoWithServices([$soldOut, $blocked]); $filtered = $this->calculator->filterAvailableServices( $bookingDto->travel->additionalServices, $bookingDto, 0 ); $this->assertSame([], array_keys($filtered)); } private function createParkingService(int $id, int $available): Service { $service = new Service(); $service->id = $id; $service->label = 'Parkplatz'; $service->subType = Constants::TOKEN_PARKING; $service->available = $available; return $service; } private function createSkiPass(int $id, ?int $available, string $status = Constants::STATUS_AVAILABLE): Service { $service = new Service(); $service->id = $id; $service->label = sprintf('Skipass %d', $id); $service->subType = Constants::TOKEN_SKI_PASS; $service->available = $available; $service->status = $status; return $service; } private function createBookingDtoWithServices(array $services): BookingDto { $travel = new Travel(); $indexed = []; foreach ($services as $service) { $indexed[$service->id] = $service; } $travel->additionalServices = $indexed; $bookingDto = new BookingDto($travel, 1); $bookingDto->participants[0] = new ParticipantDto(); return $bookingDto; } /** * @param array $services * @param list $heldServiceIds IDs participant 0 already holds in the booking */ private function createEditBookingDtoWithServices(array $services, array $heldServiceIds = []): BookingDto { $bookingDto = $this->createBookingDtoWithServices($services); $heldServices = []; foreach ($heldServiceIds as $serviceId) { $held = clone $bookingDto->travel->additionalServices[$serviceId]; $held->mapping = [0]; $heldServices[$serviceId] = $held; } // A non-null booking is what puts the DTO into edit mode $booking = new Booking(); $booking->additionalServices = $heldServices; $bookingDto->booking = $booking; return $bookingDto; } }