draftRepository->findByUserAndBooking($user, $bookingId); } /** * Saves or updates a draft for the given booking. * * Uses an upsert pattern: creates a new draft if none exists, * or updates the existing draft with new form data. * * @param User $user The user who owns the draft * @param int $bookingId The BusProNet booking ID * @param BookingDto $bookingDto The booking DTO containing user edits */ public function saveDraft(User $user, int $bookingId, BookingDto $bookingDto): void { $formData = $this->fingerprintService->extractUserData($bookingDto); $travelDate = $bookingDto->travel->dateFrom; $existingDraft = $this->findDraft($user, $bookingId); $bookingNumber = $bookingDto->booking?->bookingNumber; $dateId = $bookingDto->travel->id; $hotelId = $bookingDto->travel->hotelId; if (null !== $existingDraft) { $existingDraft->setFormData($formData); // Only set once — the draft must stay bound to the original departure date and // hotel, so these identifiers are never overwritten once assigned. if (null === $existingDraft->getBookingNumber() && null !== $bookingNumber) { $existingDraft->setBookingNumber($bookingNumber); } if (null === $existingDraft->getDateId() && null !== $dateId) { $existingDraft->setDateId($dateId); } if (null === $existingDraft->getHotelId() && null !== $hotelId) { $existingDraft->setHotelId($hotelId); } } else { $draft = new BookingEditDraft($user, $bookingId, $travelDate, $formData); $draft->setBookingNumber($bookingNumber); $draft->setDateId($dateId); $draft->setHotelId($hotelId); $this->entityManager->persist($draft); } $this->entityManager->flush(); $this->logger->debug('Saved booking edit draft', [ 'user_id' => $user->getId(), 'booking_id' => $bookingId, ]); } /** * Deletes a draft after successful booking submission. * * @param User $user The user who owns the draft * @param int $bookingId The BusProNet booking ID */ public function deleteDraft(User $user, int $bookingId): void { $this->draftRepository->deleteByUserAndBooking($user, $bookingId); $this->logger->debug('Deleted booking edit draft', [ 'user_id' => $user->getId(), 'booking_id' => $bookingId, ]); } /** * Applies draft data to a fresh BookingDto loaded from the API. * * This method merges user input from the draft onto fresh API data. * The draft contains personal data, service selections, and payment info * that the user previously entered. Services are resolved by ID against * the current Travel data to ensure prices and availability are current. * * @param BookingEditDraft $draft The draft containing saved user data * @param BookingDto $dto The fresh BookingDto from API (modified in place) * @param Travel $travel The current travel data for service resolution * * @return bool True if draft was applied successfully, false if draft data was invalid */ public function applyDraftToDto(BookingEditDraft $draft, BookingDto $dto, Travel $travel): bool { try { $formData = $draft->getFormData(); // Apply payment data if (isset($formData['paymentMethod'])) { $dto->paymentMethod = $formData['paymentMethod']; } if (isset($formData['bankAccount']) && true === is_array($formData['bankAccount'])) { $this->applyBankAccountData($dto, $formData['bankAccount']); } // Apply participant data (personal fields, body dimensions, room assignment, // vouchers, and service selections — all gated by per-participant and travel mutability) if (isset($formData['participants']) && true === is_array($formData['participants'])) { foreach ($formData['participants'] as $index => $participantData) { if (false === isset($dto->participants[$index])) { continue; } $this->participantApplier->apply( $dto, $index, $dto->participants[$index], $participantData, $travel, ); } } $this->logger->info('Applied draft to booking DTO', [ 'booking_id' => $draft->getBookingId(), 'draft_created_at' => $draft->getCreatedAt()->format('Y-m-d H:i:s'), ]); return true; } catch (\Throwable $e) { $this->logger->error('Failed to apply draft to booking DTO', [ 'booking_id' => $draft->getBookingId(), 'error' => $e->getMessage(), ]); return false; } } /** * Applies bank account data from draft to BookingDto. */ /** @param array $bankAccountData */ private function applyBankAccountData(BookingDto $dto, array $bankAccountData): void { $iban = $bankAccountData['iban'] ?? null; $accountHolder = $bankAccountData['accountHolder'] ?? null; if (null === $iban && null === $accountHolder) { return; } if (null === $dto->bankAccount) { $dto->bankAccount = new BankAccountDto(); } $dto->bankAccount->iban = $iban; $dto->bankAccount->accountHolder = $accountHolder; } }