Files
myep/src/Form/Model/ParticipantEditDto.php
T

173 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Form\Model;
use App\BusProNet\Constants;
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\PurchaseVoucher(groups: ['booking_create', 'booking_edit'])]
#[AppAssert\PromoVoucher(groups: ['booking_create', 'booking_edit'])]
#[AppAssert\MandatoryAdditionalServicesSelected(groups: ['booking_create', 'booking_edit'])]
class ParticipantEditDto
{
public function __construct(
#[Assert\Valid]
public readonly ParticipantDto $participant,
public readonly BookingDto $bookingContext,
) {
}
/**
* Validates that ski pass is selected when required.
*
* Ski pass is required for all participants in create mode, EXCEPT for babies
* (participants aged 0-2 years at travel date). Babies don't qualify for any
* ski pass and the field is hidden for them.
*
* In edit submissions, this validation is skipped as the ski pass is readonly.
*
* This validation only runs when strict_required group is active.
*/
#[Assert\Callback(groups: ['strict_required'])]
public function validateSkiPassRequired(ExecutionContextInterface $context): void
{
// Skip validation in edit submissions - ski pass is readonly there.
if (BookingDto::MODE_EDIT === $this->bookingContext->getMode()) {
return;
}
// Baby age exemption: skip validation for participants at or under BABY_MAX_AGE
$age = $this->participant->getAge($this->bookingContext->travel->dateFrom);
if (null !== $age && $age <= Constants::BABY_MAX_AGE) {
return;
}
// Ski pass is required for non-baby participants
if (null === $this->participant->skiPass) {
$context->buildViolation('Bitte auswählen')
->atPath('participant.skiPass')
->addViolation();
}
}
/**
* 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();
}
}
/**
* 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;
}
}
}
}