254 lines
9.8 KiB
PHP
254 lines
9.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\BusProNet\ApiClient;
|
|
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\Notification;
|
|
use App\BusProNet\XmlLoader\AgencyLoader;
|
|
use App\Entity\User;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Security\Crypt;
|
|
use Psr\Cache\InvalidArgumentException;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Contracts\Cache\ItemInterface;
|
|
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
|
|
|
/**
|
|
* Handles loading and initializing booking data for edit mode.
|
|
*
|
|
* Encapsulates the logic for loading booking data from session or API,
|
|
* refreshing availability data, and restoring drafts when available.
|
|
*/
|
|
class BookingEditDataLoader
|
|
{
|
|
public const string CACHE_TAG_USER_PREFIX = 'user_bookings_';
|
|
|
|
private bool $draftRestored = false;
|
|
|
|
public function __construct(
|
|
private readonly ApiClient $apiClient,
|
|
private readonly BookingDataProcessor $bookingDataProcessor,
|
|
private readonly BookingSessionManager $bookingSessionService,
|
|
private readonly BookingChangeTracker $fingerprintService,
|
|
private readonly TravelDataProvider $travelDataService,
|
|
private readonly BookingEditDraftManager $draftService,
|
|
private readonly AgencyLoader $agencyLoader,
|
|
private readonly Crypt $crypt,
|
|
private readonly TagAwareCacheInterface $bpnCache,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Returns whether a draft was restored during the last load operation.
|
|
*
|
|
* This flag is reset on each call to loadFormData() and can be used
|
|
* by the controller to show a flash message to the user.
|
|
*/
|
|
public function isDraftRestored(): bool
|
|
{
|
|
return $this->draftRestored;
|
|
}
|
|
|
|
/**
|
|
* Loads booking data from session or initializes from API.
|
|
*
|
|
* Uses session data if available and valid (same booking ID). This preserves
|
|
* participant edits between form submissions within the same edit session.
|
|
*
|
|
* If no session data exists, loads fresh data from API. If a draft exists,
|
|
* it is automatically applied on top of the fresh API data to restore
|
|
* pending user edits.
|
|
*
|
|
* @param Request $request The HTTP request
|
|
* @param int $bookingId The booking ID to load
|
|
* @param User $user The authenticated user
|
|
*
|
|
* @return BookingDto|null The loaded booking DTO, or null if loading failed
|
|
*/
|
|
public function loadFormData(Request $request, int $bookingId, User $user): ?BookingDto
|
|
{
|
|
$this->draftRestored = false;
|
|
|
|
$formData = $this->bookingSessionService->getBookingDto($request, BookingDto::MODE_EDIT);
|
|
|
|
// Validate session data matches requested booking - clear stale data if mismatched
|
|
if (null !== $formData && $formData->booking?->id !== $bookingId) {
|
|
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
|
$formData = null;
|
|
}
|
|
|
|
if (null === $formData) {
|
|
return $this->initializeFromApi($request, $bookingId, $user);
|
|
}
|
|
|
|
// No user edits pending — discard the session and reload live from API so that
|
|
// BusPro-side changes (participant data, status, surcharges) are immediately
|
|
// visible. isDirty() also returns false when originalFingerprint is null
|
|
// (inconsistent session state), which is equally safe to reload.
|
|
if (!$this->fingerprintService->isDirty($formData)) {
|
|
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
|
$this->invalidateBookingCache($bookingId, $user);
|
|
|
|
return $this->initializeFromApi($request, $bookingId, $user);
|
|
}
|
|
|
|
// Refresh availability and mutability data to ensure current state.
|
|
// Availability is refreshed on every load; mutability is patched from cache.
|
|
$this->travelDataService->enrichWithFreshAvailabilities($formData->travel);
|
|
|
|
$bookingData = $this->fetchBookingData($bookingId, $user);
|
|
if (null !== $bookingData && !($bookingData instanceof Notification)) {
|
|
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId, cached: true);
|
|
if (null !== $mutableData) {
|
|
$this->travelDataService->patchMutability($formData->travel, $mutableData);
|
|
}
|
|
}
|
|
|
|
return $formData;
|
|
}
|
|
|
|
/**
|
|
* Initializes booking data from API on first load and stores in session.
|
|
*
|
|
* If a draft exists for this user and booking, it is automatically applied
|
|
* on top of the fresh API data. This ensures user input is preserved while
|
|
* structural data (services, prices) remains current.
|
|
*
|
|
* @param Request $request The HTTP request
|
|
* @param int $bookingId The booking ID to load
|
|
* @param User $user The authenticated user
|
|
*
|
|
* @return BookingDto|null The loaded booking DTO, or null if loading failed
|
|
*/
|
|
public function initializeFromApi(Request $request, int $bookingId, User $user): ?BookingDto
|
|
{
|
|
$bookingData = $this->fetchBookingData($bookingId, $user);
|
|
|
|
if (null === $bookingData || $bookingData instanceof Notification) {
|
|
return null;
|
|
}
|
|
|
|
$travelData = $this->travelDataService->getTravelData($bookingData->dateId, $bookingData->hotelId);
|
|
if (null === $travelData) {
|
|
return null;
|
|
}
|
|
|
|
// Use cached mutability data for form/UI state.
|
|
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId, cached: true);
|
|
// Force refresh ensures fresh data at edit start, then populates cache for subsequent loads
|
|
$availabilities = $this->travelDataService->getAvailabilityData($bookingData->dateId, cached: true, forceRefresh: true);
|
|
|
|
if (null === $mutableData || null === $availabilities) {
|
|
return null;
|
|
}
|
|
|
|
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
|
|
$this->travelDataService->patchMutability($travelData, $mutableData);
|
|
|
|
// Resolve agency code before building the DTO so the internal-agency flag can be
|
|
// passed to createBookingDtoFromBooking, preventing applicant data from being
|
|
// copied into participant[0] for agency bookings where they are different people.
|
|
$agencyCode = null !== $bookingData->agencyId
|
|
? $this->agencyLoader->loadById($bookingData->agencyId)?->code
|
|
: null;
|
|
|
|
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking(
|
|
$bookingData,
|
|
$travelData,
|
|
AgencyLoader::INTERNAL_AGENCY_CODE === $agencyCode,
|
|
);
|
|
|
|
$formData->agencyCode = $agencyCode;
|
|
|
|
// Set original fingerprint BEFORE applying draft, so dirty detection
|
|
// compares against the original API data (not the draft-modified data)
|
|
$formData->originalFingerprint = $this->fingerprintService->generateFingerprint($formData, true);
|
|
|
|
// Check for existing draft and apply if found
|
|
$draft = $this->draftService->findDraft($user, $bookingId);
|
|
if (null !== $draft) {
|
|
$applied = $this->draftService->applyDraftToDto($draft, $formData, $travelData);
|
|
if (true === $applied) {
|
|
$this->draftRestored = true;
|
|
}
|
|
}
|
|
|
|
$this->bookingSessionService->saveBookingDto($request, $formData, BookingDto::MODE_EDIT);
|
|
|
|
return $formData;
|
|
}
|
|
|
|
/**
|
|
* Fetches booking data from API with caching.
|
|
*
|
|
* Cache entries are tagged with the user ID to allow bulk invalidation
|
|
* when the user updates their personal data.
|
|
*
|
|
* @param int $bookingId The booking ID to fetch
|
|
* @param User $user The authenticated user
|
|
*
|
|
* @return Booking|Notification|null The booking data, notification on error, or null on cache failure
|
|
*/
|
|
public function fetchBookingData(int $bookingId, User $user): Booking|Notification|null
|
|
{
|
|
$cacheKey = $this->getBookingCacheKey($bookingId, $user);
|
|
$userTag = self::CACHE_TAG_USER_PREFIX.$user->getId();
|
|
$email = $user->getEmail();
|
|
$password = $this->crypt->decrypt($user->getPassword());
|
|
|
|
try {
|
|
return $this->bpnCache->get($cacheKey, function (ItemInterface $item) use ($email, $password, $bookingId, $userTag) {
|
|
$item->expiresAfter(300);
|
|
$item->tag([$userTag]);
|
|
|
|
return $this->apiClient->getBooking($email, $password, $bookingId);
|
|
});
|
|
} catch (InvalidArgumentException) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invalidates the booking cache after successful update.
|
|
*
|
|
* @param int $bookingId The booking ID to invalidate
|
|
* @param User $user The authenticated user
|
|
*/
|
|
public function invalidateBookingCache(int $bookingId, User $user): void
|
|
{
|
|
try {
|
|
$cacheKey = $this->getBookingCacheKey($bookingId, $user);
|
|
$this->bpnCache->delete($cacheKey);
|
|
} catch (InvalidArgumentException) {
|
|
// Ignore cache deletion errors
|
|
}
|
|
}
|
|
|
|
private function getBookingCacheKey(int $bookingId, User $user): string
|
|
{
|
|
return sprintf('bpn_booking_%d_u_%d', $bookingId, $user->getId());
|
|
}
|
|
|
|
/**
|
|
* Invalidates all cached bookings for a user.
|
|
*
|
|
* Called when the user updates their personal data to ensure
|
|
* booking edits show the latest applicant information.
|
|
*
|
|
* @param User $user The user whose booking caches should be invalidated
|
|
*/
|
|
public function invalidateUserBookingCaches(User $user): void
|
|
{
|
|
try {
|
|
$userTag = self::CACHE_TAG_USER_PREFIX.$user->getId();
|
|
$this->bpnCache->invalidateTags([$userTag]);
|
|
} catch (InvalidArgumentException) {
|
|
// Ignore cache invalidation errors
|
|
}
|
|
}
|
|
}
|