PO/receipt attachments were uploaded via a browser presigned PUT straight
to R2 (POST /api/files/sign -> PUT uploadUrl -> linkDocument). That direct
browser->R2 PUT only succeeds when the bucket carries a CORS policy
allowing PUT from the portal origin; in production that policy was missing,
so the browser silently blocked the PUT, linkDocument never ran, and no
PODocument row was created -- "documents uploaded but not visible anywhere"
(0 PODocument rows in prod/staging).
Route uploads through a server action (uploadPoDocuments) that writes the
file with uploadBuffer and creates the PODocument row atomically -- the
same pattern the crewing module already uses for CV/crew-document uploads,
and within the 10mb serverActions.bodySizeLimit. This removes the R2-CORS
dependency and guarantees a created row always has its stored file.
Removes the now-dead presigned path (lib/upload-files.ts,
app/actions/link-document.ts, api/files/sign/route.ts).
Adds an integration test that drives the action against a real DB and
asserts the PODocument row is created and surfaced by the exact include
the PO detail page renders from (i.e. the attachment is visible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>