Files
myep/tests/Service/ServiceAvailabilityCalculatorTest.php
T

240 lines
9.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\ServiceAvailabilityCalculator;
use PHPUnit\Framework\TestCase;
class ServiceAvailabilityCalculatorTest extends TestCase
{
private ServiceAvailabilityCalculator $calculator;
protected function setUp(): void
{
$this->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<int, Service> $services
* @param list<int> $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;
}
}