fix: exclude past dates in price tables

This commit is contained in:
Björn Fromme
2026-08-06 12:19:30 +02:00
parent 577f7445ab
commit b82e017f73
3 changed files with 42 additions and 13 deletions
+10 -2
View File
@@ -15,6 +15,7 @@ use App\Repository\Groups\AccommodationPriceRepository;
use App\Repository\Groups\AccommodationRepository;
use App\Service\AccommodationPriceCoverage;
use App\Service\PriceTimelineBuilder;
use Carbon\CarbonImmutable;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -57,11 +58,18 @@ class ContingentController extends AbstractController
$yearStart = new \DateTimeImmutable("{$query->year}-01-01");
$yearEnd = new \DateTimeImmutable("{$query->year}-12-31");
$prices = $this->priceRepository->findByHotelCodeAndDateRange($query->hotelCode, $yearStart, $yearEnd);
// Past date ranges are of no use to API consumers: never start the timeline before today.
$rangeStart = max($yearStart, CarbonImmutable::now()->setTime(0, 0));
if ($rangeStart > $yearEnd) {
return $this->json([]);
}
$prices = $this->priceRepository->findByHotelCodeAndDateRange($query->hotelCode, $rangeStart, $yearEnd);
$currency = $accommodation->getCurrency();
return $this->json($this->priceTimelineBuilder->buildTimeline($prices, $yearStart, $yearEnd, $currency));
return $this->json($this->priceTimelineBuilder->buildTimeline($prices, $rangeStart, $yearEnd, $currency));
}
#[Route(path: '/contingents/calendar', name: 'api_contingents_calendar', methods: ['GET'])]
+11 -11
View File
@@ -11,16 +11,16 @@ use App\Model\PriceTimelineItem;
class PriceTimelineBuilder
{
/**
* Builds a flat, non-overlapping timeline of effective price rows for the given year.
* Builds a flat, non-overlapping timeline of effective price rows for the given date range.
*
* Base price periods are split wherever an override or discount applies, so each
* returned row represents a contiguous date range with a single effective price.
* Periods with no price configured are omitted. Prices that start before or end after
* the queried year are clamped to its boundaries.
* the queried range are clamped to its boundaries.
*
* The algorithm is an interval sweep:
* 1. Collect every dateFrom and (dateTo + 1 day) from all prices as boundary points,
* plus the year start and (yearEnd + 1 day). This ensures the sweep slices at
* plus the range start and (rangeEnd + 1 day). This ensures the sweep slices at
* every point where the set of active prices changes.
* 2. Walk adjacent boundary pairs [start, end). For each segment, find all prices
* active at `start` and resolve the winner via resolveWinner(), which prefers
@@ -39,20 +39,20 @@ class PriceTimelineBuilder
*/
public function buildTimeline(
array $prices,
\DateTimeImmutable $yearStart,
\DateTimeImmutable $yearEnd,
\DateTimeImmutable $rangeStart,
\DateTimeImmutable $rangeEnd,
string $currency,
): array {
if (empty($prices)) {
return [];
}
$yearEndNext = $yearEnd->modify('+1 day');
$rangeEndNext = $rangeEnd->modify('+1 day');
// Step 1: collect all boundary points, keyed by timestamp for deduplication.
$boundaries = [
$yearStart->getTimestamp() => $yearStart,
$yearEndNext->getTimestamp() => $yearEndNext,
$rangeStart->getTimestamp() => $rangeStart,
$rangeEndNext->getTimestamp() => $rangeEndNext,
];
foreach ($prices as $price) {
$from = $price->getDateFrom();
@@ -73,9 +73,9 @@ class PriceTimelineBuilder
$segStart = $boundaries[$i];
$segEndNext = $boundaries[$i + 1];
// Boundaries from prices outside the year are included to correctly detect
// overlaps at the year edges, but the segments themselves are skipped.
if ($segStart < $yearStart || $segStart >= $yearEndNext) {
// Boundaries from prices outside the range are included to correctly detect
// overlaps at the range edges, but the segments themselves are skipped.
if ($segStart < $rangeStart || $segStart >= $rangeEndNext) {
continue;
}