src/Service/HotelApiService.php line 1234

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Config\CommissionTypeEnum;
  4. use App\Config\HotelBadgeEnum;
  5. use App\Config\ModuleEnum;
  6. use App\Config\OrderLineTypeEnum;
  7. use App\Config\OrderStateEnum;
  8. use App\Config\ServiceEnum;
  9. use App\Entity\ApiLog;
  10. use App\Entity\Beneficiary;
  11. use App\Entity\Customer;
  12. use App\Entity\Hotel;
  13. use App\Entity\HotelAvailabilityRequest;
  14. use App\Entity\HotelContract;
  15. use App\Entity\Operation;
  16. use App\Entity\Order;
  17. use App\Entity\OrderLine;
  18. use App\Entity\OrderSubLine;
  19. use App\Entity\OtherProvider;
  20. use App\Entity\Pricing;
  21. use App\Entity\ProductElementStockOperation;
  22. use App\Entity\XmlApi;
  23. use App\Repository\AgencyRepository;
  24. use App\Repository\ApiLogRepository;
  25. use App\Repository\ArrangementRepository;
  26. use App\Repository\CancellationPolicyRepository;
  27. use App\Repository\CityRepository;
  28. use App\Repository\CustomerHotelXmlRepository;
  29. use App\Repository\CustomerXmlApiRepository;
  30. use App\Repository\HotelContractRepository;
  31. use App\Repository\HotelRepository;
  32. use App\Repository\HotelRoomTypeRepository;
  33. use App\Repository\HotelXmlRepository;
  34. use App\Repository\OrderLineRepository;
  35. use App\Repository\OrderRepository;
  36. use App\Repository\OrderSubLineRepository;
  37. use App\Repository\PartyRepository;
  38. use App\Repository\PartyZoneRepository;
  39. use App\Repository\PricingRepository;
  40. use App\Repository\PromoRepository;
  41. use App\Repository\StopsaleHotelRepository;
  42. use App\Repository\XmlApiRepository;
  43. use DateInterval;
  44. use DateTime;
  45. use Doctrine\ORM\EntityManagerInterface;
  46. use Exception;
  47. use Symfony\Component\HttpClient\HttpClient;
  48. use Symfony\Component\HttpFoundation\Request;
  49. use Symfony\Component\HttpFoundation\RequestStack;
  50. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  51. use Symfony\Contracts\HttpClient\HttpClientInterface;
  52. use Symfony\Contracts\HttpClient\ResponseInterface;
  53. use Symfony\Contracts\Translation\TranslatorInterface;
  54. use App\Repository\HotelCacheRepository;
  55. use Psr\Log\LoggerInterface;
  56. const TOKEN_FOR_BOOK_SEPARATOR = '#TFBEND#';
  57. const HUB_HOTEL_API_ACTIVATED_GDS_ENDPOINT = 'hotels-content?method=getActivatedGDS';
  58. const HUB_HOTEL_API_AUTOCOMPLETE_ENDPOINT = 'hotels-api?method=autocomplete';
  59. const HUB_HOTEL_API_DETAILS_ENDPOINT = 'hotels-api?method=hotelDetails';
  60. const HUB_HOTEL_API_CHECKRATE_ENDPOINT = 'hotels-api?method=checkRate';
  61. const HUB_HOTEL_API_AVAILABILITY_ENDPOINT = 'hotels-api?method=availability';
  62. const HUB_HOTEL_API_GET_BOOKINGS_ENDPOINT = 'hotels-api?method=getBookings';
  63. const HUB_HOTEL_API_BOOK_ENDPOINT = 'hotels-api?method=book';
  64. const HUB_HOTEL_API_CANCEL_ENDPOINT = 'hotels-api?method=cancel';
  65. const HUB_HOTEL_API_CHECKSTATUS_ENDPOINT = 'hotels-api?method=bookingStatus';
  66. const HUB_HOTEL_API_CREDIT_CHECK_ENDPOINT = 'hotels-api?method=creditCheck';
  67. const HUB_HOTEL_API_BOOKING_LIST_ENDPOINT = 'hotels-api?method=getBookings';
  68. const HUB_HOTEL_API_PAGES_ENDPOINT = 'hotels-api?method=getPages';
  69. const CANCEL_ERROR_ORDERLINE_ALREADY_CANCELED = 'CANCELLATION_FAILED_ORDERLINE_ALREADY_CANCELED';
  70. const CANCEL_ERROR_PRODUCT_ERROR = 'CANCELLATION_FAILED_PRODUCT_ERROR';
  71. const CANCEL_ERROR_RELEASE_DATE_EXCEEDED = 'CANCELLATION_FAILED_CHECKIN_DATE_EXCEEDED';
  72. const CANCEL_ERROR_NOT_CANCELLABLE = 'CANCELLATION_ERROR_NOT_CANCELLABLE';
  73. const CANCEL_SUCCESS = 'CANCELLATION_SUCCESS';
  74. class HotelApiService
  75. {
  76.     public function __construct(
  77.         private readonly CurrencyService                $currencyService,
  78.         private readonly XmlApiRepository               $xmlApiRepository,
  79.         private readonly PromoRepository                $promoRepository,
  80.         private readonly CityRepository                 $cityRepository,
  81.         private readonly HotelRepository                $hotelRepository,
  82.         private readonly PricingRepository              $pricingRepository,
  83.         private readonly PartyRepository                $partyRepository,
  84.         private readonly StopsaleHotelRepository        $stopsaleRepository,
  85.         private readonly CancellationPolicyRepository   $cancellationPolicyRepository,
  86.         private readonly OrderLineRepository            $orderLineRepository,
  87.         private readonly HotelContractRepository        $hotelContractRepository,
  88.         private readonly HotelRoomTypeRepository        $hotelRoomTypeRepository,
  89.         private readonly PartyZoneRepository            $partyZoneRepository,
  90.         private readonly AgencyRepository               $agencyRepository,
  91.         private readonly ApiLogRepository               $apiLogRepository,
  92.         private readonly Helpers                        $helpers,
  93.         private readonly ArrangementRepository          $arrangementRepository,
  94.         private readonly TranslatorInterface            $translator,
  95.         private readonly OrderSubLineRepository         $orderSublineRepository,
  96.         private readonly CustomerHotelXmlRepository     $customerHotelXmlRepository,
  97.         private readonly HotelXmlRepository             $hotelXmlRepository,
  98.         private readonly CustomerXmlApiRepository       $customerXmlApiRepository,
  99.         private readonly EntityManagerInterface         $entityManager,
  100.         private readonly XmlApiService                  $xmlApiService,
  101.         private readonly WalletService                  $walletService,
  102.         private readonly ParameterService               $parameterService,
  103.         private readonly RequestStack                   $requestStack,
  104.         private readonly FrontService                   $frontService,
  105.         private readonly StockOperationService          $stockOperationService,
  106.         private readonly OrderRepository                $orderRepository,
  107.         private readonly HttpClientInterface            $httpClient,
  108.         private readonly HotelCacheRepository         $hotelCacheRepository,
  109.         private readonly LoggerInterface              $logger,
  110.     ) {
  111.     }
  112.     /**
  113.      * This is the main function to obtain the available hotels given an hotelAvailabilityRequest
  114.      * The process include four steps : (1) preliminary, (2) hrta prices, (3) extras, (4) format output
  115.      *
  116.      * @param HotelAvailabilityRequest $hotelReq
  117.      *
  118.      * @return array
  119.      */
  120.     public function hotelsAvailability(HotelAvailabilityRequest $hotelReq): array
  121.     {
  122.         // step 1 : preliminary
  123.         $availableHotels = [];
  124.         $all_pricings = [];
  125.         foreach (array_keys($hotelReq->getRoomsOccupancies()) as $req_room_index) {
  126.             // Get pricings for the current req_room_index, hc_array_ids used to avoid searching in NA hotelContracts
  127.             $pricings = $this->pricingRepository->findAvailable($hotelReq, $req_room_index);
  128.             $all_pricings[] = $pricings;
  129.             $this->preliminaryHotelsAvailability($pricings, $hotelReq, $req_room_index, $availableHotels);
  130.         }
  131.         // step 2 : prices
  132.         $availableHotels = $this->availableHotelsWithPrices($availableHotels, $hotelReq, $all_pricings);
  133.         // step 3 : parties
  134.         $AVHTLC_ids = $this->getHotelContractsIDsFromAVHTL($availableHotels);
  135.         //dd($hotelReq->getPartiesZones());
  136.         $parties = $this->partyRepository->findAvailable($hotelReq, $req_room_index, $AVHTLC_ids);
  137.         //dd($parties);
  138.         $this->addPartiesToAvailableHotels($availableHotels, $parties, $hotelReq);
  139.         // step 4 : format array
  140.         $this->formatHotelAvailabilityArray($availableHotels);
  141.         return $availableHotels;
  142.     }
  143.     public function hotelsAvailabilityHUB(HotelAvailabilityRequest $hotelReq, $isFirstOccurrenceRoom=false): array
  144.     {
  145.         $headers = [
  146.             'api-key' => $this->parameterService->getHotelHubApiKey(),
  147.         ];
  148.         if ($hotelReq->getXmlSourceId()) {
  149.             $headers['sourceId'] = $hotelReq->getXmlSourceId();
  150.         }
  151.         $apiResponse = $this->helpers->getApiResponseFromHub(
  152.             method: 'POST',
  153.             api_base_url: $this->parameterService->getHotelHubUrl(),
  154.             endPoint: HUB_HOTEL_API_AVAILABILITY_ENDPOINT,
  155.             headers: $headers,
  156.             request_body: $hotelReq->getHubAvailabilityRequest()
  157.         );
  158.         try {
  159.             $apiResults = $apiResponse['response'];
  160.             $localSearchCode = $hotelReq->getSearchCode();
  161.             $availableHotels = $this->normalizeSearchResultsFromHubFormat($apiResults, $hotelReq->getCurrency(), $localSearchCode,null ,$hotelReq->mapping,$isFirstOccurrenceRoom);
  162.             $this->formatHotelAvailabilityArray($availableHotels);
  163.             // for the specified customer, apply the adequate margin, or hide unauthorized hotel xml
  164.             $this->applyMarginsAndHideExcludedHotelXml($availableHotels, $hotelReq->getCustomer());
  165.             return [
  166.                 'availableHotels' => $availableHotels,
  167.                 'pagesCount' => $apiResponse['pages'],
  168.             ];
  169.         } catch (Exception) {
  170.             return [
  171.                 'availableHotels' => [],
  172.                 'pagesCount' => 1
  173.             ];
  174.         }
  175.     }
  176.     /**
  177.      * @param array $pricings
  178.      * @param HotelAvailabilityRequest $hotelAvailabilityReq
  179.      * @param int $index
  180.      * @param array $availableHotels
  181.      * @return void
  182.      */
  183.     public function preliminaryHotelsAvailability(
  184.         array $pricings,
  185.         HotelAvailabilityRequest $hotelAvailabilityReq,
  186.         int $index,
  187.         array &$availableHotels
  188.     ): void {
  189.         $nb_days = [];
  190.         foreach ($pricings as $key => $pricing) {
  191.             $season = $pricing->getSeason();
  192.             $hotelContract = $season->getHotelContract();
  193.             $hotelContract_key = $hotelContract->getId();
  194.             $hotel = $hotelContract->getHotel();
  195.             $hotel_key = '0@' . $hotel->getId();
  196.             // hrta data
  197.             $hrta = $pricing->getHotelRoomTypeArrangement();
  198.             $hrta_array = [
  199.                 'id' => $hrta->getId(),
  200.                 'rateKey' => $this->getHrtaRateKey($hotelAvailabilityReq, $index, 0, $hotelContract_key, $hrta->getId()),
  201.                 'boardId' => $hrta->getArrangement()->getId(),
  202.                 'boardCode' => $hrta->getArrangement()->getCode(),
  203.                 'boardName' => $hrta->getArrangement()->getName(),
  204.                 'boardXmlName' => $hrta->getArrangement()->getName(),
  205.                 'roomId' => $hrta->getHotelRoomType()->getId(),
  206.                 'roomName' => $hrta->getHotelRoomType()->getRoomType()->getName(),
  207.                 'purchaseCurrency' => $hotelContract->getProvider()->getCurrency()
  208.             ];
  209.             // Days overlap calculation
  210.             $nb_days_pricing = $this->helpers->nbDaysOverlap(
  211.                 $hotelAvailabilityReq->getCheckInDate(),
  212.                 $hotelAvailabilityReq->getLastNightDate(),
  213.                 $season->getStartDate(),
  214.                 $season->getEndDate(),
  215.             );
  216.             if (!array_key_exists($hotelContract->getId() . '-' . $hrta->getId(), $nb_days)) {
  217.                 $nb_days[$hotelContract->getId() . '-' . $hrta->getId()] = $nb_days_pricing;
  218.             } else {
  219.                 $nb_days[$hotelContract->getId() . '-' . $hrta->getId()] += $nb_days_pricing;
  220.             }
  221.             // array of the Ids of pricings related to a Hrta and the nbDaysCovered by this pricing.
  222.             $hrta_array['pricings_data'][$key] = [
  223.                 'id' => $pricing->getId(),
  224.                 'nbDays' => $nb_days_pricing
  225.             ];
  226.             // Nb Days covered by all pricings relatated to the HRTA
  227.             $hrta_array['nb_stay_overlap'] = $nb_days[$hotelContract->getId() . '-' . $hrta->getId()];
  228.             $availableHotels[$hotel_key]['sources'][$hotelContract_key]['rooms'][$index][$hrta_array['id']] = $hrta_array;
  229.             // hotel data
  230.             if (!array_key_exists('hotel', $availableHotels[$hotel_key])) {
  231.                 $availableHotels[$hotel_key]['hotel']['id'] = $hotel_key; //'0-' . $hotel->getId();
  232.                 $availableHotels[$hotel_key]['hotel']['name'] = $hotel->getName();
  233.                 $availableHotels[$hotel_key]['hotel']['rating'] = $hotel->getStarRating();
  234.                 $availableHotels[$hotel_key]['hotel']['city'] = $hotel->getCity()->getName();
  235.                 $availableHotels[$hotel_key]['hotel']['cityId'] = $hotel->getCity()->getId();
  236.                 $availableHotels[$hotel_key]['hotel']['zone'] = $hotel->getAddress() ?? $hotel->getCity()->getName();
  237.                 $availableHotels[$hotel_key]['hotel']['country'] = $hotel->getCity()->getCountry()->getName();
  238.                 $availableHotels[$hotel_key]['hotel']['countryId'] = $hotel->getCity()->getCountry()->getId();
  239.                 $availableHotels[$hotel_key]['hotel']['latitude'] = $hotel->getLatitude();
  240.                 $availableHotels[$hotel_key]['hotel']['longitude'] = $hotel->getLongitude();
  241.                 $availableHotels[$hotel_key]['hotel']['overview'] = $hotel->getDescription();
  242.                 $availableHotels[$hotel_key]['hotel']['tripadvisorId'] = $hotel->getTripadvisorId();
  243.                 $availableHotels[$hotel_key]['hotel']['image'] = "";
  244.                 $availableHotels[$hotel_key]['hotel']['gallery'] = [];
  245.                 foreach ($hotel->getImages() as $image) {
  246.                     if ($image->getDisplayOrder() == 1) {
  247.                         array_unshift($availableHotels[$hotel_key]['hotel']['gallery'], $image->getUrl()); // Insert at the beginning
  248.                         $availableHotels[$hotel_key]['hotel']['image'] = $image->getUrl();
  249.                     } else {
  250.                         $availableHotels[$hotel_key]['hotel']['gallery'][] = $image->getUrl(); // Append to the end
  251.                     }
  252.                 }
  253.             }
  254.             // hotel contract data
  255.             if (!array_key_exists('sourceId', $availableHotels[$hotel_key]['sources'][$hotelContract_key])) {
  256.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['xmlSourceId'] = 0; // local
  257.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['xmlSourceName'] = "Direct";
  258.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['sourceId'] = $hotelContract_key;
  259.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['sourceType'] = 'local';
  260.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['searchCode'] = $hotelAvailabilityReq->getSearchCode();
  261.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['associationRequired'] = false;
  262.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['options'] = [];
  263.                 foreach ($hotel->getOptions() as $option) {
  264.                     $availableHotels[$hotel_key]['sources'][$hotelContract_key]['options'][] = $option->getName();
  265.                 }
  266.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['facilities'] = [];
  267.                 foreach ($hotel->getFacilities() as $ItemFacilities) {
  268.                     $item = [
  269.                         'name' => $ItemFacilities->getName(),
  270.                         'description' => $ItemFacilities->getDescription(),
  271.                         'icon' => $ItemFacilities->getIcon()
  272.                     ];
  273.                     $availableHotels[$hotel_key]['sources'][$hotelContract_key]['facilities'][] = $item;
  274.                 }
  275.                 $availableHotels[$hotel_key]['sources'][$hotelContract_key]['conditions'] = [];
  276.                 foreach ($hotel->getConditions() as $condition) {
  277.                     $availableHotels[$hotel_key]['sources'][$hotelContract_key]['conditions'][] = $condition;
  278.                 }
  279.             }
  280.         }
  281.     }
  282.     public function availableHotelsWithPrices(array $preAvailableHotels, HotelAvailabilityRequest $hotelReq, $pricings): array
  283.     {
  284.         $today = new DateTime();
  285.         $today->setTime(0, 0, 0);
  286.         $dateIntervalToCheckIn = $today->diff($hotelReq->getCheckInDate());
  287.         foreach ($preAvailableHotels as $hotel_key => $preAvailableHotel) { // hotels
  288.             foreach ($preAvailableHotel['sources'] as $contract_key => $source) { // sources (contracts)
  289.                 // If all requested rooms are not found, delete the contract from response
  290.                 if (count($source['rooms']) < count($hotelReq->getRoomsOccupancies())) {
  291.                     unset($preAvailableHotels[$hotel_key]['sources'][$contract_key]);
  292.                     continue;
  293.                 }
  294.                 // General AvailableQuantity
  295.                 $generalHotelContractDailyStock = $this->stockOperationService->getDailyStock($hotelReq->getCheckInDate(), $hotelReq->getLastNightDate(), $source['sourceId'], null, $hotelReq->getCustomer());
  296.                 $generalHotelContractStock = min($generalHotelContractDailyStock);
  297.                 $preAvailableHotels[$hotel_key]['sources'][$contract_key]['generalStock'] = $generalHotelContractStock;
  298.                 // contract promos
  299.                 $contract_promos_all = $this->promoRepository->availablePromos($hotelReq->getCheckInDate(), $hotelReq->getLastNightDate(), $contract_key);
  300.                 // GET only active promos (remove expired early booking)
  301.                 $contract_promos = [];
  302.                 foreach ($contract_promos_all as $contract_promo) {
  303.                     if ($contract_promo->getEbEndDate() != null) { // EB PERIOD
  304.                         if ($today->diff($contract_promo->getEbEndDate())->invert == 1) {
  305.                             continue;
  306.                         }
  307.                     } elseif ($contract_promo->getEbDaysBefore() > 0) { // EB J-
  308.                         if ($dateIntervalToCheckIn->invert == 1 || $dateIntervalToCheckIn->days < $contract_promo->getEbDaysBefore()) {
  309.                             continue;
  310.                         }
  311.                     }
  312.                     $contract_promos[] = $contract_promo;
  313.                 }
  314.                 foreach ($source['rooms'] as $request_room_key => $request_room) {
  315.                     $hrtAvailableQuantities = []; // array to save the available quantity for each HotelRoomType
  316.                     foreach ($request_room as $hrta_key => $hrta) { // check that all stay-period is covered
  317.                         if ($hrta['nb_stay_overlap'] < $hotelReq->getNbStay()) {
  318.                             unset($preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]);
  319.                             continue; // NEXT HRTA
  320.                         }
  321.                         // ********************** HERE, NB_STAY is OK for this HRTA **********************
  322.                         $hrt_id = $hrta['roomId'];
  323.                         // *** PRICES CALCULATION from pricing entities for the current HRTA ***
  324.                         $hrta_original_purchase_price = 0;
  325.                         $hrta_original_sale_price = 0;
  326.                         $hrta_promo_purchase_price = 0;
  327.                         $hrta_promo_sale_price = 0;
  328.                         $hrta_is_NRF = false;
  329.                         foreach ($hrta['pricings_data'] as $key => $pricing_data) {
  330.                             $pricing = $pricings[$request_room_key][$key]; // Pricing Entity
  331.                             $pricing_array = $this->getPriceData($pricing, $hotelReq, $request_room_key, $pricing_data, $contract_promos);
  332.                             $hrta_original_purchase_price += $pricing_array['prices']['weighted_purchase'];
  333.                             $hrta_promo_purchase_price += $pricing_array['prices']['promo_purchase'];
  334.                             $hrta_original_sale_price += $pricing_array['prices']['weighted_sale'];
  335.                             $hrta_promo_sale_price += $pricing_array['prices']['promo_sale'];
  336.                             if ($pricing->isNRF()) {
  337.                                 $hrta_is_NRF = true;
  338.                             }
  339.                             $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['pricings_data'][$key] = $pricing_array;
  340.                         }
  341.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['purchaseRate'] = $hrta_original_purchase_price;
  342.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['purchasePromoRate'] = $hrta_promo_purchase_price;
  343.                         // Exchange Rate purchaseToSaleCurrency
  344.                         $exchangeRate = $this->currencyService->calculateExchangeRate(
  345.                             $pricing->getSeason()->getHotelContract()->getProvider()->getCurrency(), // purchase currency
  346.                             $hotelReq->getCurrency()
  347.                         );
  348.                         // SALE RATE
  349.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['saleRate'] = $hrta_original_sale_price * $exchangeRate;
  350.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['salePromoRate'] = $hrta_promo_sale_price * $exchangeRate;
  351.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['saleCurrency'] = $hotelReq->getCurrency();
  352.                         // CHECK STOP SALE for the HRTA
  353.                         $countStopSales = $this->stopsaleRepository->countStopSale($hotelReq->getCheckInDate(), $hotelReq->getCheckOutDate(), $hrta_key);
  354.                         $isStopSale = $countStopSales > 0;
  355.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['stopSale'] = $isStopSale;
  356.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['notes'] = '';
  357.                         // Specific AvailableQuantity
  358.                         if (array_key_exists($hrt_id, $hrtAvailableQuantities)) { // already calculated (previous arrangement)
  359.                             $availableQuantity = $hrtAvailableQuantities[$hrta['roomId']];
  360.                         } else { // calculate the quantity of this RoomType for the first time
  361.                             $hrtaDailyStock = $this->stockOperationService->getDailyStock($hotelReq->getCheckInDate(), $hotelReq->getLastNightDate(), $source['sourceId'], $hrta['roomId'], $hotelReq->getCustomer());
  362.                             $availableQuantity = min($hrtaDailyStock); // Minimum NbItems in STOCK for this HRT along the Stay Period
  363.                             $hrtAvailableQuantities[$hrt_id] = $availableQuantity; // save it for next hrtas
  364.                         }
  365.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['availableQuantity'] = $availableQuantity;
  366.                         $hrta_is_available = ($availableQuantity + $generalHotelContractStock) > 0;
  367.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['available'] = $hrta_is_available;
  368.                         // *** END AvailableQuantity for this Hotel Room Type ***
  369.                         // Refund and cancellation fees
  370.                         //if($hrta_is_NRF){
  371.                         $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['NRF'] = $hrta_is_NRF;
  372.                         //}
  373.                         if (!$hrta_is_NRF) { // HRTA is Refundable, render the cancellations dates and fees
  374.                             $hrt_id = $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['roomId'];
  375.                             $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['cancellationsFees']
  376.                                 = $this->getCancellationsFeesArray($contract_key, $hrt_id, $hotelReq, $hrta_promo_sale_price);
  377.                             $earliestCancellation = end($preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['cancellationsFees']);
  378.                             if ($earliestCancellation) {
  379.                                 $deadline = DateTime::createFromFormat("Y-m-d", $earliestCancellation['from']);
  380.                                 $deadline->setTime(0, 0);
  381.                                 date_sub($deadline, date_interval_create_from_date_string("1 day"));
  382.                                 $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['deadline']
  383.                                     = $deadline->format('Y-m-d');
  384.                             } else {
  385.                                 $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['deadline'] = null;
  386.                             }
  387.                         }
  388.                     } // END HRTA loop
  389.                     $preAvailableHotels[$hotel_key]['sources'][$contract_key]['promos'] = $this->getHotelBadges(HotelBadgeEnum::PROMO->getValue(), $hotel_key, true);
  390.                     $preAvailableHotels[$hotel_key]['sources'][$contract_key]['infos'] = $this->getHotelBadges(HotelBadgeEnum::INFO->getValue(), $hotel_key, true);
  391.                     $preAvailableHotels[$hotel_key]['sources'][$contract_key]['discounts'] = $this->getHotelBadges(HotelBadgeEnum::DISCOUNT->getValue(), $hotel_key, true);
  392.                     // IF THE RoomIndex hasn't relevant results --> UNSET the HotelContract
  393.                     if (count($preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key]) == 0) {
  394.                         unset($preAvailableHotels[$hotel_key]['sources'][$contract_key]);
  395.                         break;
  396.                     }
  397.                 }
  398.             }
  399.             if (count($preAvailableHotels[$hotel_key]['sources']) == 0) {
  400.                 unset($preAvailableHotels[$hotel_key]);
  401.             }
  402.         }
  403.         return $preAvailableHotels;
  404.     }
  405.     /**
  406.      * @param Pricing $pricing
  407.      * @param HotelAvailabilityRequest $hotelAvailabilityReq
  408.      *
  409.      * @return array
  410.      */
  411.     private function getPriceData(
  412.         Pricing $pricing,
  413.         HotelAvailabilityRequest $hotelAvailabilityReq,
  414.         int $req_index,
  415.         $pricing_data,
  416.         $promos
  417.     ): array {
  418.         $extras = [];
  419.         $discounts = [];
  420.         $raw_purchase_price = $pricing->getPurchasePrice();
  421.         $raw_sale_price = $pricing->getSalePrice();
  422.         // prices without promo.
  423.         $season = $pricing->getSeason();
  424.         $base_arrangement = $season->getBaseArrangement();
  425.         $hrt = $pricing->getHotelRoomTypeArrangement()->getHotelRoomType();
  426.         if ($base_arrangement != null && $base_arrangement !== $pricing->getHotelRoomTypeArrangement()->getArrangement()) {
  427.             $base_pricing = $this->pricingRepository->findBasePricing($season, $hrt); // BasePricing for the given HRTA
  428.             $raw_purchase_price += $base_pricing->getPurchasePrice();
  429.             $raw_sale_price += $base_pricing->getSalePrice();
  430.         }
  431.         $valid_operations = [];
  432.         // search for applicable operations (valid_operations)
  433.         foreach ($pricing->getOperations() as $operation) {
  434.             // specific operation fields
  435.             $operation_type = get_class($operation->getOperationType()); // ExtraSingle, ExtraWeekend, ...
  436.             $operation_array = null;
  437.             switch ($operation_type) {
  438.                 case 'App\Entity\DiscountChild':
  439.                     // $req_room_index data
  440.                     $ages = $hotelAvailabilityReq->getAges('child', $req_index);
  441.                     $nbAdults = $hotelAvailabilityReq->getNbPersonGen('adult', $req_index);
  442.                     // operation info
  443.                     $op_childRank = $operation->getOperationType()->getChildRank();
  444.                     $op_nbAdultsInRoom = $operation->getOperationType()->getNbAdultsInRoom();
  445.                     $op_ageMin = $operation->getOperationType()->getAgeMin();
  446.                     $op_ageMax = $operation->getOperationType()->getAgeMax();
  447.                     if ( // check whether discountChild operation is applicable for the req_room_index
  448.                         array_key_exists($op_childRank - 1, $ages) && // the child rank exist
  449.                         $nbAdults == $op_nbAdultsInRoom &&  // the nb Adult in room is OK
  450.                         $ages[$op_childRank - 1] >= $op_ageMin && $ages[$op_childRank - 1] < $op_ageMax // the age is OK
  451.                     ) {
  452.                         $operation_array['ageMin'] = $op_ageMin;
  453.                         $operation_array['ageMax'] = $op_ageMax;
  454.                         $operation_array['childRank'] = $op_childRank;
  455.                         $operation_array['nbAdultsInRoom'] = $op_nbAdultsInRoom;
  456.                         $this->pushGenericOperationFields($operation_array, $operation);
  457.                         $valid_operations[] = $operation;
  458.                     }
  459.                     break;
  460.                 case 'App\Entity\DiscountAdult':
  461.                     // $req_room_index data
  462.                     $nbAdults = $hotelAvailabilityReq->getNbPersonGen('adult', $req_index);
  463.                     // operation info
  464.                     $op_adult_rank = $operation->getOperationType()->getRankAdult();
  465.                     if ( // check whether DiscountAdult operation is applicable for the req_room_index
  466.                         $nbAdults >= $op_adult_rank  // the nb Adult in room is OK
  467.                     ) {
  468.                         $operation_array['rank_adult'] = $op_adult_rank;
  469.                         $this->pushGenericOperationFields($operation_array, $operation);
  470.                         $valid_operations[] = $operation;
  471.                     }
  472.                     break;
  473.                 case 'App\Entity\DiscountSenior':
  474.                     // $req_room_index data
  475.                     $ages = $hotelAvailabilityReq->getAges('adult', $req_index);
  476.                     // operation info
  477.                     $op_ageMin = $operation->getOperationType()->getAgeMin();
  478.                     $op_ageMax = $operation->getOperationType()->getAgeMax();
  479.                     $nbSenior = 0;
  480.                     foreach ($ages as $age) {
  481.                         if ($age >= $op_ageMin && $age < $op_ageMax) {
  482.                             $nbSenior++;
  483.                         }
  484.                     }
  485.                     if ($nbSenior > 0) {
  486.                         $operation_array['ageMin'] = $op_ageMin;
  487.                         $operation_array['ageMax'] = $op_ageMax;
  488.                         $operation_array['nb_senior'] = $nbSenior;
  489.                         $this->pushGenericOperationFields($operation_array, $operation);
  490.                         $valid_operations[] = $operation;
  491.                     }
  492.                     break;
  493.                 case 'App\Entity\DiscountLongStay':
  494.                     $req_stay = $hotelAvailabilityReq->getNbStay(); // Nb of stay days
  495.                     $op_minStay = $operation->getOperationType()->getMinStay();
  496.                     if ($req_stay >= $op_minStay) {
  497.                         $operation_array['min_stay'] = $op_minStay;
  498.                         $this->pushGenericOperationFields($operation_array, $operation);
  499.                         $valid_operations[] = $operation;
  500.                     }
  501.                     break;
  502.                 case 'App\Entity\DiscountWeekend':
  503.                 case 'App\Entity\ExtraWeekend':
  504.                     // same fields for Extra or Discount Weekend
  505.                     $req_days = $hotelAvailabilityReq->getStayDays();
  506.                     $op_day_start = $operation->getOperationType()->getDayStart();
  507.                     $op_day_end = $operation->getOperationType()->getDayEnd();
  508.                     $nb_weekend_days = 0;
  509.                     foreach ($req_days as $req_day) {
  510.                         if ($req_day >= $op_day_start and $req_day <= $op_day_end) {
  511.                             $nb_weekend_days++;
  512.                         }
  513.                     }
  514.                     if ($nb_weekend_days > 0) {
  515.                         $operation_array['nb_weekend_days'] = $nb_weekend_days;
  516.                         $operation_array['day_start'] = $op_day_start;
  517.                         $operation_array['day_end'] = $op_day_end;
  518.                         $this->pushGenericOperationFields($operation_array, $operation);
  519.                         $valid_operations[] = $operation;
  520.                     }
  521.                     break;
  522.                 case 'App\Entity\ExtraSingle':
  523.                     $nb_adults = $hotelAvailabilityReq->getNbPersonGen('adult', $req_index);
  524.                     $nb_children = $hotelAvailabilityReq->getNbPersonGen('child', $req_index);
  525.                     $nb_persons = $nb_adults + $nb_children;
  526.                     if ($nb_persons == 1) { // if extra single applicable (EXTRA SINGLE IS APPLICABLE ONLY IF THE NB_PERSONS = 1
  527.                         $operation_array['nb_persons'] = $nb_persons;
  528.                         $this->pushGenericOperationFields($operation_array, $operation);
  529.                         $valid_operations[] = $operation;
  530.                     }
  531.                     break;
  532.             }
  533.             if ($operation_array != null) {
  534.                 ($operation->getOperationType()->getType() == 1)
  535.                     ? $extras[] = $operation_array
  536.                     : $discounts[] = $operation_array;
  537.             }
  538.         } // foreach operations END
  539.         // calculate extra and discount for sale and purchase
  540.         $purchase_discounted_amount = 0;
  541.         $sale_discounted_amount = 0;
  542.         $purchase_extra_amount = 0;
  543.         $sale_extra_amount = 0;
  544.         foreach ($valid_operations as $v_operation) {
  545.             if ($v_operation->getOperationType()->getType() == -1) {
  546.                 $purchase_discounted_amount += $this->operationAmount(
  547.                     $raw_purchase_price,
  548.                     $v_operation->getPurchaseValue(),
  549.                     $v_operation->getCalculationMethod()
  550.                 );
  551.                 $sale_discounted_amount += $this->operationAmount(
  552.                     $raw_sale_price,
  553.                     $v_operation->getSaleValue(),
  554.                     $v_operation->getCalculationMethod()
  555.                 );
  556.             } else {
  557.                 $purchase_extra_amount += $this->operationAmount(
  558.                     $raw_purchase_price,
  559.                     $v_operation->getPurchaseValue(),
  560.                     $v_operation->getCalculationMethod()
  561.                 );
  562.                 $sale_extra_amount += $this->operationAmount(
  563.                     $raw_sale_price,
  564.                     $v_operation->getSaleValue(),
  565.                     $v_operation->getCalculationMethod()
  566.                 );
  567.             }
  568.         }
  569.         // unit prices after discounts and extras
  570.         $unit_purchase_price = (!$pricing->isByPerson())
  571.             ? ($raw_purchase_price - $purchase_discounted_amount + $purchase_extra_amount)
  572.             : ($raw_purchase_price * $hotelAvailabilityReq->getNbPerson($req_index) - $purchase_discounted_amount + $purchase_extra_amount);
  573.         $unit_sale_price = (!$pricing->isByPerson())
  574.             ? ($raw_sale_price - $sale_discounted_amount + $sale_extra_amount)
  575.             : ($raw_sale_price * $hotelAvailabilityReq->getNbPerson($req_index) - $sale_discounted_amount + $sale_extra_amount);
  576.         // weighted prices after discounts and extras
  577.         $weighted_purchase_price = $unit_purchase_price * $pricing_data['nbDays'];
  578.         $weighted_sale_price = $unit_sale_price * $pricing_data['nbDays'];
  579.         // calculate promos
  580.         $season = $pricing->getSeason();
  581.         $pricingStayOverlapPeriod = $this->helpers->overlapPeriod(
  582.             $hotelAvailabilityReq->getCheckInDate(),
  583.             $hotelAvailabilityReq->getLastNightDate(),
  584.             $season->getStartDate(),
  585.             $season->getEndDate()
  586.         );
  587.         $purchase_promos_amount = 0;
  588.         $sale_promos_amount = 0;
  589.         $promos_array = [];
  590.         foreach ($promos as $promo) {
  591.             if (!$promo->getHrtaCombinations()->contains($pricing->getHotelRoomTypeArrangement())) {
  592.                 continue;
  593.             }
  594.             $nbPromoDays = $this->helpers->nbDaysOverlap(
  595.                 $pricingStayOverlapPeriod['start'],
  596.                 $pricingStayOverlapPeriod['end'],
  597.                 $promo->getCheckinStartDate(),
  598.                 $promo->getCheckInEndDate(),
  599.             );
  600.             if ($nbPromoDays > 0) { // Stay duration in a Promo Period ... OK
  601.                 if ($hotelAvailabilityReq->getNbStay() < $promo->getMinStay())
  602.                     continue; // Check MinStay ... OK
  603.                 if (!is_null($promo->getMaxStay()) and $nbPromoDays > $promo->getMaxStay()) {
  604.                     $nbPromoDays = $promo->getMaxStay();
  605.                 }
  606.                 // check booking date
  607.                 $today = new DateTime();
  608.                 $today->setTime(0, 0, 0);
  609.                 $promoType = $promo->getPromoType();
  610.                 if ($promoType == 2) {
  611.                     $dateEB_JCheckin = clone $hotelAvailabilityReq->getCheckInDate();
  612.                     $dateEB_JCheckin->sub(new DateInterval('P' . $promo->getEbDaysBefore() . 'D'));
  613.                 }
  614.                 if (
  615.                     ($promoType == 1 and $today >= $promo->getEbStartDate() && $today <= $promo->getEbEndDate()) or
  616.                     ($promoType == 2 and $today <= $dateEB_JCheckin
  617.                     ) or
  618.                     ($promoType == 3)
  619.                 ) { // Booking period ... OK
  620.                     $promo_purchase_amount = $nbPromoDays * $this->operationAmount(
  621.                         $unit_purchase_price,
  622.                         $promo->getPurchaseValue(),
  623.                         $promo->getCalculationMethod()
  624.                     );
  625.                     $promo_sale_amount = $nbPromoDays * $this->operationAmount(
  626.                         $unit_sale_price,
  627.                         $promo->getSaleValue(),
  628.                         $promo->getCalculationMethod()
  629.                     );
  630.                     $purchase_promos_amount += $promo_purchase_amount;
  631.                     $sale_promos_amount += $promo_sale_amount;
  632.                     $promos_array[$promo->getTitle()] = [
  633.                         'nbDays' => $nbPromoDays,
  634.                         'purchase' => $promo_purchase_amount,
  635.                         'sale' => $promo_sale_amount
  636.                     ];
  637.                 }
  638.             }
  639.         }
  640.         $promo_purchase_price = $weighted_purchase_price - $purchase_promos_amount;
  641.         $promo_sale_price = $weighted_sale_price - $sale_promos_amount;
  642.         $pricing_data['discounts'] = $discounts;
  643.         $pricing_data['parties'] = $extras;
  644.         $pricing_data['promos'] = $promos_array;
  645.         $pricing_data['byPerson'] = $pricing->isByPerson();
  646.         $pricing_data['prices'] = [
  647.             'raw_unit_purchase' => $raw_purchase_price,
  648.             'raw_unit_sale' => $raw_sale_price,
  649.             // unit prices after operations
  650.             'op_unit_purchase' => $unit_purchase_price,
  651.             'op_unit_sale' => $unit_sale_price,
  652.             // weighted price for the current pricing (sub-price)
  653.             'weighted_purchase' => $weighted_purchase_price,
  654.             'weighted_sale' => $weighted_sale_price,
  655.             // weighted price with promo
  656.             'promo_purchase' => $promo_purchase_price,
  657.             'promo_sale' => $promo_sale_price,
  658.         ];
  659.         return $pricing_data;
  660.     }
  661.     private function pushGenericOperationFields(array &$operation_array, Operation $operation): void
  662.     {
  663.         //generic operation fields
  664.         $operation_array['title'] = $operation->getOperationType()->__toString();
  665.         $operation_array['purchaseValue'] = $operation->getPurchaseValue();
  666.         $operation_array['saleValue'] = $operation->getSaleValue();
  667.         $operation_array['calculation'] = $operation->getCalculationMethod();
  668.         $operation_array['id'] = $operation->getId();
  669.     }
  670.     public function addPartiesToAvailableHotels(array &$availableHotels, array $parties, HotelAvailabilityRequest $request): void
  671.     {
  672.         foreach ($parties as $party) {
  673.             $contract_id = $party->getHotelContract()->getId();
  674.             $hotel_id = $party->getHotelContract()->getHotel()->getId();
  675.             $hotel_id = "0-" . $party->getHotelContract()->getHotel()->getId();
  676.             // Exchange Rate purchaseToSaleCurrency
  677.             $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate(
  678.                 $party->getHotelContract()->getProvider()->getCurrency(), // purchase currency
  679.                 $request->getCurrency()
  680.             );
  681.             $party_hrta_ids = [];
  682.             foreach ($party->getHotelRoomTypeArrangements() as $hotelRoomTypeArrangement) {
  683.                 $party_hrta_ids[] = $hotelRoomTypeArrangement->getId();
  684.             }
  685.             foreach ($availableHotels[$hotel_id]['sources'][$contract_id]['rooms'] as $request_room_key => $request_room) {
  686.                 $zones_array = null;
  687.                 foreach ($party->getProductElements() as $partyZone) {
  688.                     // Display only selected partyZone in CheckRate
  689.                     if (!empty($request->getPartiesZones()[$request_room_key])) {
  690.                         $partiesZonesInRequest = explode(',', $request->getPartiesZones()[$request_room_key]);
  691.                         if (!in_array(strval($partyZone->getId()), $partiesZonesInRequest))
  692.                             continue;
  693.                     }
  694.                     $purchase_price =
  695.                         $partyZone->getAdultPurchasePrice() * $request->getNbPersonGen('adult', $request_room_key) +
  696.                         $partyZone->getChildPurchasePrice() * $request->getNbPersonGen('child', $request_room_key);
  697.                     $sale_price =
  698.                         $partyZone->getAdultSalePrice() * $request->getNbPersonGen('adult', $request_room_key) +
  699.                         $partyZone->getChildSalePrice() * $request->getNbPersonGen('child', $request_room_key);
  700.                     $zonePartyStock = $this->stockOperationService->getDailyStock($party->getDate(), $party->getDate(), $party, $partyZone, $request->getCustomer());
  701.                     $partyZoneQuantity = $zonePartyStock[$party->getDate()->format('Y-m-d')]; // one-day
  702.                     $partyZoneRateKey = $partyZone->getId();
  703.                     $zones_array[] = [
  704.                         'id' => $partyZoneRateKey,
  705.                         'title' => $partyZone->getZone()->getTitle(),
  706.                         'purchasePrice' => $purchase_price,
  707.                         'purchaseCurrency' => $party->getHotelContract()->getProvider()->getCurrency(),
  708.                         'salePrice' => $sale_price * $exchangeRatePurchaseToSale,
  709.                         'saleCurrency' => $request->getCurrency(),
  710.                         'availableQuantity' => $partyZoneQuantity
  711.                     ];
  712.                 } // END partyZone loop
  713.                 $extra_array = [
  714.                     'id' => $party->getId(),
  715.                     'type' => 'party',
  716.                     'title' => $party->getTitle(),
  717.                     'date' => $party->getDate()->format('Y-m-d'),
  718.                     'zones' => $zones_array
  719.                 ];
  720.                 foreach ($request_room as $hrta_key => $hrta) {
  721.                     if (in_array($hrta['id'], $party_hrta_ids)) {
  722.                         $availableHotels[$hotel_id]['sources'][$contract_id]['rooms'][$request_room_key][$hrta_key]
  723.                         ['parties'][] = $extra_array;
  724.                         //$availableHotels[$hotel_id]['sources'][$contract_id]['rooms'][$request_room_key][$hrta_key]['parties'][]=$party->getId();
  725.                     }
  726.                 }
  727.             }
  728.             //$availableHotels[$hotel_id]['sources'][$hc_id]['rooms'][$index][$hrta_array['id']] =$hrta_array;
  729.             //foreach ()
  730.         }
  731.     }
  732.     /**
  733.      * format the array of replacing entitiesKeys by defaultKeys and removing unnecessary fields
  734.      *
  735.      * @param array $availableHotels
  736.      *
  737.      * @return void
  738.      */
  739.     public function formatHotelAvailabilityArray(array &$availableHotels): void
  740.     {
  741.         $hrta_fields_to_delete = ['pricings_data', 'nb_stay_overlap'];
  742.         foreach ($availableHotels as $hotel_key => $preAvailableHotel) { // hotels
  743.             foreach ($preAvailableHotel['sources'] as $contract_key => $source) { // sources (contracts)
  744.                 foreach ($source['rooms'] as $request_room_key => $request_room) { // request_rooms
  745.                     foreach ($request_room as $hrta_key => $hrta) {
  746.                         // delete unnecessary fields
  747.                         foreach ($hrta_fields_to_delete as $field) {
  748.                             unset($availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key][$field]);
  749.                         }
  750.                         if (key_exists('parties', $availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key])) {
  751.                             $parties_keys_array = array_column($availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['parties'], 'id');
  752.                             $parties_key = implode(",", $parties_keys_array);
  753.                             // concat partiesKeys to hrtaRateKey
  754.                             $availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['rateKey'] .= '|' . $parties_key;
  755.                         }
  756.                     }
  757.                     $availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key]
  758.                         = array_values($availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key]);
  759.                 }
  760.             }
  761.             $availableHotels[$hotel_key]['sources'] = array_values($availableHotels[$hotel_key]['sources']);
  762.         }
  763.         $availableHotels = array_values($availableHotels);
  764.     }
  765.     public function applyMarginsAndHideExcludedHotelXml(array &$availableHotels, Customer $customer): void
  766.     {
  767.         foreach ($availableHotels as $hotel_key => &$availableHotel) { // hotels
  768.             foreach ($availableHotel['sources'] as $contract_key => &$source) { // sources (contracts)
  769.                 $xmlSourceId = $source['xmlSourceId'];
  770.                 if ($xmlSourceId == 0)
  771.                     continue; // MARGIN is applied only for XML_API product
  772.                 $hotelXmlArray = [
  773.                     'id' => null,
  774.                     'name' => $availableHotel['hotel']['name']
  775.                 ];
  776.                 $margin = $this->getMargin($xmlSourceId, $customer, $hotelXmlArray['name']);
  777.                 if ($margin <= 0) { // hotelXml not blocked for the customer
  778.                     unset($availableHotels[$hotel_key]['sources'][$contract_key]);
  779.                     continue;
  780.                 }
  781.                 foreach ($source['rooms'] as &$request_room) { // request_rooms
  782.                     foreach ($request_room as &$hrta) {
  783.                         $hrta['saleRate'] = $hrta['saleRate'] * (100.0 + $margin) / 100.0;
  784.                         $hrta['salePromoRate'] = $hrta['salePromoRate'] * (100.0 + $margin) / 100.0;
  785.                     }
  786.                 }
  787.             }
  788.             if (count($availableHotels[$hotel_key]['sources']) == 0) {
  789.                 unset($availableHotels[$hotel_key]);
  790.             }
  791.         }
  792.         $availableHotels = array_values($availableHotels);
  793.     }
  794.     public function getMargin(int $xmlSourceId, Customer $customer, $hotelName)
  795.     {
  796.         // High priority : The customer has a specific margin for the hotelXml
  797.         $customerHotelXml = $this->customerHotelXmlRepository->findOneBy(
  798.             [
  799.                 'customer' => $customer,
  800.                 'hotelXmlName' => $hotelName
  801.             ]
  802.         );
  803.         if ($customerHotelXml) {
  804.             if ($customerHotelXml->isDisplay() == false) { // hotelXml blocked for this customer
  805.                 return -1; // -1 (i.e. blocked)
  806.             }
  807.             return $customerHotelXml->getMargin();
  808.         }
  809.         // Medium priority : The customer has a specific margin for XmlApi
  810.         $customerXmlApi = $this->customerXmlApiRepository->findOneByCustomerAndXmlSource($customer, $xmlSourceId);
  811.         if ($customerXmlApi) {
  812.             return $customerXmlApi->getMargin();
  813.         }
  814.         // Low priority : Consider XmlApi defined margin
  815.         $xmlApi = $this->xmlApiRepository->findOneBy(['source' => $xmlSourceId]);
  816.         if ($xmlApi) {
  817.             if ($customer->getClass() == "CustomerMoral") {
  818.                 return $xmlApi->getmarginB2B();
  819.             }
  820.             return $xmlApi->getmarginB2C();
  821.         }
  822.         return -1;
  823.     }
  824.     public function getHotelContractsIDsFromAVHTL(array $availableHotels): array
  825.     {
  826.         $availableHotelContracts = [];
  827.         foreach ($availableHotels as $availableHotel) {
  828.             foreach ($availableHotel['sources'] as $hc) {
  829.                 $availableHotelContracts[] = $hc;
  830.             }
  831.         }
  832.         return array_column($availableHotelContracts, 'sourceId');
  833.     }
  834.     /**
  835.      * This is the main function for HotelApi CheckRate Method
  836.      * Given the selected hrta and selected parties zones, it performs prices check (rooms and parties)
  837.      * and proposes the possible options (free or paid) to book (Parking, Transfer, Decoration, ...)
  838.      *
  839.      * @param mixed $parameters
  840.      *
  841.      * @return array
  842.      * @throws Exception
  843.      */
  844.     function checkRate(mixed $parameters, $customer): array
  845.     {
  846.         $isXml = str_contains($parameters['searchCode'], '@');
  847.         if ($isXml) { // HUB
  848.             return $this->checkRateHub($parameters, $customer);
  849.         } else { // LOCAL
  850.             // Prepare the HotelRequest
  851.             $hotelReq = $this->checkRateKey($parameters);
  852.             $hotelReq->setCustomer($customer);
  853.             $hotelReq->setSearchCode($parameters['searchCode']);
  854.             return $this->hotelsAvailability($hotelReq);
  855.         }
  856.     }
  857.     private function checkRateHub($parameters, $customer): array
  858.     {
  859.         $searchCode_full = $parameters['searchCode'];
  860.         $searchCode_external = substr(strstr($searchCode_full, '@'), 1); // right subString after @
  861.         $searchCode_local = strstr($searchCode_full, '@', true); // left subString before @
  862.         $parameters_hub = [];
  863.         $parameters_hub['searchCode'] = $searchCode_external;
  864.         $parameters_hub['language'] = 'fr-FR';
  865.         foreach ($parameters['rooms'] as $room) {
  866.             $room_hub = [
  867.                 'ratekey' => substr(strstr($room['rateKey'], '@'), 1) // right subString after @
  868.             ];
  869.             $parameters_hub['rooms'][] = $room_hub;
  870.         }
  871.         $apiResponse = $this->helpers->getApiResponse(
  872.             method: 'POST',
  873.             api_base_url: $this->parameterService->getHotelHubUrl(),
  874.             endPoint: HUB_HOTEL_API_CHECKRATE_ENDPOINT,
  875.             api_key: $this->parameterService->getHotelHubApiKey(),
  876.             request_body: $parameters_hub
  877.         );
  878.         $apiResults = $apiResponse['response'];
  879.         $tokenForBook = $apiResponse['tokenForBook'];
  880.         $availableHotels = $this->normalizeSearchResultsFromHubFormat($apiResults, $customer->getCurrency(), $searchCode_local, $tokenForBook, true);
  881.         $this->formatHotelAvailabilityArray($availableHotels);
  882.         $this->applyMarginsAndHideExcludedHotelXml($availableHotels, $customer);
  883.         return $availableHotels;
  884.     }
  885.     private function getCancellationsFeesArray(
  886.         int $contract_id,
  887.         int $hrt_id,
  888.         HotelAvailabilityRequest $request,
  889.         $hrta_promo_sale_price
  890.     ): array {
  891.         $cancellations_array = [];
  892.         $cancellationsPolicies = $this->cancellationPolicyRepository->findRelevantCancellations($request->getCheckInDate(), $contract_id, $hrt_id);
  893.         foreach ($cancellationsPolicies as $cancellationPolicy) {
  894.             // dd('here8');
  895.             $cancellation_date = clone $request->getCheckInDate();
  896.             $j_days = $cancellationPolicy->getMinimumDaysBefore() . "days";
  897.             date_sub($cancellation_date, date_interval_create_from_date_string($j_days));
  898.             $cancellation_array['from'] = $cancellation_date->format('Y-m-d');
  899.             //dd($request->getNbStay());
  900.             $cancellation_array['fee'] = $this->getCancellationFee($cancellationPolicy, $hrta_promo_sale_price, $request->getNbStay());
  901.             $cancellations_array[] = $cancellation_array;
  902.         }
  903.         return $cancellations_array;
  904.     }
  905.     private function operationAmount(?string $price, ?string $d_value, ?string $calculMethod): float
  906.     {
  907.         if ($calculMethod == 'amount') {
  908.             return $d_value;
  909.         } else { // percent
  910.             return $price * $d_value / 100.0;
  911.         }
  912.     }
  913.    /* public function getBestPriceBoard(array $hotel, $current_source_key): array
  914.     {
  915.         $bestPrice_current_sourceKey = 9999999;
  916.         $bestPrice_hotel = null;
  917.         $bestBoard_hotel = null;
  918.         $bestRooms_hotel = [];
  919.         foreach ($hotel['sources'] as $source) {
  920.             $roomsLabels = [];
  921.             foreach ($source['hrts'] as $boardName => $boards) {
  922.                 $totalPrice = 0; // Initialize total price for this boardName
  923.                 foreach ($boards as $roomIdx => $board) {
  924.                     $minPrice = null; // Initialize minimum price for this index
  925.                     $minPriceRoomType = "Room"; // The roomType corresponding to the min-price
  926.                     foreach ($board as $offer) {
  927.                         $price = $offer['salePromoRate']; // Get the price of this offer
  928.                         if ($minPrice === null || $price < $minPrice) {
  929.                             $minPrice = $price; // Update minimum price if found a lower price
  930.                             $minPriceRoomType = $offer['roomName'];
  931.                         }
  932.                     }
  933.                     $totalPrice += $minPrice; // Add the lowest price for this index to the total price
  934.                     $roomsLabels[$roomIdx] = $minPriceRoomType;
  935.                 }
  936.                 // Check if this boardName has the lowest total price
  937.                 if ($bestPrice_hotel === null || $totalPrice < $bestPrice_hotel) {
  938.                     $bestPrice_hotel = $totalPrice;
  939.                     $bestBoard_hotel = $boardName;
  940.                     $bestRooms_hotel = $roomsLabels;
  941.                     if ($current_source_key == $source['sourceKey']) {
  942.                         $bestPrice_current_sourceKey = $totalPrice;
  943.                     }
  944.                 }
  945.             }
  946.         }
  947.         return ['bestPrice' => $bestPrice_hotel,
  948.             'bestBoard' => $bestBoard_hotel,
  949.             'bestRooms' => $bestRooms_hotel,
  950.             'bestPrice_currentSourceKey' => $bestPrice_current_sourceKey
  951.         ];
  952.     }*/
  953.     public function getBestPriceBoardForSource(array $source): array
  954.     {
  955.         $bestPrice_source = null;
  956.         $bestBoard_source = null;
  957.         $bestRooms_source = [];
  958.         foreach ($source['hrts'] as $boardName => $boards) {
  959.             $totalPrice = 0;
  960.             $roomsLabels = [];
  961.             foreach ($boards as $roomIdx => $board) {
  962.                 $minPrice = null;
  963.                 $minPriceRoomType = "Room";
  964.                 foreach ($board as $offer) {
  965.                     $price = $offer['salePromoRate'];
  966.                     if ($minPrice === null || $price < $minPrice) {
  967.                         $minPrice = $price;
  968.                         $minPriceRoomType = $offer['roomName'];
  969.                     }
  970.                 }
  971.                 $totalPrice += $minPrice;
  972.                 $roomsLabels[$roomIdx] = $minPriceRoomType;
  973.             }
  974.             if ($bestPrice_source === null || $totalPrice < $bestPrice_source) {
  975.                 $bestPrice_source = $totalPrice;
  976.                 $bestBoard_source = $boardName;
  977.                 $bestRooms_source = $roomsLabels;
  978.             }
  979.         }
  980.         return [
  981.             'bestPrice' => $bestPrice_source,
  982.             'bestBoard' => $bestBoard_source,
  983.             'bestRooms' => $bestRooms_source,
  984.         ];
  985.     }
  986.     public function checkStatusHub(OrderLine $orderline)
  987.     {
  988.         $parameters = [
  989.             "bookingSource" => $orderline->getProduct()->getSource(),
  990.             "bookingId" => $orderline->getExternalOrderId()
  991.         ];
  992.         $apiResponse = $this->helpers->getApiResponse(
  993.             method: 'POST',
  994.             api_base_url: $this->parameterService->getHotelHubUrl(),
  995.             endPoint: HUB_HOTEL_API_CHECKSTATUS_ENDPOINT,
  996.             api_key: $this->parameterService->getHotelHubApiKey(),
  997.             request_body: $parameters
  998.         );
  999.         return $apiResponse['response'];
  1000.     }
  1001.     protected function getHrtaRateKey(HotelAvailabilityRequest $hotelReq, int $room_index, int $sourceXmlId, $sourceId, $hrtaCode): string
  1002.     {
  1003.         $rateKeyElements = [];
  1004.         $rateKeyElements[] = $hotelReq->getCheckInDate()->format('Ymd'); // #0
  1005.         $rateKeyElements[] = $hotelReq->getCheckOutDate()->format('Ymd'); // #1
  1006.         $rateKeyElements[] = $sourceXmlId; // sourceXmlId 0:local OR XML_ID  // #2
  1007.         $rateKeyElements[] = $sourceId; // #3
  1008.         $rateKeyElements[] = $hotelReq->getCurrency(); // #4 sale currency
  1009.         $rateKeyElements[] = $hotelReq->getAgencyID(); // #5 agency id
  1010.         $rateKeyElements[] = $hrtaCode; #6
  1011.         $occAdult = $hotelReq->getNbPersonGen('adult', $room_index) . ':' . implode(',', $hotelReq->getAges('adult', $room_index));
  1012.         $occChild = $hotelReq->getNbPersonGen('child', $room_index) . ':' . implode(',', $hotelReq->getAges('child', $room_index));
  1013.         $room_occupancy = $occAdult . '+' . $occChild;
  1014.         $rateKeyElements[] = $room_occupancy; #7
  1015.         return implode('|', $rateKeyElements);
  1016.     }
  1017.     private function getHrtaRateKeyForSpecifiedPeriod(OrderSubLine $orderSubLine, $startDate, $endDate): string
  1018.     {
  1019.         $rateKeyElements = [];
  1020.         $rateKeyElements[] = $startDate->format('Ymd'); // #0
  1021.         $rateKeyElements[] = $endDate->format('Ymd'); // #1
  1022.         $rateKeyElements[] = 0; // OrderLine management for local-contract only
  1023.         $rateKeyElements[] = $orderSubLine->getOrderLine()->getProduct()->getId(); // hotel_contract_id
  1024.         $rateKeyElements[] = $orderSubLine->getOrderLine()->getOrderAtt()->getCurrency();
  1025.         $rateKeyElements[] = $orderSubLine->getOrderLine()->getOrderAtt()->getAgency()->getId(); // Agency ID
  1026.         $rateKeyElements[] = $orderSubLine->getPeaID(); // hrta-id (pea-code : product-elt-arr-code)
  1027.         $occAdult = $orderSubLine->getNbAdult() . ':' . $this->getTmpAgesString($orderSubLine->getNbAdult(), 40);
  1028.         $occChild = $orderSubLine->getNbChild() . ':' . $this->getTmpAgesString($orderSubLine->getNbChild(), 16);
  1029.         $room_occupancy = $occAdult . '+' . $occChild;
  1030.         $rateKeyElements[] = $room_occupancy; // room occupancy
  1031.         return implode('|', $rateKeyElements);
  1032.     }
  1033.     private function getTmpAgesString($count, $age): string
  1034.     {
  1035.         $array = [];
  1036.         for ($i = 0; $i < $count; $i++) {
  1037.             $array[] = $age;
  1038.         }
  1039.         return implode(',', $array);
  1040.     }
  1041.     public function hotelDetails($codeHotel, $locale): array
  1042.     {
  1043.         $request = $this->requestStack->getCurrentRequest();
  1044.         if ($request) {
  1045.             $request->getSession()->set('_locale', $locale); // store in session
  1046.             $request->setLocale($locale);              // optional: apply immediately
  1047.         }
  1048.         $hotel = $this->entityManager->getRepository(Hotel::class)->find($codeHotel);
  1049.         $hotelType = $hotel->getType();
  1050.         $hotelCity = $hotel->getCity();
  1051.         $hotelCountry = $hotelCity->getCountry();
  1052.         return [
  1053.             "hotelId" => '0-' . $hotel->getId(), // todo hash this hotel id
  1054.             "name" => $hotel->getName(),
  1055.             "type" => ($hotelType ? $hotelType->getName() : ''),
  1056.             "starRating" => $hotel->getStarRating(),
  1057.             "country" => $hotelCountry->getName(),
  1058.             "city" => $hotelCity->getName(),
  1059.             "latitude" => ($hotel->getLatitude() != '' ? $hotel->getLatitude() : $hotel->getCity()->getLatitude()),
  1060.             "longitude" => ($hotel->getLongitude() != '' ? $hotel->getLongitude() : $hotel->getCity()->getLongitude()),
  1061.             "thumbImage" => $hotel->getPrimaryImageUrl(),
  1062.             "description" => $hotel->getDescription(),
  1063.             "phone" => $hotel->getPhone(),
  1064.             "website" => $hotel->getWebsite(),
  1065.             "tripadvisorId" => $hotel->getTripadvisorId(),
  1066.             "options" => $hotel->getOptions()->getValues(),
  1067.             'facilities' => array_map(function ($facility) {
  1068.                 return array(
  1069.                     'name' => $facility->getName(),
  1070.                     'ordre' => $facility->getOrdre(),
  1071.                     'icon' => $facility->getIcon()
  1072.                 );
  1073.             }, $hotel->getFacilities()->getValues()),
  1074.             'images' => array_map(function ($filedata) {
  1075.                 return array(
  1076.                     'alt' => $filedata->getAlt(),
  1077.                     'url' => $filedata->getUrl()
  1078.                 );
  1079.             }, $hotel->getImages()->getValues()),
  1080.             'promos' => $this->getHotelPromos($hotel->getId()),
  1081.             'seo' => [
  1082.                 'title' => $hotel->getSeoTitle(),
  1083.                 'description' => $hotel->getSeoDescription()
  1084.             ]
  1085.         ];
  1086.     }
  1087.     private function getHotelPromos(int $hotelId): array
  1088.     {
  1089.         $promos_array = [];
  1090.         $promos = $this->promoRepository->getPromosByHotel($hotelId);
  1091.         $today = DateTime::createFromFormat("Y-m-d", date("Y-m-d"));
  1092.         foreach ($promos as $promo) {
  1093.             $promoType = $promo->getPromoType();
  1094.             $promo_array = [];
  1095.             switch ($promoType) {
  1096.                 case 1:
  1097.                     if ($promo->getEbEndDate() >= $today) {
  1098.                         $promo_array = [
  1099.                             'id' => $promo->getId(),
  1100.                             'title' => $promo->getTitle(),
  1101.                             'promoType' => $promo->getPromoType(),
  1102.                             'checkinStartDate' => $promo->getCheckinStartDate()->format('Y-m-d'),
  1103.                             'checkinEndDate' => $promo->getCheckinEndDate()->format('Y-m-d'),
  1104.                             'ebStartDate' => $promo->getEbStartDate()->format('Y-m-d'),
  1105.                             'ebEndDate' => $promo->getEbEndDate()->format('Y-m-d'),
  1106.                             'minStay' => $promo->getMinStay(),
  1107.                             'maxStay' => $promo->getMaxStay()
  1108.                         ];
  1109.                     }
  1110.                     break;
  1111.                 case 2:
  1112.                     if (date_diff($today, $promo->getCheckinStartDate())->days >= $promo->getEbDaysBefore()) {
  1113.                         $promo_array = [
  1114.                             'id' => $promo->getId(),
  1115.                             'title' => $promo->getTitle(),
  1116.                             'promoType' => $promo->getPromoType(),
  1117.                             'checkinStartDate' => $promo->getCheckinStartDate()->format('Y-m-d'),
  1118.                             'checkinEndDate' => $promo->getCheckinEndDate()->format('Y-m-d'),
  1119.                             'ebDaysBefore' => $promo->getEbDaysBefore(),
  1120.                             'minStay' => $promo->getMinStay(),
  1121.                             'maxStay' => $promo->getMaxStay()
  1122.                         ];
  1123.                     }
  1124.                     break;
  1125.                 case 3:
  1126.                     $promo_array = [
  1127.                         'id' => $promo->getId(),
  1128.                         'title' => $promo->getTitle(),
  1129.                         'promoType' => $promo->getPromoType(),
  1130.                         'checkinStartDate' => $promo->getCheckinStartDate()->format('Y-m-d'),
  1131.                         'checkinEndDate' => $promo->getCheckinEndDate()->format('Y-m-d'),
  1132.                         'minStay' => $promo->getMinStay(),
  1133.                         'maxStay' => $promo->getMaxStay()
  1134.                     ];
  1135.                     break;
  1136.             }
  1137.             if ($promo_array)
  1138.                 $promos_array[] = $promo_array;
  1139.         }
  1140.         return $promos_array;
  1141.     }
  1142.     public function getRelevantDestinations($query): array
  1143.     {
  1144.         $cities = $this->cityRepository->searchBySubString($query);
  1145.         $cities_array = [];
  1146.         foreach ($cities as $city) {
  1147.             $cities_array[] = [
  1148.                 'id' => $city->getExternalId(),
  1149.                 'name' => $city->getName(),
  1150.                 'type' => 'city',
  1151.                 'city' => $city->getName(),
  1152.                 'country' => $city->getCountry() ? $city->getCountry()->getName() : '',
  1153.             ];
  1154.         }
  1155.         return $cities_array;
  1156.     }
  1157.     public function getHotels(int $cityID): array
  1158.     {
  1159.         $hotels = $this->hotelRepository->findBy(['city' => $cityID]);
  1160.         $hotels_array = [];
  1161.         foreach ($hotels as $hotel) {
  1162.             $hotels_array[] = [
  1163.                 'id' => $hotel->getId(),
  1164.                 'name' => $hotel->getName()
  1165.             ];
  1166.         }
  1167.         return $hotels_array;
  1168.     }
  1169.     /**
  1170.      * Check rateKeys from HUB to get price, then, if customer balance is sufficient, execute bookFromHUB method.
  1171.      * @param mixed $parameters
  1172.      * @param $order
  1173.      * @return OrderLine|null
  1174.      */
  1175.     public function bookFromHubChecked(mixed $parameters, $order): ?OrderLine
  1176.     {
  1177.         // 1: checkrate from HUB
  1178.         $checkRateResponse = $this->checkRateHub($parameters, $order->getCustomer());
  1179.         $checkRateAmount = $this->getAmountFromCheckRateHubResponse($checkRateResponse);
  1180.         // 2: get customer balance (add optionally overdraft)
  1181.         $customerBalance = $this->walletService->getWallet($order->getCustomer())['balance'];
  1182.         $authorizedCreditAmount = $order->getCustomer()->getCreditAmountAuthorized();
  1183.         $customerBalancePlus = $customerBalance + $authorizedCreditAmount;
  1184.         //dd($customerBalancePlus);
  1185.         // IF BALANCE IS OK : BOOK FROM HUB
  1186.         $balanceSufficient = $customerBalancePlus >= $checkRateAmount;
  1187.         if ($balanceSufficient) {
  1188.             $orderLine = self::getHubOrderLine_parameters($parameters, $order);
  1189.             return $this->bookHubOrderline($orderLine);
  1190.         }
  1191.         return null;
  1192.     }
  1193.     private function getHubOrderLine_parameters(mixed $parameters, &$order): ?OrderLine
  1194.     {
  1195.         $product = $this->xmlApiRepository->findOneBy(['source' => $parameters['source']]);
  1196.         $orderLineRef = $this->helpers->generateShortId();
  1197.         $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($product->getProvider()->getCurrency(), $order->getCurrency());
  1198.         $hotelOrderLine = new OrderLine();
  1199.         $hotelOrderLine->setSearchCode($parameters['searchCode'] ?? "");
  1200.         $hotelOrderLine->setTokenForBook($parameters['tokenForBook'] ?? "");
  1201.         $hotelOrderLine->setProduct($product);
  1202.         $hotelOrderLine->setHotelName($parameters['hotelName'] ?? "Hotel Name");
  1203.         $hotelOrderLine->setImage($parameters['image'] ?? "");
  1204.         $hotelOrderLine->setCity($parameters['city'] ?? "");
  1205.         $hotelOrderLine->setCountry($parameters['country'] ?? "");
  1206.         $hotelOrderLine->setReference($orderLineRef);
  1207.         $hotelOrderLine->setBeneficiary($parameters['beneficiary'] ?? "");
  1208.         $hotelOrderLine->setComment($parameters['recommandations'] ?? "");
  1209.         $hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
  1210.         $tempDate = DateTime::createFromFormat('d/m/Y', '01/01/2000');
  1211.         $hotelOrderLine->setStartDate($tempDate);
  1212.         $hotelOrderLine->setEndDate($tempDate);
  1213.         $hotelOrderLine->setDeadline(null);
  1214.         $description = $hotelOrderLine->getHotelName() . " - " . $orderLineRef;
  1215.         $hotelOrderLine->setDescription($description);
  1216.         $hotelOrderLine->setState(OrderStateEnum::TEMP_AVAILABLE->getValue());
  1217.         $hotelOrderLine->setError("temp_book");
  1218.         foreach ($parameters['rooms'] as $room) {
  1219.             $hotelOrderSubLine = new OrderSubLine();
  1220.             $hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
  1221.             $hotelOrderSubLine->setRatekey($room['rateKey']);
  1222.             $hotelOrderSubLine->setDescription("Temp_room");
  1223.             $hotelOrderSubLine->setSalePrice(0);
  1224.             $hotelOrderSubLine->setPurchasePrice(0);
  1225.             $hotelOrderSubLine->setNbAdult($room['nbAdult'] ?? 0);
  1226.             $hotelOrderSubLine->setNbChild($room['nbChild'] ?? 0);
  1227.             $hotelOrderSubLine->setRoomChildrenAge($room['childrenAge'] ?? "");
  1228.             $hotelOrderSubLine->setCancelable(true);
  1229.             $hotelOrderSubLine->setState(OrderStateEnum::TEMP_AVAILABLE->getValue());
  1230.             // Create beneficiaries from paxes if provided
  1231.             if (isset($room['paxes']) && is_array($room['paxes'])) {
  1232.                 foreach ($room['paxes'] as $pax) {
  1233.                     $beneficiary = new Beneficiary();
  1234.                     $beneficiary->setCivility($pax['civility'] ?? '');
  1235.                     $beneficiary->setFirstName($pax['firstName'] ?? '');
  1236.                     $beneficiary->setLastName($pax['lastName'] ?? '');
  1237.                     $beneficiary->setAge($pax['age'] ?? '');
  1238.                     $hotelOrderSubLine->addBeneficiary($beneficiary);
  1239.                 }
  1240.             }
  1241.             $hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
  1242.         }
  1243.         $order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
  1244.         return $hotelOrderLine;
  1245.     }
  1246.     /**
  1247.      * Prepare an orderLine for Hub-HOTEL-BOOK with status : TEMP_AVAILABLE Or PENDING
  1248.      * @param mixed $cart_line
  1249.      * @param $order
  1250.      * @return OrderLine
  1251.      * @throws \Twig\Error\LoaderError
  1252.      * @throws \Twig\Error\RuntimeError
  1253.      * @throws \Twig\Error\SyntaxError
  1254.      */
  1255.     public function getHubOrderLine_cartLine(mixed $cart_line, Order $order): OrderLine
  1256.     {
  1257.         $hub_searchCode_expanded = substr(strstr($cart_line['searchCode'], '@'), 1);
  1258.         $searchCode = substr(strstr($hub_searchCode_expanded, TOKEN_FOR_BOOK_SEPARATOR), strlen(TOKEN_FOR_BOOK_SEPARATOR));
  1259.         $tokenForBook = $cart_line['tokenForBook'];
  1260.         $customer = $order->getCustomer();
  1261.         $bookingSource = strstr($cart_line['elements'][0]['rateKey'], '@', true); // left subString before @
  1262.         $product = $this->xmlApiRepository->findOneBy(['source' => $bookingSource]);
  1263.         $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($product->getProvider()->getCurrency(), $order->getCurrency());
  1264.         $hotelName = $cart_line['label'];
  1265.         $comment = "";
  1266.         if (key_exists('options', $cart_line)) {
  1267.             $options = $cart_line['options'] ?? [];
  1268.             foreach ($options as $option) {
  1269.                 $comment = $comment . ' ' . $option . ', ';
  1270.             }
  1271.         }
  1272.         $comment = $comment . " " . $cart_line['comment'];
  1273.         $date_range = $cart_line['date'];
  1274.         $start_date_str = strstr($date_range, '-', true); // left subString before -
  1275.         $end_date_str = substr(strstr($date_range, '-'), 1); // right subString after -
  1276.         $start_date = DateTime::createFromFormat('d/m/Y', $start_date_str);
  1277.         $end_date = DateTime::createFromFormat('d/m/Y', $end_date_str); //->modify('-1 day')
  1278.         $margin = $this->getMargin($bookingSource, $order->getCustomer(), $hotelName);
  1279.         $hotelOrderLine = new OrderLine();
  1280.         $hotelOrderLine->setProduct($product);
  1281.         $hotelOrderLine->setHotelName($hotelName);
  1282.         $hotelOrderLine->setHotelStarRating((int) $cart_line['rating']);
  1283.         $hotelOrderLine->setImage($cart_line['image'] ?? "");
  1284.         $hotelOrderLine->setCity($cart_line['city']);
  1285.         $hotelOrderLine->setCountry($cart_line['country']);
  1286.         $hotelOrderLine->setExternalOrderId('-');
  1287.         $hotelOrderLine->setReference($this->helpers->generateShortId());
  1288.         $hotelOrderLine->setBeneficiary($cart_line['beneficiary']['name']);
  1289.         $hotelOrderLine->setExternalOrderIdReference("-");
  1290.         $hotelOrderLine->setComment($comment);
  1291.         $hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
  1292.         $hotelOrderLine->setStartDate($start_date);
  1293.         $hotelOrderLine->setEndDate($end_date);
  1294.         $hotelOrderLine->setDeadline(null);
  1295.         $hotelOrderLine->setDescription($hotelName);
  1296.         $hotelOrderLine->setError("");
  1297.         $hotelOrderLineState = $cart_line["available"] == "true" ? OrderStateEnum::TEMP_AVAILABLE->getValue() : OrderStateEnum::PENDING->getValue();
  1298.         $hotelOrderLine->setState($hotelOrderLineState);
  1299.         $hotelOrderLine->setSearchCode($searchCode);
  1300.         $hotelOrderLine->setTokenForBook($tokenForBook);
  1301.         foreach ($cart_line['elements'] as $room_index => $cart_line_element) {
  1302.             $hotelOrderSubLine = new OrderSubLine();
  1303.             $hotelOrderSubLine->setProductElement(null);
  1304.             $hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
  1305.             $hotelOrderSubLine->setDescription($cart_line_element['label']);
  1306.             $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate(
  1307.                 $product->getProvider()->getCurrency(),
  1308.                 $order->getCurrency()
  1309.             );
  1310.             $salePrice = $cart_line_element['price'];
  1311.             $purchasePrice = ($salePrice / $exchangeRatePurchaseToSale * 100) / (100 + $margin);
  1312.             $hotelOrderSubLine->setSalePrice($cart_line_element['price']);
  1313.             $resellerMargin = 0.0;
  1314.             if ($customer->getResellerMargin() > 0) {
  1315.                 $resellerMargin = $hotelOrderSubLine->getSalePrice() * ($customer->getResellerMargin() / 100);
  1316.             }
  1317.             $hotelOrderSubLine->setPartnerCommission($resellerMargin);
  1318.             $hotelOrderSubLine->setPurchasePrice($purchasePrice);
  1319.             $hotelOrderSubLine->setNbAdult(intval($cart_line_element['adultsCount']));
  1320.             $hotelOrderSubLine->setNbChild(intval($cart_line_element['childrenCount']));
  1321.             // = substr(strstr($cart_line_element['rateKey'], '@'), 1); // remove left-substring before first '@';
  1322.             $hotelOrderSubLine->setRatekey($cart_line_element['rateKey']);
  1323.             $hotelOrderSubLine->setCancelable(false);
  1324.             foreach ($cart_line['elements'][$room_index]['guests'] as $pax) {
  1325.                 $beneficiary = new Beneficiary();
  1326.                 $beneficiary->setCivility($pax['civility']);
  1327.                 $beneficiary->setFirstName($pax['firstName']);
  1328.                 $beneficiary->setLastName($pax['lastName']);
  1329.                 $beneficiary->setAge(isset($pax['age']) && $pax['age'] !== '' ? (int)$pax['age'] : null);
  1330.                 $hotelOrderSubLine->addBeneficiary($beneficiary);
  1331.             }
  1332.             $hotelOrderSubLine->setState(OrderStateEnum::NOT_BOOKED->getValue());
  1333.             $hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
  1334.         }
  1335.         if (key_exists('fee', $cart_line) and $cart_line['fee'] > 0) {
  1336.             $hotelOrderSubLineFee = new OrderSubLine();
  1337.             $hotelOrderSubLineFee->setService(ServiceEnum::product_fee->getValue());
  1338.             $hotelOrderSubLineFee->setDescription(ServiceEnum::product_fee->getValue());
  1339.             $hotelOrderSubLineFee->setPurchasePrice($cart_line['fee']);
  1340.             $hotelOrderSubLineFee->setSalePrice($cart_line['fee']);
  1341.             $hotelOrderSubLineFee->setCancelable(false);
  1342.             $hotelOrderLine->addOrderSubLine($hotelOrderSubLineFee);
  1343.         }
  1344.         $order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
  1345.         //dd($hotelOrderLine);
  1346.         return $hotelOrderLine;
  1347.     }
  1348.     public function bookFromLocal(mixed $parameters, Order &$order, OrderLine &$hotelOrderLine = null, $apply_fee = false): ?OrderLine
  1349.     {
  1350.         try {
  1351.             $searchCodeOK = $this->isSearchCodeValid($parameters['searchCode']); // 30 minutes
  1352.             if (!$searchCodeOK)
  1353.                 return null;
  1354.             $customer = $order->getCustomer();
  1355.             // Prepare the HotelRequest
  1356.             $hotelReq = $this->checkRateKey($parameters);
  1357.             $hotelReq->setCustomer($customer);
  1358.             if ($hotelReq->getXmlSourceId() != 0)
  1359.                 return null;
  1360.             $hotelIsAvailable = true;
  1361.             $availableHotels = $this->hotelsAvailability($hotelReq);
  1362.             // Check some conditions
  1363.             if (count($availableHotels) != 1)
  1364.                 return null; // book in one hotel
  1365.             if (count($availableHotels[0]['sources']) != 1)
  1366.                 return null; // book in one source (contract)
  1367.             if (count($availableHotels[0]['sources'][0]['rooms']) != $hotelReq->getNbRooms())
  1368.                 return null; // rooms check
  1369.             // Find entities
  1370.             $hotelContract = $this->hotelContractRepository->find((int) $availableHotels[0]['sources'][0]['sourceId']);
  1371.             $hotel = $hotelContract->getHotel();
  1372.             $agency = $this->agencyRepository->find($hotelReq->getAgencyID());
  1373.             $order->setAgency($agency);
  1374.             // Currencies
  1375.             $purchaseCurrency = $hotelContract->getProvider()->getCurrency();
  1376.             $saleCurrency = $hotelReq->getCurrency();
  1377.             $customerCurrency = $order->getCustomer()->getCurrency();
  1378.             $agencyCurrency = $agency->getCurrency();
  1379.             // Currencies Exchange Rates
  1380.             $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($purchaseCurrency, $saleCurrency);
  1381.             $exchangeRateSaleToReference = $this->currencyService->calculateExchangeRate($saleCurrency, $agencyCurrency);
  1382.             $exchangeRateSaleToCustomer = $this->currencyService->calculateExchangeRate($saleCurrency, $customerCurrency);
  1383.             $order->setExchangeRateSaleToReference($exchangeRateSaleToReference);
  1384.             $order->setExchangeRateSaleToCustomer($exchangeRateSaleToCustomer);
  1385.             // HOTEL-PRODUCT ORDER LINE
  1386.             if ($hotelOrderLine == null) {
  1387.                 $hotelOrderLine = new OrderLine();
  1388.                 $hotelOrderLine->setReference($this->helpers->generateShortId());
  1389.                 $hotelOrderLine->setImage($parameters['image'] ?? "");
  1390.                 $hotelOrderLine->setBeneficiary($parameters['beneficiary'] ?? null);
  1391.             }
  1392.             $comment = "";
  1393.             if (key_exists('options', $parameters)) {
  1394.                 $options = $parameters['options'] ?? [];
  1395.                 foreach ($options as $option) {
  1396.                     $comment = $comment . ' ' . $option . ', ';
  1397.                 }
  1398.             }
  1399.             $comment = $comment . ' ' . $parameters['comment'];
  1400.             $hotelOrderLine->setProduct($hotelContract);
  1401.             $hotelOrderLine->setComment($comment);
  1402.             $hotelOrderLine->setDescription($hotel->getName() . "-" . $hotelOrderLine->getReference());
  1403.             $hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
  1404.             $hotelOrderLine->setStartDate($hotelReq->getCheckInDate());
  1405.             $hotelOrderLine->setEndDate($hotelReq->getCheckOutDate());
  1406.             $hotelOrderLine->setHotelName($hotel->getName());
  1407.             $hotelOrderLine->setHotelStarRating($hotel->getStarRating());
  1408.             $hotelOrderLine->setCity($hotel->getCity()->getName());
  1409.             $hotelOrderLine->setCountry($hotel->getCity()->getCountry()->getName());
  1410.             // Init
  1411.             $partiesOrderSubLines = [];
  1412.             $places_put_aside = []; // temporary nb places grouped by partyZoneId (to calculate the final requested places for each partyZoneId)
  1413.             // rooms loop
  1414.             $rooms = [];
  1415.             foreach ($hotelReq->getRoomsOccupancies() as $index => $room) {
  1416.                 $hrta = $availableHotels[0]['sources'][0]['rooms'][$index][0];
  1417.                 $rooms[] = $hrta;
  1418.                 $hotelRoomType = $this->hotelRoomTypeRepository->find($hrta['roomId']); // hrt entity
  1419.                 $hotelOrderSubLine = new OrderSubLine();
  1420.                 $hotelOrderSubLine->setProductElement($hotelRoomType);
  1421.                 $hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
  1422.                 $OSL_description = $hotelRoomType->getRoomType()->getName();
  1423.                 $OSL_description .= " [" . $hrta['boardName'] . "]";
  1424.                 $hotelOrderSubLine->setDescription($OSL_description);
  1425.                 $hotelOrderSubLine->setSalePrice($hrta['salePromoRate']);
  1426.                 $resellerMargin = 0.0;
  1427.                 if ($customer->getResellerMargin() > 0) {
  1428.                     $resellerMargin = $hotelOrderSubLine->getSalePrice() * ($customer->getResellerMargin() / 100);
  1429.                 }
  1430.                 $hotelOrderSubLine->setPartnerCommission($resellerMargin);
  1431.                 $hotelOrderSubLine->setPurchasePrice($hrta['purchasePromoRate']);
  1432.                 $hotelOrderSubLine->setNbAdult($hotelReq->getNbPersonGen('adult', $index));
  1433.                 $hotelOrderSubLine->setNbChild($hotelReq->getNbPersonGen('child', $index));
  1434.                 $childrenAge = implode(', ', $hotelReq->getAges('child', $index));
  1435.                 $hotelOrderSubLine->setRoomChildrenAge($childrenAge);
  1436.                 $hotelOrderSubLine->setPeaID($hrta['id']);
  1437.                 $hotelOrderSubLine->setCancelable(!$hrta['NRF']);
  1438.                 $osl_cancellationFees = $this->getCancellationsFeesArray(
  1439.                     $hotelContract->getId(),
  1440.                     $hotelRoomType->getId(),
  1441.                     $hotelReq,
  1442.                     $hotelOrderSubLine->getSalePrice()
  1443.                 );
  1444.                 $osl_cancellationFees_jsonString = json_encode($osl_cancellationFees);
  1445.                 $hotelOrderSubLine->setCancellationFeesJson($osl_cancellationFees_jsonString);
  1446.                 if (isset($parameters['rooms'][$index]['paxes']) && is_array($parameters['rooms'][$index]['paxes'])) {
  1447.                     foreach ($parameters['rooms'][$index]['paxes'] as $pax) {
  1448.                         $beneficiary = new Beneficiary();
  1449.                         $beneficiary->setCivility($pax['civility'] ?? '');
  1450.                         $beneficiary->setFirstName($pax['firstName'] ?? '');
  1451.                         $beneficiary->setLastName($pax['lastName'] ?? '');
  1452.                         $hotelOrderSubLine->addBeneficiary($beneficiary);
  1453.                     }
  1454.                 }
  1455.                 // Stock
  1456.                 $hotelSaleOperation = new ProductElementStockOperation();
  1457.                 $roomIsAvailable = false;
  1458.                 if ($hrta['availableQuantity'] >= 1) { // check the Specific Stock
  1459.                     $roomIsAvailable = true;
  1460.                     $hotelSaleOperation->setProductElement($hotelRoomType); // Served from RoomType Stock
  1461.                     $hotelSaleOperation->setProduct($hotelContract);
  1462.                 } else { // Check the General Stock
  1463.                     $generalStock = $availableHotels[0]['sources'][0]['generalStock'];
  1464.                     if ($generalStock >= 1) {
  1465.                         $roomIsAvailable = true;
  1466.                         $generalStock--;
  1467.                         $availableHotels[0]['sources'][0]['generalStock'] = $generalStock; // Update General Stock
  1468.                         $hotelSaleOperation->setProductElement(null); // Served from General Stock
  1469.                         $hotelSaleOperation->setProduct($hotelContract);
  1470.                     }
  1471.                 }
  1472.                 if ($roomIsAvailable) { // Complete information about "sale" stock operation (-1) and associate it to the orderSubLine
  1473.                     $hotelSaleOperation->setOperation(-1);
  1474.                     $hotelSaleOperation->setNumberItems(1);
  1475.                     $hotelSaleOperation->setStartDate($hotelReq->getCheckInDate());
  1476.                     $hotelSaleOperation->setEndDate($hotelReq->getLastNightDate());
  1477.                     $hotelSaleOperation->setCustomerTo($order->getCustomer());
  1478.                     $hotelOrderSubLine->setDebit($hotelSaleOperation);
  1479.                     $hotelOrderSubLine->setState(OrderStateEnum::AVAILABLE->getValue());
  1480.                 } else {
  1481.                     $hotelOrderSubLine->setState(OrderStateEnum::NOT_AVAILABLE->getValue());
  1482.                 }
  1483.                 $hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
  1484.                 $hotelIsAvailable = $hotelIsAvailable && $roomIsAvailable;
  1485.                 // Parties of this hrta
  1486.                 if (array_key_exists('parties', $hrta)) {
  1487.                     // check fail condition
  1488.                     if (count($hrta['parties']) != $hotelReq->getNbParties($index)) {
  1489.                         return null;
  1490.                     }
  1491.                     foreach ($hrta['parties'] as $extra) {
  1492.                         $partyZone = $extra['zones'][0]; // get the selected zone
  1493.                         // get entity partyZone
  1494.                         $partyZoneEntity = $this->partyZoneRepository->find($partyZone['id']);
  1495.                         $partyZoneDescription = $partyZoneEntity->getProduct()->getTitle() . ' ' . $partyZoneEntity->getZone()->getTitle();
  1496.                         $partyZoneOrderSubLine = new OrderSubLine();
  1497.                         $partyZoneOrderSubLine->setProductElement($partyZoneEntity);
  1498.                         $partyZoneOrderSubLine->setService(ServiceEnum::party->getValue());
  1499.                         $partyZoneOrderSubLine->setDescription($partyZoneDescription);
  1500.                         $partyZoneOrderSubLine->setSalePrice($partyZone['salePrice']);
  1501.                         $partyZoneOrderSubLine->setPurchasePrice($partyZone['purchasePrice']);
  1502.                         $partyZoneOrderSubLine->setNbAdult($hotelReq->getNbPersonGen('adult', $index));
  1503.                         $partyZoneOrderSubLine->setNbChild($hotelReq->getNbPersonGen('child', $index));
  1504.                         $partyZoneOrderSubLine->setCancelable(false);
  1505.                         $partiesOrderSubLines[$extra['id']][] = $partyZoneOrderSubLine; // Grouped by $partyID
  1506.                         $partyZoneSaleOperation = new ProductElementStockOperation();
  1507.                         $partyZoneSaleOperation->setProductElement($partyZoneEntity);
  1508.                         $partyZoneSaleOperation->setProduct($partyZoneEntity->getProduct());
  1509.                         $partyZoneSaleOperation->setNumberItems($hotelReq->getNbPerson($index));
  1510.                         $partyZoneSaleOperation->setOperation(-1);
  1511.                         $partyZoneSaleOperation->setStartDate($partyZoneEntity->getProduct()->getDate());
  1512.                         $partyZoneSaleOperation->setEndDate($partyZoneEntity->getProduct()->getDate());
  1513.                         $partyZoneSaleOperation->setCustomerTo($order->getCustomer());
  1514.                         if (!key_exists($partyZoneEntity->getId(), $places_put_aside)) {
  1515.                             $places_put_aside[$partyZoneEntity->getId()] = 0;
  1516.                         }
  1517.                         $nbPartyZonePlacesRequested = $places_put_aside[$partyZoneEntity->getId()] + $hotelReq->getNbPerson($index);
  1518.                         if ($nbPartyZonePlacesRequested > $partyZone['availableQuantity']) {
  1519.                             $partyZoneOrderSubLine->setState(OrderStateEnum::NOT_AVAILABLE->getValue());
  1520.                         } else {
  1521.                             $partyZoneOrderSubLine->setState(OrderStateEnum::AVAILABLE->getValue());
  1522.                             $partyZoneOrderSubLine->setDebit($partyZoneSaleOperation); // this can be set null if the hotel is not available or at least one partyZone isn't available.
  1523.                             $places_put_aside[$partyZoneEntity->getId()] += $hotelReq->getNbPerson($index);
  1524.                         }
  1525.                     }
  1526.                 }
  1527.             }
  1528.             $deadlineFlag = $this->getDeadlineFromRooms($rooms);
  1529.             if ($deadlineFlag instanceof DateTime) {
  1530.                 $hotelOrderLine->setDeadline($deadlineFlag);
  1531.             }
  1532.             $hotelOrderLine->setState(
  1533.                 $hotelIsAvailable
  1534.                 ? OrderStateEnum::CONFIRMED->getValue()
  1535.                 : OrderStateEnum::PENDING->getValue()
  1536.             );
  1537.             if ($apply_fee) {
  1538.                 $product_fee = $this->frontService->getProductFee(ModuleEnum::hotel->getValue(), $order->getCustomer());
  1539.                 if ($product_fee >= 0) {
  1540.                     $hotelOrderSubLineFee = new OrderSubLine();
  1541.                     $hotelOrderSubLineFee->setService(ServiceEnum::product_fee->getValue());
  1542.                     $hotelOrderSubLineFee->setDescription(ServiceEnum::product_fee->getValue());
  1543.                     $hotelOrderSubLineFee->setPurchasePrice($product_fee);
  1544.                     $hotelOrderSubLineFee->setSalePrice($product_fee);
  1545.                     $hotelOrderSubLineFee->setCancelable(false);
  1546.                     $hotelOrderLine->addOrderSubLine($hotelOrderSubLineFee);
  1547.                 }
  1548.             }
  1549.             $order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
  1550.             // Check All Parties are available, Otherwise, set all debit as NULL
  1551.             $all_parties_available = true;
  1552.             $partiesOrderLines = []; // get all the partyOrderlines to send to emailService
  1553.             foreach ($partiesOrderSubLines as $partyID => $partyOrderSubLines) {
  1554.                 $partyEntity = $this->partyRepository->find($partyID);
  1555.                 $partyOrderLine = new OrderLine();
  1556.                 $partyOrderLine->setProduct($partyEntity);
  1557.                 $partyIsAvailable = true;
  1558.                 foreach ($partyOrderSubLines as $partyOrderSubLine) {
  1559.                     $partyOrderLine->addOrderSubLine($partyOrderSubLine);
  1560.                     if ($partyOrderSubLine->getState() == OrderStateEnum::NOT_AVAILABLE->getValue()) {
  1561.                         $partyIsAvailable = false;
  1562.                     }
  1563.                 }
  1564.                 $all_parties_available = $all_parties_available && $partyIsAvailable;
  1565.                 $partyOrderLine->setDescription($partyEntity->getTitle());
  1566.                 $partyOrderLine->setStartDate($partyEntity->getDate());
  1567.                 $partyOrderLine->setEndDate($partyEntity->getDate());
  1568.                 $partyOrderLine->setState($partyIsAvailable ? OrderStateEnum::AVAILABLE->getValue() :
  1569.                     OrderStateEnum::NOT_AVAILABLE->getValue());
  1570.                 $partyOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
  1571.                 $partyOrderLine->setHotelName($partyEntity->getHotelContract()->getHotel()->getName());
  1572.                 $partyOrderLine->setCity($partyEntity->getHotelContract()->getHotel()->getCity()->getName());
  1573.                 $partyOrderLine->setCountry($partyEntity->getHotelContract()->getHotel()->getCity()->getCountry()->getName());
  1574.                 $order->addOrderLine($partyOrderLine); // ### Add Party OrderLine ###
  1575.                 $partiesOrderLines[] = $partyOrderLine;
  1576.             }
  1577.             // If hotel is NotAvailable, or at least one party is NA => Do not associate ALL DEBIT stock operation
  1578. //            if (!$hotelIsAvailable or !$all_parties_available) {
  1579. //                $hotelOrderLine->dissociateDebitOperations();
  1580. //            }
  1581.             return $hotelOrderLine;
  1582.         } catch (Exception $e) { // error in RateKeys
  1583.             //dd($e);
  1584.             return null;
  1585.         }
  1586.     }
  1587.     /**
  1588.      * @throws Exception
  1589.      */
  1590.     private function checkRateKey(mixed $parameters): HotelAvailabilityRequest
  1591.     {
  1592.         $RATE_KEY_NB_ELEMENTS_MIN = 8;
  1593.         $hotelReq = null;
  1594.         foreach ($parameters['rooms'] as $index => $room) {
  1595.             $room_rate_key_array = explode('|', $room['rateKey']); // TODO decode rate_key
  1596.             if (sizeof($room_rate_key_array) < $RATE_KEY_NB_ELEMENTS_MIN) {
  1597.                 throw new Exception("Erroneous check rate key : size");
  1598.             }
  1599.             if ($hotelReq == null) {
  1600.                 $hotelReq = HotelAvailabilityRequest::constructWithHrtaRateKey($room_rate_key_array);
  1601.             } else {
  1602.                 $hotelReq->addHrtaRateKey($room_rate_key_array);
  1603.             }
  1604.             //dd('check rate key  OKKK');
  1605.             $room_parties_str = $hotelReq->getParties()[$index];
  1606.             //dd($room_parties_str);
  1607.             if (!is_null($room_parties_str)) {
  1608.                 $room_parties_ids = explode(',', $room_parties_str);
  1609.                 $nb_parties_for_this_room = count($room_parties_ids); // extracted from room_rate_key
  1610.                 $rate_keys_of_parties_for_this_room = $room['parties'] ?? []; // extracted from the check_rate_parties
  1611.                 $nb_rate_keys_of_parties_for_this_room = count($rate_keys_of_parties_for_this_room);
  1612.                 if ($nb_rate_keys_of_parties_for_this_room == 0) {
  1613.                     $hotelReq->setValid(false); // only ratekey for room is specified
  1614.                 } else {
  1615.                     if ($nb_parties_for_this_room != $nb_rate_keys_of_parties_for_this_room) {
  1616.                         throw new Exception("Erroneous check rate key : parties error");
  1617.                     }
  1618.                 }
  1619.                 //dd($rate_keys_of_parties_for_this_room);
  1620.                 $room_partiesZones_str = implode(',', $rate_keys_of_parties_for_this_room);
  1621.                 $hotelReq->setPartiesZonesAtIndex($index, $room_partiesZones_str);
  1622.             }
  1623.         }
  1624.         return $hotelReq;
  1625.     }
  1626.     public function isSearchCodeValid($fullSearchCode, $SEARCH_TIME = 30): bool
  1627.     {
  1628.         if ($fullSearchCode == "ADMIN_ACCESS")
  1629.             return true; // from admin area
  1630.         // Find the position of the "@" character in the string
  1631.         $atPosition = strpos($fullSearchCode, '@');
  1632.         // Extract the substring before "@" if "@" exists (That corresponds to the searchCode_local)
  1633.         if ($atPosition !== false) {
  1634.             $searchCode_local = substr($fullSearchCode, 0, $atPosition);
  1635.         } else {
  1636.             $searchCode_local = $fullSearchCode;
  1637.         }
  1638.         $SEARCH_TIME = 30; // 30 minutes
  1639.         $searchApiLogEntry = $this->apiLogRepository->findOneBy(['searchCode' => $searchCode_local, 'endpoint' => 'availability']);
  1640.         if ($searchApiLogEntry == null) {
  1641.             return false;
  1642.         } else {
  1643.             $elapsedTimeFromSearchInMinutes = $searchApiLogEntry->getDate()->diff(new DateTime())->i;
  1644.             if ($elapsedTimeFromSearchInMinutes > $SEARCH_TIME) {
  1645.                 return false;
  1646.             } else {
  1647.                 $apiLogEntryBook = $this->apiLogRepository->findOneBy(['searchCode' => $searchCode_local, 'endpoint' => 'book', 'statusCode' => 200]);
  1648.                 if ($apiLogEntryBook) { // a successfully-book is already saved for this search
  1649.                     return false;
  1650.                 }
  1651.                 return true;
  1652.             }
  1653.         }
  1654.     }
  1655.     public function cancelFromLocal(int $bookingId): array
  1656.     {
  1657.         $orderLine = $this->orderLineRepository->find($bookingId);
  1658.         if ($orderLine->getState() == OrderStateEnum::CANCELED->getValue()) {
  1659.             return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_ORDERLINE_ALREADY_CANCELED);
  1660.         }
  1661.         if (!$orderLine->isCancelable()) {
  1662.             return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_NOT_CANCELLABLE);
  1663.         }
  1664.         $cancellation_fee = 0;
  1665.         // Cancellation of CONFIRMED orderLine
  1666.         if ($orderLine->getState() == OrderStateEnum::CONFIRMED->getValue()) {
  1667.             $orderLine->setState(OrderStateEnum::CANCELED->getValue());
  1668.             $applyFee = true;
  1669.             // remove debits operations and calculate cancellation fee
  1670.             foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
  1671.                 $debit = $orderSubLine->getDebit();
  1672.                 if ($debit) {
  1673.                     $debit->setProductElement(null);
  1674.                     $this->entityManager->remove($debit);
  1675.                 }
  1676.                 $orderSubLine->setDebit(null);
  1677.                 $orderSubLine->setState(OrderStateEnum::CANCELED->getValue());
  1678.                 $this->entityManager->persist($orderSubLine);
  1679.                 if ($applyFee) {
  1680.                     $cancellation_fee += $orderSubLine->getCurrentCancellationAmount();
  1681.                 }
  1682.                 if ($cancellation_fee > 0) { // Generate a new REFERENCE (N°DOSSIER)
  1683.                     $orderLine->setState(OrderStateEnum::CANCELED_FEE->getValue());
  1684.                     $orderSubLine_cancellation = new OrderSubLine();
  1685.                     $orderSubLine_cancellation->setSalePrice($cancellation_fee);
  1686.                     $orderSubLine_cancellation->setPurchasePrice($cancellation_fee);
  1687.                     $orderSubLine_cancellation->setService(ServiceEnum::cancellation->getValue());
  1688.                     $description = $this->translator->trans('Entities.OrderSubLine.Message.CancellationAmount') . " " . $orderLine->getReference();
  1689.                     $orderSubLine_cancellation->setDescription($description);
  1690.                     $orderSubLine_cancellation->setCancelable(true);
  1691.                     $orderLine->addOrderSubLine($orderSubLine_cancellation);
  1692.                 }
  1693.             }
  1694.             $this->entityManager->persist($orderLine);
  1695.             $this->entityManager->flush();
  1696.             return [
  1697.                 'bookingId' => $bookingId,
  1698.                 'canceled' => true,
  1699.                 'message' => CANCEL_SUCCESS,
  1700.                 'cancellationFee' => 0
  1701.             ];
  1702.         }
  1703.         $productType = get_class($orderLine->getProduct()); // HotelContract, Party, ...
  1704.         if ($productType != 'App\Entity\HotelContract') {
  1705.             return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_PRODUCT_ERROR);
  1706.         }
  1707.         // Init Dates
  1708.         $today = new DateTime();
  1709.         $today->setTime(0, 0, 0);
  1710.         $releaseDate = $orderLine->getStartDate();
  1711.         $remainingDaysInterval = date_diff($today, $releaseDate); // Interval before release date.
  1712.         if ($remainingDaysInterval->invert == 1) { // checkin data exceeded (negative interval)
  1713.             return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_RELEASE_DATE_EXCEEDED);
  1714.         }
  1715.         if (!$orderLine->isCancelable()) {
  1716.             return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_NOT_CANCELLABLE);
  1717.         }
  1718.         $deadlineDate = $orderLine->getDeadline();
  1719.         $orderLineCancellationFee = 0;
  1720.         //dd($deadlineDate);
  1721.         if ($deadlineDate != null and $today > $deadlineDate) {
  1722.             // No Free cancellation, calculate fee here
  1723.             foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
  1724.                 $osl_cancellation_fees_jsonString = $orderSubLine->getCancellationFeesJson();
  1725.                 $osl_cancellation_fees = json_decode($osl_cancellation_fees_jsonString, true);
  1726.                 $closestDateDiff = PHP_INT_MAX;
  1727.                 $closest_cancellation_fee = null;
  1728.                 foreach ($osl_cancellation_fees as $cancellation_fee) {
  1729.                     $cancellation_fee_date_from = DateTime::createFromFormat('Y-m-d', $cancellation_fee['from']);
  1730.                     $dateDiff = $cancellation_fee_date_from->diff($today)->days;
  1731.                     if ($dateDiff < $closestDateDiff) {
  1732.                         $closestDateDiff = $dateDiff;
  1733.                         $closest_cancellation_fee = $cancellation_fee;
  1734.                     }
  1735.                 }
  1736.                 $orderLineCancellationFee += $closest_cancellation_fee['fee'];
  1737.             }
  1738.             $cancellationOrderSubLine = new OrderSubLine();
  1739.             $cancellationOrderSubLine->setState(OrderStateEnum::CONFIRMED->getValue()); // Cancellation is confirmed
  1740.             $cancellationOrderSubLine->setProductElement(null);
  1741.             $cancellationOrderSubLine->setDescription(ServiceEnum::cancellation->getValue());
  1742.             $cancellationOrderSubLine->setSalePrice($orderLineCancellationFee);
  1743.             $cancellationOrderSubLine->setPurchasePrice($orderLineCancellationFee);
  1744.             $cancellationOrderSubLine->setService(ServiceEnum::cancellation->getValue());
  1745.             $cancellationOrderSubLine->setCancelable(false);
  1746.             $orderLine->addOrderSubLine($cancellationOrderSubLine);
  1747.         }
  1748.         $orderLine->setState(OrderStateEnum::CANCELED->getValue());
  1749.         $this->orderLineRepository->save($orderLine, true);
  1750.         return [
  1751.             'bookingId' => $bookingId,
  1752.             'canceled' => true,
  1753.             'message' => CANCEL_SUCCESS,
  1754.             'cancellationFee' => $orderLineCancellationFee
  1755.         ];
  1756.     }
  1757.     public function cancelSublineFromLocal(int $sublineId): array
  1758.     {
  1759.         $orderSubLine = $this->orderSublineRepository->find($sublineId);
  1760.         if ($orderSubLine->getOrderLine()->getState() == OrderStateEnum::CANCELED->getValue()) {
  1761.             return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_ORDERLINE_ALREADY_CANCELED);
  1762.         }
  1763.         if (!$orderSubLine->isCancelable()) {
  1764.             return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_NOT_CANCELLABLE);
  1765.         }
  1766.         $productType = get_class($orderSubLine->getOrderLine()->getProduct()); // HotelContract, Party, ...
  1767.         if ($productType != 'App\Entity\HotelContract') {
  1768.             return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_PRODUCT_ERROR);
  1769.         }
  1770.         // Init Dates
  1771.         $today = DateTime::createFromFormat("Y-m-d", date("Y-m-d"));
  1772.         $releaseDate = $orderSubLine->getOrderLine()->getStartDate();
  1773.         $remainingDaysInterval = date_diff($today, $releaseDate); // Interval before release date.
  1774.         /* if ($remainingDaysInterval->invert == 1) { // checkin data exceeded (negative interval)
  1775.              return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_RELEASE_DATE_EXCEEDED);
  1776.          }
  1777.          if (!$orderSubLine->getOrderLine()->isCancelable()) {
  1778.              return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_NOT_CANCELLABLE);
  1779.          }*/
  1780.         $deadlineDate = $orderSubLine->getOrderLine()->getDeadline();
  1781.         $orderSubLineCancellationFee = 0;
  1782.         if ($deadlineDate != null and $today > $deadlineDate) {
  1783.             // No Free cancellation, calculate fee here
  1784.             $osl_cancellation_fees_jsonString = $orderSubLine->getCancellationFeesJson();
  1785.             $osl_cancellation_fees = json_decode($osl_cancellation_fees_jsonString, true);
  1786.             $closestDateDiff = PHP_INT_MAX;
  1787.             $closest_cancellation_fee = null;
  1788.             if ($osl_cancellation_fees) {
  1789.                 foreach ($osl_cancellation_fees as $cancellation_fee) {
  1790.                     $cancellation_fee_date_from = DateTime::createFromFormat('Y-m-d', $cancellation_fee['from']);
  1791.                     $dateDiff = $cancellation_fee_date_from->diff($today)->days;
  1792.                     if ($dateDiff < $closestDateDiff) {
  1793.                         $closestDateDiff = $dateDiff;
  1794.                         $closest_cancellation_fee = $cancellation_fee;
  1795.                     }
  1796.                 }
  1797.                 $orderSubLineCancellationFee += $closest_cancellation_fee['fee'];
  1798.             }
  1799.             if ($orderSubLineCancellationFee > 0) {
  1800.                 $cancellationOrderSubLine = new OrderSubLine();
  1801.                 $cancellationOrderSubLine->setState(OrderStateEnum::CANCELED->getValue()); // Cancellation is confirmed
  1802.                 $cancellationOrderSubLine->setProductElement(null);
  1803.                 $cancellationOrderSubLine->setDescription(ServiceEnum::cancellation->getValue() . ' : ' . $orderSubLine->getDescription());
  1804.                 $cancellationOrderSubLine->setSalePrice($orderSubLineCancellationFee);
  1805.                 $cancellationOrderSubLine->setPurchasePrice($orderSubLineCancellationFee);
  1806.                 $cancellationOrderSubLine->setService(ServiceEnum::cancellation->getValue());
  1807.                 $cancellationOrderSubLine->setCancelable(false);
  1808.                 $orderSubLine->getOrderLine()->addOrderSubLine($cancellationOrderSubLine);
  1809.             }
  1810.         }
  1811.         $orderSubLine->setState(OrderStateEnum::CANCELED->getValue());
  1812.         $this->orderSublineRepository->save($orderSubLine, true);
  1813.         return [
  1814.             'refId' => $sublineId,
  1815.             'canceled' => true,
  1816.             'message' => CANCEL_SUCCESS,
  1817.             'cancellationFee' => $orderSubLineCancellationFee
  1818.         ];
  1819.     }
  1820.     public function cancelFromHUB(int $bookingId): array
  1821.     {
  1822.         $orderLine = $this->orderLineRepository->find($bookingId);
  1823.         $hub_cancel_request_array = [
  1824.             "bookingId" => $orderLine->getExternalOrderId(),
  1825.             "bookingSource" => $orderLine->getProduct()->getSource()
  1826.         ];
  1827.         try {
  1828.             $apiResponse = $this->helpers->getApiResponse(
  1829.                 method: 'POST',
  1830.                 api_base_url: $this->parameterService->getHotelHubUrl(),
  1831.                 endPoint: HUB_HOTEL_API_CANCEL_ENDPOINT,
  1832.                 api_key: $this->parameterService->getHotelHubApiKey(),
  1833.                 request_body: $hub_cancel_request_array
  1834.             );
  1835.             $apiResponseContent = $apiResponse['response'];
  1836.             $error = $apiResponse['error'];
  1837.             $message = $apiResponse['msg'];
  1838.             if ($error || !$apiResponseContent['canceled']) {
  1839.                 return [
  1840.                     'bookingId' => $bookingId,
  1841.                     'canceled' => false,
  1842.                     'message' => $message,
  1843.                     'cancellationFee' => 0
  1844.                 ];
  1845.             }
  1846.         } catch (Exception $e) {
  1847.             return [
  1848.                 'bookingId' => $bookingId,
  1849.                 'canceled' => false,
  1850.                 'message' => $e->getMessage(),
  1851.                 'cancellationFee' => 0
  1852.             ];
  1853.         }
  1854.         /* *****  cancellation process     ********  */
  1855.         $orderLineCancellationFee = $apiResponseContent['cancellationFee'];
  1856.         if ($orderLineCancellationFee > 0) {
  1857.             // No Free cancellation, calculate fee here
  1858.             $cancellationOrderSubLine = new OrderSubLine();
  1859.             $cancellationOrderSubLine->setState(OrderStateEnum::CONFIRMED->getValue()); // Cancellation is confirmed
  1860.             $cancellationOrderSubLine->setProductElement(null);
  1861.             $cancellationOrderSubLine->setDescription(ServiceEnum::cancellation->getValue());
  1862.             $cancellationOrderSubLine->setSalePrice($orderLineCancellationFee);
  1863.             $cancellationOrderSubLine->setPurchasePrice($orderLineCancellationFee);
  1864.             $cancellationOrderSubLine->setService(ServiceEnum::cancellation->getValue());
  1865.             $cancellationOrderSubLine->setCancelable(false);
  1866.             $orderLine->addOrderSubLine($cancellationOrderSubLine);
  1867.         }
  1868.         $orderLine->setState(OrderStateEnum::CANCELED->getValue());
  1869.         foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
  1870.             $orderSubLine->setState(OrderStateEnum::NOT_BOOKED->getValue());
  1871.         }
  1872.         $this->orderLineRepository->save($orderLine, true);
  1873.         return [
  1874.             'bookingId' => $bookingId,
  1875.             'canceled' => true,
  1876.             'message' => CANCEL_SUCCESS,
  1877.             'cancellationFee' => $orderLineCancellationFee
  1878.         ];
  1879.     }
  1880.     private function getHotelApiCancelErrorArray(int $bookingId, $message): array
  1881.     {
  1882.         return [
  1883.             'bookingId' => $bookingId,
  1884.             'canceled' => false,
  1885.             'message' => $message,
  1886.             'cancellationFee' => 0
  1887.         ];
  1888.     }
  1889.     private function getCancellationFee($cancellationPolicy, float $osl_sale_price, int $nbStay): float
  1890.     {
  1891.         switch ($cancellationPolicy->getFeeType()) {
  1892.             case "percent":
  1893.                 $fee = $osl_sale_price * $cancellationPolicy->getFeeValue() / 100.0;
  1894.                 if ($cancellationPolicy->getFeeBase() == "night")
  1895.                     $fee = $fee / $nbStay;
  1896.                 break;
  1897.             case "amount": // period
  1898.                 $fee = $cancellationPolicy->getFeeValue();
  1899.                 break;
  1900.         }
  1901.         //todo alert if there is a different case.
  1902.         return $fee;
  1903.     }
  1904.     /**
  1905.      * @throws Exception
  1906.      */
  1907.     public function normalizeSearchResultsFromHubFormat(
  1908.         mixed   $apiResults,
  1909.         string  $requestCurrency,
  1910.         string  $localSearchCode,
  1911.         ?string $tokenForBook = null,
  1912.         bool    $with_mapping = true,
  1913.         bool    $isFirstOccurrenceRoom = false
  1914.     ): array
  1915.     {
  1916.         $xml_api_entities = $this->xmlApiRepository->findAll();
  1917.         $xml_api_names = [];
  1918.         foreach ($xml_api_entities as $xml_api_entity) {
  1919.             $xml_api_names[$xml_api_entity->getSource()] = $xml_api_entity->getName();
  1920.         }
  1921.         $hotelXmlMap = [];
  1922.         if ($with_mapping) {
  1923.             $hotelUids = [];
  1924.             foreach ($apiResults as $hubResult) {
  1925.                 $hotelUids[] = $hubResult['source'] . '@' . $hubResult['hotel']['hotelUid'];
  1926.             }
  1927.             $hotelXmls = $this->hotelXmlRepository->findByCodesWithHotelAndBadges($hotelUids);
  1928.             foreach ($hotelXmls as $hotelXml) {
  1929.                 $hotelXmlMap[$hotelXml->getCode()] = $hotelXml;
  1930.             }
  1931.         }
  1932.         // --- Chargement du cache ville (une seule requête, une seule fois par batch) ---
  1933.         // Une seule ville par batch => on prend le cityName du premier résultat
  1934.         $cityName = $apiResults[0]['hotel']['cityName'] ?? null;
  1935.         $hotelCacheMap = [];
  1936.         if (!empty($cityName)) {
  1937.             $hotelCacheRow = $this->hotelCacheRepository->findOneBy(['cityKey' => strtoupper($cityName)]);
  1938.             if ($hotelCacheRow !== null) {
  1939.                 $rawData = $hotelCacheRow->getHotelsJson();
  1940.                 if (is_string($rawData)) {
  1941.                     $decoded = json_decode($rawData, true);
  1942.                     $rawData = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
  1943.                 }
  1944.                 if (is_array($rawData)) {
  1945.                     foreach ($rawData as $cacheEntry) {
  1946.                         if (!isset($cacheEntry['name'])) {
  1947.                             continue;
  1948.                         }
  1949.                         $normalizedKey = $this->normalizeHotelNameKey($cacheEntry['name']);
  1950.                         $hotelCacheMap[$normalizedKey] = $cacheEntry;
  1951.                     }
  1952.                 }
  1953.             }
  1954.         }
  1955.         // Cache des taux de change pour éviter les recalculs redondants sur une même devise
  1956.         $exchangeRateCache = [];
  1957.         $availableHotels = [];
  1958.         foreach ($apiResults as $hub_result_item) {
  1959.             $searchCodeHub = $hub_result_item['searchCode'];
  1960.             $sourceId = $hub_result_item['source'];
  1961.             $hub_hotelId = $hub_result_item['hotel']['hotelId'];
  1962.             $hub_supplierSourceId = $hub_result_item['hotel']['supplierSourceId'];
  1963.             $hub_cityId = $hub_result_item['hotel']['cityId'];
  1964.             // hotelKey au format sourceId|supplierId|hubHotelId|hubCityId
  1965.             $hotelKey = $sourceId . '|' . $hub_supplierSourceId . '|' . $hub_hotelId . '|' . $hub_cityId;
  1966.             $hotelUid = $sourceId . '@' . $hub_result_item['hotel']['hotelUid'];
  1967.             $hotel = $with_mapping ? ($hotelXmlMap[$hotelUid] ?? null)?->getHotel() : null;
  1968.             $normalizedHotelName = $this->normalizeHotelNameKey($hub_result_item['hotel']['hotelName'] ?? '');
  1969.             $cacheData = $hotelCacheMap[$normalizedHotelName] ?? null;
  1970.             if (!isset($availableHotels[$hotelKey]['hotel'])) {
  1971.                 $hub_hotel_image = $hub_result_item['hotel']['thumbImage']
  1972.                     ?: ($cacheData['thumbImage'] ?? null)
  1973.                         ?: ($cacheData['image'] ?? null);
  1974.                 $availableHotels[$hotelKey]['hotel'] = [
  1975.                     'id' => $hotelKey,
  1976.                     'hotelUid' => $hotelUid,
  1977.                     'name' => $hotel?->getName() ?? $hub_result_item['hotel']['hotelName'],
  1978.                     'rating' => $hub_result_item['hotel']['ratingId'],
  1979.                     'city' => $hub_result_item['hotel']['cityName'] ?: "City",
  1980.                     'cityId' => $hub_cityId,
  1981.                     'country' => $hub_result_item['hotel']['countryName'] ?: "Country",
  1982.                     'countryId' => $hub_result_item['hotel']['countryId'],
  1983.                     'zone' => $hub_result_item['hotel']['zoneName'] ?? $hub_result_item['hotel']['cityName'],
  1984.                     'latitude' => $hotel?->getLatitude() ?? $cacheData['latitude'] ?? $hub_result_item['hotel']['latitude'],
  1985.                     'longitude' => $hotel?->getLongitude() ?? $cacheData['longitude'] ?? $hub_result_item['hotel']['longitude'],
  1986.                     'address' => $hub_result_item['hotel']['address'] ?: ($cacheData['address'] ?? ""),
  1987.                     'image' => $hotel?->getPrimaryImageUrl() ?? $hub_hotel_image,
  1988.                     'gallery' => array_map(fn($img) => $img['path'], $hub_result_item['gallery'] ?? []),
  1989.                     'amunities' => is_array($hub_result_item['hotel']['amunities'] ?? null)
  1990.                     && !empty($hub_result_item['hotel']['amunities'])
  1991.                         ? array_slice($hub_result_item['hotel']['amunities'], 0, 4)
  1992.                         : (is_array($cacheData['facilities'] ?? null) ?
  1993.                             array_slice($cacheData['facilities'], 0, 4) : []),
  1994.                     'promotionTitle' => trim($hub_result_item['promotionTitle']),
  1995.                 ];
  1996.                 if ($isFirstOccurrenceRoom) {
  1997.                     $availableHotels[$hotelKey]['hotel']['unnormalizedHotelData'] =
  1998.                         base64_encode(gzcompress(serialize($hub_result_item)));
  1999.                 }
  2000.             }
  2001.             $sourceEntry = [
  2002.                 'xmlSourceId' => $sourceId,
  2003.                 'xmlSourceName' => $xml_api_names[$sourceId] ?? null,
  2004.                 'sourceId' => $sourceId,
  2005.                 'sourceType' => 'external',
  2006.                 'associationRequired' => $hub_result_item['associationIdRequired'] === true,
  2007.             ];
  2008.             if ($tokenForBook !== null) { // $tokenForBook existe uniquement pour CheckRate (pas Availability)
  2009.                 $sourceEntry['tokenForBook'] = $tokenForBook;
  2010.                 $searchCodeHub = $tokenForBook . TOKEN_FOR_BOOK_SEPARATOR . $searchCodeHub;
  2011.             }
  2012.             $sourceEntry['searchCode'] = $localSearchCode . '@' . $searchCodeHub;
  2013.             $rooms = [];
  2014.             foreach ($hub_result_item['rooms'] as $index => $hub_room_index) {
  2015.                 $rooms_at_index = [];
  2016.                 $hub_room_index_normalized = $isFirstOccurrenceRoom
  2017.                     ? array_slice($hub_room_index, 0, 1, true)
  2018.                     : $hub_room_index;
  2019.                 foreach ($hub_room_index_normalized as $hub_hrta) {
  2020.                     $currencyPurchase = $hub_hrta['currencyPurchase'];
  2021.                     // Réutilisation du taux de change déjà calculé pour cette paire de devises
  2022.                     $rateCacheKey = $currencyPurchase . '->' . $requestCurrency;
  2023.                     if (!isset($exchangeRateCache[$rateCacheKey])) {
  2024.                         $exchangeRateCache[$rateCacheKey] = $this->currencyService->calculateExchangeRate(
  2025.                             $currencyPurchase,
  2026.                             $requestCurrency
  2027.                         );
  2028.                     }
  2029.                     $exchangeRate = $exchangeRateCache[$rateCacheKey];
  2030.                     $ratekey = $sourceId . '@' . $hub_hrta['rateKey'];
  2031.                     $requestedRateKey = array_key_exists('requestedRateKey', $hub_hrta)
  2032.                         ? $sourceId . '@' . $hub_hrta['requestedRateKey']
  2033.                         : "";
  2034.                     $cancellationsFees = [];
  2035.                     foreach ($hub_hrta['cancellationPolicies'] as $hubCancellationPolicy) {
  2036.                         // NB: ceci écrase à chaque itération -> ne garde que la dernière police.
  2037.                         // Si plusieurs paliers doivent être conservés, utiliser $cancellationsFees[] = [...]
  2038.                         $cancellationsFees['from'] = $hubCancellationPolicy['from'];
  2039.                         $cancellationsFees['fee'] = $hubCancellationPolicy['amount'];
  2040.                     }
  2041.                     $rooms_at_index[] = [
  2042.                         'boardId' => $hub_hrta['code'],
  2043.                         'boardCode' => $hub_hrta['boardCode'],
  2044.                         'boardXmlName' => $hub_hrta['boardName'],
  2045.                         'boardName' => $hub_hrta['boardName'],
  2046.                         'roomId' => $hub_hrta['code'],
  2047.                         'roomName' => $hub_hrta['name'],
  2048.                         'purchaseCurrency' => $currencyPurchase,
  2049.                         'purchaseRate' => $hub_hrta['purchase'],
  2050.                         'saleRate' => $hub_hrta['price'] * $exchangeRate,
  2051.                         'salePromoRate' => $hub_hrta['price'] * $exchangeRate,
  2052.                         'saleCurrency' => $requestCurrency,
  2053.                         'stopSale' => $hub_hrta['rateClass'] === "SS",
  2054.                         'available' => $hub_hrta['available'],
  2055.                         'rateKey' => $ratekey,
  2056.                         'requestedRateKey' => $requestedRateKey,
  2057.                         'NRF' => $hub_hrta['rateClass'] === 'NRF',
  2058.                         'deadline' => $hub_result_item['deadline'] ?: null,
  2059.                         'notes' => $hub_hrta['notes'],
  2060.                         'cancellationsFees' => $cancellationsFees,
  2061.                     ];
  2062.                 }
  2063.                 $rooms[$index] = $rooms_at_index;
  2064.             }
  2065.             $sourceEntry['rooms'] = $rooms;
  2066.             $options = [];
  2067.             if (array_key_exists('options', $hub_result_item)) {
  2068.                 foreach ($hub_result_item['options'] as $hub_result_item_option) {
  2069.                     $options[] = $hub_result_item_option['title'];
  2070.                 }
  2071.             }
  2072.             $sourceEntry['options'] = $options;
  2073.             $sourceEntry['promos'] = $this->getHotelBadges($hotel, HotelBadgeEnum::PROMO->getValue());
  2074.             $sourceEntry['discounts'] = $this->getHotelBadges($hotel, HotelBadgeEnum::DISCOUNT->getValue());
  2075.             $sourceEntry['infos'] = $this->getHotelBadges($hotel, HotelBadgeEnum::INFO->getValue());
  2076.             $sourceEntry['facilities'] = $this->getHotelFacilities($hotelUid);
  2077.             $sourceEntry['generalStock'] = 0; // Todo Stock
  2078.             $availableHotels[$hotelKey]['sources'][$sourceId] = $sourceEntry;
  2079.         }
  2080.         return $availableHotels;
  2081.     }
  2082.     private function normalizeHotelDetailFromHubFormat($hotelDetailHub, string $hotelId): array
  2083.     {
  2084.         $hotelUid = $hotelDetailHub['source'].'@'.$hotelId;
  2085.         $hotelXml = $this->hotelXmlRepository->findOneBy(['code' => $hotelUid]);
  2086.         if ($hotelXml == null) {
  2087.             $hotel = null;
  2088.         } else {
  2089.             $hotel = $hotelXml->getHotel();
  2090.         }
  2091.         $images = [];
  2092.         if ($hotel && count($hotel->getImages()) > 0) {
  2093.             foreach ($hotel->getImages() as $image) {
  2094.                 $images[] = [
  2095.                     'alt' => $image->getTitle(),
  2096.                     'url' => $image->getUrl(),
  2097.                 ];
  2098.             }
  2099.         } else {
  2100.             foreach ($hotelDetailHub['gallery'] as $hub_image) {
  2101.                 $images[] = [
  2102.                     'alt' => $hub_image['title'],
  2103.                     'url' => $hub_image['path']
  2104.                 ];
  2105.             }
  2106.         }
  2107.         $facilities = [];
  2108.         if ($hotel && count($hotel->getFacilities()) > 0) {
  2109.             foreach ($hotel->getFacilities() as $facility) {
  2110.                 $facilities[] = [
  2111.                     'name' => $facility->getName(),
  2112.                     'icon' => $facility->getIcon(),
  2113.                 ];
  2114.             }
  2115.         } else {
  2116.             foreach ($hotelDetailHub['amunities'] as $amenity) {
  2117.                 $facilities[] = [
  2118.                     'name' => $amenity['title'],
  2119.                     'icon' => $amenity['icon_class']
  2120.                 ];
  2121.             }
  2122.         }
  2123.         return [
  2124.             "hotelId" => $hotelId,
  2125.             "name" => ($hotel && $hotel->getName() != '') ? $hotel->getName() : $hotelDetailHub['hotel']['hotelName'],
  2126.             "hotelType" => ($hotelDetailHub['hotel']['hotelType'] != null ? $hotelDetailHub['hotel']['hotelType'] : ''),
  2127.             "starRating" => ($hotel && $hotel->getStarRating() != '') ? $hotel->getStarRating() : $hotelDetailHub['hotel']['rating'],
  2128.             "ratingId" => ($hotel && $hotel->getStarRating() != '') ? $hotel->getStarRating() : $hotelDetailHub['hotel']['ratingId'],
  2129.             "country" => $hotelDetailHub['hotel']['countryName'],
  2130.             "address" => $hotelDetailHub['hotel']['address'],
  2131.             "city" => $hotelDetailHub['hotel']['cityName'],
  2132.             "latitude" => ($hotel) ? (($hotel->getLatitude()) ?? $hotel->getCity()->getLatitude()) : $hotelDetailHub['hotel']['latitude'],
  2133.             "longitude" => ($hotel) ? (($hotel->getLongitude()) ?? $hotel->getCity()->getLongitude()) : $hotelDetailHub['hotel']['longitude'],
  2134.             "thumbImage" => ($hotel && $hotel->getPrimaryImageUrl() != '') ? $hotel->getPrimaryImageUrl() : $hotelDetailHub['hotel']['thumbImage'],
  2135.             "description" => ($hotel && $hotel->getDescription() != '') ? $hotel->getDescription() : $hotelDetailHub['hotel']['description'],
  2136.             "phone" => ($hotel && $hotel->getPhone() != '') ? $hotel->getPhone() : $hotelDetailHub['hotel']['phone'],
  2137.             "website" => '',
  2138.             "options" => [],
  2139.             "tripadvisorId" => "",
  2140.             "facilities" => $facilities,
  2141.             "images" => $images,
  2142.             'promos' => [], //TODO get promos from badges
  2143.             'seo' => [
  2144.                 'title' => ($hotel && $hotel->getSeoTitle() != '') ? $hotel->getSeoTitle() : $hotelDetailHub['hotel']['hotelName'],
  2145.                 'description' => ($hotel && $hotel->getSeoDescription() != '') ? $hotel->getSeoDescription() : $hotelDetailHub['hotel']['hotelName'],
  2146.             ]
  2147.         ];
  2148.     }
  2149.     /**
  2150.      * Performs an HttpRequest to HotelApi:Availability
  2151.      */
  2152.     public function query_ApiHotel_Availability($auth_data, $requestJson, $xmlSourceId, $base_uri, bool $mapping = true): array
  2153.     {
  2154.         $headers = [
  2155.             'Accept' => 'application/json',
  2156.             'Content-Type' => 'application/json',
  2157.             //            'currency' => $currency,
  2158.             'user' => $auth_data['user'],
  2159.             'timestamp' => $auth_data['timestamp'],
  2160.             'signature' => hash('sha256', $auth_data['signature'])
  2161.         ];
  2162.         if (!is_null($xmlSourceId)) {
  2163.             $headers['xmlSourceId'] = $xmlSourceId;
  2164.         }
  2165.         $requestJson['mapping'] = $mapping;
  2166.         $data['content'] = [];
  2167.         try {
  2168.             $httpClient = HttpClient::create();
  2169.             $response = $httpClient->request('POST', $base_uri . '/api/hotel/availability', [
  2170.                 'headers' => $headers,
  2171.                 'json' => $requestJson,
  2172.                 'verify_peer' => false,
  2173.                 'verify_host' => false
  2174.             ]);
  2175.             $data = json_decode($response->getContent(), true);
  2176.         } catch (Exception $e) {
  2177.             $data['error'] = [$e->getMessage()];
  2178.             $data['content'] = [];
  2179.         }
  2180.         return $data;
  2181.     }
  2182.     /**
  2183.      * Lance une requête HTTP async (non bloquante) vers l'API hotel.
  2184.      * Retourne un ResponseInterface Symfony.
  2185.      */
  2186.     public function query_ApiHotel_Availability_Async(
  2187.         $auth_data,
  2188.         $requestJson,
  2189.         $xmlSourceId,
  2190.         $base_uri,
  2191.         HttpClientInterface $httpClient,
  2192.         bool $mapping = true
  2193.     ): ResponseInterface {
  2194.         $headers = [
  2195.             'Accept'       => 'application/json',
  2196.             'Content-Type' => 'application/json',
  2197.             'user'         => $auth_data['user'],
  2198.             'timestamp'    => $auth_data['timestamp'],
  2199.             'signature'    => hash('sha256', $auth_data['signature']),
  2200.         ];
  2201.         if (!is_null($xmlSourceId)) {
  2202.             $headers['xmlSourceId'] = $xmlSourceId;
  2203.         }
  2204.         $requestJson['mapping'] = $mapping;
  2205.         // request() sans ->getContent() = non-bloquant
  2206.         return $httpClient->request('POST', $base_uri . '/api/hotel/availability', [
  2207.             'headers'     => $headers,
  2208.             'json'        => $requestJson,
  2209.             'verify_peer' => false,
  2210.             'verify_host' => false,
  2211.         ]);
  2212.     }
  2213.     public function hotelDetailsHUB(string $hotelUId): array
  2214.     {
  2215.         $parameters = [
  2216.             'hotelUid' => $hotelUId
  2217.         ];
  2218.         try {
  2219.             $apiResponse = $this->helpers->getApiResponse(
  2220.                 'POST',
  2221.                 $this->parameterService->getHotelHubUrl(),
  2222.                 HUB_HOTEL_API_DETAILS_ENDPOINT,
  2223.                 $this->parameterService->getHotelHubApiKey(),
  2224.                 $parameters
  2225.             );
  2226.             if (key_exists('response', $apiResponse)) {
  2227.                 $hotelDetailHub = $apiResponse['response'];
  2228.                 return $this->normalizeHotelDetailFromHubFormat($hotelDetailHub, $hotelUId);
  2229.             }
  2230.         } catch (Exception $e) {
  2231.             return [$e->getMessage()];
  2232.         }
  2233.         return [];
  2234.     }
  2235.     public function hotelDetailsHUBAsync(
  2236.         string $hotelId
  2237.     ): ResponseInterface {
  2238.         $parameters = [
  2239.             'hotelUid' => $hotelId
  2240.         ];
  2241.         return $this->httpClient->request(
  2242.             'POST',
  2243.             $this->parameterService->getHotelHubUrl() .
  2244.             HUB_HOTEL_API_DETAILS_ENDPOINT,
  2245.             [
  2246.                 'headers' => [
  2247.                     'api-key' => $this->parameterService->getHotelHubApiKey(),
  2248.                     'Content-Type' => 'application/json',
  2249.                 ],
  2250.                 'json' => $parameters,
  2251.                 'timeout' => 30,
  2252.                 'max_duration' => 30,
  2253.                 'max_redirects' => 0,
  2254.             ]
  2255.         );
  2256.     }
  2257.     public function streamResponses(array $responses): \Generator
  2258.     {
  2259.         $responseMap = [];
  2260.         foreach ($responses as $hotelId => $response) {
  2261.             if ($response instanceof ResponseInterface) {
  2262.                 $responseMap[spl_object_id($response)] = $hotelId;
  2263.             }
  2264.         }
  2265.         try {
  2266.             foreach ($this->httpClient->stream($responses, 30) as $response => $chunk) {
  2267.                 $hotelId = $responseMap[spl_object_id($response)] ?? null;
  2268.                 if (!$hotelId) {
  2269.                     continue;
  2270.                 }
  2271.                 if ($chunk->isTimeout()) {
  2272.                     $this->logger->warning('HUB details timeout', [
  2273.                         'hotelId' => $hotelId,
  2274.                         'url' => $response->getInfo('url'),
  2275.                     ]);
  2276.                     $response->cancel();
  2277.                     yield $hotelId => null;
  2278.                     continue;
  2279.                 }
  2280.                 if (!$chunk->isLast()) {
  2281.                     continue;
  2282.                 }
  2283.                 try {
  2284.                     $statusCode = $response->getStatusCode();
  2285.                     $content = $response->getContent(false);
  2286.                     if ($statusCode !== 200) {
  2287.                         $this->logger->error('HUB details non-200', [
  2288.                             'hotelId' => $hotelId,
  2289.                             'status' => $statusCode,
  2290.                             'url' => $response->getInfo('url'),
  2291.                             'body' => mb_substr($content, 0, 1000),
  2292.                         ]);
  2293.                         yield $hotelId => null;
  2294.                         continue;
  2295.                     }
  2296.                     $data = json_decode($content, true);
  2297.                     if (json_last_error() !== JSON_ERROR_NONE) {
  2298.                         $this->logger->warning('HUB invalid json', [
  2299.                             'hotelId' => $hotelId,
  2300.                             'error' => json_last_error_msg(),
  2301.                             'body' => mb_substr($content, 0, 500),
  2302.                         ]);
  2303.                         yield $hotelId => null;
  2304.                         continue;
  2305.                     }
  2306.                     if (isset($data['response'])) {
  2307.                         yield $hotelId => $this->normalizeHotelDetailFromHubFormat(
  2308.                             $data['response'],
  2309.                             $hotelId
  2310.                         );
  2311.                     } else {
  2312.                         yield $hotelId => $data;
  2313.                     }
  2314.                 } catch (\Throwable $e) {
  2315.                     $this->logger->error('streamResponses_item_error', [
  2316.                         'hotelId' => $hotelId,
  2317.                         'class' => get_class($e),
  2318.                         'message' => $e->getMessage(),
  2319.                         'trace' => $e->getTraceAsString(),
  2320.                     ]);
  2321.                     yield $hotelId => null;
  2322.                 }
  2323.             }
  2324.         } catch (\Throwable $e) {
  2325.             $this->logger->error('streamResponses_global_error', [
  2326.                 'class' => get_class($e),
  2327.                 'message' => $e->getMessage(),
  2328.                 'trace' => $e->getTraceAsString(),
  2329.             ]);
  2330.         }
  2331.     }
  2332.     public function getRelevantDestinationsHub(string $query, $fromAbroad)
  2333.     {
  2334.         $parameters = [
  2335.             "termSearch" => $query,
  2336.             "isEtranger" => $fromAbroad
  2337.         ];
  2338.         try {
  2339.             //http://192.168.10.20/hubchannels/
  2340.             $apiResponse = $this->helpers->getApiResponse(
  2341.                 'POST',
  2342.                 $this->parameterService->getHotelHubUrl(),
  2343.                 HUB_HOTEL_API_AUTOCOMPLETE_ENDPOINT,
  2344.                 $this->parameterService->getHotelHubApiKey(),
  2345.                 $parameters
  2346.             );
  2347.             return $apiResponse['response'];
  2348.         } catch (Exception) {
  2349.             return []; //$e->getMessage()
  2350.         }
  2351.     }
  2352.     public function getBookDetailsFromOrderline(OrderLine $orderLine): array
  2353.     {
  2354.         $rooms = [];
  2355.         foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
  2356.             if ($orderSubLine->getService() != ServiceEnum::hotel->getValue())
  2357.                 continue;
  2358.             $room = [
  2359.                 'rateKey' => $orderSubLine->getRatekey(),
  2360.                 'description' => $orderSubLine->getDescription(),
  2361.                 'adult' => $orderSubLine->getNbAdult(),
  2362.                 'child' => $orderSubLine->getNbChild(),
  2363.                 'cancellation' => json_decode($orderSubLine->getCancellationFeesJson()),
  2364.                 'NRF' => $orderSubLine->isCancelable() == false
  2365.             ];
  2366.             $rooms[] = $room;
  2367.         }
  2368.         return [
  2369.             'name' => $orderLine->getHotelName(),
  2370.             'starRating' => $orderLine->getHotelStarRating(),
  2371.             'city' => $orderLine->getCity(),
  2372.             'startDate' => $orderLine->getStartDate()->format('Y-m-d'),
  2373.             'endDate' => $orderLine->getEndDate()->format('Y-m-d'),
  2374.             'rooms' => $rooms
  2375.         ];
  2376.     }
  2377.     public function cancelOrderFromHUB($orderId): array
  2378.     {
  2379.         $order = $this->orderRepository->find($orderId);
  2380.         if (is_null($order)) {
  2381.             return $this->getHotelApiCancelErrorArray($orderId, "bookingId (order ID) Error");
  2382.         }
  2383.         $orderLinesCount = $order->getOrderLines()->count();
  2384.         if ($orderLinesCount == 0) {
  2385.             return $this->getHotelApiCancelErrorArray($orderId, "bookingID has no elements to cancel");
  2386.         }
  2387.         if ($orderLinesCount > 1) {
  2388.             return $this->getHotelApiCancelErrorArray($orderId, "bookingID has many elements");
  2389.         }
  2390.         // Here is OK (One Order that contains one orderline, so retrieve the orderLine ID and call cancelFromHub
  2391.         $orderLine = $order->getOrderLines()->get(0);
  2392.         return $this->cancelFromHUB($orderLine->getId());
  2393.     }
  2394.     public function getXmlSourceId(mixed $parameters): ?string
  2395.     {
  2396.         $first_ratekey = "";
  2397.         if (count($parameters['rooms']) > 0) {
  2398.             $first_ratekey = $parameters['rooms'][0]['rateKey'];
  2399.         }
  2400.         $xmlSourceId = 0;
  2401.         if (str_contains($first_ratekey, '@')) {
  2402.             return strstr($first_ratekey, '@', true);
  2403.         }
  2404.         return $xmlSourceId;
  2405.     }
  2406.     /**
  2407.      * @param $xml_arrangement
  2408.      * @param $arrangements_array
  2409.      * @return string
  2410.      */
  2411.     private function getTargetArrangement($xml_arrangement, $arrangements_array): string
  2412.     {
  2413.         return $xml_arrangement; // TODO optimize this auto-board_correspondance_function
  2414.         $xml_arrangement_cleaned = $xml_arrangement;
  2415.         $arrangements_distance_scores = [];
  2416.         foreach ($arrangements_array as $arrangement_name) {
  2417.             $arrangements_distance_scores[$arrangement_name] = levenshtein($xml_arrangement, $arrangement_name); // todo verify more relevant text-similarity functions
  2418.         }
  2419.         // Find and return the key with the maximum value in the similarity scores array
  2420.         return array_search(min($arrangements_distance_scores), $arrangements_distance_scores);
  2421.     }
  2422.     /**
  2423.      * Return  a deadline flag (true : FREE, false: NRF, or a datetime)
  2424.      *
  2425.      * @param array $rooms
  2426.      *
  2427.      * @return mixed
  2428.      * @throws Exception
  2429.      */
  2430.     public function getDeadlineFromRooms(array $rooms): mixed
  2431.     {
  2432.         $cancellation_free = true;
  2433.         $deadlines = [];
  2434.         foreach ($rooms as $room) {
  2435.             // CASE 1: NOT CANCELLABLE ORDER LINE
  2436.             if ($room['NRF']) {
  2437.                 return false;
  2438.             }
  2439.             if ($room['deadline'] != null) {
  2440.                 $deadlines[] = $room['deadline'];
  2441.                 $cancellation_free = false;
  2442.             }
  2443.         }
  2444.         // CASE 2 :  FREE CANCELLATION
  2445.         if ($cancellation_free) {
  2446.             return true;
  2447.         }
  2448.         // CASE 3 : We have $deadlines : we should return the earliest one.
  2449.         $dateObjects = array_map(function ($dateString) {
  2450.             return new DateTime($dateString);
  2451.         }, $deadlines);
  2452.         // return earliest date using min()
  2453.         return min($dateObjects);
  2454.     }
  2455.     public function getCancellationMessage(mixed $deadline_flag): string
  2456.     {
  2457.         if ($deadline_flag === false) {
  2458.             $cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.WarningNRF');
  2459.         } elseif ($deadline_flag === true) {
  2460.             $cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.InfoFreeCancellation');
  2461.         } else {
  2462.             $currentDate = new DateTime(); // Current date
  2463.             $deadlineDate = $deadline_flag;
  2464.             if ($currentDate > $deadlineDate) {
  2465.                 $cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.WarningCancellationWithFee');
  2466.             } else { // Before the deadline
  2467.                 $cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.WarningFreeCancellationBefore' . ' ' . $deadlineDate->format('d/m/Y'));
  2468.             }
  2469.         }
  2470.         return $cancellation_message;
  2471.     }
  2472.     public function getBookParameters_dumbo(Request $request): array
  2473.     {
  2474.         $nbRooms = $request->get("nbRooms"); // quantity : nbRooms
  2475.         $beneficiaryName = $request->get('beneficiary_name');
  2476.         if (empty($beneficiaryName)) {
  2477.             $beneficiaryFirstName = trim((string) $request->get('beneficiary_first_name', ''));
  2478.             $beneficiaryLastName = trim((string) $request->get('beneficiary_last_name', ''));
  2479.             $beneficiaryName = trim($beneficiaryFirstName . ' ' . $beneficiaryLastName);
  2480.         }
  2481.         if (empty($beneficiaryName)) {
  2482.             $beneficiaryName = trim(
  2483.                 (string) $request->get('room_0_pax_0_firstName', '') . ' ' .
  2484.                 (string) $request->get('room_0_pax_0_lastName', '')
  2485.             );
  2486.         }
  2487.         $rooms = [];
  2488.         for ($room_idx = 0; $room_idx < $nbRooms; $room_idx++) {
  2489.             /* guests */
  2490.             $room_guests = [];
  2491.             $room_adults_count = (int) $request->get("room_" . $room_idx . "_adults_count", 0);
  2492.             $room_children_count = (int) $request->get("room_" . $room_idx . "_children_count", 0);
  2493.             $room_guest_count = $room_adults_count + $room_children_count;
  2494.             for ($guest_index = 0; $guest_index < $room_guest_count; $guest_index++) {
  2495.                 // Try to get per-room per-pax detailed fields
  2496.                 $paxCivility = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_civility");
  2497.                 $paxFirstName = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_firstName");
  2498.                 $paxLastName = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_lastName");
  2499.                 $paxAge = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_age");
  2500.                 if (!empty($paxFirstName) || !empty($paxLastName)) {
  2501.                     // Use the detailed per-pax data
  2502.                     $guest = [
  2503.                         'civility' => $paxCivility ?? '',
  2504.                         'firstName' => $paxFirstName ?? '',
  2505.                         'lastName' => $paxLastName ?? '',
  2506.                         'age' => $paxAge,
  2507.                     ];
  2508.                 } else {
  2509.                     // Fallback: use the primary beneficiary name (backward compatibility)
  2510.                     $rank = $guest_index + 1;
  2511.                     $guest = [
  2512.                         'civility' => '',
  2513.                         'firstName' => $beneficiaryName,
  2514.                         'lastName' => '-' . $rank,
  2515.                     ];
  2516.                 }
  2517.                 $room_guests[] = $guest;
  2518.             }
  2519.             $room['rateKey'] = $request->get("room_" . $room_idx . "_key");
  2520.             $room['nbAdult'] = $room_adults_count;
  2521.             $room['nbChild'] = $room_children_count;
  2522.             $room['paxes'] = $room_guests;
  2523.             $rooms[] = $room;
  2524.         }
  2525.         // returning book parameters
  2526.         return [
  2527.             'rooms' => $rooms,
  2528.             'beneficiary' => $beneficiaryName,
  2529.             'comment' => $request->get("comment"),
  2530.             'options' => $request->get("options"),
  2531.             'searchCode' => $request->get('searchCode'),
  2532.         ];
  2533.     }
  2534.     public function getBookParametersForPeriodChange(OrderLine $orderLine, $startDate, $endDate): array
  2535.     {
  2536.         $rooms = [];
  2537.         foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
  2538.             if ($orderSubLine->getState() == OrderStateEnum::CANCELED->getValue())
  2539.                 continue;
  2540.             /* guests */
  2541.             $room_guests = [];
  2542.             foreach ($orderSubLine->getBeneficiaries() as $beneficiary) {
  2543.                 $guest = [
  2544.                     'civility' => '',
  2545.                     'firstName' => $beneficiary->getFirstName(),
  2546.                     'lastName' => $beneficiary->getLastName()
  2547.                 ];
  2548.                 $room_guests[] = $guest;
  2549.             }
  2550.             $room['rateKey'] = $this->getHrtaRateKeyForSpecifiedPeriod($orderSubLine, $startDate, $endDate);
  2551.             $room['paxes'] = $room_guests;
  2552.             $rooms[] = $room;
  2553.         }
  2554.         // returning book parameters
  2555.         return [
  2556.             'rooms' => $rooms,
  2557.             'comment' => $orderLine->getComment(),
  2558.             'options' => [], // no-option (already saved in comment)
  2559.             'searchCode' => "ADMIN_ACCESS",
  2560.         ];
  2561.     }
  2562.     public function reCheckRateHub(OrderLine $orderline): array
  2563.     {
  2564.         $parameters = $this->getCheckRateParametersFromOrderLine($orderline);
  2565.         $checkRateResult = $this->checkRateHub($parameters, $orderline->getOrderAtt()->getCustomer());
  2566.         if (empty($checkRateResult))
  2567.             return $checkRateResult;
  2568.         $tokenForBook = $checkRateResult[0]['sources'][0]['tokenForBook'] ?? "";
  2569.         $orderline->setTokenForBook($tokenForBook);
  2570.         $this->orderLineRepository->save($orderline, true);
  2571.         return $checkRateResult[0];
  2572.     }
  2573.     public function bookHubOrderline(OrderLine $hotelOrderLine): OrderLine
  2574.     {
  2575.         //$hub_searchCode_expanded = substr(strstr($parameters['searchCode'], '@'), 1);
  2576.         $hub_book_request_array = [];
  2577.         $hub_book_request_array['searchCode'] = $hotelOrderLine->getSearchCode();
  2578.         $hub_book_request_array['tokenForBook'] = $hotelOrderLine->getTokenForBook();
  2579.         $hub_book_request_array['recommandations'] = $hotelOrderLine->getComment();
  2580.         $hub_book_request_array['language'] = "fr-FR"; // todo dynamize this
  2581.         $hub_book_request_array['customer'] = [
  2582.             'firstName' => $hotelOrderLine->getOrderAtt()->getCustomerName(),
  2583.             'lastName' => " ",
  2584.             'email' => $hotelOrderLine->getOrderAtt()->getCustomer()->getEmail(),
  2585.             'mobile' => $hotelOrderLine->getOrderAtt()->getCustomerPhone(),
  2586.             'city' => $hotelOrderLine->getOrderAtt()->getCustomer()->getCity() ?? 'online',
  2587.             'country' => $hotelOrderLine->getOrderAtt()->getCustomer()->getCity()?->getCountry() ?? 'online'
  2588.         ];
  2589.         $hub_book_request_array['payment'] = [
  2590.             "card" => "",
  2591.             "ccv" => "",
  2592.             "expire" => ""
  2593.         ];
  2594.         foreach ($hotelOrderLine->getOrderSubLines() as $roomIdx => $orderSubLine) { // rooms
  2595.             if ($orderSubLine->getService() != ServiceEnum::hotel->getValue())
  2596.                 continue;
  2597.             if ($orderSubLine->getState() == OrderStateEnum::CANCELED->getValue())
  2598.                 continue;
  2599.             $roomRank = $roomIdx + 1;
  2600.             $paxes = [];
  2601.             // Use beneficiaries from OrderSubLine if available
  2602.             $beneficiaries = $orderSubLine->getBeneficiaries();
  2603.             if ($beneficiaries->count() > 0) {
  2604.                 foreach ($beneficiaries as $beneficiary) {
  2605.                     $paxes[] = [
  2606.                         'civility' => $beneficiary->getCivility() ?: 'Mr',
  2607.                         'firstName' => $beneficiary->getFirstName(),
  2608.                         'lastName' => $beneficiary->getLastName(),
  2609.                         'age' => $beneficiary->getAge(),
  2610.                     ];
  2611.                 }
  2612.             } else {
  2613.                 // Fallback: generate pax names from the main OrderLine beneficiary (backward compatibility)
  2614.                 $nbADT = $orderSubLine->getNbAdult();
  2615.                 for ($i = 0; $i < $nbADT; $i++) {
  2616.                     $paxRank = $i + 1;
  2617.                     $paxes[] = [
  2618.                         'civility' => "Mr",
  2619.                         'firstName' => $hotelOrderLine->getBeneficiary(),
  2620.                         'lastName' => "r" . $roomRank . " adt-" . $paxRank,
  2621.                     ];
  2622.                 }
  2623.                 $nbChild = $orderSubLine->getNbChild();
  2624.                 for ($i = 0; $i < $nbChild; $i++) {
  2625.                     $paxRank = $i + 1;
  2626.                     $paxes[] = [
  2627.                         'civility' => "Enf",
  2628.                         'firstName' => $hotelOrderLine->getBeneficiary(),
  2629.                         'lastName' => "r" . $roomRank . " chd-" . $paxRank,
  2630.                     ];
  2631.                 }
  2632.             }
  2633.             $room_hub = [
  2634.                 'ratekey' => $orderSubLine->getRatekeyHub(),
  2635.                 'paxes' => $paxes,
  2636.                 'evening' => '',
  2637.                 'supplements' => [],
  2638.             ];
  2639.             $hub_book_request_array['rooms'][] = $room_hub;
  2640.         }
  2641.         $apiResponse = $this->helpers->getApiResponse(
  2642.             method: 'POST',
  2643.             api_base_url: $this->parameterService->getHotelHubUrl(),
  2644.             endPoint: HUB_HOTEL_API_BOOK_ENDPOINT,
  2645.             api_key: $this->parameterService->getHotelHubApiKey(),
  2646.             request_body: $hub_book_request_array
  2647.         );
  2648.         $apiResponseContent = $apiResponse['response'];
  2649.         $hotelCity = "";
  2650.         $hotelName = "";
  2651.         if (isset($apiResponseContent['booking'])) {
  2652.             $hotelCity = $apiResponseContent['booking']['hotel']['cityName'];
  2653.             $hotelName = $apiResponseContent['booking']['hotel']['hotelName'];
  2654.         }
  2655.         $apiLogEntry = new ApiLog();
  2656.         $apiLogEntry->setBaseUrl('/api/hotel/');
  2657.         $apiLogEntry->setEndpoint('book');
  2658.         $apiLogEntry->setDate(new DateTime());
  2659.         $apiLogEntry->setRequest(json_encode($hub_book_request_array));
  2660.         $apiLogEntry->setUserID($hotelOrderLine->getOrderAtt()->getUser()->getId());
  2661.         $apiLogEntry->setCustomerID($hotelOrderLine->getOrderAtt()->getCustomer()->getId());
  2662.         $apiLogEntry->setSearchCode($hotelOrderLine->getSearchCode());
  2663.         $apiLogEntry->setResponse(json_encode($apiResponseContent));
  2664.         $apiLogEntry->setStatusCode($apiResponse['errorCode']);
  2665.         $apiLogEntry->setStatusMessage($apiResponse['msg']);
  2666.         $apiLogEntry->setCity($hotelCity);
  2667.         $apiLogEntry->setHotel($hotelName);
  2668.         $apiLogEntry->setTag($hotelOrderLine->getReference());
  2669.         $this->apiLogRepository->save($apiLogEntry, true);
  2670.         if (isset($apiResponseContent['booking'])) {
  2671.             $bookingID = $apiResponseContent['bookingId'] ?? "-";
  2672.             $bookingRef = $apiResponseContent['secondRef'] ?? "-";
  2673.             $hotelCountry = $apiResponseContent['booking']['hotel']['countryName'];
  2674.             $hotelStarRating = $apiResponseContent['booking']['hotel']['rating'];
  2675.             $deadline = DateTime::createFromFormat('Y-m-d', $apiResponseContent['booking']['deadline']); //->modify('-1 day')
  2676.             if (!$deadline) {
  2677.                 $deadline = null;
  2678.             }
  2679.             $hotelOrderLine->setHotelName($hotelName);
  2680.             $hotelOrderLine->setHotelStarRating((int) $hotelStarRating);
  2681.             $hotelOrderLine->setCity($hotelCity);
  2682.             $hotelOrderLine->setCountry($hotelCountry);
  2683.             $hotelOrderLine->setExternalOrderId($bookingID);
  2684.             $hotelOrderLine->setExternalOrderIdReference($bookingRef);
  2685.             $hotelOrderLine->setDeadline($deadline);
  2686.             $hotelOrderLine->setProviderComment($apiResponseContent['comments'] ?? "");
  2687.             $hotelOrderLine->setStartDate(DateTime::createFromFormat('Y-m-d', $apiResponseContent['booking']['from']));
  2688.             $hotelOrderLine->setEndDate(DateTime::createFromFormat('Y-m-d', $apiResponseContent['booking']['to']));
  2689.             $margin = $this->getMargin($hotelOrderLine->getProduct()->getSource(), $hotelOrderLine->getOrderAtt()->getCustomer(), $hotelName);
  2690.             $state = key_exists("confirmed", $apiResponseContent) && $apiResponseContent["confirmed"] == 1
  2691.                 ? OrderStateEnum::CONFIRMED->getValue()
  2692.                 : OrderStateEnum::NOT_BOOKED->getValue();
  2693.             $hotelOrderLine->setDescription($hotelName);
  2694.             $hotelOrderLine->setState($state);
  2695.             if ($state == OrderStateEnum::CONFIRMED->getValue()) {
  2696.                 $hotelOrderLine->setVoucherAuthorized(true);
  2697.             }
  2698.             foreach ($apiResponseContent['booking']['rooms'] as $room_index => $booked_room) {
  2699.                 $hotelOrderSubLine = $hotelOrderLine->getOrderSubLines()->get($room_index);
  2700.                 $OSL_description = $booked_room['name'];
  2701.                 $OSL_description .= " [" . $booked_room['boardName'] . "]";
  2702.                 $hotelOrderSubLine->setDescription($OSL_description);
  2703.                 $salePrice_ProviderCUR = $booked_room['price'] * (100.0 + $margin) / 100.0;
  2704.                 $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate(
  2705.                     $hotelOrderLine->getProduct()->getProvider()->getCurrency(),
  2706.                     $hotelOrderLine->getOrderAtt()->getCurrency()
  2707.                 );
  2708.                 $salePrice = $salePrice_ProviderCUR * $exchangeRatePurchaseToSale;
  2709.                 $hotelOrderSubLine->setSalePrice($salePrice);
  2710.                 $hotelOrderSubLine->setPurchasePrice($booked_room['price']);
  2711.                 $hotelOrderSubLine->setNbAdult(intval($booked_room['paxes']['adult']));
  2712.                 $hotelOrderSubLine->setNbChild(intval($booked_room['paxes']['child']['value']));
  2713.                 $hotelOrderSubLine->setRoomChildrenAge($booked_room['paxes']['child']['age']);
  2714.                 $hotelOrderSubLine->setCancelable($booked_room['rateClass'] != 'NRF');
  2715.                 if ($state == OrderStateEnum::CONFIRMED->getValue()) {
  2716.                     $orderSublineState = OrderStateEnum::AVAILABLE->getValue();
  2717.                 } else {
  2718.                     $orderSublineState = OrderStateEnum::NOT_AVAILABLE->getValue();
  2719.                 }
  2720.                 $hotelOrderSubLine->setState($orderSublineState);
  2721.             }
  2722.             $hotelOrderLine->setError($apiResponse['msg']);
  2723.         }
  2724.         $this->orderLineRepository->save($hotelOrderLine, true);
  2725.         return $hotelOrderLine;
  2726.     }
  2727.     public function getBookParametersFromCartLine(mixed $cart_line): array
  2728.     {
  2729.         $rooms = [];
  2730.         $ratekey = "";
  2731.         foreach ($cart_line['elements'] as $element) {
  2732.             $ratekey = $element['rateKey'];
  2733.             $room['rateKey'] = $ratekey;
  2734.             $room['paxes'] = $element['guests'];
  2735.             if (array_key_exists('parties', $element)) {
  2736.                 $room['parties'] = $element['parties'];
  2737.             }
  2738.             $rooms[] = $room;
  2739.         }
  2740.         return [
  2741.             'label' => $cart_line['label'],
  2742.             'date' => $cart_line['date'],
  2743.             'rooms' => $rooms,
  2744.             'comment' => $cart_line['comment'],
  2745.             'beneficiary' => $cart_line['beneficiary']['name'],
  2746.             'options' => $cart_line['options'],
  2747.             'searchCode' => $cart_line['searchCode'],
  2748.             'image' => $cart_line['image']
  2749.         ];
  2750.     }
  2751.     public function isAvailable(mixed $checkRateData): bool
  2752.     {
  2753.         $hotelSourceData = $checkRateData['sources'][0];
  2754.         $isAvailable = true;
  2755.         foreach ($hotelSourceData['rooms'] as $room) {
  2756.             $isRoomAvailable = $room[0]['available'];
  2757.             if (!$isRoomAvailable) {
  2758.                 $isAvailable = false;
  2759.             }
  2760.         }
  2761.         return $isAvailable;
  2762.     }
  2763.     private function getPromosFlags(array $promos): array
  2764.     {
  2765.         $promos_flags = [];
  2766.         foreach ($promos as $promo) {
  2767.             $flag = "";
  2768.             if ($promo->getCalculationMethod() == "percent") {
  2769.                 $flag .= " -" . intval($promo->getSaleValue()) . "% ";
  2770.             }
  2771.             if ($promo->getPromoType() == 1 || $promo->getPromoType() == 2) {
  2772.                 $flag .= $this->translator->trans($promo->getPromoTypeFlag()) . " ";
  2773.                 $flag .= $this->translator->trans("Pages.Common.Messages.Until") . " " . $promo->getEbEndDate()->format('d/m/Y');
  2774.             } else {
  2775.                 $flag .= $promo->getTitle();
  2776.             }
  2777.             $promos_flags[] = $flag;
  2778.         }
  2779.         return array_unique($promos_flags);
  2780.     }
  2781.     public function getHotelBadges(
  2782.         ?Hotel $hotel,
  2783.         string $badgeType
  2784.     ): array
  2785.     {
  2786.         if (!$hotel) {
  2787.             return [];
  2788.         }
  2789.         $badgesArray = [];
  2790.         foreach ($hotel->getBadges() as $badge) {
  2791.             if ($badge->getType() === $badgeType) {
  2792.                 $badgesArray[] = $badge->getName();
  2793.             }
  2794.         }
  2795.         return $badgesArray;
  2796.     }
  2797.     /* public function getHotelBadges(string $badgeType, string $hotelKey, bool $isLocalHotel = false): array
  2798.      {
  2799.          $badges_array = [];
  2800.          //        $hotelXml = $this->hotelXmlRepository->findOneBy(['code' => $hotelKey]);
  2801.          $hotelXml = $this->hotelXmlRepository->findOneHotelXmlByKey($hotelKey);
  2802.          if ($hotelXml) {
  2803.              $associatedHotel = $hotelXml->getHotel();
  2804.              if ($associatedHotel) {
  2805.                  $badges = $associatedHotel->getBadges();
  2806.                  foreach ($badges as $badge) {
  2807.                      if ($badge->getType() != $badgeType)
  2808.                          continue;
  2809.                      $badges_array[] = $badge->getName();
  2810.                  }
  2811.              }
  2812.          } else {
  2813.              // $hotelKey like 0-2541
  2814.              $localHotelCode = substr($hotelKey, strrpos($hotelKey, '-') + 1);
  2815.              if ($isLocalHotel && $localHotelCode != "") {
  2816.                  $hotel = $this->hotelRepository->find($localHotelCode);
  2817.                  if ($hotel) {
  2818.                      $badges = $hotel->getBadges();
  2819.                      foreach ($badges as $badge) {
  2820.                          if ($badge->getType() != $badgeType)
  2821.                              continue;
  2822.                          $badges_array[] = $badge->getName();
  2823.                      }
  2824.                  }
  2825.              }
  2826.          }
  2827.          return $badges_array;
  2828.      }*/
  2829.     public function getHotelFacilities(string $hotelKey): array
  2830.     {
  2831.         //        $hotelXml = $this->hotelXmlRepository->findOneBy(['code' => $hotelKey]);
  2832. //        if($hotelXml){
  2833. //            $associatedHotel = $hotelXml->getHotel();
  2834. //
  2835. ////            if($associatedHotel){
  2836. //
  2837. //                //$facilities = $associatedHotel->getFacilities();
  2838. //
  2839. ////                foreach ($facilities as $facility){
  2840. //////                    if($facility->getType() != $badgeType) continue;
  2841. //////                    $facilities_array[] = $facility->getName();
  2842. ////                }
  2843. ////            }
  2844. //        }
  2845.         return [];
  2846.     }
  2847.     private function getHotelXmlSalePrice(float $price, string $hotelName, $customer): float
  2848.     {
  2849.         return $price;
  2850.     }
  2851.     public function refreshListGDS(): ?string
  2852.     {
  2853.         $headers = [
  2854.             'api-key' => $this->parameterService->getHotelHubApiKey()
  2855.         ];
  2856.         if (!$headers['api-key']){
  2857.             return $this->translator->trans('Entities.Common.Alerts.HUBParam');
  2858.         }
  2859.         $response_hub = $this->helpers->getApiResponseFromHub('POST', $this->parameterService->getHotelHubUrl(), HUB_HOTEL_API_ACTIVATED_GDS_ENDPOINT, $headers);
  2860.         try {
  2861.             if (array_key_exists('error', $response_hub) && $response_hub['error']) {
  2862.                 $this->xmlApiService->disableXmlApi("HOTEL");
  2863.                 return $response_hub['msg'];
  2864.             } else { // Update XmlApi list
  2865.                 foreach ($response_hub['result'] as $hub_xml_line) {
  2866.                     $xmlSourceID = (int) $hub_xml_line['id_ligne'];
  2867.                     $partnerName = $hub_xml_line['partner_name'];
  2868.                     $xmlIsActive = $hub_xml_line['etat'] == '1';
  2869.                     $pagination = $hub_xml_line['pagination'];
  2870.                     $xmlApi = $this->entityManager->getRepository(XmlApi::class)->findOneBy(['source' => $xmlSourceID, 'xmlApiType' => 'HOTEL']);
  2871.                     if (is_null($xmlApi)) {
  2872.                         $provider = new OtherProvider();
  2873.                         $provider->setName($partnerName);
  2874.                         $provider->setActive(true);
  2875.                         $provider->setCurrency($this->parameterService->getReferenceCurrency());
  2876.                         $this->entityManager->persist($provider);
  2877.                         $xmlApi = new XmlApi();
  2878.                         $xmlApi->setSource($xmlSourceID);
  2879.                         $xmlApi->setName($partnerName);
  2880.                         $xmlApi->setGdsCode($hub_xml_line['gdscode']);
  2881.                         $xmlApi->setmarginB2C(XmlApi::XMLAPI_DEFAULT_MARGIN);
  2882.                         $xmlApi->setmarginB2B(XmlApi::XMLAPI_DEFAULT_MARGIN);
  2883.                         $xmlApi->setPayAtHotelAuthorized(false);
  2884.                         $xmlApi->setXmlApiType('HOTEL');
  2885.                         $xmlApi->setProvider($provider);
  2886.                     }
  2887.                     $xmlApi->setIsAllowedPagination($pagination);
  2888.                     $xmlApi->setCommissionType(CommissionTypeEnum::MARGIN->getValue());
  2889.                     $xmlApi->setActive($xmlIsActive);
  2890.                     $this->entityManager->persist($xmlApi);
  2891.                 }
  2892.                 $this->entityManager->flush();
  2893.             }
  2894.         }catch (Exception $e){
  2895.             $hub_msg = "";
  2896.             if(key_exists("msg", $response_hub)){
  2897.                 $hub_msg =  $response_hub['msg'];
  2898.             }
  2899.             return $hub_msg. " | ". $e->getMessage();
  2900.         }
  2901.         return "";
  2902.     }
  2903.     private function getCheckRateParametersFromOrderLine(OrderLine $orderline): array
  2904.     {
  2905.         $nbRooms = count($orderline->getOrderSubLines());
  2906.         $rooms = [];
  2907.         foreach ($orderline->getOrderSubLines() as $orderSubLine) {
  2908.             if ($orderSubLine->getService() != 'hotel')
  2909.                 continue;
  2910.             if (is_null($orderSubLine->getRatekey()))
  2911.                 continue;
  2912.             $room['rateKey'] = $orderSubLine->getRatekey();
  2913.             $rooms[] = $room;
  2914.         }
  2915.         // returning checkrate parameters
  2916.         return [
  2917.             'rooms' => $rooms,
  2918.             'searchCode' => $orderline->getSearchCode()
  2919.         ];
  2920.     }
  2921.     private function getAmountFromCheckRateHubResponse(array $checkRateResponse)
  2922.     {
  2923.         $amount = 0;
  2924.         $hotel = $checkRateResponse[0];
  2925.         $hotelSource = $hotel['sources'][0];
  2926.         $hotelSourceRooms = $hotelSource['rooms'];
  2927.         foreach ($hotelSourceRooms as $index => $room_index) {
  2928.             $roomPrice = $room_index[0]['saleRate'];
  2929.             $amount += $roomPrice;
  2930.         }
  2931.         return $amount;
  2932.     }
  2933.     public function getHotelCustomRating($hotelName): ?string
  2934.     {
  2935.         $hotel = $this->hotelRepository->findOneBy(['name' => $hotelName]);
  2936.         if ($hotel) {
  2937.             return $hotel->getCustomRating();
  2938.         }
  2939.         return null;
  2940.     }
  2941.     public function getHotelCustomRatings(array $hotelNames): array
  2942.     {
  2943.         if (empty($hotelNames)) {
  2944.             return [];
  2945.         }
  2946.         $hotels = $this->hotelRepository->findBy(['name' => $hotelNames]);
  2947.         $ratings = [];
  2948.         foreach ($hotels as $hotel) {
  2949.             $ratings[$hotel->getName()] = $hotel->getCustomRating();
  2950.         }
  2951.         return $ratings;
  2952.     }
  2953.     public function getNewHotelBookingsFromHUB($parameters): array
  2954.     {
  2955.         $apiResponse = $this->helpers->getApiResponseFromHub(
  2956.             method: 'POST',
  2957.             api_base_url: $this->parameterService->getHotelHubUrl(),
  2958.             endPoint: HUB_HOTEL_API_GET_BOOKINGS_ENDPOINT,
  2959.             headers: [
  2960.                 'api-key' => $this->parameterService->getHotelHubApiKey(),
  2961.             ],
  2962.             request_body: $parameters
  2963.         );
  2964.         try {
  2965.             $gdsBookings = $apiResponse['response'] ?? [];
  2966.             //            $xmlApiBookings_inProvider = array_filter($gdsBookings, function ($booking) use ($xmlApi) {
  2967. //                return isset($booking['source']) && $booking['source'] == $xmlApi->getSource();
  2968. //            });
  2969.             $bookings_provider_indexed = [];
  2970.             foreach ($gdsBookings as $item) {
  2971.                 if (isset($item['bookingId'])) {
  2972.                     $bookings_provider_indexed[$item['bookingId']] = $item;
  2973.                 }
  2974.             }
  2975.             $orderlines = $this->orderLineRepository->findAll(); // TODO get HOTEL Order lines XML_API
  2976.             $currentStates = [];
  2977.             foreach ($orderlines as $orderLine) {
  2978.                 // TODO Optimize
  2979.                 if ($orderLine->getProduct()->getClass() != 'XmlApi')
  2980.                     continue;
  2981.                 if ($orderLine->getProduct()->getXmlApiType() != 'HOTEL')
  2982.                     continue;
  2983.                 if ($orderLine->getState() == OrderStateEnum::CANCELED->getValue())
  2984.                     continue;
  2985.                 if ($orderLine->getExternalOrderId() == null)
  2986.                     continue;
  2987.                 $currentStates[$orderLine->getExternalOrderId()] = $orderLine->getState();
  2988.             }
  2989.             $already_imported_orderlines = array_keys($currentStates);
  2990.             $bookings_provider_indexed = array_filter($bookings_provider_indexed, function ($booking) use ($already_imported_orderlines) {
  2991.                 return !in_array($booking['bookingId'], $already_imported_orderlines);
  2992.             });
  2993.             return $bookings_provider_indexed;
  2994.         } catch (\Exception) {
  2995.             return [];
  2996.         }
  2997.     }
  2998.     public function importBookingsFromHub(array $selected_IDsBookings, $xmlApiBookings_inProvider_new, Customer $customer, $user): string
  2999.     {
  3000.         try {
  3001.             $sources_entities = [];
  3002.             foreach ($selected_IDsBookings as $selected_booking_ID) {
  3003.                 $hubBooking = $xmlApiBookings_inProvider_new[$selected_booking_ID];
  3004.                 //                if($hubBooking['currency'] != $customer->getCurrency()) return "Customer Currency Error" ; // ignore
  3005.                 $start_date = DateTime::createFromFormat('Y-m-d', $hubBooking['checkIn']);
  3006.                 $end_date = DateTime::createFromFormat('Y-m-d', $hubBooking['checkOut']);
  3007.                 $deadline = DateTime::createFromFormat('Y-m-d', $hubBooking['deadline']);
  3008.                 $state = match ($hubBooking["status"]) {
  3009.                     'CONFIRMED' => OrderStateEnum::CONFIRMED->getValue(),
  3010.                     default => OrderStateEnum::PENDING->getValue(),
  3011.                 };
  3012.                 $source = $hubBooking['source'];
  3013.                 if (isset($sources_entities[$source])) {
  3014.                     $xmlApi = $sources_entities[$source];
  3015.                 } else {
  3016.                     $xmlApi = $this->entityManager->getRepository(XmlApi::class)->findOneBy(['source' => $source]);
  3017.                     $sources_entities[$source] = $xmlApi;
  3018.                 }
  3019.                 $exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($xmlApi->getProvider()->getCurrency(), $customer->getCurrency());
  3020.                 $agency = $this->agencyRepository->find($this->requestStack->getSession()->get('agency') ?? 1);
  3021.                 $order = new Order();
  3022.                 $order->setUser($user);
  3023.                 $order->setReference($this->helpers->generateShortId());
  3024.                 $order->setCustomerName($customer->getName());
  3025.                 $order->setAgency($agency);
  3026.                 $order->setCustomerPhone($customer->getPhone());
  3027.                 $order->setCustomerEmail($customer->getEmail());
  3028.                 $order->setExchangeRateSaleToReference($this->currencyService->calculateExchangeRate($customer->getCurrency(), $agency->getCurrency()));
  3029.                 $order->setExchangeRateSaleToCustomer(1);
  3030.                 $order->setCurrency($customer->getCurrency());
  3031.                 $order->setCustomer($customer);
  3032.                 $hotelOrderLine = new OrderLine();
  3033.                 $orderLineRef = $this->helpers->generateShortId();
  3034.                 $hotelOrderLine->setReference($orderLineRef);
  3035.                 $hotelOrderLine->setProduct($xmlApi);
  3036.                 $hotelOrderLine->setHotelName($hubBooking['hotelName']);
  3037.                 $hotelOrderLine->setExternalOrderId($selected_booking_ID);
  3038.                 $hotelOrderLine->setBeneficiary($hubBooking['traveller'] ?? '-');
  3039.                 $hotelOrderLine->setStartDate($start_date);
  3040.                 $hotelOrderLine->setEndDate($end_date);
  3041.                 $hotelOrderLine->setDeadline($deadline);
  3042.                 $hotelOrderLine->setState($state);
  3043.                 $hotelOrderLine->setDescription($hubBooking['hotelName'] . " - " . $orderLineRef);
  3044.                 $hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
  3045.                 $hotelOrderLine->setVoucherAuthorized(true);
  3046.                 $hotelOrderLine->setError("Imported from Provider System.");
  3047.                 $countRooms = count($hubBooking['rooms']);
  3048.                 foreach ($hubBooking['rooms'] as $hubBooking_room) {
  3049.                     $hotelOrderSubLine = new OrderSubLine();
  3050.                     $hotelOrderSubLine->setProductElement(null);
  3051.                     $hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
  3052.                     $OSL_description = $hubBooking_room['room'];
  3053.                     $OSL_description .= " [" . $hubBooking_room['board'] . "]";
  3054.                     $hotelOrderSubLine->setDescription($OSL_description);
  3055.                     $subLineSalePrice = round($hubBooking['price'] * $exchangeRatePurchaseToSale / $countRooms, 4);
  3056.                     $subLinePurchasePrice = round($hubBooking['purchase'] / $countRooms, 4);
  3057.                     $hotelOrderSubLine->setSalePrice($subLineSalePrice);
  3058.                     $hotelOrderSubLine->setPurchasePrice($subLinePurchasePrice);
  3059.                     $hotelOrderSubLine->setNbAdult(intval($hubBooking_room['adult']));
  3060.                     $hotelOrderSubLine->setNbChild(intval($hubBooking_room['child']['value']));
  3061.                     // Create Adult Beneficiaries
  3062.                     for ($i = 0; $i < intval($hubBooking_room['adult']); $i++) {
  3063.                         $beneficiary = new Beneficiary();
  3064.                         $beneficiary->setCivility('Adult');
  3065.                         $beneficiary->setFirstName('Adult');
  3066.                         $beneficiary->setLastName(($i + 1));
  3067.                         $hotelOrderSubLine->addBeneficiary($beneficiary);
  3068.                     }
  3069.                     // Create Child Beneficiaries
  3070.                     for ($i = 0; $i < intval($hubBooking_room['child']['value']); $i++) {
  3071.                         $child = new Beneficiary();
  3072.                         $child->setCivility('Child');
  3073.                         $child->setFirstName('Child');
  3074.                         $child->setLastName(($i + 1));
  3075.                         // Set child age if available in the hub data
  3076.                         if (isset($hubBooking_room['child']['age'][$i])) {
  3077.                             $child->setAge(intval($hubBooking_room['child']['age'][$i]));
  3078.                         }
  3079.                         $hotelOrderSubLine->addBeneficiary($child);
  3080.                     }
  3081.                     $hotelOrderSubLine->setState(OrderStateEnum::AVAILABLE->getValue());
  3082.                     $hotelOrderSubLine->setCancelable(false);
  3083.                     $hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
  3084.                 }
  3085.                 $order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
  3086.                 $this->entityManager->persist($order);
  3087.             }
  3088.             $this->entityManager->flush();
  3089.             return "";
  3090.         } catch (Exception $e) {
  3091.             return $e->getMessage();
  3092.         }
  3093.     }
  3094.     public function getBalanceOnGDS(XmlApi $xmlApi): ?string
  3095.     {
  3096.         $parameters = [
  3097.             "Source" => $xmlApi->getSource(),
  3098.         ];
  3099.         try {
  3100.             $apiResponse = $this->helpers->getApiResponse(
  3101.                 method: 'POST',
  3102.                 api_base_url: $this->parameterService->getHotelHubUrl(),
  3103.                 endPoint: HUB_HOTEL_API_CREDIT_CHECK_ENDPOINT,
  3104.                 api_key: $this->parameterService->getHotelHubApiKey(),
  3105.                 request_body: $parameters
  3106.             );
  3107.             if ($apiResponse['error']) {
  3108.                 return $apiResponse['msg'];
  3109.             }
  3110.             return $apiResponse['response']['amount'] . ' ' . $apiResponse['response']['currency'];
  3111.         } catch (Exception $e) {
  3112.             return "Error : " . $e->getMessage();
  3113.         }
  3114.     }
  3115.     public function changePeriod(OrderLine $orderLine, $newStartDate, $newEndDate, $applyFee): bool
  3116.     {
  3117.         $orderLineReq = new OrderLine();
  3118.         $orderLineReq->setStartDate(new DateTime($newStartDate));
  3119.         $orderLineReq->setEndDate(new DateTime($newEndDate));
  3120.         $orderLineReq->setPreviousOrderline($orderLine);
  3121.         //$orderLineReq->setType(null);
  3122.         $order = $orderLine->getOrderAtt();
  3123.         //$orderLineRequest->setPreviousOrderline($orderLine);
  3124.         //$orderLineRequest->setType(OrderLineTypeEnum::REQUEST_CHANGE_PERIOD->getValue());
  3125.         if ($orderLine->getProduct() instanceof HotelContract) {
  3126.             $book_parameters = $this->getBookParametersForPeriodChange($orderLine, $orderLineReq->getStartDate(), $orderLineReq->getEndDate());
  3127.             $found = $this->bookFromLocal($book_parameters, $order, $orderLineReq);
  3128.             if (!$found)
  3129.                 return false; // period not covered by the HotelContract
  3130.             $cancellation_fee = 0;
  3131.             if (is_null($orderLine->getType())) // logiquement tous les cas (peut etre on elimine ce if)
  3132.             {
  3133.                 // remove debits operations and calculate cancellation fee
  3134.                 foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
  3135.                     $debit = $orderSubLine->getDebit();
  3136.                     if ($debit) {
  3137.                         $debit->setProductElement(null);
  3138.                         $this->entityManager->remove($debit);
  3139.                     }
  3140.                     $orderSubLine->setDebit(null);
  3141.                     $orderSubLine->setState(OrderStateEnum::CANCELED->getValue());
  3142.                     $this->entityManager->persist($orderSubLine);
  3143.                     if ($applyFee) {
  3144.                         $cancellation_fee += $orderSubLine->getCurrentCancellationAmount();
  3145.                     }
  3146.                 }
  3147.                 if ($cancellation_fee > 0) { // Generate a new REFERENCE (N°DOSSIER)
  3148.                     $orderLineReq->setReference($this->helpers->generateShortId());
  3149.                     $orderLine->setState(OrderStateEnum::CANCELED_FEE->getValue());
  3150.                     $orderLine->setType(null); // TODO delete may be this line (useless)
  3151.                     $orderSubLine_cancellation = new OrderSubLine();
  3152.                     $orderSubLine_cancellation->setSalePrice($cancellation_fee);
  3153.                     $orderSubLine_cancellation->setPurchasePrice($cancellation_fee);
  3154.                     $orderSubLine_cancellation->setService(ServiceEnum::cancellation->getValue());
  3155.                     $description = $this->translator->trans('Entities.OrderSubLine.Message.CancellationAmount') . " " . $orderLine->getReference();
  3156.                     $orderSubLine_cancellation->setDescription($description);
  3157.                     $orderSubLine_cancellation->setCancelable(false);
  3158.                     $orderLine->addOrderSubLine($orderSubLine_cancellation);
  3159.                 } else {
  3160.                     $orderLineReq->setReference($orderLine->getReference());
  3161.                     $orderLine->setState(OrderStateEnum::CANCELED->getValue());
  3162.                     $orderLine->setType(OrderLineTypeEnum::HISTORY->getValue());
  3163.                 }
  3164.                 // add debit operations to the new OrderLine
  3165.                 //&& ( $orderLine->isPending() || $orderLineReq->isConfirmed() )
  3166.                 if ($orderLineReq->isConfirmed()) {
  3167.                     foreach ($orderLineReq->getOrderSubLines() as $orderSubLine) {
  3168.                         if (!is_null($orderSubLine->getDebit()))
  3169.                             continue; //|| $orderSubLine->getState() == OrderStateEnum::CANCELED->getValue()
  3170.                         $orderSubLine->associateDebitOperation(false);
  3171.                     }
  3172.                 }
  3173.             }
  3174.             //            elseif(str_contains($orderLine->getType(), OrderLineTypeEnum::REQUEST->getValue())) { // modify request by another request
  3175. //                $orderLineReq->setPreviousOrderline($orderLine->getPreviousOrderline());
  3176. //                $orderLine->setType(OrderLineTypeEnum::HISTORY->getValue());
  3177. //                $orderLine->setState(OrderStateEnum::CANCELED->getValue());
  3178. //                //$orderLine->dissociateDebitOperations();
  3179. //            }
  3180.             $this->entityManager->persist($order);
  3181.             return $orderLineReq->isConfirmed();
  3182.         }
  3183.         return false;
  3184.     }
  3185.     public function syncOrderLineWithHub(OrderLine $orderLine, bool $applyChanges = false): array
  3186.     {
  3187.         try {
  3188.             $hubBookingData = $this->fetchBookingFromHub($orderLine);
  3189.             if (!$hubBookingData['success']) {
  3190.                 return [
  3191.                     'success' => false,
  3192.                     'message' => $hubBookingData['message']
  3193.                 ];
  3194.             }
  3195.             $hubData = $hubBookingData['data'];
  3196.             $comparisonResult = $this->compareHubWithLocal($orderLine, $hubData);
  3197.             if (!$comparisonResult['hasChanges']) {
  3198.                 return [
  3199.                     'success' => true,
  3200.                     'hasChanges' => false,
  3201.                     'message' => 'All data is synchronized. No changes detected.'
  3202.                 ];
  3203.             }
  3204.             if ($applyChanges) {
  3205.                 $updateResult = $this->applyHubChangesToLocal($orderLine, $hubData, $comparisonResult['differences']);
  3206.                 if (!$updateResult['success']) {
  3207.                     return [
  3208.                         'success' => false,
  3209.                         'message' => $updateResult['message']
  3210.                     ];
  3211.                 }
  3212.                 return [
  3213.                     'success' => true,
  3214.                     'hasChanges' => true,
  3215.                     'differences' => $comparisonResult['differences'],
  3216.                     'message' => 'Successfully synchronized with Hub data.'
  3217.                 ];
  3218.             }
  3219.             return [
  3220.                 'success' => true,
  3221.                 'hasChanges' => true,
  3222.                 'differences' => $comparisonResult['differences'],
  3223.                 'message' => 'Changes detected. Review and confirm to sync.'
  3224.             ];
  3225.         } catch (Exception $e) {
  3226.             return [
  3227.                 'success' => false,
  3228.                 'message' => 'Sync error: ' . $e->getMessage()
  3229.             ];
  3230.         }
  3231.     }
  3232.     private function fetchBookingFromHub(OrderLine $orderLine): array
  3233.     {
  3234.         try {
  3235.             $order = $orderLine->getOrderAtt();
  3236.             $externalOrderId = $orderLine->getExternalOrderId();
  3237.             $requestBody = [
  3238.                 'fromDate' => $order->getCreatedAt()->format('Y-m-d'),
  3239.                 'toDate' => (new DateTime())->format('Y-m-d'),
  3240.                 'bookingId' => $externalOrderId,
  3241.             ];
  3242.             //dd($requestBody);
  3243.             $baseUrl = $this->parameterService->getHotelHubUrl();
  3244.             $hubUrl = $baseUrl . HUB_HOTEL_API_BOOKING_LIST_ENDPOINT;
  3245.             $apiKey = $this->parameterService->getHotelHubApiKey();
  3246.             $httpClient = HttpClient::create();
  3247.             $response = $httpClient->request('POST', $hubUrl, [
  3248.                 'headers' => [
  3249.                     'Api-key' => $apiKey,
  3250.                 ],
  3251.                 'body' => (!empty($requestBody)) ? json_encode($requestBody) : []
  3252.             ]);
  3253.             // Parse response
  3254.             $statusCode = $response->getStatusCode();
  3255.             $responseData = json_decode($response->getContent(), true);
  3256.             //dd($responseData);
  3257.             // Check for API errors
  3258.             if ($statusCode !== 200 || empty($responseData)) {
  3259.                 return [
  3260.                     'success' => false,
  3261.                     'message' => 'Hub API returned status ' . $statusCode
  3262.                 ];
  3263.             }
  3264.             // Find the specific booking in the response
  3265.             $bookingFound = null;
  3266.             $bookings = $responseData['response'];
  3267.             foreach ($bookings as $booking) {
  3268.                 if (isset($booking['bookingId']) && $booking['bookingId'] === $externalOrderId) {
  3269.                     $bookingFound = $booking;
  3270.                     break;
  3271.                 }
  3272.             }
  3273.             if (!$bookingFound) {
  3274.                 return [
  3275.                     'success' => false,
  3276.                     'message' => 'Booking not found in Hub response (bookingId: ' . $externalOrderId . ')'
  3277.                 ];
  3278.             }
  3279.             return [
  3280.                 'success' => true,
  3281.                 'data' => $bookingFound
  3282.             ];
  3283.         } catch (TransportExceptionInterface $e) {
  3284.             return [
  3285.                 'success' => false,
  3286.                 'message' => 'Network error: ' . $e->getMessage()
  3287.             ];
  3288.         } catch (Exception $e) {
  3289.             return [
  3290.                 'success' => false,
  3291.                 'message' => 'Error fetching from Hub: ' . $e->getMessage()
  3292.             ];
  3293.         }
  3294.     }
  3295.     private function compareHubWithLocal(OrderLine $orderLine, array $hubData): array
  3296.     {
  3297.         $differences = [];
  3298.         $localData = [];
  3299.         $hubDataFormatted = [];
  3300.         // Compare status
  3301.         $hubStatus = $this->mapHubStatusToLocal($hubData['status'] ?? '');
  3302.         $localStatus = $orderLine->getState();
  3303.         $localData['status'] = $localStatus;
  3304.         $hubDataFormatted['status'] = $hubStatus;
  3305.         if ($hubStatus !== $localStatus) {
  3306.             $differences['status'] = [
  3307.                 'field' => 'Status',
  3308.                 'old' => $localStatus,
  3309.                 'new' => $hubStatus,
  3310.                 'changed' => true
  3311.             ];
  3312.         }
  3313.         // Compare hotel name
  3314.         $hubHotelName = $hubData['hotelName'] ?? '';
  3315.         $localHotelName = $orderLine->getHotelName() ?? '';
  3316.         $localData['hotelName'] = $localHotelName;
  3317.         $hubDataFormatted['hotelName'] = $hubHotelName;
  3318.         if ($hubHotelName !== $localHotelName && !empty($hubHotelName)) {
  3319.             $differences['hotelName'] = [
  3320.                 'field' => 'Hotel Name',
  3321.                 'old' => $localHotelName,
  3322.                 'new' => $hubHotelName,
  3323.                 'changed' => true
  3324.             ];
  3325.         }
  3326.         // Compare check-in date
  3327.         $hubCheckIn = $hubData['checkIn'] ?? null;
  3328.         $localCheckIn = $orderLine->getStartDate()?->format('Y-m-d');
  3329.         $localData['checkIn'] = $localCheckIn;
  3330.         $hubDataFormatted['checkIn'] = $hubCheckIn;
  3331.         if ($hubCheckIn && $hubCheckIn !== $localCheckIn) {
  3332.             $differences['checkIn'] = [
  3333.                 'field' => 'Check-In Date',
  3334.                 'old' => $localCheckIn,
  3335.                 'new' => $hubCheckIn,
  3336.                 'changed' => true
  3337.             ];
  3338.         }
  3339.         // Compare check-out date
  3340.         $hubCheckOut = $hubData['checkOut'] ?? null;
  3341.         $localCheckOut = $orderLine->getEndDate()?->format('Y-m-d');
  3342.         $localData['checkOut'] = $localCheckOut;
  3343.         $hubDataFormatted['checkOut'] = $hubCheckOut;
  3344.         if ($hubCheckOut && $hubCheckOut !== $localCheckOut) {
  3345.             $differences['checkOut'] = [
  3346.                 'field' => 'Check-Out Date',
  3347.                 'old' => $localCheckOut,
  3348.                 'new' => $hubCheckOut,
  3349.                 'changed' => true
  3350.             ];
  3351.         }
  3352.         // Compare deadline
  3353.         $hubDeadline = $hubData['deadline'] ?? null;
  3354.         $localDeadline = $orderLine->getDeadline()?->format('Y-m-d');
  3355.         $localData['deadline'] = $localDeadline;
  3356.         $hubDataFormatted['deadline'] = $hubDeadline;
  3357.         if ($hubDeadline && $hubDeadline !== $localDeadline) {
  3358.             $differences['deadline'] = [
  3359.                 'field' => 'Cancellation Deadline',
  3360.                 'old' => $localDeadline,
  3361.                 'new' => $hubDeadline,
  3362.                 'changed' => true
  3363.             ];
  3364.         }
  3365.         // Compare total price
  3366.         $hubPrice = (float) ($hubData['purchase'] ?? 0);
  3367.         $hotelTotals = $this->getHotelSubLinesTotals($orderLine);
  3368.         $hotelPurchaseTotal = (float) $hotelTotals['purchase'];
  3369.         $existingPriceDiffSubLine = $this->findPriceDiffSubLine($orderLine);
  3370.         $existingPriceDiff = $existingPriceDiffSubLine ? (float) $existingPriceDiffSubLine->getPurchasePrice() : 0.0;
  3371.         $localGrandTotal = $hotelPurchaseTotal + $existingPriceDiff;
  3372.         if (abs($hubPrice - $localGrandTotal) > 0.01) {
  3373.             $differences['price'] = [
  3374.                 'field' => 'Total Price',
  3375.                 'old' => $localGrandTotal,
  3376.                 'new' => $hubPrice,
  3377.                 'changed' => true
  3378.             ];
  3379.         }
  3380.         // Compare currency
  3381.         $hubCurrency = $hubData['currency'] ?? '';
  3382.         $localCurrency = $orderLine->getOrderAtt()->getCurrency() ?? '';
  3383.         $localData['currency'] = $localCurrency;
  3384.         $hubDataFormatted['currency'] = $hubCurrency;
  3385.         if ($hubCurrency !== $localCurrency && !empty($hubCurrency)) {
  3386.             $differences['currency'] = [
  3387.                 'field' => 'Currency',
  3388.                 'old' => $localCurrency,
  3389.                 'new' => $hubCurrency,
  3390.                 'changed' => true
  3391.             ];
  3392.         }
  3393.         // Compare booking reference
  3394.         $hubSecondRef = $hubData['secondRef'] ?? '';
  3395.         $localSecondRef = $orderLine->getExternalOrderIdReference() ?? '';
  3396.         $localData['secondRef'] = $localSecondRef;
  3397.         $hubDataFormatted['secondRef'] = $hubSecondRef;
  3398.         if ($hubSecondRef !== $localSecondRef && !empty($hubSecondRef)) {
  3399.             $differences['secondRef'] = [
  3400.                 'field' => 'Booking Reference',
  3401.                 'old' => $localSecondRef,
  3402.                 'new' => $hubSecondRef,
  3403.                 'changed' => true
  3404.             ];
  3405.         }
  3406.         // Compare booking notes/comments
  3407.         $hubNotes = $hubData['bookingNotes'] ?? '';
  3408.         $localComment = $orderLine->getComment() ?? '';
  3409.         $localData['notes'] = $localComment;
  3410.         $hubDataFormatted['notes'] = $hubNotes;
  3411.         if ($hubNotes !== $localComment && !empty($hubNotes)) {
  3412.             $differences['notes'] = [
  3413.                 'field' => 'Booking Notes',
  3414.                 'old' => $localComment,
  3415.                 'new' => $hubNotes,
  3416.                 'changed' => true
  3417.             ];
  3418.         }
  3419.         // Compare traveller/beneficiary
  3420.         $hubTraveller = $hubData['traveller'] ?? '';
  3421.         $localBeneficiary = $orderLine->getBeneficiary() ?? '';
  3422.         $localData['beneficiary'] = $localBeneficiary;
  3423.         $hubDataFormatted['beneficiary'] = $hubTraveller;
  3424.         if (
  3425.             $hubTraveller !== $localBeneficiary && !empty($hubTraveller) &&
  3426.             !str_contains($hubTraveller, $localBeneficiary)
  3427.         ) {
  3428.             $differences['beneficiary'] = [
  3429.                 'field' => 'Traveller/Beneficiary',
  3430.                 'old' => $localBeneficiary,
  3431.                 'new' => $hubTraveller,
  3432.                 'changed' => true
  3433.             ];
  3434.         }
  3435.         // Compare rooms (OrderSubLines)
  3436.         $hubRooms = $hubData['rooms'] ?? [];
  3437.         $localSubLines = $orderLine->getOrderSubLines();
  3438.         $localRooms = [];
  3439.         $hubRoomsFormatted = [];
  3440.         // Extract local room information from OrderSubLines
  3441.         foreach ($localSubLines as $index => $subLine) {
  3442.             if ($subLine->getService() === 'hotel') {
  3443.                 if ($subLine->getState() === OrderStateEnum::CANCELED->getValue()) {
  3444.                     continue;
  3445.                 }
  3446.                 $description = $subLine->getDescription();
  3447.                 // Parse description format: "Chambre Standard [Demi pension (DP)]"
  3448.                 $roomType = '';
  3449.                 $boardType = '';
  3450.                 if (preg_match('/^(.+?)\s*\[(.+?)\]$/', $description, $matches)) {
  3451.                     $roomType = trim($matches[1]);
  3452.                     $boardType = trim($matches[2]);
  3453.                 } else {
  3454.                     $roomType = $description;
  3455.                 }
  3456.                 // Extract child ages from roomChildrenAge field
  3457.                 $childAges = [];
  3458.                 if ($subLine->getRoomChildrenAge()) {
  3459.                     $childAges = array_map('trim', explode(',', $subLine->getRoomChildrenAge()));
  3460.                 }
  3461.                 $localRooms[] = [
  3462.                     'room' => $roomType,
  3463.                     'board' => $boardType,
  3464.                     'adult' => $subLine->getNbAdult(),
  3465.                     'price' => $subLine->getPurchasePrice(),
  3466.                     'child' => [
  3467.                         'value' => $subLine->getNbChild() ?? 0,
  3468.                         'ages' => $childAges
  3469.                     ],
  3470.                 ];
  3471.             }
  3472.         }
  3473.         // Format hub rooms for comparison
  3474.         foreach ($hubRooms as $index => $hubRoom) {
  3475.             $childData = $hubRoom['child'] ?? ['value' => '0', 'age' => ''];
  3476.             $childCount = is_array($childData) ? ($childData['value'] ?? '0') : '0';
  3477.             $childAge = is_array($childData) ? ($childData['age'] ?? '') : '';
  3478.             // Parse child ages
  3479.             $childAges = [];
  3480.             if (!empty($childAge)) {
  3481.                 $childAges = array_map('trim', explode(',', $childAge));
  3482.             }
  3483.             $hubRoomsFormatted[] = [
  3484.                 'room' => $hubRoom['room'] ?? '',
  3485.                 'board' => $hubRoom['board'] ?? '',
  3486.                 'adult' => $hubRoom['adult'] ?? '0',
  3487.                 //'price' => $hubRoom['price'] ?? 0,
  3488.                 'child' => [
  3489.                     'value' => $childCount,
  3490.                     'ages' => $childAges
  3491.                 ]
  3492.             ];
  3493.         }
  3494.         $localData['rooms'] = $localRooms;
  3495.         $hubDataFormatted['rooms'] = $hubRoomsFormatted;
  3496.         // Compare rooms count first
  3497.         if (count($hubRooms) !== count($localRooms)) {
  3498.             $differences['rooms_count'] = [
  3499.                 'field' => 'Number of Rooms',
  3500.                 'old' => count($localRooms),
  3501.                 'new' => count($hubRooms),
  3502.                 'changed' => true
  3503.             ];
  3504.         }
  3505.         // Compare each room
  3506.         $roomDifferences = [];
  3507.         $maxRooms = max(count($hubRooms), count($localRooms));
  3508.         for ($i = 0; $i < $maxRooms; $i++) {
  3509.             $localRoom = $localRooms[$i] ?? null;
  3510.             $hubRoom = $hubRoomsFormatted[$i] ?? null;
  3511.             if ($hubRoom === null) {
  3512.                 // Room exists locally but not in hub
  3513.                 $roomDifferences[$i] = [
  3514.                     'field' => 'Room ' . ($i + 1),
  3515.                     'old' => $localRoom,
  3516.                     'new' => null,
  3517.                     'changed' => true,
  3518.                     'type' => 'removed'
  3519.                 ];
  3520.             } elseif ($localRoom === null) {
  3521.                 // Room exists in hub but not locally
  3522.                 $roomDifferences[$i] = [
  3523.                     'field' => 'Room ' . ($i + 1),
  3524.                     'old' => null,
  3525.                     'new' => $hubRoom,
  3526.                     'changed' => true,
  3527.                     'type' => 'added'
  3528.                 ];
  3529.             } else {
  3530.                 // Both exist, compare all properties
  3531.                 $changeDetails = [];
  3532.                 $roomChanged = false;
  3533.                 // Compare room type
  3534.                 $hubRoomType = $hubRoom['room'] ?? '';
  3535.                 $localRoomType = $localRoom['room'];
  3536.                 if ($hubRoomType !== $localRoomType && !empty($hubRoomType)) {
  3537.                     $roomChanged = true;
  3538.                     $changeDetails['room_type'] = [
  3539.                         'old' => $localRoomType,
  3540.                         'new' => $hubRoomType
  3541.                     ];
  3542.                 }
  3543.                 // Compare board type
  3544.                 $hubBoard = $hubRoom['board'] ?? '';
  3545.                 $localBoard = $localRoom['board'];
  3546.                 if ($hubBoard !== $localBoard && !empty($hubBoard)) {
  3547.                     $roomChanged = true;
  3548.                     $changeDetails['board'] = [
  3549.                         'old' => $localBoard,
  3550.                         'new' => $hubBoard
  3551.                     ];
  3552.                 }
  3553.                 // Compare number of adults
  3554.                 $hubAdult = (int) ($hubRoom['adult'] ?? 0);
  3555.                 $localAdult = (int) ($localRoom['adult'] ?? 0);
  3556.                 if ($hubAdult !== $localAdult && $hubAdult > 0) {
  3557.                     $roomChanged = true;
  3558.                     $changeDetails['adult'] = [
  3559.                         'old' => $localAdult,
  3560.                         'new' => $hubAdult
  3561.                     ];
  3562.                 }
  3563.                 // Compare number of children
  3564.                 $hubChildCount = (int) ($hubRoom['child']['value'] ?? 0);
  3565.                 $localChildCount = (int) ($localRoom['child']['value'] ?? 0);
  3566.                 if ($hubChildCount !== $localChildCount) {
  3567.                     $roomChanged = true;
  3568.                     $changeDetails['child_count'] = [
  3569.                         'old' => $localChildCount,
  3570.                         'new' => $hubChildCount
  3571.                     ];
  3572.                 }
  3573.                 // Compare child ages
  3574.                 $hubChildAges = $hubRoom['child']['ages'] ?? [];
  3575.                 $localChildAges = $localRoom['child']['ages'] ?? [];
  3576.                 if ($hubChildAges !== $localChildAges && !empty($hubChildAges)) {
  3577.                     $roomChanged = true;
  3578.                     $changeDetails['child_ages'] = [
  3579.                         'old' => implode(',', $localChildAges),
  3580.                         'new' => implode(',', $hubChildAges)
  3581.                     ];
  3582.                 }
  3583.                 if ($roomChanged) {
  3584.                     $roomDifferences[$i] = [
  3585.                         'field' => 'Room ' . ($i + 1),
  3586.                         'old' => $localRoom,
  3587.                         'new' => $hubRoom,
  3588.                         'changed' => true,
  3589.                         'type' => 'modified',
  3590.                         'details' => $changeDetails
  3591.                     ];
  3592.                 }
  3593.             }
  3594.         }
  3595.         if (!empty($roomDifferences)) {
  3596.             foreach ($roomDifferences as $i => $roomDiff) {
  3597.                 $differences['room_' . ($i + 1)] = $roomDiff;
  3598.             }
  3599.         }
  3600.         return [
  3601.             'hasChanges' => count($differences) > 0,
  3602.             'differences' => $differences,
  3603.             'localData' => $localData,
  3604.             'hubData' => $hubDataFormatted
  3605.         ];
  3606.     }
  3607.     private function applyHubChangesToLocal(OrderLine $orderLine, array $hubData, array $differences): array
  3608.     {
  3609.         try {
  3610.             // Update status
  3611.             if (isset($differences['status'])) {
  3612.                 $newStatus = $differences['status']['new'];
  3613.                 $orderLine->setState($newStatus);
  3614.             }
  3615.             // Update hotel name
  3616.             if (isset($differences['hotelName'])) {
  3617.                 $orderLine->setHotelName($differences['hotelName']['new']);
  3618.             }
  3619.             // Update check-in date
  3620.             if (isset($differences['checkIn'])) {
  3621.                 $newCheckIn = DateTime::createFromFormat('Y-m-d', $differences['checkIn']['new']);
  3622.                 if ($newCheckIn) {
  3623.                     $orderLine->setStartDate($newCheckIn);
  3624.                 }
  3625.             }
  3626.             // Update check-out date
  3627.             if (isset($differences['checkOut'])) {
  3628.                 $newCheckOut = DateTime::createFromFormat('Y-m-d', $differences['checkOut']['new']);
  3629.                 if ($newCheckOut) {
  3630.                     $orderLine->setEndDate($newCheckOut);
  3631.                 }
  3632.             }
  3633.             // Update deadline
  3634.             if (isset($differences['deadline'])) {
  3635.                 $newDeadline = DateTime::createFromFormat('Y-m-d', $differences['deadline']['new']);
  3636.                 if ($newDeadline) {
  3637.                     $orderLine->setDeadline($newDeadline);
  3638.                 }
  3639.             }
  3640.             // Update booking reference
  3641.             if (isset($differences['secondRef'])) {
  3642.                 $orderLine->setExternalOrderIdReference($differences['secondRef']['new']);
  3643.             }
  3644.             // Update booking notes
  3645.             if (isset($differences['notes'])) {
  3646.                 $orderLine->setComment($differences['notes']['new']);
  3647.             }
  3648.             // Update beneficiary
  3649.             if (isset($differences['beneficiary'])) {
  3650.                 $orderLine->setBeneficiary($differences['beneficiary']['new']);
  3651.             }
  3652.             // Update rooms (OrderSubLines)
  3653.             $roomDiffs = array_filter(
  3654.                 $differences,
  3655.                 fn($key) => str_starts_with($key, 'room_'),
  3656.                 ARRAY_FILTER_USE_KEY
  3657.             );
  3658.             if (!empty($roomDiffs)) {
  3659.                 $this->updateOrderSubLinesFromHub($orderLine, $hubData['rooms'] ?? [], $roomDiffs);
  3660.             }
  3661.             // Update price
  3662.             if (isset($differences['price'])) {
  3663.                 $newTotalPrice = $differences['price']['new'];
  3664.                 $this->updateOrderSubLinesPrices($orderLine, $newTotalPrice);
  3665.                 // $this->consolidatePriceDiffSublines($orderLine);
  3666.             }
  3667.             $this->entityManager->persist($orderLine);
  3668.             $this->entityManager->flush();
  3669.             return [
  3670.                 'success' => true,
  3671.                 'message' => 'Changes applied successfully.'
  3672.             ];
  3673.         } catch (Exception $e) {
  3674.             return [
  3675.                 'success' => false,
  3676.                 'message' => 'Failed to apply changes: ' . $e->getMessage()
  3677.             ];
  3678.         }
  3679.     }
  3680.     private function updateOrderSubLinesFromHub(OrderLine $orderLine, array $hubRooms, array $roomDifferences): void
  3681.     {
  3682.         $localSubLines = $orderLine->getOrderSubLines()->toArray();
  3683.         $localSubLines = array_values($localSubLines);
  3684.         foreach ($roomDifferences as $key => $roomDiff) {
  3685.             if (preg_match('/^room_(\d+)$/', $key, $matches)) {
  3686.                 $index = (int) $matches[1] - 1;
  3687.             } else {
  3688.                 continue;
  3689.             }
  3690.             if (!isset($roomDiff['changed']) || !$roomDiff['changed']) {
  3691.                 continue;
  3692.             }
  3693.             $changeType = $roomDiff['type'] ?? 'modified';
  3694.             switch ($changeType) {
  3695.                 case 'removed':
  3696.                     if (isset($localSubLines[$index])) {
  3697.                         $subLine = $localSubLines[$index];
  3698.                         $subLine->setState(OrderStateEnum::CANCELED->getValue());
  3699.                         $this->entityManager->persist($subLine);
  3700.                     }
  3701.                     break;
  3702.                 case 'added':
  3703.                     $hubRoom = $hubRooms[$index] ?? null;
  3704.                     if ($hubRoom) {
  3705.                         $newSubLine = $this->createOrderSubLineFromHubRoom($orderLine, $hubRoom);
  3706.                         $orderLine->addOrderSubLine($newSubLine);
  3707.                         $this->entityManager->persist($newSubLine);
  3708.                     }
  3709.                     break;
  3710.                 case 'modified':
  3711.                     if (isset($localSubLines[$index])) {
  3712.                         $subLine = $localSubLines[$index];
  3713.                         $hubRoom = $hubRooms[$index] ?? null;
  3714.                         if ($hubRoom) {
  3715.                             $this->updateOrderSubLineFromHubRoom($subLine, $hubRoom, $roomDiff['details'] ?? []);
  3716.                         }
  3717.                         $this->entityManager->persist($subLine);
  3718.                     }
  3719.                     break;
  3720.             }
  3721.         }
  3722.     }
  3723.     private function createOrderSubLineFromHubRoom(OrderLine $orderLine, array $hubRoom): OrderSubLine
  3724.     {
  3725.         $subLine = new OrderSubLine();
  3726.         // Set description in format: "Room [Board]"
  3727.         $roomType = $hubRoom['room'] ?? '';
  3728.         $boardType = $hubRoom['board'] ?? '';
  3729.         $description = $roomType . ' [' . $boardType . ']';
  3730.         $subLine->setDescription($description);
  3731.         // occupancy
  3732.         $subLine->setNbAdult((int) ($hubRoom['adult'] ?? 2));
  3733.         $childData = $hubRoom['child'] ?? ['value' => '0', 'ages' => []];
  3734.         $childCount = is_array($childData) ? (int) ($childData['value'] ?? 0) : (int) $childData;
  3735.         $subLine->setNbChild($childCount);
  3736.         if (is_array($childData) && !empty($childData['ages'])) {
  3737.             $subLine->setRoomChildrenAge(implode(',', $childData['ages']));
  3738.         }
  3739.         $subLines = $orderLine->getOrderSubLines();
  3740.         $firstSubLine = $subLines[0];
  3741.         $subLine->setState(OrderStateEnum::AVAILABLE->getValue());
  3742.         $subLine->setService('hotel');
  3743.         $subLine->setCancelable(true);
  3744.         $subLine->setRatekey($firstSubLine->getRatekey());
  3745.         $subLine->setAgencyCommission($firstSubLine->getAgencyCommission());
  3746.         $subLine->setPartnerCommission($firstSubLine->getPartnerCommission());
  3747.         $orderLine->addOrderSubLine($subLine);
  3748.         return $subLine;
  3749.     }
  3750.     private function updateOrderSubLineFromHubRoom(OrderSubLine $subLine, array $hubRoom, array $changeDetails): void
  3751.     {
  3752.         // Update room type and board
  3753.         if (isset($changeDetails['room_type']) || isset($changeDetails['board'])) {
  3754.             $roomType = $hubRoom['room'] ?? '';
  3755.             $boardType = $hubRoom['board'] ?? '';
  3756.             $description = $roomType . ' [' . $boardType . ']';
  3757.             $subLine->setDescription($description);
  3758.         }
  3759.         // Update number of adults
  3760.         if (isset($changeDetails['adult'])) {
  3761.             $subLine->setNbAdult((int) ($hubRoom['adult'] ?? 2));
  3762.         }
  3763.         // Update number of children
  3764.         if (isset($changeDetails['child_count'])) {
  3765.             $childData = $hubRoom['child'] ?? ['value' => '0'];
  3766.             $childCount = is_array($childData) ? (int) ($childData['value'] ?? 0) : (int) $childData;
  3767.             $subLine->setNbChild($childCount);
  3768.         }
  3769.         // Update child ages
  3770.         if (isset($changeDetails['child_ages'])) {
  3771.             $childData = $hubRoom['child'] ?? [];
  3772.             if (is_array($childData) && isset($childData['ages']) && !empty($childData['ages'])) {
  3773.                 $subLine->setRoomChildrenAge(implode(',', $childData['ages']));
  3774.             } else {
  3775.                 $subLine->setRoomChildrenAge($hubRoom['child']['age']);
  3776.             }
  3777.         }
  3778.         $this->entityManager->persist($subLine);
  3779.     }
  3780.     private function updateOrderSubLinesPrices(OrderLine $orderLine, float $newTotalPurchasePrice): void
  3781.     {
  3782.         $epsilon = 0.01;
  3783.         $this->consolidatePriceDiffSublines($orderLine);
  3784.         $hotelTotals = $this->getHotelSubLinesTotals($orderLine);
  3785.         $hotelPurchaseTotal = (float) $hotelTotals['purchase'];
  3786.         $hotelSaleTotal = (float) $hotelTotals['sale'];
  3787.         $desiredDiff = round($newTotalPurchasePrice - $hotelPurchaseTotal, 2);
  3788.         $existingPriceDiff = $this->findPriceDiffSubLine($orderLine);
  3789.         $marginRatio = 1.0;
  3790.         if ($hotelPurchaseTotal > 0) {
  3791.             $marginRatio = $hotelSaleTotal / $hotelPurchaseTotal;
  3792.         } else {
  3793.             $orderPurchase = (float) ($orderLine->getPurchasePrice() ?? 0);
  3794.             $orderSale = (float) ($orderLine->getSalePrice() ?? 0);
  3795.             $marginRatio = $orderPurchase > 0 ? ($orderSale / $orderPurchase) : 1.0;
  3796.         }
  3797.         $saleForDiff = round($desiredDiff * $marginRatio, 2);
  3798.         if (abs($desiredDiff) < $epsilon) {
  3799.             if ($existingPriceDiff) {
  3800.                 if (method_exists($orderLine, 'removeOrderSubLine')) {
  3801.                     $orderLine->removeOrderSubLine($existingPriceDiff);
  3802.                 } else {
  3803.                     $existingPriceDiff->setOrderLine(null);
  3804.                 }
  3805.                 $this->entityManager->remove($existingPriceDiff);
  3806.                 $this->entityManager->persist($orderLine);
  3807.             }
  3808.             return;
  3809.         }
  3810.         if ($existingPriceDiff) {
  3811.             $existingPriceDiff->setPurchasePrice($desiredDiff);
  3812.             $existingPriceDiff->setSalePrice($saleForDiff);
  3813.             $existingPriceDiff->setDescription('Price Diff');
  3814.             $existingPriceDiff->setCancelable(true);
  3815.             $this->entityManager->persist($existingPriceDiff);
  3816.         } else {
  3817.             $priceDiff = new OrderSubLine();
  3818.             $priceDiff->setService('Price Diff');
  3819.             $priceDiff->setPurchasePrice($desiredDiff);
  3820.             $priceDiff->setSalePrice($saleForDiff);
  3821.             $priceDiff->setDescription('Price Diff');
  3822.             $priceDiff->setCancelable(true);
  3823.             $priceDiff->setState(OrderStateEnum::AVAILABLE->getValue());
  3824.             $priceDiff->setOrderLine($orderLine);
  3825.             // copy fields from a hotel subline for commissions / ratekey
  3826.             $subLines = $orderLine->getOrderSubLines();
  3827.             if ($subLines->count() > 0) {
  3828.                 $first = null;
  3829.                 foreach ($subLines as $sl) {
  3830.                     if ($sl->getService() === 'hotel') {
  3831.                         $first = $sl;
  3832.                         break;
  3833.                     }
  3834.                 }
  3835.                 if (!$first) {
  3836.                     $first = $subLines[0];
  3837.                 }
  3838.                 if ($first) {
  3839.                     $priceDiff->setRatekey($first->getRatekey());
  3840.                     $priceDiff->setAgencyCommission($first->getAgencyCommission());
  3841.                     $priceDiff->setPartnerCommission($first->getPartnerCommission());
  3842.                 }
  3843.             }
  3844.             $orderLine->addOrderSubLine($priceDiff);
  3845.             $this->entityManager->persist($priceDiff);
  3846.         }
  3847.         // Re-consolidate and persist owning side
  3848.         $this->consolidatePriceDiffSublines($orderLine);
  3849.         $this->entityManager->persist($orderLine);
  3850.     }
  3851.     private function mapHubStatusToLocal(string $hubStatus): string
  3852.     {
  3853.         return match (strtoupper($hubStatus)) {
  3854.             'CONFIRMED' => OrderStateEnum::CONFIRMED->getValue(),
  3855.             'CANCELLED', 'CANCELED' => OrderStateEnum::CANCELED->getValue(),
  3856.             'TEMP_AVAILABLE' => OrderStateEnum::TEMP_AVAILABLE->getValue(),
  3857.             default => OrderStateEnum::PENDING->getValue(),
  3858.         };
  3859.     }
  3860.     private function getHotelSubLinesTotals(OrderLine $orderLine): array
  3861.     {
  3862.         $purchase = 0.0;
  3863.         $sale = 0.0;
  3864.         foreach ($orderLine->getOrderSubLines() as $sl) {
  3865.             if ($sl->getService() === 'hotel' && $sl->getState() !== OrderStateEnum::CANCELED->getValue()) {
  3866.                 $purchase += (float) ($sl->getPurchasePrice() ?? 0);
  3867.                 $sale += (float) ($sl->getSalePrice() ?? 0);
  3868.             }
  3869.         }
  3870.         return ['purchase' => $purchase, 'sale' => $sale];
  3871.     }
  3872.     private function findPriceDiffSubLine(OrderLine $orderLine): ?OrderSubLine
  3873.     {
  3874.         foreach ($orderLine->getOrderSubLines() as $sl) {
  3875.             $svc = (string) $sl->getService();
  3876.             if (strtolower(trim($svc)) === 'price diff') {
  3877.                 return $sl;
  3878.             }
  3879.         }
  3880.         return null;
  3881.     }
  3882.     private function consolidatePriceDiffSublines(OrderLine $orderLine): ?OrderSubLine
  3883.     {
  3884.         $priceDiffs = [];
  3885.         foreach ($orderLine->getOrderSubLines() as $sl) {
  3886.             if (strtolower(trim((string) $sl->getService())) === 'price diff') {
  3887.                 $priceDiffs[] = $sl;
  3888.             }
  3889.         }
  3890.         if (empty($priceDiffs)) {
  3891.             return null;
  3892.         }
  3893.         $first = array_shift($priceDiffs);
  3894.         $totalPurchase = (float) ($first->getPurchasePrice() ?? 0);
  3895.         $totalSale = (float) ($first->getSalePrice() ?? 0);
  3896.         foreach ($priceDiffs as $extra) {
  3897.             $totalPurchase += (float) ($extra->getPurchasePrice() ?? 0);
  3898.             $totalSale += (float) ($extra->getSalePrice() ?? 0);
  3899.             if (method_exists($orderLine, 'removeOrderSubLine')) {
  3900.                 $orderLine->removeOrderSubLine($extra);
  3901.             } else {
  3902.                 $extra->setOrderLine(null);
  3903.             }
  3904.             $this->entityManager->remove($extra);
  3905.         }
  3906.         $first->setPurchasePrice($totalPurchase);
  3907.         $first->setSalePrice($totalSale);
  3908.         $this->entityManager->persist($orderLine);
  3909.         $this->entityManager->persist($first);
  3910.         return $first;
  3911.     }
  3912.     public function upsertHotelCache(string $cityKey, array $newHotels): void
  3913.     {
  3914.         $normalizeKey = static fn(string $name): string => mb_strtoupper(str_replace(' ', '', $name), 'UTF-8');
  3915.         $normalizedCityKey = $normalizeKey($cityKey);
  3916.         $cache = $this->hotelCacheRepository->findOneBy(['cityKey' => $normalizedCityKey]);
  3917.         if (!$cache) {
  3918.             $cache = new \App\Entity\HotelCache();
  3919.             $cache->setCityKey($normalizedCityKey);
  3920.             $cache->setCreatedAt(new \DateTimeImmutable());
  3921.             $cache->setLastFetchedAt(new \DateTimeImmutable());
  3922.             $existing = [];
  3923.         } else {
  3924.             $existing = $cache->getHotelsJson();
  3925.         }
  3926.         // indexation par ID pour éviter doublons
  3927.         $indexed = [];
  3928.         foreach ($existing as $keyHotel => $existHotel) {
  3929.             $indexed[$normalizeKey($keyHotel)] = $existHotel;
  3930.         }
  3931.         foreach ($newHotels as $keyNewHotel => $hotel) {
  3932.             $indexed[$normalizeKey($keyNewHotel)] = $hotel; // overwrite si existe
  3933.         }
  3934.         $cache->setHotelsJson($indexed);
  3935.         $cache->setLastFetchedAt(new \DateTimeImmutable());
  3936.         $this->entityManager->persist($cache);
  3937.         $this->entityManager->flush();
  3938.     }
  3939.     public function getPagesHub(string $cityId): array
  3940.     {
  3941.         $parameters = [
  3942.             'cityId' => $cityId
  3943.         ];
  3944.         try {
  3945.             $apiResponse = $this->helpers->getApiResponse(
  3946.                 'POST',
  3947.                 $this->parameterService->getHotelHubUrl(),
  3948.                 HUB_HOTEL_API_PAGES_ENDPOINT,
  3949.                 $this->parameterService->getHotelHubApiKey(),
  3950.                 $parameters
  3951.             );
  3952.             if (key_exists('response', $apiResponse)) {
  3953.                 return $apiResponse['response'];
  3954.             }
  3955.         } catch (Exception $e) {
  3956.             return [$e->getMessage()];
  3957.         }
  3958.         return [];
  3959.     }
  3960.     private function normalizeHotelNameKey(string $name): string
  3961.     {
  3962.         // Translittère les accents (é -> e, ü -> u, etc.)
  3963.         $transliterated = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) ?: $name;
  3964.         // Ne garde que les caractères alphanumériques, en majuscules
  3965.         return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $transliterated));
  3966.     }
  3967. }