14 KiB
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 là var allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet(); và :260 là 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.cscùng thư mụcServices/(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:
AllowedActionsHashSet{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
switchTÁCH RIÊNG, mỗi nhánhthrow 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ỏ đôiContractSigningPlan.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-193gán Phase QUA BIẾN + khuôn RULEContractFeatures.cs:356-360(KHÔNG dùngPEWS:1008vì 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(classApprovalWorkflowLeveltrong file đó từ:95); CẤMIChangelogService.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.
ICurrentUserthay 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ônFakeCurrentUsercó sẵn ≥10 file test (vdPeCcmBudgetPeriodSetterAuthzTests.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éoIdentityFixture. - 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:42new 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.Idnhư 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.csM src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs
- sub-md của mình trong run-folder. 0 file
tests/**· 0 filefe-*· 0 sửaslnx· 0 commit/push · 0 chạm agent-memory. (fe-*/khkk*,sub-implementer-frontend-1.md,.session-counter.jsontronggit statuslà 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' |
| 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.currentUserdùng khuônFakeCurrentUser(PeCcmBudgetPeriodSetterAuthzTests.cs:30). - Gọi:
await svc.TransitionAsync(planId, "approve", "ý kiến", ct)→ hiện némNotImplementedException("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_SeesPlankhô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).