Files
myep/tests/Form/Service/ParticipantTransportationDiscountReplacementFieldHandlerTest.php

474 lines
20 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Form\Service;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantTransportationDiscountReplacementFieldHandler;
use App\Service\ServiceAvailabilityCalculator;
use PHPUnit\Framework\TestCase;
class ParticipantTransportationDiscountReplacementFieldHandlerTest extends TestCase
{
private ?ParticipantTransportationDiscountReplacementFieldHandler $handler = null;
private ServiceAvailabilityCalculator $serviceAvailabilityCalculator;
protected function setUp(): void
{
$this->serviceAvailabilityCalculator = $this->createStub(ServiceAvailabilityCalculator::class);
}
private function handler(): ParticipantTransportationDiscountReplacementFieldHandler
{
return $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
{
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
// 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
{
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
// 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
{
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
// 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();
}
public function testProcessFieldSkipsDiscountRestorationForBabyParticipant(): void
{
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
// 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 baby participant (age 1) with regular PKW outbound and PKW inbound
$participant = new ParticipantDto();
$participant->dateOfBirth = new \DateTimeImmutable('-1 year');
$participant->transportationOutbound = $regularPkw;
$participant->transportationInbound = $pkwInbound;
// Setup booking with travel services - travel starts today
$travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $pkwInbound]);
$travel->dateFrom = new \DateTimeImmutable('today');
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
// Availability calculator should NOT be called for babies
$this->serviceAvailabilityCalculator
->expects($this->never())
->method('isServiceUnavailable');
$submittedData = [];
// Process
$this->handler()->processField($submittedData, $bookingDto, 0);
// Assert outbound remains regular PKW (no discount restoration for babies)
$this->assertSame($regularPkw->id, $participant->transportationOutbound->id);
// Assert no notification was generated
$this->assertEmpty($participant->notifications);
}
public function testProcessFieldSkipsDiscountRestorationForTwoYearOldParticipant(): void
{
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
// 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 at BABY_MAX_AGE (2 years) with regular PKW
$participant = new ParticipantDto();
$participant->dateOfBirth = new \DateTimeImmutable('-2 years');
$participant->transportationOutbound = $regularPkw;
$participant->transportationInbound = $pkwInbound;
// Setup booking with travel services - travel starts today
$travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $pkwInbound]);
$travel->dateFrom = new \DateTimeImmutable('today');
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
// Availability calculator should NOT be called for babies
$this->serviceAvailabilityCalculator
->expects($this->never())
->method('isServiceUnavailable');
$submittedData = [];
// Process
$this->handler()->processField($submittedData, $bookingDto, 0);
// Assert outbound remains regular PKW
$this->assertSame($regularPkw->id, $participant->transportationOutbound->id);
// Assert no notification was generated
$this->assertEmpty($participant->notifications);
}
public function testProcessFieldRestoresDiscountForThreeYearOldParticipant(): void
{
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
// 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 clearly above BABY_MAX_AGE (3+ years) with regular PKW
// Use specific dates to ensure age calculation is unambiguous
$participant = new ParticipantDto();
$participant->dateOfBirth = new \DateTimeImmutable('2020-01-01');
$participant->transportationOutbound = $regularPkw;
$participant->transportationInbound = $pkwInbound;
// Setup booking with travel services - travel starts well after 3rd birthday
$travel = $this->createTravelWithServices([$discountedPkw, $regularPkw, $pkwInbound]);
$travel->dateFrom = new \DateTimeImmutable('2024-06-01');
$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 (3-year-old is eligible)
$this->assertSame($discountedPkw->id, $participant->transportationOutbound->id);
// Assert notification was generated
$this->assertCount(1, $participant->notifications);
}
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;
}
}