feat: groups price calculator admin crud, booking/offer flow and api
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
final readonly class AccommodationBookingAdditionalServiceItem
|
||||
{
|
||||
#[Groups(['api:single'])]
|
||||
public string $label;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public int $price;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public string $type;
|
||||
|
||||
/**
|
||||
* @param array{label?: string, price?: int, type?: string, originalServiceId?: int|null} $snapshot
|
||||
*/
|
||||
public function __construct(array $snapshot)
|
||||
{
|
||||
$this->label = $snapshot['label'] ?? '';
|
||||
$this->price = $snapshot['price'] ?? 0;
|
||||
$this->type = $snapshot['type'] ?? '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
|
||||
final readonly class AccommodationBookingApiResponse
|
||||
{
|
||||
#[Groups(['api:single'])]
|
||||
public string $uuid;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public string $status;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateFrom;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateTo;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public int $nights;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public int $paxCount;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public int $minorsCount;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public int $childrenCount;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $groupName;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?\DateTimeImmutable $acceptedAt;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public AccommodationBookingPersonalData $personalData;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public AccommodationBookingHotelInfo $accommodation;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public AccommodationBookingBoardServiceInfo $boardService;
|
||||
|
||||
/**
|
||||
* @var list<AccommodationBookingAdditionalServiceItem>
|
||||
*/
|
||||
#[Groups(['api:single'])]
|
||||
public array $additionalServices;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $accommodationDiscount;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $boardServiceDiscount;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $additionalServicesDiscount;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $totalPrice;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $pricingCurrency;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $pricingVersion;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
#[Groups(['api:single'])]
|
||||
public ?array $priceBreakdown;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $priceBreakdown
|
||||
*/
|
||||
public function __construct(AccommodationBooking $booking, ?array $priceBreakdown)
|
||||
{
|
||||
$this->uuid = $booking->getUuid();
|
||||
$this->status = $booking->isInquiry() ? 'inquiry' : 'booking';
|
||||
$this->dateFrom = $booking->getDateFrom();
|
||||
$this->dateTo = $booking->getDateTo();
|
||||
$this->nights = $booking->getNights();
|
||||
$this->paxCount = $booking->getPaxCount();
|
||||
$this->minorsCount = $booking->getMinorsCount();
|
||||
$this->childrenCount = $booking->getChildrenCount();
|
||||
$this->groupName = $booking->getGroupName();
|
||||
$this->acceptedAt = $booking->getAcceptedAt();
|
||||
$this->personalData = new AccommodationBookingPersonalData($booking);
|
||||
$this->accommodation = new AccommodationBookingHotelInfo($booking->getAccommodation());
|
||||
$this->boardService = new AccommodationBookingBoardServiceInfo($booking);
|
||||
$this->additionalServices = array_map(
|
||||
static fn (array $service) => new AccommodationBookingAdditionalServiceItem($service),
|
||||
$booking->getAdditionalServices(),
|
||||
);
|
||||
$this->accommodationDiscount = $booking->getAccommodationDiscount();
|
||||
$this->boardServiceDiscount = $booking->getBoardServiceDiscount();
|
||||
$this->additionalServicesDiscount = $booking->getAdditionalServicesDiscount();
|
||||
$this->totalPrice = $booking->getTotalPrice();
|
||||
$this->pricingCurrency = $booking->getPricingCurrency();
|
||||
$this->pricingVersion = $booking->getPricingVersion();
|
||||
$this->priceBreakdown = $priceBreakdown;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
final readonly class AccommodationBookingBoardServiceInfo
|
||||
{
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $label;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $price;
|
||||
|
||||
public function __construct(AccommodationBooking $booking)
|
||||
{
|
||||
$this->label = $booking->getBoardServiceLabel();
|
||||
$this->price = $booking->getBoardServicePrice();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\AdditionalService;
|
||||
use App\Entity\Groups\BoardService;
|
||||
|
||||
final readonly class AccommodationBookingContext
|
||||
{
|
||||
/**
|
||||
* @param BoardService[] $boardServices
|
||||
* @param AdditionalService[] $additionalServices
|
||||
* @param array<string, AdditionalService[]> $groupedAdditionalServices
|
||||
* @param AdditionalService[] $ungroupedAdditionalServices
|
||||
* @param array<string, mixed>|null $priceBreakdown
|
||||
*/
|
||||
public function __construct(
|
||||
public Accommodation $accommodation,
|
||||
public ?CmsHotelData $hotelCmsData = null,
|
||||
public array $boardServices = [],
|
||||
public array $additionalServices = [],
|
||||
public array $groupedAdditionalServices = [],
|
||||
public array $ungroupedAdditionalServices = [],
|
||||
public ?array $priceBreakdown = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
final readonly class AccommodationBookingHotelInfo
|
||||
{
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $calendarCode;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $cmsCode;
|
||||
|
||||
public function __construct(?Accommodation $accommodation)
|
||||
{
|
||||
$this->calendarCode = $accommodation?->getCalendarCode();
|
||||
$this->cmsCode = $accommodation?->getCmsCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
final readonly class AccommodationBookingPersonalData
|
||||
{
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $salutation;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $firstName;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $lastName;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $email;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $phone;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $street;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $zip;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $city;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $remarks;
|
||||
|
||||
public function __construct(AccommodationBooking $booking)
|
||||
{
|
||||
$this->salutation = $booking->getSalutation();
|
||||
$this->firstName = $booking->getFirstName();
|
||||
$this->lastName = $booking->getLastName();
|
||||
$this->email = $booking->getEmail();
|
||||
$this->phone = $booking->getPhone();
|
||||
$this->street = $booking->getStreet();
|
||||
$this->zip = $booking->getZip();
|
||||
$this->city = $booking->getCity();
|
||||
$this->remarks = $booking->getRemarks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
readonly class AccommodationBookingQueryParams
|
||||
{
|
||||
public function __construct(
|
||||
#[SerializedName('hotel_code')]
|
||||
#[Assert\NotBlank]
|
||||
public string $hotelCode,
|
||||
|
||||
#[SerializedName('date_from')]
|
||||
public ?string $dateFrom = null,
|
||||
|
||||
#[SerializedName('date_to')]
|
||||
public ?string $dateTo = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
/**
|
||||
* Typed hotel CMS payload for the booking summary.
|
||||
*/
|
||||
final readonly class BookingSummaryCmsHotelData
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed>|null $images
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $name,
|
||||
public ?string $address,
|
||||
public ?array $images,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
final readonly class CalendarDay
|
||||
{
|
||||
public function __construct(
|
||||
public string $date,
|
||||
public bool $hasPrice,
|
||||
public bool $hasBoard,
|
||||
public bool $hasAdditional,
|
||||
public bool $isBlocked,
|
||||
public ?string $tooltip,
|
||||
public bool $isToday,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
final readonly class CalendarMonth
|
||||
{
|
||||
/**
|
||||
* @param array<int, CalendarDay> $days keyed by day-of-month number
|
||||
*/
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public int $firstDow,
|
||||
public array $days,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
/**
|
||||
* Typed CMS hotel payload, used both for the rich hotel-details view (booking offer page,
|
||||
* admin accommodation edit) and the slimmer booking-summary sidebar. Fields beyond name,
|
||||
* address and images are only populated when sourced from
|
||||
* {@see \App\Service\CmsDataProvider::getHotelDetails()}.
|
||||
*/
|
||||
final readonly class CmsHotelData
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed>|null $images
|
||||
* @param array<string, array{label?: string|null, value?: mixed}>|null $icons
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $name,
|
||||
public ?string $address,
|
||||
public ?array $images,
|
||||
public ?string $description = null,
|
||||
public ?string $features = null,
|
||||
public ?string $roomTypes = null,
|
||||
public ?string $additionalInformation = null,
|
||||
public ?array $icons = null,
|
||||
public ?CmsRegionData $region = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
/**
|
||||
* Typed region payload nested within CmsHotelData.
|
||||
*/
|
||||
final readonly class CmsRegionData
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed>|null $images
|
||||
* @param list<string>|null $regionMaps
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $name,
|
||||
public ?float $latitude,
|
||||
public ?float $longitude,
|
||||
public ?string $webcam,
|
||||
public ?string $skiArea,
|
||||
public ?string $skiAreaExtended,
|
||||
public ?string $description,
|
||||
public ?string $news,
|
||||
public ?int $length,
|
||||
public ?int $altitude,
|
||||
public ?int $lifts,
|
||||
public ?array $images,
|
||||
public ?array $regionMaps,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
final readonly class ContingentCalendarQuery
|
||||
{
|
||||
private const int MAX_RANGE_DAYS = 366;
|
||||
|
||||
public function __construct(
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 16)]
|
||||
#[Assert\Regex(pattern: '/^[A-Za-z0-9_-]+$/')]
|
||||
public string $hotelCode,
|
||||
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Date]
|
||||
public string $dateFrom,
|
||||
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Date]
|
||||
public string $dateTo,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validateRange(ExecutionContextInterface $context): void
|
||||
{
|
||||
$dateFrom = self::parseDate($this->dateFrom);
|
||||
$dateTo = self::parseDate($this->dateTo);
|
||||
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dateTo < $dateFrom) {
|
||||
$context->buildViolation('dateTo must not be before dateFrom.')
|
||||
->atPath('dateTo')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dateFrom->diff($dateTo)->days > self::MAX_RANGE_DAYS) {
|
||||
$context->buildViolation(sprintf('The date range must not exceed %d days.', self::MAX_RANGE_DAYS))
|
||||
->atPath('dateTo')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
public function dateFromDate(): \DateTimeImmutable
|
||||
{
|
||||
return self::parseDate($this->dateFrom)
|
||||
?? throw new \LogicException('ContingentCalendarQuery must be validated before use.');
|
||||
}
|
||||
|
||||
public function dateToDate(): \DateTimeImmutable
|
||||
{
|
||||
return self::parseDate($this->dateTo)
|
||||
?? throw new \LogicException('ContingentCalendarQuery must be validated before use.');
|
||||
}
|
||||
|
||||
private static function parseDate(string $value): ?\DateTimeImmutable
|
||||
{
|
||||
$date = \DateTimeImmutable::createFromFormat('!Y-m-d', $value);
|
||||
$errors = \DateTimeImmutable::getLastErrors();
|
||||
|
||||
if (false === $date || (false !== $errors && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date->format('Y-m-d') === $value ? $date : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final readonly class ContingentPricesQuery
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 16)]
|
||||
#[Assert\Regex(pattern: '/^[A-Za-z0-9_-]+$/')]
|
||||
public string $hotelCode,
|
||||
|
||||
#[Assert\Range(min: 1000, max: 9999)]
|
||||
public int $year,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
final readonly class InquiryStatus
|
||||
{
|
||||
/** @param string[] $reasons */
|
||||
public function __construct(
|
||||
public bool $isInquiry,
|
||||
public array $reasons,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
final readonly class PriceTimelineItem
|
||||
{
|
||||
public function __construct(
|
||||
public string $dateFrom,
|
||||
public string $dateTo,
|
||||
public ?string $season,
|
||||
public ?int $includedPax,
|
||||
public float $pricePerNight,
|
||||
public float $priceAdditionalPerson,
|
||||
public ?float $defaultPricePerNight,
|
||||
public ?float $defaultPriceAdditionalPerson,
|
||||
public string $currency,
|
||||
public ?string $type,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user