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. In that case, * the insurance field is hidden and will be automatically assigned by the bulk * insurance handler. * * In edit mode, this validation is skipped entirely because insurance data is * readonly and preserved as-is from the BPN API (the insurance field handler * does not process insurance in edit mode). * * 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 mode - insurance is readonly and preserved as-is if (BookingDto::MODE_EDIT === $this->bookingContext->getMode()) { return; } // Insurance is always required for applicant in create mode if (true === $this->participant->isApplicant()) { if (null === $this->participant->insurance) { $context->buildViolation('Bitte auswählen') ->atPath('participant.insurance') ->addViolation(); } return; } // For dependent participants, check if bulk insurance is active $applicant = $this->bookingContext->getParticipant(0); if (null === $applicant) { // Applicant not found - shouldn't happen, but validate insurance to be safe if (null === $this->participant->insurance) { $context->buildViolation('Bitte auswählen') ->atPath('participant.insurance') ->addViolation(); } return; } // If bulk insurance booking is active, skip validation (field is hidden, will be auto-assigned) if (true === $applicant->bulkInsuranceBooking) { return; } // Bulk insurance not active - insurance is required 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; } // Skip validation for applicant (index 0) - applicant's email can be shared 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 || '' === trim($this->participant->email)) { return; } // Normalize current participant's email for comparison $normalizedEmail = strtolower(trim($this->participant->email)); // 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 || '' === trim($otherParticipant->email)) { continue; } // Compare normalized emails $otherNormalizedEmail = strtolower(trim($otherParticipant->email)); 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; } } } }