Wrap the eight invitation-key downloads
CI / test (push) Canceled after 0s
CI / Build on the declared MSRV (push) Canceled after 0s
CI / Latest stable (advisory) (push) Canceled after 0s

`GOALS.md` listed these as "eight routes that take no auth token, so they do
not fit the authenticated request path and need a different entry point."
That was wrong on every count, and wrong in the way goal 6 exists to catch:
the premise came from the route names rather than the route file.

All eight sit inside `routes/api.php`'s `api/v1` group, under its
`token_auth` middleware, and the controllers' own OpenAPI blocks declare
`X-API-TOKEN`. So they needed no new entry point — just a macro, alongside
`entity_action_methods!` and for the same four entities. What the item had
actually described is the client-portal family in `routes/client.php`, which
is genuinely token-less, is not under `/api/v1`, and stays unwrapped for
that reason; it is now the gap entry in `COVERAGE.md`.

`download_{invoice,quote,credit,purchase_order}_by_invitation_key` returns
the PDF and `download_e_*_by_invitation_key` the XML e-document. These take
a contact's invitation key rather than a record id, so the server renders in
that contact's locale — a client with contacts in two languages gets a
different document per key, which the id-based `download_*` cannot express.

Two things here are easy to get wrong, so both are pinned by tests. The
entity segment is singular (`invoice/{key}/download`), the only route family
in the API that departs from the plural CRUD path. And an unknown key is a
400 carrying `no record found`: the controllers return that directly instead
of raising `ModelNotFoundException`, so the status matches the rule for a
missing record but the message does not.

The smoke test's read tier now downloads an invoice by invitation key,
taking the key from the `invitations` array the list route already returns.
It is there to re-check the token claim against a real server rather than
against a second reading of the source. The `download_e_*` four are left
out: they render from e-invoice settings a test instance need not have.

Also in here, unrelated: a rustfmt fix to a `match` arm in
`examples/smoke_test.rs` that was already off-format on main, so
`cargo fmt --check` — and with it CI's `test` job — was failing before this
change. It could not be split out cleanly, being in a file this change also
touches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 23:00:54 -04:00
co-authored by Claude Opus 5
parent 2532b83a32
commit e6e41fe5c6
8 changed files with 370 additions and 31 deletions
+24 -7
View File
@@ -157,6 +157,10 @@ Beyond "does it decode", the read tier fetches the same rows raw and fails
if a typed row lost a key (`keys kept: <resource>`, names only, never
values), and the write tier creates a purchase order, location, webhook and
recurring invoice from bare `Default` models, since those are what broke.
The read tier also downloads an invoice by invitation key, which is there to
re-check the claim that those routes are token-authenticated; the matching
`download_e_*` is left out, since it renders from e-invoice settings a test
instance need not have.
With `IN_API_PASSWORD` set as well, a password tier merges two throwaway
clients and purges the survivor (a real, permanent delete); it was run
against a disposable 5.13.50 instance on 2026-10-24 and all four checks
@@ -241,6 +245,17 @@ changes, doc tweaks) does not need an entry.
elsewhere without checking `routes/api.php`. In particular the official PHP
SDK puts bulk `download` on *every* resource, which is wrong; only
PDF-rendering entities support it.
- `invitation_download_methods!` generates `download_{x}_by_invitation_key`
and `download_e_{x}_by_invitation_key` for the same four resources. Two
traps, both pinned by tests: the entity segment is **singular**
(`invoice/{key}/download`, the only route family in the API that is), and
the routes are **authenticated** despite being the link a client follows —
they sit in `routes/api.php`'s `api/v1` group under `token_auth`. The
genuinely token-less invitation downloads are the separate client-portal
routes in `routes/client.php`, which are not under `/api/v1` and are not
wrapped. `COVERAGE.md` used to list the `/api/v1` eight as unwrapped for
that reason, having conflated the two families; don't reintroduce the
claim.
- `upload_methods!` generates `upload_{x}` for the 16 resources with a
`/{id}/upload` route; documents can only be created this way, because
`DocumentController::store` is an empty stub upstream (hence no
@@ -322,18 +337,20 @@ changes, doc tweaks) does not need an entry.
## Coverage
**All 34 of the API's `Route::resource` groups are wrapped**, plus `users`
(which isn't one). 325 methods: 30 resources with CRUD + bulk, 3 with CRUD
only, 4 with entity actions, 16 with document uploads, and 69 hand-written.
(which isn't one). 333 methods: 30 resources with CRUD + bulk, 3 with CRUD
only, 4 with entity actions, 4 with invitation-key downloads, 16 with
document uploads, and 69 hand-written.
Plus the non-CRUD surface: documents, entity actions and PDF downloads for
the four action-capable resources, `report`, `chart`, `search`, `statics`,
`send_email`, `refund_payment`, `ping`, and `health_check`.
the four action-capable resources, their invitation-key PDF and e-document
downloads, `report`, `chart`, `search`, `statics`, `send_email`,
`refund_payment`, `ping`, and `health_check`.
**`COVERAGE.md` is the detailed inventory** — every wrapped endpoint, every
gap, and the reason for each gap. The main remaining gaps are the
integration and auth routes, import/export, the public invitation-key
downloads (which take no auth token, so they don't fit the client's
request path), and a few company and user extras.
integration and auth routes, import/export, the client-portal routes (which
are not under `/api/v1`, so they don't fit the client's `base_url`), and a
few company and user extras.
Keep `COVERAGE.md` current when adding or removing an endpoint: it names
exact counts and route groups, so it goes stale quietly. The three coverage