From 423ffafb9e5e8a91fbbac5c35d20ebc90c7b5898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 29 Jun 2026 11:08:08 +0200 Subject: [PATCH] wip: api endpoint for calendar data to be consumed by cms --- api.http | 6 ++ src/Controller/Api/ContingentController.php | 101 ++++++++++++++++++ .../Groups/AccommodationPriceRepository.php | 49 ++++----- 3 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 src/Controller/Api/ContingentController.php diff --git a/api.http b/api.http index 3131572b..c678ddfe 100644 --- a/api.http +++ b/api.http @@ -121,6 +121,12 @@ Authorization: Bearer {{$auth.token("oauth2_newsletter")}} "listIds": [10321569,10321382,10321383] } +### API contingent calendar +# @no-cookie-jar +GET {{base_url}}/api/contingents/calendar?hotelCode=DGS&dateFrom=2026-06-01&dateTo=2026-06-30 +Accept: application/json +Authorization: Bearer {{$auth.token("oauth2_api")}} + ### API pickups planning webhook # @no-cookie-jar POST {{base_url}}/api/pickups-planning diff --git a/src/Controller/Api/ContingentController.php b/src/Controller/Api/ContingentController.php new file mode 100644 index 00000000..45948890 --- /dev/null +++ b/src/Controller/Api/ContingentController.php @@ -0,0 +1,101 @@ +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); + } + + 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); + + $data = array_map( + fn ($entry) => $this->enrichEntry($entry->date, $entry->status->value, $prices), + $calendar->data, + ); + + return $this->json($data); + } + + /** + * @param AccommodationPrice[] $prices + * @return array{date: string, status: string, pricePerNight: float|null, pricePerNightOverride: float|null, includedPax: int|null} + */ + private function enrichEntry(string $date, string $status, array $prices): array + { + $day = (new \DateTimeImmutable($date))->setTime(0, 0); + $price = null; + + foreach ($prices as $candidate) { + if ($candidate->getDateFrom() > $day || $candidate->getDateTo() < $day) { + continue; + } + + if (null === $price || $candidate->getDateFrom() > $price->getDateFrom()) { + $price = $candidate; + } + } + + return [ + 'date' => $date, + 'status' => $status, + 'pricePerNight' => null !== $price ? round($price->getPricePerNight() / 100, 2) : null, + 'pricePerNightOverride' => null !== $price?->getPricePerNightOverride() ? round($price->getPricePerNightOverride() / 100, 2) : null, + 'priceAdditionalPerson' => null !== $price ? round($price->getPriceAdditionalPerson() / 100, 2) : null, + 'priceAdditionalPersonOverride' => null !== $price?->getPriceAdditionalPersonOverride() ? round($price->getPriceAdditionalPersonOverride() / 100, 2) : null, + 'includedPax' => $price?->getIncludedPax(), + 'minNights' => $price?->getMinNights(), + ]; + } +} diff --git a/src/Repository/Groups/AccommodationPriceRepository.php b/src/Repository/Groups/AccommodationPriceRepository.php index 49b69c46..d44713d9 100644 --- a/src/Repository/Groups/AccommodationPriceRepository.php +++ b/src/Repository/Groups/AccommodationPriceRepository.php @@ -1,5 +1,7 @@ createQueryBuilder('a') - // ->andWhere('a.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('a.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?AccommodationPrice - // { - // return $this->createQueryBuilder('a') - // ->andWhere('a.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + /** + * Returns all AccommodationPrice records whose period overlaps with [dateFrom, dateTo] + * for the accommodation identified by the given hotel code. + * + * @return AccommodationPrice[] + */ + public function findByHotelCodeAndDateRange( + string $hotelCode, + \DateTimeImmutable $dateFrom, + \DateTimeImmutable $dateTo, + ): array { + return $this->createQueryBuilder('ap') + ->join('ap.accommodation', 'a') + ->where('a.code = :hotelCode') + ->andWhere('ap.dateFrom <= :dateTo') + ->andWhere('ap.dateTo >= :dateFrom') + ->setParameter('hotelCode', $hotelCode) + ->setParameter('dateFrom', $dateFrom) + ->setParameter('dateTo', $dateTo) + ->orderBy('ap.dateFrom', 'ASC') + ->getQuery() + ->getResult(); + } }