feat: derive calendar date range from price configurations
This commit is contained in:
@@ -72,9 +72,6 @@ class ContingentController extends AbstractController
|
||||
#[MapQueryString(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
|
||||
ContingentCalendarQuery $query,
|
||||
): JsonResponse {
|
||||
$dateFrom = $query->dateFromDate();
|
||||
$dateTo = $query->dateToDate();
|
||||
|
||||
$accommodation = $this->accommodationRepository->findOneBy(['calendarCode' => $query->hotelCode]);
|
||||
|
||||
if (null === $accommodation) {
|
||||
@@ -83,6 +80,30 @@ class ContingentController extends AbstractController
|
||||
return $this->json(['error' => 'Hotel not found for hotelCode.'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$dateFrom = $query->dateFromDate();
|
||||
$dateTo = $query->dateToDate();
|
||||
|
||||
// Without an explicit range the persisted prices define one: what we have priced is what we sell.
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
$bounds = $this->priceRepository->findPricedDateBoundsByHotelCode($query->hotelCode);
|
||||
|
||||
// Nothing priced means nothing sold, so there is no calendar to serve. This returns before
|
||||
// the snapshot is consulted, and deliberately so: the 502 below exists to avoid passing
|
||||
// unknown availability off as "all blocked", and here there is nothing to be unsure about.
|
||||
if (null === $bounds) {
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
// Past date ranges are of no use to API consumers: never start the range before today.
|
||||
$dateFrom = max($bounds['from'], CarbonImmutable::now()->setTime(0, 0));
|
||||
$dateTo = $bounds['to'];
|
||||
|
||||
// Every priced period has already ended.
|
||||
if ($dateFrom > $dateTo) {
|
||||
return $this->json([]);
|
||||
}
|
||||
}
|
||||
|
||||
// An empty or stale snapshot must not be served as though every day were blocked: that
|
||||
// is a plausible-looking 200 nobody can distinguish from real data. Fail the way this
|
||||
// endpoint always failed instead, so existing consumers need no change.
|
||||
|
||||
@@ -17,21 +17,40 @@ final readonly class ContingentCalendarQuery
|
||||
#[Assert\Regex(pattern: '/^[A-Za-z0-9_-]+$/')]
|
||||
public string $hotelCode,
|
||||
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Date]
|
||||
public string $dateFrom,
|
||||
public ?string $dateFrom = null,
|
||||
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Date]
|
||||
public string $dateTo,
|
||||
public ?string $dateTo = null,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validateRange(ExecutionContextInterface $context): void
|
||||
{
|
||||
$dateFrom = self::parseDate($this->dateFrom);
|
||||
$dateTo = self::parseDate($this->dateTo);
|
||||
$from = $this->dateFrom;
|
||||
$to = $this->dateTo;
|
||||
|
||||
$hasFrom = null !== $from && '' !== $from;
|
||||
$hasTo = null !== $to && '' !== $to;
|
||||
|
||||
// Neither supplied: the caller wants the full priced span, which the persisted prices define.
|
||||
if (!$hasFrom && !$hasTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A lone dateFrom would have to mean "from there to the end of the prices", a third mode
|
||||
// nobody asked for. Demand the pair instead of inventing a meaning for half of it.
|
||||
if (!$hasFrom || !$hasTo) {
|
||||
$context->buildViolation('dateFrom and dateTo must be supplied together.')
|
||||
->atPath($hasFrom ? 'dateTo' : 'dateFrom')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dateFrom = self::parseDate($from);
|
||||
$dateTo = self::parseDate($to);
|
||||
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
return;
|
||||
@@ -52,15 +71,29 @@ final readonly class ContingentCalendarQuery
|
||||
}
|
||||
}
|
||||
|
||||
public function dateFromDate(): \DateTimeImmutable
|
||||
/**
|
||||
* Null when the parameter was not supplied, which puts the request into full-span mode.
|
||||
*/
|
||||
public function dateFromDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return self::parseDate($this->dateFrom)
|
||||
?? throw new \LogicException('ContingentCalendarQuery must be validated before use.');
|
||||
return $this->suppliedDate($this->dateFrom);
|
||||
}
|
||||
|
||||
public function dateToDate(): \DateTimeImmutable
|
||||
/**
|
||||
* Null when the parameter was not supplied, which puts the request into full-span mode.
|
||||
*/
|
||||
public function dateToDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return self::parseDate($this->dateTo)
|
||||
return $this->suppliedDate($this->dateTo);
|
||||
}
|
||||
|
||||
private function suppliedDate(?string $value): ?\DateTimeImmutable
|
||||
{
|
||||
if (null === $value || '' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::parseDate($value)
|
||||
?? throw new \LogicException('ContingentCalendarQuery must be validated before use.');
|
||||
}
|
||||
|
||||
|
||||
@@ -41,4 +41,33 @@ class AccommodationPriceRepository extends ServiceEntityRepository
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* The outer bounds of every persisted price period for the accommodation, or null when it has none.
|
||||
*
|
||||
* @return array{from: \DateTimeImmutable, to: \DateTimeImmutable}|null
|
||||
*/
|
||||
public function findPricedDateBoundsByHotelCode(string $hotelCode): ?array
|
||||
{
|
||||
/** @var array{minFrom: string|null, maxTo: string|null} $bounds */
|
||||
$bounds = $this->createQueryBuilder('ap')
|
||||
->select('MIN(ap.dateFrom) AS minFrom', 'MAX(ap.dateTo) AS maxTo')
|
||||
->join('ap.accommodation', 'a')
|
||||
->where('a.calendarCode = :hotelCode')
|
||||
->setParameter('hotelCode', $hotelCode)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
|
||||
// An aggregate over no rows still returns one row, with both bounds null.
|
||||
if (null === $bounds['minFrom'] || null === $bounds['maxTo']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Both columns are DATE_IMMUTABLE, so the aggregates come back as Y-m-d and land at 00:00 -
|
||||
// the same normalisation the day-by-day comparisons downstream expect.
|
||||
return [
|
||||
'from' => new \DateTimeImmutable($bounds['minFrom']),
|
||||
'to' => new \DateTimeImmutable($bounds['maxTo']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user