shoc-backend/docs/work-orders/staff-review-findings.md
Arthur Bassi f61aff06ff docs(work-orders): add Fase 2 staff review findings
Record adversarial review results and fixes for service notes, search,
paging, and migration hardening.
2026-07-17 10:37:25 -03:00

276 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Staff Review Findings — Work Orders (Fase 2)
Adversarial reviews per delivery. Do not treat preliminary inventário notes as final.
---
## Entrega A — Structured Service Notes / POC / Tech-Vendor
### How it works
1. **Create** `POST /api/workorders/board`: FluentValidation → resolve `primaryService`/`trade` → serialize `extraServices` JSON → persist POC/service/tech/vendor on `WorkOrder`; if vendor/appt present, create `Dispatch` and store tech/vendor notes there instead of WO.
2. **Patch** `PATCH .../board` with `pm` | `serviceNotes` | `extraServices`.
3. **Read**: board projection prefers WO POC fields over `WorkOrderContacts`; tech phone = Dispatch.TechPhone → WO.TechPhone → Vendor.Phone. Detail exposes `serviceNotes`, `extraServices`, `vendorNotes`.
### Domain invariants
- Extra services require a primary service on create.
- Tech/vendor notes live on dispatch when a vendor dispatch exists; otherwise on WO.
- Board POC free-text overrides linked contact projection.
### Half-done
- No board PATCH for `pocName` / `pocPhone` / `pocNotes` / `techPhone` / `vendorNotes` (create-only for those).
- `ParseExtraServices` duplicated in board + detail services.
- Create validation file has excessive blank lines (noise).
- Test `PatchField_ExtraServicesWithoutPrimary_Throws` disagrees with promotion logic in `ApplyExtraServices`.
### Findings
#### A1 — ExtraServices without primary: test vs promotion
- **Severity:** High
- **File:** `SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs` (~294–298); test `WorkOrderBoardUpdateServiceTests.cs` (~331)
- **Explanation:** Create requires primary when extras exist; PATCH promotes first extra to `Trade` without auditing Trade. Test expects `InvalidValue`.
- **Scenario:** Legacy WO with empty Trade; FE patches `extraServices` only.
- **Repro:** PATCH `extraServices=["Filter change"]` on WO with null Trade.
- **Fix:** Reject when Trade empty (align create + test); do not promote silently.
- **When:** **Now** (blocks green CI / domain consistency).
#### A2 — ExtraServices JSON can exceed nvarchar(2000)
- **Severity:** High
- **File:** Create `SerializeExtraServices`; Update `ApplyExtraServices`
- **Explanation:** Per-item max 128; no check on serialized JSON length vs column 2000.
- **Scenario:** Many extras → `DbUpdateException` 500.
- **Repro:** PATCH/create with ~20×128-char labels.
- **Fix:** After serialize, throw `InvalidValue` if length > 2000.
- **When:** **Now**.
#### A3 — Create does not cap ExtraServices JSON length / per-item in serializer
- **Severity:** Medium
- **File:** `WorkOrderBoardCreateService.SerializeExtraServices`
- **Explanation:** FluentValidation caps each item at 128 but create serializer does not re-validate length or total JSON; update path has item length check, create does not.
- **Fix:** Share normalize helper; enforce item + JSON caps on create.
- **When:** **Now** (with A2).
#### A4 — POC / tech / vendor notes not patchable on board
- **Severity:** Medium
- **File:** `WorkOrderBoardFieldNames.cs` / UpdateService
- **Explanation:** Create persists structured POC/tech/vendor; no PATCH fields → FE cannot edit after create without another API.
- **When:** **Later** if FE has another edit path; otherwise before FE ship.
#### A5 — Dual POC sources can diverge
- **Severity:** Low
- **File:** CreateService + Projection
- **Explanation:** `PocContactId` + free-text both allowed; board prefers free-text; contact row may hold different notes.
- **When:** Later (document or clear contact notes when free-text set).
#### A6 — Duplicate ParseExtraServices
- **Severity:** Low
- **File:** `WorkOrderBoardService.cs`, `WorkOrderDetailService.cs`
- **When:** Later (extract helper).
### Verdict A
**Merge blocked** until A1–A3 fixed. A4 is a product gap for FE edit flows.
---
## Entrega B — Flag Color
### How it works
PATCH `flagColor` → normalize `#RRGGBB` → whitelist palette → persist on WO → project on board/detail. Distinct from dispatcher avatar `color`.
### Half-done
- None material. Migration + model + PATCH + tests present.
### Findings
#### B1 — No finding Critical/High
Palette enforcement and clear semantics are consistent with tests.
#### B2 — Snapshot/model coupled with Entrega A
- **Severity:** Low (process)
- **Explanation:** Same `WorkerOrder` / snapshot file as service notes; branch split needs careful ordering.
- **When:** During organize.
### Verdict B
**OK to merge** after A schema lands (or same branch ordered commits). No code blockers unique to B.
---
## Entrega C — Search / overdue / paging
### How it works
Advanced search: normalize page (≥0), pageSize≤100, resolve Mon–Fri presets, whitelist sortBy, apply type/overdue/unscheduled filters, project board rows. Board GET maps `ArgumentException` → 400.
### Half-done / risks
- Shared `PagedResult` flipped to 0-based helpers while Location/Contact/etc. still use 1-based `page` defaults.
### Findings
#### C1 — PagedResult 0-based breaks 1-based APIs
- **Severity:** Critical
- **File:** `SeaHaven.Services/DTOs/ContactDTOs.cs`
- **Explanation:** `HasPrevious`/`HasNext` assume 0-based; Location etc. pass `page=1` on first page → `HasPrevious=true` incorrectly; last-page `HasNext` wrong for 0-based search too if mixed.
- **Scenario:** Any paged Location/Contact/Vendor list UI using helpers.
- **Fix:** Restore 1-based helpers by default; opt-in 0-based for WO search only (`PageBase` or dedicated DTO).
- **When:** **Now**.
#### C2 — Unscheduled always included in date range
- **Severity:** High
- **File:** `WorkOrderBoardQueryFilters.ApplyDateRangeFilter`
- **Explanation:** Every advanced search with a date range also returns all unscheduled non-terminal WOs → unbounded result growth.
- **Scenario:** Narrow custom range + large backlog of undated WOs.
- **Fix:** Explicit `includeUnscheduled` or cap; confirm FE intent.
- **When:** Confirm with FE; fix before prod if unintentional.
#### C3 — WorkOrderType.Other = overdue sentinel
- **Severity:** High (domain)
- **File:** `WorkOrderBoardQueryFilters.ApplyTypeAndOverdueFilter`
- **Explanation:** Cannot filter real `Other` types via `types=99`.
- **When:** Document now; long-term separate `isPastDue` query param.
#### C4 — Label search `label.Contains(s)` matches short tokens broadly
- **Severity:** Medium
- **File:** `WorkOrderBoardSearchFilter`
- **When:** Later / monitor perf.
#### C5 — Overdue uses UTC date vs possibly local scheduled dates
- **Severity:** Medium
- **File:** QueryFilters / search filter
- **When:** Later if US sites report off-by-one day.
#### C6 — Search helper methods not EF-translatable (FIXED)
- **Severity:** Critical
- **File:** `WorkOrderBoardSearchFilter.Apply`
- **Explanation:** Refactor extracted `Matches*` private methods; EF Core InMemory/SQL cannot translate them → all text searches throw at runtime.
- **Repro:** Any `search` with length ≥ 2 on board/search.
- **Fix:** Inline predicates in the `Where` expression (applied).
- **When:** **Now** — fixed during Fase 2.
### Verdict C
**Merge blocked** on C1 (fixed via `PageBase`) and C6 (fixed via inline predicates). C2 needs product confirmation. Prefer separate PR.
---
## Entrega D — Comments edit + display names
### How it works
`GetDisplayNamesByIdsAsync` → comment/audit projections. PATCH comment: author or Admin; General+WorkOrder only; read-only WO blocked.
### Half-done
- Comment edit not written to audit log.
### Findings
#### D1 — Comment updates leave no audit trail
- **Severity:** Medium
- **File:** `WorkOrderCommentService.UpdateCommentAsync`
- **When:** Later if compliance requires it; note in PR.
#### D2 — No max length on comment text on update
- **Severity:** Low (pre-existing on create)
- **When:** Later.
### Verdict D
**OK to merge** with D1 noted. No Critical/High code defects found.
---
## Entrega E — Sites + Vendor ContactName
### How it works
`GET /api/locations/sites?search=` → Id/Code/City/State (`Code` ← `Locations.Name`). Vendor Dropdown adds `contactName`.
### Findings
#### E1 — GetSites unbounded
- **Severity:** Medium
- **File:** `LocationDataService.GetSiteOptionsAsync`
- **Explanation:** No Take/limit; large Locations table → heavy payload.
- **When:** Later or add Take(500) if volume unknown.
#### E2 — No soft-delete filter
- **Severity:** Low
- **Explanation:** Locations inherit audit entity; if soft-delete used elsewhere, sites may include deleted.
- **When:** Later if soft-delete applies to Locations.
### Verdict E
**OK to merge** independently.
---
## Entrega F1 — Phase4 migration hardening
### How it works
Before creating indexes, alter `SiteCode`/`InternalWONumber` to nvarchar(450).
### Findings
#### F1-1 — Editing already-shipped migration
- **Severity:** High (ops)
- **File:** `20260624200000_Phase4_SearchIndexes.cs`
- **Explanation:** Environments that already applied old `Up` will not re-run. Failed partial applies need manual repair or a **new** migration.
- **When:** Verify `__EFMigrationsHistory` per environment **before** merge. Prefer new corrective migration if any env already applied.
### Verdict F1
**Conditional merge** after env check. Apply on `feat/work-orders-phase-4` only.
---
## Entrega F2 — Phase6 migration hardening
### How it works
Idempotent SQL for Category column, CompletionDocTemplates, index; EF attributes for discovery.
### Findings
#### F2-1 — Same applied-migration caveat as F1
- **Severity:** High (ops)
- **When:** Verify env history; idempotent SQL helps partial DBs if migration re-run manually, not if EF skips applied migrations.
### Verdict F2
**Conditional merge** on slideover branch after env check.
---
## Cross-cutting mandatory fixes before organize
1. A1 — ExtraServices without primary throws
2. A2/A3 — JSON length ≤ 2000
3. C1 — PagedResult 1-based default + WO search 0-based opt-in