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

14 KiB
Raw Blame History

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/ 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ì :259var allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet();:260if (!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-45services.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
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ớpgrep -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.All500 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 đã landContractSigningPlansController.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'
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).