feat: consolidate micro-services and unify draft merge logic
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Entity\BookingEditDraft;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\BankAccountDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Repository\BookingEditDraftRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Manages draft persistence for the booking edit flow.
|
||||
*
|
||||
* This service handles saving, loading, and applying draft data to prevent
|
||||
* data loss when BusProNet API rejects booking update submissions. Drafts
|
||||
* are automatically applied on top of fresh API data when a user returns
|
||||
* to edit a booking.
|
||||
*/
|
||||
class BookingEditDraftManager
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BookingEditDraftRepository $draftRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly BookingChangeTracker $fingerprintService,
|
||||
private readonly BookingEditDraftMerger $participantApplier,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an existing draft for a user and booking combination.
|
||||
*
|
||||
* @param User $user The user who owns the draft
|
||||
* @param int $bookingId The BusProNet booking ID
|
||||
*
|
||||
* @return BookingEditDraft|null The draft if found, null otherwise
|
||||
*/
|
||||
public function findDraft(User $user, int $bookingId): ?BookingEditDraft
|
||||
{
|
||||
return $this->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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user