<?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;
#[Route('/hotel')]
class HotelController extends AbstractController
{
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,
)
{
}
#[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);
$frontMenus = $this->frontService->getFrontMenu();
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()
]);
}
#[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'] = $this->helpers->formatArrayCounts($bestOffers['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,
ParameterService $parameterService,
HotelXmlRepository $hotelXmlRepository
): Response
{
$hotel_id_elements = explode("|", $id);
$xmlSourceId = $hotel_id_elements[0];
// dd($xmlSourceId);
$codeHotel = $hotel_id_elements[2];
if ($xmlSourceId == 0) {
$hotelDetails = $this->hotelApiService->hotelDetails($codeHotel, $request->getLocale());
} else {
$hotelDetails = $this->hotelApiService->hotelDetailsHUB($id);
}
$recommendedHotels = $hotelXmlRepository->getRecommendedHotels($city_label, $id);
$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, $id),
]);
}
#[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);
}
#[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;
$user = $this->getUser();
$auth_data = $this->apiAuthenticationService->getAuthenticationArray($user);
$customer = $this->apiAuthenticationService->getCustomer($user);
$showResellingRates = $this->shouldShowResellingRates($customer, $user);
$nbRooms = count($requestJson['occupancies'] ?? []);
$baseUri = $request->getSchemeAndHttpHost();
return new StreamedResponse(function () use (
$requestJson, $xmlSourceId, $auth_data, $customer,
$showResellingRates, $nbRooms, $baseUri, $hotelApiService, $request
) {
$startTime = microtime(true);
// ════════════════════════════════════════════════
// SSE INIT
// ════════════════════════════════════════════════
$request->getSession()?->save();
@ini_set('output_buffering', '0');
@ini_set('zlib.output_compression', '0');
@ini_set('implicit_flush', '1');
while (ob_get_level() > 0) ob_end_clean();
ob_implicit_flush(true);
echo ":" . str_repeat(" ", 2048) . "\n\n";
flush();
// ════════════════════════════════════════════════
// CONFIG
// ════════════════════════════════════════════════
$batchSize = 150;
$totalLoaded = 0;
$httpClient = HttpClient::create([
'timeout' => 120,
'max_duration' => 180,
]);
// ════════════════════════════════════════════════
// HELPER — flush hôtels en SSE
// ════════════════════════════════════════════════
$flushContent = function (array $content) use (
&$totalLoaded, $nbRooms, $showResellingRates, $customer, $batchSize
): void {
if (empty($content)) return;
$chunkData = $this->buildContentFront(
$content, $nbRooms, $showResellingRates, $customer
);
$buffer = [];
foreach ($chunkData as $hotel) {
$buffer[] = $hotel;
$totalLoaded++;
if (count($buffer) >= $batchSize) {
$this->emitSSE('hotels_batch', $buffer);
$buffer = [];
}
if (connection_aborted()) return;
}
if (!empty($buffer)) {
$this->emitSSE('hotels_batch', $buffer);
}
unset($buffer, $chunkData);
};
// ════════════════════════════════════════════════
// PAGE 1 — bloquant pour récupérer pagesCount
// ════════════════════════════════════════════════
try {
$req1 = $requestJson;
$req1['page'] = 1;
$response1 = $hotelApiService->query_ApiHotel_Availability_Async(
$auth_data, $req1, $xmlSourceId, $baseUri, $httpClient, true
);
$raw1 = $response1->getContent(false);
$data1 = json_decode($raw1, true);
$content1 = $data1['content'] ?? [];
if (empty($content1)) {
$this->logger->info('content-page-1',[$content1]);
$this->emitSSE('done', [
'total' => 0,
'message' => 'empty result set',
'time_seconds' => round(microtime(true) - $startTime, 2),
]);
return;
}
$flushContent($content1);
$pagesCount = (int)($data1['pagesCount'] ?? 0);
$this->emitSSE('progress', [
'page' => 1,
'pagesCount' => $pagesCount,
'loaded' => $totalLoaded,
]);
unset($data1, $content1, $response1, $req1);
} catch (\Throwable $e) {
$this->emitSSE('done', [
'total' => 0,
'message' => 'error page 1: ' . $e->getMessage(),
'time_seconds' => round(microtime(true) - $startTime, 2),
]);
return;
}
if ($pagesCount <= 1) {
$this->emitSSE('done', [
'total' => $totalLoaded,
'message' => 'completed (single page)',
'time_seconds' => round(microtime(true) - $startTime, 2),
]);
return;
}
// ════════════════════════════════════════════════
// PAGES 2..N — toutes en parallèle d'un seul coup
// ════════════════════════════════════════════════
if ($pagesCount > 1 && !connection_aborted()) {
// Lancer pages 2..pagesCount toutes simultanément
$responses = [];
for ($p = 2; $p <= $pagesCount; $p++) {
$req = $requestJson;
$req['page'] = $p;
$responses[$p] = $hotelApiService->query_ApiHotel_Availability_Async(
$auth_data, $req, $xmlSourceId, $baseUri, $httpClient, true
);
}
// Collecter et streamer dès qu'une page arrive
foreach ($httpClient->stream($responses, 120) as $response => $chunk) {
if (connection_aborted()) return;
if ($chunk->isTimeout()){
$this->logger->info('isTimeout');
continue;
}
if (!$chunk->isLast()) {
continue;
}
try {
$raw = $response->getContent(false);
if (empty($raw)) {
continue;
}
$data = json_decode($raw, true);
if (!is_array($data)) {
continue;
}
$content = $data['content'] ?? [];
if (empty($content)) {
continue;
} else {
$flushContent($content);
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
continue;
}
unset($data, $content);
}
$this->emitSSE('progress', [
'page' => $pagesCount,
'pagesCount' => $pagesCount,
'loaded' => $totalLoaded,
]);
unset($responses);
}
$duration = round(microtime(true) - $startTime, 2);
$this->logger->info('hotelSearchStream completed in ' . $duration . 's — ' . $totalLoaded . ' hotels');
$this->emitSSE('done', [
'total' => $totalLoaded,
'message' => 'stream completed',
'time_seconds' => $duration,
]);
}, 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
{
$payload = json_encode(
$data,
JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR
);
if ($payload === false) {
$payload = json_encode(['error' => 'json encode failed']);
}
echo "event: {$event}\n";
echo "data: {$payload}\n\n";
// flush sécurisé
if (ob_get_length()) {
@ob_flush();
}
@flush();
}
private function buildContentFront(
array $hotels,
int $nbRooms,
bool $showResellingRates,
mixed $customer
): array
{
$today = new DateTime();
$content_front = []; // prepare data to fit the front design
foreach ($hotels 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'] = 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'] = [];
//$content_front[$hotel_key]['hotel']['minPrice'] = $hotel['minPrice'];
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'] = $this->helpers->formatArrayCounts($bestOffers['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
$hotels = array_values($content_front);
// array values sources
foreach ($hotels 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 $hotels;
}
/**
* 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);
}
}