<?php
namespace App\Controller\Front\Product;
use App\Config\FaqRouteEnum;
use App\Config\ModuleEnum;
use App\Entity\CustomerMoral;
use App\Repository\CityRepository;
use App\Repository\FrontThemeRepository;
use App\Repository\HotelXmlPriceRepository;
use App\Repository\HotelXmlRepository;
use App\Repository\OrderLineRepository;
use App\Repository\OrderRepository;
use App\Service\Api3TAuthenticationService;
use App\Service\CartService;
use App\Service\CurrencyService;
use App\Service\FrontRecentSearchService;
use App\Service\FrontService;
use App\Service\Helpers;
use App\Service\HotelApiService;
use App\Service\ParameterService;
use DateTime;
use Exception;
use Gedmo\Translatable\TranslatableListener;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Annotation\Route;
use Psr\Cache\CacheItemPoolInterface;
#[Route('/hotel')]
class HotelController extends AbstractController
{
private const UNNORMALIZED_CACHE_PREFIX = 'hub_unnormalized_rooms_';
private const UNNORMALIZED_CACHE_TTL = 1800; // 30 min
public function __construct(
private readonly Api3TAuthenticationService $apiAuthenticationService,
private readonly HotelApiService $hotelApiService,
private readonly CurrencyService $currencyService,
private readonly FrontService $frontService,
private readonly ParameterService $parameterService,
private readonly Helpers $helpers,
private readonly LoggerInterface $logger,
private CacheItemPoolInterface $searchCachePool,
)
{
}
#[Route('/hotelcity/{name}', name: 'app_front_hotel_city')]
public function hotelByCity(
string $name,
CityRepository $cityRepository,
HotelXmlPriceRepository $hotelXmlPriceRepository,
FrontService $frontService
): Response
{
$hotels = [];
$city = $cityRepository->findOneBy(['name' => $name]);
$description = "";
$country = "";
if ($city) {
$description = $city->getDescription();
$country = $city->getCountry()->getName();
}
$hotelXmlPrices = $hotelXmlPriceRepository->getHotelXmlPriceByCriterias(['cities' => [$name]]);
foreach ($hotelXmlPrices as $HotelXmlPrice) {
$hotels[] = $frontService->getItemHotel($HotelXmlPrice);
}
return $this->render('front/hotel/hotel_by_city.html.twig', [
'hotels' => $hotels,
'city' => $city,
'name' => $name,
'country' => $country,
'society' => $this->parameterService->getSocietyParameters(),
'social_networks' => $this->frontService->getSocialNetworks(),
'agencies' => $this->frontService->getAgencies(),
'currencies' => $this->frontService->getCurrencies(),
'currencySwitcher' => true,
'tracking_tools' => $this->frontService->getTrackingTools(),
'faqs' => $this->frontService->getFaqs(FaqRouteEnum::APP_FRONT_HOTEL_CITY, strtolower($name)),
]);
}
#[Route('/hoteltheme/{name}', name: 'app_front_hotel_theme')]
public function hotelByTheme(
string $name,
FrontThemeRepository $frontThemeRepository,
HotelXmlPriceRepository $hotelXmlPriceRepository,
FrontService $frontService
): Response
{
$hotels = [];
$theme = $frontThemeRepository->findOneBy(['name' => $name]);
$description = "";
if ($theme) {
$description = $theme->getDescription();
}
$hotelXmlPrices = $hotelXmlPriceRepository->getHotelXmlPriceByCriterias(['themes' => $name]);
foreach ($hotelXmlPrices as $HotelXmlPrice) {
$hotels[] = $frontService->getItemHotel($HotelXmlPrice);
}
return $this->render('front/hotel/hotel_by_theme.html.twig', [
'hotels' => $hotels,
'description' => $description,
'name' => $name,
'society' => $this->parameterService->getSocietyParameters(),
'social_networks' => $this->frontService->getSocialNetworks(),
'agencies' => $this->frontService->getAgencies(),
'currencies' => $this->frontService->getCurrencies(),
'tracking_tools' => $this->frontService->getTrackingTools()
]);
}
#[Route('/search', name: 'hotel_search', methods: ['GET', 'POST'])]
public function hotelSearch(Request $request, FrontRecentSearchService $frontRecentSearchService): Response
{
$data = $request->get("data"); // data : JSON request
$requestData = json_decode($data, true);
$date = new DateTime();
// Output the date in default format
$currentDate = $date->format('Y-m-d'); // Example: 2025-01-02 15:30:45
if ($requestData['checkIn'] < $currentDate || $requestData['checkOut'] < $currentDate) {
return $this->redirectToRoute('app_main');
}
$customer = $this->apiAuthenticationService->getCustomer($this->getUser());
if (is_null($customer)) {
$this->addFlash('error', 'UNKNOWN_CUSTOMER');
return $this->redirectToRoute('front_info_message');
}
$hotel_search_history = $frontRecentSearchService->updateHotelSearchHistory($requestData, $data);
$isSearchByMap = $this->parameterService->isSearchMap();
return $this->render('front/hotel/search.html.twig', [
'requestData' => $requestData,
'customer' => $customer,
'society' => $this->parameterService->getSocietyParameters(),
'social_networks' => $this->frontService->getSocialNetworks(),
'agencies' => $this->frontService->getAgencies(),
'currencies' => $this->frontService->getCurrencies(),
'hotel_search_history' => $hotel_search_history,
'sections' => $this->frontService->getSections(),
'currencySwitcher' => true,
'tracking_tools' => $this->frontService->getTrackingTools(),
'isSearchByMap'=> $isSearchByMap,
]);
}
#[Route('/formsearch/{city_label}/{hotel_label}', name: 'hotel_form_search', methods: ['GET', 'POST'])]
public function hotelFormSearch(Request $request, string $city_label, string $hotel_label): Response
{
$requestData = json_decode($request->get("data"), true);
return $this->render('front/hotel/form_search.html.twig', [
'requestData' => $requestData,
'hotel_label' => $hotel_label,
'city_label' => $city_label
/* 'society' => $this->parameterService->getSocietyParameters(),
'social_networks' => $this->frontService->getSocialNetworks(),
'agencies' => $this->frontService->getAgencies(),
'currencies' => $this->frontService->getCurrencies(),*/
]);
}
#[Route('/search_json', name: 'hotel_search_json', methods: ['GET', 'POST'])]
public function hotelSearchJson(Request $request, HotelApiService $hotelApiService): JsonResponse
{
// ----- PREPARE the HttpRequestCall ----- //
// Get auth_data : user, timestamp, signature
$user = $this->getUser();
$auth_data = $this->apiAuthenticationService->getAuthenticationArray($user);
$customer = $this->apiAuthenticationService->getCustomer($user);
$showResellingRates = false;
if ($customer instanceof CustomerMoral) {
if ($customer->getResellerMargin() > 0 && (in_array('ROLE_B2B_ADMIN', $user->getRoles()) || in_array('ROLE_B2B_AGENT', $user->getRoles()))) {
$showResellingRates = true;
}
}
$requestJson = json_decode($request->getContent(), true);
$xmlSourceId = $request->headers->get('xmlSourceId') ?? 0;
$base_uri = $request->getSchemeAndHttpHost();
// ----- PERFORM the HttpRequestCall ----- //
$data = $hotelApiService->query_ApiHotel_Availability($auth_data, $requestJson, $xmlSourceId, $base_uri, true);
$today = DateTime::createFromFormat("Y-m-d", date("Y-m-d"));
if (count($data['content']) > 0) { // Re-format data to fit the FRONT-B2C :::: VIEW_BY_BOARD ::::
$nbRooms = count($requestJson['occupancies']);
$content_front = []; // prepare data to fit the front design
foreach ($data['content'] as $hotel) {
$hotel_key = strtoupper($hotel['hotel']['name']); // HOTEL_ITEM_IDENTIFIER TO GROUP MULTIPLE HOTELS WITH SAME NAME
$hotel_offer_count = 0;
if (isset($content_front[$hotel_key]['offersCount'])) {
$hotel_offer_count = $content_front[$hotel_key]['offersCount'];
}
$content_front[$hotel_key]['hotel'] = $hotel['hotel'];
$content_front[$hotel_key]['hotel']['customRating'] = $this->hotelApiService->getHotelCustomRating($hotel['hotel']['name']);
// TODO Add function getReviewRating(hotelName, reviewsSource);
$content_front[$hotel_key]['hotel']['reviewRating'] = null;//rand(4, 10); // TODO GET RATING FROM External src like Tripadvisor
if (count($hotel['sources']) > 0) {
$content_front[$hotel_key]['hotel']['facilities'] = $hotel['sources'][0]['facilities'];
}
$content_front[$hotel_key]['hotel']['promos'] = [];
$content_front[$hotel_key]['hotel']['discounts'] = [];
$content_front[$hotel_key]['hotel']['infos'] = [];
foreach ($hotel['sources'] as $hotel_source) {
$sourceKey = $hotel['hotel']['id']; // considering the full-hotel-id as the sourceKey (string containing the end-source-id)
$content_front[$hotel_key]['sources'][$sourceKey]['sourceKey'] = $sourceKey;
$hotelSourcePrice = 0;
$hotelSourceAvailable = false;
$hotelSourceFreeCancellation = false;
$tab_boards = [];
foreach ($hotel_source['rooms'] as $index => $index_hrts) { // room_index
// SORT hrts for current hotel-source by salePromoRate
usort($index_hrts, function ($a, $b) {
return $a['salePromoRate'] <=> $b['salePromoRate']; // Ascending
});
foreach ($index_hrts as $hrt) {
// freeCancellation FLAG to use in React Filter (i.e. is there a possibility to cancel without fees )
if ($hrt['NRF']) {
$hrt['freeCancellation'] = false;
} else {
if (is_null($hrt['deadline'])) {
$hrt['freeCancellation'] = true;
} else {
$deadline = new DateTime($hrt['deadline']);
$difference = date_diff($today, $deadline); // Interval before deadline.
if ($difference->invert) { // deadline is exceeded
$hrt['freeCancellation'] = false;
} else {
$hrt['freeCancellation'] = true;
}
}
}
$hotelSourceFreeCancellation = $hotelSourceFreeCancellation || $hrt['freeCancellation'];
// END freeCancellation FLAG
// SHOW SaleResellingRates rather thant saleRate applied to the customer
if ($showResellingRates) {
$hrt['salePromoRate'] = $hrt['salePromoRate'] * (100.0 + $customer->getResellerMargin()) / 100.0;
$hrt['saleRate'] = $hrt['saleRate'] * (100.0 + $customer->getResellerMargin()) / 100.0;
}
$content_front[$hotel_key]['sources'][$sourceKey]['hrts'][$hrt['boardName']][$index][] = $hrt;
$tab_boards[$hrt['boardName']] = $hrt['boardXmlName'];
// Increment the number of available-offers
if ($hrt['available']) {
$hotel_offer_count++;
$hotelSourceAvailable = true;
}
}
} // END Room Indexes
// HOTEL BADGES (Promos, Discounts, Infos)
foreach ($hotel_source['promos'] as $item) {
$content_front[$hotel_key]['hotel']['promos'][] = $item;
}
foreach ($hotel_source['discounts'] as $item) {
$content_front[$hotel_key]['hotel']['discounts'][] = $item;
}
foreach ($hotel_source['infos'] as $item) {
$content_front[$hotel_key]['hotel']['infos'][] = $item;
}
$bestOffers = $this->hotelApiService->getBestPriceBoard($content_front[$hotel_key], $sourceKey);
$content_front[$hotel_key]['sources'][$sourceKey]['bestPrice'] = $bestOffers['bestPrice_currentSourceKey'];
$content_front[$hotel_key]['bestPrice'] = $bestOffers['bestPrice'];
$content_front[$hotel_key]['bestBoard'] = $bestOffers['bestBoard'];
$content_front[$hotel_key]['bestRooms'] = !empty($sourceBest['bestRooms'])
? $this->helpers->formatArrayCounts($sourceBest['bestRooms'])
: '';
$content_front[$hotel_key]['offersCount'] = $hotel_offer_count;
foreach ($content_front[$hotel_key]['sources'][$sourceKey]['hrts'] as $board_key => $board_hrts) {
if (count($board_hrts) < $nbRooms) {
unset($content_front[$hotel_key]['sources'][$sourceKey]['hrts'][$board_key]);
}
}
foreach ($tab_boards as $board_key => $board_value) {
$content_front[$hotel_key]['sources'][$sourceKey]['boards'][] =
[
'key' => $board_value,
'value' => $board_value,
];
}
// set general stock
$content_front[$hotel_key]['sources'][$sourceKey]['generalStock'] = $hotel_source['generalStock'];
// set search code
$content_front[$hotel_key]['sources'][$sourceKey]['searchCode'] = $hotel_source['searchCode'];
// set source id
$content_front[$hotel_key]['sources'][$sourceKey]['xmlSourceId'] = $hotel_source['xmlSourceId'];
// set xml source name
$content_front[$hotel_key]['sources'][$sourceKey]['xmlSourceName'] = $hotel_source['xmlSourceName'];
// set hotel Source available
$content_front[$hotel_key]['sources'][$sourceKey]['available'] = $hotelSourceAvailable;
// set hotel Source available
$content_front[$hotel_key]['sources'][$sourceKey]['freeCancellation'] = $hotelSourceFreeCancellation;
// set hotel Source available
$content_front[$hotel_key]['sources'][$sourceKey]['associationRequired'] = $hotel_source['associationRequired'];
}
}
// array values hotels
$data['content'] = array_values($content_front);
// array values sources
foreach ($data['content'] as &$hotel) {
usort($hotel['sources'], function ($a, $b) {
return $a['bestPrice'] <=> $b['bestPrice']; // Ascending
});
$hotel['sources'] = array_values($hotel['sources']);
foreach ($hotel['sources'] as &$source) {
$source['hrts'] = array_values($source['hrts']);
}
}
}
return $this->json($data);
}
#[Route('/autocomplete', name: 'hotel_autocomplete', methods: ['GET'])]
public function autoComplete(Request $request): JsonResponse
{
$query = $request->query->get('query');
if ($query == null) {
return $this->json([]);
}
$query_trim = trim($query);
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
try {
$httpClient = HttpClient::create();
$response = $httpClient->request('GET', $request->getSchemeAndHttpHost() . '/api/hotel/autocomplete', [
'headers' => $headers,
'query' => ['query' => $query_trim]
]);
$data = json_decode($response->getContent(), true);
return $this->json($data['response']);
} catch (Exception $e) {
return $this->json(['error' => $e->getMessage()]);
}
}
/**
* The detail of selected offer from the front : hotel search results page. --- Check-Rate ---
* Supposed to be called either from "hotel/search or "orderline_add_room"
*/
#[Route('/offer-detail', name: 'hotel_offer_detail', methods: ['GET', 'POST'])]
public function hotelOfferDetail(
Request $request,
OrderLineRepository $orderLineRepository,
OrderRepository $orderRepository,
): Response
{
$guestData = [];
if ($request->isMethod('POST')) {
$guestData = [
'first_name' => $request->request->get('first_name'),
'last_name' => $request->request->get('last_name'),
'phone' => $request->request->get('phone'),
'email' => $request->request->get('email'),
];
$request->getSession()->set('guestData', $guestData);
}
$customer = $this->apiAuthenticationService->getCustomer($this->getUser());
$auth_data = $this->apiAuthenticationService->getAuthenticationArray($this->getUser());
$rooms_rate_keys = [];
for ($i = 0; $i < $request->get('nbRooms'); $i++) {
$rooms_rate_keys[] = ["rateKey" => $request->get("room-" . $i)];
}
$checkRateRequest = [
'rooms' => $rooms_rate_keys,
'searchCode' => $request->get('searchCode')
];
$saleCurrency = $this->currencyService->getSessionCurrency();
$product_fee = $this->frontService->getProductFee(ModuleEnum::hotel->getValue(), $customer);
/** call api/hotel/checkrate */
$httpClient = HttpClient::create();
$response = $httpClient->request(
'POST', $request->getUriForPath('/') . 'api/hotel/checkrate',
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'currency' => $saleCurrency,
'user' => $auth_data['user'],
'timestamp' => $auth_data['timestamp'],
'signature' => hash('sha256', $auth_data['signature'])
],
'json' => $checkRateRequest
]
);
$responseData = json_decode($response->getContent(), true);
$searchCriteriaArray = json_decode($request->get("searchCriteria"), true);
$twig_parameters = [
'searchCriteriaArray' => $searchCriteriaArray,
'searchCode' => null,
'checkRateData' => null,
"social_networks" => $this->frontService->getSocialNetworks(),
'menu_front' => $this->frontService->getFrontMenu(),
"currencies" => $this->frontService->getCurrencies(),
"agencies" => $this->frontService->getAgencies(),
"society" => $this->parameterService->getSocietyParameters(),
"referenceCurrency" => $this->parameterService->getReferenceCurrency(),
'guestData' => $guestData,
'action' => '',
'customer' => $customer
];
$template = "";
if (is_null($responseData) or $responseData['error']) {
// CASE 0 : Template for Error
return $this->render("front/hotel/offer_error.html.twig", $twig_parameters);
} else {
$checkRateData = reset($responseData['content']);
$checkRateData['available'] = $this->hotelApiService->isAvailable($checkRateData) ? "true" : "false";
$rooms = [];
foreach ($checkRateData['sources'][0]['rooms'] as $room_index) {
$rooms[] = $room_index[0]; // the selected room for the index.
}
//$checkRateData['cancellation'] = $this->hotelApiService->getDeadlineFromRooms($rooms);
$twig_parameters['checkRateData'] = $checkRateData;
$twig_parameters['searchCode'] = $responseData['searchCode'];
$twig_parameters['product_fee'] = $product_fee;
$twig_parameters['META_PIXEL_ID'] = $this->parameterService->getTrackerMetaId();
// CASE 1 : Template to add something to an existing orderline
if ($request->get('orderlineId')) {
$orderlineID = $request->get('orderlineId');
$twig_parameters['orderLine'] = $orderLineRepository->find($orderlineID);
$twig_parameters['action'] = $this->generateUrl("orderline_add_room", ['id' => $orderlineID]);
}
// CASE 2 : new Order and new OrderLine
if (empty($request->get('orderlineId')) && empty($request->get('orderId'))) { //
$twig_parameters['action'] = $this->generateUrl("app_admin_add_cart_line_hotel");
}
// CASE 3 : Add new OrderLine into an existing Order
if (empty($request->get('orderlineId')) && $request->get('orderId')) { //
$orderID = $request->get('orderId');
$twig_parameters['order'] = $orderRepository->find($orderID);
$twig_parameters['action'] = $this->generateUrl("order_add_orderline_hotel_book", ['id' => $orderID]);
}
return $this->render("front/hotel/offer_detail.html.twig", $twig_parameters);
}
}
/**
* The detail of a given hotelId
*/
#[Route('/{country_label}/{city_label}/{hotel_label}/{id}', name: 'hotel_detail', methods: ['GET'])]
public function hotelDetail(
Request $request,
$id,
string $city_label,
string $hotel_label,
HotelXmlRepository $hotelXmlRepository
): Response
{
$uid = $id; // source@uid
$hotel_source = strstr($uid, '@', true); // left subString before @
$hotel_uid = substr(strstr($uid, '@'), 1); // right subString after -
if ($hotel_source == 0) {
$hotelDetails = $this->hotelApiService->hotelDetails($hotel_uid, $request->getLocale());
} else {
$hotelDetails = $this->hotelApiService->hotelDetailsHUB($hotel_uid);
}
//$recommendedHotels = $hotelXmlRepository->getRecommendedHotels($city_label, $hotel_uid);
$recommendedHotels = [];
//dd(count($recommendedHotels));
$currentDateTime = new DateTime();
$arrivalDate = $currentDateTime->format('Y-m-d');//
$departureDate = new DateTime("tomorrow");
$departureDate = $departureDate->format('Y-m-d');
return $this->render('front/hotel/hotel_detail.html.twig', [
'hotel_data' => $hotelDetails,
'society' => $this->parameterService->getSocietyParameters(),
'social_networks' => $this->frontService->getSocialNetworks(),
'currencies' => $this->frontService->getCurrencies(),
'agencies' => $this->frontService->getAgencies(),
'menu_front' => $this->frontService->getFrontMenu(),
'recommendedHotels' => $recommendedHotels,
"arrivalDate" => $arrivalDate,
"departureDate" => $departureDate,
"city_label" => $city_label,
"hotel_label" => $hotel_label,
"META_PIXEL_ID" => $this->parameterService->getTrackerMetaId(),
'faqs' => $this->frontService->getFaqs(FaqRouteEnum::HOTEL_DETAIL, $uid),
]);
}
#[Route('/{id}/gallery', name: 'hotel_gallery_json', methods: ['GET'])]
public function hotelGallery(
Request $request,
$id
): JsonResponse
{
$auth_data = $this->apiAuthenticationService->getAuthenticationArray($this->getUser());
// call hotel detail and get images
$hotelDetailsRequest = [
'hotelId' => $id
];
$httpClient = HttpClient::create();
$response = $httpClient->request(
'POST', $request->getUriForPath('/') . 'api/hotel/details',
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'user' => $auth_data['user'],
'timestamp' => $auth_data['timestamp'],
'signature' => hash('sha256', $auth_data['signature'])
],
'json' => $hotelDetailsRequest
]
);
$responseData = json_decode($response->getContent(), true);
if (is_null($responseData) || !key_exists('content', $responseData)) {
return $this->json([]);
}
$hotel_data_array = $responseData['content']['images'];
return $this->json($hotel_data_array);
}
/**
* @throws Exception
*/
#[Route('/promo', name: 'app_front_hotel_promo', methods: ['GET'])]
public function hotelPromo(HotelXmlPriceRepository $hotelXmlPriceRepository,
FrontService $frontService
): Response
{
$hotels = [];
$hotelXmlPricesWithPromos = $hotelXmlPriceRepository->getHotelXmlPriceWithPromos();
foreach ($hotelXmlPricesWithPromos as $hotelXmlPricesWithPromo) {
$hotels[] = $frontService->getItemHotel($hotelXmlPricesWithPromo);
}
return $this->render('front/hotel/hotel_promo.html.twig', [
'hotelsWithPromos' => $hotels,
'social_networks' => $this->frontService->getSocialNetworks(),
'currencies' => $this->frontService->getCurrencies(),
'agencies' => $this->frontService->getAgencies(),
'society' => $this->parameterService->getSocietyParameters(),
'currencySwitcher' => true,
'META_PIXEL_ID' => $this->parameterService->getTrackerMetaId()
]);
}
#[Route('/cart/addProductHotel', name: 'app_admin_add_cart_line_hotel', methods: ['POST'])]
public function addProductHotel(
Request $request,
SessionInterface $session,
CartService $cartService,
): RedirectResponse
{
$cart_array = $cartService->initCart($this->getUser(), $request);
/* 2- add a line into cart_array['products'] */
$nbRooms = $request->get("nbRooms"); // quantity : nbRooms
$cart_line_label = $request->get('product_label');
$cart_line_date = $request->get('product_date');
$beneficiaryName = $request->get('beneficiary_name');
if (empty($beneficiaryName)) {
$beneficiaryFirstName = trim((string) $request->get('beneficiary_first_name', ''));
$beneficiaryLastName = trim((string) $request->get('beneficiary_last_name', ''));
$beneficiaryName = trim($beneficiaryFirstName . ' ' . $beneficiaryLastName);
}
if (empty($beneficiaryName)) {
$beneficiaryName = trim(
(string) $request->get('room_0_pax_0_firstName', '') . ' ' .
(string) $request->get('room_0_pax_0_lastName', '')
);
}
$beneficiaryEmail = $request->get('beneficiary_email');
$product_price = $request->get("totalAmount");
$productFee = $request->get('product_fee') ?? 0;
$dates = explode('-', $cart_line_date);
$date1 = DateTime::createFromFormat('d/m/Y', $dates[0]);
$date2 = DateTime::createFromFormat('d/m/Y', $dates[1]);
$interval = $date1->diff($date2);
$nbNights = $interval->days;
$hotel_with_party = false;
$product_elements = null;
for ($room_idx = 0; $room_idx < $nbRooms; $room_idx++) {
/* guests */
$room_guests = [];
$room_adults_count = $request->get("room_" . $room_idx . "_adults_count");
$room_children_count = $request->get("room_" . $room_idx . "_children_count");
$room_guest_count = $room_adults_count + $room_children_count;
for ($guest_index = 0; $guest_index < $room_guest_count; $guest_index++) {
$paxCivility = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_civility");
$paxFirstName = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_firstName");
$paxLastName = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_lastName");
$paxAge = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_age");
if (!empty($paxFirstName) || !empty($paxLastName)) {
// Use the detailed per-pax data
$guest = [
'civility' => $paxCivility ?? '',
'firstName' => $paxFirstName ?? '',
'lastName' => $paxLastName ?? '',
'age' => $paxAge,
];
} else {
// Fallback: use the primary beneficiary name (backward compatibility)
$guest = [
'civility' => '',
'firstName' => $beneficiaryName,
'lastName' => '-',
];
}
$room_guests[] = $guest;
}
$room = [
'rateKey' => $request->get("room_" . $room_idx . "_key"),
'label' => $request->get("room_" . $room_idx . "_label"),
'isAvailable' => $request->get("room_" . $room_idx . "_available"),
'price' => $request->get("room_" . $room_idx . "_price"),
'adultsCount' => $room_adults_count,
'childrenCount' => $room_children_count,
'guests' => $room_guests
];
$room_nb_parties = $request->get("room_" . $room_idx . "_nbParties");
$room_parties_zones = [];
if (!is_null($room_nb_parties)) { // if there is parties associated to the current room
$hotel_with_party = true;
for ($party_index = 0; $party_index < $room_nb_parties; $party_index++) {
$room_parties_zones[] = $request->get("room_" . $room_idx . "_party_" . $party_index);
}
$room['parties'] = $room_parties_zones;
//$product_price+= $request->get("partyPrice_".$party_index);
}
$product_elements[] = $room;
}
if ($request->isMethod('POST')) {
$guestData = [
'first_name' => $request->request->get('first_name'),
'last_name' => $request->request->get('last_name'),
'phone' => $request->request->get('phone'),
'email' => $request->request->get('email'),
];
$request->getSession()->set('guestData', $guestData);
}
$cart_line_hotel = [
'module' => ModuleEnum::hotel->getValue(),
'elements' => $product_elements,
'label' => $cart_line_label,
'date' => $cart_line_date,
'checkIn' => $dates[0],
'checkOut' => $dates[1],
'nbNights' => $nbNights,
'quantity' => $nbRooms,
'options' => $request->get("options"),
'comment' => $request->get("comment"),
'price' => $product_price,
'fee' => $productFee,
'available' => $request->get('available'),
'searchCode' => $request->get('searchCode'),
'tokenForBook' => $request->get('tokenForBook'),
'beneficiary' => [
'name' => $beneficiaryName,
'email' => $beneficiaryEmail
],
'image' => $request->get('hotel_image'),
'city' => $request->get('city'),
'country' => $request->get('country'),
'rating' => $request->get('rating'),
'guestData' => $guestData,
];
$cart_array['products'][] = $cart_line_hotel; // add a cart line ( a product )
$session->set("cart", $cart_array);
//return $this->redirectToRoute('app_shared_cart_index', [], Response::HTTP_SEE_OTHER);
return $this->redirectToRoute('app_shared_cart_hotel_product_index', [], Response::HTTP_SEE_OTHER);
}
private function getCountPages($cityId, $request,$auth_data): array
{
$GetPagesRequest = [
'cityId' => $cityId
];
$httpClient = HttpClient::create();
$response= $httpClient->request(
'POST', $request->getUriForPath('/') . 'api/hotel/getPages',
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'user' => $auth_data['user'],
'timestamp' => $auth_data['timestamp'],
'signature' => hash('sha256', $auth_data['signature'])
],
'json' => $GetPagesRequest
]
);
$responseData = json_decode($response->getContent(), true);
if (
!is_array($responseData) ||
!array_key_exists('content', $responseData)
) {
return [];
}
return $responseData['content'];
}
#[Route('/search_stream', name: 'hotel_search_stream', methods: ['POST'])]
public function hotelSearchStream(
Request $request,
HotelApiService $hotelApiService
): StreamedResponse
{
$requestJson = json_decode($request->getContent(), true);
$xmlSourceId = $request->headers->get('xmlSourceId') ?? 0;
$isAllowedPagination = $request->headers->get('isAllowedPagination') ?? 0;
$user = $this->getUser();
$auth_data = $this->apiAuthenticationService->getAuthenticationArray($user);
$customer = $this->apiAuthenticationService->getCustomer($user);
$showResellingRates = $this->shouldShowResellingRates($customer, $user);
$request->getSession()?->save();
$nbRooms = count($requestJson['occupancies'] ?? []);
$baseUri = $request->getSchemeAndHttpHost();
return new StreamedResponse(function () use (
$requestJson,
$xmlSourceId,
$auth_data,
$customer,
$showResellingRates,
$nbRooms,
$baseUri,
$hotelApiService,
$request,$isAllowedPagination
) {
// =========================================================
// SSE INIT
// =========================================================
set_time_limit(0);
while (ob_get_level() > 0) {
ob_end_clean();
}
@ini_set('output_buffering', '0');
@ini_set('zlib.output_compression', '0');
@ini_set('implicit_flush', '1');
ob_implicit_flush(true);
$startTime = microtime(true);
$idSearch = $request->query->get('idSearch') ?? bin2hex(random_bytes(8));
$cacheKey = self::UNNORMALIZED_CACHE_PREFIX . $idSearch;
// =========================================================
// HELPER : ANNULER TOUTES LES REQUETES ENCORE EN VOL
// =========================================================
$cancelAllResponses = function (array $responses): void {
foreach ($responses as $r) {
try {
$r->cancel();
} catch (\Throwable $e) {
// on ignore : la requête peut déjà être terminée/annulée
}
}
};
// =========================================================
// HELPER : PERSISTER LES DONNEES UNNORMALIZED (une seule fois)
// =========================================================
$persistUnnormalizedChunk = function (array $hotelsUnnormalizedData) use ($cacheKey): void {
if (empty($hotelsUnnormalizedData)) {
return;
}
try {
$item = $this->searchCachePool->getItem($cacheKey);
$existing = $item->isHit() ? $item->get() : [];
foreach ($hotelsUnnormalizedData as $entry) {
if ($entry['unnormalizedData'] == "") {
continue;
}
$hotelKey = $entry['hotelKey'];
$existing[$hotelKey] = array_merge(
$existing[$hotelKey] ?? [],
[$entry['unnormalizedData']]
);
}
$item->set($existing);
$item->expiresAfter(self::UNNORMALIZED_CACHE_TTL);
$this->searchCachePool->save($item);
$this->logger->info('SSE: unnormalized rooms chunk cached', [
'cacheKey' => $cacheKey,
'hotelCount' => count($existing),
]);
} catch (\Throwable $e) {
$this->logger->error('SSE: failed to cache unnormalized rooms chunk', [
'cacheKey' => $cacheKey,
'message' => $e->getMessage(),
]);
}
};
$this->logger->info('SSE: callback START');
echo ": heartbeat\n\n";
flush();
$this->logger->info('SSE: heartbeat SENT');
// =========================================================
// CONFIG
// =========================================================
$totalLoaded = 0;
$httpClient = HttpClient::create(
[
'timeout' => 120,
'max_duration' => 180,
],
30
);
// =========================================================
// HELPER : ENVOYER LES HOTELS EN SSE
// Retourne false si la connexion a été perdue en cours de route,
// pour que l'appelant sache qu'il doit annuler et sortir.
// =========================================================
$flushContent = function (array $content, int $pageNumber) use (
&$totalLoaded,
$persistUnnormalizedChunk,
$nbRooms,
$showResellingRates,
$customer,
$request
): bool {
$metaBatchSize = 50;
if (empty($content)) {
return true;
}
$chunkData = $this->buildContentFront(
$content,
$nbRooms,
$showResellingRates,
$customer
);
$metaHotels = $chunkData['meta'];
$hotelsUnnormalizedData = $chunkData['hotelsUnnormalizedData'];
// --- persistance immédiate, page consultable dès maintenant ---
$persistUnnormalizedChunk($hotelsUnnormalizedData);
$metaBuffer = [];
foreach ($metaHotels as $hotel) {
$metaBuffer[] = $hotel;
$totalLoaded++;
if (count($metaBuffer) >= $metaBatchSize) {
$this->emitSSE('hotels_meta', [
'page' => $pageNumber,
'hotels' => $metaBuffer,
]);
$metaBuffer = [];
}
if (connection_aborted()) {
return false;
}
}
if (!empty($metaBuffer)) {
$this->emitSSE('hotels_meta', [
'page' => $pageNumber,
'hotels' => $metaBuffer,
]);
}
unset($metaBuffer);
if (connection_aborted()) {
return false;
}
unset($metaHotels, $chunkData, $hotelsUnnormalizedData);
return true;
};
// =========================================================
// STATUS : RECHERCHE EN COURS
// =========================================================
$this->emitSSE('status', [
'status' => 'searching',
'message' => 'Recherche des disponibilités...',
'idSearch' => $idSearch,
]);
$responses = [];
$pagesCount = 1;
if ($xmlSourceId != 0 && $isAllowedPagination!=0) {
$t = microtime(true);
try {
$pages = $this->getCountPages($requestJson['cityId'] ?? '', $request, $auth_data);
$pagesCount = (int)($pages['pages_count'] ?? 1);
} catch (\Throwable $e) {
$this->logger->error('SSE: getCountPages failed', ['message' => $e->getMessage()]);
}
$this->logger->info('SSE: getCountPages duration', [
'ms' => round((microtime(true) - $t) * 1000),
]);
}
// =========================================================
// LANCEMENT DES PAGES 1..N
// =========================================================
try {
if (!connection_aborted()) {
for ($p = 1; $p <= $pagesCount; $p++) {
if (connection_aborted()) {
$cancelAllResponses($responses);
return; // pas de persistUnnormalized() ici
}
$req = $requestJson;
$req['page'] = $p;
$responses[$p] = $hotelApiService->query_ApiHotel_Availability_Async(
$auth_data,
$req,
$xmlSourceId,
$baseUri,
$httpClient,
true
);
}
}
} catch (\Throwable $e) {
$this->logger->error('SSE: error launching pages', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// annuler ce qui a déjà été lancé avant l'exception ---
$cancelAllResponses($responses);
return;
}
// =========================================================
// PAGES 1..N : réception au fil de l'eau
// On boucle avec un while : si une page échoue en erreur HTTP
// (4xx/5xx, ex: 504), Symfony HttpClient lève l'exception au
// niveau du foreach lui-même (donc hors de portée du try/catch
// interne). On identifie la page fautive via $e->getResponse(),
// on la retire, et on relance stream() sur les pages restantes
// au lieu d'abandonner tout le batch.
// =========================================================
if (!empty($responses) && !connection_aborted()) {
$remainingResponses = $responses;
while (!empty($remainingResponses)) {
try {
foreach ($httpClient->stream($remainingResponses, 120) as $response => $chunk) {
if (connection_aborted()) {
$this->logger->info('SSE: connection aborted', [
'totalLoaded' => $totalLoaded,
]);
$cancelAllResponses($remainingResponses);
return; // pas de persistUnnormalized()
}
$pageNumber = null;
foreach ($responses as $page => $pageResponse) {
if ($pageResponse === $response) {
$pageNumber = $page;
break;
}
}
try {
if ($chunk->isTimeout()) {
$this->logger->warning('SSE: page request timeout', [
'page' => $pageNumber,
]);
continue;
}
if (!$chunk->isLast()) {
continue;
}
$pageStart = microtime(true);
$raw = $response->getContent(false);
// la page a répondu (avec ou sans contenu) -> on la retire des "en attente"
unset($remainingResponses[$pageNumber]);
if (empty($raw)) {
$this->logger->warning('SSE: empty response', [
'page' => $pageNumber,
]);
continue;
}
$data = json_decode($raw, true);
if (!is_array($data)) {
$this->logger->warning('SSE: invalid JSON', [
'page' => $pageNumber,
]);
continue;
}
$content = $data['content'] ?? [];
if (empty($content)) {
$this->logger->info('SSE: empty content', [
'page' => $pageNumber,
]);
continue;
}
// -------------------------------------------------
// TRAITER ET ENVOYER LA PAGE
// -------------------------------------------------
$stillConnected = $flushContent($content,$pageNumber);
if (!$stillConnected) {
$this->logger->info('SSE: connection aborted during flushContent', [
'page' => $pageNumber,
'totalLoaded' => $totalLoaded,
]);
$cancelAllResponses($remainingResponses);
return;
}
$this->emitSSE('progress', [
'page' => $pageNumber,
'pagesCount' => $pagesCount,
'loaded' => $totalLoaded,
]);
unset($data, $content, $raw);
} catch (\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface $e) {
// filet de sécurité si ça arrive ici pour une page identifiée
$this->logger->error('SSE: page request failed (inner)', [
'page' => $pageNumber,
'message' => $e->getMessage(),
]);
unset($remainingResponses[$pageNumber]);
continue;
} catch (\Throwable $e) {
$this->logger->error('SSE: error processing page', [
'page' => $pageNumber,
'message' => $e->getMessage(),
]);
unset($remainingResponses[$pageNumber]);
continue;
}
}
// foreach terminé sans exception fatale -> toutes les pages restantes traitées
break;
} catch (\Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface $e) {
// Cas du 504 : l'exception a échappé au try/catch interne car levée
// directement par le générateur stream(), avant l'entrée dans le foreach.
// getResponse() permet d'identifier précisément quelle page a échoué.
$failedResponse = $e->getResponse();
$failedPage = null;
foreach ($remainingResponses as $page => $r) {
if ($r === $failedResponse) {
$failedPage = $page;
break;
}
}
if ($failedPage !== null) {
$this->logger->error('SSE: page failed with HTTP error, removing and continuing', [
'page' => $failedPage,
'status' => $e->getResponse()->getStatusCode(),
'message' => $e->getMessage(),
]);
unset($remainingResponses[$failedPage]);
// on relance stream() avec les pages restantes
continue;
}
$this->logger->error('SSE: HTTP error, page not identified, stopping to avoid infinite loop', [
'message' => $e->getMessage(),
]);
$cancelAllResponses($remainingResponses);
break;
} catch (\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface $e) {
// erreur de transport pure (DNS, connexion refusée, etc.) sans réponse HTTP
// associée -> impossible d'identifier la page fautive de façon fiable
$this->logger->error('SSE: transport error streaming pages, stopping to avoid infinite loop', [
'message' => $e->getMessage(),
]);
$cancelAllResponses($remainingResponses);
break;
} catch (\Throwable $e) {
$this->logger->error('SSE: fatal error streaming pages', [
'message' => $e->getMessage(),
]);
$cancelAllResponses($remainingResponses);
break;
}
}
unset($remainingResponses);
}
// =========================================================
// FIN : stream terminé normalement
// =========================================================
$duration = round(microtime(true) - $startTime, 2);
$this->emitSSE('progress', [
'page' => $pagesCount,
'pagesCount' => $pagesCount,
'loaded' => $totalLoaded,
]);
$this->emitSSE('done', [
'total' => $totalLoaded,
'idSearch' => $idSearch,
'message' => 'stream completed',
'time_seconds' => $duration,
]);
unset($responses);
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache, no-transform',
'X-Accel-Buffering' => 'no',
'Connection' => 'keep-alive',
]);
}
private function emitSSE(string $event, mixed $data): void
{
// 1. JSON encode
$payload = json_encode(
$data,
JSON_UNESCAPED_UNICODE |
JSON_UNESCAPED_SLASHES |
JSON_PARTIAL_OUTPUT_ON_ERROR
);
if ($payload === false) {
$payload = json_encode([
'error' => 'json encode failed'
]);
}
// 2. Construction SSE
$output =
"event: {$event}\n" .
"data: {$payload}\n\n";
// 3. Echo
echo $output;
// 4. Output buffer
$bufferLength = ob_get_length();
if ($bufferLength !== false && $bufferLength > 0) {
@ob_flush();
}
// 5. PHP flush
@flush();
}
private function buildContentFront(
array $hotels,
int $nbRooms,
bool $showResellingRates,
mixed $customer, bool $isGetOffer = false
): array
{
$today = new DateTime();
$content_front = [];
$hotelNames = array_unique(array_map(
fn($hotel) => $hotel['hotel']['name'],
$hotels
));
$customRatingsByName = $this->hotelApiService->getHotelCustomRatings($hotelNames);
foreach ($hotels as $hotel) {
$hotel_key = strtoupper($hotel['hotel']['name']);
$sourceKey = $hotel['hotel']['id'];
if (!$isGetOffer) {
$unnormalized = $hotel['hotel']['unnormalizedHotelData'] ?? null;
unset($hotel['hotel']['unnormalizedHotelData']);
}
$hotel_offer_count = 0;
if (isset($content_front[$hotel_key]['offersCount'])) {
$hotel_offer_count = $content_front[$hotel_key]['offersCount'];
}
if (!isset($content_front[$hotel_key]['hotel'])) {
$content_front[$hotel_key]['hotel'] = $hotel['hotel'];
$content_front[$hotel_key]['hotel']['promos'] = [];
$content_front[$hotel_key]['hotel']['discounts'] = [];
$content_front[$hotel_key]['hotel']['infos'] = [];
$content_front[$hotel_key]['hotel']['customRating'] = $customRatingsByName[$hotel['hotel']['name']] ?? null;
$content_front[$hotel_key]['hotel']['reviewRating'] = rand(4, 10);
$content_front[$hotel_key]['hotel']['HotelDataUnnormalizedTmp'] = [];
}
if (!$isGetOffer) {
$content_front[$hotel_key]['hotel']['HotelDataUnnormalizedTmp'][] = $unnormalized;
}
if (count($hotel['sources']) > 0) {
$content_front[$hotel_key]['hotel']['facilities'] = $hotel['sources'][0]['facilities'];
}
foreach ($hotel['sources'] as $hotel_source) {
$content_front[$hotel_key]['sources'][$sourceKey]['sourceKey'] = $sourceKey;
$hotelSourcePrice = 0;
$hotelSourceAvailable = false;
$hotelSourceFreeCancellation = false;
$tab_boards = [];
foreach ($hotel_source['rooms'] as $index => $index_hrts) {
usort($index_hrts, function ($a, $b) {
return $a['salePromoRate'] <=> $b['salePromoRate'];
});
foreach ($index_hrts as $hrt) {
if ($hrt['NRF']) {
$hrt['freeCancellation'] = false;
} else {
if (is_null($hrt['deadline'])) {
$hrt['freeCancellation'] = true;
} else {
$deadline = new DateTime($hrt['deadline']);
$difference = date_diff($today, $deadline);
if ($difference->invert) {
$hrt['freeCancellation'] = false;
} else {
$hrt['freeCancellation'] = true;
}
}
}
$hotelSourceFreeCancellation = $hotelSourceFreeCancellation || $hrt['freeCancellation'];
if ($showResellingRates) {
$hrt['salePromoRate'] = $hrt['salePromoRate'] * (100.0 + $customer->getResellerMargin()) / 100.0;
$hrt['saleRate'] = $hrt['saleRate'] * (100.0 + $customer->getResellerMargin()) / 100.0;
}
$content_front[$hotel_key]['sources'][$sourceKey]['hrts'][$hrt['boardName']][$index][] = $hrt;
$tab_boards[$hrt['boardName']] = $hrt['boardXmlName'];
if ($hrt['available']) {
$hotel_offer_count++;
$hotelSourceAvailable = true;
}
}
} // END Room Indexes
foreach ($hotel_source['promos'] as $item) {
$content_front[$hotel_key]['hotel']['promos'][] = $item;
}
foreach ($hotel_source['discounts'] as $item) {
$content_front[$hotel_key]['hotel']['discounts'][] = $item;
}
foreach ($hotel_source['infos'] as $item) {
$content_front[$hotel_key]['hotel']['infos'][] = $item;
}
$currentSourceHrts = $content_front[$hotel_key]['sources'][$sourceKey]['hrts'] ?? [];
$sourceBest = $this->hotelApiService->getBestPriceBoardForSource([
'hrts' => $currentSourceHrts,
]);
$content_front[$hotel_key]['sources'][$sourceKey]['bestPrice'] = $sourceBest['bestPrice'];
$currentHotelBest = $content_front[$hotel_key]['bestPrice'] ?? null;
if ($currentHotelBest === null || ($sourceBest['bestPrice'] !== null && $sourceBest['bestPrice'] < $currentHotelBest)) {
$content_front[$hotel_key]['bestPrice'] = $sourceBest['bestPrice'];
$content_front[$hotel_key]['bestBoard'] = $sourceBest['bestBoard'];
$content_front[$hotel_key]['bestRooms'] = !empty($sourceBest['bestRooms'])
? $this->helpers->formatArrayCounts($sourceBest['bestRooms'])
: '';
}
$content_front[$hotel_key]['offersCount'] = $hotel_offer_count;
foreach ($content_front[$hotel_key]['sources'][$sourceKey]['hrts'] as $board_key => $board_hrts) {
if (count($board_hrts) < $nbRooms) {
unset($content_front[$hotel_key]['sources'][$sourceKey]['hrts'][$board_key]);
}
}
foreach ($tab_boards as $board_key => $board_value) {
$content_front[$hotel_key]['sources'][$sourceKey]['boards'][] =
[
'key' => $board_value,
'value' => $board_value,
];
}
$content_front[$hotel_key]['sources'][$sourceKey]['generalStock'] = $hotel_source['generalStock'];
$content_front[$hotel_key]['sources'][$sourceKey]['searchCode'] = $hotel_source['searchCode'];
$content_front[$hotel_key]['sources'][$sourceKey]['xmlSourceId'] = $hotel_source['xmlSourceId'];
$content_front[$hotel_key]['sources'][$sourceKey]['xmlSourceName'] = $hotel_source['xmlSourceName'];
$content_front[$hotel_key]['sources'][$sourceKey]['available'] = $hotelSourceAvailable;
$content_front[$hotel_key]['sources'][$sourceKey]['freeCancellation'] = $hotelSourceFreeCancellation;
$content_front[$hotel_key]['sources'][$sourceKey]['associationRequired'] = $hotel_source['associationRequired'];
}
}
$hotels = array_values($content_front);
foreach ($hotels as &$hotel) {
usort($hotel['sources'], function ($a, $b) {
return $a['bestPrice'] <=> $b['bestPrice'];
});
$hotel['sources'] = array_values($hotel['sources']);
foreach ($hotel['sources'] as &$source) {
$source['hrts'] = array_values($source['hrts']);
}
}
if (!$isGetOffer) {
[$hotelsMeta, $hotelsUnnormalizedData] = $this->splitMetaAndRates($hotels);
return [
'meta' => $hotelsMeta,
'hotelsUnnormalizedData' => $hotelsUnnormalizedData
];
} else {
return $hotels;
}
}
private function splitMetaAndRates(array $hotels): array
{
$hotelsMeta = [];
$hotelsUnnormalizedData = [];
foreach ($hotels as $hotel) {
$hotelKey = strtoupper($hotel['hotel']['name']);
foreach (($hotel['hotel']['HotelDataUnnormalizedTmp'] ?? []) as $key => $data) {
$hotelsUnnormalizedData[] = [
'hotelKey' => $hotelKey,
'unnormalizedData' => $data,
];
};
unset($hotel['hotel']['HotelDataUnnormalizedTmp']);
$hotelsMeta[] = [
'hotelKey' => $hotelKey,
'hotel' => $hotel['hotel'],
'bestPrice' => $hotel['bestPrice'] ?? null,
'bestBoard' => $hotel['bestBoard'] ?? null,
'bestRooms' => $hotel['bestRooms'] ?? '',
'offersCount' => $hotel['offersCount'] ?? 0
];
}
return [$hotelsMeta, $hotelsUnnormalizedData];
}
private function getUnnormalizedRoomsData(
string $idSearch,
array $hotelsKey
): array {
$item = $this->searchCachePool->getItem(self::UNNORMALIZED_CACHE_PREFIX . $idSearch);
if (!$item->isHit()) {
return [];
}
$data = $item->get();
$dataToNormalize = [];
foreach ($hotelsKey as $hotelName) {
$hotelKey = strtoupper($hotelName);
$encoded = $data[$hotelKey] ?? null;
if ($encoded === null) {
continue;
}
foreach ($encoded as $value) {
$dataToNormalize[] = unserialize(gzuncompress(base64_decode($value)));
}
}
return $dataToNormalize;
}
#[Route('/getOfferDetails', name: 'hotel_offer_details', methods: ['POST'])]
public function getRateDetail(Request $request): JsonResponse
{
// 3- User & Customer Authentication & Authorization
$user = $this->getUser();
$auth_data = $this->apiAuthenticationService->getAuthenticationArray($user);
$customer = $this->apiAuthenticationService->getCustomer($user);
$currency = $customer->getCurrency();
$body = json_decode($request->getContent(), true);
$idSearch = $body['idSearch'] ?? null;
$hotelsKey = $body['hotelsKey'] ?? null;
$nbRooms = $body['nbRooms'] ?? 1;
$localSearchCode = uniqid(more_entropy: true) ?? null;
$showResellingRates = $this->shouldShowResellingRates($customer, $user);
if (!$idSearch || !$hotelsKey) {
return $this->json(['error' => 'Missing parameters'], 400);
}
// 1 & 2. Charger le cache une seule fois et récupérer les données de tous les hôtels demandés
$dataToNormalize = $this->getUnnormalizedRoomsData($idSearch, $hotelsKey);
if (empty($dataToNormalize)) {
return $this->json([
'error' => 'rate_expired'
], 404);
}
// 3. Normalisation en un seul appel
$hotelsNormalizedHub = $this->hotelApiService->normalizeSearchResultsFromHubFormat(
$dataToNormalize,
$currency,
$localSearchCode
);
// 4. Formatage en un seul appel
$this->hotelApiService->formatHotelAvailabilityArray($hotelsNormalizedHub);
// 5. Marges & exclusions en un seul appel
$this->hotelApiService->applyMarginsAndHideExcludedHotelXml($hotelsNormalizedHub, $customer);
// 6. Construction du contenu front en un seul appel
$hotelsToNormalize = $this->buildContentFront(
$hotelsNormalizedHub,
$nbRooms,
$showResellingRates,
$customer,
true
);
if (empty($hotelsToNormalize)) {
return $this->json([
'error' => 'rate_expired'
], 404);
}
return $this->json($hotelsToNormalize);
}
/**
* Déterminer si les tarifs revendeur doivent être affichés.
*/
private function shouldShowResellingRates(mixed $customer, mixed $user): bool
{
if (!$customer instanceof CustomerMoral) {
return false;
}
if ($customer->getResellerMargin() <= 0) {
return false;
}
$roles = $user->getRoles();
return in_array('ROLE_B2B_ADMIN', $roles, true)
|| in_array('ROLE_B2B_AGENT', $roles, true);
}
}