feat: load hotel data locally for sidebar
This commit is contained in:
@@ -36,12 +36,17 @@ class HotelLoader extends AbstractLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function mapCodeToId(string $hotelCode, ?string $filename = 'hotel.xml'): ?int
|
public function mapCodeToId(string $hotelCode, ?string $filename = 'hotel.xml'): ?int
|
||||||
|
{
|
||||||
|
return $this->loadByCode($hotelCode, $filename)?->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadByCode(string $hotelCode, ?string $filename = 'hotel.xml'): ?Hotel
|
||||||
{
|
{
|
||||||
$hotels = $this->loadAll($filename);
|
$hotels = $this->loadAll($filename);
|
||||||
|
|
||||||
foreach ($hotels as $hotel) {
|
foreach ($hotels as $hotel) {
|
||||||
if ($hotelCode === $hotel->code) {
|
if ($hotelCode === $hotel->code) {
|
||||||
return $hotel->id;
|
return $hotel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Hotel;
|
||||||
|
use App\BusProNet\XmlLoader\HotelLoader;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\BookingSummaryDto;
|
use App\Form\Model\BookingSummaryDto;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -21,6 +23,7 @@ class BookingSummaryDataService
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingPriceCalculatorService $priceCalculator,
|
private readonly BookingPriceCalculatorService $priceCalculator,
|
||||||
private readonly CmsDataService $cmsDataService,
|
private readonly CmsDataService $cmsDataService,
|
||||||
|
private readonly HotelLoader $hotelLoader,
|
||||||
private readonly CacheInterface $cache,
|
private readonly CacheInterface $cache,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
@@ -128,43 +131,49 @@ class BookingSummaryDataService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches CMS data for a product and hotel combination.
|
* Fetches hotel display data combining local base hotel data with CMS images.
|
||||||
*
|
*
|
||||||
* Data is cached for 1 hour as it rarely changes. Returns null
|
* Uses normalized codes (first 3 characters, or characters 4-6 for 'SER' codes)
|
||||||
* if the API call fails or if product code is not available.
|
* for both local hotel lookup and CMS image retrieval. Local hotel data provides
|
||||||
* This method can be called early in the booking flow to warm the cache.
|
* name and address while CMS provides images as a nice-to-have enhancement.
|
||||||
|
*
|
||||||
|
* Data is cached for 1 hour. This method can be called early in the booking
|
||||||
|
* flow to warm the cache.
|
||||||
*/
|
*/
|
||||||
public function getCmsDataForProduct(?string $productCode, ?string $hotelCode): ?array
|
public function getCmsDataForProduct(?string $productCode, ?string $hotelCode): ?array
|
||||||
{
|
{
|
||||||
if (null === $productCode) {
|
$normalizedHotelCode = $this->cmsDataService->normalizeCode($hotelCode);
|
||||||
|
|
||||||
|
if (null === $normalizedHotelCode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = sprintf('cms_data.%s.%s', $productCode, $hotelCode ?? 'none');
|
$cacheKey = sprintf('cms_data.%s.%s', $normalizedHotelCode, $normalizedHotelCode);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($productCode, $hotelCode) {
|
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($productCode, $hotelCode, $normalizedHotelCode) {
|
||||||
$item->expiresAfter(3600); // 1 hour
|
$item->expiresAfter(3600); // 1 hour
|
||||||
|
|
||||||
$result = $this->cmsDataService->getProductDetails($productCode, $hotelCode);
|
// Fetch base hotel from local BusProNet data
|
||||||
|
$baseHotel = $this->hotelLoader->loadByCode($normalizedHotelCode);
|
||||||
|
|
||||||
// Return null if API call failed
|
// Fetch CMS images (nice to have)
|
||||||
if (true === isset($result['success']) && false === $result['success']) {
|
$images = $this->cmsDataService->getProductImages($productCode, $hotelCode);
|
||||||
$this->logger->warning('CMS API call failed', [
|
|
||||||
'product_code' => $productCode,
|
|
||||||
'hotel_code' => $hotelCode,
|
|
||||||
'message' => $result['message'] ?? 'Unknown error',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return null;
|
// Return combined data structure compatible with templates
|
||||||
}
|
return [
|
||||||
|
'hotel' => [
|
||||||
return $result;
|
'name' => $baseHotel?->name,
|
||||||
|
'address' => $this->formatHotelAddress($baseHotel),
|
||||||
|
'images' => $images,
|
||||||
|
],
|
||||||
|
];
|
||||||
});
|
});
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->logger->error('Failed to fetch CMS data', [
|
$this->logger->error('Failed to fetch hotel display data', [
|
||||||
'product_code' => $productCode,
|
'product_code' => $productCode,
|
||||||
'hotel_code' => $hotelCode,
|
'hotel_code' => $hotelCode,
|
||||||
|
'normalized_hotel_code' => $normalizedHotelCode,
|
||||||
'exception' => $e->getMessage(),
|
'exception' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -172,6 +181,20 @@ class BookingSummaryDataService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a hotel's address from street and city.
|
||||||
|
*/
|
||||||
|
private function formatHotelAddress(?Hotel $hotel): ?string
|
||||||
|
{
|
||||||
|
if (null === $hotel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = array_filter([$hotel->street, $hotel->city]);
|
||||||
|
|
||||||
|
return [] === $parts ? null : implode("\n", $parts);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches CMS data for the product and hotel in the booking.
|
* Fetches CMS data for the product and hotel in the booking.
|
||||||
*/
|
*/
|
||||||
@@ -180,8 +203,8 @@ class BookingSummaryDataService
|
|||||||
$productCode = $bookingDto->travel->productCode;
|
$productCode = $bookingDto->travel->productCode;
|
||||||
$hotelCode = $bookingDto->travel->hotel?->code;
|
$hotelCode = $bookingDto->travel->hotel?->code;
|
||||||
|
|
||||||
if (null === $productCode) {
|
if (null === $hotelCode) {
|
||||||
$this->logger->debug('Cannot fetch CMS data: product code is not available', [
|
$this->logger->debug('Cannot fetch hotel data: hotel code is not available', [
|
||||||
'travel_id' => $bookingDto->travel->id,
|
'travel_id' => $bookingDto->travel->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,48 @@ class CmsDataService
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a product or hotel code for CMS lookup.
|
||||||
|
*
|
||||||
|
* Returns the first 3 characters of the code, except for codes
|
||||||
|
* starting with 'SER' where characters 4-6 are returned instead.
|
||||||
|
*/
|
||||||
|
public function normalizeCode(?string $code): ?string
|
||||||
|
{
|
||||||
|
if (null === $code || 3 > strlen($code)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($code, 'SER') && 6 <= strlen($code)) {
|
||||||
|
return substr($code, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($code, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches only the hotel images from the CMS using normalized codes.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>|null The images array or null if unavailable
|
||||||
|
*/
|
||||||
|
public function getProductImages(string $productCode, ?string $hotelCode = null): ?array
|
||||||
|
{
|
||||||
|
$normalizedProduct = $this->normalizeCode($productCode);
|
||||||
|
$normalizedHotel = $this->normalizeCode($hotelCode);
|
||||||
|
|
||||||
|
if (null === $normalizedProduct) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->getProductDetails($normalizedProduct, $normalizedHotel);
|
||||||
|
|
||||||
|
if (true === isset($result['success']) && false === $result['success']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result['hotel']['images'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getProductDetails(string $productCode, ?string $hotelCode = null): array
|
public function getProductDetails(string $productCode, ?string $hotelCode = null): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
{% if summaryData.cmsData.hotel.images is defined %}
|
{% if summaryData.cmsData.hotel.name %}
|
||||||
<div class="grid grid-cols-3 gap-x-4 py-4 border-t border-primary-bg">
|
<div class="grid grid-cols-3 gap-x-4 py-4 border-t border-primary-bg">
|
||||||
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}"
|
{% if summaryData.cmsData.hotel.images.resized.l[0] is defined %}
|
||||||
alt="{{ summaryData.cmsData.hotel.images.resized.l[0].alt }}"
|
<img src="{{ summaryData.cmsData.hotel.images.resized.l[0].url }}"
|
||||||
class="block w-full h-auto">
|
alt="{{ summaryData.cmsData.hotel.images.resized.l[0].alt }}"
|
||||||
<div class="col-span-2">
|
class="block w-full h-auto">
|
||||||
|
{% endif %}
|
||||||
|
<div class="{{ summaryData.cmsData.hotel.images.resized.l[0] is defined ? 'col-span-2' : 'col-span-3' }}">
|
||||||
<span class="block font-semibold uppercase">{{ summaryData.cmsData.hotel.name }}</span>
|
<span class="block font-semibold uppercase">{{ summaryData.cmsData.hotel.name }}</span>
|
||||||
{% if summaryData.cmsData.hotel.address is defined %}
|
{% if summaryData.cmsData.hotel.address %}
|
||||||
{{ summaryData.cmsData.hotel.address | nl2br }}
|
{{ summaryData.cmsData.hotel.address | nl2br }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user