374 lines
15 KiB
PHP
374 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Service;
|
|
|
|
use App\Consts\ConfigVariableConsts;
|
|
use App\Entity\FareTable;
|
|
use App\Entity\RoomCategory;
|
|
use App\Entity\RoomFare;
|
|
use App\Repository\FareTableRepository;
|
|
use App\Repository\RoomCategoryRepository;
|
|
use App\Repository\RoomFareRepository;
|
|
use App\Repository\RoomPriceRepository;
|
|
use App\Response\SearchPricesResponse;
|
|
use App\Response\SearchPricesResponsePriceItem;
|
|
use App\Util\PriceSelectionMethods;
|
|
use Doctrine\Common\Collections\Collection;
|
|
use Ds\Set;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Servicio para calcular los precios de las estancias.
|
|
* En un futuro este servicio deberá admitir múltiples estrategias de cálculo de precios finales.
|
|
*/
|
|
class PricesService
|
|
{
|
|
private string $configFareMethod;
|
|
private string $configPriceMethod;
|
|
|
|
public function __construct(
|
|
private RoomCategoryRepository $repoRoomCategory,
|
|
private FareTableRepository $repoFareTable,
|
|
private RoomPriceRepository $repoRoomPrices,
|
|
private ConfigVariableService $configVarService,
|
|
private LoggerInterface $logger
|
|
) {
|
|
$this->configFareMethod = $configVarService->getVariableFromConst(ConfigVariableConsts::CONFIG_PRICES_FAREMETHOD)->getValue();
|
|
$this->configPriceMethod = $configVarService->getVariableFromConst(ConfigVariableConsts::CONFIG_PRICES_PRICEMETHOD)->getValue();
|
|
}
|
|
|
|
/**
|
|
* Calcula los precios de la estancia para la categoría y periodo indicados.
|
|
* Devolverá para cada numero de personas, el precio final de la estancia.
|
|
*
|
|
* e. g. --> [ 2 => 120€, 3 => 180€ ]
|
|
*
|
|
* @return array Contiene los precios en función del número de personas.
|
|
*/
|
|
public function getFinalPricesForSearch(RoomCategory $category, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
|
|
{
|
|
$daysDiff = $startDate->diff($endDate)->days;
|
|
if (!$daysDiff) {
|
|
$this->logger->warning("PricesService.getFinalPricesForSearch - Invalid search period");
|
|
return [];
|
|
}
|
|
|
|
$fareTablesForPeriod = $this->repoFareTable->findFareTable($category->getId(), $startDate, $endDate);
|
|
$roomPriceList = $this->repoRoomPrices->getListForPeriod($startDate, $endDate, $category->getId());
|
|
|
|
//"Organize" RoomPrices.
|
|
$filteredRoomPriceList = [];
|
|
foreach ($roomPriceList as $roomPrice) {
|
|
$priceDate = $roomPrice->getDateString();
|
|
$filteredRoomPriceList[$roomPrice->getGuestNumber()][$priceDate] = $roomPrice->getPrice();
|
|
}
|
|
|
|
//Obtenemos el número de personas permitido por todos los tarifarios activos.
|
|
$allowedGuestNumberList = $this->getAllowedGuestNumberFromFaretableList($fareTablesForPeriod);
|
|
|
|
//Calculate stay price for each allowed GuestNumber.
|
|
$priceItems = [];
|
|
foreach ($allowedGuestNumberList as $currentGuestNumber) {
|
|
|
|
//Get RoomPrices for current GuestNumber.
|
|
$roomPricesForGuestNum = [];
|
|
if (array_key_exists($currentGuestNumber, $filteredRoomPriceList)) {
|
|
$roomPricesForGuestNum = $filteredRoomPriceList[$currentGuestNumber];
|
|
}
|
|
|
|
//Calculate the price for each day.
|
|
$dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $fareTablesForPeriod, $roomPricesForGuestNum);
|
|
|
|
$stayPrice = $this->getTotalPriceForStay($dailyPrices);
|
|
|
|
$item = new SearchPricesResponsePriceItem();
|
|
$item->guestNumber = $currentGuestNumber;
|
|
$item->price = $stayPrice;
|
|
$priceItems[] = $item;
|
|
}
|
|
|
|
return $priceItems;
|
|
}
|
|
|
|
/**
|
|
* Calculate stay total price on each roomCategory for the given time period
|
|
* Returns a list with all roomCategories, containing the total price by guestnumber.
|
|
*
|
|
* @return SearchPricesResponse[] Total prices for each guestnumber of each roomCategory.
|
|
*/
|
|
public function getRoomSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
|
|
{
|
|
$daysDiff = $startDate->diff($endDate)->days;
|
|
if (!$daysDiff) {
|
|
$this->logger->warning("PricesService.getFinalPricesForSearch - Invalid search period");
|
|
return [];
|
|
}
|
|
$rs = [];
|
|
|
|
$roomCategoryList = $this->repoRoomCategory->findAll();
|
|
$fareTableList = $this->repoFareTable->getFareTableListForPeriod($startDate, $endDate);
|
|
|
|
$faretablesByRoomCategory = [];
|
|
foreach ($fareTableList as $faretable) {
|
|
$faretablesByRoomCategory[$faretable->getRoomCategory()->getId()][] = $faretable;
|
|
}
|
|
|
|
//Get and filter prices.
|
|
$priceList = $this->repoRoomPrices->getListForPeriod($startDate, $endDate);
|
|
$filteredPrices = [];
|
|
foreach ($priceList as $price) {
|
|
$priceDate = $price->getDateString();
|
|
//Store price in multi-level array by: RoomCategoryId, GuestNumber and Date.
|
|
$filteredPrices[$price->getRoomCategory()->getId()][$price->getGuestNumber()][$priceDate] = $price->getPrice();
|
|
}
|
|
|
|
foreach ($roomCategoryList as $roomCat) {
|
|
$currentRoomCategoryId = $roomCat->getId();
|
|
|
|
//Get Faretables for RoomCategory.
|
|
$roomFaretables = [];
|
|
if (array_key_exists($currentRoomCategoryId, $faretablesByRoomCategory)) {
|
|
$roomFaretables = $faretablesByRoomCategory[$currentRoomCategoryId];
|
|
}
|
|
|
|
//Get RoomPrices for current RoomCategory.
|
|
$roomPricesForCategory = [];
|
|
if (array_key_exists($currentRoomCategoryId, $filteredPrices)) {
|
|
$roomPricesForCategory = $filteredPrices[$currentRoomCategoryId];
|
|
}
|
|
|
|
//Calculate distinct GuestNumber for RoomCategory by it's faretables.
|
|
$allowedGuestNumberList = $this->getAllowedGuestNumberFromFaretableList($roomFaretables);
|
|
|
|
//Calculate stay price for each allowed GuestNumber.
|
|
$priceItems = [];
|
|
foreach ($allowedGuestNumberList as $currentGuestNumber) {
|
|
|
|
//Get RoomPrices for current GuestNumber.
|
|
$roomPricesForGuestNum = [];
|
|
if (array_key_exists($currentGuestNumber, $roomPricesForCategory)) {
|
|
$roomPricesForGuestNum = $roomPricesForCategory[$currentGuestNumber];
|
|
}
|
|
|
|
//Calculate the price for each day.
|
|
$dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $roomFaretables, $roomPricesForGuestNum);
|
|
|
|
$stayPrice = $this->getTotalPriceForStay($dailyPrices);
|
|
|
|
$item = new SearchPricesResponsePriceItem();
|
|
$item->guestNumber = $currentGuestNumber;
|
|
$item->price = $stayPrice;
|
|
$priceItems[] = $item;
|
|
}
|
|
|
|
$roomItem = new SearchPricesResponse();
|
|
$roomItem->roomCategoryId = $roomCat->getId();
|
|
$roomItem->prices = $priceItems;
|
|
|
|
$rs[] = $roomItem;
|
|
}
|
|
|
|
return $rs;
|
|
}
|
|
|
|
/**
|
|
* Dado un número de huéspedes, calcula el precio de cada día del periodo indicado.
|
|
* @param \DateTimeInterface $startDate Fecha de inicio del periodo.
|
|
* @param \DateTimeInterface $endDate Fecha fin del periodo.
|
|
* @param int $guestNumber Número de huéspedes para los que calcular los precios.
|
|
* @param array $fareTableList Array con los tarifarios que aplican en el periodo indicado.
|
|
* @return array Devuelve un array con los precios: e.g. --> [ "2024-04-30" => 2500, "2024-05-01" => 3500, "2024-05-02" => 3000, ...].
|
|
*/
|
|
private function getDailyPricesForSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate, int $guestNumber, array $fareTableList, array $roomPricesByGuestNum): array
|
|
{
|
|
$daysDiff = $startDate->diff($endDate)->days;
|
|
|
|
$dailyPrices = [];
|
|
$currentDate = new \DateTime($startDate->format("Y-m-d")); //Copy start date to operate.
|
|
for ($i = 0; $i < $daysDiff; $i++) {
|
|
$strCurrentDate = $currentDate->format("Y-m-d");
|
|
|
|
//Get Faretable for current day.
|
|
//TODO: Possible optimization - This data doesn't need to be recalculated so often.
|
|
$currentFareTableList = \array_filter($fareTableList, function (FareTable $element) use ($currentDate) {
|
|
return $element->isActiveOnDate($currentDate);
|
|
});
|
|
|
|
if (count($currentFareTableList) != 1) {
|
|
$this->logger->error(
|
|
"PricesService.getDailyPricesForSearch: Can't calculate prices: Invalid number of FareTable found: " . count($currentFareTableList),
|
|
[
|
|
"startDate" => $startDate,
|
|
"endDate" => $endDate,
|
|
"fareTableList" => $fareTableList,
|
|
"currentFareTableList" => $currentFareTableList
|
|
]
|
|
);
|
|
return [];
|
|
}
|
|
|
|
|
|
//Check for Prices.
|
|
$roomPrice = null;
|
|
if (array_key_exists($strCurrentDate, $roomPricesByGuestNum)) {
|
|
$roomPrice = $roomPricesByGuestNum[$strCurrentDate];
|
|
}
|
|
|
|
//Si no hay precio, calcular mediante tarifarios.
|
|
$fareAmount = null;
|
|
if ($roomPrice == null) {
|
|
//Get Faretables for guestNumber.
|
|
$currentFareTable = array_values($currentFareTableList)[0];
|
|
$roomFares = $currentFareTable->getRoomFaresByGuestNumber($guestNumber);
|
|
if (count($roomFares) < 1) {
|
|
$this->logger->error(
|
|
"PricesService.getDailyPricesForSearch: Can't calculate prices: Found faretable without fares",
|
|
[
|
|
"currentFareTable" => $currentFareTable,
|
|
"guestNumber" => $guestNumber,
|
|
"roomFaresForGuestNumber" => $roomFares
|
|
]
|
|
);
|
|
return [];
|
|
}
|
|
|
|
//Calculate RoomFare.
|
|
$fareAmount = $this->calculateFareApplyingConditions($roomFares, $currentDate);
|
|
}
|
|
|
|
//If any price can't be calculated, NO price should be returned.
|
|
if ($roomPrice == null && $fareAmount == null) {
|
|
return [];
|
|
}
|
|
|
|
$dailyPrices[$strCurrentDate] = $roomPrice ?? $fareAmount;
|
|
|
|
//Add 1 day to current date.
|
|
$currentDate->add(new \DateInterval("P1D"));
|
|
}
|
|
|
|
return $dailyPrices;
|
|
}
|
|
|
|
/**
|
|
* Calcula el precio a aplicar según las condiciones de las tarifas.
|
|
* @param Collection<RoomFare> $roomFareList Lista con las posibles tarifas.
|
|
* @param DateTimeInterface $fareCondDate Fecha utilizada para las condiciones.
|
|
*
|
|
*/
|
|
private function calculateFareApplyingConditions(Collection $roomFareList, \DateTimeInterface $fareCondDate): int|null
|
|
{
|
|
if (count($roomFareList) == 1) {
|
|
return $roomFareList->first()->getFareAmount();
|
|
}
|
|
|
|
$currentDayOfWeek = $fareCondDate->format("w");
|
|
//Filter applicable fares.
|
|
//TODO: Possible optimization - This data doesn't need to be recalculated so often; just once for each FareTable.
|
|
/** @var RoomFare[] */
|
|
$applFaresByDay = [];
|
|
/** @var RoomFare[] */
|
|
$applFaresNonCondition = [];
|
|
foreach ($roomFareList as $fare) {
|
|
$dayType = $fare->getDayType();
|
|
if ($dayType == null || $dayType == "") {
|
|
$applFaresNonCondition[] = $fare;
|
|
continue;
|
|
}
|
|
if ($dayType == $currentDayOfWeek) {
|
|
$applFaresByDay[] = $fare;
|
|
}
|
|
}
|
|
|
|
//Return final fare value.
|
|
//Use fares for current weekday.
|
|
$finalFare = $this->getPriceFromRoomFareList($applFaresByDay);
|
|
if ($finalFare != null) {
|
|
return $finalFare;
|
|
}
|
|
|
|
//Use non-conditional fares.
|
|
$finalFare = $this->getPriceFromRoomFareList($applFaresNonCondition);
|
|
if ($finalFare != null) {
|
|
return $finalFare;
|
|
}
|
|
|
|
//No valid fare found!
|
|
$this->logger->error(
|
|
"PricesService.calculateFareApplyingConditions: No RoomFare found. Price can't be calculated.",
|
|
["RoomFareList" => $roomFareList, "Date" => $fareCondDate]
|
|
);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the fare with bigger priority from a list of fares.
|
|
* @param RoomFare[] $roomFareList List of RoomFares.
|
|
*/
|
|
private function getPriceFromRoomFareList(array $roomFareList): int|null
|
|
{
|
|
$countFares = count($roomFareList);
|
|
if ($roomFareList == null || $countFares == 0) {
|
|
return null;
|
|
}
|
|
|
|
if ($countFares == 1) {
|
|
return $roomFareList[0]->getFareAmount();
|
|
}
|
|
|
|
switch ($this->configFareMethod) {
|
|
case 'HIGHER':
|
|
return PriceSelectionMethods::selectFare_Highest($roomFareList);
|
|
case 'LOWER':
|
|
return PriceSelectionMethods::selectFare_Lowest($roomFareList);
|
|
case 'AVERAGE':
|
|
return PriceSelectionMethods::selectFare_Average($roomFareList);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a list of allowed GuestNumbers from a list of FareTables.
|
|
* @param FareTable[] $faretableList List of Faretables
|
|
* @return int[] Array of allowed guestNumbers from faretable list.
|
|
*/
|
|
private function getAllowedGuestNumberFromFaretableList(array $faretableList): array
|
|
{
|
|
if (count($faretableList) == 0) {
|
|
return [];
|
|
}
|
|
|
|
$guestSet = new Set();
|
|
foreach ($faretableList as $table) {
|
|
$fares = $table->getRoomFares();
|
|
foreach ($fares as $roomFare) {
|
|
$guestSet->add($roomFare->getGuestNumber());
|
|
}
|
|
}
|
|
|
|
return $guestSet->toArray();
|
|
}
|
|
|
|
/**
|
|
* Calculate total stay price given a list of prices.
|
|
* @param array $dailyPrices Array containing the price for each day.
|
|
* @return int Total stay price.
|
|
*/
|
|
private function getTotalPriceForStay(array $dailyPrices): int|null
|
|
{
|
|
switch ($this->configPriceMethod) {
|
|
case 'SUM':
|
|
return PriceSelectionMethods::priceMethod_Sum($dailyPrices);
|
|
case 'AVERAGE':
|
|
return PriceSelectionMethods::priceMethod_Average($dailyPrices);
|
|
case 'HIGHER':
|
|
return PriceSelectionMethods::priceMethod_Highest($dailyPrices);
|
|
case 'LOWER':
|
|
return PriceSelectionMethods::priceMethod_Lowest($dailyPrices);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|