Files
solution-erp/.claude/workflows/runs/2026-07-29-S161-khkk-w3-duyet/sub-implementer-backend-0.md
2026-07-29 21:49:38 +07:00

138 lines
14 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.

# sub-implementer-backend-0 — S161 W3 ĐỢT-1 SKELETON (test-before, 0 logic)
> Ghi TỪNG MỤC trong lúc làm (bài #53: chỉ thứ trên đĩa mới sống).
> Scope MINE: `Application/ContractSigningPlans/Services/` + `Infrastructure/Services/` + `Infrastructure/DependencyInjection.cs` + `Api/Controllers/ContractSigningPlansController.cs`.
> KHÔNG chạm: `tests/**` (test-specialist) · `fe-*/**` (implementer-frontend) · slnx · commit/push.
---
## §0 — AUDIT TRƯỚC KHI GÕ (đo, không đoán)
| Mục đo | Lệnh / anchor | Kết quả |
|---|---|---|
| Service KHKK workflow đã có chưa? | `grep -rn "ContractSigningPlanWorkflowService" --include=*.cs src/` | **0 hit** ⇒ scaffold sạch, không đụng file người khác |
| Run folder | `ls .claude/workflows/runs/2026-07-29-S161-khkk-w3-duyet/` | có `run.md` (đã scaffold) — KHÔNG tạo folder |
| Khuôn interface+DI để mirror | `IContractSigningPlanCodeGenerator.cs` (Application/ContractSigningPlans/Services/) + `DependencyInjection.cs:42` | xác nhận ĐÚNG chỗ |
| Controller đích | `Api/Controllers/ContractSigningPlansController.cs:21` | comment sẵn: "🔴 KHÔNG có endpoint transition (submit/approve/reject/return) — đó là W3" ⇒ đợt này gỡ đúng dòng đó |
| Enum phase | `Domain/ContractSigningPlans/ContractSigningPlanPhase.cs:8-15` | DangSoanThao=1 · ChoDuyet=2 · DaDuyet=3 · TraLai=98 · TuChoi=99 |
| Con-trỏ đôi (field THẬT trên entity) | `Domain/ContractSigningPlans/ContractSigningPlan.cs:35-37` | `ApprovalWorkflowId` · `CurrentWorkflowStepIndex` (0-based) · `CurrentApprovalLevelOrder` (1-based) |
| Exception có sẵn | `Application/Common/Exceptions/ApplicationException.cs:37-40` | `ConflictException(string)` — dùng cho action sai |
| Anchor nguồn copy đợt-3 | `Infrastructure/Services/ContractWorkflowService.cs:217-394` | `:217` = `private async Task ApproveV2Async(` ✔ · `:246` GroupBy Order ✔ · `:255-266` OR-of-N (vế `allowedUserIds.Contains``:259-260`) ✔ |
**Ghi chú anchor:** spec §③-A nói OR-of-N tại `:259-260` — đọc thực tế thì `:259``var allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet();``:260``if (!allowedUserIds.Contains(actorUserId.Value))`. Anchor KHỚP (không drift).
---
## §1 — Interface + record (Application) — MỤC 1 ✔
**File MỚI:** `src/Backend/SolutionErp.Application/ContractSigningPlans/Services/IContractSigningPlanWorkflowService.cs` (43 dòng)
- `IContractSigningPlanWorkflowService.TransitionAsync(Guid planId, string action, string? comment, CancellationToken ct = default)``Task<ContractSigningPlanTransitionResult>` — ĐÚNG hợp-đồng lead.
- `record ContractSigningPlanTransitionResult(int Phase, int? CurrentWorkflowStepIndex, int? CurrentApprovalLevelOrder)` — đúng 3 field response, không thừa.
- Đặt cạnh `IContractSigningPlanCodeGenerator.cs` cùng thư mục `Services/` (khuôn đã có).
**QUYẾT-1 (khai để lead phân xử): `Phase` để kiểu `int`, KHÔNG `ContractSigningPlanPhase`.**
Lý do: hợp-đồng chốt `"phase": int`. Đo được: `Api/Program.cs:33-34` `AddJsonOptions` CHỈ thêm `UtcDateTimeJsonConverter`, KHÔNG đăng ký `JsonStringEnumConverter` và KHÔNG đổi `PropertyNamingPolicy` (grep `JsonStringEnumConverter` toàn `SolutionErp.Api` = 0 hit) ⇒ hôm nay enum vẫn ra số. Nhưng nếu ngày nào đó ai thêm converter enum thì MỌI enum lật sang chuỗi và hợp-đồng vỡ ÂM THẦM. Ép `int` tại nguồn = rào chống hồi-quy đó.
**Camel-case bind:** không có `PropertyNamingPolicy` override ⇒ mặc định ASP.NET Core camelCase ⇒ `Phase``"phase"`, `CurrentWorkflowStepIndex``"currentWorkflowStepIndex"`, `CurrentApprovalLevelOrder``"currentApprovalLevelOrder"`; body `"action"/"comment"` bind vào `Action`/`Comment`. Khớp literal 3/3 + 2/2.
---
## §2 — Service skeleton (Infrastructure) — MỤC 2 ✔
**File MỚI:** `src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs`
- Rào action: `AllowedActions` HashSet `{submit,approve,return,reject}` (`StringComparer.OrdinalIgnoreCase`) → sai ⇒ `throw new ConflictException("Hành động không hợp lệ.")` — ĐỨNG TRƯỚC mọi truy vấn DB.
- 4 nhánh `switch` TÁCH RIÊNG, mỗi nhánh `throw new NotImplementedException("W3 đợt-3: " + action)` + 1 dòng ghi rõ đợt-3 phải điền gì (submit: guard trình + đặt con-trỏ (1,1) + notify Cấp 1 · approve: OR-of-N + UPSERT + tiến con-trỏ + finalize · return: TraLai · reject: TuChoi). `default:` ném Conflict (switch đóng kín).
- **Comment đầu file = hợp-đồng đợt-3** (yêu cầu mục 2): nguồn copy `ContractWorkflowService.cs:217-394`; CẤM Proposal-flatten; 4 điểm GIỮ (con-trỏ đôi `ContractSigningPlan.cs:35-37` · GroupBy `:246` · OR-of-N `:255-266` · UPSERT `:292-316`); 3 chỗ ADAPT (a) finalize **2 write-path** kể cả admin-override `:190-193` gán Phase QUA BIẾN + khuôn RULE `ContractFeatures.cs:356-360` (KHÔNG dùng `PEWS:1008` vì dòng đó tự-đếm sai) (b) KHÔNG port 2 nhánh kết thúc sớm (c) reject → TraLai toàn phần, cờ 4 return-mode ở `ApprovalWorkflow.cs:116-125` (class `ApprovalWorkflowLevel` trong file đó từ `:95`); CẤM `IChangelogService.LogWorkflowTransitionAsync` (FK-547, `ContractChangelogConfiguration.cs:22-26`).
**QUYẾT-2: 4 dependency `(IApplicationDbContext db, ICurrentUser currentUser, IDateTime dateTime, INotificationService notifications)` — khai từ đợt-1 dù đợt-1 chưa dùng.**
- `ICurrentUser` thay cặp `(actorUserId, actorRoles)` của khuôn Contract/PE, **bắt buộc** vì chữ ký lead chốt KHÔNG có tham số actor. Test dựng được: khuôn `FakeCurrentUser` có sẵn ≥10 file test (vd `PeCcmBudgetPeriodSetterAuthzTests.cs:30`).
- **KHÔNG** `UserManager<User>`: role lấy từ `ICurrentUser.Roles`, phòng ban từ `db.Users` (`IApplicationDbContext.cs:29`) ⇒ test khỏi kéo `IdentityFixture`.
- **KHÔNG** `IChangelogService` — CỐ Ý: không inject thì không lỡ tay gọi hàm gây FK-547 (rào bằng thiết-kế, không bằng trí nhớ).
- **KHÔNG** codegen — mã KHKK gen ngay lúc TẠO phiếu (W2), không gen lúc trình.
**QUYẾT-3: `#pragma warning disable CS9113` bao quanh class (2 dòng, đợt-3 xoá).**
Đo được: build lượt-1 ra **4 warning CS9113** `Parameter 'db'/'currentUser'/'dateTime'/'notifications' is unread` (dòng 71-74). 2 đường xử lý:
- (i) bỏ tham số cho sạch warning ⇒ test-specialist phải dựng `new ContractSigningPlanWorkflowService()` hôm nay rồi đợt-3 sửa lại **file tests** — mà tests KHÔNG phải phần việc lane này (sửa chéo);
- (ii) giữ chữ ký cuối + tắt đúng mã CS9113 đúng phạm vi class.
Chọn (ii) vì mục đích test-before là **khoá chữ ký**. Bằng chứng khuôn dựng thẳng: `tests/.../Services/ContractWorkflowServiceApproveV2Tests.cs:42` `new ContractWorkflowService(db, codeGen, dt, notify, changelog, um)`.
---
## §3 — DI + Controller — MỤC 3, 4 ✔
**DI** `src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs:43-45``services.AddScoped<IContractSigningPlanWorkflowService, ContractSigningPlanWorkflowService>();` NGAY DƯỚI dòng codegen `:42` (đúng chỗ lead chỉ).
**Controller** `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs`:
- Primary ctor `(IMediator mediator, IContractSigningPlanWorkflowService workflow)` + `using SolutionErp.Application.ContractSigningPlans.Services;`.
- `POST {id:guid}/transitions` + `[Authorize(Policy = "KeHoachKyKet.Update")]``workflow.TransitionAsync(id, body.Action, body.Comment, ct)``Ok(result)`.
- Body record `ContractSigningPlanTransitionBody(string Action, string? Comment = null)`.
- **Không try-catch** (GlobalExceptionMiddleware map → ProblemDetails).
- **KHÔNG kiểm `id != cmd.Id`** như các endpoint dossier-item: body transition KHÔNG mang id ⇒ 1 nguồn sự thật là route.
**Sửa comment STALE (bài S116/S122):** header controller `:21` cũ ghi *"🔴 KHÔNG có endpoint transition (submit/approve/reject/return) — đó là W3"* — sau đợt này là SAI. Đã thay bằng đính chính + khai rõ "đợt-1 mới skeleton ⇒ endpoint trả 500 tới khi đợt-3 điền". Grep cùng-lớp `grep -rn "đó là W3"` phần dưới §5 để chắc không còn site nào khác.
---
## §4 — BUILD + TEST (số đo thật) — MỤC 5 ✔
| Lượt | Lệnh | Kết quả |
|---|---|---|
| Build #1 | `dotnet build SolutionErp.slnx` | **Build succeeded · 0 Error · 4 Warning** (CS9113 ×4, dòng 71-74 `ContractSigningPlanWorkflowService.cs`) — đo được, không đoán |
| Vá | thêm `#pragma warning disable/restore CS9113` bao class (2 dòng, có comment "đợt-3 xoá") | — |
| Build #2 | `dotnet build SolutionErp.slnx` | **Build succeeded · 0 Warning(s) · 0 Error(s)** · 30.02s · 6/6 project (Domain, Application, Infrastructure, Api, Domain.Tests, Infrastructure.Tests) |
| Test | `dotnet test SolutionErp.slnx` | **Domain 45/45 pass · Infrastructure 529/529 pass · Failed 0 · Skipped 0** ⇒ tổng **574** = ĐÚNG baseline (45D + 529I). Skeleton KHÔNG phá gì. |
---
## §5 — Grep verify sau khi vá (soi lại bằng lệnh KHÁC, bài S146)
**A. Stale-comment cùng-lớp**`grep -rn "đó là W3\|KHÔNG có endpoint transition" --include=*.{cs,ts,tsx,md} src/ fe-admin/src fe-user/src`
→ đúng **1 hit** và là dòng ĐÍNH CHÍNH mình vừa viết (`ContractSigningPlansController.cs:22`), 0 site sai còn sót.
**B. Policy resolve được không** (không có trong `MenuKeys.All`**500 lúc chạy**, không phải 403) — `grep -n "KeHoachKyKet" MenuKeys.cs``:42` const + **`:180` NẰM TRONG `All`** ⇒ 4 policy `KeHoachKyKet.{Read,Create,Update,Delete}` sinh runtime ⇒ `KeHoachKyKet.Update` của endpoint mới hợp lệ. ✔
**C. Code đã land**`ContractSigningPlansController.cs:207` `[HttpPost("{id:guid}/transitions")]` · `:225` `record ContractSigningPlanTransitionBody` · `DependencyInjection.cs:45` `AddScoped<IContractSigningPlanWorkflowService, ...>`. ✔
**D. Containment (git status)** — file mình chạm ĐÚNG 4:
- `?? src/Backend/SolutionErp.Application/ContractSigningPlans/Services/IContractSigningPlanWorkflowService.cs` (mới)
- `?? src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs` (mới)
- ` M src/Backend/SolutionErp.Infrastructure/DependencyInjection.cs`
- ` M src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs`
+ sub-md của mình trong run-folder. **0 file `tests/**` · 0 file `fe-*` · 0 sửa `slnx` · 0 commit/push · 0 chạm agent-memory.**
(`fe-*/khkk*`, `sub-implementer-frontend-1.md`, `.session-counter.json` trong `git status` là của **lane FE + harness**, KHÔNG phải mình.)
---
## §6 — ĐỐI CHỨNG CHÉO LANE FE (đọc-only) — hợp-đồng khớp 5/5
Bài W2 "6 điểm đứt" nên soi ngay thay vì chờ tích hợp. Đọc `fe-admin/src/types/khkk.ts:250-276` + `KhkkWorkflowPanel.tsx:132`:
| Điểm | FE (đĩa) | BE (mình) | Khớp |
|---|---|---|---|
| URL | `KhkkWorkflowPanel.tsx:132` `/contract-signing-plans/${plan.id}/transitions` | `[HttpPost("{id:guid}/transitions")]` trên `[Route("api/contract-signing-plans")]` | ✔ |
| action 4 literal | `khkk.ts:257-262` `'submit'|'approve'|'return'|'reject'` | `AllowedActions` cùng 4 chuỗi | ✔ |
| body field | `khkk.ts:266-269` `{ action, comment }` | `ContractSigningPlanTransitionBody(Action, Comment)` → camelCase | ✔ |
| response 3 field | `khkk.ts:272-276` `phase / currentWorkflowStepIndex / currentApprovalLevelOrder` | record cùng 3 field | ✔ |
| kiểu `phase` | `khkk.ts:273` `phase: number` (**INT**, comment FE ghi rõ) | `int Phase` (QUYẾT-1) | ✔ — 2 lane độc lập cùng chốt INT |
⇒ QUYẾT-1 (ép `int` thay enum) được lane FE xác nhận độc lập, không phải mình tự diễn giải.
---
## §7 — BÀN GIAO
**Cho test-specialist (viết 6 test RED ngay được):**
- Dựng service: `new ContractSigningPlanWorkflowService(db, currentUser, dateTime, notifications)` — chữ ký này ĐÃ CUỐI, đợt-3 không đổi. `currentUser` dùng khuôn `FakeCurrentUser` (`PeCcmBudgetPeriodSetterAuthzTests.cs:30`).
- Gọi: `await svc.TransitionAsync(planId, "approve", "ý kiến", ct)` → hiện ném `NotImplementedException("W3 đợt-3: approve")` ⇒ **6 test sàn ĐỎ THẬT** (đúng ý test-before), KHÔNG đỏ vì lỗi biên dịch.
- Action lạ (vd `"finalize"`) → `ConflictException("Hành động không hợp lệ.")` — hành vi này ĐÃ THẬT ở đợt-1, muốn test luôn cũng được.
- `Inbox_OnlyPendingLevelApprover_SeesPlan` không chạm service này (đi qua query handler W2).
**Cho đợt-3 (điền logic):** đọc block comment đầu `ContractSigningPlanWorkflowService.cs` — đã chép sẵn 4 điểm GIỮ + 3 chỗ ADAPT + 2 lệnh CẤM (Proposal-flatten, `IChangelogService.LogWorkflowTransitionAsync`) kèm `file:line`. Việc đầu tiên: **xoá 2 dòng `#pragma CS9113`** (4 dep sẽ được dùng thật).
**Cần lead phân xử (3 quyết đã khai ở §1-§2):** QUYẾT-1 `phase` kiểu `int` · QUYẾT-2 bộ 4 dependency (đặc biệt `ICurrentUser` thay tham số actor, và **cố ý không inject `IChangelogService`** làm rào FK-547) · QUYẾT-3 pragma CS9113 tạm 2 dòng.
**Ambiguity còn lại (khai, không tự quyết):** rào action nhận **không phân biệt hoa/thường** (`"Submit"` cũng qua). Hợp-đồng ghi chữ thường; FE gửi chữ thường (`khkk.ts:257-262`) nên nới này KHÔNG ảnh hưởng acceptance nào — nếu lead muốn STRICT ordinal thì đổi 1 chỗ (`AllowedActions` bỏ `OrdinalIgnoreCase` + bỏ `ToLowerInvariant`).