refactor: move the bulk export to its own /api/exports resource
/api/travels/{dateCode} accepts an arbitrary string, so /api/travels/export
shared a namespace with every date code and stayed reachable only because
byCode() carried a negative lookahead ruling that one string out. Nothing
in the URL announced the constraint, and getting it wrong mis-routed
silently: the first attempt widened the segment to `.+` and swallowed
/api/travels/{dateId}/{hotelId}/{dateTo}/availability whole.
/api/exports never takes a parameterised segment, the same shape as
/api/contingents, so the collision stops existing instead of being
guarded. TravelController returns byte-for-byte to its state on master,
lookahead included, and the export moves to its own resource controller.
The routing test now also asserts that nothing is reintroduced under the
date code namespace.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
b27639c7fa
commit
3221008f90
5 files changed
+130
-80
No files matched your search
@@ -12,7 +12,7 @@ A Symfony application exposing a read-mostly JSON API under the `/api` prefix. A
|
||||
- Enabled grants: **client_credentials**, **authorization_code** (PKCE required for public clients), **refresh_token**. Password and implicit grants are disabled.
|
||||
- Access token TTL is **10 minutes** for every grant.
|
||||
Do not hardcode it — read `expires_in` from the token response, it is authoritative and may be tuned.
|
||||
A token can therefore expire part-way through a long page walk of `GET /api/travels/export`; re-request one on
|
||||
A token can therefore expire part-way through a long page walk of `GET /api/exports/travels`; re-request one on
|
||||
401 and resume at the page you were on. At the default `perPage=500` the whole export is a single page today.
|
||||
- Available scopes: `email`, `id`, `profile`, `roles`, `api`. Default scope if none requested: `email`.
|
||||
- Each granted scope maps to a role `ROLE_OAUTH2_<SCOPE_UPPERCASE>` (e.g. scope `api` → `ROLE_OAUTH2_API`).
|
||||
@@ -50,9 +50,9 @@ There is no single error envelope; three shapes occur:
|
||||
- `{"error": "...", "type": "api_error"}` — upstream failures on some endpoints. **Caution:** on `/api/products` and `/api/travels/{dateId}/{hotelId}/{dateTo}/availability` this shape is returned with HTTP **200**, so clients must check for the presence of an `error` key, not only the status code.
|
||||
- `{"success": false, "message": "..."}` — the write endpoints (contact form, newsletter).
|
||||
|
||||
Validation failures on endpoints using mapped query strings / payloads (`/api/contingents/*`, `/api/travels/export`, `/api/newsletter-subscriptions`) return **400** with Symfony's constraint-violation body. Missing/invalid token yields **401**; insufficient scope yields **403**.
|
||||
Validation failures on endpoints using mapped query strings / payloads (`/api/contingents/*`, `/api/exports/*`, `/api/newsletter-subscriptions`) return **400** with Symfony's constraint-violation body. Missing/invalid token yields **401**; insufficient scope yields **403**.
|
||||
|
||||
`GET /api/travels/export` deliberately uses real status codes throughout and never the 200-with-an-`error`-key shape: a failure there is a 5xx, so retry logic can key on the status code alone.
|
||||
`GET /api/exports/travels` deliberately uses real status codes throughout and never the 200-with-an-`error`-key shape: a failure there is a 5xx, so retry logic can key on the status code alone.
|
||||
|
||||
The HTTP methods listed below are the intended ones. Several read routes do not restrict methods server-side; clients should still use `GET` for them.
|
||||
|
||||
@@ -139,19 +139,19 @@ This is a full overwrite, not a merge.
|
||||
|
||||
---
|
||||
|
||||
### Travels
|
||||
### Bulk exports
|
||||
|
||||
#### `GET /api/travels` — scope `api`
|
||||
|
||||
Index/mapping of all known travel dates. Object keyed by numeric travel id, each value `{id, code, label, dateFrom, dateTo, hotels: [...]}`.
|
||||
|
||||
#### `GET /api/travels/export` — scope `api`
|
||||
#### `GET /api/exports/travels` — scope `api`
|
||||
|
||||
Bulk export of every travel date × hotel pair with its boarding points resolved, for clients that mirror the
|
||||
disposition locally. One request replaces a walk over `GET /api/travels` plus a per-pair detail call.
|
||||
|
||||
This is the only paginated endpoint and the only one with a `meta` envelope.
|
||||
|
||||
It sits under `/api/exports`, not `/api/travels`, on purpose: `/api/travels/{dateCode}` accepts an
|
||||
arbitrary string, so an export hung off that collection would share a namespace with every date code.
|
||||
`/api/exports` never takes a parameterised segment, so there is nothing for it to collide with.
|
||||
|
||||
Query parameters:
|
||||
|
||||
| Parameter | Type | Default | Notes |
|
||||
@@ -238,6 +238,14 @@ rest. `total` is the row count **after** skips, and `totalPages` is `ceil(total
|
||||
under one cache tag, so all three move together on an upstream sync rather than drifting between your calls.
|
||||
It is cached for 3 hours; `generatedAt` is when the cached copy was assembled, not when you requested it.
|
||||
|
||||
---
|
||||
|
||||
### Travels
|
||||
|
||||
#### `GET /api/travels` — scope `api`
|
||||
|
||||
Index/mapping of all known travel dates. Object keyed by numeric travel id, each value `{id, code, label, dateFrom, dateTo, hotels: [...]}`.
|
||||
|
||||
#### `GET /api/travels/{dateId}/{hotelId}` — scope `api`
|
||||
|
||||
Single travel by **numeric** ids (`hotelId` optional). Route matches only when both segments are digits.
|
||||
|
||||
Reference in new issue
Block a user