15 Commits
Author SHA1 Message Date
Daniel-Garmig fe2bc7b78c Fixed Deprecations: EasyAdmin and Symfony Security
CI / Docker Lint (push) Successful in 31s
2025-10-23 22:13:19 +02:00
Daniel-Garmig 850e8afaa1 Updated Symfony Recipes - easycorp/easyadmin-bundle 2025-10-21 22:32:03 +02:00
Daniel-Garmig bb688a724a Updated Symfony Recipes - symfony/web-profiler-bundle 2025-10-21 22:32:03 +02:00
Daniel-Garmig 74cf4621f4 Updated Symfony Recipes - symfony/ux-twig-component 2025-10-21 22:32:03 +02:00
Daniel-Garmig bce27d5fa4 Updated Symfony Recipes - symfony/translation 2025-10-21 22:32:03 +02:00
Daniel-Garmig 2960bf29d8 Updated Symfony Recipes - symfony/routing 2025-10-21 22:32:03 +02:00
Daniel-Garmig e4a68d2aa0 Updated Symfony Recipes - symfony/framework-bundle 2025-10-21 22:32:03 +02:00
Daniel-Garmig 65605d6c5f Updated Symfony Recipes - symfony/flex 2025-10-21 22:32:03 +02:00
Daniel-Garmig da71f813cb Updated Symfony Recipes - symfony/asset-mapper 2025-10-21 22:32:03 +02:00
Daniel-Garmig ca13863c41 Updated Symfony Recipes - doctrine/doctrine-bundle 2025-10-21 22:32:03 +02:00
Daniel-Garmig 734a4a8fee Updated dependencies (mayor)
- nelmio/api-doc-bundle 4.x to 5.6
- phpstan/phpdoc-parser 1.x to 2.x
- Fixed deprecations.
2025-10-21 22:31:56 +02:00
Daniel-Garmig ed6b8335c8 Updated dependencies. 2025-10-21 21:00:30 +02:00
Daniel-Garmig c9e1f95c81 API - Search Prices - Added RoomPrices 2025-10-21 19:58:00 +02:00
Daniel-Garmig f703727ccb Updated .gitignore - Ignore bold db files. 2025-10-16 19:47:08 +02:00
Daniel-Garmig 77f8e9ec31 API - Search Prices - Improvements
- Use config variables for API responses.
2025-10-16 19:44:51 +02:00
32 changed files with 1351 additions and 693 deletions
+3
View File
@@ -21,6 +21,9 @@ indent_size = 2
[*.json] [*.json]
indent_size = 2 indent_size = 2
[{compose.yaml,compose.*.yaml}]
indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
+9 -3
View File
@@ -16,19 +16,25 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
APP_ENV=dev APP_ENV=dev
APP_SECRET=VerySecretKey APP_SECRET=
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
# #
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8.0.32&charset=utf8mb4" # DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" # DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:[email protected]:54195/app?serverVersion=16&charset=utf8" DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> nelmio/cors-bundle ### ###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< nelmio/cors-bundle ### ###< nelmio/cors-bundle ###
###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
+4
View File
@@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=344d36e897253340ff9de6cfacce2da9
###< symfony/framework-bundle ###
+3
View File
@@ -12,6 +12,9 @@
# Draw.io backup file. # Draw.io backup file.
*bkp *bkp
# Bolt db files
[Bb]olt.db
# JS files # JS files
/node_modules/ /node_modules/
+1 -1
View File
@@ -35,5 +35,5 @@ services:
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
database: database:
ports: ports:
- 54195:5432 - "5432"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
+1 -1
View File
@@ -50,7 +50,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app} POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck: healthcheck:
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER:-app}"] test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 60s start_period: 60s
+5 -5
View File
@@ -13,12 +13,12 @@
"doctrine/doctrine-bundle": "^2.14", "doctrine/doctrine-bundle": "^2.14",
"doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3",
"easycorp/easyadmin-bundle": "^4.10", "easycorp/easyadmin-bundle": "^4.25",
"nelmio/api-doc-bundle": "^4.30", "nelmio/api-doc-bundle": "^5.0",
"nelmio/cors-bundle": "^2.4", "nelmio/cors-bundle": "^2.4",
"php-ds/php-ds": "^1.5", "php-ds/php-ds": "^1.5",
"phpdocumentor/reflection-docblock": "^5.4", "phpdocumentor/reflection-docblock": "^5.4",
"phpstan/phpdoc-parser": "^1.28", "phpstan/phpdoc-parser": "^2.0",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*", "symfony/asset-mapper": "7.3.*",
@@ -41,8 +41,8 @@
"symfony/validator": "7.3.*", "symfony/validator": "7.3.*",
"symfony/var-exporter": "7.3.*", "symfony/var-exporter": "7.3.*",
"symfony/yaml": "7.3.*", "symfony/yaml": "7.3.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^3.0",
"twig/twig": "^2.12|^3.0" "twig/twig": "^3.0"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
Generated
+711 -514
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -3,3 +3,9 @@ framework:
# The paths to make available to the asset mapper. # The paths to make available to the asset mapper.
paths: paths:
- assets/ - assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn
+3 -1
View File
@@ -14,6 +14,8 @@ doctrine:
report_fields_where_declared: true report_fields_where_declared: true
validate_xml_mapping: true validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true auto_mapping: true
mappings: mappings:
App: App:
@@ -23,7 +25,7 @@ doctrine:
prefix: 'App\Entity' prefix: 'App\Entity'
alias: App alias: App
controller_resolver: controller_resolver:
auto_mapping: true auto_mapping: false
when@test: when@test:
doctrine: doctrine:
-1
View File
@@ -1,7 +1,6 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html # see https://symfony.com/doc/current/reference/configuration/framework.html
framework: framework:
secret: '%env(APP_SECRET)%' secret: '%env(APP_SECRET)%'
#csrf_protection: true
# Note that the session will be started ONLY if you read or write from it. # Note that the session will be started ONLY if you read or write from it.
session: true session: true
+1
View File
@@ -1,4 +1,5 @@
nelmio_api_doc: nelmio_api_doc:
type_info: true
documentation: documentation:
info: info:
title: SomniaRooms title: SomniaRooms
+1 -1
View File
@@ -2,7 +2,7 @@ framework:
router: router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost default_uri: '%env(DEFAULT_URI)%'
when@prod: when@prod:
framework: framework:
-2
View File
@@ -2,6 +2,4 @@ framework:
default_locale: en default_locale: en
translator: translator:
default_path: '%kernel.project_dir%/translations' default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers: providers:
+3 -7
View File
@@ -1,17 +1,13 @@
when@dev: when@dev:
web_profiler: web_profiler:
toolbar: true toolbar: true
intercept_redirects: false
framework: framework:
profiler: profiler:
only_exceptions: false
collect_serializer_data: true collect_serializer_data: true
when@test: when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework: framework:
profiler: { collect: false } profiler:
collect: false
collect_serializer_data: true
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251017201308 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE room_price (date VARCHAR(10) NOT NULL, guest_number SMALLINT NOT NULL, price INT NOT NULL, room_category_id INT NOT NULL, PRIMARY KEY(room_category_id, date, guest_number))
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_8074837267333DD ON room_price (room_category_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE room_price ADD CONSTRAINT FK_8074837267333DD FOREIGN KEY (room_category_id) REFERENCES room_category (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE room_price DROP CONSTRAINT FK_8074837267333DD
SQL);
$this->addSql(<<<'SQL'
DROP TABLE room_price
SQL);
}
}
+7 -7
View File
@@ -28,18 +28,18 @@ class UserCrudController extends AbstractCrudController
public function configureFields(string $pageName): iterable public function configureFields(string $pageName): iterable
{ {
yield TextField::new ('username'); yield TextField::new('username');
yield TextField::new ('username')->onlyWhenUpdating()->setDisabled(true); yield TextField::new('username')->onlyWhenUpdating()->setDisabled(true);
$availableRoles = ['User' => 'ROLE_USER', 'Admin' => 'ROLE_ADMIN', 'Test' => 'ROLE_TEST', 'SuperAdmin' => 'ROLE_SUPERADMIN']; $availableRoles = ['User' => 'ROLE_USER', 'Admin' => 'ROLE_ADMIN', 'Test' => 'ROLE_TEST', 'SuperAdmin' => 'ROLE_SUPERADMIN'];
yield ChoiceField::new ('roles') yield ChoiceField::new('roles')
->setChoices($availableRoles) ->setChoices($availableRoles)
->allowMultipleChoices(true); ->allowMultipleChoices(true);
yield FormField::addPanel('Set password')->setIcon('fa fa-key'); yield FormField::addFieldset('Set password')->setIcon('fa fa-key');
$fieldPassword = Field::new ('plainPassword', 'New password') $fieldPassword = Field::new('plainPassword', 'New password')
->hideOnIndex() ->hideOnIndex()
->setRequired(false) ->setRequired(false)
->setFormType(RepeatedType::class) ->setFormType(RepeatedType::class)
@@ -50,7 +50,7 @@ class UserCrudController extends AbstractCrudController
'error_bubbling' => true, 'error_bubbling' => true,
'invalid_message' => 'The password fields do not match.', 'invalid_message' => 'The password fields do not match.',
]); ]);
if($pageName == Crud::PAGE_NEW) { if ($pageName == Crud::PAGE_NEW) {
$fieldPassword->setRequired(true); $fieldPassword->setRequired(true);
} }
@@ -79,7 +79,7 @@ class UserCrudController extends AbstractCrudController
{ {
$plaintextPassword = $entityInstance->getPlainPassword(); $plaintextPassword = $entityInstance->getPlainPassword();
if($plaintextPassword && $plaintextPassword != "") { if ($plaintextPassword && $plaintextPassword != "") {
//New password set. Hash password. //New password set. Hash password.
$hashedPassword = $this->userPasswordHasher->hashPassword($entityInstance, $plaintextPassword); $hashedPassword = $this->userPasswordHasher->hashPassword($entityInstance, $plaintextPassword);
$entityInstance->setPassword($hashedPassword); $entityInstance->setPassword($hashedPassword);
+1 -1
View File
@@ -14,7 +14,7 @@ use App\Request\BookingDetailsRequest;
use App\Response\BookingCreateResponse; use App\Response\BookingCreateResponse;
use App\Service\PricesService; use App\Service\PricesService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Attribute\Model;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
+1 -1
View File
@@ -9,7 +9,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use App\Repository\ConfigVariableRepository; use App\Repository\ConfigVariableRepository;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
class ConfigVariableController extends AbstractController class ConfigVariableController extends AbstractController
+1 -6
View File
@@ -3,17 +3,12 @@
namespace App\Controller; namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Doctrine\ORM\EntityManagerInterface;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
use App\Entity\RoomCategory; use App\Entity\RoomCategory;
use App\Entity\RoomCategoryPhoto; use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations\Property;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\HttpKernel\Exception\HttpException;
class RoomCategoryController extends AbstractController class RoomCategoryController extends AbstractController
{ {
+44 -18
View File
@@ -3,6 +3,7 @@
namespace App\Controller; namespace App\Controller;
use App\ApiErrorException; use App\ApiErrorException;
use App\Consts\ConfigVariableConsts;
use App\DTO\ApiErrorMessage; use App\DTO\ApiErrorMessage;
use App\Repository\RoomAvailabilityRepository; use App\Repository\RoomAvailabilityRepository;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
@@ -11,16 +12,28 @@ use App\Response\SearchAvailabilityResponse;
use App\Request\SearchPricesForCategoryRequest; use App\Request\SearchPricesForCategoryRequest;
use App\Response\SearchPricesResponse; use App\Response\SearchPricesResponse;
use App\Response\SearchPricesResponsePriceItem; use App\Response\SearchPricesResponsePriceItem;
use App\Service\ConfigVariableService;
use App\Service\PricesService; use App\Service\PricesService;
use Nelmio\ApiDocBundle\Annotation\Model; use DateTimeInterface;
use Nelmio\ApiDocBundle\Attribute\Model;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
class SearchRoomsController extends AbstractController class SearchRoomsController extends AbstractController
{ {
private string $configMaxAllowedNightAmount;
public function __construct(
private ConfigVariableService $configVarService,
private LoggerInterface $logger
) {
$this->configMaxAllowedNightAmount = $configVarService->getVariableFromConst(ConfigVariableConsts::CONFIG_API_NAXNIGHTS)->getValue();
}
/** /**
* Get availability for all rooms on period. * Get availability for all rooms on period.
* For each room, the minimal availability will be returned. * For each room, the minimal availability will be returned.
@@ -52,12 +65,11 @@ class SearchRoomsController extends AbstractController
#[MapRequestPayload()] SearchPeriodRequest $searchRequest #[MapRequestPayload()] SearchPeriodRequest $searchRequest
): JsonResponse { ): JsonResponse {
if ($searchRequest->startDate >= $searchRequest->endDate) { $datesValid = $this->areRequestDatesValid($searchRequest->startDate, $searchRequest->endDate);
throw new ApiErrorException( if ($datesValid != null) {
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE), throw $datesValid;
'startDate cant be before endDate',
);
} }
$rs = $repoAvailability->getAvailabilityForPeriod($searchRequest->startDate, $searchRequest->endDate); $rs = $repoAvailability->getAvailabilityForPeriod($searchRequest->startDate, $searchRequest->endDate);
return $this->json(["results" => $rs]); return $this->json(["results" => $rs]);
@@ -93,11 +105,10 @@ class SearchRoomsController extends AbstractController
PricesService $priceService, PricesService $priceService,
#[MapRequestPayload()] SearchPeriodRequest $searchRequest #[MapRequestPayload()] SearchPeriodRequest $searchRequest
): JsonResponse { ): JsonResponse {
if ($searchRequest->startDate >= $searchRequest->endDate) {
throw new ApiErrorException( $datesValid = $this->areRequestDatesValid($searchRequest->startDate, $searchRequest->endDate);
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE), if ($datesValid != null) {
'startDate cant be before endDate', throw $datesValid;
);
} }
$rs = $priceService->getRoomSearch($searchRequest->startDate, $searchRequest->endDate); $rs = $priceService->getRoomSearch($searchRequest->startDate, $searchRequest->endDate);
@@ -138,6 +149,11 @@ class SearchRoomsController extends AbstractController
#[MapRequestPayload()] SearchPricesForCategoryRequest $searchRequest #[MapRequestPayload()] SearchPricesForCategoryRequest $searchRequest
): JsonResponse { ): JsonResponse {
$datesValid = $this->areRequestDatesValid($searchRequest->startDate, $searchRequest->endDate);
if ($datesValid != null) {
throw $datesValid;
}
$roomCategory = $repoCat->find($searchRequest->roomCategory); $roomCategory = $repoCat->find($searchRequest->roomCategory);
if ($roomCategory === null) { if ($roomCategory === null) {
throw new ApiErrorException( throw new ApiErrorException(
@@ -146,19 +162,29 @@ class SearchRoomsController extends AbstractController
); );
} }
if ($searchRequest->startDate >= $searchRequest->endDate) { $rs = $priceService->getFinalPricesForSearch($roomCategory, $searchRequest->startDate, $searchRequest->endDate);
throw new ApiErrorException(
return $this->json(["results" => $rs]);
}
private function areRequestDatesValid(DateTimeInterface $dateStart, DateTimeInterface $dateEnd): ApiErrorException|null
{
if ($dateStart >= $dateEnd) {
return new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE), new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE),
'startDate cant be before endDate', 'startDate cant be before endDate',
); );
} }
$fareList = $priceService->getFinalPricesForSearch($roomCategory, $searchRequest->startDate, $searchRequest->endDate); $daysDiff = $dateStart->diff($dateEnd)->days;
$rs = []; if ($daysDiff > $this->configMaxAllowedNightAmount) {
foreach ($fareList as $key => $value) { return new ApiErrorException(
$rs[] = ["guestNumber" => $key, "price" => $value]; new ApiErrorMessage(400, ApiErrorMessage::TYPE_API_LIMIT),
'Query exceeded max allowed night amount',
);
} }
return $this->json(["results" => $rs]); return null;
} }
} }
+2
View File
@@ -11,6 +11,7 @@ class ApiErrorMessage
const TYPE_LOGICAL_ERROR = 'logical_error'; const TYPE_LOGICAL_ERROR = 'logical_error';
const TYPE_SERVER_ERROR = 'server_error'; const TYPE_SERVER_ERROR = 'server_error';
const TYPE_INVALID_DATE = 'invalid_date'; const TYPE_INVALID_DATE = 'invalid_date';
const TYPE_API_LIMIT = 'api_limit';
private static $titles = array( private static $titles = array(
self::TYPE_VALIDATION_ERROR => 'There was a validation error', self::TYPE_VALIDATION_ERROR => 'There was a validation error',
@@ -18,6 +19,7 @@ class ApiErrorMessage
self::TYPE_LOGICAL_ERROR => 'Invalid request due to a logic error', self::TYPE_LOGICAL_ERROR => 'Invalid request due to a logic error',
self::TYPE_SERVER_ERROR => 'Error on server side', self::TYPE_SERVER_ERROR => 'Error on server side',
self::TYPE_INVALID_DATE => 'Invalid date on request', self::TYPE_INVALID_DATE => 'Invalid date on request',
self::TYPE_API_LIMIT => 'Reached API Limit'
); );
private int $statusCode; private int $statusCode;
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Entity;
use App\Repository\RoomPriceRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: RoomPriceRepository::class)]
class RoomPrice
{
#[ORM\Id]
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?RoomCategory $roomCategory = null;
#[ORM\Id]
#[ORM\Column(length: 10, nullable: false)]
private ?string $date = null;
#[ORM\Id]
#[ORM\Column(type: Types::SMALLINT, nullable: false)]
private ?int $guestNumber = null;
#[ORM\Column(nullable: false)]
private ?int $price = null;
public function getRoomCategory(): ?RoomCategory
{
return $this->roomCategory;
}
public function setRoomCategory(?RoomCategory $roomCategory): static
{
$this->roomCategory = $roomCategory;
return $this;
}
public function getDate(): ?\DateTime
{
return new \DateTime($this->date);
}
public function getDateString(): ?string
{
return $this->date;
}
public function setDate(\DateTime $date): static
{
$this->date = $date->format('Y-m-d');
return $this;
}
public function getGuestNumber(): ?int
{
return $this->guestNumber;
}
public function setGuestNumber(int $guestNumber): static
{
$this->guestNumber = $guestNumber;
return $this;
}
public function getPrice(): ?int
{
return $this->price;
}
public function setPrice(int $price): static
{
$this->price = $price;
return $this;
}
}
+2
View File
@@ -4,6 +4,7 @@ namespace App\Entity;
use App\Enum\UserRoleEnum; use App\Enum\UserRoleEnum;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Deprecated;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@@ -120,6 +121,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
/** /**
* @see UserInterface * @see UserInterface
*/ */
#[Deprecated("Not used after Symfony 7.3")]
public function eraseCredentials(): void public function eraseCredentials(): void
{ {
// If you store any temporary, sensitive data on the user, clear it here // If you store any temporary, sensitive data on the user, clear it here
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Repository;
use App\Entity\RoomPrice;
use DateTimeInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<RoomPrice>
*/
class RoomPriceRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, RoomPrice::class);
}
/**
* Get all pricer for the given period.
* @param DateTimeInterface $startDate Period start date (included)
* @param DateTimeInterface $endDate Period end date (included)
* @return RoomPrice[] Array of RoomPrices
*/
public function getListForPeriod(DateTimeInterface $startDate, DateTimeInterface $endDate, ?int $roomCategoryId = null): array
{
$strStartDate = $startDate->format('Y-m-d');
$strEndDate = $endDate->format('Y-m-d');
$hasRoomCategory = $roomCategoryId != null;
$dql = 'SELECT p, rc FROM App\Entity\RoomPrice p
JOIN p.roomCategory rc
WHERE p.date >= :startDate
AND p.date <= :endDate';
if ($hasRoomCategory) {
$dql .= ' AND IDENTITY(p.roomCategory) = :roomCategoryId';
}
$query = $this->getEntityManager()->createQuery($dql)
->setParameter('startDate', $strStartDate)
->setParameter('endDate', $strEndDate);
if ($hasRoomCategory) {
$query->setParameter('roomCategoryId', $roomCategoryId);
}
return $query->getResult();
}
// /**
// * @return RoomPrice[] Returns an array of RoomPrice objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('r.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?RoomPrice
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+1 -1
View File
@@ -2,7 +2,7 @@
namespace App\Response; namespace App\Response;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use OpenApi\Attributes\Items; use OpenApi\Attributes\Items;
+10
View File
@@ -28,6 +28,16 @@ class ConfigVariableService
return $this->repoVariables->findVariableByKey($key); return $this->repoVariables->findVariableByKey($key);
} }
public function getVariableFromConst(array $varConst): ConfigVariable|null
{
$key = ConfigVariableConsts::getKeyFromConstant($varConst);
if (is_null($key)) {
return null;
}
return $this->getVariable($key);
}
/** /**
* Create and init all variables. * Create and init all variables.
* To be used during Setup process. * To be used during Setup process.
+155 -67
View File
@@ -2,13 +2,17 @@
namespace App\Service; namespace App\Service;
use App\Consts\ConfigVariableConsts;
use App\Entity\FareTable; use App\Entity\FareTable;
use App\Entity\RoomCategory; use App\Entity\RoomCategory;
use App\Entity\RoomFare;
use App\Repository\FareTableRepository; use App\Repository\FareTableRepository;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
use App\Repository\RoomFareRepository; use App\Repository\RoomFareRepository;
use App\Repository\RoomPriceRepository;
use App\Response\SearchPricesResponse; use App\Response\SearchPricesResponse;
use App\Response\SearchPricesResponsePriceItem; use App\Response\SearchPricesResponsePriceItem;
use App\Util\PriceSelectionMethods;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Ds\Set; use Ds\Set;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -19,13 +23,19 @@ use Psr\Log\LoggerInterface;
*/ */
class PricesService class PricesService
{ {
private string $configFareMethod;
private string $configPriceMethod;
public function __construct( public function __construct(
private RoomCategoryRepository $repoRoomCategory, private RoomCategoryRepository $repoRoomCategory,
private FareTableRepository $repoFareTable, private FareTableRepository $repoFareTable,
private RoomFareRepository $repoRoomFares, private RoomPriceRepository $repoRoomPrices,
private ConfigVariableService $configVarService,
private LoggerInterface $logger 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. * Calcula los precios de la estancia para la categoría y periodo indicados.
@@ -44,43 +54,46 @@ class PricesService
} }
$fareTablesForPeriod = $this->repoFareTable->findFareTable($category->getId(), $startDate, $endDate); $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. //Obtenemos el número de personas permitido por todos los tarifarios activos.
$allowedGuestNumbers = $this->getAllowedGuestNumberFromFaretableList($fareTablesForPeriod); $allowedGuestNumberList = $this->getAllowedGuestNumberFromFaretableList($fareTablesForPeriod);
$rs = []; //Calculate stay price for each allowed GuestNumber.
$priceItems = [];
foreach ($allowedGuestNumberList as $currentGuestNumber) {
//Calculamos los precios para cada GuestNumber permitido. //Get RoomPrices for current GuestNumber.
foreach ($allowedGuestNumbers as $currentGuestNumber) { $roomPricesForGuestNum = [];
if (array_key_exists($currentGuestNumber, $filteredRoomPriceList)) {
//Calculamos el precio de cada día. $roomPricesForGuestNum = $filteredRoomPriceList[$currentGuestNumber];
// 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. //Calculate the price for each day.
// TODOME: Crear y configurar diversas estrategias de precios -> Por el momento utilizamos "Suma del precio de cada día". $dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $fareTablesForPeriod, $roomPricesForGuestNum);
$finalResult = []; $stayPrice = $this->getTotalPriceForStay($dailyPrices);
foreach ($rs as $guestNum => $priceArray) {
$finalAmount = \array_reduce($priceArray, function ($acc, $element) {
return $acc + $element;
});
$finalResult[$guestNum] = $finalAmount; $item = new SearchPricesResponsePriceItem();
$item->guestNumber = $currentGuestNumber;
$item->price = $stayPrice;
$priceItems[] = $item;
} }
return $finalResult; return $priceItems;
} }
/** /**
* Calculate stay total price on each roomCategory for the given time period * Calculate stay total price on each roomCategory for the given time period
* Returns a list with all roomCategories, containing the total price by guestnumber. * 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. * @return SearchPricesResponse[] Total prices for each guestnumber of each roomCategory.
*/ */
public function getRoomSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array public function getRoomSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
@@ -100,41 +113,57 @@ class PricesService
$faretablesByRoomCategory[$faretable->getRoomCategory()->getId()][] = $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) { foreach ($roomCategoryList as $roomCat) {
$currentRoomCategoryId = $roomCat->getId();
//Get Faretables for RoomCategory. //Get Faretables for RoomCategory.
$roomFaretables = []; $roomFaretables = [];
if (array_key_exists($roomCat->getId(), $faretablesByRoomCategory)) { if (array_key_exists($currentRoomCategoryId, $faretablesByRoomCategory)) {
$roomFaretables = $faretablesByRoomCategory[$roomCat->getId()]; $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. //Calculate distinct GuestNumber for RoomCategory by it's faretables.
$allowedGuestNumberList = $this->getAllowedGuestNumberFromFaretableList($roomFaretables); $allowedGuestNumberList = $this->getAllowedGuestNumberFromFaretableList($roomFaretables);
//Calculate stay price for each allowed GuestNumber. //Calculate stay price for each allowed GuestNumber.
$roomPricesByGuestNum = []; $priceItems = [];
foreach ($allowedGuestNumberList as $currentGuestNumber) { 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. //Calculate the price for each day.
$dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $roomFaretables); $dailyPrices = $this->getDailyPricesForSearch($startDate, $endDate, $currentGuestNumber, $roomFaretables, $roomPricesForGuestNum);
$stayPrice = $this->getTotalPriceForStay($dailyPrices); $stayPrice = $this->getTotalPriceForStay($dailyPrices);
$roomPricesByGuestNum[$currentGuestNumber] = $stayPrice;
}
$roomPrices = [];
foreach ($roomPricesByGuestNum as $guestNum => $price) {
$item = new SearchPricesResponsePriceItem(); $item = new SearchPricesResponsePriceItem();
$item->guestNumber = $guestNum; $item->guestNumber = $currentGuestNumber;
$item->price = $price; $item->price = $stayPrice;
$priceItems[] = $item;
$roomPrices[] = $item;
} }
$roomItem = new SearchPricesResponse(); $roomItem = new SearchPricesResponse();
$roomItem->roomCategoryId = $roomCat->getId(); $roomItem->roomCategoryId = $roomCat->getId();
$roomItem->prices = $roomPrices; $roomItem->prices = $priceItems;
$rs[] = $roomItem; $rs[] = $roomItem;
} }
@@ -150,7 +179,7 @@ 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, ...].
*/ */
private function getDailyPricesForSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate, int $guestNumber, array $fareTableList): array private function getDailyPricesForSearch(\DateTimeInterface $startDate, \DateTimeInterface $endDate, int $guestNumber, array $fareTableList, array $roomPricesByGuestNum): array
{ {
$daysDiff = $startDate->diff($endDate)->days; $daysDiff = $startDate->diff($endDate)->days;
@@ -159,7 +188,8 @@ class PricesService
for ($i = 0; $i < $daysDiff; $i++) { for ($i = 0; $i < $daysDiff; $i++) {
$strCurrentDate = $currentDate->format("Y-m-d"); $strCurrentDate = $currentDate->format("Y-m-d");
//Obtenemos el tarifario que aplica --> TODOME: Buscar si se puede cachear esto, para no recarlcularlo para cada GuestNumber. //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) { $currentFareTableList = \array_filter($fareTableList, function (FareTable $element) use ($currentDate) {
return $element->isActiveOnDate($currentDate); return $element->isActiveOnDate($currentDate);
}); });
@@ -177,11 +207,18 @@ class PricesService
return []; return [];
} }
$currentFareTable = array_values($currentFareTableList)[0];
//TODOME: -> Obtener precio específico (RoomPrices). //Check for Prices.
$roomPrice = null;
if (array_key_exists($strCurrentDate, $roomPricesByGuestNum)) {
$roomPrice = $roomPricesByGuestNum[$strCurrentDate];
}
//Si no hay precio, calcular mediante tarifarios. //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); $roomFares = $currentFareTable->getRoomFaresByGuestNumber($guestNumber);
if (count($roomFares) < 1) { if (count($roomFares) < 1) {
$this->logger->error( $this->logger->error(
@@ -195,15 +232,16 @@ class PricesService
return []; return [];
} }
//Si sólo hay 1 tarifa, aplico esa, sino calculo la apropiada según condiciones. //Calculate RoomFare.
$fareAmount = null;
if (count($roomFares) == 1) {
$fareAmount = $roomFares->first()->getFareAmount();
} else {
$fareAmount = $this->calculateFareApplyingConditions($roomFares, $currentDate); $fareAmount = $this->calculateFareApplyingConditions($roomFares, $currentDate);
} }
$dailyPrices[$strCurrentDate] = $fareAmount; //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. //Add 1 day to current date.
$currentDate->add(new \DateInterval("P1D")); $currentDate->add(new \DateInterval("P1D"));
@@ -218,32 +256,76 @@ class PricesService
* @param DateTimeInterface $fareCondDate Fecha utilizada para las condiciones. * @param DateTimeInterface $fareCondDate Fecha utilizada para las condiciones.
* *
*/ */
private function calculateFareApplyingConditions(Collection $roomFareList, \DateTimeInterface $fareCondDate): int private function calculateFareApplyingConditions(Collection $roomFareList, \DateTimeInterface $fareCondDate): int|null
{ {
$currentDayOfWeek = $fareCondDate->format("w");
if (count($roomFareList) == 1) { if (count($roomFareList) == 1) {
return $roomFareList->first()->getFareAmount(); return $roomFareList->first()->getFareAmount();
} }
//Comprobamos si a alguna de las tarifas se la aplica la condición de "DayType" y coincide con el actual. $currentDayOfWeek = $fareCondDate->format("w");
// Si es así, devolvemos su valor. //Filter applicable fares.
foreach ($roomFareList as $roomFare) { //TODO: Possible optimization - This data doesn't need to be recalculated so often; just once for each FareTable.
$dayType = $roomFare->getDayType(); /** @var RoomFare[] */
$applFaresByDay = [];
/** @var RoomFare[] */
$applFaresNonCondition = [];
foreach ($roomFareList as $fare) {
$dayType = $fare->getDayType();
if ($dayType == null || $dayType == "") { if ($dayType == null || $dayType == "") {
$applFaresNonCondition[] = $fare;
continue; continue;
} }
if ($dayType == $currentDayOfWeek) { if ($dayType == $currentDayOfWeek) {
return $roomFare->getFareAmount(); $applFaresByDay[] = $fare;
} }
} }
//TODOME: Si existen múltiples tarifas posibles, debería haber una estrategia para elegir una u otra. Variable de configuración para esto. //Return final fare value.
$faresWithoutConditions = $roomFareList->filter(function ($element) { //Use fares for current weekday.
return $element->getDayType() == null; $finalFare = $this->getPriceFromRoomFareList($applFaresByDay);
}); if ($finalFare != null) {
return $finalFare;
}
return $faresWithoutConditions->first()->getFareAmount(); //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;
}
} }
/** /**
@@ -273,13 +355,19 @@ class PricesService
* @param array $dailyPrices Array containing the price for each day. * @param array $dailyPrices Array containing the price for each day.
* @return int Total stay price. * @return int Total stay price.
*/ */
private function getTotalPriceForStay(array $dailyPrices): int private function getTotalPriceForStay(array $dailyPrices): int|null
{ {
// TODOME: Crear y configurar diversas estrategias de precios -> Por el momento utilizamos "Suma del precio de cada día". switch ($this->configPriceMethod) {
case 'SUM':
$finalAmount = \array_reduce($dailyPrices, function ($acc, $element) { return PriceSelectionMethods::priceMethod_Sum($dailyPrices);
return $acc + $element; case 'AVERAGE':
}); return PriceSelectionMethods::priceMethod_Average($dailyPrices);
return $finalAmount; case 'HIGHER':
return PriceSelectionMethods::priceMethod_Highest($dailyPrices);
case 'LOWER':
return PriceSelectionMethods::priceMethod_Lowest($dailyPrices);
default:
return null;
}
} }
} }
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace App\Util;
use App\Entity\RoomFare;
//Utils - Functions to select prices and fares.
class PriceSelectionMethods
{
#region Fare Selection
/**
* Fare Selection - Get fare with highest price.
* @param RoomFare[] $fareList List of fares.
*/
public static function selectFare_Highest(array $fareList): int
{
$pricesArr = array_map(function (RoomFare $f) {
return $f->getFareAmount();
}, $fareList);
return max($pricesArr);
}
/**
* Fare Selection - Get fare with lowest price.
* @param RoomFare[] $fareList List of fares.
*/
public static function selectFare_Lowest(array $fareList): int
{
$pricesArr = array_map(function (RoomFare $f) {
return $f->getFareAmount();
}, $fareList);
return min($pricesArr);
}
/**
* Fare Selection - Get average price of given fares.
* @param RoomFare[] $fareList List of fares.
*/
public static function selectFare_Average(array $fareList): int|null
{
$totalAmount = 0;
$count = 0;
foreach ($fareList as $fare) {
$fareAmount = $fare->getFareAmount();
if ($fareAmount != null) {
$totalAmount += $fareAmount;
$count++;
}
}
return $count > 0 ? ($totalAmount / $count) : null;
}
#endregion
#region Price Selection
/**
* Price Method - Total price is the sum of daily prices.
* @param array $prices
* @return int Total price.
*/
public static function priceMethod_Sum(array $prices): int
{
$values = array_values($prices);
return array_sum($values);
}
/**
* Price Method - Apply the hightest daily price to all days.
* @param array $prices
* @return int Total price.
*/
public static function priceMethod_Highest(array $prices): int
{
$highestPrice = max($prices);
$numDays = count($prices);
return $numDays * $highestPrice;
}
/**
* Price Method - Apply the lowest daily price to all days.
* @param array $prices
* @return int Total price.
*/
public static function priceMethod_Lowest(array $prices): int
{
$highestPrice = min($prices);
$numDays = count($prices);
return $numDays * $highestPrice;
}
/**
* Price Method - Calculate average daily price and apply it to all days.
* @param array $prices
* @return int Total price.
*/
public static function priceMethod_Average(array $prices): int|null
{
$sum = array_sum($prices);
$numDays = count($prices);
$averagePrice = $numDays > 0 ? $sum / $numDays : null;
return $averagePrice != null ? $averagePrice * $numDays : null;
}
#endregion
}
+42 -37
View File
@@ -9,17 +9,17 @@
} }
}, },
"doctrine/doctrine-bundle": { "doctrine/doctrine-bundle": {
"version": "2.12", "version": "2.18",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "2.12", "version": "2.13",
"ref": "32f890075233ac4a1176e848a0f8e932ac6536d6" "ref": "620b57f496f2e599a6015a9fa222c2ee0a32adcb"
}, },
"files": [ "files": [
"./config/packages/doctrine.yaml", "config/packages/doctrine.yaml",
"./src/Entity/.gitignore", "src/Entity/.gitignore",
"./src/Repository/.gitignore" "src/Repository/.gitignore"
] ]
}, },
"doctrine/doctrine-migrations-bundle": { "doctrine/doctrine-migrations-bundle": {
@@ -36,13 +36,16 @@
] ]
}, },
"easycorp/easyadmin-bundle": { "easycorp/easyadmin-bundle": {
"version": "4.10", "version": "4.26",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "3.0", "version": "4.14",
"ref": "b131e6cbfe1b898a508987851963fff485986285" "ref": "13b3e524d1038a6861ad6c7fc64e31b644ba0c54"
} },
"files": [
"config/routes/easyadmin.yaml"
]
}, },
"nelmio/api-doc-bundle": { "nelmio/api-doc-bundle": {
"version": "4.30", "version": "4.30",
@@ -70,18 +73,18 @@
] ]
}, },
"symfony/asset-mapper": { "symfony/asset-mapper": {
"version": "7.0", "version": "7.3",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "6.4", "version": "6.4",
"ref": "6c28c471640cc2c6e60812ebcb961c526ef8997f" "ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
}, },
"files": [ "files": [
"./assets/app.js", "assets/app.js",
"./assets/styles/app.css", "assets/styles/app.css",
"./config/packages/asset_mapper.yaml", "config/packages/asset_mapper.yaml",
"./importmap.php" "importmap.php"
] ]
}, },
"symfony/console": { "symfony/console": {
@@ -97,15 +100,16 @@
] ]
}, },
"symfony/flex": { "symfony/flex": {
"version": "2.4", "version": "2.8",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "1.0", "version": "2.4",
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" "ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
}, },
"files": [ "files": [
".env" ".env",
".env.dev"
] ]
}, },
"symfony/form": { "symfony/form": {
@@ -121,14 +125,15 @@
] ]
}, },
"symfony/framework-bundle": { "symfony/framework-bundle": {
"version": "7.0", "version": "7.3",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "7.0", "version": "7.3",
"ref": "6356c19b9ae08e7763e4ba2d9ae63043efc75db5" "ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
}, },
"files": [ "files": [
".editorconfig",
"config/packages/cache.yaml", "config/packages/cache.yaml",
"config/packages/framework.yaml", "config/packages/framework.yaml",
"config/preload.php", "config/preload.php",
@@ -173,12 +178,12 @@
] ]
}, },
"symfony/routing": { "symfony/routing": {
"version": "7.0", "version": "7.3",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "7.0", "version": "7.0",
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d" "ref": "ab1e60e2afd5c6f4a6795908f646e235f2564eb2"
}, },
"files": [ "files": [
"config/packages/routing.yaml", "config/packages/routing.yaml",
@@ -199,16 +204,16 @@
] ]
}, },
"symfony/translation": { "symfony/translation": {
"version": "7.0", "version": "7.3",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "6.3", "version": "6.3",
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b" "ref": "620a1b84865ceb2ba304c8f8bf2a185fbf32a843"
}, },
"files": [ "files": [
"./config/packages/translation.yaml", "config/packages/translation.yaml",
"./translations/.gitignore" "translations/.gitignore"
] ]
}, },
"symfony/twig-bundle": { "symfony/twig-bundle": {
@@ -246,15 +251,15 @@
] ]
}, },
"symfony/ux-twig-component": { "symfony/ux-twig-component": {
"version": "2.25", "version": "2.30",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "2.13", "version": "2.13",
"ref": "9b627037a510e20eddb559e06050a34854ce51d6" "ref": "f367ae2a1faf01c503de2171f1ec22567febeead"
}, },
"files": [ "files": [
"./config/packages/twig_component.yaml" "config/packages/twig_component.yaml"
] ]
}, },
"symfony/validator": { "symfony/validator": {
@@ -270,16 +275,16 @@
] ]
}, },
"symfony/web-profiler-bundle": { "symfony/web-profiler-bundle": {
"version": "7.0", "version": "7.3",
"recipe": { "recipe": {
"repo": "github.com/symfony/recipes", "repo": "github.com/symfony/recipes",
"branch": "main", "branch": "main",
"version": "6.1", "version": "7.3",
"ref": "e42b3f0177df239add25373083a564e5ead4e13a" "ref": "a363460c1b0b4a4d0242f2ce1a843ca0f6ac9026"
}, },
"files": [ "files": [
"./config/packages/web_profiler.yaml", "config/packages/web_profiler.yaml",
"./config/routes/web_profiler.yaml" "config/routes/web_profiler.yaml"
] ]
}, },
"twig/extra-bundle": { "twig/extra-bundle": {
+1 -1
View File
@@ -1,7 +1,7 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} {# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} {# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} {# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% if ea.crud.currentAction == 'detail' %} {% if ea().crud.currentAction == 'detail' %}
<div class="field-icon-div d-flex gap-3"> <div class="field-icon-div d-flex gap-3">
<span title="{{ field.value }}">{{ field.formattedValue|raw }}</span> <span title="{{ field.value }}">{{ field.formattedValue|raw }}</span>
<div class="badge text-bg-secondary"> <div class="badge text-bg-secondary">
+1 -1
View File
@@ -8,8 +8,8 @@
{% block stylesheets %} {% block stylesheets %}
{% endblock %} {% endblock %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% block javascripts %} {% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %} {% endblock %}
</head> </head>