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 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-29 14:45:44 +01:00
1 parent 5d41660a9a
commit bf9afd143d
1 file changed
+49
@@ -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)