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.
The 422 still told dispatchers to update Due Date. Point the remedy at Schedule On and make the SH-121 successor visible to EF so G6 lineage is complete.
Auto-approval now uses the WO-scoped $500/$5,000 Emergency cap instead of dispatch NTE, rejects a second open request across dispatches, and cancelling a WO withdraws pending uplifts with audit.
Expose workorders/{id}/uplifts list/create/cancel/revoke for the SH-196 dialog, aggregate upliftSummary on board rows, and add service/controller regression tests.
Add set-once IsAddOn with server cutoff at create, board DTO exposure, legacy type-7 backfill, and Types=AddOn search compat. Aligns with FE PR #61 frozen contract.
Past Due must track the deadline (Due Date), not Schedule On. Keep dueDate and scheduledDate PATCH mutations independent so rescheduling alone does not clear Past Due.
Map board PendingUpliftCount through WorkOrderDetailService.MapInfo and ignore soft-deleted dispatches in the aggregate so SH-188 gating is authoritative for board/search/detail.
Pass ClaimsPrincipal into GetCommentsAsync and filter via
GetAllForAccountAsync so account-scoped callers cannot enumerate
cross-tenant comments. ADR + cross-account tests updated.
Close the remaining SH-221 bypass: board/legacy comments and completion-doc now enforce server-derived account scope, authorize before blob storage, and cover cross-account regressions.
Close the remaining SH-221 read gap so Getworkorders, filtered lists, and GetWorkorderById enforce the same server-derived account boundary as board/media.
Co-authored-by: Cursor <cursoragent@cursor.com>
Seed resolvable Customer accounts and mock account resolution so create-path CI tests match fail-closed account scope.
Co-authored-by: Cursor <cursoragent@cursor.com>
Stamp WorkOrder.AccountId on all create paths and filter board/list/search/detail by server-derived account claims so scoped callers cannot cross accounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Derive staff vs technician scope from claims (Assigned for User), map
DbUpdateConcurrencyException to a stable 409, and add SQLite competing-write
tests for categorize-vs-categorize and categorize-vs-delete.
Require an authenticated ClaimsPrincipal at service entry and filter
tracked work orders with board base scope so deleted/template rows
surface as NotFound without disclosure.
Enforce MIME/extension/magic-byte validation, auth and workOrderVersion concurrency, audit on category changes, and validate-before-store with blob compensate.