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
@@ -21,8 +21,9 @@ class ParticipantDto
|
||||
/**
|
||||
* Age below which a participant counts as a child.
|
||||
*
|
||||
* Children may share an email address with an adult and are not required to have one of
|
||||
* their own; both rules go through isChild(), so this is the only place the age lives.
|
||||
* Children may share an email address with an adult, and have the applicant's address
|
||||
* pre-filled for them; both rules go through isChild(), so this is the only place the
|
||||
* age lives.
|
||||
*/
|
||||
public const CHILD_AGE_THRESHOLD = 16;
|
||||
|
||||
@@ -79,14 +80,11 @@ class ParticipantDto
|
||||
#[Assert\NotNull(message: 'Bitte angeben', groups: ['strict_required'])]
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
|
||||
// Requiredness is delegated to requiresOwnEmail() so the constraint and the field state
|
||||
// conditions cannot state the rule differently.
|
||||
// Every participant needs an email address: BusPro rejects the step-3 Anfrage with 650
|
||||
// otherwise, children included. Under-16s have the applicant's address pre-filled by
|
||||
// ParticipantEmailFieldHandler and may override it, but may not leave it blank.
|
||||
#[Assert\Email(message: 'Bitte eine gültige E-Mail Adresse angeben', mode: 'strict', groups: ['booking_edit', 'booking_create'])]
|
||||
#[Assert\When(
|
||||
expression: 'this.requiresOwnEmail()',
|
||||
constraints: [new Assert\NotBlank(message: 'Bitte angeben')],
|
||||
groups: ['strict_required']
|
||||
)]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['strict_required'])]
|
||||
public ?string $email = null;
|
||||
|
||||
public ?string $mobile = null;
|
||||
@@ -369,22 +367,6 @@ class ParticipantDto
|
||||
*
|
||||
* @return bool True if participant is under the age threshold, false otherwise or if age unknown
|
||||
*/
|
||||
/**
|
||||
* Decides whether this participant has to supply an email address of their own.
|
||||
*
|
||||
* The applicant is the booking's contact and always has to be reachable. Everyone else only
|
||||
* needs an address once they are old enough to have one, so children are exempt. An unknown
|
||||
* date of birth counts as an adult, following isChild().
|
||||
*
|
||||
* Single source for the rule: the Assert\When on $email enforces it, and the 'required'
|
||||
* field state conditions in CreateFieldStateProvider/EditFieldStateProvider render the
|
||||
* matching mandatory marker.
|
||||
*/
|
||||
public function requiresOwnEmail(): bool
|
||||
{
|
||||
return $this->isApplicant() || false === $this->isChild();
|
||||
}
|
||||
|
||||
public function isChild(int $ageThreshold = self::CHILD_AGE_THRESHOLD): bool
|
||||
{
|
||||
$age = $this->getAge();
|
||||
@@ -495,28 +477,73 @@ class ParticipantDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the participant has a complete address when required.
|
||||
* Validates that this person is usable as the booking's applicant.
|
||||
*
|
||||
* In regular create/edit submission flows this only applies to participant 0.
|
||||
* The edit overview preflight checks address fields separately for the applicant only.
|
||||
* Runs on its own in the 'booking_applicant' group, which only the applicant form activates,
|
||||
* the way 'applicant_address' already scopes the address NotNull above. BookingDto delegates
|
||||
* to the same body for the step-2 submit, so the rule has one home.
|
||||
*/
|
||||
#[Assert\Callback(groups: ['strict_required'])]
|
||||
public function validateApplicantAddress(ExecutionContextInterface $context): void
|
||||
#[Assert\Callback(groups: ['booking_applicant'])]
|
||||
public function validateAsApplicant(ExecutionContextInterface $context): void
|
||||
{
|
||||
// Only validate applicant's address in regular create/edit submission flows.
|
||||
if (0 !== $this->index) {
|
||||
// The applicant record is the only ParticipantDto without an index. Travellers carry one,
|
||||
// and BookingDto cascades the 'booking_applicant' group into $participants as well, so
|
||||
// without this guard every traveller would be held to the applicant's rules - an adult
|
||||
// address and a minimum age of 18.
|
||||
if (null !== $this->index) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validateAddressFields($context);
|
||||
$this->validateApplicantCompleteness($context);
|
||||
}
|
||||
|
||||
private function validateAddressFields(ExecutionContextInterface $context): void
|
||||
/**
|
||||
* Shared body. $pathPrefix places the violations in the caller's object graph; it is not a
|
||||
* callback itself, because Symfony passes the constraint payload as the second argument.
|
||||
*/
|
||||
public function validateApplicantCompleteness(ExecutionContextInterface $context, string $pathPrefix = ''): void
|
||||
{
|
||||
foreach (['firstName', 'lastName', 'gender', 'nationality', 'email', 'mobile'] as $field) {
|
||||
$value = $this->$field;
|
||||
if (null === $value || '' === trim($value)) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath($pathPrefix.$field)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
$this->validateAddressFields($context, $pathPrefix);
|
||||
|
||||
if (null === $this->dateOfBirth) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath($pathPrefix.'dateOfBirth')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$age = $this->getAge();
|
||||
if (null !== $age && 18 > $age) {
|
||||
$context->buildViolation('Die anmeldende Person muss volljährig sein.')
|
||||
->atPath($pathPrefix.'dateOfBirth')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that this person has a complete address.
|
||||
*
|
||||
* Only the booking's contact needs one: BusPro does not require an anschrift from an
|
||||
* ordinary participant (measured on 2036, an empty anschrift passes). Deciding who the
|
||||
* contact is needs the booking, so the callers own the gate and this method only ever
|
||||
* runs the field checks. $pathPrefix places the violations in the caller's object graph.
|
||||
*/
|
||||
public function validateAddressFields(ExecutionContextInterface $context, string $pathPrefix = ''): void
|
||||
{
|
||||
// Address object is required for the validated participant
|
||||
if (null === $this->address) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath('address')
|
||||
->atPath($pathPrefix.'address')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
@@ -525,25 +552,25 @@ class ParticipantDto
|
||||
// Validate address subfields
|
||||
if (null === $this->address->street || u($this->address->street)->trim()->isEmpty()) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath('address.street')
|
||||
->atPath($pathPrefix.'address.street')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->address->postCode || u($this->address->postCode)->trim()->isEmpty()) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath('address.postCode')
|
||||
->atPath($pathPrefix.'address.postCode')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->address->city || u($this->address->city)->trim()->isEmpty()) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath('address.city')
|
||||
->atPath($pathPrefix.'address.city')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->address->country || u($this->address->country)->trim()->isEmpty()) {
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath('address.country')
|
||||
->atPath($pathPrefix.'address.country')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user