shoc-backend/docs/work-orders/phase-2/README.md

155 lines
5.4 KiB
Markdown
Raw Normal View History

# Fase 2 — Inline Edit + Concorrência
**Programa:** Work Orders Board
**Objetivo:** Edição spreadsheet-style célula a célula no board SHOC, com dual RowVersion, audit granular e regras de domínio no backend.
**Depende de:** [Fase 0](../phase-0/README.md), [Fase 1](../phase-1/README.md)
---
## Endpoints
### `PATCH /api/workorders/{id}/board`
Atualiza um único campo do board com validação otimista de concorrência.
**Autenticação:** Bearer JWT (`[Authorize]`)
**Request body:**
```json
{
"field": "scheduledDate",
"value": "2026-06-25",
"workOrderVersion": "<base64 RowVersion>",
"dispatchVersion": "<base64 RowVersion | null para campos WO-only>",
"primaryDispatchId": 123
}
```
| Campo | Obrigatório | Descrição |
|-------|-------------|-----------|
| `field` | Sim | Nome canônico do campo (case-insensitive) |
| `value` | Sim* | Valor serializado como string (*pode ser vazio para limpar datas) |
| `workOrderVersion` | Sim | `RowVersion` atual da WO (Base64) |
| `dispatchVersion` | Condicional | Obrigatório para campos Dispatch quando dispatch já existe |
| `primaryDispatchId` | Não | Valida que o dispatch pertence à WO |
**Responses:**
| HTTP | Body | Quando |
|------|------|--------|
| 200 | `WorkOrderBoardRowDto` | Sucesso — row completo com versões atualizadas |
| 409 | `WorkOrderBoardConflictDto` + `currentState` | Conflito de versão |
| 422 | `WorkOrderBoardValidationErrorDto` | Regra de domínio violada |
| 400 | `Response` | Erro genérico / argumento inválido |
**Exemplo:**
```bash
curl -X PATCH -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"field":"siteCode","value":"BK5","workOrderVersion":"AQAAAAAAAAA="}' \
"https://localhost:5001/api/workorders/1/board"
```
---
## Campos editáveis
| field (API) | Agregado | Coluna board | Audit FieldName |
|-------------|----------|--------------|-----------------|
| `woNumber` | WorkOrder | WO# | InternalWONumber |
| `workOrderType` | WorkOrder | Type | WorkOrderType |
| `siteCode` | WorkOrder | Site | SiteCode |
| `lifecycleStatus` | WorkOrder | Status | LifecycleStatus |
| `assignTo` | WorkOrder | Assignee | AssignTo |
| `dueDate` | WorkOrder | Due | DueDate |
| `scheduledDate` | WorkOrder | Scheduled | ScheduledDate |
| `targetWeek` | WorkOrder | Scheduled (week-only) | TargetWeek |
| `scheduleWeekOnly` | WorkOrder | Scheduled | ScheduleWeekOnly |
| `vendorId` | Dispatch | Vendor | VendorId |
| `apptDate` | Dispatch | Appt | ApptDate |
| `apptTime` | Dispatch/WO | Appt | ApptTime |
| `docStatus` | WorkOrder | Doc | DocStatus |
**Read-only:** `isPastDue`, `carriedOver`, `rescheduleCount` (badge derivado; count incrementado por regra).
---
## Dual RowVersion
| Agregado | Quando exigir versão |
|----------|---------------------|
| WorkOrder | Sempre (`workOrderVersion`) |
| Dispatch | Campos vendor/appt quando dispatch primário já existe (`dispatchVersion`) |
O GET board retorna `rowVersion`, `dispatchRowVersion` e `primaryDispatchId` em cada row.
**409 currentState:** inclui row recarregado do DB para refresh imediato no FE.
---
## Regras de domínio
| Regra | Comportamento |
|-------|---------------|
| Auto-schedule | `Incomplete` + `scheduledDate` + `assignTo` → `Scheduled` |
| Reschedule | Mudança de `scheduledDate` com data anterior → `rescheduleCount++` |
| Set-once tracking | Primeira `scheduledDate` → `OriginalDate` / `OriginalWeek` |
| Past Due block | Não permite mudar `lifecycleStatus` quando `isPastDue` |
| Cancel read-only | Status terminal (`Canceled`, `Closed`, `Complete`) bloqueia PATCH |
| WO# 11 dígitos | Normalização numérica + unicidade |
| Vendor ADR | Mutação em `Dispatch`; cria dispatch primário se ausente |
---
## Audit
- 1 evento por campo alterado (`FieldChanged`, `StatusChanged`, ou `AssignmentChanged`)
- Side effects (auto-schedule, rescheduleCount) geram eventos adicionais
- Field lock (`WorkOrderFieldLocks`) criado automaticamente em edições manuais
- Eventos Dispatch incluem `dispatchId`
---
## Código entregue
| Camada | Arquivo |
|--------|---------|
| DTOs | `SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs` |
| Helpers | `WorkOrderBoardMutationRules`, `WorkOrderNumberNormalizer`, `WorkOrderBoardFieldNames`, `WorkOrderBoardApptTimeParser` |
| Update service | `SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs` |
| Audit | `IWorkOrderAuditService.StageFieldChanged`, `LogFieldChangedAsync` |
| API | `WorkOrderController` — `PATCH {id}/board` |
| Testes | `WorkOrderBoardMutationRulesTests`, `WorkOrderNumberNormalizerTests`, `WorkOrderBoardUpdateServiceTests`, `WorkOrderBoardConcurrencyTests` |
---
## Gates de aceite
### Desenvolvimento
- [x] `PATCH /api/workorders/{id}/board` para todos os campos da matriz
- [x] Dual RowVersion validado; 409 com `currentState`
- [x] Auto-schedule Incomplete→Scheduled
- [x] RescheduleCount++ em reagendamento
- [x] Bloqueio status quando PastDue; reagendar limpa flag on-read
- [x] WO cancelado/closed rejeita edição (422)
- [x] WO# normalizado 11 dígitos + unicidade
- [x] Vendor/appt muta Dispatch; cria primário se ausente
- [x] 1 audit event por campo; field lock criado
- [x] 51 testes unitários total (25 Fase 2 + 26 Fases 0–1)
### Staging (pendente)
- [ ] Smoke Sync durante edição SHOC
- [ ] UAT dispatcher: editar células reais no board
- [ ] Latência PATCH aceitável (<200ms p95 Tier S)
---
## Próximo passo
**Fase 3** — Criação wizard/inline, soft cancel dedicado, ManualEditFlag na criação.