Approve, reject, request-changes, expiry and escalation staged their work
order audit with WorkOrderId = dispatch.WorkOrderId ?? 0. WorkOrderAuditLogs
requires a real work order, so any uplift on a dispatch without an owning
work order failed to save: the decision returned a 500 and the request stayed
Pending, and the expiry sweep failed on it every run.
The audit now goes to the work order the uplift resolves to through the
existing owner-or-linked read that revoke already uses. When none resolves,
the status change is saved on the request and no audit row is written.
UserCanApprove only accepted roles listed in Approvals:Tier1Roles/Tier2Roles,
so an environment whose config omits Admin denied every approval surface to
admins: can-approve, per-row CanDecide, approve/reject/request-changes and
evidence download. Admin now short-circuits the check; tier-role config still
governs every other role.
After SH-393 the uplift dispatch resolves only through dispatches the work
order owns or links through DispatchWorkOrders. Model the shared dispatch
that way so the concurrent-insert conflict test exercises the conflict
mapping again; assertions are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Board create left the new primary dispatch with no WorkOrderId, so uplifts
created on those work orders were written to a dispatch the work order's
uplift reads never resolve: not listed, allowance never consumed, queue WO
number blank. The same orphan made ApptDate/vendor patches fail with
"A primary dispatch is required".
- Board create backfills Dispatch.WorkOrderId after the first save.
- Uplift create resolves its dispatch through the read-side scope
(non-deleted, owned or linked); otherwise the stable
"no primary dispatch" error.
- Data-only migration assigns existing orphaned primaries to the single
work order naming them primary; idempotent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the operation work order when staging uplift audit logs so a dispatch linked through DispatchWorkOrders cannot write the event to its primary work order. Add coverage for the cross-work-order fallback case.
The merge_group pass-through added in #159 produced a second, skipped check
run with the same name on every pull request, because GitHub reports a check
for a job whose if: is false. The pinned dependency-review action resolves its
refs from merge_group.base_sha and head_sha itself, so the single real job now
triggers on both events with no conditions. Pull requests and merge groups
each get one check run, and the required check is still satisfied in the
queue.
The gate computed changed files with a two-dot diff against the PR base tip,
so everything main gained after the branch point counted as this change. A
branch behind main that touched C# failed G13 whenever main had merged
Terraform in between, which is how #152 failed after #154, #156 and #158
landed. The merge queue no longer requires branches to be current, so the
false positive would have hit every stale PR. Both diffs now start at the
merge base. Push and merge-group runs are unchanged because their base is an
ancestor of the head.
The org PR template pre-filled Summary / Validation / Tests / Notes here while
REVIEW_AND_PR_FRAMEWORK.md section 8 prescribes Summary / Changes and value /
Ticket. A repo template now overrides the org one, and the framework notes the
divergence from the org pr-policy workflow, which is not wired in.
README.md orients a reader: environments, architecture in one line, project
map, local commands, the governance gate, deployment, and a documentation map.
Cleanup: TODO.md is removed because Jira owns work status and its items are
stale or done. BACKEND_ARCHITECTURE.md is removed as superseded; the two
references now point at git history. .env.example loses its BOM and mojibake
dashes. .gitattributes keeps its one active rule.
The org window (before 6am on Monday) has produced no PRs in this repository
since the overlay landed, so the overlay now opens every weekday morning. A
regex manager tracks EF_VERSION in the Elastic Beanstalk packaging script,
which installs dotnet-ef at deploy time and would otherwise fall behind the
tool manifest and EF Core packages; it joins the nuget minor and patch group.
ASP.NET Core, EF Core and dotnet-ef majors are disabled while the target is
net8.0 so the dashboard approval list only shows updates that can be taken.
Build and test, architecture, and dependency-review are the required checks
on main. A merge queue only counts checks that ran on the merge_group event,
so each workflow now triggers on it. The architecture gate reads the merge
group's own base and head because github.event.before is empty there. The
dependency-review action cannot diff a merge group, so that event reports the
same check name from a pass-through job; the real review already gated the
pull request before it could be queued.
The manual POC "follow the site" clear-rule compared the edit against every
live Site contact. A work order only ever displays one of them, so editing to
a different live contact (Site has Alice primary and Bob; WO shows Alice; edit
to Bob) matched, cleared the override, and left the row showing Alice with no
audit row written — the SH-379 symptom on a different input. A work order with
a linked WorkOrderContacts POC hit the same bug when the dispatcher typed the
Site's primary: the override cleared and the linked contact showed instead.
Compare the edit against the single contact the work order actually follows —
the linked WorkOrderContacts POC, or else the Site primary (ResolvePrimary) —
matching the board projection's override -> linked -> site precedence, and
store the override whenever the edit differs from it. The create path in
WorkOrderBoardCreateService had the same any-contact rule and gets the same
fix; a supplied PocContactId that is not a live Site contact leaves no follow
target, so the typed POC is stored.
Replaces MatchesAnySiteContact with Matches(name, phone, contact); comment and
PR-body wording updated to state the followed-contact rule. Adds tests for the
second-site-contact edit, the linked-contact-differs edit, and the
second-site-contact create case.