feat: split booking between participants and applicant
addresses #869f2q1vu
This commit is contained in:
1 parent
2d49212b08
commit
1ff293f41f
44 files changed
+2024
-316
No files matched your search
@@ -4,11 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Model;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
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.
|
||||
@@ -128,4 +132,240 @@ class BookingDtoTest extends TestCase
|
||||
$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 testInternalAgencyBookingNeverHasTheFirstParticipantAsApplicant(): void
|
||||
{
|
||||
$dto = $this->createCreateModeDto();
|
||||
$dto->agencyCode = AgencyLoader::INTERNAL_AGENCY_CODE;
|
||||
|
||||
$this->assertFalse(
|
||||
$dto->isFirstParticipantApplicant(),
|
||||
'Their applicant is a staff member who never travels'
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private function createAddress(): Address
|
||||
{
|
||||
$address = new Address();
|
||||
$address->street = 'Probeweg 7';
|
||||
$address->postCode = '99999';
|
||||
$address->city = 'MusterOrt';
|
||||
$address->country = 'D';
|
||||
|
||||
return $address;
|
||||
}
|
||||
}
|
||||
@@ -205,46 +205,34 @@ class ParticipantDtoTest extends TestCase
|
||||
return $insurance;
|
||||
}
|
||||
|
||||
public function testRequiresOwnEmailIsFalseForNonApplicantChild(): void
|
||||
public function testIsChildIsTrueBelowTheAgeThreshold(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 1;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$this->assertFalse($participant->requiresOwnEmail());
|
||||
$this->assertTrue($participant->isChild());
|
||||
}
|
||||
|
||||
public function testRequiresOwnEmailIsTrueForNonApplicantAdult(): void
|
||||
public function testIsChildIsFalseForAnAdult(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 1;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
|
||||
$this->assertTrue($participant->requiresOwnEmail());
|
||||
$this->assertFalse($participant->isChild());
|
||||
}
|
||||
|
||||
public function testRequiresOwnEmailIsTrueForTheApplicantEvenAsAChild(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 0;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$this->assertTrue(
|
||||
$participant->requiresOwnEmail(),
|
||||
'The applicant is the booking contact and always has to be reachable'
|
||||
);
|
||||
}
|
||||
|
||||
public function testRequiresOwnEmailTreatsUnknownAgeAsAdult(): void
|
||||
public function testIsChildTreatsUnknownAgeAsAdult(): void
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 1;
|
||||
$participant->dateOfBirth = null;
|
||||
|
||||
$this->assertTrue($participant->requiresOwnEmail());
|
||||
$this->assertFalse($participant->isChild(), 'An unknown date of birth counts as an adult');
|
||||
}
|
||||
|
||||
public function testRequiresOwnEmailBoundaryAtChildAgeThreshold(): void
|
||||
public function testIsChildBoundaryAtAgeThreshold(): void
|
||||
{
|
||||
$exactlyThreshold = new ParticipantDto();
|
||||
$exactlyThreshold->index = 1;
|
||||
@@ -257,7 +245,7 @@ class ParticipantDtoTest extends TestCase
|
||||
->modify(sprintf('-%d years', ParticipantDto::CHILD_AGE_THRESHOLD))
|
||||
->modify('+1 day');
|
||||
|
||||
$this->assertTrue($exactlyThreshold->requiresOwnEmail());
|
||||
$this->assertFalse($oneDayShort->requiresOwnEmail());
|
||||
$this->assertFalse($exactlyThreshold->isChild());
|
||||
$this->assertTrue($oneDayShort->isChild());
|
||||
}
|
||||
}
|
||||
@@ -877,7 +877,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
* available ski pass. Ski pass validation depends on the travel actually offering
|
||||
* passes, so a travel without them is a distinct fixture, not the default.
|
||||
*/
|
||||
public function testChildWithoutEmailPassesStrictValidation(): void
|
||||
public function testChildWithoutEmailFailsStrictValidation(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
@@ -886,10 +886,14 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame([], $this->emailRequiredMessages($violations));
|
||||
$this->assertSame(
|
||||
['Bitte angeben'],
|
||||
$this->emailRequiredMessages($violations),
|
||||
'BusPro requires a Mail-Adresse for every participant, children included'
|
||||
);
|
||||
}
|
||||
|
||||
public function testChildWithNullEmailPassesStrictValidation(): void
|
||||
public function testChildWithNullEmailFailsStrictValidation(): void
|
||||
{
|
||||
$child = $this->createChildParticipant('');
|
||||
$child->email = null;
|
||||
@@ -901,7 +905,23 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame([], $this->emailRequiredMessages($violations));
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testChildSharingTheApplicantsEmailPassesStrictValidation(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
$this->createChildParticipant('[email protected]'),
|
||||
]);
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$this->emailRequiredMessages($violations),
|
||||
'A child inherits the applicant address, which BusPro accepts'
|
||||
);
|
||||
}
|
||||
|
||||
public function testChildWithMalformedEmailStillFailsEmailFormatValidation(): void
|
||||
@@ -984,7 +1004,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testDependentOneDayShortOfSixteenDoesNotRequireEmail(): void
|
||||
public function testDependentOneDayShortOfSixteenStillRequiresEmail(): void
|
||||
{
|
||||
$participant = $this->createAdultParticipant('');
|
||||
$participant->dateOfBirth = (new \DateTimeImmutable('today'))->modify('-16 years')->modify('+1 day');
|
||||
@@ -996,7 +1016,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame([], $this->emailRequiredMessages($violations));
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user