wip: booking process, refactoring

This commit is contained in:
Björn Fromme
2025-07-16 19:37:17 +02:00
parent 47faa6b08e
commit eecea0abd1
43 changed files with 2269 additions and 366 deletions
+11 -20
View File
@@ -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();
+552
View File
@@ -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(),
]);
}
}
}