serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class); $this->handler = new ParticipantTransportationDiscountReplacementFieldHandler( $this->serviceAvailabilityCalculator ); } public function testGetFieldName(): void { $this->assertSame('transportationDiscountReplacement', $this->handler->getFieldName()); } public function testGetDependencies(): void { $this->assertSame(['transportationOutbound', 'transportationInbound'], $this->handler->getDependencies()); } public function testShouldProcessAlwaysReturnsTrue(): void { $this->assertTrue($this->handler->shouldProcess([], BookingDto::MODE_CREATE, 0)); $this->assertTrue($this->handler->shouldProcess(['some' => 'data'], BookingDto::MODE_EDIT, 5)); } public function testProcessFieldReplacesDiscountedWithRegularWhenInboundIsBus(): void { // Setup services $discountedPkw = $this->createPkwService(100, 'Selbstorganisiert (-15€ Rabatt)', -15.0); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); $busInbound = $this->createBusService(200, 'Bus', DirectionMapper::INBOUND_TRAVEL); // Setup participant with discounted PKW outbound and BUS inbound $participant = new ParticipantDto(); $participant->transportationOutbound = $discountedPkw; $participant->transportationInbound = $busInbound; // Setup booking with travel services $travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $busInbound]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $submittedData = []; // Process $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound was replaced with regular PKW $this->assertSame($regularPkw->id, $participant->transportationOutbound->id); $this->assertSame($regularPkw->label, $participant->transportationOutbound->label); // Assert notification was generated $this->assertCount(1, $participant->notifications); $notification = reset($participant->notifications); // Get first notification (keyed by MD5 hash) $this->assertSame('warning', $notification['type']); $this->assertStringContainsString('Hinfahrt automatisch angepasst', $notification['message']); $this->assertStringContainsString('Selbstorganisiert (-15€ Rabatt)', $notification['message']); $this->assertStringContainsString('Selbstorganisiert', $notification['message']); } public function testProcessFieldRestoresDiscountedPkwWhenInboundChangesToPkw(): void { // Setup services $discountedPkw = $this->createPkwService(100, 'Selbstorganisiert (-15€ Rabatt)', -15.0); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); $pkwInbound = $this->createPkwService(201, 'Selbstorganisiert', 0.0, DirectionMapper::INBOUND_TRAVEL); // Setup participant with regular PKW outbound and PKW inbound $participant = new ParticipantDto(); $participant->transportationOutbound = $regularPkw; $participant->transportationInbound = $pkwInbound; // Setup booking with travel services $travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $pkwInbound]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; // Mock availability: discounted PKW is available $this->serviceAvailabilityCalculator ->expects($this->once()) ->method('isServiceUnavailable') ->with($discountedPkw->id, $bookingDto, 0) ->willReturn(false); $submittedData = []; // Process $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound was replaced with discounted PKW $this->assertSame($discountedPkw->id, $participant->transportationOutbound->id); $this->assertSame($discountedPkw->label, $participant->transportationOutbound->label); // Assert notification was generated $this->assertCount(1, $participant->notifications); $notification = reset($participant->notifications); // Get first notification (keyed by MD5 hash) $this->assertSame('success', $notification['type']); $this->assertStringContainsString('Hinfahrt automatisch angepasst', $notification['message']); $this->assertStringContainsString('wieder verfügbar', $notification['message']); } public function testProcessFieldDoesNotRestoreDiscountWhenUnavailable(): void { // Setup services $discountedPkw = $this->createPkwService(100, 'Selbstorganisiert (-15€ Rabatt)', -15.0); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); $pkwInbound = $this->createPkwService(201, 'Selbstorganisiert', 0.0, DirectionMapper::INBOUND_TRAVEL); // Setup participant with regular PKW outbound and PKW inbound $participant = new ParticipantDto(); $participant->transportationOutbound = $regularPkw; $participant->transportationInbound = $pkwInbound; // Setup booking with travel services $travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $pkwInbound]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; // Mock availability: discounted PKW is sold out $this->serviceAvailabilityCalculator ->expects($this->once()) ->method('isServiceUnavailable') ->with($discountedPkw->id, $bookingDto, 0) ->willReturn(true); $submittedData = []; // Process $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound remains regular PKW $this->assertSame($regularPkw, $participant->transportationOutbound); // Assert no notification was generated $this->assertEmpty($participant->notifications); } public function testProcessFieldNoReplacementWhenOutboundIsBus(): void { // Setup services $busOutbound = $this->createBusService(100, 'Bus', DirectionMapper::OUTBOUND_TRAVEL); $busInbound = $this->createBusService(200, 'Bus', DirectionMapper::INBOUND_TRAVEL); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); // Setup participant with BUS outbound and BUS inbound $participant = new ParticipantDto(); $participant->transportationOutbound = $busOutbound; $participant->transportationInbound = $busInbound; // Setup booking with travel services $travel = $this->createTravelWithServices([$busOutbound, $regularPkw, $busInbound]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $submittedData = []; // Process $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound remains BUS (no replacement) $this->assertSame($busOutbound, $participant->transportationOutbound); // Assert no notification was generated $this->assertEmpty($participant->notifications); } public function testProcessFieldNoReplacementWhenOutboundIsRegularPkwAndInboundIsBus(): void { // Setup services $discountedPkw = $this->createPkwService(100, 'Selbstorganisiert (-15€ Rabatt)', -15.0); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); $busInbound = $this->createBusService(200, 'Bus', DirectionMapper::INBOUND_TRAVEL); // Setup participant with regular PKW outbound and BUS inbound $participant = new ParticipantDto(); $participant->transportationOutbound = $regularPkw; $participant->transportationInbound = $busInbound; // Setup booking with travel services $travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $busInbound]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $submittedData = []; // Process $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound remains regular PKW (already correct) $this->assertSame($regularPkw, $participant->transportationOutbound); // Assert no notification was generated $this->assertEmpty($participant->notifications); } public function testProcessFieldNoReplacementWhenOutboundIsDiscountedAndInboundIsPkw(): void { // Setup services $discountedPkw = $this->createPkwService(100, 'Selbstorganisiert (-15€ Rabatt)', -15.0); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); $pkwInbound = $this->createPkwService(201, 'Selbstorganisiert', 0.0, DirectionMapper::INBOUND_TRAVEL); // Setup participant with discounted PKW outbound and PKW inbound (optimal state) $participant = new ParticipantDto(); $participant->transportationOutbound = $discountedPkw; $participant->transportationInbound = $pkwInbound; // Setup booking with travel services $travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $pkwInbound]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $submittedData = []; // Process $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound remains discounted PKW (already optimal) $this->assertSame($discountedPkw, $participant->transportationOutbound); // Assert no notification was generated $this->assertEmpty($participant->notifications); } public function testProcessFieldHandlesNullOutbound(): void { // Setup participant with null outbound $participant = new ParticipantDto(); $participant->transportationOutbound = null; $participant->transportationInbound = $this->createBusService(200, 'Bus', DirectionMapper::INBOUND_TRAVEL); // Setup booking $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $submittedData = []; // Process - should handle gracefully $this->handler->processField($submittedData, $bookingDto, 0); // Assert no error and no notification $this->assertNull($participant->transportationOutbound); $this->assertEmpty($participant->notifications); } public function testProcessFieldHandlesNullInbound(): void { // Setup services $discountedPkw = $this->createPkwService(100, 'Selbstorganisiert (-15€ Rabatt)', -15.0); $regularPkw = $this->createPkwService(101, 'Selbstorganisiert', 0.0); // Setup participant with regular PKW outbound and null inbound $participant = new ParticipantDto(); $participant->transportationOutbound = $regularPkw; $participant->transportationInbound = null; // Setup booking with travel services $travel = $this->createTravelWithServices([$discountedPkw, $regularPkw]); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; // Mock availability: discounted PKW is available $this->serviceAvailabilityCalculator ->expects($this->once()) ->method('isServiceUnavailable') ->with($discountedPkw->id, $bookingDto, 0) ->willReturn(false); $submittedData = []; // Process - null inbound is treated as non-BUS, should attempt restoration $this->handler->processField($submittedData, $bookingDto, 0); // Assert outbound was replaced with discounted PKW $this->assertSame($discountedPkw->id, $participant->transportationOutbound->id); $this->assertSame($discountedPkw->label, $participant->transportationOutbound->label); } public function testProcessFieldHandlesNullParticipant(): void { // Setup booking with no participants $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = []; $submittedData = []; // Process - should handle gracefully $this->handler->processField($submittedData, $bookingDto, 0); // Should not throw error $this->expectNotToPerformAssertions(); } private function createPkwService(int $id, string $label, float $price, string $direction = DirectionMapper::OUTBOUND_TRAVEL): Service { $service = new Service(); $service->id = $id; $service->label = $label; $service->subType = DirectionMapper::SUBTYPE_CAR_API; // 'PKW' $service->direction = $direction; $service->price = $price; return $service; } private function createBusService(int $id, string $label, string $direction): Service { $service = new Service(); $service->id = $id; $service->label = $label; $service->subType = DirectionMapper::SUBTYPE_BUS_API; // 'BUS' $service->direction = $direction; $service->price = 0.0; return $service; } private function createTravelWithServices(array $services): Travel { $travel = new Travel(); $travel->transportationServices = $services; return $travel; } }