wip: booking process, refactoring
This commit is contained in:
@@ -3,18 +3,16 @@
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use App\Service\TravelDataService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class BookingService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelDataLoader,
|
||||
private readonly HotelLoader $hotelDataLoader,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -23,30 +21,23 @@ class BookingService
|
||||
$bookingUuid = $request->query->get('uid');
|
||||
$bookingCreateDto = $request->getSession()->get('booking_create');
|
||||
|
||||
// No UID parameter - try to get existing DTO from session
|
||||
if (null === $bookingUuid) {
|
||||
return $bookingCreateDto ?? throw new NotFoundHttpException('No booking data found. Please start from the beginning.');
|
||||
// No UID parameter - return existing DTO from session if available
|
||||
if (null === $bookingUuid && null !== $bookingCreateDto) {
|
||||
return $bookingCreateDto;
|
||||
}
|
||||
|
||||
// Create a new DTO - we need travel_id and hotel_id for this
|
||||
$travelId = $request->query->getInt('travel_id');
|
||||
// Create a new DTO - we need date_id and hotel_id for this
|
||||
$dateId = $request->query->getInt('date_id');
|
||||
$hotelId = $request->query->getInt('hotel_id');
|
||||
|
||||
if (0 === $travelId || 0 === $hotelId) {
|
||||
throw new NotFoundHttpException('Missing travel_id or hotel_id parameters');
|
||||
if (0 === $dateId || 0 === $hotelId) {
|
||||
throw new NotFoundHttpException('Missing date_id or hotel_id parameters');
|
||||
}
|
||||
|
||||
$travelData = $this->travelDataLoader->loadById($travelId, $hotelId);
|
||||
$travelData = $this->travelDataService->getTravelData($dateId, $hotelId);
|
||||
if (null === $travelData) {
|
||||
throw new NotFoundHttpException('Travel data not found');
|
||||
throw new NotFoundHttpException(sprintf('Travel data not found for date ID %d and hotel ID %d', $dateId, $hotelId));
|
||||
}
|
||||
|
||||
$hotelData = $this->hotelDataLoader->loadById($hotelId);
|
||||
if (null === $hotelData) {
|
||||
throw new NotFoundHttpException('Hotel data not found');
|
||||
}
|
||||
|
||||
$travelData->hotel = $hotelData;
|
||||
$roomsIdsAndQuantities = $this->processRoomQuantities($request);
|
||||
$availableRooms = $travelData->getAvailableRooms();
|
||||
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
/**
|
||||
* Unified service for retrieving travel data from both local XML files and remote API.
|
||||
*
|
||||
* This service provides a unified interface for accessing travel data regardless of source,
|
||||
* supporting automatic fallback between local XML files and remote API calls. It handles caching,
|
||||
* error recovery, and data enrichment for both data sources.
|
||||
*/
|
||||
class TravelDataService
|
||||
{
|
||||
public const SOURCE_LOCAL = 'local';
|
||||
public const SOURCE_REMOTE = 'remote';
|
||||
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelLoader,
|
||||
private readonly HotelLoader $hotelLoader,
|
||||
private readonly PickupLoader $pickupLoader,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly bool $preferRemote = false,
|
||||
private readonly bool $enableFallback = true,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve travel data with automatic source selection and fallback.
|
||||
*
|
||||
* Attempts to load travel data from the preferred source first, then falls back
|
||||
* to the alternative source if the primary fails. Handles caching and enrichment
|
||||
* of data from both sources.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param bool $preferRemote Whether to prefer remote API over XML for this call
|
||||
* @param bool $enableCache Whether to use caching for this request
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found in any source
|
||||
*/
|
||||
public function getTravelData(
|
||||
int $dateId,
|
||||
?int $hotelId = null,
|
||||
?bool $preferRemote = null,
|
||||
bool $enableCache = true,
|
||||
): ?Travel {
|
||||
$preferRemote = $preferRemote ?? $this->preferRemote;
|
||||
$cacheKey = sprintf('travel_unified_%d_%d_%s', $dateId, $hotelId ?? 0, $preferRemote ? 'remote' : 'local');
|
||||
|
||||
if (!$enableCache) {
|
||||
return $this->loadTravelDataUncached($dateId, $hotelId, $preferRemote);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($dateId, $hotelId, $preferRemote) {
|
||||
$item->expiresAfter(300); // 5 minutes cache
|
||||
|
||||
return $this->loadTravelDataUncached($dateId, $hotelId, $preferRemote);
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->error('Cache error in TravelDataService', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->loadTravelDataUncached($dateId, $hotelId, $preferRemote);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve travel data specifically from XML files.
|
||||
*
|
||||
* Loads travel data from local XML files with full data enrichment including
|
||||
* hotel details and pickup information.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found in XML
|
||||
*/
|
||||
public function getTravelDataFromXml(int $dateId, ?int $hotelId = null): ?Travel
|
||||
{
|
||||
try {
|
||||
$travel = $this->travelLoader->loadById($dateId, $hotelId);
|
||||
|
||||
if (null === $travel) {
|
||||
$this->logger->debug('Travel not found in XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->enrichTravelData($travel);
|
||||
$this->logger->debug('Travel data loaded from XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'travelId' => $travel->id,
|
||||
]);
|
||||
|
||||
return $travel;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to load travel data from XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve travel data specifically from remote API.
|
||||
*
|
||||
* Loads travel data from the remote BusProNet API. Note that the API uses
|
||||
* product IDs rather than date IDs, so mapping is performed internally.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found via API
|
||||
*/
|
||||
public function getTravelDataFromApi(int $dateId, ?int $hotelId = null): ?Travel
|
||||
{
|
||||
try {
|
||||
// Map dateId to productId for API call
|
||||
$productId = $this->mapDateIdToProductId($dateId);
|
||||
if (null === $productId) {
|
||||
$this->logger->debug('Cannot map dateId to productId for API call', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->apiClient->getTravelData($productId, $hotelId);
|
||||
|
||||
if (!$result instanceof Travel) {
|
||||
$this->logger->debug('API returned non-travel result', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'productId' => $productId,
|
||||
'resultType' => get_class($result),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->debug('Travel data loaded from API', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'productId' => $productId,
|
||||
'travelId' => $result->id,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Failed to load travel data from API', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if travel data exists in XML files.
|
||||
*
|
||||
* Performs a lightweight check to determine if travel data exists in XML
|
||||
* files without loading the full travel object.
|
||||
*
|
||||
* @param int $dateId The travel date ID to check
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return bool True if travel data exists in XML files
|
||||
*/
|
||||
public function existsInXml(int $dateId, ?int $hotelId = null): bool
|
||||
{
|
||||
$mapping = $this->generateFilesMap();
|
||||
|
||||
if (false === isset($mapping[$dateId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If hotelId is specified, check if it exists in the travel's hotels
|
||||
if (null !== $hotelId && false === isset($mapping[$dateId]['hotels'][$hotelId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about available data sources for a travel.
|
||||
*
|
||||
* Returns information about which data sources (local XML, remote API, or both) have
|
||||
* data available for the specified travel.
|
||||
*
|
||||
* @param int $dateId The travel date ID to check
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return array<string, bool> Array with 'local' and 'remote' keys indicating availability
|
||||
*/
|
||||
public function getAvailableSources(int $dateId, ?int $hotelId = null): array
|
||||
{
|
||||
return [
|
||||
static::SOURCE_LOCAL => $this->existsInXml($dateId, $hotelId),
|
||||
static::SOURCE_REMOTE => null !== $this->mapDateIdToProductId($dateId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load travel data directly without caching.
|
||||
*
|
||||
* Internal method that handles the actual loading logic with fallback support.
|
||||
* Tries the preferred source first, then falls back to the alternative if enabled.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param bool $preferRemote Whether to prefer remote API over XML
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found
|
||||
*/
|
||||
private function loadTravelDataUncached(int $dateId, ?int $hotelId = null, bool $preferRemote = false): ?Travel
|
||||
{
|
||||
$primarySource = $preferRemote ? self::SOURCE_REMOTE : self::SOURCE_LOCAL;
|
||||
$fallbackSource = $preferRemote ? self::SOURCE_LOCAL : self::SOURCE_REMOTE;
|
||||
|
||||
// Try primary source first
|
||||
$travel = $this->loadFromSource($dateId, $hotelId, $primarySource);
|
||||
|
||||
if (null !== $travel) {
|
||||
return $travel;
|
||||
}
|
||||
|
||||
// Try fallback source if enabled
|
||||
if (true === $this->enableFallback) {
|
||||
$this->logger->debug('Fallback to alternative source', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'primarySource' => $primarySource,
|
||||
'fallbackSource' => $fallbackSource,
|
||||
]);
|
||||
|
||||
$travel = $this->loadFromSource($dateId, $hotelId, $fallbackSource);
|
||||
}
|
||||
|
||||
return $travel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load travel data from a specific source.
|
||||
*
|
||||
* Internal method that routes to the appropriate loader based on source type.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param string $source The source type (SOURCE_LOCAL or SOURCE_REMOTE)
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found
|
||||
*/
|
||||
private function loadFromSource(int $dateId, ?int $hotelId, string $source): ?Travel
|
||||
{
|
||||
return match ($source) {
|
||||
self::SOURCE_LOCAL => $this->getTravelDataFromXml($dateId, $hotelId),
|
||||
self::SOURCE_REMOTE => $this->getTravelDataFromApi($dateId, $hotelId),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map date code to date ID.
|
||||
*
|
||||
* Converts a date code string to its corresponding date ID using the
|
||||
* date loader's mapping functionality.
|
||||
*
|
||||
* @param string $dateCode The date code to map
|
||||
*
|
||||
* @return int|null The corresponding date ID or null if not found
|
||||
*/
|
||||
public function mapDateCodeToId(string $dateCode): ?int
|
||||
{
|
||||
try {
|
||||
$dateId = $this->travelLoader->mapCodeToId($dateCode);
|
||||
|
||||
$this->logger->debug('Date code mapping', [
|
||||
'dateCode' => $dateCode,
|
||||
'dateId' => $dateId,
|
||||
]);
|
||||
|
||||
return $dateId;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to map date code to ID', [
|
||||
'dateCode' => $dateCode,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map hotel code to hotel ID.
|
||||
*
|
||||
* Converts a hotel code string to its corresponding hotel ID using the
|
||||
* hotel loader's mapping functionality.
|
||||
*
|
||||
* @param string $hotelCode The hotel code to map
|
||||
*
|
||||
* @return int|null The corresponding hotel ID or null if not found
|
||||
*/
|
||||
public function mapHotelCodeToId(string $hotelCode): ?int
|
||||
{
|
||||
try {
|
||||
$hotelId = $this->hotelLoader->mapCodeToId($hotelCode);
|
||||
|
||||
$this->logger->debug('Hotel code mapping', [
|
||||
'hotelCode' => $hotelCode,
|
||||
'hotelId' => $hotelId,
|
||||
]);
|
||||
|
||||
return $hotelId;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to map hotel code to ID', [
|
||||
'hotelCode' => $hotelCode,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map date ID to product ID for API calls.
|
||||
*
|
||||
* Converts a date ID to its corresponding product ID for use with the
|
||||
* remote API. This method exposes the existing loader functionality
|
||||
* through the service layer.
|
||||
*
|
||||
* @param int $dateId The date ID to map
|
||||
*
|
||||
* @return int|null The corresponding product ID or null if not found
|
||||
*/
|
||||
public function mapDateIdToProductId(int $dateId): ?int
|
||||
{
|
||||
try {
|
||||
$productId = $this->travelLoader->mapDateIdToProductId($dateId);
|
||||
|
||||
$this->logger->debug('Date ID to product ID mapping', [
|
||||
'dateId' => $dateId,
|
||||
'productId' => $productId,
|
||||
]);
|
||||
|
||||
return $productId;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to map date ID to product ID', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate files mapping for available travel data.
|
||||
*
|
||||
* Creates a mapping of all available travel data files with their
|
||||
* corresponding travel and hotel information. This method exposes
|
||||
* the existing loader functionality through the service layer.
|
||||
*
|
||||
* @return array<int, array<string, mixed>> Array mapping of travel data files
|
||||
*/
|
||||
public function generateFilesMap(): array
|
||||
{
|
||||
try {
|
||||
$mapping = $this->travelLoader->generateFilesMap();
|
||||
|
||||
$this->logger->debug('Generated files mapping', [
|
||||
'count' => count($mapping),
|
||||
]);
|
||||
|
||||
return $mapping;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to generate files mapping', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch mutability data from API.
|
||||
*
|
||||
* Retrieves mutability configuration data from the API for a specific travel date.
|
||||
* Handles API errors and notification responses gracefully.
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
*
|
||||
* @return BaseData|null The mutability data or null if not available or error occurred
|
||||
*/
|
||||
public function getMutabilityData(int $dateId): ?BaseData
|
||||
{
|
||||
try {
|
||||
$mutableData = $this->apiClient->getMutableData($dateId);
|
||||
|
||||
if ($mutableData instanceof Notification) {
|
||||
$this->logger->warning('API returned notification for mutability data', [
|
||||
'dateId' => $dateId,
|
||||
'message' => $mutableData->message,
|
||||
'isError' => $mutableData->isError(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->debug('Successfully fetched mutability data', [
|
||||
'dateId' => $dateId,
|
||||
]);
|
||||
|
||||
return $mutableData;
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Failed to fetch mutability data from API', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mutability data to travel services.
|
||||
*
|
||||
* Updates the mutability status of various travel services based on the
|
||||
* provided mutability configuration data.
|
||||
*
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param BaseData $mutableData The mutability configuration data
|
||||
*/
|
||||
public function patchMutability(Travel $travel, BaseData $mutableData): void
|
||||
{
|
||||
$this->travelLoader->patchMutability($travel, $mutableData);
|
||||
|
||||
$this->logger->debug('Successfully patched mutability data', [
|
||||
'travelId' => $travel->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch availability data from API.
|
||||
*
|
||||
* Retrieves availability information from the API for a specific travel date.
|
||||
* Handles API errors and notification responses gracefully.
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
*
|
||||
* @return BaseData|null The availability data or null if not available or error occurred
|
||||
*/
|
||||
public function getAvailabilityData(int $dateId): ?BaseData
|
||||
{
|
||||
try {
|
||||
$availabilities = $this->apiClient->getAvailabilities($dateId);
|
||||
|
||||
if ($availabilities instanceof Notification) {
|
||||
$this->logger->warning('API returned notification for availability data', [
|
||||
'dateId' => $dateId,
|
||||
'message' => $availabilities->message,
|
||||
'isError' => $availabilities->isError(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->debug('Successfully fetched availability data', [
|
||||
'dateId' => $dateId,
|
||||
]);
|
||||
|
||||
return $availabilities;
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Failed to fetch availability data from API', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply availability data to travel services.
|
||||
*
|
||||
* Updates the availability status of additional and transportation
|
||||
* services based on the provided availability data.
|
||||
*
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param BaseData $availabilities The availability data for services
|
||||
*/
|
||||
public function patchAvailabilities(Travel $travel, BaseData $availabilities): void
|
||||
{
|
||||
$this->travelLoader->patchAvailabilities($travel, $availabilities);
|
||||
|
||||
$this->logger->debug('Successfully patched availability data', [
|
||||
'travelId' => $travel->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich travel data with additional information.
|
||||
*
|
||||
* Adds hotel details and pickup information to travel data loaded from XML.
|
||||
* This enrichment is necessary for complete travel information.
|
||||
*
|
||||
* @param Travel $travel The travel object to enrich
|
||||
*/
|
||||
private function enrichTravelData(Travel $travel): void
|
||||
{
|
||||
try {
|
||||
$this->pickupLoader->patchPickupsDetails($travel);
|
||||
$this->hotelLoader->patchHotelDetails($travel);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Failed to enrich travel data', [
|
||||
'travelId' => $travel->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user