# sub-implementer-frontend-6 — WAVE 5 · [F-7] nối dây FE cho `PUT /approval-workflows-v2/{id}` (vá F3 HIGH)

> run `2026-07-27-S155-pe-delete-approver` · WAVE 5 (vá 3 FLAG HIGH trước deploy) · vai `implementer-frontend`
> Ghi TRONG LÚC LÀM, flush từng phần (#53 đã ăn 5/5 sub run này). File luôn hợp lệ giữa chừng.

## 0. F3 nói gì (đọc nguyên văn `reviewer-diff-dot1.md:84-113`, không tóm tắt lại theo trí nhớ)

- **Đo được, không phải cảm giác:** `grep -rn "api.put" fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` → **0 hit**.
  3 đường ghi duy nhất của Designer: `api.delete :286` · `api.patch …/user-selectable :297` · `api.post('/approval-workflows-v2') :589`.
- **Hệ quả:** ~600 dòng BE mới (`UpdateAwDefinitionCommand` + Validator + Handler + 3 helper) là **mã chết** với người dùng UAT.
  Owner tick cờ F6 → Lưu → POST → sinh **Id MỚI** → phiếu đang treo pin **Id CŨ** (`PurchaseEvaluationWorkflowService.cs:674` `w.Id == awId`) ⇒ cờ **không bao giờ tới** phiếu cần cứu.
- **Reviewer cho 3 đường (a)/(b)/(c); owner đã chốt (a) = nối dây FE trong đợt 1.**
- **Tiêu chí nghiệm thu reviewer đặt (`:113`):** tick cờ F6 cho 1 NV trên version đang có phiếu treo → lưu bằng đường "Sửa tại chỗ" → `GET /api/approval-workflows-v2` trả `allowApproverDelete=true` trên **đúng `id` cũ**, `Version` **KHÔNG tăng**.

## 1. Hợp đồng BE — đọc THẲNG từ mã, không tin MEMORY-PACK

| Neo | File:line | Sự thật |
|---|---|---|
| Endpoint | `ApprovalWorkflowsV2Controller.cs:45-51` | `[HttpPut("{id:guid}")] [Authorize(Policy="Workflows.Create")]`, `mediator.Send(cmd with { Id = id })` → **204 NoContent** |
| Command | `ApprovalWorkflowV2AdminFeatures.cs:449-464` | `{ Id, Name, Description, Steps, Reason?=null, Code?=null, ApplicableType?=null, CeoApprovalThreshold?=null }` |
| Step input | `:442-447` | `Id` **BẮT BUỘC** (Guid, không nullable) · `Order` · `Name` · `DepartmentId` · `Levels` |
| Level input | `:424-440` | `Id` **Guid?** (`null` = dòng MỚI) · `Order` · `Name` · `ApproverUserId` · **7 cờ an-toàn** (4×Return, EditDetails, EditBudget, **AllowApproverDelete**) · **2 cờ echo `bool?`** (SkipToFinal, Finalize) |

**3 bẫy payload (task nêu) — đối chiếu mã, cả 3 đều THẬT:**

1. **`CeoApprovalThreshold` ECHO NGHIÊM** — `:529` `if (request.CeoApprovalThreshold != def.CeoApprovalThreshold) throw ConflictException`.
   KHÔNG phải "null = bỏ qua" (khác hẳn `Code`/`ApplicableType` ở `:521`/`:525` dùng `is string`/`is int` nên null mới được bỏ qua).
   ⇒ gửi thiếu ⇒ `null != 5_000_000` ⇒ **409**. Comment BE `:459-463` tự khai lý do: null LÀ giá trị có nghĩa (= bỏ ngưỡng).
2. **Chỉ trường hạng AN TOÀN.** Cấm (409): ±Bước (`:539`) · đổi `Step.Order` (`:554`) · đổi `Level.Order` từng dòng (`:575`) · ±Cấp / bỏ trống Cấp (`:618-630`) · `Code` · `ApplicableType` · `CeoApprovalThreshold` · F5 `AllowApproverFinalize` (`:580`) · F2 `AllowApproverSkipToFinal` (`:583`) · bỏ/đổi người đang có **chữ ký sống** (`:658-673`).
   Cho sửa: thêm/bớt người trong Cấp CÓ SẴN · đổi `ApproverUserId` · 7 cờ quyền-tại-chỗ (gồm **F6**) · tên (workflow/step/level) · `Step.DepartmentId` (`:705-706`, BE **không** kiểm NV có thuộc Phòng hay không).
3. **Payload liệt kê tay từng field** (`:605-621` khối POST) — thêm field mà quên dây = **silent drop**, TS xanh, BE default `false`, UI vẫn tick. Đúng bẫy đã xảy ra thật ở `sub-implementer-frontend-2.md §2.4`.

<!-- phần 2+ ghi tiếp sau khi áp thay đổi -->
