# sub-implementer-backend-s171 — nhật-ký lane BE (Nhóm 1, BE-1 → BE-7)

> Run: `2026-08-04-S171-khkk-ui-mirror-pe` · vai `implementer-backend` · spec §②-D + §③ Nhóm 1
> Luật ghi: sau MỖI file sửa xong ⇒ append ngay 1 dòng `<path> — <đã làm gì> — <acceptance nào phủ>`.

## 0. Trạng thái

- [x] BE-1 DetailDto +5 field
- [x] BE-1b FinalizeStepName/FinalizeLevelName DẪN XUẤT + 3-ngả (in-memory thay 3 subquery — khai ở §2, và SỬA công thức con-trỏ cho đúng KHKK)
- [x] BE-2 ContractSigningPlanApprovalDto (đẻ mới) + projection
- [x] BE-3 tên người duyệt cho Approval (join `Users` — đường thứ ba, lý do §2)
- [x] BE-4 WorkflowStepDto +DepartmentName
- [x] BE-5 changelogs endpoint policy KeHoachKyKet.Read (+ rào hàng `EnsureCanViewAsync`)
- [x] BE-6 map Cấp cho Approvals (StepOrder/StepName/LevelOrder/LevelName)
- [x] BE-7 enum +ApprovalAttachment=3 + endpoint upload riêng (policy Update)
- [x] Mirror `types/khkk.ts` ×2 app — SHA256 trùng khít
- [x] `dotnet build` sạch (0 warning / 0 error) + `tsc -b` ×2 app exit 0
- [ ] 🔴 **B-3 acceptance RUNTIME** — treo, DB dev 0 phiếu (§4.1)

## 1. Đọc / khảo sát

- Spec đọc TRỌN. 3 QĐ owner nhận: (1) panel 1 = cây + 4 mitigation, (2) panel 3 chưa chọn = hộp dashed, (3) file đính kèm khi duyệt = LÀM THẬT.
- Nhóm 1 chỉ đụng BE ⇒ QĐ-1/QĐ-2 là việc của lane FE; lane này phục vụ bằng dữ-liệu (BE-1..BE-7). QĐ-3 = BE-7, làm thật.
- Đo được (khác spec, ghi để lane sau không chép sai):
  - `POST {id}/attachments` ĐÃ TỒN-TẠI, policy `KeHoachKyKet.Update`, handler `UploadContractSigningPlanAttachmentCommandHandler` **KHÔNG có** guard owner/phase ⇒ người duyệt (có Update) upload được. BE-7 chỉ cần enum + 1 đường riêng ép `Purpose`.
  - `ContractSigningPlansController` TRƯỚC S171: R = **18** action, `^\s*\[Authorize\(Policy` = **19** = 1 + R (đúng số spec nêu). Không có `[Authorize]` trần.
  - `ApprovalDecision`: Pending=0 · Approve=1 · Reject=2 · AutoApprove=3 (`Domain/Contracts/ApprovalDecision.cs`).
  - `ApprovalWorkflow.Version` (`:24`) có thật ⇒ `WorkflowVersion` lấy trực tiếp.
  - 🔴 **KHKK ≠ PE ở con-trỏ Bước**: `CurrentWorkflowStepIndex` là INDEX 0-based vào list ĐÃ SORT (`ContractSigningPlanWorkflowService.cs:26,422-435`), KHÔNG phải `Step.Order - 1` như PE. Chép nguyên công thức PE = sai khi Order không tuần-tự.

## 2. Nhật-ký file sửa

- `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` — (a) `ContractSigningPlanWorkflowStepDto` **+`DepartmentName`** (append cuối, default null) [BE-4/B-4]; (b) **đẻ mới** `ContractSigningPlanApprovalDto` 13 field (kèm StepOrder/StepName/LevelOrder/LevelName dẫn xuất từ `ApprovalWorkflowLevelId` loose-Guid) [BE-2/BE-6/B-2]; (c) `ContractSigningPlanDetailDto` **+5**: `WorkflowVersion` · `EndedByLevelFinalize` · `FinalizeStepName` · `FinalizeLevelName` · `Approvals` [BE-1/B-1].
- `…/ContractSigningPlanFeatures.cs` (handler `GetContractSigningPlanQueryHandler`) — (a) `levelMeta` +`LevelName`; (b) join `Departments` 1 lượt cho `DepartmentName` của Bước (không N+1) [BE-4]; (c) **BE-1b DẪN XUẤT** finalize step/level + 3-ngả `EndedByLevelFinalize` in-memory (cây đã nạp) thay 3 subquery của PE — cùng ngữ nghĩa, bớt 3 lượt truy, và SỬA công thức con-trỏ theo INDEX 0-based của KHKK; (d) query `ContractSigningPlanApprovals` + join `Users` lấy tên [BE-2/BE-3]; (e) call-site `new ContractSigningPlanDetailDto(:784)` truyền 5 field mới.
- `…/ContractSigningPlanFeatures.cs` (cuối file, Region 8 mới) — `ContractSigningPlanChangelogDto` + `ListContractSigningPlanChangelogsQuery` + handler **có `EnsureCanViewAsync`** (rào hàng, KHÔNG chép lỗ #82 của PE) [BE-5].
- `src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanAttachment.cs` — enum `+ApprovalAttachment = 3`, **KHÔNG** đánh số lại `Other = 99` [BE-7].
- `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` — (a) `GET {id}/changelogs` policy **`KeHoachKyKet.Read`** [BE-5/B-5]; (b) `POST {id}/approval-attachments` policy **`KeHoachKyKet.Update`**, ép `Purpose=ApprovalAttachment` ở BE (FE không truyền số enum) [BE-7/QĐ-3].

### 🔴 BE-3 — khai lý do chọn đường THỨ BA (spec cho 2 lựa chọn)

Spec: *"lấy tên từ changelog cùng transition **hoặc** denorm cột tên vào Approval. Chọn 1, khai lý do."* Tôi chọn **join `Users`**, vì:
1. **Ghép theo changelog KHÔNG CÓ KHOÁ CHÍNH XÁC**: `Approval.ApprovedAt` = `now` của domain (`ContractSigningPlanWorkflowService.cs:532`), còn `Changelog.CreatedAt` do `AuditingInterceptor` đóng lúc `SaveChanges` ⇒ **hai giá trị lệch nhau vài ms**. Ghép được thì phải dùng "bucket thời gian" — đúng cái hack 5s mà PE đang phải chịu ở FE (`PeDetailTabs.tsx:3152-3192`), tức nhập thêm nợ chứ không trả nợ.
2. **Denorm cột tên = cần MIGRATION** (Mig 72) — ngoài phạm vi đợt UI này, và đợt này không có mục migration nào khác để đi kèm.
3. Chi phí thật của join: **1 query batch** trên tập `DISTINCT ApprovedByUserId` của đúng 1 phiếu (thực tế ≤ ~6 người). Handler này vốn đã multi-query (pe · project · dept · drafter · suppliers · workflow), không phải projection 1 phát.

## 3. Tự kiểm B-1 · B-2 · B-4 · B-5 · B-6 (số ĐO được, không phải tuyên bố)

| Thước | Yêu cầu | Đo được | Kết |
|---|---|---|---|
| **B-1** | 5 tên mới trong `record ContractSigningPlanDetailDto -A 80` | `WorkflowVersion :216` · `EndedByLevelFinalize :217` · `FinalizeStepName :218` · `FinalizeLevelName :219` · `List<ContractSigningPlanApprovalDto> Approvals :220` | ✅ 5/5 |
| **B-2** | `grep -c ContractSigningPlanApprovalDto src/Backend` ≥ 2 (hôm nay 0) | **3** (1 định-nghĩa + 1 field DetailDto + 1 call-site projection) | ✅ |
| **B-3** | 🔴 RUNTIME: phiếu `DaDuyet` ⇒ `approvals[]` có ≥1 `Decision=Reject` **và** `approvedByFullName ≠ null` ∀ phần tử | **CHƯA CHẠY ĐƯỢC** — xem §4 | ⛔ treo |
| **B-4** | `DepartmentName` trong `…WorkflowStepDto` ≥ 1 | **1** | ✅ |
| **B-5** | `grep -n changelogs <controller>` = **1 hit** + `[Authorize(Policy = "KeHoachKyKet.Read")]` liền kề | **1 hit** (`:91`), policy ở `:92` ngay dưới `[HttpGet]` — đúng convention file (mọi action đều `[Http*]` rồi `[Authorize(Policy…)]`) | ✅ |
| **B-6** | `R` = `^\s*\[Http(Get\|Post\|Put\|Delete\|Patch)` · `^\s*\[Authorize\(Policy` = 1+R · `^\s*\[Authorize\]\s*$` = 0 | R = **20** (18 cũ + changelogs + approval-attachments) · Policy = **21** = 1+20 ✅ · trần = **0** ✅ | ✅ |
| build | `dotnet build SolutionErp.slnx` | **Build succeeded · 0 Warning · 0 Error** (lượt 1 FAIL CS1739 `dossierItemId` → sửa thành `DossierItemId`, lượt 2+3 sạch) | ✅ |
| tsc | `fe-admin` + `fe-user` `tsc -b` | **exit 0 cả hai** (types-only, mọi consumer đọc từ response — không nơi nào dựng object literal) | ✅ |
| **E-1** | mirror `types/khkk.ts` SHA256 | `aa4103de6362e67e68d026e125fe65a33efb92af4bd6ab811d6e0d9db3610812` **×2 app trùng khít** | ✅ |
| **E-2** | 0 hit 4 path cấm + 2 path nhạy (`hooks/usePipelineStages.ts`, `components/pipeline/`) | diff lane này = **đúng 5 file** (3 BE + 2 `types/khkk.ts`), 0 path cấm, **KHÔNG chạm** hook/pipeline | ✅ |

### 🧪 Fault-injection — chứng thước B-6 CÓ RĂNG (không chỉ happy-path PASS)

Cắm 1 action `[Authorize]` **trần** vào **cây tạm** (`…/scratchpad/injected.cs`, KHÔNG đụng repo):
- gauge 3 `^\s*\[Authorize\]\s*$`: **0 → 1** ⇒ FLAG đúng.
- gauge 2 đồng thời vỡ: R **21** nhưng `Authorize(Policy)` vẫn **21** ≠ 1+R=22 ⇒ FLAG lần hai.
⇒ Cả 2 vế của thước đều bắt được ca xấu; số xanh ở bảng trên KHÔNG phải do thước mù.

### 🔸 1 chỗ tôi tự sửa để thước B-5 khỏi bị bơm tử-số

Comment cảnh-báo #82 của tôi ban đầu chứa literal `changelogs` ⇒ `grep -c` ra **2** trên file 100% đúng — đúng lớp **citation-trap** mà chính spec vừa phải sửa cho B-6. Đã đổi chữ trong comment (`endpoint lịch-sử`), giữ nguyên nội dung cảnh-báo. Đo lại = **1**.

## 4. Chưa làm / lý do (khai thẳng — lỗ có khai > báo xanh mà sai)

1. **B-3 (RUNTIME) — CHƯA CHẠY.** Lý do đo được, không phải phỏng đoán: DB dev `(localdb)\MSSQLLocalDB / SolutionErp_Dev` có **`ContractSigningPlans` = 0 hàng**, **`DaDuyet` = 0**, **`ContractSigningPlanApprovals` = 0 hàng** ⇒ không tồn tại phiếu nào để gọi thật. Cần chạy ở UAT (lane E-5/E-6, `fe-user:8080`).
   *Cận-dưới đã có từ code, KHÔNG thay được B-3:* `LogSigningPlanTransitionAsync` ghi `Approvals` **thẳng trong thân hàm, không `if` nào bọc** (`…WorkflowService.cs:534`), 6 call-site phủ trọn (`:204` `:283` `:296` `:311` `:325` `:393` — `:393` phủ CẢ Trả lại lẫn Từ chối cùng `ApprovalDecision.Reject`); bộ lọc của tôi chỉ bỏ `Pending` ⇒ hàng `Reject` **không thể** bị nuốt. `approvedByFullName` null CHỈ khi `ApprovedByUserId` null (transition do system/SLA) hoặc user đã bị xoá — không có đường nào khác.
2. **Không đụng `api/khkk.ts` của FE.** Lane FE (nhóm 2-3) sở hữu file đó và đang sửa song song; tôi ghi hợp-đồng endpoint ở §5 thay vì chen vào (tránh đụng độ). **Không phải "đã wire"** — FE còn phải gọi.
3. **Không viết test.** Theo `docs/rules.md §7`: đây là feature mới ⇒ test-after (UAT ổn rồi viết), không phải bug-fix/critical-algorithm. `dotnet test` chưa chạy trong lane này (E-4 là thước của lane reviewer, và build agent nền khác đang chạy — bài #68).
4. **BE-3 chọn đường THỨ BA** so với 2 lựa chọn spec đưa — lý do đầy đủ ở §2, tóm: ghép-theo-changelog **không có khoá chính xác** (2 timestamp khác nguồn), denorm cột **cần migration** ngoài phạm vi.

## 5. Hợp-đồng cho lane FE (literal — khỏi đoán)

- `GET /api/contract-signing-plans/{id}` → `KhkkDetailDto` **+5**: `workflowVersion: number|null` · `endedByLevelFinalize: boolean` · `finalizeStepName: string|null` · `finalizeLevelName: string|null` · `approvals: KhkkApprovalDto[]`; và mỗi `workflowSteps[]` **+`departmentName: string|null`**.
- `GET /api/contract-signing-plans/{id}/changelogs?take=200` → `KhkkChangelogDto[]` (mới nhất trước). Policy `KeHoachKyKet.Read`.
- `POST /api/contract-signing-plans/{id}/approval-attachments` — **multipart**, field `file` (+ `note` tuỳ chọn). BE tự ép `purpose = ApprovalAttachment (3)`; **FE KHÔNG gửi purpose**. Policy `KeHoachKyKet.Update` (cùng policy với `/transitions` ⇒ người duyệt đã có quyền).
- Khối "📎 File đính kèm khi duyệt" lọc `attachments.filter(a => a.purpose === KhkkAttachmentPurpose.ApprovalAttachment)`, rỗng ⇒ **tự ẩn** (D-9 vế 2).
- `approvals[]` **BE đã lọc `decision = Pending`** ⇒ FE **KHÔNG** dedupe lại; `(N)` = `approvals.length` là cùng nghĩa với `(N)` của PE (D-11).

END implementer-backend-s171 — 7/7 mục BE xong (B-3 acceptance RUNTIME còn treo, đã khai) · build PASS
