query->get('hotelCode'); $yearRaw = $request->query->get('year'); if (!$hotelCode || !$yearRaw) { return $this->json(['error' => 'hotelCode and year are required.'], Response::HTTP_BAD_REQUEST); } if (!preg_match('/^\d{4}$/', $yearRaw)) { return $this->json(['error' => 'year must be a 4-digit integer.'], Response::HTTP_BAD_REQUEST); } $accommodation = $this->accommodationRepository->findOneBy(['calendarCode' => $hotelCode]); if (null === $accommodation) { return $this->json(['error' => 'hotel not found for hotelCode.'], Response::HTTP_BAD_REQUEST); } $year = (int) $yearRaw; $yearStart = new \DateTimeImmutable("{$year}-01-01"); $yearEnd = new \DateTimeImmutable("{$year}-12-31"); $prices = $this->priceRepository->findByHotelCodeAndDateRange($hotelCode, $yearStart, $yearEnd); $currency = $accommodation->getCurrency(); return $this->json($this->priceTimelineBuilder->buildTimeline($prices, $yearStart, $yearEnd, $currency)); } #[Route(path: '/contingents/calendar', name: 'api_contingents_calendar', methods: ['GET'])] public function calendar(Request $request): JsonResponse { $hotelCode = $request->query->get('hotelCode'); $dateFromRaw = $request->query->get('dateFrom'); $dateToRaw = $request->query->get('dateTo'); if (!$hotelCode || !$dateFromRaw || !$dateToRaw) { return $this->json(['error' => 'hotelCode, dateFrom and dateTo are required.'], Response::HTTP_BAD_REQUEST); } $dateFrom = \DateTimeImmutable::createFromFormat('Y-m-d', $dateFromRaw)?->setTime(0, 0); $dateTo = \DateTimeImmutable::createFromFormat('Y-m-d', $dateToRaw)?->setTime(0, 0); if (!$dateFrom || !$dateTo) { return $this->json(['error' => 'dateFrom and dateTo must be in Y-m-d format.'], Response::HTTP_BAD_REQUEST); } $accommodation = $this->accommodationRepository->findOneBy(['calendarCode' => $hotelCode]); if (null === $accommodation) { return $this->json(['error' => 'hotel not found for hotelCode.'], Response::HTTP_BAD_REQUEST); } try { $cacheKey = sprintf('contingents_calendar_%s_%s_%s', $hotelCode, $dateFromRaw, $dateToRaw); $calendar = $this->cache->get($cacheKey, function (ItemInterface $item) use ($hotelCode, $dateFromRaw, $dateToRaw) { $item->expiresAfter(3600); return $this->contingentsClient->getContingentCalendar($hotelCode, $dateFromRaw, $dateToRaw); }); } catch (BpnConnectException|InvalidArgumentException $e) { return $this->json(['error' => 'Failed to fetch contingent data.'], Response::HTTP_BAD_GATEWAY); } $prices = $this->priceRepository->findByHotelCodeAndDateRange($hotelCode, $dateFrom, $dateTo); $currency = $accommodation->getCurrency(); $data = array_map( fn ($entry) => $this->enrichEntry($entry->date, $entry->status->value, $prices, $currency), $calendar->data, ); return $this->json($data); } /** * @param AccommodationPrice[] $prices * @return array{date: string, status: string, type: string|null, pricePerNight: float|null, defaultPricePerNight: float|null, priceAdditionalPerson: float|null, defaultPriceAdditionalPerson: float|null, includedPax: int|null, minNights: int|null} */ private function enrichEntry(string $date, string $status, array $prices, string $currency): array { $day = (new \DateTimeImmutable($date))->setTime(0, 0); // dateFrom and dateTo are both inclusive (last night, not checkout day) $candidates = array_filter( $prices, fn($p) => $p->getDateFrom() <= $day && $p->getDateTo() >= $day, ); $winner = $this->priceTimelineBuilder->resolveWinner($candidates); $defaultPrice = null; if ($winner?->getType() === PriceType::DISCOUNT) { $defaults = array_filter($candidates, fn($p) => $p->getType() === null); $defaultPrice = $this->priceTimelineBuilder->resolveWinner($defaults); } return [ 'date' => $date, 'status' => $status, 'type' => $winner?->getType()?->value, 'pricePerNight' => $winner !== null ? round($winner->getPricePerNight() / 100, 2) : null, 'defaultPricePerNight' => $defaultPrice !== null ? round($defaultPrice->getPricePerNight() / 100, 2) : null, 'priceAdditionalPerson' => $winner !== null ? round($winner->getPriceAdditionalPerson() / 100, 2) : null, 'defaultPriceAdditionalPerson' => $defaultPrice !== null ? round($defaultPrice->getPriceAdditionalPerson() / 100, 2) : null, 'currency' => $currency, 'includedPax' => $winner?->getIncludedPax(), 'minNights' => $winner?->getMinNights(), ]; } }