Storing the submitted full name in FirstName while leaving a seeded
LastName intact made an unchanged save project a duplicated name
(e.g. "Legacy Manager Manager"). Leave FirstName/LastName untouched when
the submitted name already matches the stored first+last projection, and
otherwise split the submitted name across FirstName/LastName so renames
round-trip cleanly.
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.