CreateAsync checks FindByEmailAsync and then inserts, so two concurrent
creates with the same email can both pass the check and hit the unique
user-name index. That DbUpdateException was unhandled and surfaced as a
500. Catch it at the CreateAsync call and return the existing
"Email is already in use." message, matching the check-then-insert
converge pattern already used by SetOverrideCoreAsync.
DeleteUserWithCascadeAsync never removed UserPermissionOverrides rows, and
the FK on UserId is Restrict. Once an override was saved for a member, the
admin hard-delete (DeleteUserAsync -> DeleteUserWithCascadeAsync) failed the
foreign key inside the transaction and the controller surfaced it as a 400,
so the user was never deleted. Add an explicit ExecuteDelete on
UserPermissionOverrides before the user is removed, matching how UserRoles is
already cleared in the same method (no schema change).
SetOverrideAsync was check-then-insert on the composite key with no
DbUpdateException handling, so two concurrent PUTs for the same
(UserId, PermissionKey) let the loser violate PK_UserPermissionOverrides and
return 500. Catch the conflict, detach the pending insert, and converge by
updating the persisted row to the caller's requested state.
ScheduledDate is persisted as a midnight calendar value (board create writes
request.ScheduledDate.Value.Date; board patch writes the parsed .Date into a
datetime2 column with no offset). GetTrendAsync treated it as a UTC instant and
converted to America/New_York, so midnight became 19:00/20:00 the previous day
and every board-scheduled work order fell into the prior bucket: a job scheduled
today read as yesterday and overdue, and the Today bucket showed zero.
Bucket by DateOnly.FromDateTime(scheduled.Date) with no conversion, matching
DashboardMetrics and WorkOrderDerivedFields, so Trend and Stats agree on the
same rows. Rewrite the daily and yearly trend tests to assert calendar-date
classification (the removed conversion had encoded the shift into their
expectations) and add a regression test that a midnight-today work order stays
in the Today bucket and is not overdue.
SH-320: the WO number normalizer stripped every non-digit, so a manually
entered SH placeholder (e.g. SH00001) was saved as 00000000001 on both
create and patch. Keep the SH prefix, and reject replacing a saved real
APM number with an SH placeholder.
The media allowlist refused any upload whose multipart part had an empty or
application/octet-stream Content-Type before looking at the extension or the
bytes. Browsers take that header from File.type, which mobile browsers leave
empty when the OS cannot classify a picked file, while the client-side gate
already accepts such files on extension alone. A real JPG/MP4/MOV could pass
the dialog and still be refused by the API.
Only an undetermined type now falls back to the extension. The resolved type
still goes through the SH-171 document/category rule, the extension pairing,
and the magic-byte signature check, so an octet-stream .pdf stays refused for
Completion, Before and After, and a declared type is never overridden.
Incomplete and Scheduled are derived by the server. A direct lifecycleStatus
PATCH may only restate the status derivation already produced; any other
request to move a work order into an automatic state returns the stable
AutomaticLifecycleStatus validation error and leaves status and audit untouched.
Seed an organization-wide Area catalogue (East, Central, West, California)
with stable ids, add a nullable AreaId to VendorCompany, allow only Admins
to change it through the roster endpoints, expose areas facet metadata and
an areas[n] company-directory filter with the __unassigned__ sentinel.
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 dev Terraform rollback verify was the one gate the previous commit
missed, and it is the copy that actually ran on 34293894914. It still
decided on the first Ready poll: previous version correctly restored,
health not yet converged, reported as a failed rollback.
Split the version and health conditions the same way the other three
gates now do — a Ready poll on the wrong version fails immediately and
names the version that came up, while the correct version with unsettled
health keeps polling inside the unchanged 80 x 15s budget. Timeout now
reports the last observed status, version and health.