3 Commits
Author SHA1 Message Date
Daniel-Garmig 7673521708 API - Search Prices:
CI / Docker Lint (push) Successful in 31s
- Added endpoint to get prices for all RoomCategories.
- Improvements to Prices API.
2025-10-13 14:52:36 +02:00
Daniel-Garmig aaae8563c6 Fixed xdebug step debugging when using Podman containers. 2025-10-13 14:40:40 +02:00
Daniel-Garmig 1051921a20 Small Changes:
- Config Vars - Added config variable "CompanyLogoSize" to set the logo size in the frontend.
- API - Prices - Fixed returned string for invalid date periods.
2025-10-13 14:39:01 +02:00
10 changed files with 246 additions and 45 deletions
+3
View File
@@ -17,8 +17,11 @@ docker compose build --no-cache
Start Containers: Start Containers:
```bash ```bash
# Run SomniaRooms
docker compose up --pull always -d --wait docker compose up --pull always -d --wait
docker compose --env-file .\.env.local up -d --wait docker compose --env-file .\.env.local up -d --wait
# Run SomniaRooms with debug.
docker compose --env-file .\.env.dev.local up -d --wait
``` ```
Stop containers Stop containers
+2 -1
View File
@@ -19,13 +19,14 @@ services:
MERCURE_EXTRA_DIRECTIVES: demo MERCURE_EXTRA_DIRECTIVES: demo
# See https://xdebug.org/docs/all_settings#mode # See https://xdebug.org/docs/all_settings#mode
XDEBUG_MODE: "${XDEBUG_MODE:-off}" XDEBUG_MODE: "${XDEBUG_MODE:-off}"
XDEBUG_CONFIG: "client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} discover_client_host=${XDEBUG_DISCOVER_CLIENT_HOST:-off}" XDEBUG_CONFIG: "client_host=${XDEBUG_CLIENT_HOST:-host.containers.internal} discover_client_host=${XDEBUG_DISCOVER_CLIENT_HOST:-off}"
APP_ENV: "${APP_ENV:-dev}" APP_ENV: "${APP_ENV:-dev}"
# Change APP log path. # Change APP log path.
# SOMNIA_LOGSDIR: "/somnia_logs" # SOMNIA_LOGSDIR: "/somnia_logs"
extra_hosts: extra_hosts:
# Ensure that host.docker.internal is correctly defined on Linux # Ensure that host.docker.internal is correctly defined on Linux
- host.docker.internal:host-gateway - host.docker.internal:host-gateway
- host.containers.internal:host-gateway
tty: true tty: true
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
+2
View File
@@ -12,6 +12,7 @@ class ConfigVariableConsts
public const CONFIG_COMPANY_NAME = ["CompanyName" => [ConfigVariableSectionEnum::Company, "SomniaRooms"]]; public const CONFIG_COMPANY_NAME = ["CompanyName" => [ConfigVariableSectionEnum::Company, "SomniaRooms"]];
public const CONFIG_COMPANY_WEBSITE = ["CompanyWebsite" => [ConfigVariableSectionEnum::Company, "somnia.dev"]]; public const CONFIG_COMPANY_WEBSITE = ["CompanyWebsite" => [ConfigVariableSectionEnum::Company, "somnia.dev"]];
public const CONFIG_COMPANY_LOGO = ["CompanyLogo" => [ConfigVariableSectionEnum::Company, ""]]; public const CONFIG_COMPANY_LOGO = ["CompanyLogo" => [ConfigVariableSectionEnum::Company, ""]];
public const CONFIG_COMPANY_LOGOSIZE = ["CompanyLogoSize" => [ConfigVariableSectionEnum::Company, ""]];
public const CONFIG_COMPANY_ADDRESS = ["CompanyAddress" => [ConfigVariableSectionEnum::Company, ""]]; public const CONFIG_COMPANY_ADDRESS = ["CompanyAddress" => [ConfigVariableSectionEnum::Company, ""]];
public const CONFIG_COMPANY_EMAIL = ["CompanyEmail" => [ConfigVariableSectionEnum::Company, ""]]; public const CONFIG_COMPANY_EMAIL = ["CompanyEmail" => [ConfigVariableSectionEnum::Company, ""]];
public const CONFIG_COMPANY_PHONE = ["CompanyPhone" => [ConfigVariableSectionEnum::Company, ""]]; public const CONFIG_COMPANY_PHONE = ["CompanyPhone" => [ConfigVariableSectionEnum::Company, ""]];
@@ -42,6 +43,7 @@ class ConfigVariableConsts
ConfigVariableConsts::CONFIG_COMPANY_NAME, ConfigVariableConsts::CONFIG_COMPANY_NAME,
ConfigVariableConsts::CONFIG_COMPANY_WEBSITE, ConfigVariableConsts::CONFIG_COMPANY_WEBSITE,
ConfigVariableConsts::CONFIG_COMPANY_LOGO, ConfigVariableConsts::CONFIG_COMPANY_LOGO,
ConfigVariableConsts::CONFIG_COMPANY_LOGOSIZE,
ConfigVariableConsts::CONFIG_COMPANY_ADDRESS, ConfigVariableConsts::CONFIG_COMPANY_ADDRESS,
ConfigVariableConsts::CONFIG_COMPANY_EMAIL, ConfigVariableConsts::CONFIG_COMPANY_EMAIL,
ConfigVariableConsts::CONFIG_COMPANY_PHONE, ConfigVariableConsts::CONFIG_COMPANY_PHONE,
+40 -13
View File
@@ -6,10 +6,11 @@ use App\ApiErrorException;
use App\DTO\ApiErrorMessage; use App\DTO\ApiErrorMessage;
use App\Repository\RoomAvailabilityRepository; use App\Repository\RoomAvailabilityRepository;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
use App\Request\SearchAvailabilityPeriodRequest; use App\Request\SearchPeriodRequest;
use App\Response\SearchAvailabilityResponse; use App\Response\SearchAvailabilityResponse;
use App\Request\SearchPricesForCategoryRequest; use App\Request\SearchPricesForCategoryRequest;
use App\Response\SearchPricesForCategoryResponse; use App\Response\SearchPricesResponse;
use App\Response\SearchPricesResponsePriceItem;
use App\Service\PricesService; use App\Service\PricesService;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -43,12 +44,12 @@ class SearchRoomsController extends AbstractController
#[OA\RequestBody( #[OA\RequestBody(
content: new OA\JsonContent( content: new OA\JsonContent(
type: 'object', type: 'object',
ref: new Model(type: SearchAvailabilityPeriodRequest::class), ref: new Model(type: SearchPeriodRequest::class),
) )
)] )]
public function getAvailabilityForPeriod( public function getAvailabilityForPeriod(
RoomAvailabilityRepository $repoAvailability, RoomAvailabilityRepository $repoAvailability,
#[MapRequestPayload()] SearchAvailabilityPeriodRequest $searchRequest #[MapRequestPayload()] SearchPeriodRequest $searchRequest
): JsonResponse { ): JsonResponse {
if ($searchRequest->startDate >= $searchRequest->endDate) { if ($searchRequest->startDate >= $searchRequest->endDate) {
@@ -63,17 +64,43 @@ class SearchRoomsController extends AbstractController
} }
/** /**
* Get Availability for all rooms on each day of a period. * Get the price of each room on each day of a period.
* (Not used on Front-end APP) *
*/ */
#[Route('/api/search/availabilityDetails', name: 'app_search_availabilityDetails', methods: ["POST"], format: "json")] #[Route('/api/search/prices', name: 'app_search_prices', methods: ["POST"], format: "json")]
#[OA\Tag(name: "Searchs")] #[OA\Tag(name: "Searchs")]
public function getAvailabilityForPeriodDetails( #[OA\RequestBody(
RoomAvailabilityRepository $repoAvailability, content: new OA\JsonContent(
#[MapRequestPayload()] SearchAvailabilityPeriodRequest $searchRequest type: 'object',
ref: new Model(type: SearchPeriodRequest::class),
)
)]
#[OA\Response(
response: 200,
description: 'Total Prices for the period of each room',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(
property: "results",
type: "array",
items: new OA\Items(ref: new Model(type: SearchPricesResponse::class))
)
]
)
)]
public function getPricesForPeriod(
PricesService $priceService,
#[MapRequestPayload()] SearchPeriodRequest $searchRequest
): JsonResponse { ): JsonResponse {
if ($searchRequest->startDate >= $searchRequest->endDate) {
throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE),
'startDate cant be before endDate',
);
}
$rs = $repoAvailability->getAvailabilityForPeriodDetails($searchRequest->startDate, $searchRequest->endDate); $rs = $priceService->getRoomSearch($searchRequest->startDate, $searchRequest->endDate);
return $this->json(["results" => $rs]); return $this->json(["results" => $rs]);
} }
@@ -83,7 +110,7 @@ class SearchRoomsController extends AbstractController
* Returns price for a room category. * Returns price for a room category.
* This price is the total for all the selected days by guest number. * This price is the total for all the selected days by guest number.
*/ */
#[Route('/api/search/prices', name: 'app_search_pricesForCategory', methods: ["POST"], format: "json")] #[Route('/api/search/pricesCategory', name: 'app_search_pricesForCategory', methods: ["POST"], format: "json")]
#[OA\Tag(name: "Searchs")] #[OA\Tag(name: "Searchs")]
#[OA\Response( #[OA\Response(
response: 200, response: 200,
@@ -94,7 +121,7 @@ class SearchRoomsController extends AbstractController
new OA\Property( new OA\Property(
property: "results", property: "results",
type: "array", type: "array",
items: new OA\Items(ref: new Model(type: SearchPricesForCategoryResponse::class)) items: new OA\Items(ref: new Model(type: SearchPricesResponsePriceItem::class))
) )
] ]
) )
+20
View File
@@ -44,6 +44,26 @@ class FareTableRepository extends ServiceEntityRepository
->getResult(); ->getResult();
} }
/**
* Gets all fareTables for time period.
* @param integer|string ID Categoria.
* @param DateTimeInterface Period start date
* @param DateTimeInterface Period end date
* @return FareTable[] Array with all faretables active in given period.
*/
public function getFareTableListForPeriod(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$strStartDate = $startDate->format('Y-m-d');
$strEndDate = $endDate->format('Y-m-d');
$dql = 'SELECT f, rf, rc FROM App\Entity\FareTable f JOIN f.roomFares rf JOIN f.roomCategory rc WHERE f.startDate < :endDate AND f.endDate >= :startDate';
return $this->getEntityManager()->createQuery($dql)
->setParameter('startDate', $strStartDate)
->setParameter('endDate', $strEndDate)
->getResult();
}
/** /**
* Obtiene una lista con los números de huéspedes permitidos por todos los tarifarios pasados. * 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. * @param FareTable[] $fareTableList Lista con los FareTable a consultar.
@@ -6,7 +6,7 @@ use DateTimeInterface;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
class SearchAvailabilityPeriodRequest class SearchPeriodRequest
{ {
#[OA\Property(description: 'Period start date')] #[OA\Property(description: 'Period start date')]
#[Assert\NotBlank] #[Assert\NotBlank]
@@ -1,14 +0,0 @@
<?php
namespace App\Response;
use OpenApi\Attributes as OA;
class SearchPricesForCategoryResponse
{
#[OA\Property(description: 'Number of guests')]
public int $guestNumber;
#[OA\Property(description: 'room price')]
public int $price;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Response;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use OpenApi\Attributes\Items;
class SearchPricesResponse
{
#[OA\Property(description: 'RoomCategory Id')]
public int $roomCategoryId;
#[OA\Property(
description: 'RoomCategory prices by guest number',
type: 'array',
items: new Items(ref: new Model(type: SearchPricesResponsePriceItem::class))
)]
public array $prices;
}
@@ -0,0 +1,14 @@
<?php
namespace App\Response;
use OpenApi\Attributes as OA;
class SearchPricesResponsePriceItem
{
#[OA\Property(description: 'Guest number for price')]
public int $guestNumber;
#[OA\Property(description: 'Total price')]
public int $price;
}
+142 -14
View File
@@ -5,9 +5,13 @@ namespace App\Service;
use App\Entity\FareTable; use App\Entity\FareTable;
use App\Entity\RoomCategory; use App\Entity\RoomCategory;
use App\Repository\FareTableRepository; use App\Repository\FareTableRepository;
use App\Repository\RoomCategoryRepository;
use App\Repository\RoomFareRepository; use App\Repository\RoomFareRepository;
use App\Response\SearchPricesResponse;
use Doctrine\Common\Collections\ArrayCollection; use App\Response\SearchPricesResponsePriceItem;
use Doctrine\Common\Collections\Collection;
use Ds\Set;
use Psr\Log\LoggerInterface;
/** /**
* Servicio para calcular los precios de las estancias. * Servicio para calcular los precios de las estancias.
@@ -17,10 +21,11 @@ class PricesService
{ {
public function __construct( public function __construct(
private RoomCategoryRepository $repoRoomCategory,
private FareTableRepository $repoFareTable, private FareTableRepository $repoFareTable,
private RoomFareRepository $repoRoomFares private RoomFareRepository $repoRoomFares,
) { private LoggerInterface $logger
} ) {}
/** /**
* Calcula los precios de la estancia para la categoría y periodo indicados. * Calcula los precios de la estancia para la categoría y periodo indicados.
@@ -34,13 +39,14 @@ class PricesService
{ {
$daysDiff = $startDate->diff($endDate)->days; $daysDiff = $startDate->diff($endDate)->days;
if (!$daysDiff) { if (!$daysDiff) {
return "Invalid date period :( "; $this->logger->warning("PricesService.getFinalPricesForSearch - Invalid search period");
return [];
} }
$fareTablesForPeriod = $this->repoFareTable->findFareTable($category->getId(), $startDate, $endDate); $fareTablesForPeriod = $this->repoFareTable->findFareTable($category->getId(), $startDate, $endDate);
//Obtenemos el número de personas permitido por todos los tarifarios activos. //Obtenemos el número de personas permitido por todos los tarifarios activos.
$allowedGuestNumbers = $this->repoFareTable->getAllowedGuestNumberOnFareTableList($fareTablesForPeriod); $allowedGuestNumbers = $this->getAllowedGuestNumberFromFaretableList($fareTablesForPeriod);
$rs = []; $rs = [];
@@ -67,7 +73,73 @@ class PricesService
} }
return $finalResult; return $finalResult;
}
/**
* Calculate stay total price on each roomCategory for the given time period
* Returns a list with all roomCategories, containing the total price by guestnumber.
*
* e. g. --> [ 2 => 120€, 3 => 180€ ]
*
* @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;
}
foreach ($roomCategoryList as $roomCat) {
//Get Faretables for RoomCategory.
$roomFaretables = [];
if (array_key_exists($roomCat->getId(), $faretablesByRoomCategory)) {
$roomFaretables = $faretablesByRoomCategory[$roomCat->getId()];
}
//Calculate distinct GuestNumber for RoomCategory by it's faretables.
$allowedGuestNumberList = $this->getAllowedGuestNumberFromFaretableList($roomFaretables);
//Calculate stay price for each allowed GuestNumber.
$roomPricesByGuestNum = [];
foreach ($allowedGuestNumberList as $currentGuestNumber) {
//Calculate the price for each day.
$dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $roomFaretables);
$stayPrice = $this->getTotalPriceForStay($dailyPrices);
$roomPricesByGuestNum[$currentGuestNumber] = $stayPrice;
}
$roomPrices = [];
foreach ($roomPricesByGuestNum as $guestNum => $price) {
$item = new SearchPricesResponsePriceItem();
$item->guestNumber = $guestNum;
$item->price = $price;
$roomPrices[] = $item;
}
$roomItem = new SearchPricesResponse();
$roomItem->roomCategoryId = $roomCat->getId();
$roomItem->prices = $roomPrices;
$rs[] = $roomItem;
}
return $rs;
} }
/** /**
@@ -78,12 +150,12 @@ class PricesService
* @param array $fareTableList Array con los tarifarios que aplican en el periodo indicado. * @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, ...]. * @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 private function getDailyPricesForSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate, int $guestNumber, array $fareTableList): array
{ {
$daysDiff = $startDate->diff($endDate)->days; $daysDiff = $startDate->diff($endDate)->days;
$dailyPrices = []; $dailyPrices = [];
$currentDate = new \DateTime($startDate->format("Y-m-d")); //TODOME: Comprobar si es tipo referencia y está cambiando. $currentDate = new \DateTime($startDate->format("Y-m-d")); //Copy start date to operate.
for ($i = 0; $i < $daysDiff; $i++) { for ($i = 0; $i < $daysDiff; $i++) {
$strCurrentDate = $currentDate->format("Y-m-d"); $strCurrentDate = $currentDate->format("Y-m-d");
@@ -93,8 +165,16 @@ class PricesService
}); });
if (count($currentFareTableList) != 1) { if (count($currentFareTableList) != 1) {
$dailyPrices[$strCurrentDate] = "ERROR - Number of FareTable found: " . count($currentFareTableList); $this->logger->error(
continue; "PricesService.getDailyPricesForSearch: Can't calculate prices: Invalid number of FareTable found: " . count($currentFareTableList),
[
"startDate" => $startDate,
"endDate" => $endDate,
"fareTableList" => $fareTableList,
"currentFareTableList" => $currentFareTableList
]
);
return [];
} }
$currentFareTable = array_values($currentFareTableList)[0]; $currentFareTable = array_values($currentFareTableList)[0];
@@ -104,8 +184,15 @@ class PricesService
//Si no hay precio, calcular mediante tarifarios. //Si no hay precio, calcular mediante tarifarios.
$roomFares = $currentFareTable->getRoomFaresByGuestNumber($guestNumber); $roomFares = $currentFareTable->getRoomFaresByGuestNumber($guestNumber);
if (count($roomFares) < 1) { if (count($roomFares) < 1) {
$dailyPrices[$strCurrentDate] = "ERROR - Invalid RoomFares found"; $this->logger->error(
continue; "PricesService.getDailyPricesForSearch: Can't calculate prices: Found faretable without fares",
[
"currentFareTable" => $currentFareTable,
"guestNumber" => $guestNumber,
"roomFaresForGuestNumber" => $roomFares
]
);
return [];
} }
//Si sólo hay 1 tarifa, aplico esa, sino calculo la apropiada según condiciones. //Si sólo hay 1 tarifa, aplico esa, sino calculo la apropiada según condiciones.
@@ -131,10 +218,14 @@ class PricesService
* @param DateTimeInterface $fareCondDate Fecha utilizada para las condiciones. * @param DateTimeInterface $fareCondDate Fecha utilizada para las condiciones.
* *
*/ */
public function calculateFareApplyingConditions(ArrayCollection $roomFareList, \DateTimeInterface $fareCondDate): int private function calculateFareApplyingConditions(Collection $roomFareList, \DateTimeInterface $fareCondDate): int
{ {
$currentDayOfWeek = $fareCondDate->format("w"); $currentDayOfWeek = $fareCondDate->format("w");
if (count($roomFareList) == 1) {
return $roomFareList->first()->getFareAmount();
}
//Comprobamos si a alguna de las tarifas se la aplica la condición de "DayType" y coincide con el actual. //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. // Si es así, devolvemos su valor.
foreach ($roomFareList as $roomFare) { foreach ($roomFareList as $roomFare) {
@@ -154,4 +245,41 @@ class PricesService
return $faresWithoutConditions->first()->getFareAmount(); return $faresWithoutConditions->first()->getFareAmount();
} }
/**
* 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
{
// TODOME: Crear y configurar diversas estrategias de precios -> Por el momento utilizamos "Suma del precio de cada día".
$finalAmount = \array_reduce($dailyPrices, function ($acc, $element) {
return $acc + $element;
});
return $finalAmount;
}
} }