419 lines
15 KiB
PHP
419 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Form\Model;
|
|
|
|
use App\BusProNet\Model\Address;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\PersonalData;
|
|
use App\BusProNet\Model\Travel;
|
|
use App\BusProNet\XmlLoader\AgencyLoader;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\ParticipantDto;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\Validator\Validation;
|
|
|
|
/**
|
|
* Tests for BookingDto validation and business logic.
|
|
*
|
|
* Email uniqueness validation tests have been moved to ParticipantEditDtoTest
|
|
* as part of the refactoring to use the wrapper DTO pattern.
|
|
*/
|
|
class BookingDtoTest extends TestCase
|
|
{
|
|
public function testBookingDtoCanBeInstantiated(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
|
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
$this->assertInstanceOf(BookingDto::class, $bookingDto);
|
|
$this->assertSame($travel, $bookingDto->travel);
|
|
$this->assertSame(BookingDto::MODE_CREATE, $bookingDto->getMode());
|
|
}
|
|
|
|
public function testUnserializeHandlesIntTravelId(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 42;
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
|
|
|
$dto = new BookingDto($travel, 1);
|
|
$serialized = serialize($dto);
|
|
$restored = unserialize($serialized);
|
|
|
|
$this->assertInstanceOf(BookingDto::class, $restored);
|
|
$this->assertSame(42, $restored->travel->id);
|
|
}
|
|
|
|
public function testUnserializeHandlesLegacyTravelObject(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 42;
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
|
|
|
// Simulate old session format where 'travel' is a full Travel object
|
|
$data = [
|
|
'travel' => $travel,
|
|
'hotelId' => 1,
|
|
];
|
|
|
|
$dto = new BookingDto(new Travel(), 1);
|
|
$dto->__unserialize($data);
|
|
|
|
$this->assertSame($travel, $dto->travel);
|
|
$this->assertSame(42, $dto->travel->id);
|
|
}
|
|
|
|
public function testUnserializeNormalizesLegacyBodyDimensionsOnParticipants(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 42;
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
|
|
|
$participant = new ParticipantDto();
|
|
$participant->height = '149-157';
|
|
$participant->weight = '75';
|
|
$participant->shoeSize = '043';
|
|
|
|
$dto = new BookingDto($travel, 1);
|
|
$dto->participants = [$participant];
|
|
|
|
$restored = unserialize(serialize($dto));
|
|
|
|
$this->assertInstanceOf(BookingDto::class, $restored);
|
|
$this->assertNull($restored->participants[0]->height);
|
|
$this->assertSame('75', $restored->participants[0]->weight);
|
|
$this->assertSame('43', $restored->participants[0]->shoeSize);
|
|
}
|
|
|
|
public function testUnserializeNormalizesLegacyParticipantStringsOnLoad(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 42;
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
|
|
|
$participant = new ParticipantDto();
|
|
$participant->firstName = ' Max ';
|
|
$participant->lastName = ' Mustermann ';
|
|
$participant->email = ' [email protected] ';
|
|
$participant->remarksRoom = ' ';
|
|
$participant->licensePlate = ' B-AB 123 ';
|
|
$participant->purchaseVoucherCode = ' ABC-123 ';
|
|
$participant->promoVoucherCode = ' PROMO ';
|
|
$participant->address = new Address();
|
|
$participant->address->street = ' Test Street 1 ';
|
|
$participant->address->postCode = ' 12345 ';
|
|
$participant->address->city = ' Test City ';
|
|
$participant->address->country = ' DE ';
|
|
|
|
$dto = new BookingDto($travel, 1);
|
|
$dto->participants = [$participant];
|
|
|
|
$restored = unserialize(serialize($dto));
|
|
|
|
$this->assertInstanceOf(BookingDto::class, $restored);
|
|
$this->assertSame('Max', $restored->participants[0]->firstName);
|
|
$this->assertSame('Mustermann', $restored->participants[0]->lastName);
|
|
$this->assertSame('[email protected]', $restored->participants[0]->email);
|
|
$this->assertNull($restored->participants[0]->remarksRoom);
|
|
$this->assertSame('B-AB 123', $restored->participants[0]->licensePlate);
|
|
$this->assertSame('ABC-123', $restored->participants[0]->purchaseVoucherCode);
|
|
$this->assertSame('PROMO', $restored->participants[0]->promoVoucherCode);
|
|
$this->assertSame('Test Street 1', $restored->participants[0]->address->street);
|
|
$this->assertSame('12345', $restored->participants[0]->address->postCode);
|
|
$this->assertSame('Test City', $restored->participants[0]->address->city);
|
|
$this->assertSame('DE', $restored->participants[0]->address->country);
|
|
}
|
|
|
|
public function testFirstParticipantIsTheApplicantByDefaultInCreateMode(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
|
|
$this->assertTrue($dto->isFirstParticipantApplicant());
|
|
}
|
|
|
|
public function testFirstParticipantIsNotTheApplicantWhenTheApplicantDoesNotTravel(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->applicantTravels = false;
|
|
|
|
$this->assertFalse($dto->isFirstParticipantApplicant());
|
|
}
|
|
|
|
public function testInternalAgencyBookingNeverHasTheFirstParticipantAsApplicantInEditMode(): void
|
|
{
|
|
$dto = $this->createEditModeDto(100, 100);
|
|
$dto->agencyCode = AgencyLoader::INTERNAL_AGENCY_CODE;
|
|
|
|
$this->assertFalse(
|
|
$dto->isFirstParticipantApplicant(),
|
|
'Their applicant is a customer who is not the first traveller, whatever the ids say'
|
|
);
|
|
}
|
|
|
|
public function testAgencyCodeDoesNotDecideAnythingInCreateMode(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->agencyCode = AgencyLoader::INTERNAL_AGENCY_CODE;
|
|
|
|
$this->assertTrue(
|
|
$dto->isFirstParticipantApplicant(),
|
|
'Internal agency bookings are never created here, so the code is only passed on'
|
|
);
|
|
}
|
|
|
|
public function testEditModeDerivesTheSplitFromPersonIds(): void
|
|
{
|
|
$dto = $this->createEditModeDto(applicantPersonId: 4711, firstParticipantPersonId: 4711);
|
|
|
|
$this->assertTrue($dto->isFirstParticipantApplicant());
|
|
|
|
$split = $this->createEditModeDto(applicantPersonId: 4711, firstParticipantPersonId: 4712);
|
|
|
|
$this->assertFalse($split->isFirstParticipantApplicant());
|
|
}
|
|
|
|
public function testEditModeTreatsAnUnknownPersonIdAsTheSamePerson(): void
|
|
{
|
|
$dto = $this->createEditModeDto(applicantPersonId: 4711, firstParticipantPersonId: null);
|
|
|
|
$this->assertTrue(
|
|
$dto->isFirstParticipantApplicant(),
|
|
'Incomplete BPN data must fall back to the established, locked-down behaviour'
|
|
);
|
|
}
|
|
|
|
public function testFamilyBookingRequiresTheApplicantToTravel(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->participants = [
|
|
$this->createParticipantAged(40),
|
|
$this->createParticipantAged(12),
|
|
];
|
|
|
|
$this->assertTrue($dto->isFamilyBooking());
|
|
|
|
$dto->applicantTravels = false;
|
|
|
|
$this->assertFalse(
|
|
$dto->isFamilyBooking(),
|
|
'Family insurance is not offered when the applicant does not travel'
|
|
);
|
|
}
|
|
|
|
public function testAdultSiblingWithAChildIsNotAFamilyBookingOnceTheApplicantStaysHome(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->applicantTravels = false;
|
|
$dto->participants = [
|
|
$this->createParticipantAged(18),
|
|
$this->createParticipantAged(12),
|
|
];
|
|
|
|
$this->assertFalse(
|
|
$dto->isFamilyBooking(),
|
|
'The age distribution alone would still qualify, which is why the flag is checked first'
|
|
);
|
|
}
|
|
|
|
public function testOldSessionsWithoutTheApplicantFieldsRestoreToTheDefaults(): void
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 42;
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
|
|
$dto = new BookingDto($travel, 1);
|
|
$data = $dto->__serialize();
|
|
unset($data['applicantTravels'], $data['applicant']);
|
|
|
|
$restored = new BookingDto($travel, 1);
|
|
$restored->__unserialize($data);
|
|
|
|
$this->assertTrue($restored->applicantTravels);
|
|
$this->assertNull($restored->applicant);
|
|
}
|
|
|
|
private function createCreateModeDto(): BookingDto
|
|
{
|
|
$travel = new Travel();
|
|
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
|
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
|
|
|
$dto = new BookingDto($travel, 1);
|
|
$dto->participants = [$this->createParticipantAged(40)];
|
|
|
|
return $dto;
|
|
}
|
|
|
|
private function createEditModeDto(?int $applicantPersonId, ?int $firstParticipantPersonId): BookingDto
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
|
|
$applicant = new PersonalData();
|
|
$applicant->personId = $applicantPersonId;
|
|
|
|
$booking = new Booking();
|
|
$booking->applicant = $applicant;
|
|
$dto->booking = $booking;
|
|
|
|
$dto->participants[0]->personId = $firstParticipantPersonId;
|
|
|
|
return $dto;
|
|
}
|
|
|
|
private function createParticipantAged(int $years): ParticipantDto
|
|
{
|
|
$participant = new ParticipantDto();
|
|
$participant->dateOfBirth = (new \DateTimeImmutable('2025-06-01'))->modify(sprintf('-%d years', $years));
|
|
|
|
return $participant;
|
|
}
|
|
|
|
public function testApplicantRulesDoNotLeakOntoTravellers(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->applicantTravels = false;
|
|
$dto->participants = [$this->createParticipantAged(15)];
|
|
$dto->participants[0]->index = 0;
|
|
|
|
$applicant = new ParticipantDto();
|
|
$applicant->firstName = 'Probe';
|
|
$applicant->lastName = 'Elternteil';
|
|
$applicant->gender = 'W';
|
|
$applicant->nationality = 'D';
|
|
$applicant->email = '[email protected]';
|
|
$applicant->mobile = '0171/222222';
|
|
$applicant->dateOfBirth = new \DateTimeImmutable('1981-03-16');
|
|
$applicant->address = $this->createAddress();
|
|
$dto->applicant = $applicant;
|
|
|
|
$validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
|
|
$violations = $validator->validate($dto, null, ['booking_applicant']);
|
|
|
|
$paths = [];
|
|
foreach ($violations as $violation) {
|
|
$paths[] = $violation->getPropertyPath();
|
|
}
|
|
|
|
$this->assertSame(
|
|
[],
|
|
$paths,
|
|
'A 15-year-old traveller must not be held to the applicant rules through the group cascade'
|
|
);
|
|
}
|
|
|
|
public function testIncompleteApplicantIsReportedUnderTheApplicantPathOnlyOnce(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->applicantTravels = false;
|
|
$dto->participants[0]->index = 0;
|
|
|
|
$applicant = new ParticipantDto();
|
|
$applicant->dateOfBirth = new \DateTimeImmutable('1981-03-16');
|
|
$dto->applicant = $applicant;
|
|
|
|
$validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
|
|
$violations = $validator->validate($dto, null, ['booking_applicant']);
|
|
|
|
$paths = [];
|
|
foreach ($violations as $violation) {
|
|
$paths[] = $violation->getPropertyPath();
|
|
}
|
|
|
|
$this->assertContains('applicant.firstName', $paths);
|
|
$this->assertContains(
|
|
'applicant.address.street',
|
|
$paths,
|
|
'A fresh ParticipantDto carries an empty Address, so the gaps surface per subfield'
|
|
);
|
|
$this->assertSame(
|
|
array_unique($paths),
|
|
$paths,
|
|
'Explicit delegation plus a cascade would report each gap twice'
|
|
);
|
|
}
|
|
|
|
public function testMinorApplicantIsRejected(): void
|
|
{
|
|
$dto = $this->createCreateModeDto();
|
|
$dto->applicantTravels = false;
|
|
$dto->participants[0]->index = 0;
|
|
|
|
$applicant = new ParticipantDto();
|
|
$applicant->firstName = 'Probe';
|
|
$applicant->lastName = 'Elternteil';
|
|
$applicant->gender = 'W';
|
|
$applicant->nationality = 'D';
|
|
$applicant->email = '[email protected]';
|
|
$applicant->mobile = '0171/222222';
|
|
$applicant->dateOfBirth = (new \DateTimeImmutable('today'))->modify('-17 years');
|
|
$applicant->address = $this->createAddress();
|
|
$dto->applicant = $applicant;
|
|
|
|
$validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
|
|
$violations = $validator->validate($dto, null, ['booking_applicant']);
|
|
|
|
$messages = [];
|
|
foreach ($violations as $violation) {
|
|
$messages[] = $violation->getMessage();
|
|
}
|
|
|
|
$this->assertContains('Die anmeldende Person muss volljährig sein.', $messages);
|
|
}
|
|
|
|
public function testEditModeDoesNotAskASplitBookingsFirstParticipantForAnAddress(): void
|
|
{
|
|
$dto = $this->createEditModeDto(100, 200);
|
|
$dto->participants[0]->index = 0;
|
|
$dto->participants[0]->address = null;
|
|
|
|
$this->assertSame([], $this->collectFirstParticipantAddressPaths($dto));
|
|
}
|
|
|
|
public function testEditModeStillAsksTheApplicantForAnAddress(): void
|
|
{
|
|
$dto = $this->createEditModeDto(100, 100);
|
|
$dto->participants[0]->index = 0;
|
|
$dto->participants[0]->address = null;
|
|
|
|
$this->assertSame(['participants[0].address'], $this->collectFirstParticipantAddressPaths($dto));
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function collectFirstParticipantAddressPaths(BookingDto $dto): array
|
|
{
|
|
$validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
|
|
$violations = $validator->validate($dto, null, ['booking_edit', 'strict_required']);
|
|
|
|
$paths = [];
|
|
foreach ($violations as $violation) {
|
|
if (str_starts_with($violation->getPropertyPath(), 'participants[0].address')) {
|
|
$paths[] = $violation->getPropertyPath();
|
|
}
|
|
}
|
|
|
|
return $paths;
|
|
}
|
|
|
|
private function createAddress(): Address
|
|
{
|
|
$address = new Address();
|
|
$address->street = 'Probeweg 7';
|
|
$address->postCode = '99999';
|
|
$address->city = 'MusterOrt';
|
|
$address->country = 'D';
|
|
|
|
return $address;
|
|
}
|
|
}
|