2 Commits
Author SHA1 Message Date
Daniel-Garmig 15a884fafa PriceService + XDEBUG.
- Arreglados problemas que impedian el funcionamiento de xdebug para hacer debug.
- Correción errores PriceService.
2024-05-01 20:21:14 +02:00
Daniel-Garmig 9c8759e24d FareTable y RoomFares
- Implementación modelo tarifarios y tarifas.
- Implementación inicial servicio precios.
2024-05-01 18:07:49 +02:00
11 changed files with 461 additions and 44 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"request": "launch",
"port": 9003,
"pathMappings": {
"/app" : "/src"
"/app" : "${workspaceFolder}"
}
},
{
+4 -3
View File
@@ -1,10 +1,10 @@
# InstantRooms
# SomniaRooms
A Hotel Room booking system.
Powered by:
- PHP Synfony.
- Easyadmin
- PHP Symfony.
- Easyadmin.
## Commands:
@@ -16,6 +16,7 @@ docker compose build --no-cache
Start Containers:
```
docker compose up --pull always -d --wait
docker compose --env-file .\.env.local up -d --watch
```
Stop containers
+54
View File
@@ -0,0 +1,54 @@
# SomniaRooms - App Configuration Variables.
This document contains a list of all config variables of SomniaRooms. Variables are stored in DB (table _`config_variable`_) and can be configured from admin panel.
Variables are divided in sections. Some sections are ony accesible from SomniaRoom, but others can be retrieved by anyone using config endpoint (_`/config`_).
## List of sections:
WIP
TODO -> Add default table with basic information to all sections.
### **Company**
Variables in this section contains information about the Hotel.
- SectionName -> company
- Availability -> public
- Nº variables -> X
**Variable list:**
- Name - Name of hotel.
### **Price Management**
Configure how backend services calculates prices for bookings.
- SectionName -> price_mng
- Availability -> internal
- Nº variables -> X
**Variable list:**
WIP
#### FinalPriceCalculation.
Establece cómo se deben tratar los precios de cada día para generar el precio final de la estancia.
Algunos algoritmos para estos serán:
- Precio más alto.
- Precio más bajo.
- Suma de precios. --> Por defecto.
- Media de precios.
#### MultipleRoomFareValid
Estrategia de selección de tarifa en caso de que haya varias válidas (mientras no exista un sistema de prioridad de tarifas...)
Posibles algoritmos:
- Precio más alto.
- Precio más bajo.
- Media.
+5 -2
View File
@@ -2,6 +2,9 @@
; See https://github.com/docker/for-linux/issues/264
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
; Add `start_with_request=yes` to start debug session on each request
xdebug.client_host = host.docker.internal
xdebug.client_host=host.docker.internal
xdebug.start_with_request=yes
xdebug.mode=debug
;xdebug.mode=debug
xdebug.discover_client_host=1
xdebug.idekey=docker
;xdebug.log=/dev/stdout
@@ -20,11 +20,12 @@ class FareTableCrudController extends AbstractCrudController
{
//$catRepo = $this->entityManager->getRepository(SomeEntity::class);
yield TextField::new('comment')->setLabel('Name');
yield AssociationField::new('roomCategory');
yield DateField::new('startDate');
yield DateField::new('endDate');
yield TextField::new('comment');
}
}
@@ -9,6 +9,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\MoneyField;
use EasyCorp\Bundle\EasyAdminBundle\Field\NumberField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
class RoomFareCrudController extends AbstractCrudController
{
@@ -27,7 +28,9 @@ class RoomFareCrudController extends AbstractCrudController
yield NumberField::new ('guestNumber');
yield MoneyField::new ('fareAmount')->setCurrency('EUR');
$dayTypeList = ["MONDAY" => 1, "TUESDAY" => 2, "WEDNESDAY" => 3, "THURSDAY" => 4, "FRIDAY" => 5, "SATURDAY" => 6, "SUNDAY" => 0];
yield ChoiceField::new('dayType')->setChoices($dayTypeList);
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Controller;
use App\Repository\FareTableRepository;
use App\Repository\RoomAvailabilityRepository;
use App\Repository\RoomCategoryRepository;
use App\Service\PricesService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SearchRoomsController extends AbstractController
{
#[Route('/search/availability', name: 'app_search_availability')]
public function getAvailabilityForPeriod(Request $request, RoomAvailabilityRepository $repoAvailability): Response
{
$payload = $request->toArray();
if (\is_null($payload)) {
throw new \Exception('Invalid Payload', 400);
}
$filter = $payload["filter"];
$startDate = new \DateTime($filter["startDate"]);
$endDate = new \DateTime($filter["endDate"]);
$rs = $repoAvailability->getAvailabilityForPeriod($startDate, $endDate);
return $this->json($rs);
}
#[Route('/search/availabilityDetails', name: 'app_search_availabilityDetails')]
public function getAvailabilityForPeriodDetails(Request $request, RoomAvailabilityRepository $repoAvailability): Response
{
$payload = $request->toArray();
if (\is_null($payload)) {
throw new \Exception('Invalid Payload', 400);
}
$filter = $payload["filter"];
$startDate = new \DateTime($filter["startDate"]);
$endDate = new \DateTime($filter["endDate"]);
$rs = $repoAvailability->getAvailabilityForPeriodDetails($startDate, $endDate);
return $this->json($rs);
}
#[Route('/search/fares', name: 'app_search_faresForCategory')]
public function getFaresForCategoryOnPeriod(Request $request, RoomCategoryRepository $repoCat): Response
{
$payload = $request->toArray();
if (\is_null($payload)) {
throw new \Exception('Invalid Payload', 400);
}
$filter = $payload["filter"];
$catId = $filter["roomCategory"];
if ($repoCat->find($catId) === null) {
throw new \Exception('Invalid Category', 400);
}
$startDate = new \DateTime($filter["startDate"]);
$endDate = new \DateTime($filter["endDate"]);
$daysDiff = $startDate->diff($endDate)->days;
if (!$daysDiff || $startDate > $endDate) {
throw new \Exception('Invalid time interval', 400);
}
return $this->json(["prices" => $dailyPrices]);
}
#[Route('/search/debug', name: 'app_search_faresDebug')]
public function faresDebug(Request $request, PricesService $priceService, FareTableRepository $repoFareTable, RoomCategoryRepository $repoCat): Response
{
$fareTable[] = $repoFareTable->find(1);
$fareTable[] = $repoFareTable->find(2);
$cat = $repoCat->find(4);
$startDate = new \DateTime("2024-04-27");
$endDate = new \DateTime("2024-05-02");
$rs = $priceService->getFinalPricesForSearch($cat, $startDate, $endDate);
return $this->json($rs);
}
}
+51 -4
View File
@@ -23,10 +23,10 @@ class FareTable
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $startDate = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
#[ORM\Column(type : Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $endDate = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(length : 255, nullable: true)]
private ?string $comment = null;
/**
@@ -123,10 +123,57 @@ class FareTable
return $this;
}
public function __toString() : string {
if(!\is_null($this->comment)) {
public function __toString(): string
{
if (!\is_null($this->comment)) {
return $this->comment;
}
return $this->id;
}
/**
* Get max guest number for current roomFares.
*/
public function getMaxGuestNumber(): int
{
$maxGuests = $this->getRoomFares()->reduce(function (int $acc, RoomFare $value): int {
return \max($acc, $value->getGuestNumber());
}, -1);
return $maxGuests;
}
/**
* Get min guest number for current roomFares.
*/
public function getMinGuestNumber(): int
{
$firstGuestNum = $this->getRoomFares()->first()->getGuestNumber() ?? 999;
$maxGuests = $this->getRoomFares()->reduce(function (int $acc, RoomFare $value): int {
return \min($acc, $value->getGuestNumber());
}, $firstGuestNum);
return $maxGuests;
}
/**
* Obtiene las tarifas que aplican al GuestNumber indicado.
* @param int $guestNumber GuestNumber para búsqueda.
* @return RoomFare[] Lista de tarifas.
*/
public function getRoomFaresByGuestNumber(int $guestNumber)
{
$roomFareList = $this->getRoomFares()->filter(function (RoomFare $element) use ($guestNumber) {
return $element->getGuestNumber() == $guestNumber;
});
return $roomFareList;
}
public function isActiveOnDate(\DateTimeInterface $date): bool
{
$active = ($this->startDate <= $date && $this->endDate > $date);
return $active;
}
}
+57 -23
View File
@@ -21,28 +21,62 @@ class FareTableRepository extends ServiceEntityRepository
parent::__construct($registry, FareTable::class);
}
// /**
// * @return FareTable[] Returns an array of FareTable objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('f.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
/**
* Gets all fareTables for time period.
* @param integer|string ID Categoria.
* @param DateTimeInterface Period start date
* @param DateTimeInterface Period end date
* @return array Array with all faretables active in given period.
*/
public function findFareTable(int $roomCategory, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$strStartDate = $startDate->format('Y-m-d');
$strEndDate = $endDate->format('Y-m-d');
// public function findOneBySomeField($value): ?FareTable
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
return $this->createQueryBuilder('f')
->andWhere('f.roomCategory = :category')
->andWhere('f.startDate < :endDate')
->andWhere('f.endDate >= :startDate')
->setParameter('category', $roomCategory)
->setParameter('startDate', $strStartDate)
->setParameter('endDate', $strEndDate)
->getQuery()
->getResult();
}
/**
* Obtiene una lista con los números de huéspedes permitidos por todos los tarifarios pasados.
* @param FareTable[] $fareTableList Lista con los FareTable a consultar.
* @return int[] Lista con los números de huéspedes permitidos.
*/
public function getAllowedGuestNumberOnFareTableList(array $fareTableList): array
{
$fareTableCount = count($fareTableList);
$fareTableIdList = \array_map(function (FareTable $element) {
return $element->getId();
}, $fareTableList);
$strIdList = \implode(",", $fareTableIdList);
//Subquery -> Obtiene los números de huéspedes de las tarifas de un tarifario.
//Query -> Cuenta en cuantos tarifarios aparece cada número de huéspedes y queda con aquellos que aparecen en todos (having).
$sql = "SELECT guest_number
FROM (SELECT DISTINCT fare_table_id, guest_number FROM room_fare WHERE fare_table_id IN ($strIdList))
GROUP BY guest_number
HAVING COUNT(fare_table_id) = $fareTableCount";
$conn = $this->getEntityManager()->getConnection();
$rs = $conn->executeQuery($sql);
$allowedGuestNumbers = [];
while (true) {
$value = $rs->fetchOne();
if (!$value) {
break;
}
$allowedGuestNumbers[] = $value;
}
return $allowedGuestNumbers;
}
}
+37 -9
View File
@@ -34,13 +34,41 @@ class RoomAvailabilityRepository extends ServiceEntityRepository
->getResult();
}
// public function findOneBySomeField($value): ?RoomAvailability
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
/**
* @param \DateTimeInterface Period start (included in range).
* @param \DateTimeInterface Period end (not included in range).
* @return RoomAvailability[] Returns an array of RoomAvailability objects
*/
public function getAvailabilityForPeriod(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$strStartDate = $startDate->format('Y-m-d');
$strEndDate = $endDate->format('Y-m-d');
return $this->createQueryBuilder('ra')
->select('IDENTITY(ra.roomCategory) as roomCategory, MIN(ra.numAvailable) as availability')
->andWhere('ra.day >= :startDate')
->andWhere('ra.day < :endDate')
->groupBy('ra.roomCategory')
->setParameter('startDate', $strStartDate)
->setParameter('endDate', $strEndDate)
->getQuery()
->getArrayResult()
;
}
public function getAvailabilityForPeriodDetails(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$strStartDate = $startDate->format('Y-m-d');
$strEndDate = $endDate->format('Y-m-d');
return $this->createQueryBuilder('ra')
->andWhere('ra.day >= :startDate')
->andWhere('ra.day < :endDate')
->setParameter('startDate', $strStartDate)
->setParameter('endDate', $strEndDate)
->getQuery()
->getArrayResult()
;
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace App\Service;
use App\Entity\FareTable;
use App\Entity\RoomCategory;
use App\Repository\FareTableRepository;
use App\Repository\RoomFareRepository;
use Doctrine\Common\Collections\ArrayCollection;
/**
* 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
{
public function __construct(
private FareTableRepository $repoFareTable,
private RoomFareRepository $repoRoomFares
) {
}
/**
* 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) {
return "Invalid date period :( ";
}
$fareTablesForPeriod = $this->repoFareTable->findFareTable($category->getId(), $startDate, $endDate);
//Obtenemos el número de personas permitido por todos los tarifarios activos.
$allowedGuestNumbers = $this->repoFareTable->getAllowedGuestNumberOnFareTableList($fareTablesForPeriod);
$rs = [];
//Calculamos los precios para cada GuestNumber permitido.
foreach ($allowedGuestNumbers as $currentGuestNumber) {
//Calculamos el precio de cada día.
// e.g. --> [ "2024-04-30" => 20, "2024-05-01" => 28, "2024-05-02" => 20, ...].
$dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $fareTablesForPeriod);
$rs[$currentGuestNumber] = $dailyPrices;
}
//Aplicamos la estrategia de precios configurada para calcular el precio final de la estancia.
// TODOME: Crear y configurar diversas estrategias de precios -> Por el momento utilizamos "Suma del precio de cada día".
$finalResult = [];
foreach ($rs as $guestNum => $priceArray) {
$finalAmount = \array_reduce($priceArray, function ($acc, $element) {
return $acc + $element;
});
$finalResult[$guestNum] = $finalAmount;
}
return $finalResult;
}
/**
* 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, ...].
*/
public function getDailyPricesForSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate, int $guestNumber, array $fareTableList): array
{
$daysDiff = $startDate->diff($endDate)->days;
$dailyPrices = [];
$currentDate = new \DateTime($startDate->format("Y-m-d")); //TODOME: Comprobar si es tipo referencia y está cambiando.
for ($i = 0; $i < $daysDiff; $i++) {
$strCurrentDate = $currentDate->format("Y-m-d");
//Obtenemos el tarifario que aplica --> TODOME: Buscar si se puede cachear esto, para no recarlcularlo para cada GuestNumber.
$currentFareTableList = \array_filter($fareTableList, function (FareTable $element) use ($currentDate) {
return $element->isActiveOnDate($currentDate);
});
if (count($currentFareTableList) != 1) {
$dailyPrices[$strCurrentDate] = "ERROR - Number of FareTable found: " . count($currentFareTableList);
continue;
}
$currentFareTable = array_values($currentFareTableList)[0];
//TODOME: -> Obtener precio específico (RoomPrices).
//Si no hay precio, calcular mediante tarifarios.
$roomFares = $currentFareTable->getRoomFaresByGuestNumber($guestNumber);
if (count($roomFares) < 1) {
$dailyPrices[$strCurrentDate] = "ERROR - Invalid RoomFares found";
continue;
}
//Si sólo hay 1 tarifa, aplico esa, sino calculo la apropiada según condiciones.
$fareAmount = null;
if (count($roomFares) == 1) {
$fareAmount = $roomFares->first()->getFareAmount();
} else {
$fareAmount = $this->calculateFareApplyingConditions($roomFares, $currentDate);
}
$dailyPrices[$strCurrentDate] = $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 RoomFare[] $roomFareList Lista con las posibles tarifas.
* @param DateTimeInterface $fareCondDate Fecha utilizada para las condiciones.
*
*/
public function calculateFareApplyingConditions(ArrayCollection $roomFareList, \DateTimeInterface $fareCondDate): int
{
$currentDayOfWeek = $fareCondDate->format("w");
//Comprobamos si a alguna de las tarifas se la aplica la condición de "DayType" y coincide con el actual.
// Si es así, devolvemos su valor.
foreach ($roomFareList as $roomFare) {
$dayType = $roomFare->getDayType();
if ($dayType == null || $dayType == "") {
continue;
}
if($dayType == $currentDayOfWeek) {
return $roomFare->getFareAmount();
}
}
//TODOME: Si existen múltiples tarifas posibles, debería haber una estrategia para elegir una u otra. Variable de configuración para esto.
$faresWithoutConditions = $roomFareList->filter(function ($element) {
return $element->getDayType() == null;
});
return $faresWithoutConditions->first()->getFareAmount();
}
}