162 lines
6.1 KiB
PHP
162 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Model;
|
|
|
|
use App\Validator\Constraints as AppAssert;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|
|
|
use function Symfony\Component\String\u;
|
|
|
|
/**
|
|
* Wrapper DTO for editing a participant within a booking context.
|
|
*
|
|
* This wrapper enables clean validation of participant email uniqueness
|
|
* without requiring back-references or form coupling. It contains both
|
|
* the participant being edited and the full booking context needed for
|
|
* cross-participant validation.
|
|
*/
|
|
#[AppAssert\MandatoryAdditionalServicesSelected(groups: ['booking_create', 'booking_edit'])]
|
|
#[AppAssert\SkiPassSelection(groups: ['strict_required'])]
|
|
class ParticipantEditDto
|
|
{
|
|
public function __construct(
|
|
#[Assert\Valid]
|
|
public readonly ParticipantDto $participant,
|
|
public readonly BookingDto $bookingContext,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Validates that insurance is selected when required.
|
|
*
|
|
* Insurance is required for all participants in create mode, EXCEPT for dependent
|
|
* participants when the applicant has enabled bulk insurance booking, or when the
|
|
* applicant has a family insurance selected. In both cases, the insurance field is
|
|
* hidden and coverage is derived from the applicant instead.
|
|
*
|
|
* In edit submissions, this validation is skipped entirely because insurance
|
|
* data is readonly and preserved as-is from the BPN API.
|
|
*
|
|
* This validation only runs when strict_required group is active.
|
|
*/
|
|
#[Assert\Callback(groups: ['strict_required'])]
|
|
public function validateInsuranceRequired(ExecutionContextInterface $context): void
|
|
{
|
|
// Skip validation in edit submissions - insurance is readonly and preserved as-is.
|
|
if (BookingDto::MODE_EDIT === $this->bookingContext->getMode()) {
|
|
return;
|
|
}
|
|
|
|
if (false === $this->participant->isApplicant()) {
|
|
$applicant = $this->bookingContext->getParticipant(0);
|
|
|
|
if (true === $applicant?->bulkInsuranceBooking) {
|
|
return;
|
|
}
|
|
|
|
if (null !== $applicant?->insurance && true === $applicant->insurance->familyInsurance) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (null === $this->participant->insurance) {
|
|
$context->buildViolation('Bitte auswählen')
|
|
->atPath('participant.insurance')
|
|
->addViolation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reports voucher codes BusPro rejected.
|
|
*
|
|
* ParticipantVoucherFieldHandler checks the codes with BusPro while handling the submission and
|
|
* keeps the outcome on the participant, since the htmx refresh rebuilds the form and drops form
|
|
* errors. This turns it into a violation so the step cannot be left with a rejected code.
|
|
*/
|
|
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
|
|
public function validateVoucherCodes(ExecutionContextInterface $context): void
|
|
{
|
|
if (null !== $this->participant->voucherCodeError) {
|
|
$context->buildViolation($this->participant->voucherCodeError)
|
|
->atPath('participant.voucherCode')
|
|
->addViolation();
|
|
}
|
|
|
|
if (null !== $this->participant->promoVoucherCode && null !== $this->participant->promoVoucherError) {
|
|
$context->buildViolation($this->participant->promoVoucherError)
|
|
->atPath('participant.voucherCode')
|
|
->addViolation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that non-applicant adult participants have unique email addresses.
|
|
*
|
|
* Exempt from validation:
|
|
* - Applicant (index 0): Can share email with dependents they're booking for
|
|
* - Children (under 16): Can share email addresses with adults
|
|
*
|
|
* Email comparison is case-insensitive and whitespace is normalized.
|
|
*/
|
|
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
|
|
public function validateEmailUniqueness(ExecutionContextInterface $context): void
|
|
{
|
|
// Skip for internal agency bookings — staff use shared placeholder emails
|
|
if ($this->bookingContext->isInternalAgencyBooking()) {
|
|
return;
|
|
}
|
|
|
|
// In regular create/edit submission flows, the applicant can share an email with dependents.
|
|
if (true === $this->participant->isApplicant()) {
|
|
return;
|
|
}
|
|
|
|
// Skip validation for children (under 16)
|
|
if (true === $this->participant->isChild()) {
|
|
return;
|
|
}
|
|
|
|
// Skip if email is null or empty (handled by @Email and @NotBlank constraints)
|
|
if (null === $this->participant->email || u($this->participant->email)->trim()->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Normalize current participant's email for comparison
|
|
$normalizedEmail = u($this->participant->email)->trim()->lower()->toString();
|
|
|
|
// Check against all adult participants in booking context
|
|
foreach ($this->bookingContext->participants as $index => $otherParticipant) {
|
|
// Skip self-comparison (same participant index)
|
|
if ($index === $this->participant->index) {
|
|
continue;
|
|
}
|
|
|
|
// Skip children (they can share email addresses)
|
|
if (true === $otherParticipant->isChild()) {
|
|
continue;
|
|
}
|
|
|
|
// Skip null or empty emails
|
|
if (null === $otherParticipant->email || u($otherParticipant->email)->trim()->isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
// Compare normalized emails
|
|
$otherNormalizedEmail = u($otherParticipant->email)->trim()->lower()->toString();
|
|
|
|
if ($normalizedEmail === $otherNormalizedEmail) {
|
|
// Add violation to participant.email path
|
|
$context->buildViolation('Diese E-Mail Adresse wird bereits von einem anderen Teilnehmer verwendet')
|
|
->atPath('participant.email')
|
|
->addViolation();
|
|
|
|
// Stop after first duplicate found (no need to report multiple times)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|