docs: CMS API and calendars
docs/cms-api.md describes the endpoints TYPO3 serves to MyE&P and the chatbot (/api/hotel, /api/product, /api/calendar-hotels, /api/content/*): key, parameters, responses, errors. It is the other side of MyE&P's API consumer guide. docs/calendars.md covers the price calendar, the Belegungskalender and the iCal feed: which product gets which, the MyE&P endpoints behind them, caching, troubleshooting, and the rule that a product's calendar hotel must carry the BusPro code, not a group house's CMS code (which broke three example trips). The README replaces the TYPO3 base distribution boilerplate and links both. The plugin renaming SQL in ep_products' README is obsolete since the migration. The cmsApiKey description names /api/calendar-hotels. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bf9afd143d
commit
2bd663a9bf
5 files changed
+234
-73
No files matched your search
@@ -0,0 +1,116 @@
|
||||
# Availability and prices on product pages
|
||||
|
||||
TYPO3 shows three things built from contingent data. None of them talks to bpn-connect directly:
|
||||
all go through **MyE&P**, which keeps a local snapshot of bpn-connect's availability and holds the
|
||||
group house prices.
|
||||
|
||||
| What | Where | Shown for | MyE&P endpoint |
|
||||
|---|---|---|---|
|
||||
| **Price calendar** + price tables | product detail, tab "Termine/Preise" | products with a calendar hotel and season **summer** (`s`) | `contingents/calendar`, `contingents/prices` |
|
||||
| **Belegungskalender** (availability calendar) | product detail, sidebar | products with a calendar hotel and season **not summer** | `contingents/availability` (+ `contingents/calendar` for the months to leave out) |
|
||||
| **iCal feed** for booking portals | `https://www.ep-reisen.de/contingents/<hotel>.ical` | every hotel | `contingents/availability` |
|
||||
|
||||
So every product with a calendar hotel shows exactly one calendar. The MyE&P side is documented in
|
||||
its `docs/api-consumer-guide.md` (`EP/myep`).
|
||||
|
||||
## The calendar hotel field
|
||||
|
||||
A product's **calendar hotel** (`calendar_hotel`) decides whose availability and prices it shows.
|
||||
TYPO3 sends that hotel's **code** to MyE&P, which only understands **BusPro calendar codes** (the
|
||||
codes bpn-connect knows, e.g. `DGS`, `LPJ`, `DKS`).
|
||||
|
||||
Most hotel records carry the BusPro code. Group houses also have **content records** of their own
|
||||
(`DKSGRP`, `LPJGRP`, `SERASCSO`, …) whose code is the **CMS code** MyE&P uses to fetch content via
|
||||
`/api/hotel`. These must **not** be used as calendar hotel: MyE&P and bpn-connect do not know
|
||||
them, and the calendar shows its error message. This broke three example trips until 2026-10-29.
|
||||
|
||||
| Use as calendar hotel | Not |
|
||||
|---|---|
|
||||
| `DKS` Sportclub Schweizerhaus (uid 193) | `DKSGRP` Davos Klosters - Gruppenhaus Schweizerhaus |
|
||||
| `LPJ` Sportclub Jenatsch (uid 37) | `LPJGRP` Lenzerheide - Gruppenhaus Jenatsch |
|
||||
|
||||
`/api/calendar-hotels` lists all calendar hotels in use; every code in it should be a BusPro code.
|
||||
A hidden hotel record counts as no calendar hotel: the product shows no calendar and the code is
|
||||
not in the list (e.g. `ASB` Sportclub Bader).
|
||||
|
||||
## Price calendar (summer)
|
||||
|
||||
- `Product/Detail.html`, section `Dates`, partial `Product/PriceCalendar`;
|
||||
`AjaxCalendarController::priceCalendarAction` → `contingents/calendar` (status and price per
|
||||
day), `priceTableAction` → `contingents/prices` for the current and the next year (loaded when
|
||||
scrolled into view).
|
||||
- Only works for hotels that are an **accommodation with prices** in MyE&P (summer group houses).
|
||||
|
||||
## Belegungskalender (winter)
|
||||
|
||||
- `Product/Detail.html`, section `AvailabilityCalendar`, loaded by htmx when it scrolls into view
|
||||
(`intersect once`, so the hidden copy of the sidebar on the other breakpoint does not load);
|
||||
`AjaxCalendarController::availabilityAction`, template `AjaxCalendar/Availability.html`,
|
||||
month select `calendar_controller.js`.
|
||||
- All months in one response; the select only switches between them. 12 months from the current
|
||||
one, extended to the last month MyE&P has data for.
|
||||
- **Months with prices are left out**: they are summer, covered by the price calendar. Only summer
|
||||
months have prices today; that is the whole distinction. If `contingents/calendar` fails, all
|
||||
months are shown (not cached).
|
||||
- Colours as on TYPO3 10: green available (`OK`, and days without data), yellow on request
|
||||
(`ON_REQUEST`), red not available (`BLOCKED`), past days grey; a status change from the previous
|
||||
day is drawn as a diagonal. The `availability-*` classes are built in PHP, so they are safelisted
|
||||
in `tailwind.config.js`; without that Tailwind purges them.
|
||||
- Works for every hotel MyE&P syncs, with or without an accommodation (see below).
|
||||
|
||||
## iCal feed
|
||||
|
||||
- `IcalController::contingentsAction`, `IcalService`; route `/contingents/{hotel}.ical` on the
|
||||
`ep-reisen` site (`config/sites/ep-reisen/config.yaml`, page 9, type 1710), `{hotel}` is the
|
||||
hotel's path segment.
|
||||
- One `Unavailable` event per run of `BLOCKED` or `ON_REQUEST` days with the same status, as on
|
||||
TYPO3 10.
|
||||
- Never cached. When MyE&P has no availability it answers `503` with `Retry-After: 900` instead of
|
||||
an empty calendar, which the portals would read as "every day free"; they keep their last import.
|
||||
|
||||
## Which hotels MyE&P has availability for
|
||||
|
||||
MyE&P syncs its snapshot from bpn-connect every 15 minutes during the day and hourly at night, for
|
||||
the calendar codes of its accommodations **plus** every code in TYPO3's `/api/calendar-hotels`
|
||||
(see [cms-api.md](cms-api.md)). So a hotel offered only in winter needs nothing in MyE&P: setting
|
||||
it as calendar hotel of a visible product is enough; it is synced with the next run.
|
||||
|
||||
`contingents/availability` answers:
|
||||
- `400` for a code that is not synced (not an accommodation, not in TYPO3's list),
|
||||
- `502` when the hotel's snapshot is missing or older than 6 hours (sync failing),
|
||||
- `200` with one entry per known day otherwise.
|
||||
|
||||
`contingents/calendar` and `contingents/prices` additionally need an accommodation with prices.
|
||||
|
||||
MyE&P's `GET /api/contingents/status` lists every synced code with its source, prices, last sync
|
||||
and last error: the first place to look when a calendar does not load.
|
||||
|
||||
## Caching
|
||||
|
||||
- The AJAX responses (price calendar, price tables, Belegungskalender) are page-cached on the AJAX
|
||||
page for at most **1 hour** (`cache_period = 3600`) and never past midnight
|
||||
(`cache_clearAtMidnight`, past days must turn grey). Browsers do not cache them
|
||||
(`sendCacheHeaders = 0`).
|
||||
- **Errors are not cached** (`disablePageCache()`): the next visitor triggers a new attempt.
|
||||
- Every AJAX URL carries a cHash (required for `tx_epproducts_ajax`), so each hotel and year has its
|
||||
own cache entry.
|
||||
- Entries are tagged `ep_hotel_<code>`. Nothing flushes them automatically; a deploy flushes all
|
||||
caches. To refresh by hand: `vendor/bin/typo3 cache:flush --group pages`, or flush the tag via
|
||||
the `CacheManager` for a single hotel.
|
||||
- On top of that comes MyE&P's sync interval, so a calendar is at most about 1¼ hours behind
|
||||
bpn-connect.
|
||||
|
||||
## When a calendar does not load
|
||||
|
||||
The message is "Der Kalender konnte aktuell nicht geladen werden."; the iCal feed answers `503`.
|
||||
|
||||
| Cause | How to tell | Fix |
|
||||
|---|---|---|
|
||||
| Calendar hotel has a CMS code (`…GRP`, `…SO`) | code not BusPro; MyE&P `400`, bpn-connect `404` | set the hotel record with the BusPro code as calendar hotel |
|
||||
| Code not synced by MyE&P | not in `contingents/status`; MyE&P `400` | check that the product and its calendar hotel are visible (then it is in `/api/calendar-hotels`) |
|
||||
| Sync failing for the hotel | `contingents/status`: `available: false`, `lastError` | MyE&P / bpn-connect |
|
||||
| MyE&P unreachable or its OAuth failing | all hotels fail; TYPO3 log `EP.EpProducts.MyEP.ApiClient` | MyE&P |
|
||||
| Price calendar only: no accommodation or no prices | MyE&P `400` / empty | prices are maintained in MyE&P's Groups admin |
|
||||
|
||||
TYPO3 logs every failed MyE&P call in `var/log/typo3_*.log` (component
|
||||
`EP.EpProducts.MyEP.ApiClient`, for the iCal feed also `EP.EpProducts.Service.IcalService`).
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# CMS API
|
||||
|
||||
Endpoints TYPO3 serves to other systems: **MyE&P** (hotel and product content, calendar hotels)
|
||||
and the **chatbot** (content feed). They are answered by `EP\EpProducts\Middleware\ApiMiddleware`
|
||||
before the page resolver, so they do not depend on a page and are never page-cached.
|
||||
|
||||
The other side of these contracts is documented in MyE&P's `docs/api-consumer-guide.md`
|
||||
(`EP/myep`).
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints need the shared key `cmsApiKey` from the `ep_products` extension configuration
|
||||
(Settings → Extension Configuration → ep_products, tab ContentFeed):
|
||||
|
||||
- header `X-Api-Key: <key>` (preferred, query strings end up in access logs), or
|
||||
- query parameter `?key=<key>`.
|
||||
|
||||
A missing or wrong key answers `403 {"message": "Invalid or missing api key"}`. An empty
|
||||
`cmsApiKey` denies every request. MyE&P sends the key as `APP_CMS_API_KEY`; both sides must be
|
||||
changed together.
|
||||
|
||||
## `GET /api/hotel?hotel=<code>`
|
||||
|
||||
Content of one hotel, for MyE&P's group house pages and booking flow.
|
||||
|
||||
- `hotel`: the TYPO3 hotel code or one of its matchcode aliases. In MyE&P this is the
|
||||
accommodation's **CMS code** (e.g. `DGSGRP`), which can differ from its calendar code.
|
||||
- `400` without `hotel`, `404 {"message": "Hotel not found"}` for an unknown code.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "…", "code": "…",
|
||||
"description": "…", "features": "…", "room_types": "…",
|
||||
"additional_information": "…", "address": "…",
|
||||
"images": {"original": [{"url": "…", "alt": "…", "title": "…"}], "resized": […]},
|
||||
"icons": […],
|
||||
"region": {"name": "…", "latitude": …, "longitude": …, "webcam": "…", "ski_area": "…",
|
||||
"ski_area_extended": "…", "description": "…", "news": "…", "length": …,
|
||||
"altitude": …, "lifts": …, "images": {…}, "region_maps": [{"url": "…", "alt": "…", "title": "…"}]}
|
||||
}
|
||||
```
|
||||
|
||||
## `GET /api/product?product=<code>[&hotel=<code>]`
|
||||
|
||||
Content of one product, optionally with a hotel.
|
||||
|
||||
- `product`: the product code. `400` without it, `404 {"message": "Product not found"}` for an
|
||||
unknown code.
|
||||
- `hotel` (optional): adds `hotel` with the same content as `/api/hotel`.
|
||||
|
||||
```json
|
||||
{
|
||||
"product": {"name": "…", "code": "…", "images": {…}},
|
||||
"country": {"name": "…"},
|
||||
"region": {…},
|
||||
"city": {"name": "…"},
|
||||
"hotel": {…}
|
||||
}
|
||||
```
|
||||
|
||||
## `GET /api/calendar-hotels`
|
||||
|
||||
The calendar hotel of every visible product: the hotel codes whose availability TYPO3 shows
|
||||
(iCal feed, availability calendar, price calendar). MyE&P syncs its contingent snapshot for
|
||||
these codes in addition to its accommodations, so hotels offered only in winter, which have no
|
||||
accommodation in MyE&P, have availability too. See [calendars.md](calendars.md).
|
||||
|
||||
- Only products and hotels that are not hidden or deleted; hotels without a code are left out.
|
||||
- Sorted by code; products by uid. Names have line breaks collapsed.
|
||||
- `season`: `"s"` (summer), `"w"` (winter) or `null`.
|
||||
|
||||
```json
|
||||
[
|
||||
{"code": "DKI", "name": "Sportclub Schwendi", "products": [
|
||||
{"uid": 155, "name": "Sportclub Schwendi", "season": "w"},
|
||||
{"uid": 362, "name": "Gruppenhaus Schwendi", "season": "s"}
|
||||
]}
|
||||
]
|
||||
```
|
||||
|
||||
Every code in this list should be a **BusPro calendar code**. A group house's content record
|
||||
(`…GRP`, `…SO`) set as a product's calendar hotel puts its CMS code in here, which bpn-connect does
|
||||
not know (see [calendars.md](calendars.md#the-calendar-hotel-field)).
|
||||
|
||||
## `GET /api/content/manifest` and `GET /api/content/records?type=<type>&uids=<uids>`
|
||||
|
||||
The content feed for the chatbot (`EP\EpProducts\ContentFeed\ContentFeedApi`).
|
||||
|
||||
- `manifest`: everything the feed publishes (pages of the configured sites and records), with a
|
||||
modification timestamp per item. Cached for 10 minutes.
|
||||
- `records`: up to 100 records of one type per request. `type` is one of `product`, `hotel`,
|
||||
`region`, `country`, `city`, `concept`, `news`; `uids` comma-separated. Uids that are no longer
|
||||
eligible are left out, not answered with an error (the consumer reads them as deleted).
|
||||
- `400` for an unknown type, no uids or more than 100 uids; `404` for other paths.
|
||||
- `503` with `Retry-After: 3600` when the feed is switched off (`contentFeedEnabled = 0`, the kill
|
||||
switch for when the host is struggling), `503` when `contentFeedSites` is empty. The consumer
|
||||
keeps what it has on a 503; nothing is deleted on the chatbot side.
|
||||
|
||||
Configuration (extension configuration `ep_products`, tab ContentFeed): `contentFeedEnabled`,
|
||||
`contentFeedSites`, `contentFeedLanguages`, `contentFeedExcludedPages`,
|
||||
`contentFeedNewsDetailPages`.
|
||||
Reference in new issue
Block a user