93 lines
3.0 KiB
PHP
93 lines
3.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\BpnConnect\Model\ContingentStatus;
|
|
use App\Entity\Groups\Accommodation;
|
|
use App\Repository\Groups\ContingentDayRepository;
|
|
use App\Repository\Groups\ContingentSyncStateRepository;
|
|
use Carbon\CarbonImmutable;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Reads contingent availability out of the local snapshot, and says when it cannot be trusted.
|
|
*
|
|
* Callers get null rather than a silently empty result, because "no data" and "everything is
|
|
* blocked" are indistinguishable once the statuses are flattened — and the two current callers
|
|
* want opposite fallbacks: the API fails loud with a 502, the booking calendar falls back to
|
|
* price coverage alone.
|
|
*/
|
|
class ContingentSnapshotReader
|
|
{
|
|
/**
|
|
* How old the snapshot may get before we stop presenting it as current availability.
|
|
*
|
|
* The near-term sync runs every 15 minutes during the day and hourly overnight, so this only
|
|
* trips when the scheduler is genuinely broken, never on the normal cadence.
|
|
*/
|
|
private const string MAX_SNAPSHOT_AGE = '-6 hours';
|
|
|
|
public function __construct(
|
|
private readonly ContingentDayRepository $dayRepository,
|
|
private readonly ContingentSyncStateRepository $syncStateRepository,
|
|
private readonly LoggerInterface $logger,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Per-day statuses keyed by Y-m-d, or null when there is no usable snapshot for this hotel.
|
|
*
|
|
* Days inside the range that the snapshot does not cover are simply absent from the map;
|
|
* what that means is the caller's decision.
|
|
*
|
|
* @return array<string, ContingentStatus>|null
|
|
*/
|
|
public function statusesFor(
|
|
Accommodation $accommodation,
|
|
\DateTimeImmutable $dateFrom,
|
|
\DateTimeImmutable $dateTo,
|
|
): ?array {
|
|
$hotelCode = $accommodation->getCalendarCode();
|
|
|
|
if (null === $hotelCode || '' === $hotelCode) {
|
|
return null;
|
|
}
|
|
|
|
if (!$this->isUsable($accommodation, $hotelCode)) {
|
|
return null;
|
|
}
|
|
|
|
$statuses = [];
|
|
|
|
foreach ($this->dayRepository->findByHotelCodeAndDateRange($hotelCode, $dateFrom, $dateTo) as $date => $day) {
|
|
$statuses[$date] = $day->getStatus();
|
|
}
|
|
|
|
return $statuses;
|
|
}
|
|
|
|
private function isUsable(Accommodation $accommodation, string $hotelCode): bool
|
|
{
|
|
$syncedAt = $this->syncStateRepository->findOneByAccommodation($accommodation)?->getSyncedAt();
|
|
|
|
if (null === $syncedAt) {
|
|
$this->logger->error('No contingent snapshot has been synced yet', ['hotelCode' => $hotelCode]);
|
|
|
|
return false;
|
|
}
|
|
|
|
if ($syncedAt < CarbonImmutable::now()->modify(self::MAX_SNAPSHOT_AGE)) {
|
|
$this->logger->error('Contingent snapshot is stale', [
|
|
'hotelCode' => $hotelCode,
|
|
'syncedAt' => $syncedAt->format(\DateTimeInterface::ATOM),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|