Add multipart document uploads for the 16 resources that have the route

First item on the GOALS.md roadmap. `upload_{x}(id, files, is_public)`
attaches files through `/{resource}/{id}/upload` and returns the
refreshed record. Verified against upstream that all 16 controllers
answer with `itemResponse($x->fresh())`.

Three details that look wrong but are not, each checked in the upstream
source rather than taken from the published spec:

- The request is a POST carrying `_method=PUT`. The route is registered
  PUT, but PHP does not parse a multipart body on a real PUT, so the
  files never arrive. Laravel's kernel calls
  `enableHttpMethodParameterOverride()`, turning the spoofed method back
  into a PUT before routing. The spec documents POST with `_method=POST`,
  which would 405 against these routes.
- `is_public` is sent as 1/0. Laravel's `boolean` rule accepts only
  true, false, 0, 1, '0' and '1' under strict comparison, so "true"
  fails validation.
- Uploads are never retried. An upload adds a document, so it is not
  idempotent whatever its verb, and a replay after a lost response would
  attach the file twice. This falls out of reqwest's multipart body being
  un-cloneable, which nothing in the types enforces, so a test pins it
  for 500, 503 and 429.

An upload with no files, a nameless file or an unparseable MIME type is
refused locally (`Error::InvalidUpload`), since the server answers an
empty upload with a success that changed nothing.

Corrects two earlier claims:

- GOALS.md and COVERAGE.md said uploads would make `create_document`
  possible. They do not: `DocumentController::store` is an empty stub,
  and an upload is the only way a document is created. There is no
  `create_document`.
- The documented method count (265) had been stale since the typed
  `statics`/`search`/`health_check` commit, which added three `_as`
  methods without updating it. Recounted from the source: 284 = 210 +
  18 + 12 + 16 uploads + 28 hand-written.

Also fixes a README example, `let statics: Value = client.statics()`,
that stopped compiling when `statics` lost its type parameter.

`edocument/upload` is left out: it imports an expense from an
e-document rather than attaching a file, so it moves to the
import/export roadmap item.

Enabling `multipart` adds mime, mime_guess and unicase; MSRV stays 1.88,
re-verified by running the suite on it. 101 tests, up from 91.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 14:26:41 -04:00
co-authored by Claude Opus 5
parent 355cf08324
commit e4f2373b53
12 changed files with 608 additions and 32 deletions
+16 -4
View File
@@ -161,6 +161,18 @@ 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.
- `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
`create_document`). Three things are easy to get wrong, all verified
against the upstream source: the request is a **`POST` with
`_method=PUT`** (PHP does not parse multipart on a real `PUT`, and the
spec's `_method=POST` would 405); `is_public` goes out as **`1`/`0`**
(Laravel's strict `boolean` rule rejects the string `"true"`); and uploads
are **never retried**, because reqwest's multipart body is not cloneable
and `execute` gives an un-cloneable request exactly one attempt — a test
pins this, since nothing in the types would notice if it changed. Don't
"fix" any of the three.
- Non-CRUD endpoints (documents, reports, charts, search, statics, emails,
refunds, ping/health) are hand-written in the last `impl` block rather than
macro-generated, because their request and response shapes are all
@@ -224,16 +236,16 @@ 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). 265 methods: 30 resources with CRUD + bulk, 3 with CRUD
only, 4 with entity actions, and 25 hand-written.
(which isn't one). 284 methods: 30 resources with CRUD + bulk, 3 with CRUD
only, 4 with entity actions, 16 with document uploads, and 28 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`.
**`COVERAGE.md` is the detailed inventory** — every wrapped endpoint, every
gap, and the reason for each gap. The main remaining gaps are multipart
uploads (needs a reqwest feature), a handful of per-resource extras
gap, and the reason for each gap. The main remaining gaps are a handful of
per-resource extras
(`clients/{id}/purge`, `company_gateways/{id}/test`, …), and the
integration/auth routes.