Updating this branch onto dev turned MediaRules_StillRejectPdf red, and the
test was the thing that had gone stale, not the rule. SH-171 added documents
to the SH-116 media allowlist for Extra and Aveta in 3454125 — a deliberate
widening with its own review — so asserting that media rejects a PDF outright
now contradicts shipped behaviour.
What this branch actually needs guarded is that the completion-doc allowlist
stopped there. Assert it per category instead: Completion, the category
SH-337 touches, plus Before and After, must all still refuse a PDF.
The completion-document endpoint persisted whatever file it received: the
only checks were non-null, non-empty, and a 30 MB request limit. Its sibling
media endpoint has enforced a MIME allowlist, MIME-to-extension pairing, and
a magic-byte signature check since SH-116.
Validate before the file reaches storage, so a rejected upload leaves nothing
behind. An undetermined content type is accepted only alongside a .pdf name
and a %PDF- signature, because the browser leaves File.type empty when the OS
cannot classify the file and the completion-doc dialog already allows that.
Stop client writes from changing Locations.AccountId, make the SH-221 migration discoverable, and thread the board-create CancellationToken through lookup and persistence.
Creating a Pending dispatch now stages VendorId FieldChanged from the previous
assignment so field lock and concurrency checks run like a live vendor PATCH.
Cancelled, Canceled, and Refused primaries are not live company assignments.
VendorId PATCH now inserts a Pending dispatch instead of mutating the refused row.
Three contract gaps found reviewing the frontend consumer:
- Revoking an auto-approved uplift never restored the dispatch NTE. Create
raises NTE for both auto-approved and approved requests, but revoke restored
it only for Approved, so the allowance was freed while the NTE stayed raised
and every create -> auto-approve -> revoke cycle compounded the inflation.
Revoke now compensates for NoApprovalRequired symmetrically.
- Revoke and cancel had no work-order lifecycle check, so a direct API call
could still mutate uplifts on a Completed or Canceled work order; the board
dialog's read-only state is UX only. Both now reject terminal work orders in
the service.
- WorkOrderBoardCancelService read the pending-uplift list outside any gate, so
an in-flight create could commit after that read and leave a pending uplift on
a Canceled work order. The cancel flow now runs inside the same per-work-order
gate as create, so the pending read, withdrawal and status audit serialize
against it.