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
@@ -1,65 +1,26 @@
|
|||||||
# TYPO3 CMS Base Distribution
|
# E&P Reisen websites (TYPO3 13)
|
||||||
|
|
||||||
Get going quickly with TYPO3 CMS.
|
TYPO3 13 LTS for www.ep-reisen.de and its subsites (ski-boarderweek, snowzone, semesterende-skireisen,
|
||||||
|
ski-wochenenden, …). Products, hotels, regions and dates live in `packages/ep_products`, templates
|
||||||
|
and assets in `packages/ep_theme`. Dates, prices and availability come from **MyE&P** (`EP/myep`),
|
||||||
|
which in turn reads bpn-connect.
|
||||||
|
|
||||||
## Prerequisites
|
## Local setup
|
||||||
|
|
||||||
* PHP 8.2
|
DDEV project: `ddev start`, then `ddev composer install` and `ddev npm install && ddev npm run build`.
|
||||||
* [Composer](https://getcomposer.org/download/)
|
Rebuild the assets after template changes: Tailwind purges classes it does not find in the
|
||||||
|
templates.
|
||||||
|
|
||||||
## Quickstart
|
## Documentation
|
||||||
|
|
||||||
* `composer create-project typo3/cms-base-distribution project-name ^13`
|
- [docs/cms-api.md](docs/cms-api.md): the API TYPO3 serves to MyE&P and the chatbot
|
||||||
* `cd project-name`
|
(`/api/hotel`, `/api/product`, `/api/calendar-hotels`, `/api/content/*`).
|
||||||
|
- [docs/calendars.md](docs/calendars.md): price calendar, Belegungskalender and iCal feed: where
|
||||||
|
the data comes from, caching, and what to check when a calendar does not load.
|
||||||
|
|
||||||
Note that this distribution installs most, but not all of the TYPO3 CMS core extensions.
|
## Deployment
|
||||||
Depending on your need you might also want to install other TYPO3 extensions from
|
|
||||||
[packagist.org](https://packagist.org/?type=typo3-cms-framework).
|
|
||||||
|
|
||||||
### Setup
|
`deploy.php` (Deployer). A deploy runs `extension:setup`, resets OPcache and flushes all caches.
|
||||||
|
|
||||||
To start an interactive installation, you can do so by executing the following
|
|
||||||
command and then follow the wizard:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer exec typo3 setup
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup unattended (optional)
|
|
||||||
|
|
||||||
If you're a more advanced user, you might want to leverage the unattended installation.
|
|
||||||
To do this, you need to execute the following command and substitute the arguments
|
|
||||||
with your own environment configuration.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export TYPO3_SETUP_ADMIN_PASSWORD=$(tr -dc "_A-Za-z0-9#=$()/" < /dev/urandom | head -c24)
|
|
||||||
composer exec -- typo3 setup \
|
|
||||||
--no-interaction \
|
|
||||||
--server-type=other \
|
|
||||||
--driver=sqlite \
|
|
||||||
--admin-username=admin \
|
|
||||||
--admin-email="[email protected]" \
|
|
||||||
--project-name="My TYPO3 Project" \
|
|
||||||
--create-site="http://localhost:8000/"
|
|
||||||
echo "Admin password: ${TYPO3_SETUP_ADMIN_PASSWORD}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development server
|
|
||||||
|
|
||||||
While it's advised to use a more sophisticated web server such as
|
|
||||||
Apache 2 or Nginx, you can instantly run the project by using PHPs` built-in
|
|
||||||
[web server](https://secure.php.net/manual/en/features.commandline.webserver.php).
|
|
||||||
|
|
||||||
* `TYPO3_CONTEXT=Development php -S localhost:8000 -t public`
|
|
||||||
* open your browser at "http://localhost:8000"
|
|
||||||
|
|
||||||
Please be aware that the built-in web server is single threaded and only meant
|
|
||||||
to be used for development.
|
|
||||||
|
|
||||||
## Next steps
|
|
||||||
|
|
||||||
* [Getting Started with TYPO3](https://docs.typo3.org/permalink/t3start:start)
|
|
||||||
* [Create a Site Package](https://docs.typo3.org/permalink/t3sitepackage:start)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
Queries for plugin renaming
|
|
||||||
===========================
|
|
||||||
|
|
||||||
```mysql
|
|
||||||
update tt_content set list_type='epproducts_searchbar' where list_type='epproducts_pi1';
|
|
||||||
update tt_content set list_type='epproducts_product_detail' where list_type='epproducts_pi3';
|
|
||||||
update tt_content set list_type='epproducts_searchresult' where list_type='epproducts_pi2';
|
|
||||||
update tt_content set list_type='epproducts_product_teasergroup' where list_type='epproducts_pi4';
|
|
||||||
update tt_content set list_type='epproducts_country_detail' where list_type='epproducts_pi5';
|
|
||||||
update tt_content set list_type='epproducts_region_teasergroup' where list_type='epproducts_pi6';
|
|
||||||
update tt_content set list_type='epproducts_region_detail' where list_type='epproducts_pi10';
|
|
||||||
update tt_content set list_type='epproducts_city_teasergroup' where list_type='epproducts_pi7';
|
|
||||||
update tt_content set list_type='epproducts_city_detail' where list_type='epproducts_pi11';
|
|
||||||
update tt_content set list_type='epproducts_hotel_teasergroup' where list_type='epproducts_pi8';
|
|
||||||
update tt_content set list_type='epproducts_hotel_detail' where list_type='epproducts_pi12';
|
|
||||||
update tt_content set list_type='epproducts_faq' where list_type='epproducts_pi9';
|
|
||||||
```
|
|
||||||
@@ -40,7 +40,7 @@ bpnConnectApiTimeout = 5
|
|||||||
# cat=MyEpAPI; type=int; label=Bpn Connect API max total request duration in seconds (0 = unlimited)
|
# cat=MyEpAPI; type=int; label=Bpn Connect API max total request duration in seconds (0 = unlimited)
|
||||||
bpnConnectApiMaxDuration = 15
|
bpnConnectApiMaxDuration = 15
|
||||||
|
|
||||||
# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints used by MyE&P (/api/hotel, /api/product, /api/content/*); sent as X-Api-Key or ?key=. Empty denies every request.
|
# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints (/api/hotel, /api/product, /api/calendar-hotels for MyE&P; /api/content/* for the chatbot; see docs/cms-api.md); sent as X-Api-Key or ?key=. Empty denies every request.
|
||||||
cmsApiKey =
|
cmsApiKey =
|
||||||
|
|
||||||
# cat=ContentFeed; type=boolean; label=Serve the chatbot content feed (off: all feed endpoints answer 503)
|
# cat=ContentFeed; type=boolean; label=Serve the chatbot content feed (off: all feed endpoints answer 503)
|
||||||
|
|||||||
Reference in new issue
Block a user