From bf9afd143dfa3a46f16106aaa3df97115a0d4de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 29 Oct 2026 14:45:44 +0100 Subject: [PATCH] feat: API lists the calendar hotels for the contingent sync of MyE&P GET /api/calendar-hotels returns the calendar hotel of every visible product, with the products using it and their season, behind the same API key as /api/hotel and /api/product. MyE&P only syncs contingent snapshots for its accommodations, so hotels that are only offered in winter (e.g. DKI, DWW, ASCGR) have no availability for the iCal feed and the availability calendar. With this list MyE&P can sync every hotel code TYPO3 actually uses, without a second list to maintain. The products and seasons are for a status overview of which hotel supports iCal, availability and prices. Hidden hotels are left out, as on the website (e.g. ASB). Co-Authored-By: Claude Opus 5.5 --- .../Classes/Middleware/ApiMiddleware.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/ep_products/Classes/Middleware/ApiMiddleware.php b/packages/ep_products/Classes/Middleware/ApiMiddleware.php index 86d847d..1344d8a 100644 --- a/packages/ep_products/Classes/Middleware/ApiMiddleware.php +++ b/packages/ep_products/Classes/Middleware/ApiMiddleware.php @@ -43,6 +43,10 @@ class ApiMiddleware implements MiddlewareInterface return $this->handleProductRequest($request); } + if ('/api/calendar-hotels' === $path) { + return $this->handleCalendarHotelsRequest($request); + } + if (str_starts_with($path, ContentFeedApi::PATH_PREFIX)) { return $this->contentFeedApi->handle($request); } @@ -97,6 +101,51 @@ class ApiMiddleware implements MiddlewareInterface return new JsonResponse($data); } + /** + * The calendar hotels of all visible products, for the contingent sync of MyE&P: their + * availability is needed for the iCal feed and the availability calendar, whether or not + * MyE&P has an accommodation with prices for them. + */ + private function handleCalendarHotelsRequest(ServerRequestInterface $request): ResponseInterface + { + if (!$this->authenticator->isAuthorized($request)) { + return new JsonResponse(['message' => 'Invalid or missing api key'], 403); + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('tx_epproducts_domain_model_product'); + + $rows = $queryBuilder + ->select('hotel.code', 'hotel.name AS hotelName', 'product.uid', 'product.name', 'product.season') + ->from('tx_epproducts_domain_model_product', 'product') + ->join('product', 'tx_epproducts_domain_model_hotel', 'hotel', 'hotel.uid = product.calendar_hotel') + ->where( + $queryBuilder->expr()->eq('product.sys_language_uid', 0), + $queryBuilder->expr()->neq('hotel.code', $queryBuilder->createNamedParameter('')), + ) + ->orderBy('hotel.code') + ->addOrderBy('product.uid') + ->executeQuery() + ->fetchAllAssociative(); + + $hotels = []; + foreach ($rows as $row) { + $code = trim((string)$row['code']); + $hotels[$code] ??= [ + 'code' => $code, + 'name' => trim((string)preg_replace('/\s+/', ' ', (string)$row['hotelName'])), + 'products' => [], + ]; + $hotels[$code]['products'][] = [ + 'uid' => (int)$row['uid'], + 'name' => trim((string)preg_replace('/\s+/', ' ', (string)$row['name'])), + 'season' => '' !== (string)$row['season'] ? (string)$row['season'] : null, + ]; + } + + return new JsonResponse(array_values($hotels)); + } + private function fetchProductData(string $productCode): ?array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)