<?php
namespace App\Service;
use App\Config\CommissionTypeEnum;
use App\Config\HotelBadgeEnum;
use App\Config\ModuleEnum;
use App\Config\OrderLineTypeEnum;
use App\Config\OrderStateEnum;
use App\Config\ServiceEnum;
use App\Entity\ApiLog;
use App\Entity\Beneficiary;
use App\Entity\Customer;
use App\Entity\Hotel;
use App\Entity\HotelAvailabilityRequest;
use App\Entity\HotelContract;
use App\Entity\Operation;
use App\Entity\Order;
use App\Entity\OrderLine;
use App\Entity\OrderSubLine;
use App\Entity\OtherProvider;
use App\Entity\Pricing;
use App\Entity\ProductElementStockOperation;
use App\Entity\XmlApi;
use App\Repository\AgencyRepository;
use App\Repository\ApiLogRepository;
use App\Repository\ArrangementRepository;
use App\Repository\CancellationPolicyRepository;
use App\Repository\CityRepository;
use App\Repository\CustomerHotelXmlRepository;
use App\Repository\CustomerXmlApiRepository;
use App\Repository\HotelContractRepository;
use App\Repository\HotelRepository;
use App\Repository\HotelRoomTypeRepository;
use App\Repository\HotelXmlRepository;
use App\Repository\OrderLineRepository;
use App\Repository\OrderRepository;
use App\Repository\OrderSubLineRepository;
use App\Repository\PartyRepository;
use App\Repository\PartyZoneRepository;
use App\Repository\PricingRepository;
use App\Repository\PromoRepository;
use App\Repository\StopsaleHotelRepository;
use App\Repository\XmlApiRepository;
use DateInterval;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Repository\HotelCacheRepository;
use Psr\Log\LoggerInterface;
const TOKEN_FOR_BOOK_SEPARATOR = '#TFBEND#';
const HUB_HOTEL_API_ACTIVATED_GDS_ENDPOINT = 'hotels-content?method=getActivatedGDS';
const HUB_HOTEL_API_AUTOCOMPLETE_ENDPOINT = 'hotels-api?method=autocomplete';
const HUB_HOTEL_API_DETAILS_ENDPOINT = 'hotels-api?method=hotelDetails';
const HUB_HOTEL_API_CHECKRATE_ENDPOINT = 'hotels-api?method=checkRate';
const HUB_HOTEL_API_AVAILABILITY_ENDPOINT = 'hotels-api?method=availability';
const HUB_HOTEL_API_GET_BOOKINGS_ENDPOINT = 'hotels-api?method=getBookings';
const HUB_HOTEL_API_BOOK_ENDPOINT = 'hotels-api?method=book';
const HUB_HOTEL_API_CANCEL_ENDPOINT = 'hotels-api?method=cancel';
const HUB_HOTEL_API_CHECKSTATUS_ENDPOINT = 'hotels-api?method=bookingStatus';
const HUB_HOTEL_API_CREDIT_CHECK_ENDPOINT = 'hotels-api?method=creditCheck';
const HUB_HOTEL_API_BOOKING_LIST_ENDPOINT = 'hotels-api?method=getBookings';
const HUB_HOTEL_API_PAGES_ENDPOINT = 'hotels-api?method=getPages';
const CANCEL_ERROR_ORDERLINE_ALREADY_CANCELED = 'CANCELLATION_FAILED_ORDERLINE_ALREADY_CANCELED';
const CANCEL_ERROR_PRODUCT_ERROR = 'CANCELLATION_FAILED_PRODUCT_ERROR';
const CANCEL_ERROR_RELEASE_DATE_EXCEEDED = 'CANCELLATION_FAILED_CHECKIN_DATE_EXCEEDED';
const CANCEL_ERROR_NOT_CANCELLABLE = 'CANCELLATION_ERROR_NOT_CANCELLABLE';
const CANCEL_SUCCESS = 'CANCELLATION_SUCCESS';
class HotelApiService
{
public function __construct(
private readonly CurrencyService $currencyService,
private readonly XmlApiRepository $xmlApiRepository,
private readonly PromoRepository $promoRepository,
private readonly CityRepository $cityRepository,
private readonly HotelRepository $hotelRepository,
private readonly PricingRepository $pricingRepository,
private readonly PartyRepository $partyRepository,
private readonly StopsaleHotelRepository $stopsaleRepository,
private readonly CancellationPolicyRepository $cancellationPolicyRepository,
private readonly OrderLineRepository $orderLineRepository,
private readonly HotelContractRepository $hotelContractRepository,
private readonly HotelRoomTypeRepository $hotelRoomTypeRepository,
private readonly PartyZoneRepository $partyZoneRepository,
private readonly AgencyRepository $agencyRepository,
private readonly ApiLogRepository $apiLogRepository,
private readonly Helpers $helpers,
private readonly ArrangementRepository $arrangementRepository,
private readonly TranslatorInterface $translator,
private readonly OrderSubLineRepository $orderSublineRepository,
private readonly CustomerHotelXmlRepository $customerHotelXmlRepository,
private readonly HotelXmlRepository $hotelXmlRepository,
private readonly CustomerXmlApiRepository $customerXmlApiRepository,
private readonly EntityManagerInterface $entityManager,
private readonly XmlApiService $xmlApiService,
private readonly WalletService $walletService,
private readonly ParameterService $parameterService,
private readonly RequestStack $requestStack,
private readonly FrontService $frontService,
private readonly StockOperationService $stockOperationService,
private readonly OrderRepository $orderRepository,
private readonly HttpClientInterface $httpClient,
private readonly HotelCacheRepository $hotelCacheRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* This is the main function to obtain the available hotels given an hotelAvailabilityRequest
* The process include four steps : (1) preliminary, (2) hrta prices, (3) extras, (4) format output
*
* @param HotelAvailabilityRequest $hotelReq
*
* @return array
*/
public function hotelsAvailability(HotelAvailabilityRequest $hotelReq): array
{
// step 1 : preliminary
$availableHotels = [];
$all_pricings = [];
foreach (array_keys($hotelReq->getRoomsOccupancies()) as $req_room_index) {
// Get pricings for the current req_room_index, hc_array_ids used to avoid searching in NA hotelContracts
$pricings = $this->pricingRepository->findAvailable($hotelReq, $req_room_index);
$all_pricings[] = $pricings;
$this->preliminaryHotelsAvailability($pricings, $hotelReq, $req_room_index, $availableHotels);
}
// step 2 : prices
$availableHotels = $this->availableHotelsWithPrices($availableHotels, $hotelReq, $all_pricings);
// step 3 : parties
$AVHTLC_ids = $this->getHotelContractsIDsFromAVHTL($availableHotels);
//dd($hotelReq->getPartiesZones());
$parties = $this->partyRepository->findAvailable($hotelReq, $req_room_index, $AVHTLC_ids);
//dd($parties);
$this->addPartiesToAvailableHotels($availableHotels, $parties, $hotelReq);
// step 4 : format array
$this->formatHotelAvailabilityArray($availableHotels);
return $availableHotels;
}
public function hotelsAvailabilityHUB(HotelAvailabilityRequest $hotelReq, $isFirstOccurrenceRoom=false): array
{
$headers = [
'api-key' => $this->parameterService->getHotelHubApiKey(),
];
if ($hotelReq->getXmlSourceId()) {
$headers['sourceId'] = $hotelReq->getXmlSourceId();
}
$apiResponse = $this->helpers->getApiResponseFromHub(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_AVAILABILITY_ENDPOINT,
headers: $headers,
request_body: $hotelReq->getHubAvailabilityRequest()
);
try {
$apiResults = $apiResponse['response'];
$localSearchCode = $hotelReq->getSearchCode();
$availableHotels = $this->normalizeSearchResultsFromHubFormat($apiResults, $hotelReq->getCurrency(), $localSearchCode,null ,$hotelReq->mapping,$isFirstOccurrenceRoom);
$this->formatHotelAvailabilityArray($availableHotels);
// for the specified customer, apply the adequate margin, or hide unauthorized hotel xml
$this->applyMarginsAndHideExcludedHotelXml($availableHotels, $hotelReq->getCustomer());
return [
'availableHotels' => $availableHotels,
'pagesCount' => $apiResponse['pages'],
];
} catch (Exception) {
return [
'availableHotels' => [],
'pagesCount' => 1
];
}
}
/**
* @param array $pricings
* @param HotelAvailabilityRequest $hotelAvailabilityReq
* @param int $index
* @param array $availableHotels
* @return void
*/
public function preliminaryHotelsAvailability(
array $pricings,
HotelAvailabilityRequest $hotelAvailabilityReq,
int $index,
array &$availableHotels
): void {
$nb_days = [];
foreach ($pricings as $key => $pricing) {
$season = $pricing->getSeason();
$hotelContract = $season->getHotelContract();
$hotelContract_key = $hotelContract->getId();
$hotel = $hotelContract->getHotel();
$hotel_key = '0@' . $hotel->getId();
// hrta data
$hrta = $pricing->getHotelRoomTypeArrangement();
$hrta_array = [
'id' => $hrta->getId(),
'rateKey' => $this->getHrtaRateKey($hotelAvailabilityReq, $index, 0, $hotelContract_key, $hrta->getId()),
'boardId' => $hrta->getArrangement()->getId(),
'boardCode' => $hrta->getArrangement()->getCode(),
'boardName' => $hrta->getArrangement()->getName(),
'boardXmlName' => $hrta->getArrangement()->getName(),
'roomId' => $hrta->getHotelRoomType()->getId(),
'roomName' => $hrta->getHotelRoomType()->getRoomType()->getName(),
'purchaseCurrency' => $hotelContract->getProvider()->getCurrency()
];
// Days overlap calculation
$nb_days_pricing = $this->helpers->nbDaysOverlap(
$hotelAvailabilityReq->getCheckInDate(),
$hotelAvailabilityReq->getLastNightDate(),
$season->getStartDate(),
$season->getEndDate(),
);
if (!array_key_exists($hotelContract->getId() . '-' . $hrta->getId(), $nb_days)) {
$nb_days[$hotelContract->getId() . '-' . $hrta->getId()] = $nb_days_pricing;
} else {
$nb_days[$hotelContract->getId() . '-' . $hrta->getId()] += $nb_days_pricing;
}
// array of the Ids of pricings related to a Hrta and the nbDaysCovered by this pricing.
$hrta_array['pricings_data'][$key] = [
'id' => $pricing->getId(),
'nbDays' => $nb_days_pricing
];
// Nb Days covered by all pricings relatated to the HRTA
$hrta_array['nb_stay_overlap'] = $nb_days[$hotelContract->getId() . '-' . $hrta->getId()];
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['rooms'][$index][$hrta_array['id']] = $hrta_array;
// hotel data
if (!array_key_exists('hotel', $availableHotels[$hotel_key])) {
$availableHotels[$hotel_key]['hotel']['id'] = $hotel_key; //'0-' . $hotel->getId();
$availableHotels[$hotel_key]['hotel']['name'] = $hotel->getName();
$availableHotels[$hotel_key]['hotel']['rating'] = $hotel->getStarRating();
$availableHotels[$hotel_key]['hotel']['city'] = $hotel->getCity()->getName();
$availableHotels[$hotel_key]['hotel']['cityId'] = $hotel->getCity()->getId();
$availableHotels[$hotel_key]['hotel']['zone'] = $hotel->getAddress() ?? $hotel->getCity()->getName();
$availableHotels[$hotel_key]['hotel']['country'] = $hotel->getCity()->getCountry()->getName();
$availableHotels[$hotel_key]['hotel']['countryId'] = $hotel->getCity()->getCountry()->getId();
$availableHotels[$hotel_key]['hotel']['latitude'] = $hotel->getLatitude();
$availableHotels[$hotel_key]['hotel']['longitude'] = $hotel->getLongitude();
$availableHotels[$hotel_key]['hotel']['overview'] = $hotel->getDescription();
$availableHotels[$hotel_key]['hotel']['tripadvisorId'] = $hotel->getTripadvisorId();
$availableHotels[$hotel_key]['hotel']['image'] = "";
$availableHotels[$hotel_key]['hotel']['gallery'] = [];
foreach ($hotel->getImages() as $image) {
if ($image->getDisplayOrder() == 1) {
array_unshift($availableHotels[$hotel_key]['hotel']['gallery'], $image->getUrl()); // Insert at the beginning
$availableHotels[$hotel_key]['hotel']['image'] = $image->getUrl();
} else {
$availableHotels[$hotel_key]['hotel']['gallery'][] = $image->getUrl(); // Append to the end
}
}
}
// hotel contract data
if (!array_key_exists('sourceId', $availableHotels[$hotel_key]['sources'][$hotelContract_key])) {
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['xmlSourceId'] = 0; // local
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['xmlSourceName'] = "Direct";
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['sourceId'] = $hotelContract_key;
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['sourceType'] = 'local';
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['searchCode'] = $hotelAvailabilityReq->getSearchCode();
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['associationRequired'] = false;
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['options'] = [];
foreach ($hotel->getOptions() as $option) {
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['options'][] = $option->getName();
}
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['facilities'] = [];
foreach ($hotel->getFacilities() as $ItemFacilities) {
$item = [
'name' => $ItemFacilities->getName(),
'description' => $ItemFacilities->getDescription(),
'icon' => $ItemFacilities->getIcon()
];
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['facilities'][] = $item;
}
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['conditions'] = [];
foreach ($hotel->getConditions() as $condition) {
$availableHotels[$hotel_key]['sources'][$hotelContract_key]['conditions'][] = $condition;
}
}
}
}
public function availableHotelsWithPrices(array $preAvailableHotels, HotelAvailabilityRequest $hotelReq, $pricings): array
{
$today = new DateTime();
$today->setTime(0, 0, 0);
$dateIntervalToCheckIn = $today->diff($hotelReq->getCheckInDate());
foreach ($preAvailableHotels as $hotel_key => $preAvailableHotel) { // hotels
foreach ($preAvailableHotel['sources'] as $contract_key => $source) { // sources (contracts)
// If all requested rooms are not found, delete the contract from response
if (count($source['rooms']) < count($hotelReq->getRoomsOccupancies())) {
unset($preAvailableHotels[$hotel_key]['sources'][$contract_key]);
continue;
}
// General AvailableQuantity
$generalHotelContractDailyStock = $this->stockOperationService->getDailyStock($hotelReq->getCheckInDate(), $hotelReq->getLastNightDate(), $source['sourceId'], null, $hotelReq->getCustomer());
$generalHotelContractStock = min($generalHotelContractDailyStock);
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['generalStock'] = $generalHotelContractStock;
// contract promos
$contract_promos_all = $this->promoRepository->availablePromos($hotelReq->getCheckInDate(), $hotelReq->getLastNightDate(), $contract_key);
// GET only active promos (remove expired early booking)
$contract_promos = [];
foreach ($contract_promos_all as $contract_promo) {
if ($contract_promo->getEbEndDate() != null) { // EB PERIOD
if ($today->diff($contract_promo->getEbEndDate())->invert == 1) {
continue;
}
} elseif ($contract_promo->getEbDaysBefore() > 0) { // EB J-
if ($dateIntervalToCheckIn->invert == 1 || $dateIntervalToCheckIn->days < $contract_promo->getEbDaysBefore()) {
continue;
}
}
$contract_promos[] = $contract_promo;
}
foreach ($source['rooms'] as $request_room_key => $request_room) {
$hrtAvailableQuantities = []; // array to save the available quantity for each HotelRoomType
foreach ($request_room as $hrta_key => $hrta) { // check that all stay-period is covered
if ($hrta['nb_stay_overlap'] < $hotelReq->getNbStay()) {
unset($preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]);
continue; // NEXT HRTA
}
// ********************** HERE, NB_STAY is OK for this HRTA **********************
$hrt_id = $hrta['roomId'];
// *** PRICES CALCULATION from pricing entities for the current HRTA ***
$hrta_original_purchase_price = 0;
$hrta_original_sale_price = 0;
$hrta_promo_purchase_price = 0;
$hrta_promo_sale_price = 0;
$hrta_is_NRF = false;
foreach ($hrta['pricings_data'] as $key => $pricing_data) {
$pricing = $pricings[$request_room_key][$key]; // Pricing Entity
$pricing_array = $this->getPriceData($pricing, $hotelReq, $request_room_key, $pricing_data, $contract_promos);
$hrta_original_purchase_price += $pricing_array['prices']['weighted_purchase'];
$hrta_promo_purchase_price += $pricing_array['prices']['promo_purchase'];
$hrta_original_sale_price += $pricing_array['prices']['weighted_sale'];
$hrta_promo_sale_price += $pricing_array['prices']['promo_sale'];
if ($pricing->isNRF()) {
$hrta_is_NRF = true;
}
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['pricings_data'][$key] = $pricing_array;
}
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['purchaseRate'] = $hrta_original_purchase_price;
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['purchasePromoRate'] = $hrta_promo_purchase_price;
// Exchange Rate purchaseToSaleCurrency
$exchangeRate = $this->currencyService->calculateExchangeRate(
$pricing->getSeason()->getHotelContract()->getProvider()->getCurrency(), // purchase currency
$hotelReq->getCurrency()
);
// SALE RATE
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['saleRate'] = $hrta_original_sale_price * $exchangeRate;
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['salePromoRate'] = $hrta_promo_sale_price * $exchangeRate;
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['saleCurrency'] = $hotelReq->getCurrency();
// CHECK STOP SALE for the HRTA
$countStopSales = $this->stopsaleRepository->countStopSale($hotelReq->getCheckInDate(), $hotelReq->getCheckOutDate(), $hrta_key);
$isStopSale = $countStopSales > 0;
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['stopSale'] = $isStopSale;
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['notes'] = '';
// Specific AvailableQuantity
if (array_key_exists($hrt_id, $hrtAvailableQuantities)) { // already calculated (previous arrangement)
$availableQuantity = $hrtAvailableQuantities[$hrta['roomId']];
} else { // calculate the quantity of this RoomType for the first time
$hrtaDailyStock = $this->stockOperationService->getDailyStock($hotelReq->getCheckInDate(), $hotelReq->getLastNightDate(), $source['sourceId'], $hrta['roomId'], $hotelReq->getCustomer());
$availableQuantity = min($hrtaDailyStock); // Minimum NbItems in STOCK for this HRT along the Stay Period
$hrtAvailableQuantities[$hrt_id] = $availableQuantity; // save it for next hrtas
}
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['availableQuantity'] = $availableQuantity;
$hrta_is_available = ($availableQuantity + $generalHotelContractStock) > 0;
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['available'] = $hrta_is_available;
// *** END AvailableQuantity for this Hotel Room Type ***
// Refund and cancellation fees
//if($hrta_is_NRF){
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['NRF'] = $hrta_is_NRF;
//}
if (!$hrta_is_NRF) { // HRTA is Refundable, render the cancellations dates and fees
$hrt_id = $preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['roomId'];
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['cancellationsFees']
= $this->getCancellationsFeesArray($contract_key, $hrt_id, $hotelReq, $hrta_promo_sale_price);
$earliestCancellation = end($preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['cancellationsFees']);
if ($earliestCancellation) {
$deadline = DateTime::createFromFormat("Y-m-d", $earliestCancellation['from']);
$deadline->setTime(0, 0);
date_sub($deadline, date_interval_create_from_date_string("1 day"));
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['deadline']
= $deadline->format('Y-m-d');
} else {
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['deadline'] = null;
}
}
} // END HRTA loop
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['promos'] = $this->getHotelBadges(HotelBadgeEnum::PROMO->getValue(), $hotel_key, true);
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['infos'] = $this->getHotelBadges(HotelBadgeEnum::INFO->getValue(), $hotel_key, true);
$preAvailableHotels[$hotel_key]['sources'][$contract_key]['discounts'] = $this->getHotelBadges(HotelBadgeEnum::DISCOUNT->getValue(), $hotel_key, true);
// IF THE RoomIndex hasn't relevant results --> UNSET the HotelContract
if (count($preAvailableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key]) == 0) {
unset($preAvailableHotels[$hotel_key]['sources'][$contract_key]);
break;
}
}
}
if (count($preAvailableHotels[$hotel_key]['sources']) == 0) {
unset($preAvailableHotels[$hotel_key]);
}
}
return $preAvailableHotels;
}
/**
* @param Pricing $pricing
* @param HotelAvailabilityRequest $hotelAvailabilityReq
*
* @return array
*/
private function getPriceData(
Pricing $pricing,
HotelAvailabilityRequest $hotelAvailabilityReq,
int $req_index,
$pricing_data,
$promos
): array {
$extras = [];
$discounts = [];
$raw_purchase_price = $pricing->getPurchasePrice();
$raw_sale_price = $pricing->getSalePrice();
// prices without promo.
$season = $pricing->getSeason();
$base_arrangement = $season->getBaseArrangement();
$hrt = $pricing->getHotelRoomTypeArrangement()->getHotelRoomType();
if ($base_arrangement != null && $base_arrangement !== $pricing->getHotelRoomTypeArrangement()->getArrangement()) {
$base_pricing = $this->pricingRepository->findBasePricing($season, $hrt); // BasePricing for the given HRTA
$raw_purchase_price += $base_pricing->getPurchasePrice();
$raw_sale_price += $base_pricing->getSalePrice();
}
$valid_operations = [];
// search for applicable operations (valid_operations)
foreach ($pricing->getOperations() as $operation) {
// specific operation fields
$operation_type = get_class($operation->getOperationType()); // ExtraSingle, ExtraWeekend, ...
$operation_array = null;
switch ($operation_type) {
case 'App\Entity\DiscountChild':
// $req_room_index data
$ages = $hotelAvailabilityReq->getAges('child', $req_index);
$nbAdults = $hotelAvailabilityReq->getNbPersonGen('adult', $req_index);
// operation info
$op_childRank = $operation->getOperationType()->getChildRank();
$op_nbAdultsInRoom = $operation->getOperationType()->getNbAdultsInRoom();
$op_ageMin = $operation->getOperationType()->getAgeMin();
$op_ageMax = $operation->getOperationType()->getAgeMax();
if ( // check whether discountChild operation is applicable for the req_room_index
array_key_exists($op_childRank - 1, $ages) && // the child rank exist
$nbAdults == $op_nbAdultsInRoom && // the nb Adult in room is OK
$ages[$op_childRank - 1] >= $op_ageMin && $ages[$op_childRank - 1] < $op_ageMax // the age is OK
) {
$operation_array['ageMin'] = $op_ageMin;
$operation_array['ageMax'] = $op_ageMax;
$operation_array['childRank'] = $op_childRank;
$operation_array['nbAdultsInRoom'] = $op_nbAdultsInRoom;
$this->pushGenericOperationFields($operation_array, $operation);
$valid_operations[] = $operation;
}
break;
case 'App\Entity\DiscountAdult':
// $req_room_index data
$nbAdults = $hotelAvailabilityReq->getNbPersonGen('adult', $req_index);
// operation info
$op_adult_rank = $operation->getOperationType()->getRankAdult();
if ( // check whether DiscountAdult operation is applicable for the req_room_index
$nbAdults >= $op_adult_rank // the nb Adult in room is OK
) {
$operation_array['rank_adult'] = $op_adult_rank;
$this->pushGenericOperationFields($operation_array, $operation);
$valid_operations[] = $operation;
}
break;
case 'App\Entity\DiscountSenior':
// $req_room_index data
$ages = $hotelAvailabilityReq->getAges('adult', $req_index);
// operation info
$op_ageMin = $operation->getOperationType()->getAgeMin();
$op_ageMax = $operation->getOperationType()->getAgeMax();
$nbSenior = 0;
foreach ($ages as $age) {
if ($age >= $op_ageMin && $age < $op_ageMax) {
$nbSenior++;
}
}
if ($nbSenior > 0) {
$operation_array['ageMin'] = $op_ageMin;
$operation_array['ageMax'] = $op_ageMax;
$operation_array['nb_senior'] = $nbSenior;
$this->pushGenericOperationFields($operation_array, $operation);
$valid_operations[] = $operation;
}
break;
case 'App\Entity\DiscountLongStay':
$req_stay = $hotelAvailabilityReq->getNbStay(); // Nb of stay days
$op_minStay = $operation->getOperationType()->getMinStay();
if ($req_stay >= $op_minStay) {
$operation_array['min_stay'] = $op_minStay;
$this->pushGenericOperationFields($operation_array, $operation);
$valid_operations[] = $operation;
}
break;
case 'App\Entity\DiscountWeekend':
case 'App\Entity\ExtraWeekend':
// same fields for Extra or Discount Weekend
$req_days = $hotelAvailabilityReq->getStayDays();
$op_day_start = $operation->getOperationType()->getDayStart();
$op_day_end = $operation->getOperationType()->getDayEnd();
$nb_weekend_days = 0;
foreach ($req_days as $req_day) {
if ($req_day >= $op_day_start and $req_day <= $op_day_end) {
$nb_weekend_days++;
}
}
if ($nb_weekend_days > 0) {
$operation_array['nb_weekend_days'] = $nb_weekend_days;
$operation_array['day_start'] = $op_day_start;
$operation_array['day_end'] = $op_day_end;
$this->pushGenericOperationFields($operation_array, $operation);
$valid_operations[] = $operation;
}
break;
case 'App\Entity\ExtraSingle':
$nb_adults = $hotelAvailabilityReq->getNbPersonGen('adult', $req_index);
$nb_children = $hotelAvailabilityReq->getNbPersonGen('child', $req_index);
$nb_persons = $nb_adults + $nb_children;
if ($nb_persons == 1) { // if extra single applicable (EXTRA SINGLE IS APPLICABLE ONLY IF THE NB_PERSONS = 1
$operation_array['nb_persons'] = $nb_persons;
$this->pushGenericOperationFields($operation_array, $operation);
$valid_operations[] = $operation;
}
break;
}
if ($operation_array != null) {
($operation->getOperationType()->getType() == 1)
? $extras[] = $operation_array
: $discounts[] = $operation_array;
}
} // foreach operations END
// calculate extra and discount for sale and purchase
$purchase_discounted_amount = 0;
$sale_discounted_amount = 0;
$purchase_extra_amount = 0;
$sale_extra_amount = 0;
foreach ($valid_operations as $v_operation) {
if ($v_operation->getOperationType()->getType() == -1) {
$purchase_discounted_amount += $this->operationAmount(
$raw_purchase_price,
$v_operation->getPurchaseValue(),
$v_operation->getCalculationMethod()
);
$sale_discounted_amount += $this->operationAmount(
$raw_sale_price,
$v_operation->getSaleValue(),
$v_operation->getCalculationMethod()
);
} else {
$purchase_extra_amount += $this->operationAmount(
$raw_purchase_price,
$v_operation->getPurchaseValue(),
$v_operation->getCalculationMethod()
);
$sale_extra_amount += $this->operationAmount(
$raw_sale_price,
$v_operation->getSaleValue(),
$v_operation->getCalculationMethod()
);
}
}
// unit prices after discounts and extras
$unit_purchase_price = (!$pricing->isByPerson())
? ($raw_purchase_price - $purchase_discounted_amount + $purchase_extra_amount)
: ($raw_purchase_price * $hotelAvailabilityReq->getNbPerson($req_index) - $purchase_discounted_amount + $purchase_extra_amount);
$unit_sale_price = (!$pricing->isByPerson())
? ($raw_sale_price - $sale_discounted_amount + $sale_extra_amount)
: ($raw_sale_price * $hotelAvailabilityReq->getNbPerson($req_index) - $sale_discounted_amount + $sale_extra_amount);
// weighted prices after discounts and extras
$weighted_purchase_price = $unit_purchase_price * $pricing_data['nbDays'];
$weighted_sale_price = $unit_sale_price * $pricing_data['nbDays'];
// calculate promos
$season = $pricing->getSeason();
$pricingStayOverlapPeriod = $this->helpers->overlapPeriod(
$hotelAvailabilityReq->getCheckInDate(),
$hotelAvailabilityReq->getLastNightDate(),
$season->getStartDate(),
$season->getEndDate()
);
$purchase_promos_amount = 0;
$sale_promos_amount = 0;
$promos_array = [];
foreach ($promos as $promo) {
if (!$promo->getHrtaCombinations()->contains($pricing->getHotelRoomTypeArrangement())) {
continue;
}
$nbPromoDays = $this->helpers->nbDaysOverlap(
$pricingStayOverlapPeriod['start'],
$pricingStayOverlapPeriod['end'],
$promo->getCheckinStartDate(),
$promo->getCheckInEndDate(),
);
if ($nbPromoDays > 0) { // Stay duration in a Promo Period ... OK
if ($hotelAvailabilityReq->getNbStay() < $promo->getMinStay())
continue; // Check MinStay ... OK
if (!is_null($promo->getMaxStay()) and $nbPromoDays > $promo->getMaxStay()) {
$nbPromoDays = $promo->getMaxStay();
}
// check booking date
$today = new DateTime();
$today->setTime(0, 0, 0);
$promoType = $promo->getPromoType();
if ($promoType == 2) {
$dateEB_JCheckin = clone $hotelAvailabilityReq->getCheckInDate();
$dateEB_JCheckin->sub(new DateInterval('P' . $promo->getEbDaysBefore() . 'D'));
}
if (
($promoType == 1 and $today >= $promo->getEbStartDate() && $today <= $promo->getEbEndDate()) or
($promoType == 2 and $today <= $dateEB_JCheckin
) or
($promoType == 3)
) { // Booking period ... OK
$promo_purchase_amount = $nbPromoDays * $this->operationAmount(
$unit_purchase_price,
$promo->getPurchaseValue(),
$promo->getCalculationMethod()
);
$promo_sale_amount = $nbPromoDays * $this->operationAmount(
$unit_sale_price,
$promo->getSaleValue(),
$promo->getCalculationMethod()
);
$purchase_promos_amount += $promo_purchase_amount;
$sale_promos_amount += $promo_sale_amount;
$promos_array[$promo->getTitle()] = [
'nbDays' => $nbPromoDays,
'purchase' => $promo_purchase_amount,
'sale' => $promo_sale_amount
];
}
}
}
$promo_purchase_price = $weighted_purchase_price - $purchase_promos_amount;
$promo_sale_price = $weighted_sale_price - $sale_promos_amount;
$pricing_data['discounts'] = $discounts;
$pricing_data['parties'] = $extras;
$pricing_data['promos'] = $promos_array;
$pricing_data['byPerson'] = $pricing->isByPerson();
$pricing_data['prices'] = [
'raw_unit_purchase' => $raw_purchase_price,
'raw_unit_sale' => $raw_sale_price,
// unit prices after operations
'op_unit_purchase' => $unit_purchase_price,
'op_unit_sale' => $unit_sale_price,
// weighted price for the current pricing (sub-price)
'weighted_purchase' => $weighted_purchase_price,
'weighted_sale' => $weighted_sale_price,
// weighted price with promo
'promo_purchase' => $promo_purchase_price,
'promo_sale' => $promo_sale_price,
];
return $pricing_data;
}
private function pushGenericOperationFields(array &$operation_array, Operation $operation): void
{
//generic operation fields
$operation_array['title'] = $operation->getOperationType()->__toString();
$operation_array['purchaseValue'] = $operation->getPurchaseValue();
$operation_array['saleValue'] = $operation->getSaleValue();
$operation_array['calculation'] = $operation->getCalculationMethod();
$operation_array['id'] = $operation->getId();
}
public function addPartiesToAvailableHotels(array &$availableHotels, array $parties, HotelAvailabilityRequest $request): void
{
foreach ($parties as $party) {
$contract_id = $party->getHotelContract()->getId();
$hotel_id = $party->getHotelContract()->getHotel()->getId();
$hotel_id = "0-" . $party->getHotelContract()->getHotel()->getId();
// Exchange Rate purchaseToSaleCurrency
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate(
$party->getHotelContract()->getProvider()->getCurrency(), // purchase currency
$request->getCurrency()
);
$party_hrta_ids = [];
foreach ($party->getHotelRoomTypeArrangements() as $hotelRoomTypeArrangement) {
$party_hrta_ids[] = $hotelRoomTypeArrangement->getId();
}
foreach ($availableHotels[$hotel_id]['sources'][$contract_id]['rooms'] as $request_room_key => $request_room) {
$zones_array = null;
foreach ($party->getProductElements() as $partyZone) {
// Display only selected partyZone in CheckRate
if (!empty($request->getPartiesZones()[$request_room_key])) {
$partiesZonesInRequest = explode(',', $request->getPartiesZones()[$request_room_key]);
if (!in_array(strval($partyZone->getId()), $partiesZonesInRequest))
continue;
}
$purchase_price =
$partyZone->getAdultPurchasePrice() * $request->getNbPersonGen('adult', $request_room_key) +
$partyZone->getChildPurchasePrice() * $request->getNbPersonGen('child', $request_room_key);
$sale_price =
$partyZone->getAdultSalePrice() * $request->getNbPersonGen('adult', $request_room_key) +
$partyZone->getChildSalePrice() * $request->getNbPersonGen('child', $request_room_key);
$zonePartyStock = $this->stockOperationService->getDailyStock($party->getDate(), $party->getDate(), $party, $partyZone, $request->getCustomer());
$partyZoneQuantity = $zonePartyStock[$party->getDate()->format('Y-m-d')]; // one-day
$partyZoneRateKey = $partyZone->getId();
$zones_array[] = [
'id' => $partyZoneRateKey,
'title' => $partyZone->getZone()->getTitle(),
'purchasePrice' => $purchase_price,
'purchaseCurrency' => $party->getHotelContract()->getProvider()->getCurrency(),
'salePrice' => $sale_price * $exchangeRatePurchaseToSale,
'saleCurrency' => $request->getCurrency(),
'availableQuantity' => $partyZoneQuantity
];
} // END partyZone loop
$extra_array = [
'id' => $party->getId(),
'type' => 'party',
'title' => $party->getTitle(),
'date' => $party->getDate()->format('Y-m-d'),
'zones' => $zones_array
];
foreach ($request_room as $hrta_key => $hrta) {
if (in_array($hrta['id'], $party_hrta_ids)) {
$availableHotels[$hotel_id]['sources'][$contract_id]['rooms'][$request_room_key][$hrta_key]
['parties'][] = $extra_array;
//$availableHotels[$hotel_id]['sources'][$contract_id]['rooms'][$request_room_key][$hrta_key]['parties'][]=$party->getId();
}
}
}
//$availableHotels[$hotel_id]['sources'][$hc_id]['rooms'][$index][$hrta_array['id']] =$hrta_array;
//foreach ()
}
}
/**
* format the array of replacing entitiesKeys by defaultKeys and removing unnecessary fields
*
* @param array $availableHotels
*
* @return void
*/
public function formatHotelAvailabilityArray(array &$availableHotels): void
{
$hrta_fields_to_delete = ['pricings_data', 'nb_stay_overlap'];
foreach ($availableHotels as $hotel_key => $preAvailableHotel) { // hotels
foreach ($preAvailableHotel['sources'] as $contract_key => $source) { // sources (contracts)
foreach ($source['rooms'] as $request_room_key => $request_room) { // request_rooms
foreach ($request_room as $hrta_key => $hrta) {
// delete unnecessary fields
foreach ($hrta_fields_to_delete as $field) {
unset($availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key][$field]);
}
if (key_exists('parties', $availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key])) {
$parties_keys_array = array_column($availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['parties'], 'id');
$parties_key = implode(",", $parties_keys_array);
// concat partiesKeys to hrtaRateKey
$availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key][$hrta_key]['rateKey'] .= '|' . $parties_key;
}
}
$availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key]
= array_values($availableHotels[$hotel_key]['sources'][$contract_key]['rooms'][$request_room_key]);
}
}
$availableHotels[$hotel_key]['sources'] = array_values($availableHotels[$hotel_key]['sources']);
}
$availableHotels = array_values($availableHotels);
}
public function applyMarginsAndHideExcludedHotelXml(array &$availableHotels, Customer $customer): void
{
foreach ($availableHotels as $hotel_key => &$availableHotel) { // hotels
foreach ($availableHotel['sources'] as $contract_key => &$source) { // sources (contracts)
$xmlSourceId = $source['xmlSourceId'];
if ($xmlSourceId == 0)
continue; // MARGIN is applied only for XML_API product
$hotelXmlArray = [
'id' => null,
'name' => $availableHotel['hotel']['name']
];
$margin = $this->getMargin($xmlSourceId, $customer, $hotelXmlArray['name']);
if ($margin <= 0) { // hotelXml not blocked for the customer
unset($availableHotels[$hotel_key]['sources'][$contract_key]);
continue;
}
foreach ($source['rooms'] as &$request_room) { // request_rooms
foreach ($request_room as &$hrta) {
$hrta['saleRate'] = $hrta['saleRate'] * (100.0 + $margin) / 100.0;
$hrta['salePromoRate'] = $hrta['salePromoRate'] * (100.0 + $margin) / 100.0;
}
}
}
if (count($availableHotels[$hotel_key]['sources']) == 0) {
unset($availableHotels[$hotel_key]);
}
}
$availableHotels = array_values($availableHotels);
}
public function getMargin(int $xmlSourceId, Customer $customer, $hotelName)
{
// High priority : The customer has a specific margin for the hotelXml
$customerHotelXml = $this->customerHotelXmlRepository->findOneBy(
[
'customer' => $customer,
'hotelXmlName' => $hotelName
]
);
if ($customerHotelXml) {
if ($customerHotelXml->isDisplay() == false) { // hotelXml blocked for this customer
return -1; // -1 (i.e. blocked)
}
return $customerHotelXml->getMargin();
}
// Medium priority : The customer has a specific margin for XmlApi
$customerXmlApi = $this->customerXmlApiRepository->findOneByCustomerAndXmlSource($customer, $xmlSourceId);
if ($customerXmlApi) {
return $customerXmlApi->getMargin();
}
// Low priority : Consider XmlApi defined margin
$xmlApi = $this->xmlApiRepository->findOneBy(['source' => $xmlSourceId]);
if ($xmlApi) {
if ($customer->getClass() == "CustomerMoral") {
return $xmlApi->getmarginB2B();
}
return $xmlApi->getmarginB2C();
}
return -1;
}
public function getHotelContractsIDsFromAVHTL(array $availableHotels): array
{
$availableHotelContracts = [];
foreach ($availableHotels as $availableHotel) {
foreach ($availableHotel['sources'] as $hc) {
$availableHotelContracts[] = $hc;
}
}
return array_column($availableHotelContracts, 'sourceId');
}
/**
* This is the main function for HotelApi CheckRate Method
* Given the selected hrta and selected parties zones, it performs prices check (rooms and parties)
* and proposes the possible options (free or paid) to book (Parking, Transfer, Decoration, ...)
*
* @param mixed $parameters
*
* @return array
* @throws Exception
*/
function checkRate(mixed $parameters, $customer): array
{
$isXml = str_contains($parameters['searchCode'], '@');
if ($isXml) { // HUB
return $this->checkRateHub($parameters, $customer);
} else { // LOCAL
// Prepare the HotelRequest
$hotelReq = $this->checkRateKey($parameters);
$hotelReq->setCustomer($customer);
$hotelReq->setSearchCode($parameters['searchCode']);
return $this->hotelsAvailability($hotelReq);
}
}
private function checkRateHub($parameters, $customer): array
{
$searchCode_full = $parameters['searchCode'];
$searchCode_external = substr(strstr($searchCode_full, '@'), 1); // right subString after @
$searchCode_local = strstr($searchCode_full, '@', true); // left subString before @
$parameters_hub = [];
$parameters_hub['searchCode'] = $searchCode_external;
$parameters_hub['language'] = 'fr-FR';
foreach ($parameters['rooms'] as $room) {
$room_hub = [
'ratekey' => substr(strstr($room['rateKey'], '@'), 1) // right subString after @
];
$parameters_hub['rooms'][] = $room_hub;
}
$apiResponse = $this->helpers->getApiResponse(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_CHECKRATE_ENDPOINT,
api_key: $this->parameterService->getHotelHubApiKey(),
request_body: $parameters_hub
);
$apiResults = $apiResponse['response'];
$tokenForBook = $apiResponse['tokenForBook'];
$availableHotels = $this->normalizeSearchResultsFromHubFormat($apiResults, $customer->getCurrency(), $searchCode_local, $tokenForBook, true);
$this->formatHotelAvailabilityArray($availableHotels);
$this->applyMarginsAndHideExcludedHotelXml($availableHotels, $customer);
return $availableHotels;
}
private function getCancellationsFeesArray(
int $contract_id,
int $hrt_id,
HotelAvailabilityRequest $request,
$hrta_promo_sale_price
): array {
$cancellations_array = [];
$cancellationsPolicies = $this->cancellationPolicyRepository->findRelevantCancellations($request->getCheckInDate(), $contract_id, $hrt_id);
foreach ($cancellationsPolicies as $cancellationPolicy) {
// dd('here8');
$cancellation_date = clone $request->getCheckInDate();
$j_days = $cancellationPolicy->getMinimumDaysBefore() . "days";
date_sub($cancellation_date, date_interval_create_from_date_string($j_days));
$cancellation_array['from'] = $cancellation_date->format('Y-m-d');
//dd($request->getNbStay());
$cancellation_array['fee'] = $this->getCancellationFee($cancellationPolicy, $hrta_promo_sale_price, $request->getNbStay());
$cancellations_array[] = $cancellation_array;
}
return $cancellations_array;
}
private function operationAmount(?string $price, ?string $d_value, ?string $calculMethod): float
{
if ($calculMethod == 'amount') {
return $d_value;
} else { // percent
return $price * $d_value / 100.0;
}
}
/* public function getBestPriceBoard(array $hotel, $current_source_key): array
{
$bestPrice_current_sourceKey = 9999999;
$bestPrice_hotel = null;
$bestBoard_hotel = null;
$bestRooms_hotel = [];
foreach ($hotel['sources'] as $source) {
$roomsLabels = [];
foreach ($source['hrts'] as $boardName => $boards) {
$totalPrice = 0; // Initialize total price for this boardName
foreach ($boards as $roomIdx => $board) {
$minPrice = null; // Initialize minimum price for this index
$minPriceRoomType = "Room"; // The roomType corresponding to the min-price
foreach ($board as $offer) {
$price = $offer['salePromoRate']; // Get the price of this offer
if ($minPrice === null || $price < $minPrice) {
$minPrice = $price; // Update minimum price if found a lower price
$minPriceRoomType = $offer['roomName'];
}
}
$totalPrice += $minPrice; // Add the lowest price for this index to the total price
$roomsLabels[$roomIdx] = $minPriceRoomType;
}
// Check if this boardName has the lowest total price
if ($bestPrice_hotel === null || $totalPrice < $bestPrice_hotel) {
$bestPrice_hotel = $totalPrice;
$bestBoard_hotel = $boardName;
$bestRooms_hotel = $roomsLabels;
if ($current_source_key == $source['sourceKey']) {
$bestPrice_current_sourceKey = $totalPrice;
}
}
}
}
return ['bestPrice' => $bestPrice_hotel,
'bestBoard' => $bestBoard_hotel,
'bestRooms' => $bestRooms_hotel,
'bestPrice_currentSourceKey' => $bestPrice_current_sourceKey
];
}*/
public function getBestPriceBoardForSource(array $source): array
{
$bestPrice_source = null;
$bestBoard_source = null;
$bestRooms_source = [];
foreach ($source['hrts'] as $boardName => $boards) {
$totalPrice = 0;
$roomsLabels = [];
foreach ($boards as $roomIdx => $board) {
$minPrice = null;
$minPriceRoomType = "Room";
foreach ($board as $offer) {
$price = $offer['salePromoRate'];
if ($minPrice === null || $price < $minPrice) {
$minPrice = $price;
$minPriceRoomType = $offer['roomName'];
}
}
$totalPrice += $minPrice;
$roomsLabels[$roomIdx] = $minPriceRoomType;
}
if ($bestPrice_source === null || $totalPrice < $bestPrice_source) {
$bestPrice_source = $totalPrice;
$bestBoard_source = $boardName;
$bestRooms_source = $roomsLabels;
}
}
return [
'bestPrice' => $bestPrice_source,
'bestBoard' => $bestBoard_source,
'bestRooms' => $bestRooms_source,
];
}
public function checkStatusHub(OrderLine $orderline)
{
$parameters = [
"bookingSource" => $orderline->getProduct()->getSource(),
"bookingId" => $orderline->getExternalOrderId()
];
$apiResponse = $this->helpers->getApiResponse(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_CHECKSTATUS_ENDPOINT,
api_key: $this->parameterService->getHotelHubApiKey(),
request_body: $parameters
);
return $apiResponse['response'];
}
protected function getHrtaRateKey(HotelAvailabilityRequest $hotelReq, int $room_index, int $sourceXmlId, $sourceId, $hrtaCode): string
{
$rateKeyElements = [];
$rateKeyElements[] = $hotelReq->getCheckInDate()->format('Ymd'); // #0
$rateKeyElements[] = $hotelReq->getCheckOutDate()->format('Ymd'); // #1
$rateKeyElements[] = $sourceXmlId; // sourceXmlId 0:local OR XML_ID // #2
$rateKeyElements[] = $sourceId; // #3
$rateKeyElements[] = $hotelReq->getCurrency(); // #4 sale currency
$rateKeyElements[] = $hotelReq->getAgencyID(); // #5 agency id
$rateKeyElements[] = $hrtaCode; #6
$occAdult = $hotelReq->getNbPersonGen('adult', $room_index) . ':' . implode(',', $hotelReq->getAges('adult', $room_index));
$occChild = $hotelReq->getNbPersonGen('child', $room_index) . ':' . implode(',', $hotelReq->getAges('child', $room_index));
$room_occupancy = $occAdult . '+' . $occChild;
$rateKeyElements[] = $room_occupancy; #7
return implode('|', $rateKeyElements);
}
private function getHrtaRateKeyForSpecifiedPeriod(OrderSubLine $orderSubLine, $startDate, $endDate): string
{
$rateKeyElements = [];
$rateKeyElements[] = $startDate->format('Ymd'); // #0
$rateKeyElements[] = $endDate->format('Ymd'); // #1
$rateKeyElements[] = 0; // OrderLine management for local-contract only
$rateKeyElements[] = $orderSubLine->getOrderLine()->getProduct()->getId(); // hotel_contract_id
$rateKeyElements[] = $orderSubLine->getOrderLine()->getOrderAtt()->getCurrency();
$rateKeyElements[] = $orderSubLine->getOrderLine()->getOrderAtt()->getAgency()->getId(); // Agency ID
$rateKeyElements[] = $orderSubLine->getPeaID(); // hrta-id (pea-code : product-elt-arr-code)
$occAdult = $orderSubLine->getNbAdult() . ':' . $this->getTmpAgesString($orderSubLine->getNbAdult(), 40);
$occChild = $orderSubLine->getNbChild() . ':' . $this->getTmpAgesString($orderSubLine->getNbChild(), 16);
$room_occupancy = $occAdult . '+' . $occChild;
$rateKeyElements[] = $room_occupancy; // room occupancy
return implode('|', $rateKeyElements);
}
private function getTmpAgesString($count, $age): string
{
$array = [];
for ($i = 0; $i < $count; $i++) {
$array[] = $age;
}
return implode(',', $array);
}
public function hotelDetails($codeHotel, $locale): array
{
$request = $this->requestStack->getCurrentRequest();
if ($request) {
$request->getSession()->set('_locale', $locale); // store in session
$request->setLocale($locale); // optional: apply immediately
}
$hotel = $this->entityManager->getRepository(Hotel::class)->find($codeHotel);
$hotelType = $hotel->getType();
$hotelCity = $hotel->getCity();
$hotelCountry = $hotelCity->getCountry();
return [
"hotelId" => '0-' . $hotel->getId(), // todo hash this hotel id
"name" => $hotel->getName(),
"type" => ($hotelType ? $hotelType->getName() : ''),
"starRating" => $hotel->getStarRating(),
"country" => $hotelCountry->getName(),
"city" => $hotelCity->getName(),
"latitude" => ($hotel->getLatitude() != '' ? $hotel->getLatitude() : $hotel->getCity()->getLatitude()),
"longitude" => ($hotel->getLongitude() != '' ? $hotel->getLongitude() : $hotel->getCity()->getLongitude()),
"thumbImage" => $hotel->getPrimaryImageUrl(),
"description" => $hotel->getDescription(),
"phone" => $hotel->getPhone(),
"website" => $hotel->getWebsite(),
"tripadvisorId" => $hotel->getTripadvisorId(),
"options" => $hotel->getOptions()->getValues(),
'facilities' => array_map(function ($facility) {
return array(
'name' => $facility->getName(),
'ordre' => $facility->getOrdre(),
'icon' => $facility->getIcon()
);
}, $hotel->getFacilities()->getValues()),
'images' => array_map(function ($filedata) {
return array(
'alt' => $filedata->getAlt(),
'url' => $filedata->getUrl()
);
}, $hotel->getImages()->getValues()),
'promos' => $this->getHotelPromos($hotel->getId()),
'seo' => [
'title' => $hotel->getSeoTitle(),
'description' => $hotel->getSeoDescription()
]
];
}
private function getHotelPromos(int $hotelId): array
{
$promos_array = [];
$promos = $this->promoRepository->getPromosByHotel($hotelId);
$today = DateTime::createFromFormat("Y-m-d", date("Y-m-d"));
foreach ($promos as $promo) {
$promoType = $promo->getPromoType();
$promo_array = [];
switch ($promoType) {
case 1:
if ($promo->getEbEndDate() >= $today) {
$promo_array = [
'id' => $promo->getId(),
'title' => $promo->getTitle(),
'promoType' => $promo->getPromoType(),
'checkinStartDate' => $promo->getCheckinStartDate()->format('Y-m-d'),
'checkinEndDate' => $promo->getCheckinEndDate()->format('Y-m-d'),
'ebStartDate' => $promo->getEbStartDate()->format('Y-m-d'),
'ebEndDate' => $promo->getEbEndDate()->format('Y-m-d'),
'minStay' => $promo->getMinStay(),
'maxStay' => $promo->getMaxStay()
];
}
break;
case 2:
if (date_diff($today, $promo->getCheckinStartDate())->days >= $promo->getEbDaysBefore()) {
$promo_array = [
'id' => $promo->getId(),
'title' => $promo->getTitle(),
'promoType' => $promo->getPromoType(),
'checkinStartDate' => $promo->getCheckinStartDate()->format('Y-m-d'),
'checkinEndDate' => $promo->getCheckinEndDate()->format('Y-m-d'),
'ebDaysBefore' => $promo->getEbDaysBefore(),
'minStay' => $promo->getMinStay(),
'maxStay' => $promo->getMaxStay()
];
}
break;
case 3:
$promo_array = [
'id' => $promo->getId(),
'title' => $promo->getTitle(),
'promoType' => $promo->getPromoType(),
'checkinStartDate' => $promo->getCheckinStartDate()->format('Y-m-d'),
'checkinEndDate' => $promo->getCheckinEndDate()->format('Y-m-d'),
'minStay' => $promo->getMinStay(),
'maxStay' => $promo->getMaxStay()
];
break;
}
if ($promo_array)
$promos_array[] = $promo_array;
}
return $promos_array;
}
public function getRelevantDestinations($query): array
{
$cities = $this->cityRepository->searchBySubString($query);
$cities_array = [];
foreach ($cities as $city) {
$cities_array[] = [
'id' => $city->getExternalId(),
'name' => $city->getName(),
'type' => 'city',
'city' => $city->getName(),
'country' => $city->getCountry() ? $city->getCountry()->getName() : '',
];
}
return $cities_array;
}
public function getHotels(int $cityID): array
{
$hotels = $this->hotelRepository->findBy(['city' => $cityID]);
$hotels_array = [];
foreach ($hotels as $hotel) {
$hotels_array[] = [
'id' => $hotel->getId(),
'name' => $hotel->getName()
];
}
return $hotels_array;
}
/**
* Check rateKeys from HUB to get price, then, if customer balance is sufficient, execute bookFromHUB method.
* @param mixed $parameters
* @param $order
* @return OrderLine|null
*/
public function bookFromHubChecked(mixed $parameters, $order): ?OrderLine
{
// 1: checkrate from HUB
$checkRateResponse = $this->checkRateHub($parameters, $order->getCustomer());
$checkRateAmount = $this->getAmountFromCheckRateHubResponse($checkRateResponse);
// 2: get customer balance (add optionally overdraft)
$customerBalance = $this->walletService->getWallet($order->getCustomer())['balance'];
$authorizedCreditAmount = $order->getCustomer()->getCreditAmountAuthorized();
$customerBalancePlus = $customerBalance + $authorizedCreditAmount;
//dd($customerBalancePlus);
// IF BALANCE IS OK : BOOK FROM HUB
$balanceSufficient = $customerBalancePlus >= $checkRateAmount;
if ($balanceSufficient) {
$orderLine = self::getHubOrderLine_parameters($parameters, $order);
return $this->bookHubOrderline($orderLine);
}
return null;
}
private function getHubOrderLine_parameters(mixed $parameters, &$order): ?OrderLine
{
$product = $this->xmlApiRepository->findOneBy(['source' => $parameters['source']]);
$orderLineRef = $this->helpers->generateShortId();
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($product->getProvider()->getCurrency(), $order->getCurrency());
$hotelOrderLine = new OrderLine();
$hotelOrderLine->setSearchCode($parameters['searchCode'] ?? "");
$hotelOrderLine->setTokenForBook($parameters['tokenForBook'] ?? "");
$hotelOrderLine->setProduct($product);
$hotelOrderLine->setHotelName($parameters['hotelName'] ?? "Hotel Name");
$hotelOrderLine->setImage($parameters['image'] ?? "");
$hotelOrderLine->setCity($parameters['city'] ?? "");
$hotelOrderLine->setCountry($parameters['country'] ?? "");
$hotelOrderLine->setReference($orderLineRef);
$hotelOrderLine->setBeneficiary($parameters['beneficiary'] ?? "");
$hotelOrderLine->setComment($parameters['recommandations'] ?? "");
$hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
$tempDate = DateTime::createFromFormat('d/m/Y', '01/01/2000');
$hotelOrderLine->setStartDate($tempDate);
$hotelOrderLine->setEndDate($tempDate);
$hotelOrderLine->setDeadline(null);
$description = $hotelOrderLine->getHotelName() . " - " . $orderLineRef;
$hotelOrderLine->setDescription($description);
$hotelOrderLine->setState(OrderStateEnum::TEMP_AVAILABLE->getValue());
$hotelOrderLine->setError("temp_book");
foreach ($parameters['rooms'] as $room) {
$hotelOrderSubLine = new OrderSubLine();
$hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
$hotelOrderSubLine->setRatekey($room['rateKey']);
$hotelOrderSubLine->setDescription("Temp_room");
$hotelOrderSubLine->setSalePrice(0);
$hotelOrderSubLine->setPurchasePrice(0);
$hotelOrderSubLine->setNbAdult($room['nbAdult'] ?? 0);
$hotelOrderSubLine->setNbChild($room['nbChild'] ?? 0);
$hotelOrderSubLine->setRoomChildrenAge($room['childrenAge'] ?? "");
$hotelOrderSubLine->setCancelable(true);
$hotelOrderSubLine->setState(OrderStateEnum::TEMP_AVAILABLE->getValue());
// Create beneficiaries from paxes if provided
if (isset($room['paxes']) && is_array($room['paxes'])) {
foreach ($room['paxes'] as $pax) {
$beneficiary = new Beneficiary();
$beneficiary->setCivility($pax['civility'] ?? '');
$beneficiary->setFirstName($pax['firstName'] ?? '');
$beneficiary->setLastName($pax['lastName'] ?? '');
$beneficiary->setAge($pax['age'] ?? '');
$hotelOrderSubLine->addBeneficiary($beneficiary);
}
}
$hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
}
$order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
return $hotelOrderLine;
}
/**
* Prepare an orderLine for Hub-HOTEL-BOOK with status : TEMP_AVAILABLE Or PENDING
* @param mixed $cart_line
* @param $order
* @return OrderLine
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function getHubOrderLine_cartLine(mixed $cart_line, Order $order): OrderLine
{
$hub_searchCode_expanded = substr(strstr($cart_line['searchCode'], '@'), 1);
$searchCode = substr(strstr($hub_searchCode_expanded, TOKEN_FOR_BOOK_SEPARATOR), strlen(TOKEN_FOR_BOOK_SEPARATOR));
$tokenForBook = $cart_line['tokenForBook'];
$customer = $order->getCustomer();
$bookingSource = strstr($cart_line['elements'][0]['rateKey'], '@', true); // left subString before @
$product = $this->xmlApiRepository->findOneBy(['source' => $bookingSource]);
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($product->getProvider()->getCurrency(), $order->getCurrency());
$hotelName = $cart_line['label'];
$comment = "";
if (key_exists('options', $cart_line)) {
$options = $cart_line['options'] ?? [];
foreach ($options as $option) {
$comment = $comment . ' ' . $option . ', ';
}
}
$comment = $comment . " " . $cart_line['comment'];
$date_range = $cart_line['date'];
$start_date_str = strstr($date_range, '-', true); // left subString before -
$end_date_str = substr(strstr($date_range, '-'), 1); // right subString after -
$start_date = DateTime::createFromFormat('d/m/Y', $start_date_str);
$end_date = DateTime::createFromFormat('d/m/Y', $end_date_str); //->modify('-1 day')
$margin = $this->getMargin($bookingSource, $order->getCustomer(), $hotelName);
$hotelOrderLine = new OrderLine();
$hotelOrderLine->setProduct($product);
$hotelOrderLine->setHotelName($hotelName);
$hotelOrderLine->setHotelStarRating((int) $cart_line['rating']);
$hotelOrderLine->setImage($cart_line['image'] ?? "");
$hotelOrderLine->setCity($cart_line['city']);
$hotelOrderLine->setCountry($cart_line['country']);
$hotelOrderLine->setExternalOrderId('-');
$hotelOrderLine->setReference($this->helpers->generateShortId());
$hotelOrderLine->setBeneficiary($cart_line['beneficiary']['name']);
$hotelOrderLine->setExternalOrderIdReference("-");
$hotelOrderLine->setComment($comment);
$hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
$hotelOrderLine->setStartDate($start_date);
$hotelOrderLine->setEndDate($end_date);
$hotelOrderLine->setDeadline(null);
$hotelOrderLine->setDescription($hotelName);
$hotelOrderLine->setError("");
$hotelOrderLineState = $cart_line["available"] == "true" ? OrderStateEnum::TEMP_AVAILABLE->getValue() : OrderStateEnum::PENDING->getValue();
$hotelOrderLine->setState($hotelOrderLineState);
$hotelOrderLine->setSearchCode($searchCode);
$hotelOrderLine->setTokenForBook($tokenForBook);
foreach ($cart_line['elements'] as $room_index => $cart_line_element) {
$hotelOrderSubLine = new OrderSubLine();
$hotelOrderSubLine->setProductElement(null);
$hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
$hotelOrderSubLine->setDescription($cart_line_element['label']);
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate(
$product->getProvider()->getCurrency(),
$order->getCurrency()
);
$salePrice = $cart_line_element['price'];
$purchasePrice = ($salePrice / $exchangeRatePurchaseToSale * 100) / (100 + $margin);
$hotelOrderSubLine->setSalePrice($cart_line_element['price']);
$resellerMargin = 0.0;
if ($customer->getResellerMargin() > 0) {
$resellerMargin = $hotelOrderSubLine->getSalePrice() * ($customer->getResellerMargin() / 100);
}
$hotelOrderSubLine->setPartnerCommission($resellerMargin);
$hotelOrderSubLine->setPurchasePrice($purchasePrice);
$hotelOrderSubLine->setNbAdult(intval($cart_line_element['adultsCount']));
$hotelOrderSubLine->setNbChild(intval($cart_line_element['childrenCount']));
// = substr(strstr($cart_line_element['rateKey'], '@'), 1); // remove left-substring before first '@';
$hotelOrderSubLine->setRatekey($cart_line_element['rateKey']);
$hotelOrderSubLine->setCancelable(false);
foreach ($cart_line['elements'][$room_index]['guests'] as $pax) {
$beneficiary = new Beneficiary();
$beneficiary->setCivility($pax['civility']);
$beneficiary->setFirstName($pax['firstName']);
$beneficiary->setLastName($pax['lastName']);
$beneficiary->setAge(isset($pax['age']) && $pax['age'] !== '' ? (int)$pax['age'] : null);
$hotelOrderSubLine->addBeneficiary($beneficiary);
}
$hotelOrderSubLine->setState(OrderStateEnum::NOT_BOOKED->getValue());
$hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
}
if (key_exists('fee', $cart_line) and $cart_line['fee'] > 0) {
$hotelOrderSubLineFee = new OrderSubLine();
$hotelOrderSubLineFee->setService(ServiceEnum::product_fee->getValue());
$hotelOrderSubLineFee->setDescription(ServiceEnum::product_fee->getValue());
$hotelOrderSubLineFee->setPurchasePrice($cart_line['fee']);
$hotelOrderSubLineFee->setSalePrice($cart_line['fee']);
$hotelOrderSubLineFee->setCancelable(false);
$hotelOrderLine->addOrderSubLine($hotelOrderSubLineFee);
}
$order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
//dd($hotelOrderLine);
return $hotelOrderLine;
}
public function bookFromLocal(mixed $parameters, Order &$order, OrderLine &$hotelOrderLine = null, $apply_fee = false): ?OrderLine
{
try {
$searchCodeOK = $this->isSearchCodeValid($parameters['searchCode']); // 30 minutes
if (!$searchCodeOK)
return null;
$customer = $order->getCustomer();
// Prepare the HotelRequest
$hotelReq = $this->checkRateKey($parameters);
$hotelReq->setCustomer($customer);
if ($hotelReq->getXmlSourceId() != 0)
return null;
$hotelIsAvailable = true;
$availableHotels = $this->hotelsAvailability($hotelReq);
// Check some conditions
if (count($availableHotels) != 1)
return null; // book in one hotel
if (count($availableHotels[0]['sources']) != 1)
return null; // book in one source (contract)
if (count($availableHotels[0]['sources'][0]['rooms']) != $hotelReq->getNbRooms())
return null; // rooms check
// Find entities
$hotelContract = $this->hotelContractRepository->find((int) $availableHotels[0]['sources'][0]['sourceId']);
$hotel = $hotelContract->getHotel();
$agency = $this->agencyRepository->find($hotelReq->getAgencyID());
$order->setAgency($agency);
// Currencies
$purchaseCurrency = $hotelContract->getProvider()->getCurrency();
$saleCurrency = $hotelReq->getCurrency();
$customerCurrency = $order->getCustomer()->getCurrency();
$agencyCurrency = $agency->getCurrency();
// Currencies Exchange Rates
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($purchaseCurrency, $saleCurrency);
$exchangeRateSaleToReference = $this->currencyService->calculateExchangeRate($saleCurrency, $agencyCurrency);
$exchangeRateSaleToCustomer = $this->currencyService->calculateExchangeRate($saleCurrency, $customerCurrency);
$order->setExchangeRateSaleToReference($exchangeRateSaleToReference);
$order->setExchangeRateSaleToCustomer($exchangeRateSaleToCustomer);
// HOTEL-PRODUCT ORDER LINE
if ($hotelOrderLine == null) {
$hotelOrderLine = new OrderLine();
$hotelOrderLine->setReference($this->helpers->generateShortId());
$hotelOrderLine->setImage($parameters['image'] ?? "");
$hotelOrderLine->setBeneficiary($parameters['beneficiary'] ?? null);
}
$comment = "";
if (key_exists('options', $parameters)) {
$options = $parameters['options'] ?? [];
foreach ($options as $option) {
$comment = $comment . ' ' . $option . ', ';
}
}
$comment = $comment . ' ' . $parameters['comment'];
$hotelOrderLine->setProduct($hotelContract);
$hotelOrderLine->setComment($comment);
$hotelOrderLine->setDescription($hotel->getName() . "-" . $hotelOrderLine->getReference());
$hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
$hotelOrderLine->setStartDate($hotelReq->getCheckInDate());
$hotelOrderLine->setEndDate($hotelReq->getCheckOutDate());
$hotelOrderLine->setHotelName($hotel->getName());
$hotelOrderLine->setHotelStarRating($hotel->getStarRating());
$hotelOrderLine->setCity($hotel->getCity()->getName());
$hotelOrderLine->setCountry($hotel->getCity()->getCountry()->getName());
// Init
$partiesOrderSubLines = [];
$places_put_aside = []; // temporary nb places grouped by partyZoneId (to calculate the final requested places for each partyZoneId)
// rooms loop
$rooms = [];
foreach ($hotelReq->getRoomsOccupancies() as $index => $room) {
$hrta = $availableHotels[0]['sources'][0]['rooms'][$index][0];
$rooms[] = $hrta;
$hotelRoomType = $this->hotelRoomTypeRepository->find($hrta['roomId']); // hrt entity
$hotelOrderSubLine = new OrderSubLine();
$hotelOrderSubLine->setProductElement($hotelRoomType);
$hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
$OSL_description = $hotelRoomType->getRoomType()->getName();
$OSL_description .= " [" . $hrta['boardName'] . "]";
$hotelOrderSubLine->setDescription($OSL_description);
$hotelOrderSubLine->setSalePrice($hrta['salePromoRate']);
$resellerMargin = 0.0;
if ($customer->getResellerMargin() > 0) {
$resellerMargin = $hotelOrderSubLine->getSalePrice() * ($customer->getResellerMargin() / 100);
}
$hotelOrderSubLine->setPartnerCommission($resellerMargin);
$hotelOrderSubLine->setPurchasePrice($hrta['purchasePromoRate']);
$hotelOrderSubLine->setNbAdult($hotelReq->getNbPersonGen('adult', $index));
$hotelOrderSubLine->setNbChild($hotelReq->getNbPersonGen('child', $index));
$childrenAge = implode(', ', $hotelReq->getAges('child', $index));
$hotelOrderSubLine->setRoomChildrenAge($childrenAge);
$hotelOrderSubLine->setPeaID($hrta['id']);
$hotelOrderSubLine->setCancelable(!$hrta['NRF']);
$osl_cancellationFees = $this->getCancellationsFeesArray(
$hotelContract->getId(),
$hotelRoomType->getId(),
$hotelReq,
$hotelOrderSubLine->getSalePrice()
);
$osl_cancellationFees_jsonString = json_encode($osl_cancellationFees);
$hotelOrderSubLine->setCancellationFeesJson($osl_cancellationFees_jsonString);
if (isset($parameters['rooms'][$index]['paxes']) && is_array($parameters['rooms'][$index]['paxes'])) {
foreach ($parameters['rooms'][$index]['paxes'] as $pax) {
$beneficiary = new Beneficiary();
$beneficiary->setCivility($pax['civility'] ?? '');
$beneficiary->setFirstName($pax['firstName'] ?? '');
$beneficiary->setLastName($pax['lastName'] ?? '');
$hotelOrderSubLine->addBeneficiary($beneficiary);
}
}
// Stock
$hotelSaleOperation = new ProductElementStockOperation();
$roomIsAvailable = false;
if ($hrta['availableQuantity'] >= 1) { // check the Specific Stock
$roomIsAvailable = true;
$hotelSaleOperation->setProductElement($hotelRoomType); // Served from RoomType Stock
$hotelSaleOperation->setProduct($hotelContract);
} else { // Check the General Stock
$generalStock = $availableHotels[0]['sources'][0]['generalStock'];
if ($generalStock >= 1) {
$roomIsAvailable = true;
$generalStock--;
$availableHotels[0]['sources'][0]['generalStock'] = $generalStock; // Update General Stock
$hotelSaleOperation->setProductElement(null); // Served from General Stock
$hotelSaleOperation->setProduct($hotelContract);
}
}
if ($roomIsAvailable) { // Complete information about "sale" stock operation (-1) and associate it to the orderSubLine
$hotelSaleOperation->setOperation(-1);
$hotelSaleOperation->setNumberItems(1);
$hotelSaleOperation->setStartDate($hotelReq->getCheckInDate());
$hotelSaleOperation->setEndDate($hotelReq->getLastNightDate());
$hotelSaleOperation->setCustomerTo($order->getCustomer());
$hotelOrderSubLine->setDebit($hotelSaleOperation);
$hotelOrderSubLine->setState(OrderStateEnum::AVAILABLE->getValue());
} else {
$hotelOrderSubLine->setState(OrderStateEnum::NOT_AVAILABLE->getValue());
}
$hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
$hotelIsAvailable = $hotelIsAvailable && $roomIsAvailable;
// Parties of this hrta
if (array_key_exists('parties', $hrta)) {
// check fail condition
if (count($hrta['parties']) != $hotelReq->getNbParties($index)) {
return null;
}
foreach ($hrta['parties'] as $extra) {
$partyZone = $extra['zones'][0]; // get the selected zone
// get entity partyZone
$partyZoneEntity = $this->partyZoneRepository->find($partyZone['id']);
$partyZoneDescription = $partyZoneEntity->getProduct()->getTitle() . ' ' . $partyZoneEntity->getZone()->getTitle();
$partyZoneOrderSubLine = new OrderSubLine();
$partyZoneOrderSubLine->setProductElement($partyZoneEntity);
$partyZoneOrderSubLine->setService(ServiceEnum::party->getValue());
$partyZoneOrderSubLine->setDescription($partyZoneDescription);
$partyZoneOrderSubLine->setSalePrice($partyZone['salePrice']);
$partyZoneOrderSubLine->setPurchasePrice($partyZone['purchasePrice']);
$partyZoneOrderSubLine->setNbAdult($hotelReq->getNbPersonGen('adult', $index));
$partyZoneOrderSubLine->setNbChild($hotelReq->getNbPersonGen('child', $index));
$partyZoneOrderSubLine->setCancelable(false);
$partiesOrderSubLines[$extra['id']][] = $partyZoneOrderSubLine; // Grouped by $partyID
$partyZoneSaleOperation = new ProductElementStockOperation();
$partyZoneSaleOperation->setProductElement($partyZoneEntity);
$partyZoneSaleOperation->setProduct($partyZoneEntity->getProduct());
$partyZoneSaleOperation->setNumberItems($hotelReq->getNbPerson($index));
$partyZoneSaleOperation->setOperation(-1);
$partyZoneSaleOperation->setStartDate($partyZoneEntity->getProduct()->getDate());
$partyZoneSaleOperation->setEndDate($partyZoneEntity->getProduct()->getDate());
$partyZoneSaleOperation->setCustomerTo($order->getCustomer());
if (!key_exists($partyZoneEntity->getId(), $places_put_aside)) {
$places_put_aside[$partyZoneEntity->getId()] = 0;
}
$nbPartyZonePlacesRequested = $places_put_aside[$partyZoneEntity->getId()] + $hotelReq->getNbPerson($index);
if ($nbPartyZonePlacesRequested > $partyZone['availableQuantity']) {
$partyZoneOrderSubLine->setState(OrderStateEnum::NOT_AVAILABLE->getValue());
} else {
$partyZoneOrderSubLine->setState(OrderStateEnum::AVAILABLE->getValue());
$partyZoneOrderSubLine->setDebit($partyZoneSaleOperation); // this can be set null if the hotel is not available or at least one partyZone isn't available.
$places_put_aside[$partyZoneEntity->getId()] += $hotelReq->getNbPerson($index);
}
}
}
}
$deadlineFlag = $this->getDeadlineFromRooms($rooms);
if ($deadlineFlag instanceof DateTime) {
$hotelOrderLine->setDeadline($deadlineFlag);
}
$hotelOrderLine->setState(
$hotelIsAvailable
? OrderStateEnum::CONFIRMED->getValue()
: OrderStateEnum::PENDING->getValue()
);
if ($apply_fee) {
$product_fee = $this->frontService->getProductFee(ModuleEnum::hotel->getValue(), $order->getCustomer());
if ($product_fee >= 0) {
$hotelOrderSubLineFee = new OrderSubLine();
$hotelOrderSubLineFee->setService(ServiceEnum::product_fee->getValue());
$hotelOrderSubLineFee->setDescription(ServiceEnum::product_fee->getValue());
$hotelOrderSubLineFee->setPurchasePrice($product_fee);
$hotelOrderSubLineFee->setSalePrice($product_fee);
$hotelOrderSubLineFee->setCancelable(false);
$hotelOrderLine->addOrderSubLine($hotelOrderSubLineFee);
}
}
$order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
// Check All Parties are available, Otherwise, set all debit as NULL
$all_parties_available = true;
$partiesOrderLines = []; // get all the partyOrderlines to send to emailService
foreach ($partiesOrderSubLines as $partyID => $partyOrderSubLines) {
$partyEntity = $this->partyRepository->find($partyID);
$partyOrderLine = new OrderLine();
$partyOrderLine->setProduct($partyEntity);
$partyIsAvailable = true;
foreach ($partyOrderSubLines as $partyOrderSubLine) {
$partyOrderLine->addOrderSubLine($partyOrderSubLine);
if ($partyOrderSubLine->getState() == OrderStateEnum::NOT_AVAILABLE->getValue()) {
$partyIsAvailable = false;
}
}
$all_parties_available = $all_parties_available && $partyIsAvailable;
$partyOrderLine->setDescription($partyEntity->getTitle());
$partyOrderLine->setStartDate($partyEntity->getDate());
$partyOrderLine->setEndDate($partyEntity->getDate());
$partyOrderLine->setState($partyIsAvailable ? OrderStateEnum::AVAILABLE->getValue() :
OrderStateEnum::NOT_AVAILABLE->getValue());
$partyOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
$partyOrderLine->setHotelName($partyEntity->getHotelContract()->getHotel()->getName());
$partyOrderLine->setCity($partyEntity->getHotelContract()->getHotel()->getCity()->getName());
$partyOrderLine->setCountry($partyEntity->getHotelContract()->getHotel()->getCity()->getCountry()->getName());
$order->addOrderLine($partyOrderLine); // ### Add Party OrderLine ###
$partiesOrderLines[] = $partyOrderLine;
}
// If hotel is NotAvailable, or at least one party is NA => Do not associate ALL DEBIT stock operation
// if (!$hotelIsAvailable or !$all_parties_available) {
// $hotelOrderLine->dissociateDebitOperations();
// }
return $hotelOrderLine;
} catch (Exception $e) { // error in RateKeys
//dd($e);
return null;
}
}
/**
* @throws Exception
*/
private function checkRateKey(mixed $parameters): HotelAvailabilityRequest
{
$RATE_KEY_NB_ELEMENTS_MIN = 8;
$hotelReq = null;
foreach ($parameters['rooms'] as $index => $room) {
$room_rate_key_array = explode('|', $room['rateKey']); // TODO decode rate_key
if (sizeof($room_rate_key_array) < $RATE_KEY_NB_ELEMENTS_MIN) {
throw new Exception("Erroneous check rate key : size");
}
if ($hotelReq == null) {
$hotelReq = HotelAvailabilityRequest::constructWithHrtaRateKey($room_rate_key_array);
} else {
$hotelReq->addHrtaRateKey($room_rate_key_array);
}
//dd('check rate key OKKK');
$room_parties_str = $hotelReq->getParties()[$index];
//dd($room_parties_str);
if (!is_null($room_parties_str)) {
$room_parties_ids = explode(',', $room_parties_str);
$nb_parties_for_this_room = count($room_parties_ids); // extracted from room_rate_key
$rate_keys_of_parties_for_this_room = $room['parties'] ?? []; // extracted from the check_rate_parties
$nb_rate_keys_of_parties_for_this_room = count($rate_keys_of_parties_for_this_room);
if ($nb_rate_keys_of_parties_for_this_room == 0) {
$hotelReq->setValid(false); // only ratekey for room is specified
} else {
if ($nb_parties_for_this_room != $nb_rate_keys_of_parties_for_this_room) {
throw new Exception("Erroneous check rate key : parties error");
}
}
//dd($rate_keys_of_parties_for_this_room);
$room_partiesZones_str = implode(',', $rate_keys_of_parties_for_this_room);
$hotelReq->setPartiesZonesAtIndex($index, $room_partiesZones_str);
}
}
return $hotelReq;
}
public function isSearchCodeValid($fullSearchCode, $SEARCH_TIME = 30): bool
{
if ($fullSearchCode == "ADMIN_ACCESS")
return true; // from admin area
// Find the position of the "@" character in the string
$atPosition = strpos($fullSearchCode, '@');
// Extract the substring before "@" if "@" exists (That corresponds to the searchCode_local)
if ($atPosition !== false) {
$searchCode_local = substr($fullSearchCode, 0, $atPosition);
} else {
$searchCode_local = $fullSearchCode;
}
$SEARCH_TIME = 30; // 30 minutes
$searchApiLogEntry = $this->apiLogRepository->findOneBy(['searchCode' => $searchCode_local, 'endpoint' => 'availability']);
if ($searchApiLogEntry == null) {
return false;
} else {
$elapsedTimeFromSearchInMinutes = $searchApiLogEntry->getDate()->diff(new DateTime())->i;
if ($elapsedTimeFromSearchInMinutes > $SEARCH_TIME) {
return false;
} else {
$apiLogEntryBook = $this->apiLogRepository->findOneBy(['searchCode' => $searchCode_local, 'endpoint' => 'book', 'statusCode' => 200]);
if ($apiLogEntryBook) { // a successfully-book is already saved for this search
return false;
}
return true;
}
}
}
public function cancelFromLocal(int $bookingId): array
{
$orderLine = $this->orderLineRepository->find($bookingId);
if ($orderLine->getState() == OrderStateEnum::CANCELED->getValue()) {
return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_ORDERLINE_ALREADY_CANCELED);
}
if (!$orderLine->isCancelable()) {
return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_NOT_CANCELLABLE);
}
$cancellation_fee = 0;
// Cancellation of CONFIRMED orderLine
if ($orderLine->getState() == OrderStateEnum::CONFIRMED->getValue()) {
$orderLine->setState(OrderStateEnum::CANCELED->getValue());
$applyFee = true;
// remove debits operations and calculate cancellation fee
foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
$debit = $orderSubLine->getDebit();
if ($debit) {
$debit->setProductElement(null);
$this->entityManager->remove($debit);
}
$orderSubLine->setDebit(null);
$orderSubLine->setState(OrderStateEnum::CANCELED->getValue());
$this->entityManager->persist($orderSubLine);
if ($applyFee) {
$cancellation_fee += $orderSubLine->getCurrentCancellationAmount();
}
if ($cancellation_fee > 0) { // Generate a new REFERENCE (N°DOSSIER)
$orderLine->setState(OrderStateEnum::CANCELED_FEE->getValue());
$orderSubLine_cancellation = new OrderSubLine();
$orderSubLine_cancellation->setSalePrice($cancellation_fee);
$orderSubLine_cancellation->setPurchasePrice($cancellation_fee);
$orderSubLine_cancellation->setService(ServiceEnum::cancellation->getValue());
$description = $this->translator->trans('Entities.OrderSubLine.Message.CancellationAmount') . " " . $orderLine->getReference();
$orderSubLine_cancellation->setDescription($description);
$orderSubLine_cancellation->setCancelable(true);
$orderLine->addOrderSubLine($orderSubLine_cancellation);
}
}
$this->entityManager->persist($orderLine);
$this->entityManager->flush();
return [
'bookingId' => $bookingId,
'canceled' => true,
'message' => CANCEL_SUCCESS,
'cancellationFee' => 0
];
}
$productType = get_class($orderLine->getProduct()); // HotelContract, Party, ...
if ($productType != 'App\Entity\HotelContract') {
return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_PRODUCT_ERROR);
}
// Init Dates
$today = new DateTime();
$today->setTime(0, 0, 0);
$releaseDate = $orderLine->getStartDate();
$remainingDaysInterval = date_diff($today, $releaseDate); // Interval before release date.
if ($remainingDaysInterval->invert == 1) { // checkin data exceeded (negative interval)
return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_RELEASE_DATE_EXCEEDED);
}
if (!$orderLine->isCancelable()) {
return $this->getHotelApiCancelErrorArray($bookingId, CANCEL_ERROR_NOT_CANCELLABLE);
}
$deadlineDate = $orderLine->getDeadline();
$orderLineCancellationFee = 0;
//dd($deadlineDate);
if ($deadlineDate != null and $today > $deadlineDate) {
// No Free cancellation, calculate fee here
foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
$osl_cancellation_fees_jsonString = $orderSubLine->getCancellationFeesJson();
$osl_cancellation_fees = json_decode($osl_cancellation_fees_jsonString, true);
$closestDateDiff = PHP_INT_MAX;
$closest_cancellation_fee = null;
foreach ($osl_cancellation_fees as $cancellation_fee) {
$cancellation_fee_date_from = DateTime::createFromFormat('Y-m-d', $cancellation_fee['from']);
$dateDiff = $cancellation_fee_date_from->diff($today)->days;
if ($dateDiff < $closestDateDiff) {
$closestDateDiff = $dateDiff;
$closest_cancellation_fee = $cancellation_fee;
}
}
$orderLineCancellationFee += $closest_cancellation_fee['fee'];
}
$cancellationOrderSubLine = new OrderSubLine();
$cancellationOrderSubLine->setState(OrderStateEnum::CONFIRMED->getValue()); // Cancellation is confirmed
$cancellationOrderSubLine->setProductElement(null);
$cancellationOrderSubLine->setDescription(ServiceEnum::cancellation->getValue());
$cancellationOrderSubLine->setSalePrice($orderLineCancellationFee);
$cancellationOrderSubLine->setPurchasePrice($orderLineCancellationFee);
$cancellationOrderSubLine->setService(ServiceEnum::cancellation->getValue());
$cancellationOrderSubLine->setCancelable(false);
$orderLine->addOrderSubLine($cancellationOrderSubLine);
}
$orderLine->setState(OrderStateEnum::CANCELED->getValue());
$this->orderLineRepository->save($orderLine, true);
return [
'bookingId' => $bookingId,
'canceled' => true,
'message' => CANCEL_SUCCESS,
'cancellationFee' => $orderLineCancellationFee
];
}
public function cancelSublineFromLocal(int $sublineId): array
{
$orderSubLine = $this->orderSublineRepository->find($sublineId);
if ($orderSubLine->getOrderLine()->getState() == OrderStateEnum::CANCELED->getValue()) {
return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_ORDERLINE_ALREADY_CANCELED);
}
if (!$orderSubLine->isCancelable()) {
return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_NOT_CANCELLABLE);
}
$productType = get_class($orderSubLine->getOrderLine()->getProduct()); // HotelContract, Party, ...
if ($productType != 'App\Entity\HotelContract') {
return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_PRODUCT_ERROR);
}
// Init Dates
$today = DateTime::createFromFormat("Y-m-d", date("Y-m-d"));
$releaseDate = $orderSubLine->getOrderLine()->getStartDate();
$remainingDaysInterval = date_diff($today, $releaseDate); // Interval before release date.
/* if ($remainingDaysInterval->invert == 1) { // checkin data exceeded (negative interval)
return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_RELEASE_DATE_EXCEEDED);
}
if (!$orderSubLine->getOrderLine()->isCancelable()) {
return $this->getHotelApiCancelErrorArray($sublineId, CANCEL_ERROR_NOT_CANCELLABLE);
}*/
$deadlineDate = $orderSubLine->getOrderLine()->getDeadline();
$orderSubLineCancellationFee = 0;
if ($deadlineDate != null and $today > $deadlineDate) {
// No Free cancellation, calculate fee here
$osl_cancellation_fees_jsonString = $orderSubLine->getCancellationFeesJson();
$osl_cancellation_fees = json_decode($osl_cancellation_fees_jsonString, true);
$closestDateDiff = PHP_INT_MAX;
$closest_cancellation_fee = null;
if ($osl_cancellation_fees) {
foreach ($osl_cancellation_fees as $cancellation_fee) {
$cancellation_fee_date_from = DateTime::createFromFormat('Y-m-d', $cancellation_fee['from']);
$dateDiff = $cancellation_fee_date_from->diff($today)->days;
if ($dateDiff < $closestDateDiff) {
$closestDateDiff = $dateDiff;
$closest_cancellation_fee = $cancellation_fee;
}
}
$orderSubLineCancellationFee += $closest_cancellation_fee['fee'];
}
if ($orderSubLineCancellationFee > 0) {
$cancellationOrderSubLine = new OrderSubLine();
$cancellationOrderSubLine->setState(OrderStateEnum::CANCELED->getValue()); // Cancellation is confirmed
$cancellationOrderSubLine->setProductElement(null);
$cancellationOrderSubLine->setDescription(ServiceEnum::cancellation->getValue() . ' : ' . $orderSubLine->getDescription());
$cancellationOrderSubLine->setSalePrice($orderSubLineCancellationFee);
$cancellationOrderSubLine->setPurchasePrice($orderSubLineCancellationFee);
$cancellationOrderSubLine->setService(ServiceEnum::cancellation->getValue());
$cancellationOrderSubLine->setCancelable(false);
$orderSubLine->getOrderLine()->addOrderSubLine($cancellationOrderSubLine);
}
}
$orderSubLine->setState(OrderStateEnum::CANCELED->getValue());
$this->orderSublineRepository->save($orderSubLine, true);
return [
'refId' => $sublineId,
'canceled' => true,
'message' => CANCEL_SUCCESS,
'cancellationFee' => $orderSubLineCancellationFee
];
}
public function cancelFromHUB(int $bookingId): array
{
$orderLine = $this->orderLineRepository->find($bookingId);
$hub_cancel_request_array = [
"bookingId" => $orderLine->getExternalOrderId(),
"bookingSource" => $orderLine->getProduct()->getSource()
];
try {
$apiResponse = $this->helpers->getApiResponse(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_CANCEL_ENDPOINT,
api_key: $this->parameterService->getHotelHubApiKey(),
request_body: $hub_cancel_request_array
);
$apiResponseContent = $apiResponse['response'];
$error = $apiResponse['error'];
$message = $apiResponse['msg'];
if ($error || !$apiResponseContent['canceled']) {
return [
'bookingId' => $bookingId,
'canceled' => false,
'message' => $message,
'cancellationFee' => 0
];
}
} catch (Exception $e) {
return [
'bookingId' => $bookingId,
'canceled' => false,
'message' => $e->getMessage(),
'cancellationFee' => 0
];
}
/* ***** cancellation process ******** */
$orderLineCancellationFee = $apiResponseContent['cancellationFee'];
if ($orderLineCancellationFee > 0) {
// No Free cancellation, calculate fee here
$cancellationOrderSubLine = new OrderSubLine();
$cancellationOrderSubLine->setState(OrderStateEnum::CONFIRMED->getValue()); // Cancellation is confirmed
$cancellationOrderSubLine->setProductElement(null);
$cancellationOrderSubLine->setDescription(ServiceEnum::cancellation->getValue());
$cancellationOrderSubLine->setSalePrice($orderLineCancellationFee);
$cancellationOrderSubLine->setPurchasePrice($orderLineCancellationFee);
$cancellationOrderSubLine->setService(ServiceEnum::cancellation->getValue());
$cancellationOrderSubLine->setCancelable(false);
$orderLine->addOrderSubLine($cancellationOrderSubLine);
}
$orderLine->setState(OrderStateEnum::CANCELED->getValue());
foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
$orderSubLine->setState(OrderStateEnum::NOT_BOOKED->getValue());
}
$this->orderLineRepository->save($orderLine, true);
return [
'bookingId' => $bookingId,
'canceled' => true,
'message' => CANCEL_SUCCESS,
'cancellationFee' => $orderLineCancellationFee
];
}
private function getHotelApiCancelErrorArray(int $bookingId, $message): array
{
return [
'bookingId' => $bookingId,
'canceled' => false,
'message' => $message,
'cancellationFee' => 0
];
}
private function getCancellationFee($cancellationPolicy, float $osl_sale_price, int $nbStay): float
{
switch ($cancellationPolicy->getFeeType()) {
case "percent":
$fee = $osl_sale_price * $cancellationPolicy->getFeeValue() / 100.0;
if ($cancellationPolicy->getFeeBase() == "night")
$fee = $fee / $nbStay;
break;
case "amount": // period
$fee = $cancellationPolicy->getFeeValue();
break;
}
//todo alert if there is a different case.
return $fee;
}
/**
* @throws Exception
*/
public function normalizeSearchResultsFromHubFormat(
mixed $apiResults,
string $requestCurrency,
string $localSearchCode,
?string $tokenForBook = null,
bool $with_mapping = true,
bool $isFirstOccurrenceRoom = false
): array
{
$xml_api_entities = $this->xmlApiRepository->findAll();
$xml_api_names = [];
foreach ($xml_api_entities as $xml_api_entity) {
$xml_api_names[$xml_api_entity->getSource()] = $xml_api_entity->getName();
}
$hotelXmlMap = [];
if ($with_mapping) {
$hotelUids = [];
foreach ($apiResults as $hubResult) {
$hotelUids[] = $hubResult['source'] . '@' . $hubResult['hotel']['hotelUid'];
}
$hotelXmls = $this->hotelXmlRepository->findByCodesWithHotelAndBadges($hotelUids);
foreach ($hotelXmls as $hotelXml) {
$hotelXmlMap[$hotelXml->getCode()] = $hotelXml;
}
}
// --- Chargement du cache ville (une seule requête, une seule fois par batch) ---
// Une seule ville par batch => on prend le cityName du premier résultat
$cityName = $apiResults[0]['hotel']['cityName'] ?? null;
$hotelCacheMap = [];
if (!empty($cityName)) {
$hotelCacheRow = $this->hotelCacheRepository->findOneBy(['cityKey' => strtoupper($cityName)]);
if ($hotelCacheRow !== null) {
$rawData = $hotelCacheRow->getHotelsJson();
if (is_string($rawData)) {
$decoded = json_decode($rawData, true);
$rawData = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
}
if (is_array($rawData)) {
foreach ($rawData as $cacheEntry) {
if (!isset($cacheEntry['name'])) {
continue;
}
$normalizedKey = $this->normalizeHotelNameKey($cacheEntry['name']);
$hotelCacheMap[$normalizedKey] = $cacheEntry;
}
}
}
}
// Cache des taux de change pour éviter les recalculs redondants sur une même devise
$exchangeRateCache = [];
$availableHotels = [];
foreach ($apiResults as $hub_result_item) {
$searchCodeHub = $hub_result_item['searchCode'];
$sourceId = $hub_result_item['source'];
$hub_hotelId = $hub_result_item['hotel']['hotelId'];
$hub_supplierSourceId = $hub_result_item['hotel']['supplierSourceId'];
$hub_cityId = $hub_result_item['hotel']['cityId'];
// hotelKey au format sourceId|supplierId|hubHotelId|hubCityId
$hotelKey = $sourceId . '|' . $hub_supplierSourceId . '|' . $hub_hotelId . '|' . $hub_cityId;
$hotelUid = $sourceId . '@' . $hub_result_item['hotel']['hotelUid'];
$hotel = $with_mapping ? ($hotelXmlMap[$hotelUid] ?? null)?->getHotel() : null;
$normalizedHotelName = $this->normalizeHotelNameKey($hub_result_item['hotel']['hotelName'] ?? '');
$cacheData = $hotelCacheMap[$normalizedHotelName] ?? null;
if (!isset($availableHotels[$hotelKey]['hotel'])) {
$hub_hotel_image = $hub_result_item['hotel']['thumbImage']
?: ($cacheData['thumbImage'] ?? null)
?: ($cacheData['image'] ?? null);
$availableHotels[$hotelKey]['hotel'] = [
'id' => $hotelKey,
'hotelUid' => $hotelUid,
'name' => $hotel?->getName() ?? $hub_result_item['hotel']['hotelName'],
'rating' => $hub_result_item['hotel']['ratingId'],
'city' => $hub_result_item['hotel']['cityName'] ?: "City",
'cityId' => $hub_cityId,
'country' => $hub_result_item['hotel']['countryName'] ?: "Country",
'countryId' => $hub_result_item['hotel']['countryId'],
'zone' => $hub_result_item['hotel']['zoneName'] ?? $hub_result_item['hotel']['cityName'],
'latitude' => $hotel?->getLatitude() ?? $cacheData['latitude'] ?? $hub_result_item['hotel']['latitude'],
'longitude' => $hotel?->getLongitude() ?? $cacheData['longitude'] ?? $hub_result_item['hotel']['longitude'],
'address' => $hub_result_item['hotel']['address'] ?: ($cacheData['address'] ?? ""),
'image' => $hotel?->getPrimaryImageUrl() ?? $hub_hotel_image,
'gallery' => array_map(fn($img) => $img['path'], $hub_result_item['gallery'] ?? []),
'amunities' => is_array($hub_result_item['hotel']['amunities'] ?? null)
&& !empty($hub_result_item['hotel']['amunities'])
? array_slice($hub_result_item['hotel']['amunities'], 0, 4)
: (is_array($cacheData['facilities'] ?? null) ?
array_slice($cacheData['facilities'], 0, 4) : []),
'promotionTitle' => trim($hub_result_item['promotionTitle']),
];
if ($isFirstOccurrenceRoom) {
$availableHotels[$hotelKey]['hotel']['unnormalizedHotelData'] =
base64_encode(gzcompress(serialize($hub_result_item)));
}
}
$sourceEntry = [
'xmlSourceId' => $sourceId,
'xmlSourceName' => $xml_api_names[$sourceId] ?? null,
'sourceId' => $sourceId,
'sourceType' => 'external',
'associationRequired' => $hub_result_item['associationIdRequired'] === true,
];
if ($tokenForBook !== null) { // $tokenForBook existe uniquement pour CheckRate (pas Availability)
$sourceEntry['tokenForBook'] = $tokenForBook;
$searchCodeHub = $tokenForBook . TOKEN_FOR_BOOK_SEPARATOR . $searchCodeHub;
}
$sourceEntry['searchCode'] = $localSearchCode . '@' . $searchCodeHub;
$rooms = [];
foreach ($hub_result_item['rooms'] as $index => $hub_room_index) {
$rooms_at_index = [];
$hub_room_index_normalized = $isFirstOccurrenceRoom
? array_slice($hub_room_index, 0, 1, true)
: $hub_room_index;
foreach ($hub_room_index_normalized as $hub_hrta) {
$currencyPurchase = $hub_hrta['currencyPurchase'];
// Réutilisation du taux de change déjà calculé pour cette paire de devises
$rateCacheKey = $currencyPurchase . '->' . $requestCurrency;
if (!isset($exchangeRateCache[$rateCacheKey])) {
$exchangeRateCache[$rateCacheKey] = $this->currencyService->calculateExchangeRate(
$currencyPurchase,
$requestCurrency
);
}
$exchangeRate = $exchangeRateCache[$rateCacheKey];
$ratekey = $sourceId . '@' . $hub_hrta['rateKey'];
$requestedRateKey = array_key_exists('requestedRateKey', $hub_hrta)
? $sourceId . '@' . $hub_hrta['requestedRateKey']
: "";
$cancellationsFees = [];
foreach ($hub_hrta['cancellationPolicies'] as $hubCancellationPolicy) {
// NB: ceci écrase à chaque itération -> ne garde que la dernière police.
// Si plusieurs paliers doivent être conservés, utiliser $cancellationsFees[] = [...]
$cancellationsFees['from'] = $hubCancellationPolicy['from'];
$cancellationsFees['fee'] = $hubCancellationPolicy['amount'];
}
$rooms_at_index[] = [
'boardId' => $hub_hrta['code'],
'boardCode' => $hub_hrta['boardCode'],
'boardXmlName' => $hub_hrta['boardName'],
'boardName' => $hub_hrta['boardName'],
'roomId' => $hub_hrta['code'],
'roomName' => $hub_hrta['name'],
'purchaseCurrency' => $currencyPurchase,
'purchaseRate' => $hub_hrta['purchase'],
'saleRate' => $hub_hrta['price'] * $exchangeRate,
'salePromoRate' => $hub_hrta['price'] * $exchangeRate,
'saleCurrency' => $requestCurrency,
'stopSale' => $hub_hrta['rateClass'] === "SS",
'available' => $hub_hrta['available'],
'rateKey' => $ratekey,
'requestedRateKey' => $requestedRateKey,
'NRF' => $hub_hrta['rateClass'] === 'NRF',
'deadline' => $hub_result_item['deadline'] ?: null,
'notes' => $hub_hrta['notes'],
'cancellationsFees' => $cancellationsFees,
];
}
$rooms[$index] = $rooms_at_index;
}
$sourceEntry['rooms'] = $rooms;
$options = [];
if (array_key_exists('options', $hub_result_item)) {
foreach ($hub_result_item['options'] as $hub_result_item_option) {
$options[] = $hub_result_item_option['title'];
}
}
$sourceEntry['options'] = $options;
$sourceEntry['promos'] = $this->getHotelBadges($hotel, HotelBadgeEnum::PROMO->getValue());
$sourceEntry['discounts'] = $this->getHotelBadges($hotel, HotelBadgeEnum::DISCOUNT->getValue());
$sourceEntry['infos'] = $this->getHotelBadges($hotel, HotelBadgeEnum::INFO->getValue());
$sourceEntry['facilities'] = $this->getHotelFacilities($hotelUid);
$sourceEntry['generalStock'] = 0; // Todo Stock
$availableHotels[$hotelKey]['sources'][$sourceId] = $sourceEntry;
}
return $availableHotels;
}
private function normalizeHotelDetailFromHubFormat($hotelDetailHub, string $hotelId): array
{
$hotelUid = $hotelDetailHub['source'].'@'.$hotelId;
$hotelXml = $this->hotelXmlRepository->findOneBy(['code' => $hotelUid]);
if ($hotelXml == null) {
$hotel = null;
} else {
$hotel = $hotelXml->getHotel();
}
$images = [];
if ($hotel && count($hotel->getImages()) > 0) {
foreach ($hotel->getImages() as $image) {
$images[] = [
'alt' => $image->getTitle(),
'url' => $image->getUrl(),
];
}
} else {
foreach ($hotelDetailHub['gallery'] as $hub_image) {
$images[] = [
'alt' => $hub_image['title'],
'url' => $hub_image['path']
];
}
}
$facilities = [];
if ($hotel && count($hotel->getFacilities()) > 0) {
foreach ($hotel->getFacilities() as $facility) {
$facilities[] = [
'name' => $facility->getName(),
'icon' => $facility->getIcon(),
];
}
} else {
foreach ($hotelDetailHub['amunities'] as $amenity) {
$facilities[] = [
'name' => $amenity['title'],
'icon' => $amenity['icon_class']
];
}
}
return [
"hotelId" => $hotelId,
"name" => ($hotel && $hotel->getName() != '') ? $hotel->getName() : $hotelDetailHub['hotel']['hotelName'],
"hotelType" => ($hotelDetailHub['hotel']['hotelType'] != null ? $hotelDetailHub['hotel']['hotelType'] : ''),
"starRating" => ($hotel && $hotel->getStarRating() != '') ? $hotel->getStarRating() : $hotelDetailHub['hotel']['rating'],
"ratingId" => ($hotel && $hotel->getStarRating() != '') ? $hotel->getStarRating() : $hotelDetailHub['hotel']['ratingId'],
"country" => $hotelDetailHub['hotel']['countryName'],
"address" => $hotelDetailHub['hotel']['address'],
"city" => $hotelDetailHub['hotel']['cityName'],
"latitude" => ($hotel) ? (($hotel->getLatitude()) ?? $hotel->getCity()->getLatitude()) : $hotelDetailHub['hotel']['latitude'],
"longitude" => ($hotel) ? (($hotel->getLongitude()) ?? $hotel->getCity()->getLongitude()) : $hotelDetailHub['hotel']['longitude'],
"thumbImage" => ($hotel && $hotel->getPrimaryImageUrl() != '') ? $hotel->getPrimaryImageUrl() : $hotelDetailHub['hotel']['thumbImage'],
"description" => ($hotel && $hotel->getDescription() != '') ? $hotel->getDescription() : $hotelDetailHub['hotel']['description'],
"phone" => ($hotel && $hotel->getPhone() != '') ? $hotel->getPhone() : $hotelDetailHub['hotel']['phone'],
"website" => '',
"options" => [],
"tripadvisorId" => "",
"facilities" => $facilities,
"images" => $images,
'promos' => [], //TODO get promos from badges
'seo' => [
'title' => ($hotel && $hotel->getSeoTitle() != '') ? $hotel->getSeoTitle() : $hotelDetailHub['hotel']['hotelName'],
'description' => ($hotel && $hotel->getSeoDescription() != '') ? $hotel->getSeoDescription() : $hotelDetailHub['hotel']['hotelName'],
]
];
}
/**
* Performs an HttpRequest to HotelApi:Availability
*/
public function query_ApiHotel_Availability($auth_data, $requestJson, $xmlSourceId, $base_uri, bool $mapping = true): array
{
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
// 'currency' => $currency,
'user' => $auth_data['user'],
'timestamp' => $auth_data['timestamp'],
'signature' => hash('sha256', $auth_data['signature'])
];
if (!is_null($xmlSourceId)) {
$headers['xmlSourceId'] = $xmlSourceId;
}
$requestJson['mapping'] = $mapping;
$data['content'] = [];
try {
$httpClient = HttpClient::create();
$response = $httpClient->request('POST', $base_uri . '/api/hotel/availability', [
'headers' => $headers,
'json' => $requestJson,
'verify_peer' => false,
'verify_host' => false
]);
$data = json_decode($response->getContent(), true);
} catch (Exception $e) {
$data['error'] = [$e->getMessage()];
$data['content'] = [];
}
return $data;
}
/**
* Lance une requête HTTP async (non bloquante) vers l'API hotel.
* Retourne un ResponseInterface Symfony.
*/
public function query_ApiHotel_Availability_Async(
$auth_data,
$requestJson,
$xmlSourceId,
$base_uri,
HttpClientInterface $httpClient,
bool $mapping = true
): ResponseInterface {
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'user' => $auth_data['user'],
'timestamp' => $auth_data['timestamp'],
'signature' => hash('sha256', $auth_data['signature']),
];
if (!is_null($xmlSourceId)) {
$headers['xmlSourceId'] = $xmlSourceId;
}
$requestJson['mapping'] = $mapping;
// request() sans ->getContent() = non-bloquant
return $httpClient->request('POST', $base_uri . '/api/hotel/availability', [
'headers' => $headers,
'json' => $requestJson,
'verify_peer' => false,
'verify_host' => false,
]);
}
public function hotelDetailsHUB(string $hotelUId): array
{
$parameters = [
'hotelUid' => $hotelUId
];
try {
$apiResponse = $this->helpers->getApiResponse(
'POST',
$this->parameterService->getHotelHubUrl(),
HUB_HOTEL_API_DETAILS_ENDPOINT,
$this->parameterService->getHotelHubApiKey(),
$parameters
);
if (key_exists('response', $apiResponse)) {
$hotelDetailHub = $apiResponse['response'];
return $this->normalizeHotelDetailFromHubFormat($hotelDetailHub, $hotelUId);
}
} catch (Exception $e) {
return [$e->getMessage()];
}
return [];
}
public function hotelDetailsHUBAsync(
string $hotelId
): ResponseInterface {
$parameters = [
'hotelUid' => $hotelId
];
return $this->httpClient->request(
'POST',
$this->parameterService->getHotelHubUrl() .
HUB_HOTEL_API_DETAILS_ENDPOINT,
[
'headers' => [
'api-key' => $this->parameterService->getHotelHubApiKey(),
'Content-Type' => 'application/json',
],
'json' => $parameters,
'timeout' => 30,
'max_duration' => 30,
'max_redirects' => 0,
]
);
}
public function streamResponses(array $responses): \Generator
{
$responseMap = [];
foreach ($responses as $hotelId => $response) {
if ($response instanceof ResponseInterface) {
$responseMap[spl_object_id($response)] = $hotelId;
}
}
try {
foreach ($this->httpClient->stream($responses, 30) as $response => $chunk) {
$hotelId = $responseMap[spl_object_id($response)] ?? null;
if (!$hotelId) {
continue;
}
if ($chunk->isTimeout()) {
$this->logger->warning('HUB details timeout', [
'hotelId' => $hotelId,
'url' => $response->getInfo('url'),
]);
$response->cancel();
yield $hotelId => null;
continue;
}
if (!$chunk->isLast()) {
continue;
}
try {
$statusCode = $response->getStatusCode();
$content = $response->getContent(false);
if ($statusCode !== 200) {
$this->logger->error('HUB details non-200', [
'hotelId' => $hotelId,
'status' => $statusCode,
'url' => $response->getInfo('url'),
'body' => mb_substr($content, 0, 1000),
]);
yield $hotelId => null;
continue;
}
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->logger->warning('HUB invalid json', [
'hotelId' => $hotelId,
'error' => json_last_error_msg(),
'body' => mb_substr($content, 0, 500),
]);
yield $hotelId => null;
continue;
}
if (isset($data['response'])) {
yield $hotelId => $this->normalizeHotelDetailFromHubFormat(
$data['response'],
$hotelId
);
} else {
yield $hotelId => $data;
}
} catch (\Throwable $e) {
$this->logger->error('streamResponses_item_error', [
'hotelId' => $hotelId,
'class' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
yield $hotelId => null;
}
}
} catch (\Throwable $e) {
$this->logger->error('streamResponses_global_error', [
'class' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
}
public function getRelevantDestinationsHub(string $query, $fromAbroad)
{
$parameters = [
"termSearch" => $query,
"isEtranger" => $fromAbroad
];
try {
//http://192.168.10.20/hubchannels/
$apiResponse = $this->helpers->getApiResponse(
'POST',
$this->parameterService->getHotelHubUrl(),
HUB_HOTEL_API_AUTOCOMPLETE_ENDPOINT,
$this->parameterService->getHotelHubApiKey(),
$parameters
);
return $apiResponse['response'];
} catch (Exception) {
return []; //$e->getMessage()
}
}
public function getBookDetailsFromOrderline(OrderLine $orderLine): array
{
$rooms = [];
foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
if ($orderSubLine->getService() != ServiceEnum::hotel->getValue())
continue;
$room = [
'rateKey' => $orderSubLine->getRatekey(),
'description' => $orderSubLine->getDescription(),
'adult' => $orderSubLine->getNbAdult(),
'child' => $orderSubLine->getNbChild(),
'cancellation' => json_decode($orderSubLine->getCancellationFeesJson()),
'NRF' => $orderSubLine->isCancelable() == false
];
$rooms[] = $room;
}
return [
'name' => $orderLine->getHotelName(),
'starRating' => $orderLine->getHotelStarRating(),
'city' => $orderLine->getCity(),
'startDate' => $orderLine->getStartDate()->format('Y-m-d'),
'endDate' => $orderLine->getEndDate()->format('Y-m-d'),
'rooms' => $rooms
];
}
public function cancelOrderFromHUB($orderId): array
{
$order = $this->orderRepository->find($orderId);
if (is_null($order)) {
return $this->getHotelApiCancelErrorArray($orderId, "bookingId (order ID) Error");
}
$orderLinesCount = $order->getOrderLines()->count();
if ($orderLinesCount == 0) {
return $this->getHotelApiCancelErrorArray($orderId, "bookingID has no elements to cancel");
}
if ($orderLinesCount > 1) {
return $this->getHotelApiCancelErrorArray($orderId, "bookingID has many elements");
}
// Here is OK (One Order that contains one orderline, so retrieve the orderLine ID and call cancelFromHub
$orderLine = $order->getOrderLines()->get(0);
return $this->cancelFromHUB($orderLine->getId());
}
public function getXmlSourceId(mixed $parameters): ?string
{
$first_ratekey = "";
if (count($parameters['rooms']) > 0) {
$first_ratekey = $parameters['rooms'][0]['rateKey'];
}
$xmlSourceId = 0;
if (str_contains($first_ratekey, '@')) {
return strstr($first_ratekey, '@', true);
}
return $xmlSourceId;
}
/**
* @param $xml_arrangement
* @param $arrangements_array
* @return string
*/
private function getTargetArrangement($xml_arrangement, $arrangements_array): string
{
return $xml_arrangement; // TODO optimize this auto-board_correspondance_function
$xml_arrangement_cleaned = $xml_arrangement;
$arrangements_distance_scores = [];
foreach ($arrangements_array as $arrangement_name) {
$arrangements_distance_scores[$arrangement_name] = levenshtein($xml_arrangement, $arrangement_name); // todo verify more relevant text-similarity functions
}
// Find and return the key with the maximum value in the similarity scores array
return array_search(min($arrangements_distance_scores), $arrangements_distance_scores);
}
/**
* Return a deadline flag (true : FREE, false: NRF, or a datetime)
*
* @param array $rooms
*
* @return mixed
* @throws Exception
*/
public function getDeadlineFromRooms(array $rooms): mixed
{
$cancellation_free = true;
$deadlines = [];
foreach ($rooms as $room) {
// CASE 1: NOT CANCELLABLE ORDER LINE
if ($room['NRF']) {
return false;
}
if ($room['deadline'] != null) {
$deadlines[] = $room['deadline'];
$cancellation_free = false;
}
}
// CASE 2 : FREE CANCELLATION
if ($cancellation_free) {
return true;
}
// CASE 3 : We have $deadlines : we should return the earliest one.
$dateObjects = array_map(function ($dateString) {
return new DateTime($dateString);
}, $deadlines);
// return earliest date using min()
return min($dateObjects);
}
public function getCancellationMessage(mixed $deadline_flag): string
{
if ($deadline_flag === false) {
$cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.WarningNRF');
} elseif ($deadline_flag === true) {
$cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.InfoFreeCancellation');
} else {
$currentDate = new DateTime(); // Current date
$deadlineDate = $deadline_flag;
if ($currentDate > $deadlineDate) {
$cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.WarningCancellationWithFee');
} else { // Before the deadline
$cancellation_message = $this->translator->trans('Entities.OrderLine.Alerts.WarningFreeCancellationBefore' . ' ' . $deadlineDate->format('d/m/Y'));
}
}
return $cancellation_message;
}
public function getBookParameters_dumbo(Request $request): array
{
$nbRooms = $request->get("nbRooms"); // quantity : nbRooms
$beneficiaryName = $request->get('beneficiary_name');
if (empty($beneficiaryName)) {
$beneficiaryFirstName = trim((string) $request->get('beneficiary_first_name', ''));
$beneficiaryLastName = trim((string) $request->get('beneficiary_last_name', ''));
$beneficiaryName = trim($beneficiaryFirstName . ' ' . $beneficiaryLastName);
}
if (empty($beneficiaryName)) {
$beneficiaryName = trim(
(string) $request->get('room_0_pax_0_firstName', '') . ' ' .
(string) $request->get('room_0_pax_0_lastName', '')
);
}
$rooms = [];
for ($room_idx = 0; $room_idx < $nbRooms; $room_idx++) {
/* guests */
$room_guests = [];
$room_adults_count = (int) $request->get("room_" . $room_idx . "_adults_count", 0);
$room_children_count = (int) $request->get("room_" . $room_idx . "_children_count", 0);
$room_guest_count = $room_adults_count + $room_children_count;
for ($guest_index = 0; $guest_index < $room_guest_count; $guest_index++) {
// Try to get per-room per-pax detailed fields
$paxCivility = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_civility");
$paxFirstName = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_firstName");
$paxLastName = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_lastName");
$paxAge = $request->get("room_" . $room_idx . "_pax_" . $guest_index . "_age");
if (!empty($paxFirstName) || !empty($paxLastName)) {
// Use the detailed per-pax data
$guest = [
'civility' => $paxCivility ?? '',
'firstName' => $paxFirstName ?? '',
'lastName' => $paxLastName ?? '',
'age' => $paxAge,
];
} else {
// Fallback: use the primary beneficiary name (backward compatibility)
$rank = $guest_index + 1;
$guest = [
'civility' => '',
'firstName' => $beneficiaryName,
'lastName' => '-' . $rank,
];
}
$room_guests[] = $guest;
}
$room['rateKey'] = $request->get("room_" . $room_idx . "_key");
$room['nbAdult'] = $room_adults_count;
$room['nbChild'] = $room_children_count;
$room['paxes'] = $room_guests;
$rooms[] = $room;
}
// returning book parameters
return [
'rooms' => $rooms,
'beneficiary' => $beneficiaryName,
'comment' => $request->get("comment"),
'options' => $request->get("options"),
'searchCode' => $request->get('searchCode'),
];
}
public function getBookParametersForPeriodChange(OrderLine $orderLine, $startDate, $endDate): array
{
$rooms = [];
foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
if ($orderSubLine->getState() == OrderStateEnum::CANCELED->getValue())
continue;
/* guests */
$room_guests = [];
foreach ($orderSubLine->getBeneficiaries() as $beneficiary) {
$guest = [
'civility' => '',
'firstName' => $beneficiary->getFirstName(),
'lastName' => $beneficiary->getLastName()
];
$room_guests[] = $guest;
}
$room['rateKey'] = $this->getHrtaRateKeyForSpecifiedPeriod($orderSubLine, $startDate, $endDate);
$room['paxes'] = $room_guests;
$rooms[] = $room;
}
// returning book parameters
return [
'rooms' => $rooms,
'comment' => $orderLine->getComment(),
'options' => [], // no-option (already saved in comment)
'searchCode' => "ADMIN_ACCESS",
];
}
public function reCheckRateHub(OrderLine $orderline): array
{
$parameters = $this->getCheckRateParametersFromOrderLine($orderline);
$checkRateResult = $this->checkRateHub($parameters, $orderline->getOrderAtt()->getCustomer());
if (empty($checkRateResult))
return $checkRateResult;
$tokenForBook = $checkRateResult[0]['sources'][0]['tokenForBook'] ?? "";
$orderline->setTokenForBook($tokenForBook);
$this->orderLineRepository->save($orderline, true);
return $checkRateResult[0];
}
public function bookHubOrderline(OrderLine $hotelOrderLine): OrderLine
{
//$hub_searchCode_expanded = substr(strstr($parameters['searchCode'], '@'), 1);
$hub_book_request_array = [];
$hub_book_request_array['searchCode'] = $hotelOrderLine->getSearchCode();
$hub_book_request_array['tokenForBook'] = $hotelOrderLine->getTokenForBook();
$hub_book_request_array['recommandations'] = $hotelOrderLine->getComment();
$hub_book_request_array['language'] = "fr-FR"; // todo dynamize this
$hub_book_request_array['customer'] = [
'firstName' => $hotelOrderLine->getOrderAtt()->getCustomerName(),
'lastName' => " ",
'email' => $hotelOrderLine->getOrderAtt()->getCustomer()->getEmail(),
'mobile' => $hotelOrderLine->getOrderAtt()->getCustomerPhone(),
'city' => $hotelOrderLine->getOrderAtt()->getCustomer()->getCity() ?? 'online',
'country' => $hotelOrderLine->getOrderAtt()->getCustomer()->getCity()?->getCountry() ?? 'online'
];
$hub_book_request_array['payment'] = [
"card" => "",
"ccv" => "",
"expire" => ""
];
foreach ($hotelOrderLine->getOrderSubLines() as $roomIdx => $orderSubLine) { // rooms
if ($orderSubLine->getService() != ServiceEnum::hotel->getValue())
continue;
if ($orderSubLine->getState() == OrderStateEnum::CANCELED->getValue())
continue;
$roomRank = $roomIdx + 1;
$paxes = [];
// Use beneficiaries from OrderSubLine if available
$beneficiaries = $orderSubLine->getBeneficiaries();
if ($beneficiaries->count() > 0) {
foreach ($beneficiaries as $beneficiary) {
$paxes[] = [
'civility' => $beneficiary->getCivility() ?: 'Mr',
'firstName' => $beneficiary->getFirstName(),
'lastName' => $beneficiary->getLastName(),
'age' => $beneficiary->getAge(),
];
}
} else {
// Fallback: generate pax names from the main OrderLine beneficiary (backward compatibility)
$nbADT = $orderSubLine->getNbAdult();
for ($i = 0; $i < $nbADT; $i++) {
$paxRank = $i + 1;
$paxes[] = [
'civility' => "Mr",
'firstName' => $hotelOrderLine->getBeneficiary(),
'lastName' => "r" . $roomRank . " adt-" . $paxRank,
];
}
$nbChild = $orderSubLine->getNbChild();
for ($i = 0; $i < $nbChild; $i++) {
$paxRank = $i + 1;
$paxes[] = [
'civility' => "Enf",
'firstName' => $hotelOrderLine->getBeneficiary(),
'lastName' => "r" . $roomRank . " chd-" . $paxRank,
];
}
}
$room_hub = [
'ratekey' => $orderSubLine->getRatekeyHub(),
'paxes' => $paxes,
'evening' => '',
'supplements' => [],
];
$hub_book_request_array['rooms'][] = $room_hub;
}
$apiResponse = $this->helpers->getApiResponse(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_BOOK_ENDPOINT,
api_key: $this->parameterService->getHotelHubApiKey(),
request_body: $hub_book_request_array
);
$apiResponseContent = $apiResponse['response'];
$hotelCity = "";
$hotelName = "";
if (isset($apiResponseContent['booking'])) {
$hotelCity = $apiResponseContent['booking']['hotel']['cityName'];
$hotelName = $apiResponseContent['booking']['hotel']['hotelName'];
}
$apiLogEntry = new ApiLog();
$apiLogEntry->setBaseUrl('/api/hotel/');
$apiLogEntry->setEndpoint('book');
$apiLogEntry->setDate(new DateTime());
$apiLogEntry->setRequest(json_encode($hub_book_request_array));
$apiLogEntry->setUserID($hotelOrderLine->getOrderAtt()->getUser()->getId());
$apiLogEntry->setCustomerID($hotelOrderLine->getOrderAtt()->getCustomer()->getId());
$apiLogEntry->setSearchCode($hotelOrderLine->getSearchCode());
$apiLogEntry->setResponse(json_encode($apiResponseContent));
$apiLogEntry->setStatusCode($apiResponse['errorCode']);
$apiLogEntry->setStatusMessage($apiResponse['msg']);
$apiLogEntry->setCity($hotelCity);
$apiLogEntry->setHotel($hotelName);
$apiLogEntry->setTag($hotelOrderLine->getReference());
$this->apiLogRepository->save($apiLogEntry, true);
if (isset($apiResponseContent['booking'])) {
$bookingID = $apiResponseContent['bookingId'] ?? "-";
$bookingRef = $apiResponseContent['secondRef'] ?? "-";
$hotelCountry = $apiResponseContent['booking']['hotel']['countryName'];
$hotelStarRating = $apiResponseContent['booking']['hotel']['rating'];
$deadline = DateTime::createFromFormat('Y-m-d', $apiResponseContent['booking']['deadline']); //->modify('-1 day')
if (!$deadline) {
$deadline = null;
}
$hotelOrderLine->setHotelName($hotelName);
$hotelOrderLine->setHotelStarRating((int) $hotelStarRating);
$hotelOrderLine->setCity($hotelCity);
$hotelOrderLine->setCountry($hotelCountry);
$hotelOrderLine->setExternalOrderId($bookingID);
$hotelOrderLine->setExternalOrderIdReference($bookingRef);
$hotelOrderLine->setDeadline($deadline);
$hotelOrderLine->setProviderComment($apiResponseContent['comments'] ?? "");
$hotelOrderLine->setStartDate(DateTime::createFromFormat('Y-m-d', $apiResponseContent['booking']['from']));
$hotelOrderLine->setEndDate(DateTime::createFromFormat('Y-m-d', $apiResponseContent['booking']['to']));
$margin = $this->getMargin($hotelOrderLine->getProduct()->getSource(), $hotelOrderLine->getOrderAtt()->getCustomer(), $hotelName);
$state = key_exists("confirmed", $apiResponseContent) && $apiResponseContent["confirmed"] == 1
? OrderStateEnum::CONFIRMED->getValue()
: OrderStateEnum::NOT_BOOKED->getValue();
$hotelOrderLine->setDescription($hotelName);
$hotelOrderLine->setState($state);
if ($state == OrderStateEnum::CONFIRMED->getValue()) {
$hotelOrderLine->setVoucherAuthorized(true);
}
foreach ($apiResponseContent['booking']['rooms'] as $room_index => $booked_room) {
$hotelOrderSubLine = $hotelOrderLine->getOrderSubLines()->get($room_index);
$OSL_description = $booked_room['name'];
$OSL_description .= " [" . $booked_room['boardName'] . "]";
$hotelOrderSubLine->setDescription($OSL_description);
$salePrice_ProviderCUR = $booked_room['price'] * (100.0 + $margin) / 100.0;
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate(
$hotelOrderLine->getProduct()->getProvider()->getCurrency(),
$hotelOrderLine->getOrderAtt()->getCurrency()
);
$salePrice = $salePrice_ProviderCUR * $exchangeRatePurchaseToSale;
$hotelOrderSubLine->setSalePrice($salePrice);
$hotelOrderSubLine->setPurchasePrice($booked_room['price']);
$hotelOrderSubLine->setNbAdult(intval($booked_room['paxes']['adult']));
$hotelOrderSubLine->setNbChild(intval($booked_room['paxes']['child']['value']));
$hotelOrderSubLine->setRoomChildrenAge($booked_room['paxes']['child']['age']);
$hotelOrderSubLine->setCancelable($booked_room['rateClass'] != 'NRF');
if ($state == OrderStateEnum::CONFIRMED->getValue()) {
$orderSublineState = OrderStateEnum::AVAILABLE->getValue();
} else {
$orderSublineState = OrderStateEnum::NOT_AVAILABLE->getValue();
}
$hotelOrderSubLine->setState($orderSublineState);
}
$hotelOrderLine->setError($apiResponse['msg']);
}
$this->orderLineRepository->save($hotelOrderLine, true);
return $hotelOrderLine;
}
public function getBookParametersFromCartLine(mixed $cart_line): array
{
$rooms = [];
$ratekey = "";
foreach ($cart_line['elements'] as $element) {
$ratekey = $element['rateKey'];
$room['rateKey'] = $ratekey;
$room['paxes'] = $element['guests'];
if (array_key_exists('parties', $element)) {
$room['parties'] = $element['parties'];
}
$rooms[] = $room;
}
return [
'label' => $cart_line['label'],
'date' => $cart_line['date'],
'rooms' => $rooms,
'comment' => $cart_line['comment'],
'beneficiary' => $cart_line['beneficiary']['name'],
'options' => $cart_line['options'],
'searchCode' => $cart_line['searchCode'],
'image' => $cart_line['image']
];
}
public function isAvailable(mixed $checkRateData): bool
{
$hotelSourceData = $checkRateData['sources'][0];
$isAvailable = true;
foreach ($hotelSourceData['rooms'] as $room) {
$isRoomAvailable = $room[0]['available'];
if (!$isRoomAvailable) {
$isAvailable = false;
}
}
return $isAvailable;
}
private function getPromosFlags(array $promos): array
{
$promos_flags = [];
foreach ($promos as $promo) {
$flag = "";
if ($promo->getCalculationMethod() == "percent") {
$flag .= " -" . intval($promo->getSaleValue()) . "% ";
}
if ($promo->getPromoType() == 1 || $promo->getPromoType() == 2) {
$flag .= $this->translator->trans($promo->getPromoTypeFlag()) . " ";
$flag .= $this->translator->trans("Pages.Common.Messages.Until") . " " . $promo->getEbEndDate()->format('d/m/Y');
} else {
$flag .= $promo->getTitle();
}
$promos_flags[] = $flag;
}
return array_unique($promos_flags);
}
public function getHotelBadges(
?Hotel $hotel,
string $badgeType
): array
{
if (!$hotel) {
return [];
}
$badgesArray = [];
foreach ($hotel->getBadges() as $badge) {
if ($badge->getType() === $badgeType) {
$badgesArray[] = $badge->getName();
}
}
return $badgesArray;
}
/* public function getHotelBadges(string $badgeType, string $hotelKey, bool $isLocalHotel = false): array
{
$badges_array = [];
// $hotelXml = $this->hotelXmlRepository->findOneBy(['code' => $hotelKey]);
$hotelXml = $this->hotelXmlRepository->findOneHotelXmlByKey($hotelKey);
if ($hotelXml) {
$associatedHotel = $hotelXml->getHotel();
if ($associatedHotel) {
$badges = $associatedHotel->getBadges();
foreach ($badges as $badge) {
if ($badge->getType() != $badgeType)
continue;
$badges_array[] = $badge->getName();
}
}
} else {
// $hotelKey like 0-2541
$localHotelCode = substr($hotelKey, strrpos($hotelKey, '-') + 1);
if ($isLocalHotel && $localHotelCode != "") {
$hotel = $this->hotelRepository->find($localHotelCode);
if ($hotel) {
$badges = $hotel->getBadges();
foreach ($badges as $badge) {
if ($badge->getType() != $badgeType)
continue;
$badges_array[] = $badge->getName();
}
}
}
}
return $badges_array;
}*/
public function getHotelFacilities(string $hotelKey): array
{
// $hotelXml = $this->hotelXmlRepository->findOneBy(['code' => $hotelKey]);
// if($hotelXml){
// $associatedHotel = $hotelXml->getHotel();
//
//// if($associatedHotel){
//
// //$facilities = $associatedHotel->getFacilities();
//
//// foreach ($facilities as $facility){
////// if($facility->getType() != $badgeType) continue;
////// $facilities_array[] = $facility->getName();
//// }
//// }
// }
return [];
}
private function getHotelXmlSalePrice(float $price, string $hotelName, $customer): float
{
return $price;
}
public function refreshListGDS(): ?string
{
$headers = [
'api-key' => $this->parameterService->getHotelHubApiKey()
];
if (!$headers['api-key']){
return $this->translator->trans('Entities.Common.Alerts.HUBParam');
}
$response_hub = $this->helpers->getApiResponseFromHub('POST', $this->parameterService->getHotelHubUrl(), HUB_HOTEL_API_ACTIVATED_GDS_ENDPOINT, $headers);
try {
if (array_key_exists('error', $response_hub) && $response_hub['error']) {
$this->xmlApiService->disableXmlApi("HOTEL");
return $response_hub['msg'];
} else { // Update XmlApi list
foreach ($response_hub['result'] as $hub_xml_line) {
$xmlSourceID = (int) $hub_xml_line['id_ligne'];
$partnerName = $hub_xml_line['partner_name'];
$xmlIsActive = $hub_xml_line['etat'] == '1';
$pagination = $hub_xml_line['pagination'];
$xmlApi = $this->entityManager->getRepository(XmlApi::class)->findOneBy(['source' => $xmlSourceID, 'xmlApiType' => 'HOTEL']);
if (is_null($xmlApi)) {
$provider = new OtherProvider();
$provider->setName($partnerName);
$provider->setActive(true);
$provider->setCurrency($this->parameterService->getReferenceCurrency());
$this->entityManager->persist($provider);
$xmlApi = new XmlApi();
$xmlApi->setSource($xmlSourceID);
$xmlApi->setName($partnerName);
$xmlApi->setGdsCode($hub_xml_line['gdscode']);
$xmlApi->setmarginB2C(XmlApi::XMLAPI_DEFAULT_MARGIN);
$xmlApi->setmarginB2B(XmlApi::XMLAPI_DEFAULT_MARGIN);
$xmlApi->setPayAtHotelAuthorized(false);
$xmlApi->setXmlApiType('HOTEL');
$xmlApi->setProvider($provider);
}
$xmlApi->setIsAllowedPagination($pagination);
$xmlApi->setCommissionType(CommissionTypeEnum::MARGIN->getValue());
$xmlApi->setActive($xmlIsActive);
$this->entityManager->persist($xmlApi);
}
$this->entityManager->flush();
}
}catch (Exception $e){
$hub_msg = "";
if(key_exists("msg", $response_hub)){
$hub_msg = $response_hub['msg'];
}
return $hub_msg. " | ". $e->getMessage();
}
return "";
}
private function getCheckRateParametersFromOrderLine(OrderLine $orderline): array
{
$nbRooms = count($orderline->getOrderSubLines());
$rooms = [];
foreach ($orderline->getOrderSubLines() as $orderSubLine) {
if ($orderSubLine->getService() != 'hotel')
continue;
if (is_null($orderSubLine->getRatekey()))
continue;
$room['rateKey'] = $orderSubLine->getRatekey();
$rooms[] = $room;
}
// returning checkrate parameters
return [
'rooms' => $rooms,
'searchCode' => $orderline->getSearchCode()
];
}
private function getAmountFromCheckRateHubResponse(array $checkRateResponse)
{
$amount = 0;
$hotel = $checkRateResponse[0];
$hotelSource = $hotel['sources'][0];
$hotelSourceRooms = $hotelSource['rooms'];
foreach ($hotelSourceRooms as $index => $room_index) {
$roomPrice = $room_index[0]['saleRate'];
$amount += $roomPrice;
}
return $amount;
}
public function getHotelCustomRating($hotelName): ?string
{
$hotel = $this->hotelRepository->findOneBy(['name' => $hotelName]);
if ($hotel) {
return $hotel->getCustomRating();
}
return null;
}
public function getHotelCustomRatings(array $hotelNames): array
{
if (empty($hotelNames)) {
return [];
}
$hotels = $this->hotelRepository->findBy(['name' => $hotelNames]);
$ratings = [];
foreach ($hotels as $hotel) {
$ratings[$hotel->getName()] = $hotel->getCustomRating();
}
return $ratings;
}
public function getNewHotelBookingsFromHUB($parameters): array
{
$apiResponse = $this->helpers->getApiResponseFromHub(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_GET_BOOKINGS_ENDPOINT,
headers: [
'api-key' => $this->parameterService->getHotelHubApiKey(),
],
request_body: $parameters
);
try {
$gdsBookings = $apiResponse['response'] ?? [];
// $xmlApiBookings_inProvider = array_filter($gdsBookings, function ($booking) use ($xmlApi) {
// return isset($booking['source']) && $booking['source'] == $xmlApi->getSource();
// });
$bookings_provider_indexed = [];
foreach ($gdsBookings as $item) {
if (isset($item['bookingId'])) {
$bookings_provider_indexed[$item['bookingId']] = $item;
}
}
$orderlines = $this->orderLineRepository->findAll(); // TODO get HOTEL Order lines XML_API
$currentStates = [];
foreach ($orderlines as $orderLine) {
// TODO Optimize
if ($orderLine->getProduct()->getClass() != 'XmlApi')
continue;
if ($orderLine->getProduct()->getXmlApiType() != 'HOTEL')
continue;
if ($orderLine->getState() == OrderStateEnum::CANCELED->getValue())
continue;
if ($orderLine->getExternalOrderId() == null)
continue;
$currentStates[$orderLine->getExternalOrderId()] = $orderLine->getState();
}
$already_imported_orderlines = array_keys($currentStates);
$bookings_provider_indexed = array_filter($bookings_provider_indexed, function ($booking) use ($already_imported_orderlines) {
return !in_array($booking['bookingId'], $already_imported_orderlines);
});
return $bookings_provider_indexed;
} catch (\Exception) {
return [];
}
}
public function importBookingsFromHub(array $selected_IDsBookings, $xmlApiBookings_inProvider_new, Customer $customer, $user): string
{
try {
$sources_entities = [];
foreach ($selected_IDsBookings as $selected_booking_ID) {
$hubBooking = $xmlApiBookings_inProvider_new[$selected_booking_ID];
// if($hubBooking['currency'] != $customer->getCurrency()) return "Customer Currency Error" ; // ignore
$start_date = DateTime::createFromFormat('Y-m-d', $hubBooking['checkIn']);
$end_date = DateTime::createFromFormat('Y-m-d', $hubBooking['checkOut']);
$deadline = DateTime::createFromFormat('Y-m-d', $hubBooking['deadline']);
$state = match ($hubBooking["status"]) {
'CONFIRMED' => OrderStateEnum::CONFIRMED->getValue(),
default => OrderStateEnum::PENDING->getValue(),
};
$source = $hubBooking['source'];
if (isset($sources_entities[$source])) {
$xmlApi = $sources_entities[$source];
} else {
$xmlApi = $this->entityManager->getRepository(XmlApi::class)->findOneBy(['source' => $source]);
$sources_entities[$source] = $xmlApi;
}
$exchangeRatePurchaseToSale = $this->currencyService->calculateExchangeRate($xmlApi->getProvider()->getCurrency(), $customer->getCurrency());
$agency = $this->agencyRepository->find($this->requestStack->getSession()->get('agency') ?? 1);
$order = new Order();
$order->setUser($user);
$order->setReference($this->helpers->generateShortId());
$order->setCustomerName($customer->getName());
$order->setAgency($agency);
$order->setCustomerPhone($customer->getPhone());
$order->setCustomerEmail($customer->getEmail());
$order->setExchangeRateSaleToReference($this->currencyService->calculateExchangeRate($customer->getCurrency(), $agency->getCurrency()));
$order->setExchangeRateSaleToCustomer(1);
$order->setCurrency($customer->getCurrency());
$order->setCustomer($customer);
$hotelOrderLine = new OrderLine();
$orderLineRef = $this->helpers->generateShortId();
$hotelOrderLine->setReference($orderLineRef);
$hotelOrderLine->setProduct($xmlApi);
$hotelOrderLine->setHotelName($hubBooking['hotelName']);
$hotelOrderLine->setExternalOrderId($selected_booking_ID);
$hotelOrderLine->setBeneficiary($hubBooking['traveller'] ?? '-');
$hotelOrderLine->setStartDate($start_date);
$hotelOrderLine->setEndDate($end_date);
$hotelOrderLine->setDeadline($deadline);
$hotelOrderLine->setState($state);
$hotelOrderLine->setDescription($hubBooking['hotelName'] . " - " . $orderLineRef);
$hotelOrderLine->setExchangeRatePurchaseToSale($exchangeRatePurchaseToSale);
$hotelOrderLine->setVoucherAuthorized(true);
$hotelOrderLine->setError("Imported from Provider System.");
$countRooms = count($hubBooking['rooms']);
foreach ($hubBooking['rooms'] as $hubBooking_room) {
$hotelOrderSubLine = new OrderSubLine();
$hotelOrderSubLine->setProductElement(null);
$hotelOrderSubLine->setService(ServiceEnum::hotel->getValue());
$OSL_description = $hubBooking_room['room'];
$OSL_description .= " [" . $hubBooking_room['board'] . "]";
$hotelOrderSubLine->setDescription($OSL_description);
$subLineSalePrice = round($hubBooking['price'] * $exchangeRatePurchaseToSale / $countRooms, 4);
$subLinePurchasePrice = round($hubBooking['purchase'] / $countRooms, 4);
$hotelOrderSubLine->setSalePrice($subLineSalePrice);
$hotelOrderSubLine->setPurchasePrice($subLinePurchasePrice);
$hotelOrderSubLine->setNbAdult(intval($hubBooking_room['adult']));
$hotelOrderSubLine->setNbChild(intval($hubBooking_room['child']['value']));
// Create Adult Beneficiaries
for ($i = 0; $i < intval($hubBooking_room['adult']); $i++) {
$beneficiary = new Beneficiary();
$beneficiary->setCivility('Adult');
$beneficiary->setFirstName('Adult');
$beneficiary->setLastName(($i + 1));
$hotelOrderSubLine->addBeneficiary($beneficiary);
}
// Create Child Beneficiaries
for ($i = 0; $i < intval($hubBooking_room['child']['value']); $i++) {
$child = new Beneficiary();
$child->setCivility('Child');
$child->setFirstName('Child');
$child->setLastName(($i + 1));
// Set child age if available in the hub data
if (isset($hubBooking_room['child']['age'][$i])) {
$child->setAge(intval($hubBooking_room['child']['age'][$i]));
}
$hotelOrderSubLine->addBeneficiary($child);
}
$hotelOrderSubLine->setState(OrderStateEnum::AVAILABLE->getValue());
$hotelOrderSubLine->setCancelable(false);
$hotelOrderLine->addOrderSubLine($hotelOrderSubLine);
}
$order->addOrderLine($hotelOrderLine); // ### ADD Hotel Order Line ###
$this->entityManager->persist($order);
}
$this->entityManager->flush();
return "";
} catch (Exception $e) {
return $e->getMessage();
}
}
public function getBalanceOnGDS(XmlApi $xmlApi): ?string
{
$parameters = [
"Source" => $xmlApi->getSource(),
];
try {
$apiResponse = $this->helpers->getApiResponse(
method: 'POST',
api_base_url: $this->parameterService->getHotelHubUrl(),
endPoint: HUB_HOTEL_API_CREDIT_CHECK_ENDPOINT,
api_key: $this->parameterService->getHotelHubApiKey(),
request_body: $parameters
);
if ($apiResponse['error']) {
return $apiResponse['msg'];
}
return $apiResponse['response']['amount'] . ' ' . $apiResponse['response']['currency'];
} catch (Exception $e) {
return "Error : " . $e->getMessage();
}
}
public function changePeriod(OrderLine $orderLine, $newStartDate, $newEndDate, $applyFee): bool
{
$orderLineReq = new OrderLine();
$orderLineReq->setStartDate(new DateTime($newStartDate));
$orderLineReq->setEndDate(new DateTime($newEndDate));
$orderLineReq->setPreviousOrderline($orderLine);
//$orderLineReq->setType(null);
$order = $orderLine->getOrderAtt();
//$orderLineRequest->setPreviousOrderline($orderLine);
//$orderLineRequest->setType(OrderLineTypeEnum::REQUEST_CHANGE_PERIOD->getValue());
if ($orderLine->getProduct() instanceof HotelContract) {
$book_parameters = $this->getBookParametersForPeriodChange($orderLine, $orderLineReq->getStartDate(), $orderLineReq->getEndDate());
$found = $this->bookFromLocal($book_parameters, $order, $orderLineReq);
if (!$found)
return false; // period not covered by the HotelContract
$cancellation_fee = 0;
if (is_null($orderLine->getType())) // logiquement tous les cas (peut etre on elimine ce if)
{
// remove debits operations and calculate cancellation fee
foreach ($orderLine->getOrderSubLines() as $orderSubLine) {
$debit = $orderSubLine->getDebit();
if ($debit) {
$debit->setProductElement(null);
$this->entityManager->remove($debit);
}
$orderSubLine->setDebit(null);
$orderSubLine->setState(OrderStateEnum::CANCELED->getValue());
$this->entityManager->persist($orderSubLine);
if ($applyFee) {
$cancellation_fee += $orderSubLine->getCurrentCancellationAmount();
}
}
if ($cancellation_fee > 0) { // Generate a new REFERENCE (N°DOSSIER)
$orderLineReq->setReference($this->helpers->generateShortId());
$orderLine->setState(OrderStateEnum::CANCELED_FEE->getValue());
$orderLine->setType(null); // TODO delete may be this line (useless)
$orderSubLine_cancellation = new OrderSubLine();
$orderSubLine_cancellation->setSalePrice($cancellation_fee);
$orderSubLine_cancellation->setPurchasePrice($cancellation_fee);
$orderSubLine_cancellation->setService(ServiceEnum::cancellation->getValue());
$description = $this->translator->trans('Entities.OrderSubLine.Message.CancellationAmount') . " " . $orderLine->getReference();
$orderSubLine_cancellation->setDescription($description);
$orderSubLine_cancellation->setCancelable(false);
$orderLine->addOrderSubLine($orderSubLine_cancellation);
} else {
$orderLineReq->setReference($orderLine->getReference());
$orderLine->setState(OrderStateEnum::CANCELED->getValue());
$orderLine->setType(OrderLineTypeEnum::HISTORY->getValue());
}
// add debit operations to the new OrderLine
//&& ( $orderLine->isPending() || $orderLineReq->isConfirmed() )
if ($orderLineReq->isConfirmed()) {
foreach ($orderLineReq->getOrderSubLines() as $orderSubLine) {
if (!is_null($orderSubLine->getDebit()))
continue; //|| $orderSubLine->getState() == OrderStateEnum::CANCELED->getValue()
$orderSubLine->associateDebitOperation(false);
}
}
}
// elseif(str_contains($orderLine->getType(), OrderLineTypeEnum::REQUEST->getValue())) { // modify request by another request
// $orderLineReq->setPreviousOrderline($orderLine->getPreviousOrderline());
// $orderLine->setType(OrderLineTypeEnum::HISTORY->getValue());
// $orderLine->setState(OrderStateEnum::CANCELED->getValue());
// //$orderLine->dissociateDebitOperations();
// }
$this->entityManager->persist($order);
return $orderLineReq->isConfirmed();
}
return false;
}
public function syncOrderLineWithHub(OrderLine $orderLine, bool $applyChanges = false): array
{
try {
$hubBookingData = $this->fetchBookingFromHub($orderLine);
if (!$hubBookingData['success']) {
return [
'success' => false,
'message' => $hubBookingData['message']
];
}
$hubData = $hubBookingData['data'];
$comparisonResult = $this->compareHubWithLocal($orderLine, $hubData);
if (!$comparisonResult['hasChanges']) {
return [
'success' => true,
'hasChanges' => false,
'message' => 'All data is synchronized. No changes detected.'
];
}
if ($applyChanges) {
$updateResult = $this->applyHubChangesToLocal($orderLine, $hubData, $comparisonResult['differences']);
if (!$updateResult['success']) {
return [
'success' => false,
'message' => $updateResult['message']
];
}
return [
'success' => true,
'hasChanges' => true,
'differences' => $comparisonResult['differences'],
'message' => 'Successfully synchronized with Hub data.'
];
}
return [
'success' => true,
'hasChanges' => true,
'differences' => $comparisonResult['differences'],
'message' => 'Changes detected. Review and confirm to sync.'
];
} catch (Exception $e) {
return [
'success' => false,
'message' => 'Sync error: ' . $e->getMessage()
];
}
}
private function fetchBookingFromHub(OrderLine $orderLine): array
{
try {
$order = $orderLine->getOrderAtt();
$externalOrderId = $orderLine->getExternalOrderId();
$requestBody = [
'fromDate' => $order->getCreatedAt()->format('Y-m-d'),
'toDate' => (new DateTime())->format('Y-m-d'),
'bookingId' => $externalOrderId,
];
//dd($requestBody);
$baseUrl = $this->parameterService->getHotelHubUrl();
$hubUrl = $baseUrl . HUB_HOTEL_API_BOOKING_LIST_ENDPOINT;
$apiKey = $this->parameterService->getHotelHubApiKey();
$httpClient = HttpClient::create();
$response = $httpClient->request('POST', $hubUrl, [
'headers' => [
'Api-key' => $apiKey,
],
'body' => (!empty($requestBody)) ? json_encode($requestBody) : []
]);
// Parse response
$statusCode = $response->getStatusCode();
$responseData = json_decode($response->getContent(), true);
//dd($responseData);
// Check for API errors
if ($statusCode !== 200 || empty($responseData)) {
return [
'success' => false,
'message' => 'Hub API returned status ' . $statusCode
];
}
// Find the specific booking in the response
$bookingFound = null;
$bookings = $responseData['response'];
foreach ($bookings as $booking) {
if (isset($booking['bookingId']) && $booking['bookingId'] === $externalOrderId) {
$bookingFound = $booking;
break;
}
}
if (!$bookingFound) {
return [
'success' => false,
'message' => 'Booking not found in Hub response (bookingId: ' . $externalOrderId . ')'
];
}
return [
'success' => true,
'data' => $bookingFound
];
} catch (TransportExceptionInterface $e) {
return [
'success' => false,
'message' => 'Network error: ' . $e->getMessage()
];
} catch (Exception $e) {
return [
'success' => false,
'message' => 'Error fetching from Hub: ' . $e->getMessage()
];
}
}
private function compareHubWithLocal(OrderLine $orderLine, array $hubData): array
{
$differences = [];
$localData = [];
$hubDataFormatted = [];
// Compare status
$hubStatus = $this->mapHubStatusToLocal($hubData['status'] ?? '');
$localStatus = $orderLine->getState();
$localData['status'] = $localStatus;
$hubDataFormatted['status'] = $hubStatus;
if ($hubStatus !== $localStatus) {
$differences['status'] = [
'field' => 'Status',
'old' => $localStatus,
'new' => $hubStatus,
'changed' => true
];
}
// Compare hotel name
$hubHotelName = $hubData['hotelName'] ?? '';
$localHotelName = $orderLine->getHotelName() ?? '';
$localData['hotelName'] = $localHotelName;
$hubDataFormatted['hotelName'] = $hubHotelName;
if ($hubHotelName !== $localHotelName && !empty($hubHotelName)) {
$differences['hotelName'] = [
'field' => 'Hotel Name',
'old' => $localHotelName,
'new' => $hubHotelName,
'changed' => true
];
}
// Compare check-in date
$hubCheckIn = $hubData['checkIn'] ?? null;
$localCheckIn = $orderLine->getStartDate()?->format('Y-m-d');
$localData['checkIn'] = $localCheckIn;
$hubDataFormatted['checkIn'] = $hubCheckIn;
if ($hubCheckIn && $hubCheckIn !== $localCheckIn) {
$differences['checkIn'] = [
'field' => 'Check-In Date',
'old' => $localCheckIn,
'new' => $hubCheckIn,
'changed' => true
];
}
// Compare check-out date
$hubCheckOut = $hubData['checkOut'] ?? null;
$localCheckOut = $orderLine->getEndDate()?->format('Y-m-d');
$localData['checkOut'] = $localCheckOut;
$hubDataFormatted['checkOut'] = $hubCheckOut;
if ($hubCheckOut && $hubCheckOut !== $localCheckOut) {
$differences['checkOut'] = [
'field' => 'Check-Out Date',
'old' => $localCheckOut,
'new' => $hubCheckOut,
'changed' => true
];
}
// Compare deadline
$hubDeadline = $hubData['deadline'] ?? null;
$localDeadline = $orderLine->getDeadline()?->format('Y-m-d');
$localData['deadline'] = $localDeadline;
$hubDataFormatted['deadline'] = $hubDeadline;
if ($hubDeadline && $hubDeadline !== $localDeadline) {
$differences['deadline'] = [
'field' => 'Cancellation Deadline',
'old' => $localDeadline,
'new' => $hubDeadline,
'changed' => true
];
}
// Compare total price
$hubPrice = (float) ($hubData['purchase'] ?? 0);
$hotelTotals = $this->getHotelSubLinesTotals($orderLine);
$hotelPurchaseTotal = (float) $hotelTotals['purchase'];
$existingPriceDiffSubLine = $this->findPriceDiffSubLine($orderLine);
$existingPriceDiff = $existingPriceDiffSubLine ? (float) $existingPriceDiffSubLine->getPurchasePrice() : 0.0;
$localGrandTotal = $hotelPurchaseTotal + $existingPriceDiff;
if (abs($hubPrice - $localGrandTotal) > 0.01) {
$differences['price'] = [
'field' => 'Total Price',
'old' => $localGrandTotal,
'new' => $hubPrice,
'changed' => true
];
}
// Compare currency
$hubCurrency = $hubData['currency'] ?? '';
$localCurrency = $orderLine->getOrderAtt()->getCurrency() ?? '';
$localData['currency'] = $localCurrency;
$hubDataFormatted['currency'] = $hubCurrency;
if ($hubCurrency !== $localCurrency && !empty($hubCurrency)) {
$differences['currency'] = [
'field' => 'Currency',
'old' => $localCurrency,
'new' => $hubCurrency,
'changed' => true
];
}
// Compare booking reference
$hubSecondRef = $hubData['secondRef'] ?? '';
$localSecondRef = $orderLine->getExternalOrderIdReference() ?? '';
$localData['secondRef'] = $localSecondRef;
$hubDataFormatted['secondRef'] = $hubSecondRef;
if ($hubSecondRef !== $localSecondRef && !empty($hubSecondRef)) {
$differences['secondRef'] = [
'field' => 'Booking Reference',
'old' => $localSecondRef,
'new' => $hubSecondRef,
'changed' => true
];
}
// Compare booking notes/comments
$hubNotes = $hubData['bookingNotes'] ?? '';
$localComment = $orderLine->getComment() ?? '';
$localData['notes'] = $localComment;
$hubDataFormatted['notes'] = $hubNotes;
if ($hubNotes !== $localComment && !empty($hubNotes)) {
$differences['notes'] = [
'field' => 'Booking Notes',
'old' => $localComment,
'new' => $hubNotes,
'changed' => true
];
}
// Compare traveller/beneficiary
$hubTraveller = $hubData['traveller'] ?? '';
$localBeneficiary = $orderLine->getBeneficiary() ?? '';
$localData['beneficiary'] = $localBeneficiary;
$hubDataFormatted['beneficiary'] = $hubTraveller;
if (
$hubTraveller !== $localBeneficiary && !empty($hubTraveller) &&
!str_contains($hubTraveller, $localBeneficiary)
) {
$differences['beneficiary'] = [
'field' => 'Traveller/Beneficiary',
'old' => $localBeneficiary,
'new' => $hubTraveller,
'changed' => true
];
}
// Compare rooms (OrderSubLines)
$hubRooms = $hubData['rooms'] ?? [];
$localSubLines = $orderLine->getOrderSubLines();
$localRooms = [];
$hubRoomsFormatted = [];
// Extract local room information from OrderSubLines
foreach ($localSubLines as $index => $subLine) {
if ($subLine->getService() === 'hotel') {
if ($subLine->getState() === OrderStateEnum::CANCELED->getValue()) {
continue;
}
$description = $subLine->getDescription();
// Parse description format: "Chambre Standard [Demi pension (DP)]"
$roomType = '';
$boardType = '';
if (preg_match('/^(.+?)\s*\[(.+?)\]$/', $description, $matches)) {
$roomType = trim($matches[1]);
$boardType = trim($matches[2]);
} else {
$roomType = $description;
}
// Extract child ages from roomChildrenAge field
$childAges = [];
if ($subLine->getRoomChildrenAge()) {
$childAges = array_map('trim', explode(',', $subLine->getRoomChildrenAge()));
}
$localRooms[] = [
'room' => $roomType,
'board' => $boardType,
'adult' => $subLine->getNbAdult(),
'price' => $subLine->getPurchasePrice(),
'child' => [
'value' => $subLine->getNbChild() ?? 0,
'ages' => $childAges
],
];
}
}
// Format hub rooms for comparison
foreach ($hubRooms as $index => $hubRoom) {
$childData = $hubRoom['child'] ?? ['value' => '0', 'age' => ''];
$childCount = is_array($childData) ? ($childData['value'] ?? '0') : '0';
$childAge = is_array($childData) ? ($childData['age'] ?? '') : '';
// Parse child ages
$childAges = [];
if (!empty($childAge)) {
$childAges = array_map('trim', explode(',', $childAge));
}
$hubRoomsFormatted[] = [
'room' => $hubRoom['room'] ?? '',
'board' => $hubRoom['board'] ?? '',
'adult' => $hubRoom['adult'] ?? '0',
//'price' => $hubRoom['price'] ?? 0,
'child' => [
'value' => $childCount,
'ages' => $childAges
]
];
}
$localData['rooms'] = $localRooms;
$hubDataFormatted['rooms'] = $hubRoomsFormatted;
// Compare rooms count first
if (count($hubRooms) !== count($localRooms)) {
$differences['rooms_count'] = [
'field' => 'Number of Rooms',
'old' => count($localRooms),
'new' => count($hubRooms),
'changed' => true
];
}
// Compare each room
$roomDifferences = [];
$maxRooms = max(count($hubRooms), count($localRooms));
for ($i = 0; $i < $maxRooms; $i++) {
$localRoom = $localRooms[$i] ?? null;
$hubRoom = $hubRoomsFormatted[$i] ?? null;
if ($hubRoom === null) {
// Room exists locally but not in hub
$roomDifferences[$i] = [
'field' => 'Room ' . ($i + 1),
'old' => $localRoom,
'new' => null,
'changed' => true,
'type' => 'removed'
];
} elseif ($localRoom === null) {
// Room exists in hub but not locally
$roomDifferences[$i] = [
'field' => 'Room ' . ($i + 1),
'old' => null,
'new' => $hubRoom,
'changed' => true,
'type' => 'added'
];
} else {
// Both exist, compare all properties
$changeDetails = [];
$roomChanged = false;
// Compare room type
$hubRoomType = $hubRoom['room'] ?? '';
$localRoomType = $localRoom['room'];
if ($hubRoomType !== $localRoomType && !empty($hubRoomType)) {
$roomChanged = true;
$changeDetails['room_type'] = [
'old' => $localRoomType,
'new' => $hubRoomType
];
}
// Compare board type
$hubBoard = $hubRoom['board'] ?? '';
$localBoard = $localRoom['board'];
if ($hubBoard !== $localBoard && !empty($hubBoard)) {
$roomChanged = true;
$changeDetails['board'] = [
'old' => $localBoard,
'new' => $hubBoard
];
}
// Compare number of adults
$hubAdult = (int) ($hubRoom['adult'] ?? 0);
$localAdult = (int) ($localRoom['adult'] ?? 0);
if ($hubAdult !== $localAdult && $hubAdult > 0) {
$roomChanged = true;
$changeDetails['adult'] = [
'old' => $localAdult,
'new' => $hubAdult
];
}
// Compare number of children
$hubChildCount = (int) ($hubRoom['child']['value'] ?? 0);
$localChildCount = (int) ($localRoom['child']['value'] ?? 0);
if ($hubChildCount !== $localChildCount) {
$roomChanged = true;
$changeDetails['child_count'] = [
'old' => $localChildCount,
'new' => $hubChildCount
];
}
// Compare child ages
$hubChildAges = $hubRoom['child']['ages'] ?? [];
$localChildAges = $localRoom['child']['ages'] ?? [];
if ($hubChildAges !== $localChildAges && !empty($hubChildAges)) {
$roomChanged = true;
$changeDetails['child_ages'] = [
'old' => implode(',', $localChildAges),
'new' => implode(',', $hubChildAges)
];
}
if ($roomChanged) {
$roomDifferences[$i] = [
'field' => 'Room ' . ($i + 1),
'old' => $localRoom,
'new' => $hubRoom,
'changed' => true,
'type' => 'modified',
'details' => $changeDetails
];
}
}
}
if (!empty($roomDifferences)) {
foreach ($roomDifferences as $i => $roomDiff) {
$differences['room_' . ($i + 1)] = $roomDiff;
}
}
return [
'hasChanges' => count($differences) > 0,
'differences' => $differences,
'localData' => $localData,
'hubData' => $hubDataFormatted
];
}
private function applyHubChangesToLocal(OrderLine $orderLine, array $hubData, array $differences): array
{
try {
// Update status
if (isset($differences['status'])) {
$newStatus = $differences['status']['new'];
$orderLine->setState($newStatus);
}
// Update hotel name
if (isset($differences['hotelName'])) {
$orderLine->setHotelName($differences['hotelName']['new']);
}
// Update check-in date
if (isset($differences['checkIn'])) {
$newCheckIn = DateTime::createFromFormat('Y-m-d', $differences['checkIn']['new']);
if ($newCheckIn) {
$orderLine->setStartDate($newCheckIn);
}
}
// Update check-out date
if (isset($differences['checkOut'])) {
$newCheckOut = DateTime::createFromFormat('Y-m-d', $differences['checkOut']['new']);
if ($newCheckOut) {
$orderLine->setEndDate($newCheckOut);
}
}
// Update deadline
if (isset($differences['deadline'])) {
$newDeadline = DateTime::createFromFormat('Y-m-d', $differences['deadline']['new']);
if ($newDeadline) {
$orderLine->setDeadline($newDeadline);
}
}
// Update booking reference
if (isset($differences['secondRef'])) {
$orderLine->setExternalOrderIdReference($differences['secondRef']['new']);
}
// Update booking notes
if (isset($differences['notes'])) {
$orderLine->setComment($differences['notes']['new']);
}
// Update beneficiary
if (isset($differences['beneficiary'])) {
$orderLine->setBeneficiary($differences['beneficiary']['new']);
}
// Update rooms (OrderSubLines)
$roomDiffs = array_filter(
$differences,
fn($key) => str_starts_with($key, 'room_'),
ARRAY_FILTER_USE_KEY
);
if (!empty($roomDiffs)) {
$this->updateOrderSubLinesFromHub($orderLine, $hubData['rooms'] ?? [], $roomDiffs);
}
// Update price
if (isset($differences['price'])) {
$newTotalPrice = $differences['price']['new'];
$this->updateOrderSubLinesPrices($orderLine, $newTotalPrice);
// $this->consolidatePriceDiffSublines($orderLine);
}
$this->entityManager->persist($orderLine);
$this->entityManager->flush();
return [
'success' => true,
'message' => 'Changes applied successfully.'
];
} catch (Exception $e) {
return [
'success' => false,
'message' => 'Failed to apply changes: ' . $e->getMessage()
];
}
}
private function updateOrderSubLinesFromHub(OrderLine $orderLine, array $hubRooms, array $roomDifferences): void
{
$localSubLines = $orderLine->getOrderSubLines()->toArray();
$localSubLines = array_values($localSubLines);
foreach ($roomDifferences as $key => $roomDiff) {
if (preg_match('/^room_(\d+)$/', $key, $matches)) {
$index = (int) $matches[1] - 1;
} else {
continue;
}
if (!isset($roomDiff['changed']) || !$roomDiff['changed']) {
continue;
}
$changeType = $roomDiff['type'] ?? 'modified';
switch ($changeType) {
case 'removed':
if (isset($localSubLines[$index])) {
$subLine = $localSubLines[$index];
$subLine->setState(OrderStateEnum::CANCELED->getValue());
$this->entityManager->persist($subLine);
}
break;
case 'added':
$hubRoom = $hubRooms[$index] ?? null;
if ($hubRoom) {
$newSubLine = $this->createOrderSubLineFromHubRoom($orderLine, $hubRoom);
$orderLine->addOrderSubLine($newSubLine);
$this->entityManager->persist($newSubLine);
}
break;
case 'modified':
if (isset($localSubLines[$index])) {
$subLine = $localSubLines[$index];
$hubRoom = $hubRooms[$index] ?? null;
if ($hubRoom) {
$this->updateOrderSubLineFromHubRoom($subLine, $hubRoom, $roomDiff['details'] ?? []);
}
$this->entityManager->persist($subLine);
}
break;
}
}
}
private function createOrderSubLineFromHubRoom(OrderLine $orderLine, array $hubRoom): OrderSubLine
{
$subLine = new OrderSubLine();
// Set description in format: "Room [Board]"
$roomType = $hubRoom['room'] ?? '';
$boardType = $hubRoom['board'] ?? '';
$description = $roomType . ' [' . $boardType . ']';
$subLine->setDescription($description);
// occupancy
$subLine->setNbAdult((int) ($hubRoom['adult'] ?? 2));
$childData = $hubRoom['child'] ?? ['value' => '0', 'ages' => []];
$childCount = is_array($childData) ? (int) ($childData['value'] ?? 0) : (int) $childData;
$subLine->setNbChild($childCount);
if (is_array($childData) && !empty($childData['ages'])) {
$subLine->setRoomChildrenAge(implode(',', $childData['ages']));
}
$subLines = $orderLine->getOrderSubLines();
$firstSubLine = $subLines[0];
$subLine->setState(OrderStateEnum::AVAILABLE->getValue());
$subLine->setService('hotel');
$subLine->setCancelable(true);
$subLine->setRatekey($firstSubLine->getRatekey());
$subLine->setAgencyCommission($firstSubLine->getAgencyCommission());
$subLine->setPartnerCommission($firstSubLine->getPartnerCommission());
$orderLine->addOrderSubLine($subLine);
return $subLine;
}
private function updateOrderSubLineFromHubRoom(OrderSubLine $subLine, array $hubRoom, array $changeDetails): void
{
// Update room type and board
if (isset($changeDetails['room_type']) || isset($changeDetails['board'])) {
$roomType = $hubRoom['room'] ?? '';
$boardType = $hubRoom['board'] ?? '';
$description = $roomType . ' [' . $boardType . ']';
$subLine->setDescription($description);
}
// Update number of adults
if (isset($changeDetails['adult'])) {
$subLine->setNbAdult((int) ($hubRoom['adult'] ?? 2));
}
// Update number of children
if (isset($changeDetails['child_count'])) {
$childData = $hubRoom['child'] ?? ['value' => '0'];
$childCount = is_array($childData) ? (int) ($childData['value'] ?? 0) : (int) $childData;
$subLine->setNbChild($childCount);
}
// Update child ages
if (isset($changeDetails['child_ages'])) {
$childData = $hubRoom['child'] ?? [];
if (is_array($childData) && isset($childData['ages']) && !empty($childData['ages'])) {
$subLine->setRoomChildrenAge(implode(',', $childData['ages']));
} else {
$subLine->setRoomChildrenAge($hubRoom['child']['age']);
}
}
$this->entityManager->persist($subLine);
}
private function updateOrderSubLinesPrices(OrderLine $orderLine, float $newTotalPurchasePrice): void
{
$epsilon = 0.01;
$this->consolidatePriceDiffSublines($orderLine);
$hotelTotals = $this->getHotelSubLinesTotals($orderLine);
$hotelPurchaseTotal = (float) $hotelTotals['purchase'];
$hotelSaleTotal = (float) $hotelTotals['sale'];
$desiredDiff = round($newTotalPurchasePrice - $hotelPurchaseTotal, 2);
$existingPriceDiff = $this->findPriceDiffSubLine($orderLine);
$marginRatio = 1.0;
if ($hotelPurchaseTotal > 0) {
$marginRatio = $hotelSaleTotal / $hotelPurchaseTotal;
} else {
$orderPurchase = (float) ($orderLine->getPurchasePrice() ?? 0);
$orderSale = (float) ($orderLine->getSalePrice() ?? 0);
$marginRatio = $orderPurchase > 0 ? ($orderSale / $orderPurchase) : 1.0;
}
$saleForDiff = round($desiredDiff * $marginRatio, 2);
if (abs($desiredDiff) < $epsilon) {
if ($existingPriceDiff) {
if (method_exists($orderLine, 'removeOrderSubLine')) {
$orderLine->removeOrderSubLine($existingPriceDiff);
} else {
$existingPriceDiff->setOrderLine(null);
}
$this->entityManager->remove($existingPriceDiff);
$this->entityManager->persist($orderLine);
}
return;
}
if ($existingPriceDiff) {
$existingPriceDiff->setPurchasePrice($desiredDiff);
$existingPriceDiff->setSalePrice($saleForDiff);
$existingPriceDiff->setDescription('Price Diff');
$existingPriceDiff->setCancelable(true);
$this->entityManager->persist($existingPriceDiff);
} else {
$priceDiff = new OrderSubLine();
$priceDiff->setService('Price Diff');
$priceDiff->setPurchasePrice($desiredDiff);
$priceDiff->setSalePrice($saleForDiff);
$priceDiff->setDescription('Price Diff');
$priceDiff->setCancelable(true);
$priceDiff->setState(OrderStateEnum::AVAILABLE->getValue());
$priceDiff->setOrderLine($orderLine);
// copy fields from a hotel subline for commissions / ratekey
$subLines = $orderLine->getOrderSubLines();
if ($subLines->count() > 0) {
$first = null;
foreach ($subLines as $sl) {
if ($sl->getService() === 'hotel') {
$first = $sl;
break;
}
}
if (!$first) {
$first = $subLines[0];
}
if ($first) {
$priceDiff->setRatekey($first->getRatekey());
$priceDiff->setAgencyCommission($first->getAgencyCommission());
$priceDiff->setPartnerCommission($first->getPartnerCommission());
}
}
$orderLine->addOrderSubLine($priceDiff);
$this->entityManager->persist($priceDiff);
}
// Re-consolidate and persist owning side
$this->consolidatePriceDiffSublines($orderLine);
$this->entityManager->persist($orderLine);
}
private function mapHubStatusToLocal(string $hubStatus): string
{
return match (strtoupper($hubStatus)) {
'CONFIRMED' => OrderStateEnum::CONFIRMED->getValue(),
'CANCELLED', 'CANCELED' => OrderStateEnum::CANCELED->getValue(),
'TEMP_AVAILABLE' => OrderStateEnum::TEMP_AVAILABLE->getValue(),
default => OrderStateEnum::PENDING->getValue(),
};
}
private function getHotelSubLinesTotals(OrderLine $orderLine): array
{
$purchase = 0.0;
$sale = 0.0;
foreach ($orderLine->getOrderSubLines() as $sl) {
if ($sl->getService() === 'hotel' && $sl->getState() !== OrderStateEnum::CANCELED->getValue()) {
$purchase += (float) ($sl->getPurchasePrice() ?? 0);
$sale += (float) ($sl->getSalePrice() ?? 0);
}
}
return ['purchase' => $purchase, 'sale' => $sale];
}
private function findPriceDiffSubLine(OrderLine $orderLine): ?OrderSubLine
{
foreach ($orderLine->getOrderSubLines() as $sl) {
$svc = (string) $sl->getService();
if (strtolower(trim($svc)) === 'price diff') {
return $sl;
}
}
return null;
}
private function consolidatePriceDiffSublines(OrderLine $orderLine): ?OrderSubLine
{
$priceDiffs = [];
foreach ($orderLine->getOrderSubLines() as $sl) {
if (strtolower(trim((string) $sl->getService())) === 'price diff') {
$priceDiffs[] = $sl;
}
}
if (empty($priceDiffs)) {
return null;
}
$first = array_shift($priceDiffs);
$totalPurchase = (float) ($first->getPurchasePrice() ?? 0);
$totalSale = (float) ($first->getSalePrice() ?? 0);
foreach ($priceDiffs as $extra) {
$totalPurchase += (float) ($extra->getPurchasePrice() ?? 0);
$totalSale += (float) ($extra->getSalePrice() ?? 0);
if (method_exists($orderLine, 'removeOrderSubLine')) {
$orderLine->removeOrderSubLine($extra);
} else {
$extra->setOrderLine(null);
}
$this->entityManager->remove($extra);
}
$first->setPurchasePrice($totalPurchase);
$first->setSalePrice($totalSale);
$this->entityManager->persist($orderLine);
$this->entityManager->persist($first);
return $first;
}
public function upsertHotelCache(string $cityKey, array $newHotels): void
{
$normalizeKey = static fn(string $name): string => mb_strtoupper(str_replace(' ', '', $name), 'UTF-8');
$normalizedCityKey = $normalizeKey($cityKey);
$cache = $this->hotelCacheRepository->findOneBy(['cityKey' => $normalizedCityKey]);
if (!$cache) {
$cache = new \App\Entity\HotelCache();
$cache->setCityKey($normalizedCityKey);
$cache->setCreatedAt(new \DateTimeImmutable());
$cache->setLastFetchedAt(new \DateTimeImmutable());
$existing = [];
} else {
$existing = $cache->getHotelsJson();
}
// indexation par ID pour éviter doublons
$indexed = [];
foreach ($existing as $keyHotel => $existHotel) {
$indexed[$normalizeKey($keyHotel)] = $existHotel;
}
foreach ($newHotels as $keyNewHotel => $hotel) {
$indexed[$normalizeKey($keyNewHotel)] = $hotel; // overwrite si existe
}
$cache->setHotelsJson($indexed);
$cache->setLastFetchedAt(new \DateTimeImmutable());
$this->entityManager->persist($cache);
$this->entityManager->flush();
}
public function getPagesHub(string $cityId): array
{
$parameters = [
'cityId' => $cityId
];
try {
$apiResponse = $this->helpers->getApiResponse(
'POST',
$this->parameterService->getHotelHubUrl(),
HUB_HOTEL_API_PAGES_ENDPOINT,
$this->parameterService->getHotelHubApiKey(),
$parameters
);
if (key_exists('response', $apiResponse)) {
return $apiResponse['response'];
}
} catch (Exception $e) {
return [$e->getMessage()];
}
return [];
}
private function normalizeHotelNameKey(string $name): string
{
// Translittère les accents (é -> e, ü -> u, etc.)
$transliterated = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) ?: $name;
// Ne garde que les caractères alphanumériques, en majuscules
return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $transliterated));
}
}