feat: make email optional for under-16 participants except applicant
This commit is contained in:
1 parent
55a9189284
commit
1ec9c972cd
18 files changed
+435
-336
No files matched your search
@@ -302,7 +302,6 @@ services:
|
||||
- 'App\Form\Service\ParticipantDropOffFieldHandler'
|
||||
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
|
||||
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
|
||||
- 'App\Form\Service\ParticipantEmailFieldHandler'
|
||||
# Complex handlers with dependencies - use service references
|
||||
- '@App\Form\Service\ParticipantDateOfBirthFieldHandler'
|
||||
- '@App\Form\Service\ParticipantTransportationDiscountReplacementFieldHandler'
|
||||
|
||||
@@ -167,11 +167,19 @@ fails on `Telefon-Mobil` and `Nationalität` because its `Vorgang_Details` recor
|
||||
`telefonprivat` and no `nationalitaet`.
|
||||
|
||||
**Participants are held to less than the Anmelder** (measured 2026-09-16 on 2036, travel 12500, from a
|
||||
real step-3 `Anfrage`). Once a room is present, every `teilnehmer` needs a `Mail-Adresse`, children
|
||||
included: without `kommunikation` the answer is `650 … 1.Teilnehmer-Mail-Adresse, 2.Teilnehmer-Mail-Adresse`.
|
||||
`telefonmobil` and an empty `anschrift` pass for participants. An email identical to the Anmelder's is
|
||||
accepted. The field list above (Strasse, Plz, Ort, Telefon-Mobil for every participant) was measured on
|
||||
the placeholder records and is stricter than what BusPro applies to ordinary participants.
|
||||
real step-3 `Anfrage`). `telefonmobil` and an empty `anschrift` pass for participants. The field list
|
||||
above (Strasse, Plz, Ort, Telefon-Mobil for every participant) was measured on the placeholder records and
|
||||
is stricter than what BusPro applies to ordinary participants.
|
||||
|
||||
**The participant `Mail-Adresse` is BusPro configuration, not a fixed rule.** Until 2026-10-13 every
|
||||
`teilnehmer` needed one once a room was present, children included: without it the answer was
|
||||
`650 … 1.Teilnehmer-Mail-Adresse, 2.Teilnehmer-Mail-Adresse`, while an email identical to the Anmelder's
|
||||
was accepted. On 2026-10-15 the requirement was switched off on the test system, and the same requests
|
||||
(no `email`, no `kommunikation`, an empty `<email/>`, adults and children, with and without a travelling
|
||||
Anmelder) all pass with an unchanged price. BusPro now accepts a participant without an email at any age,
|
||||
so the rule MyE&P applies (required from 16, and for the applicant at any age) is enforced only by
|
||||
`ParticipantDto` and `BookingDto` validation. Production needs the same configuration before under-16s
|
||||
without an email can be booked there.
|
||||
|
||||
**The Anmelder does not have to travel.** An `<anmelder>` that matches no `teilnehmer` (a parent booking
|
||||
for their children) returns `möglich` with the same price as the control where the Anmelder is
|
||||
|
||||
@@ -285,7 +285,7 @@ class BookingPayloadBuilder
|
||||
$participantData = [
|
||||
'@id' => $index + 1,
|
||||
'status' => self::DEFAULT_PARTICIPANT_STATUS,
|
||||
] + $this->buildPersonalDataPayload($participant, $applicantSource->email, true);
|
||||
] + $this->buildPersonalDataPayload($participant, true);
|
||||
|
||||
// Add body dimensions
|
||||
if (null !== $participant->height) {
|
||||
@@ -387,16 +387,12 @@ class BookingPayloadBuilder
|
||||
* Maps one person's personal data onto the payload shape shared by anmelder and teilnehmer.
|
||||
*
|
||||
* @param ParticipantDto $person The applicant or participant to map
|
||||
* @param string|null $fallbackEmail Used when the person has none of their own. BusPro
|
||||
* requires a Mail-Adresse for every participant once
|
||||
* a room is present and rejects the Anfrage with 650
|
||||
* otherwise, but accepts one shared with the Anmelder
|
||||
* @param bool $alwaysIncludeAddress Participants always carry the anschrift structure
|
||||
* even when empty; the anmelder only when set
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildPersonalDataPayload(ParticipantDto $person, ?string $fallbackEmail = null, bool $alwaysIncludeAddress = false): array
|
||||
private function buildPersonalDataPayload(ParticipantDto $person, bool $alwaysIncludeAddress = false): array
|
||||
{
|
||||
$data = [
|
||||
'name' => $person->lastName,
|
||||
@@ -421,9 +417,9 @@ class BookingPayloadBuilder
|
||||
];
|
||||
}
|
||||
|
||||
$email = null !== $person->email && '' !== trim($person->email)
|
||||
? $person->email
|
||||
: $fallbackEmail;
|
||||
// A participant may come without an email (under-16s, see ParticipantDto::$email), and
|
||||
// BusPro accepts that, so a blank is sent as a blank rather than borrowing the applicant's.
|
||||
$email = null !== $person->email && '' !== trim($person->email) ? $person->email : null;
|
||||
|
||||
if (null !== $email || null !== $person->mobile) {
|
||||
$data['kommunikation'] = [];
|
||||
|
||||
@@ -210,9 +210,7 @@ class BookingParticipantType extends AbstractType
|
||||
if ($this->fieldStateProvider->shouldIncludeField('email', $bookingDto, $participantIndex)) {
|
||||
$form->add('email', EmailType::class, $this->mergeFieldState([
|
||||
'label' => 'E-Mail',
|
||||
// Required for every participant (BPN 650); EditFieldStateProvider clears it
|
||||
// again for internal agency bookings, where staff may leave data blank.
|
||||
'required' => true,
|
||||
'required' => false,
|
||||
'property_path' => 'participant.email',
|
||||
'attr' => [
|
||||
'autocomplete' => 'leave-me-alone-chrome',
|
||||
|
||||
@@ -213,6 +213,50 @@ class BookingDto
|
||||
$this->applicantTravels = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the participant at $index has to supply an email address.
|
||||
*
|
||||
* Everyone does except an under-16 who is not the applicant; BusPro itself accepts a blank
|
||||
* for any participant. Validation enforces this in two parts, the Assert\When on
|
||||
* ParticipantDto::$email (age) and validateFirstParticipantApplicant() (the applicant), and
|
||||
* this is the same rule in one place for the field state. A participant that does not exist
|
||||
* yet counts as needing one, matching the "unknown counts as an adult" stance of isChild().
|
||||
*/
|
||||
public function requiresParticipantEmail(int $index): bool
|
||||
{
|
||||
$participant = $this->getParticipant($index);
|
||||
|
||||
if (null === $participant || false === $participant->isChild()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 0 === $index && true === $this->isFirstParticipantApplicant();
|
||||
}
|
||||
|
||||
/**
|
||||
* The applicant's email address, wherever the applicant is held.
|
||||
*
|
||||
* - participant[0] where they are the applicant
|
||||
* - create, split: the separate applicant block
|
||||
* - edit, split: the loaded booking's anmelder, since $applicant is only filled in create mode
|
||||
*/
|
||||
public function getApplicantEmail(): ?string
|
||||
{
|
||||
if (true === $this->isFirstParticipantApplicant()) {
|
||||
$email = $this->getParticipant(0)?->email;
|
||||
} elseif (self::MODE_CREATE === $this->getMode()) {
|
||||
$email = $this->applicant?->email;
|
||||
} else {
|
||||
$email = $this->booking?->applicant?->communication->email;
|
||||
}
|
||||
|
||||
if (null === $email || '' === trim($email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether participant[0] is also the applicant.
|
||||
*
|
||||
@@ -311,18 +355,19 @@ class BookingDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires a complete address from participant[0] only where they are the applicant.
|
||||
* Requires a complete address and an email from participant[0] only where they are the applicant.
|
||||
*
|
||||
* BusPro does not require an anschrift from an ordinary participant, so on a split booking
|
||||
* the traveller at index 0 is not asked for one.
|
||||
* BusPro asks neither of an ordinary participant, so on a split booking the traveller at index 0
|
||||
* is not asked for an address, and an under-16 there may leave the email blank.
|
||||
*/
|
||||
#[Assert\Callback(groups: ['strict_required'])]
|
||||
public function validateFirstParticipantAddress(ExecutionContextInterface $context): void
|
||||
public function validateFirstParticipantApplicant(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (false === $this->isFirstParticipantApplicant()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getParticipant(0)?->validateApplicantEmail($context, 'participants[0].');
|
||||
$this->getParticipant(0)?->validateAddressFields($context, 'participants[0].');
|
||||
}
|
||||
|
||||
|
||||
@@ -80,11 +80,17 @@ class ParticipantDto
|
||||
#[Assert\NotNull(message: 'Bitte angeben', groups: ['strict_required'])]
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
|
||||
// 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.
|
||||
// BusPro accepts a participant without an email address, so this is the only gate. Under-16s
|
||||
// may leave it blank, everyone else needs one. An under-16 applicant needs one as well; that
|
||||
// case depends on the booking and is enforced by BookingDto::validateFirstParticipantApplicant()
|
||||
// and ParticipantEditDto::validateApplicant(). It stays a property constraint so the missing-field
|
||||
// labels keep their order (see BookingEditPreFlightChecker).
|
||||
#[Assert\Email(message: 'Bitte eine gültige E-Mail Adresse angeben', mode: 'strict', groups: ['booking_edit', 'booking_create'])]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['strict_required'])]
|
||||
#[Assert\When(
|
||||
expression: 'not this.isChild()',
|
||||
constraints: [new Assert\NotBlank(message: 'Bitte angeben')],
|
||||
groups: ['strict_required'],
|
||||
)]
|
||||
public ?string $email = null;
|
||||
|
||||
public ?string $mobile = null;
|
||||
@@ -530,6 +536,28 @@ class ParticipantDto
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that this person, as the booking's contact, has an email address.
|
||||
*
|
||||
* The property constraint on $email already holds everyone aged 16 or older (and anyone
|
||||
* without a date of birth) to it, so this only adds the under-16 contact and never reports
|
||||
* the same field twice. Like validateAddressFields(), the callers decide who the contact is.
|
||||
*/
|
||||
public function validateApplicantEmail(ExecutionContextInterface $context, string $pathPrefix = ''): void
|
||||
{
|
||||
if (false === $this->isChild()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->email && false === u($this->email)->trim()->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context->buildViolation('Bitte angeben')
|
||||
->atPath($pathPrefix.'email')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that this person has a complete address.
|
||||
*
|
||||
|
||||
@@ -93,13 +93,13 @@ class ParticipantEditDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires a complete address from the participant form's applicant only.
|
||||
* Requires a complete address and an email from the participant form's applicant only.
|
||||
*
|
||||
* Mirrors BookingDto::validateFirstParticipantAddress() for the per-participant form, which
|
||||
* Mirrors BookingDto::validateFirstParticipantApplicant() for the per-participant form, which
|
||||
* binds this wrapper rather than the booking.
|
||||
*/
|
||||
#[Assert\Callback(groups: ['strict_required'])]
|
||||
public function validateApplicantAddress(ExecutionContextInterface $context): void
|
||||
public function validateApplicant(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (false === $this->participant->isApplicant()) {
|
||||
return;
|
||||
@@ -109,6 +109,7 @@ class ParticipantEditDto
|
||||
return;
|
||||
}
|
||||
|
||||
$this->participant->validateApplicantEmail($context, 'participant.');
|
||||
$this->participant->validateAddressFields($context, 'participant.');
|
||||
}
|
||||
|
||||
@@ -117,7 +118,11 @@ class ParticipantEditDto
|
||||
*
|
||||
* Exempt from validation:
|
||||
* - The applicant: can share an email with dependents they're booking for
|
||||
* - Children (under 16): Can share email addresses with adults
|
||||
* - Children (under 16): Can share email addresses with adults, or have none at all
|
||||
*
|
||||
* Where the applicant does not travel they are not among the participants, so their address
|
||||
* is checked separately: an adult traveller may not reuse it, just as on an ordinary booking
|
||||
* an adult may not reuse participant[0]'s.
|
||||
*
|
||||
* Email comparison is case-insensitive and whitespace is normalized.
|
||||
*/
|
||||
@@ -140,7 +145,7 @@ class ParticipantEditDto
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if email is null or empty (handled by @Email and @NotBlank constraints)
|
||||
// Nothing to compare. Whether a blank is allowed is the property constraint's business.
|
||||
if (null === $this->participant->email || u($this->participant->email)->trim()->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -178,5 +183,17 @@ class ParticipantEditDto
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $this->bookingContext->isFirstParticipantApplicant()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$applicantEmail = $this->bookingContext->getApplicantEmail();
|
||||
|
||||
if (null !== $applicantEmail && $normalizedEmail === u($applicantEmail)->lower()->toString()) {
|
||||
$context->buildViolation('Diese E-Mail Adresse wird bereits von der anmeldenden Person verwendet')
|
||||
->atPath('participant.email')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that mirrors BookingDto::requiresParticipantEmail() into the field state layer.
|
||||
*
|
||||
* It deliberately holds no logic of its own, so the mandatory marker rendered next to the field
|
||||
* is derived from the same method that describes what validation enforces, rather than from a
|
||||
* second, independently maintained composition of age and applicant conditions.
|
||||
*/
|
||||
class RequiresOwnEmailCondition implements FieldConditionInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates whether the participant has to supply an email address of their own.
|
||||
*
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (unused)
|
||||
*
|
||||
* @return bool True if the participant needs their own email address
|
||||
*/
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return $bookingDto->requiresParticipantEmail($participantIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* The rule reads the date of birth, so the state has to be recalculated when it changes.
|
||||
*
|
||||
* @return string[] Array containing 'dateOfBirth'
|
||||
*/
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return ['dateOfBirth'];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Checks if the participant needs an email address of their own';
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use App\Form\Service\Condition\FirstParticipantReadOnlyCondition;
|
||||
use App\Form\Service\Condition\MultipleParticipantsCondition;
|
||||
use App\Form\Service\Condition\RentalInsuranceAvailableCondition;
|
||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||
use App\Form\Service\Condition\RequiresOwnEmailCondition;
|
||||
use App\Form\Service\Condition\RoomSelectionCondition;
|
||||
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||
use App\Form\Service\Condition\SingleRoomTypeCondition;
|
||||
@@ -112,10 +113,9 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
'static_text' => $firstParticipantReadOnlyCondition,
|
||||
];
|
||||
|
||||
// Every participant needs an email address, children included (BPN 650), so
|
||||
// requiredness is the form type's base option and not a condition here.
|
||||
$this->fieldStateConditions['email'] = [
|
||||
'static_text' => $firstParticipantReadOnlyCondition,
|
||||
'required' => new RequiresOwnEmailCondition(),
|
||||
];
|
||||
|
||||
// Mobile field: static text for first participant (when read-only). Mandatory only for the
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Form\Service\Condition\PersonalDataMutabilityCondition;
|
||||
use App\Form\Service\Condition\PickupsMutabilityCondition;
|
||||
use App\Form\Service\Condition\RentalInsuranceAvailableCondition;
|
||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||
use App\Form\Service\Condition\RequiresOwnEmailCondition;
|
||||
use App\Form\Service\Condition\RoomSelectionCondition;
|
||||
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
||||
use App\Form\Service\Condition\SkiPassSelectionCondition;
|
||||
@@ -92,7 +93,10 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
||||
// data optional throughout.
|
||||
$this->fieldStateConditions['email'] = [
|
||||
'static_text' => $firstParticipantReadOnlyCondition,
|
||||
'required' => CompositeCondition::not($internalAgencyBookingCondition),
|
||||
'required' => CompositeCondition::and(
|
||||
CompositeCondition::not($internalAgencyBookingCondition),
|
||||
new RequiresOwnEmailCondition(),
|
||||
),
|
||||
];
|
||||
|
||||
// Address fields - render as static text if first participant in non-internal agency OR participant not mutable
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
* Handles the email field, pre-filling the applicant's address for under-16 participants.
|
||||
*
|
||||
* BusPro requires a Mail-Adresse for every participant once a room is present and rejects the
|
||||
* step-3 Anfrage with 650 otherwise, children included. It does accept an address identical to
|
||||
* the Anmelder's, so children inherit the applicant's rather than being asked for one they
|
||||
* typically do not have. The value stays editable and stays mandatory. This happens in the
|
||||
* create flow only; in edit mode the handler just stores what was submitted.
|
||||
*
|
||||
* The copy only ever fills a blank, so an address somebody typed is never overwritten. A later
|
||||
* correction to the applicant's own address therefore does not propagate to children who already
|
||||
* inherited the previous one.
|
||||
*
|
||||
* Depends on dateOfBirth: whether a participant counts as a child is only known once their date
|
||||
* of birth has been processed.
|
||||
*/
|
||||
class ParticipantEmailFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'email';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return ['dateOfBirth'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
if (false === $participant instanceof ParticipantDto) {
|
||||
return;
|
||||
}
|
||||
|
||||
$submitted = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
if (true === is_string($submitted)) {
|
||||
$participant->email = '' !== trim($submitted) ? trim($submitted) : null;
|
||||
}
|
||||
|
||||
$this->applyApplicantEmail($bookingDto, $participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the applicant's address onto a child who has none of their own.
|
||||
*/
|
||||
public function applyApplicantEmail(BookingDto $bookingDto, ParticipantDto $participant): void
|
||||
{
|
||||
// Create mode only. An existing booking already carries whatever BusPro accepted, a
|
||||
// blank may be intended where personal data is optional, and participant[0] is not
|
||||
// reliably the applicant there.
|
||||
if (BookingDto::MODE_CREATE !== $bookingDto->getMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (false === $participant->isChild()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$applicantEmail = $this->resolveApplicantEmail($bookingDto);
|
||||
|
||||
if (null === $applicantEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The applicant themselves never inherits, and on an ordinary booking that is participant[0].
|
||||
if (true === $participant->isApplicant() && true === $bookingDto->applicantTravels) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $participant->email || '' === trim($participant->email)) {
|
||||
$participant->email = $applicantEmail;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveApplicantEmail(BookingDto $bookingDto): ?string
|
||||
{
|
||||
$applicant = $bookingDto->applicantTravels
|
||||
? ($bookingDto->participants[0] ?? null)
|
||||
: $bookingDto->applicant;
|
||||
|
||||
$email = $applicant?->email;
|
||||
|
||||
if (null === $email || '' === trim($email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim($email);
|
||||
}
|
||||
}
|
||||
@@ -250,9 +250,6 @@ class ParticipantFieldHandlerRegistry
|
||||
{
|
||||
// Define fields that may be reassigned by handlers and need syncing
|
||||
$serviceFields = [
|
||||
// Not a service: ParticipantEmailFieldHandler pre-fills a child's blank email from
|
||||
// the applicant, and without syncing it back the form would bind the blank over it.
|
||||
'email',
|
||||
'transportationOutbound',
|
||||
'transportationInbound',
|
||||
'insurance',
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Form\Model\ParticipantDto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Covers who the <anmelder> block is built from, and the participant email fallback.
|
||||
* Covers who the <anmelder> block is built from, and that a participant's blank email stays blank.
|
||||
*/
|
||||
class BookingPayloadBuilderTest extends TestCase
|
||||
{
|
||||
@@ -69,7 +69,7 @@ class BookingPayloadBuilderTest extends TestCase
|
||||
$this->assertArrayNotHasKey('idadresse', $payload['anmelder']);
|
||||
}
|
||||
|
||||
public function testParticipantWithoutEmailInheritsTheApplicants(): void
|
||||
public function testParticipantWithoutEmailIsSentWithoutOne(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->applicantTravels = false;
|
||||
@@ -78,10 +78,10 @@ class BookingPayloadBuilderTest extends TestCase
|
||||
|
||||
$payload = $this->builder->buildCreatePayload($bookingDto, 'BUCHUNG');
|
||||
|
||||
$this->assertSame(
|
||||
'e[email protected]',
|
||||
$payload['teilnehmerliste']['teilnehmer'][0]['kommunikation']['email'],
|
||||
'BusPro rejects a participant without a Mail-Adresse with 650, but accepts a shared one'
|
||||
$this->assertArrayNotHasKey(
|
||||
'email',
|
||||
$payload['teilnehmerliste']['teilnehmer'][0]['kommunikation'],
|
||||
'BusPro accepts a participant without a Mail-Adresse; the applicant\'s must not be borrowed'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -429,6 +429,97 @@ class BookingDtoTest extends TestCase
|
||||
$this->assertSame(['participants[0].address'], $this->collectFirstParticipantAddressPaths($dto));
|
||||
}
|
||||
|
||||
public function testChildApplicantWithoutEmailIsReportedOnce(): void
|
||||
{
|
||||
$dto = $this->createCreateModeDto();
|
||||
$dto->participants[0] = $this->createParticipantWithoutEmail(10);
|
||||
|
||||
$this->assertSame(
|
||||
['participants[0].email'],
|
||||
$this->collectFirstParticipantEmailPaths($dto),
|
||||
'The applicant is the booking contact and needs an email at any age'
|
||||
);
|
||||
}
|
||||
|
||||
public function testChildAtIndexZeroOfASplitBookingMayLeaveTheEmailBlank(): void
|
||||
{
|
||||
$dto = $this->createCreateModeDto();
|
||||
$dto->applicantTravels = false;
|
||||
$dto->participants[0] = $this->createParticipantWithoutEmail(10);
|
||||
|
||||
$this->assertSame([], $this->collectFirstParticipantEmailPaths($dto));
|
||||
}
|
||||
|
||||
public function testAdultAtIndexZeroOfASplitBookingStillNeedsAnEmail(): void
|
||||
{
|
||||
$dto = $this->createCreateModeDto();
|
||||
$dto->applicantTravels = false;
|
||||
$dto->participants[0] = $this->createParticipantWithoutEmail(30);
|
||||
|
||||
$this->assertSame(['participants[0].email'], $this->collectFirstParticipantEmailPaths($dto));
|
||||
}
|
||||
|
||||
public function testApplicantEmailComesFromParticipantZeroWhenTheApplicantTravels(): void
|
||||
{
|
||||
$dto = $this->createCreateModeDto();
|
||||
$dto->participants[0]->email = ' [email protected] ';
|
||||
$dto->applicant = new ParticipantDto();
|
||||
$dto->applicant->email = '[email protected]';
|
||||
|
||||
$this->assertSame('[email protected]', $dto->getApplicantEmail());
|
||||
}
|
||||
|
||||
public function testApplicantEmailComesFromTheApplicantBlockOnACreateModeSplit(): void
|
||||
{
|
||||
$dto = $this->createCreateModeDto();
|
||||
$dto->participants[0]->email = '[email protected]';
|
||||
$dto->applicantTravels = false;
|
||||
$dto->applicant = new ParticipantDto();
|
||||
$dto->applicant->email = '[email protected]';
|
||||
|
||||
$this->assertSame('[email protected]', $dto->getApplicantEmail());
|
||||
}
|
||||
|
||||
public function testApplicantEmailComesFromTheAnmelderOnAnEditModeSplit(): void
|
||||
{
|
||||
$dto = $this->createEditModeDto(100, 200);
|
||||
$dto->participants[0]->email = '[email protected]';
|
||||
$dto->booking->applicant->communication->email = '[email protected]';
|
||||
|
||||
$this->assertSame('[email protected]', $dto->getApplicantEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ages relative to today, since isChild() measures against the current date.
|
||||
*/
|
||||
private function createParticipantWithoutEmail(int $years): ParticipantDto
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 0;
|
||||
$participant->dateOfBirth = (new \DateTimeImmutable('today'))->modify(sprintf('-%d years', $years));
|
||||
$participant->address = $this->createAddress();
|
||||
|
||||
return $participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function collectFirstParticipantEmailPaths(BookingDto $dto): array
|
||||
{
|
||||
$validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
|
||||
$violations = $validator->validate($dto, null, ['booking_create', 'strict_required']);
|
||||
|
||||
$paths = [];
|
||||
foreach ($violations as $violation) {
|
||||
if ('participants[0].email' === $violation->getPropertyPath()) {
|
||||
$paths[] = $violation->getPropertyPath();
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
|
||||
@@ -909,7 +909,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 testChildWithoutEmailFailsStrictValidation(): void
|
||||
public function testChildWithoutEmailPassesStrictValidation(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
@@ -918,14 +918,10 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame(
|
||||
['Bitte angeben'],
|
||||
$this->emailRequiredMessages($violations),
|
||||
'BusPro requires a Mail-Adresse for every participant, children included'
|
||||
);
|
||||
$this->assertSame([], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testChildWithNullEmailFailsStrictValidation(): void
|
||||
public function testChildWithNullEmailPassesStrictValidation(): void
|
||||
{
|
||||
$child = $this->createChildParticipant('');
|
||||
$child->email = null;
|
||||
@@ -937,7 +933,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
$this->assertSame([], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testChildSharingTheApplicantsEmailPassesStrictValidation(): void
|
||||
@@ -952,7 +948,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->assertSame(
|
||||
[],
|
||||
$this->emailRequiredMessages($violations),
|
||||
'A child inherits the applicant address, which BusPro accepts'
|
||||
'A child may use the applicant\'s address instead of leaving it blank'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1021,6 +1017,99 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testChildAtIndexZeroOfASplitBookingMayLeaveTheEmailBlank(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createChildParticipant(''),
|
||||
]);
|
||||
$bookingDto->applicantTravels = false;
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 0);
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$this->emailRequiredMessages($violations),
|
||||
'Where the applicant does not travel, participant[0] is an ordinary traveller'
|
||||
);
|
||||
}
|
||||
|
||||
public function testAdultAtIndexZeroOfASplitBookingStillNeedsAnEmail(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createAdultParticipant(''),
|
||||
]);
|
||||
$bookingDto->applicantTravels = false;
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 0);
|
||||
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testAdultTravellerMayNotReuseTheNonTravellingApplicantsEmail(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
]);
|
||||
$bookingDto->applicantTravels = false;
|
||||
$bookingDto->applicant = $this->createValidParticipant(new \DateTimeImmutable('1980-01-01'), '[email protected]');
|
||||
|
||||
$violations = $this->validator->validate(
|
||||
new ParticipantEditDto(participant: $bookingDto->participants[0], bookingContext: $bookingDto),
|
||||
null,
|
||||
['booking_create']
|
||||
);
|
||||
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('participant.email', $violations->get(0)->getPropertyPath());
|
||||
$this->assertSame(
|
||||
'Diese E-Mail Adresse wird bereits von der anmeldenden Person verwendet',
|
||||
$violations->get(0)->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public function testChildTravellerMayReuseTheNonTravellingApplicantsEmail(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createChildParticipant('[email protected]'),
|
||||
]);
|
||||
$bookingDto->applicantTravels = false;
|
||||
$bookingDto->applicant = $this->createValidParticipant(new \DateTimeImmutable('1980-01-01'), '[email protected]');
|
||||
|
||||
$violations = $this->validator->validate(
|
||||
new ParticipantEditDto(participant: $bookingDto->participants[0], bookingContext: $bookingDto),
|
||||
null,
|
||||
['booking_create']
|
||||
);
|
||||
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testAdultTravellerMayNotReuseTheAnmelderEmailOfASplitBookingInEditMode(): void
|
||||
{
|
||||
$applicant = new PersonalData();
|
||||
$applicant->personId = 100;
|
||||
$applicant->communication->email = '[email protected]';
|
||||
|
||||
$bookingDto = $this->createBookingDtoWithParticipants([
|
||||
$this->createAdultParticipant('[email protected]'),
|
||||
]);
|
||||
$bookingDto->booking = new Booking();
|
||||
$bookingDto->booking->applicant = $applicant;
|
||||
$bookingDto->participants[0]->personId = 200;
|
||||
|
||||
$violations = $this->validator->validate(
|
||||
new ParticipantEditDto(participant: $bookingDto->participants[0], bookingContext: $bookingDto),
|
||||
null,
|
||||
['booking_edit']
|
||||
);
|
||||
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame(
|
||||
'Diese E-Mail Adresse wird bereits von der anmeldenden Person verwendet',
|
||||
$violations->get(0)->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public function testDependentTurningExactlySixteenRequiresEmail(): void
|
||||
{
|
||||
$participant = $this->createAdultParticipant('');
|
||||
@@ -1036,7 +1125,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
public function testDependentOneDayShortOfSixteenStillRequiresEmail(): void
|
||||
public function testDependentOneDayShortOfSixteenDoesNotRequireEmail(): void
|
||||
{
|
||||
$participant = $this->createAdultParticipant('');
|
||||
$participant->dateOfBirth = (new \DateTimeImmutable('today'))->modify('-16 years')->modify('+1 day');
|
||||
@@ -1048,7 +1137,7 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
$violations = $this->validateStrict($bookingDto, 1);
|
||||
|
||||
$this->assertSame(['Bitte angeben'], $this->emailRequiredMessages($violations));
|
||||
$this->assertSame([], $this->emailRequiredMessages($violations));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,14 +74,69 @@ class CreateFieldStateProviderTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredForEveryParticipantIncludingChildren(): void
|
||||
public function testEmailIsNotRequiredForNonApplicantChild(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
|
||||
$this->assertArrayNotHasKey(
|
||||
'required',
|
||||
$this->provider->getFieldState('email', $bookingDto, 1),
|
||||
'Requiredness is the form type base option now that it is unconditional'
|
||||
$this->assertFalse(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'A child travelling with an applicant does not need an email address of their own'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredForNonApplicantAdult(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('1990-01-01'));
|
||||
|
||||
$this->assertTrue($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredForTheApplicantEvenAsAChild(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$bookingDto->participants[0]->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 0)['required'],
|
||||
'The applicant is the booking contact and always has to be reachable'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsNotRequiredForAChildAtIndexZeroWhenTheApplicantDoesNotTravel(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$bookingDto->participants[0]->dateOfBirth = new \DateTimeImmutable('2015-01-01');
|
||||
$bookingDto->applicantTravels = false;
|
||||
|
||||
$this->assertFalse(
|
||||
$this->provider->getFieldState('email', $bookingDto, 0)['required'],
|
||||
'On a split booking participant[0] is an ordinary traveller'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredWhenDateOfBirthIsUnknown(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$bookingDto->participants[1]->dateOfBirth = null;
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'An unknown age counts as an adult'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailRequirementFlipsWhenDateOfBirthChanges(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDtoWithApplicantAndDependent(new \DateTimeImmutable('2015-01-01'));
|
||||
$dependent = $bookingDto->participants[1];
|
||||
|
||||
$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'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,11 @@ class EditFieldStateProviderTest extends TestCase
|
||||
$this->assertTrue($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
}
|
||||
|
||||
public function testEmailIsRequiredForNonApplicantChild(): void
|
||||
public function testEmailIsNotRequiredForNonApplicantChild(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto(new \DateTimeImmutable('2015-01-01'), false);
|
||||
|
||||
$this->assertTrue(
|
||||
$this->provider->getFieldState('email', $bookingDto, 1)['required'],
|
||||
'BusPro requires a Mail-Adresse for every participant, children included'
|
||||
);
|
||||
$this->assertFalse($this->provider->getFieldState('email', $bookingDto, 1)['required']);
|
||||
}
|
||||
|
||||
public function testEmailIsNotRequiredForInternalAgencyBookingsEvenForAdults(): void
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\AgencyLoader;
|
||||
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 testEditModeNeverPreFills(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->booking = new Booking();
|
||||
|
||||
$this->handler->processField(['email' => ''], $bookingDto, 1);
|
||||
|
||||
$this->assertNull(
|
||||
$bookingDto->participants[1]->email,
|
||||
'participant[0] is not reliably the applicant in edit mode, and a blank may be intended'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEditModeLeavesAnInternalAgencyChildWithoutEmail(): void
|
||||
{
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingDto->booking = new Booking();
|
||||
$bookingDto->agencyCode = AgencyLoader::INTERNAL_AGENCY_CODE;
|
||||
|
||||
$this->handler->processField(['email' => ''], $bookingDto, 1);
|
||||
|
||||
$this->assertNull($bookingDto->participants[1]->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