0, 'ON_REQUEST' => 1, 'BLOCKED' => 2, ]; public function __construct( private readonly ContingentLoader $contingentLoader, private readonly TravelLoader $travelLoader, private readonly BookingUrlUtility $bookingUrlUtility, ) { } /** * @return array * * @throws TravelNotFoundException * @throws HotelNotInTravelException */ public function getAvailableContingents(int $hotelId, int $dateId): array { $travel = $this->travelLoader->loadById($dateId, $hotelId); if (null === $travel->dateFrom || null === $travel->dateTo) { return []; } $data = $this->contingentLoader->loadByHotelId($hotelId); $rows = $this->filterRowsByRange($data['rows'], $travel->dateFrom, $travel->dateTo); $belKalStatusByDate = $this->getBelKalStatusByDate($rows); $result = []; foreach ($rows as $row) { if (true === ($row['isControlRoom'] ?? false)) { continue; } $dateKey = $row['date']->format('Y-m-d'); if (false === isset($result[$dateKey])) { $result[$dateKey] = new ContingentDateSummary($dateKey, null, 0, 0); } if (null !== $row['minNights']) { $result[$dateKey]->minNights = null === $result[$dateKey]->minNights ? $row['minNights'] : min($result[$dateKey]->minNights, $row['minNights']); } $result[$dateKey]->total += (int) ($row['available'] ?? 0); $result[$dateKey]->capacity += (int) ($row['pax'] ?? 0); } foreach ($belKalStatusByDate as $dateKey => $status) { if (false === isset($result[$dateKey])) { continue; } if ('OK' !== $status) { $result[$dateKey]->total = 0; } } return $result; } /** * @return array * * @throws TravelNotFoundException * @throws HotelNotInTravelException * @throws \InvalidArgumentException */ public function getAvailableRooms( string $dateFrom, string $dateTo, int $hotelId, int $dateId, ?string $myEpUrl = null, ): array { $range = $this->parseDateRange($dateFrom, $dateTo); $travel = $this->travelLoader->loadById($dateId, $hotelId); if (null !== $travel->dateFrom && null !== $travel->dateTo) { if ($range['from'] < $travel->dateFrom || $range['to'] > $travel->dateTo) { throw new \InvalidArgumentException('Requested range must be within the travel date range'); } } $data = $this->contingentLoader->loadByHotelId($hotelId); $rows = $this->filterRowsByRange($data['rows'], $range['from'], $range['to']); $belKalStatusByDate = $this->getBelKalStatusByDate($rows); $nights = $range['from']->diff($range['to'])->days; $bookingUrl = $this->bookingUrlUtility->build($hotelId, $dateId, $myEpUrl); $result = []; foreach ($rows as $row) { if (true === ($row['isControlRoom'] ?? false)) { continue; } $dateKey = $row['date']->format('Y-m-d'); $status = $row['status'] ?? 'OK'; if (isset($belKalStatusByDate[$dateKey]) && 'OK' !== $belKalStatusByDate[$dateKey]) { $status = $belKalStatusByDate[$dateKey]; } $minNights = $row['minNights'] ?? 0; $additionalNightMinPrice = $row['additionalNightMinPrice'] ?? 0.0; $minPrice = $row['minPrice'] ?? 0.0; $extraNights = max(0, $nights - (int) $minNights); $priceForSelection = $minPrice + ($extraNights * $additionalNightMinPrice); $result[] = new ContingentRoomAvailability( date: $dateKey, roomCode: (string) $row['roomCode'], roomLabel: (string) $row['roomLabel'], bookingUrl: $bookingUrl, pax: (int) ($row['pax'] ?? 0), available: 'OK' === $status ? (int) ($row['available'] ?? 0) : 0, status: $status, minPrice: $row['minPrice'], minNights: $row['minNights'], additionalNightMinPrice: $row['additionalNightMinPrice'], additionalNightMinNights: $row['additionalNightMinNights'], priceForSelection: (float) $priceForSelection, ); } return $result; } /** * @return array */ public function getCalendarEvents(int $hotelId, string $dateFrom, string $dateTo): array { $range = $this->parseDateRange($dateFrom, $dateTo); $data = $this->contingentLoader->loadByHotelId($hotelId); $rows = $this->filterRowsByRange($data['rows'], $range['from'], $range['to']); $events = []; foreach ($rows as $row) { $dateKey = $row['date']->format('Y-m-d'); if (false === isset($events[$dateKey])) { $events[$dateKey] = new ContingentCalendarEvent($dateKey, 'OK', 0, 0); } $events[$dateKey]->status = $this->mergeStatus( $events[$dateKey]->status, $row['status'] ?? 'OK' ); if (false === ($row['isControlRoom'] ?? false)) { $events[$dateKey]->pax += (int) ($row['pax'] ?? 0); $events[$dateKey]->available += (int) ($row['available'] ?? 0); } } ksort($events); return array_values($events); } /** * @param array> $rows * * @return array> */ private function filterRowsByRange(array $rows, \DateTimeImmutable $dateFrom, \DateTimeImmutable $dateTo): array { return array_values(array_filter($rows, function (array $row) use ($dateFrom, $dateTo) { if (false === isset($row['date']) || !$row['date'] instanceof \DateTimeImmutable) { return false; } return $row['date'] >= $dateFrom && $row['date'] <= $dateTo; })); } /** * @param array> $rows * * @return array */ private function getBelKalStatusByDate(array $rows): array { $statuses = []; foreach ($rows as $row) { if (false === ($row['isControlRoom'] ?? false)) { continue; } $dateKey = $row['date']->format('Y-m-d'); $statuses[$dateKey] = $this->mergeStatus($statuses[$dateKey] ?? 'OK', $row['status'] ?? 'OK'); } return $statuses; } private function mergeStatus(string $current, string $candidate): string { $currentOrder = self::STATUS_ORDER[$current] ?? 0; $candidateOrder = self::STATUS_ORDER[$candidate] ?? 0; return $candidateOrder > $currentOrder ? $candidate : $current; } /** * @return array{from: \DateTimeImmutable, to: \DateTimeImmutable} */ private function parseDateRange(string $dateFrom, string $dateTo): array { $from = $this->parseDate($dateFrom); $to = $this->parseDate($dateTo); if ($to < $from) { throw new \InvalidArgumentException('dateTo must be on or after dateFrom'); } return ['from' => $from, 'to' => $to]; } private function parseDate(string $date): \DateTimeImmutable { $parsed = \DateTimeImmutable::createFromFormat('Y-m-d', $date); if (false === $parsed || $parsed->format('Y-m-d') !== $date) { throw new \InvalidArgumentException('Invalid date format, expected Y-m-d'); } return $parsed->setTime(0, 0, 0); } }