*/ #[Assert\Valid] public array $roomSelections = []; /** * @var array */ #[Assert\Valid] public array $participants = []; #[Assert\Choice( choices: [Constants::PAYMENT_METHOD_TRANSFER, Constants::PAYMENT_METHOD_DEBIT], message: 'Bitte wählen Sie eine gültige Zahlungsart.' )] public ?string $paymentMethod = Constants::PAYMENT_METHOD_TRANSFER; public ?BankAccountDto $bankAccount = null; public ?int $agencyId = null; /** * Booking status code for API submission. * Values: 'F' (Final/Frei), 'A' (Anfrage/Inquiry). */ public string $bookingStatus = 'F'; /** * Reference to booking entity (only populated in edit mode). */ public ?Booking $booking = null; /** * Timestamp of last session update (for staleness detection). * Updated automatically by BookingService::saveBookingDto(). */ public ?\DateTimeImmutable $lastSessionUpdate = null; /** * Fingerprint of the booking state when loaded from API (edit mode only). * This property stores the original state and is never updated after initial load. * Used to detect unsaved changes in edit mode by comparing with current state. */ public ?string $originalFingerprint = null; public function __construct(public Travel $travel, public int $hotelId) { } public function getMode(): string { return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE; } /** * @return array */ public function getSelectedRooms(): array { // In edit mode, rooms are fixed - return empty array if (self::MODE_EDIT === $this->getMode()) { return []; } return array_filter($this->roomSelections, function (RoomSelectionDto $roomSelection) { return 0 < $roomSelection->quantity; }); } public function getParticipants(): array { return $this->participants; } public function hasParticipant(int $index): bool { return isset($this->participants[$index]); } public function getParticipant(int $index): ?ParticipantDto { return $this->participants[$index] ?? null; } /** * Determines if this is a family booking based on participant age distribution. * * A family booking is defined as: * - 1 or 2 participants aged 18 or older (adults) * - At least 1 participant younger than 18 (children) */ public function isFamilyBooking(): bool { $adults = 0; $children = 0; $travelStartDate = $this->travel->dateFrom; foreach ($this->participants as $participant) { $age = $participant->getAge($travelStartDate); if (null === $age) { continue; } if ($age >= 18) { ++$adults; } else { ++$children; } } return ($adults >= 1 && $adults <= 2) && ($children >= 1); } public function isCanceled(): bool { return null !== $this->booking && 'S' === $this->booking->status; } public function isOption(): bool { return null !== $this->booking && 'O' === $this->booking->status; } #[Assert\Callback(callback: 'validateRoomSelection', groups: ['booking_create_step_1'])] public function validateRoomSelection(ExecutionContextInterface $context): void { $selectedRooms = $this->getSelectedRooms(); if (0 === count($selectedRooms)) { $context->buildViolation('Bitte mindestens ein Zimmer/Bett auswählen') ->addViolation(); } } #[Assert\Callback] public function validateBankAccount(ExecutionContextInterface $context): void { if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) { return; } if (null === $this->bankAccount) { $context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.') ->atPath('bankAccount') ->addViolation(); return; } if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) { $context->buildViolation('Bitte geben Sie Ihre IBAN ein.') ->atPath('bankAccount.iban') ->addViolation(); } if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) { $context->buildViolation('Bitte geben Sie den Kontoinhaber ein.') ->atPath('bankAccount.accountHolder') ->addViolation(); } if (false === $this->bankAccount->sepaMandateAccepted) { $context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.') ->atPath('bankAccount.sepaMandateAccepted') ->addViolation(); } } }