feat: derive calendar date range from price configurations

This commit is contained in:
Björn Fromme
2026-08-26 13:32:25 +02:00
parent 61e020c3d0
commit 80d734e7d6
6 changed files with 260 additions and 25 deletions
+12 -4
View File
@@ -180,16 +180,24 @@ Prices are **decimal major units** (converted from integer minor units server-si
#### `GET /api/contingents/calendar?hotelCode=…&dateFrom=…&dateTo=…` — scope `api`
Per-day availability enriched with prices. Both dates are `Y-m-d` and **inclusive** (`dateTo` is the last night, not the checkout day); the range may not exceed 366 days and `dateTo` must not precede `dateFrom`.
Per-day availability enriched with prices.
`dateFrom` and `dateTo` are **optional, and must be supplied together** — one without the other is a `400`. Both are `Y-m-d` and **inclusive** (`dateTo` is the last night, not the checkout day); when supplied, the range may not exceed 366 days and `dateTo` must not precede `dateFrom`.
**Omit both to get the full priced span.** The range is then derived from the accommodation's persisted prices, `MIN(dateFrom)` to `MAX(dateTo)`, and never starts before today. This is the way to fetch everything a hotel sells in one call, and it is not subject to the 366-day cap — a hotel priced over two seasons returns well over a year of entries.
Full-span responses are contiguous, exactly like ranged ones: every day between the derived bounds gets an entry, including days no price covers. Those come back `BLOCKED` with `null` price fields — a hotel closed over winter reports the whole closure day by day rather than skipping it, so a consumer keying off `status` always finds one.
The response is `[]` — a `200`, not a `502` — when the hotel has no prices at all, or when every priced period has already ended.
Availability is served from a **local snapshot** refreshed over a 24-month horizon — every 15 minutes during the day, hourly overnight — not fetched from the upstream contingent service per request. The endpoint therefore responds in single-digit milliseconds and stays available during an upstream outage, at the cost of being at most one sync interval stale.
The response format is unchanged from the previous upstream-backed implementation: same fields, same order, same types, same `status` values. One behavioural note — the endpoint now returns **one entry per requested day**. Previously days the upstream service did not mention were simply absent, so a response may now contain days it would not have contained before. It is a superset, never a different shape.
The response format is unchanged from the previous upstream-backed implementation: same fields, same order, same types, same `status` values. One behavioural note — the endpoint returns **one entry per day of the range**, whether that range came from `dateFrom`/`dateTo` or was derived from the prices. Previously days the upstream service did not mention were simply absent, so a response may contain days it would not have contained before. It is a superset, never a different shape.
Two kinds of day resolve to `BLOCKED` regardless of the underlying contingent:
- **days in the past** — the snapshot is maintained from today forward only (`prices` likewise refuses past ranges)
- **days beyond the synced horizon** — currently 24 months out
- **days beyond the synced horizon** — currently 24 months out. Prices are often maintained further ahead than the contingent is synced, so a full-span response can end in a run of `BLOCKED` days that simply have no availability data yet.
Each entry:
@@ -208,7 +216,7 @@ Each entry:
}
```
Important rule: **a day with no maintained price is forced to `Blocked`** regardless of what the upstream contingent says. Price fields may be `null` in that case.
Important rule: **a day with no maintained price is forced to `Blocked`** regardless of what the upstream contingent says. Price fields may be `null` in that case. This applies to both modes.
Errors: `400 {"error":"Hotel not found for hotelCode."}`, `400` with violations for invalid parameters.