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
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\BookingApplicantTravelsType;
|
||||
use App\Form\Model\BookingDto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\Forms;
|
||||
|
||||
class BookingApplicantTravelsTypeTest extends TestCase
|
||||
{
|
||||
public function testBooleanChoicesRenderAsRadioValues(): void
|
||||
{
|
||||
$form = $this->createForm($this->createBookingDto());
|
||||
|
||||
$values = [];
|
||||
foreach ($form->get('applicantTravels')->createView()->vars['choices'] as $choice) {
|
||||
$values[] = $choice->value;
|
||||
}
|
||||
|
||||
// Symfony derives these from the booleans on its own, so the type needs no choice_value.
|
||||
// Pinned because the whole toggle silently stops working if that ever changes.
|
||||
$this->assertSame(['1', '0'], $values);
|
||||
}
|
||||
|
||||
public function testSubmittingTheSplitChoiceFlipsTheFlag(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$form = $this->createForm($bookingDto);
|
||||
|
||||
$form->submit(['applicantTravels' => '0']);
|
||||
|
||||
$this->assertTrue($form->isSynchronized());
|
||||
$this->assertTrue($form->isValid());
|
||||
$this->assertFalse($bookingDto->applicantTravels);
|
||||
}
|
||||
|
||||
public function testSubmittingTheTravellingChoiceRestoresTheFlag(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->applicantTravels = false;
|
||||
$form = $this->createForm($bookingDto);
|
||||
|
||||
$form->submit(['applicantTravels' => '1']);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
$this->assertTrue($bookingDto->applicantTravels);
|
||||
}
|
||||
|
||||
public function testCurrentChoiceIsPreselected(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->applicantTravels = false;
|
||||
|
||||
$view = $this->createForm($bookingDto)->get('applicantTravels')->createView();
|
||||
|
||||
$this->assertSame('0', $view->vars['value']);
|
||||
}
|
||||
|
||||
public function testNoValidationRunsOnTheToggle(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
// Deliberately incomplete: flipping the radio must not report participant gaps.
|
||||
$bookingDto->participants = [];
|
||||
|
||||
$form = $this->createForm($bookingDto);
|
||||
$form->submit(['applicantTravels' => '0']);
|
||||
|
||||
$this->assertCount(0, $form->getErrors(true));
|
||||
}
|
||||
|
||||
private function createBookingDto(): BookingDto
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->dateFrom = new \DateTimeImmutable('2026-06-01');
|
||||
|
||||
return new BookingDto($travel, 1);
|
||||
}
|
||||
|
||||
/** @return \Symfony\Component\Form\FormInterface<BookingDto> */
|
||||
private function createForm(BookingDto $bookingDto): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
return Forms::createFormFactory()->create(BookingApplicantTravelsType::class, $bookingDto);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,57 +74,55 @@ class CreateFieldStateProviderTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsNotRequiredForNonApplicantChild(): void
|
||||
public function testEmailIsRequiredForEveryParticipantIncludingChildren(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
|
||||
$this->assertFalse(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'A child travelling with an applicant does not need an email address of their own'
|
||||
$this->assertArrayNotHasKey(
|
||||
'required',
|
||||
$this->provider->getFieldState('email', $bookingDto, 1),
|
||||
'Requiredness is the form type base option now that it is unconditional'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredForNonApplicantAdult(): void
|
||||
public function testMobileIsRequiredForTheApplicantOnly(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('1990-01-01'));
|
||||
|
||||
$this->assertTrue($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
$this->assertTrue($this->provider->getFieldState('mobile', $bookingDto, 0)['required']);
|
||||
$this->assertFalse($this->provider->getFieldState('mobile', $bookingDto, 1)['required']);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredForTheApplicantEvenAsAChild(): void
|
||||
public function testMobileAndAddressAreNotRequiredOfParticipantZeroWhenTheApplicantDoesNotTravel(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$bookingDto->participants[0]->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
$bookingDto->applicantTravels = false;
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 0)['required'],
|
||||
'The applicant is the booking contact and always has to be reachable'
|
||||
$this->assertFalse(
|
||||
$this->provider->getFieldState('mobile', $bookingDto, 0)['required'],
|
||||
'BusPro does not ask a telefonmobil of an ordinary participant'
|
||||
);
|
||||
$this->assertFalse(
|
||||
$this->provider->getFieldState('address', $bookingDto, 0)['required'],
|
||||
'BusPro accepts an empty anschrift for an ordinary participant'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredWhenDateOfBirthIsUnknown(): void
|
||||
public function testParticipantZeroIsEditableWhenTheApplicantDoesNotTravel(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$bookingDto->participants[1]->dateOfBirth = null;
|
||||
$bookingDto->participants[0]->personId = 4711;
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'An unknown age counts as an adult'
|
||||
$this->provider->shouldRenderAsStaticText('firstName', $bookingDto, 0),
|
||||
'A logged-in applicant who travels keeps their prefilled data locked'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailRequirementFlipsWhenDateOfBirthChanges(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$dependent = $bookingDto->participants[1];
|
||||
$bookingDto->applicantTravels = false;
|
||||
|
||||
$this->assertFalse($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
|
||||
$dependent->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'Correcting a child date of birth to an adult one must reinstate the requirement'
|
||||
$this->assertFalse(
|
||||
$this->provider->shouldRenderAsStaticText('firstName', $bookingDto, 0),
|
||||
'Once the applicant does not travel, participant[0] is an ordinary traveller'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,14 @@ class EditFieldStateProviderTest extends TestCase
|
||||
$this->assertTrue($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
}
|
||||
|
||||
public function testEmailIsNotRequiredForNonApplicantChild(): void
|
||||
public function testEmailIsRequiredForNonApplicantChild(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto(new \DateTimeImmutable('2015-01-01'), false);
|
||||
|
||||
$this->assertFalse($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'BusPro requires a Mail-Adresse for every participant, children included'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsNotRequiredForInternalAgencyBookingsEvenForAdults(): void
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\ParticipantDateOfBirthFieldHandler;
|
||||
use App\Form\Service\ParticipantEmailFieldHandler;
|
||||
use App\Form\Service\ParticipantFieldHandlerRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
class ParticipantEmailFieldHandlerTest extends TestCase
|
||||
{
|
||||
private ParticipantEmailFieldHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->handler = new ParticipantEmailFieldHandler();
|
||||
}
|
||||
|
||||
public function testChildWithoutEmailInheritsTheApplicants(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
|
||||
$this->handler->processField(['email' => ''], $bookingDto, 1);
|
||||
|
||||
$this->assertSame('[email protected]', $bookingDto->participants[1]->email);
|
||||
}
|
||||
|
||||
public function testChildKeepsAnEmailThatWasTypedIn(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
|
||||
$this->handler->processField(['email' => '[email protected]'], $bookingDto, 1);
|
||||
|
||||
$this->assertSame(
|
||||
'[email protected]',
|
||||
$bookingDto->participants[1]->email,
|
||||
'The pre-filled address stays overridable'
|
||||
);
|
||||
}
|
||||
|
||||
public function testAdultWithoutEmailDoesNotInherit(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->participants[1]->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
|
||||
$this->handler->processField(['email' => ''], $bookingDto, 1);
|
||||
|
||||
$this->assertNull(
|
||||
$bookingDto->participants[1]->email,
|
||||
'An adult supplies their own address; NotBlank reports the gap'
|
||||
);
|
||||
}
|
||||
|
||||
public function testApplicantDoesNotInheritFromThemselves(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->participants[0]->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$this->handler->processField(['email' => ''], $bookingDto, 0);
|
||||
|
||||
$this->assertNull($bookingDto->participants[0]->email);
|
||||
}
|
||||
|
||||
public function testChildAtIndexZeroInheritsWhenTheApplicantDoesNotTravel(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->applicantTravels = false;
|
||||
$bookingDto->applicant = $this->createParticipant('[email protected]', '1981-03-16');
|
||||
$bookingDto->participants[0]->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$this->handler->processField(['email' => ''], $bookingDto, 0);
|
||||
|
||||
$this->assertSame('[email protected]', $bookingDto->participants[0]->email);
|
||||
}
|
||||
|
||||
public function testHandlerRunsAfterDateOfBirth(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
['dateOfBirth'],
|
||||
$this->handler->getDependencies(),
|
||||
'Whether someone counts as a child is only known once their DOB is processed'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler runs in PRE_SUBMIT and writes to the DTO, but the form then binds the submitted
|
||||
* data over it. Only fields the registry syncs back survive, so the pre-filled address has to
|
||||
* reappear in the returned data - otherwise the blank wins and the field renders empty.
|
||||
*/
|
||||
public function testPreFilledEmailIsSyncedBackIntoTheSubmittedData(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$registry = new ParticipantFieldHandlerRegistry([
|
||||
new ParticipantDateOfBirthFieldHandler(new NullLogger()),
|
||||
new ParticipantEmailFieldHandler(),
|
||||
]);
|
||||
|
||||
$synced = $registry->processFieldsForParticipantAndSync(['email' => ''], $bookingDto, 1);
|
||||
|
||||
$this->assertSame('[email protected]', $bookingDto->participants[1]->email);
|
||||
$this->assertSame(
|
||||
'[email protected]',
|
||||
$synced['email'],
|
||||
'Without the sync the form binds the blank back over the pre-filled address'
|
||||
);
|
||||
}
|
||||
|
||||
private function createBookingDto(): BookingDto
|
||||
{
|
||||
$bookingDto = new BookingDto(new Travel(), 1);
|
||||
$bookingDto->participants = [
|
||||
$this->createParticipant('[email protected]', '1980-01-01'),
|
||||
$this->createParticipant('', '2015-01-01'),
|
||||
];
|
||||
$bookingDto->participants[0]->index = 0;
|
||||
$bookingDto->participants[1]->index = 1;
|
||||
$bookingDto->participants[1]->email = null;
|
||||
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
private function createParticipant(string $email, string $dateOfBirth): ParticipantDto
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->email = '' !== $email ? $email : null;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth);
|
||||
|
||||
return $participant;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user