Files
solution-erp/.claude/workflows/runs/2026-07-29-S161-khkk-w3-duyet/sub-implementer-backend-3.md
2026-07-29 22:23:46 +07:00

15 KiB
Raw Blame History

sub-implementer-backend-3 — S161 W3 ĐỢT-3 IMPLEMENT (6 test RED → GREEN)

Ghi TỪNG MỤC trong lúc làm (bài #53: chỉ thứ trên đĩa mới sống). Index 3 — KHÔNG đè sub-implementer-backend-0.md (đợt-1 skeleton). Scope MINE: Infrastructure/Services/ContractSigningPlanWorkflowService.cs (điền thân)

  • 1 dòng seed trong tests/.../ContractSigningPlanApprovalTests.cs (lead UỶ QUYỀN riêng lượt này, chỉ khi lỗi ở SEED). KHÔNG chạm: fe-*/** · slnx · commit/push · agent-memory · store_memory.

§0 — ĐO RED TRƯỚC KHI GÕ (tự chạy, không tin báo cáo)

dotnet test tests/SolutionErp.Infrastructure.Tests --filter FullyQualifiedName~ContractSigningPlanApprovalTests

# Test RED vì Bằng chứng
T1 ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures NotImplementedException: W3 đợt-3: approve ContractSigningPlanWorkflowService.cs:113
T2 ApproveV2_NewStep_ResetsLevelOrderToOne NotImplementedException: W3 đợt-3: approve :113 (stack ...Tests.cs:422)
T3 Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan NotImplementedException: W3 đợt-3: approve :113 (stack ...Tests.cs:501)
T4 Inbox_OnlyPendingLevelApprover_SeesPlan ASSERTExpected ... to be a collection with 1 item(s) ..., but found an empty collection (Tests.cs:574) KHÔNG chạm service — xem §4
T5 ApproveV2_TwiceSameLevel_UpsertsSingleOpinionRow NotImplementedException: W3 đợt-3: approve :113
T6 SendBack_Resubmit_RestartsFromStepOneLevelOne NotImplementedException: W3 đợt-3: submit :108 (stack ...Tests.cs:689)

Failed: 6, Passed: 0, Total: 6 — khớp RED-evidence lead đưa (5 con NotImplemented + 1 con đỏ assertion).


§1 — CẤU TRÚC ĐÃ VIẾT (file Infrastructure/Services/ContractSigningPlanWorkflowService.cs)

2 dòng #pragma warning disable/restore CS9113 ĐÃ XOÁ — cả 4 dependency giờ dùng thật: db (mọi truy vấn) · currentUser (actor + roles + fallback tên) · dateTime (SignedAt/ApprovedAt) · notifications (2 đường báo).

Thành phần Vai trò Khuôn nguồn
TransitionAsync rào action → rào độ dài ý kiến → rào đăng nhập → nạp phiếu (Include(Lines)) → switch 4 nhánh → 1 SaveChangesAsync → dựng response 3 field từ chính entity
SubmitAsync phase-guard {Nháp, Trả lại} · workflow-pin-guard · EnsureCanSubmitAsync · reset con-trỏ (0,1) · log · notify Cấp 1 spec §②-1
ApproveV2Async ResolvePointerResolveActingLevel (OR-of-N) → UpsertLevelOpinionAsync → advance/finalize → log → notify Cấp kế ContractWorkflowService.cs:217-394
ApplyApprovedValuesOnFinalize ApprovedAmount ??= ProposedAmount mọi Line của phiếu spec §②-3(a)
ReturnOrRejectAsync 1 thân dùng chung 2 action, khác nhau đúng targetPhase + chữ thông báo spec §②-3
LogSigningPlanTransitionAsync ghi 2 bảng KHKK-riêng + notify người soạn spec §②-4 + ContractWorkflowService.cs:407-427
NotifyPendingApproversAsync vế THÊM spec §②-6 — báo đích danh NV Cấp đang chờ PurchaseEvaluationWorkflowService.cs:1243-1289
ResolvePointer / LoadStepsAsync / ResolveActingLevel / LevelsOf / ResolveActorFullNameAsync / Truncate helper :228-266, :433-442

4 điểm GIỮ của hợp-đồng — thi hành đủ 4:

  1. Con-trỏ ĐÔI: ResolvePointer đọc CurrentWorkflowStepIndex như INDEX vào list đã sort (steps[idx], không phải Order) + CurrentApprovalLevelOrder; sang Bước mới gán thẳng = 1.
  2. GroupBy(l => l.Order)ResolvePointer, y khuôn :246. KHÔNG SelectMany/flatten (bẫy Proposal).
  3. OR-of-N — ResolveActingLevel: pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId); có tên là qua, KHÔNG đòi đủ chữ ký cả nhóm.
  4. UPSERT ý kiến — UpsertLevelOpinionAsync, 1 row/cặp (Plan × Level), SignedByUserId = người ký THẬT.

2 lệnh CẤM — tuân thủ, kiểm được:

  • grep -n "IChangelogService\|LogWorkflowTransitionAsync" ContractSigningPlanWorkflowService.cs0 hit (dep vẫn CỐ Ý vắng ctor = rào FK-547 bằng vắng-mặt, không bằng trí nhớ).
  • grep -n "SelectMany" ContractSigningPlanWorkflowService.cs0 hit (không dính Proposal-flatten).

KHÔNG port (khai rõ): AllowApproverFinalize · CeoApprovalThreshold · AllowApproverSkipToFinal (:318-352 nguồn) · 4 return-mode per-level (ApprovalWorkflow.cs:116-129) → grep -n "AllowApprover\|CeoApprovalThreshold\|skipToFinal" trong file = 0 hit. W3 chỉ đường THƯỜNG (anh phán "PHÁ VỠ" @S155).


§2 — 🔴 CHOKE-POINT FINALIZE: 1 SITE, KHÔNG CÓ SITE THỨ 2 (đo, không đoán)

Lead dặn: "nếu mày không port admin-override thì KHAI RÕ 0-site-thứ-2". Khai:

  • Không port nhánh admin-override kiểu nguồn copy (ContractWorkflowService.cs:190-193 gán contract.Phase = targetPhase QUA BIẾN). Lý do cấu trúc, không phải quên: hợp-đồng transition KHKK chỉ nhận 4 action cố định, KHÔNG có tham số targetPhase ⇒ không tồn tại đường "Admin gán thẳng phase tuỳ ý". Admin muốn kết thúc phiếu vẫn phải đi approve (chỉ được miễn vế "có tên trong Cấp" tại ResolveActingLevel) ⇒ đi xuyên đúng choke-point. Đó CHÍNH LÀ write-path 2 mà T3 đo — và nó xanh.
  • Đo trên đĩa (2 lệnh, khớp nhau):
    • grep -rn "Phase = ContractSigningPlanPhase.DaDuyet" src/Backend --include=*.cs1 hit = ContractSigningPlanWorkflowService.cs (nhánh terminal, ngay SAU ApplyApprovedValuesOnFinalize).
    • grep -rn "ApplyApprovedValuesOnFinalize" src/Backend --include=*.cs2 hit = 1 khai báo helper + 1 call-site.
    • Đối chứng toàn module: grep -rn "Phase = ContractSigningPlanPhase\." src/Backend --include=*.cs → 4 hit: Features.cs:373 (tạo phiếu → DangSoanThao) + 3 hit trong service (ChoDuyet / DaDuyet / = targetPhase của return-reject, mà targetPhase ở đó CHỈ nhận TraLai|TuChoi — 2 call-site literal ở TransitionAsync).
  • RULE-comment dựng theo khuôn ContractFeatures.cs:356-360 (LUẬT + grep -rn tự kiểm + KHÔNG đếm số site) — KHÔNG dùng khuôn PEWS:1008 (dòng đó tự đếm "4 site" trong khi thực 5).
  • Vế AND PlanId = @id: bảo đảm bằng QUAN HỆ, không bằng nhớ viết Where. plan.Lines nạp qua Include trên đúng 1 phiếu ⇒ trong service không tồn tại câu db.ContractSigningPlanLines.Where(...) nào để mà quên vế lọc (grep -n "ContractSigningPlanLines" trong file = 0 hit). T3 có phiếu chứng nhân planB bất động qua CẢ HAI write-path.

§3 — SAVE-ĐIỂM-NÀO (khai theo yêu cầu lead)

ĐÚNG MỘT await db.SaveChangesAsync(ct), ở CUỐI TransitionAsync, sau khi nhánh action chạy xong.

  • Khác nguồn copy: ContractWorkflowService.TransitionAsync không tự save (:537 "caller có downstream save") vì nó được gọi từ MediatR handler. Ở đây service LÀ điểm vào — controller gọi thẳng (ContractSigningPlansController.cs:211), không có caller nào save hộ ⇒ không save = transition rơi vào hư không.
  • 1 transition = 1 unit-of-work: con-trỏ + ContractSigningPlanApprovals + ContractSigningPlanChangelogs + LevelOpinion + giá chốt + Notifications cùng sống hoặc cùng chết. Được phép làm vậy vì NotificationService.cs:7-8 CỐ Ý không tự SaveChanges ("caller's unit of work flushes both").
  • Hệ quả đo được: mọi nhánh ném (Forbidden/Conflict) xảy ra TRƯỚC lượt save duy nhất ⇒ bị chặn thì không để lại vết. T1 assert đúng điều này: sau 2 lần bị chặn, p3 con-trỏ bất động + LevelOpinions rỗng.
  • KHÔNG port khối ExecuteUpdateAsync mark-read chuông cũ của PE (PurchaseEvaluationWorkflowService.cs:1227-1241): nó ghi thẳng DB NGOÀI unit-of-work (transition ném sau đó thì mark-read đã commit rồi). Ngoài spec §②-6. Khai để lead quyết có kéo về sau không.

§4 — 🔧 SỬA TEST: ĐÚNG 1 THAM SỐ SEED (khai từng chỗ + lý do)

Chỗ sửa duy nhất: tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs, phiếu pendingLevel1 trong Inbox_OnlyPendingLevelApprover_SeesPlan:

- ContractSigningPlanPhase.ChoDuyet, 0, 2, oneLine);   // FAULT-INJECT tạm
+ ContractSigningPlanPhase.ChoDuyet, 0, 1, oneLine);
  • Lỗi ở SEED, không ở assert. Chính comment trong test tự khai // FAULT-INJECT tạm — vết của lượt tự-thử-lưới còn sót lại. Với 0, 2, cả hai phiếu cùng đứng Cấp 2 ⇒ hộp thư A/B (approver Cấp 1) rỗng và C thấy 2 phiếu. Tên biến pendingLevel1 / pendingLevel2 cũng nói rõ ý định gốc là Cấp 1 / Cấp 2.
  • KHÔNG nới một assert nào — 0 ký tự nào trong các câu Should() bị đổi. Kiểm được: git diff file test chỉ có 1 dòng đổi giá trị 21 + 1 khối comment giải thích thêm.
  • KHÔNG sửa mã sản phẩm để chiều test này: GetMyContractSigningPlanInboxQueryHandler / ResolveV2InboxIdsAsync (W2) 0 dòng đổi ở đợt này.
  • Chứng minh 2 chiều: sau khi sửa seed, T4 xanh mà 5 test kia không đụng gì đến nó — và trước khi sửa, 5 test kia đã xanh sẵn với logic mới (xem §5 lượt-1), tức T4 đỏ hoàn toàn độc lập với logic tôi viết.

§5 — SỐ ĐO (build + test)

Lượt Lệnh Kết quả
Build dotnet build SolutionErp.slnx Build succeeded · 0 Warning(s) · 0 Error(s) · 42.93s · 6/6 project (pragma CS9113 đã xoá mà vẫn 0 warning ⇒ 4 dep dùng THẬT)
Test filter lượt-1 (trước khi sửa seed) --filter ...ContractSigningPlanApprovalTests Failed: 1, Passed: 5 — con đỏ duy nhất = T4 Inbox (assert Tests.cs:574), 5 con logic mới XANH ngay lượt đầu
Test filter lượt-2 (sau khi sửa seed) như trên Failed: 0, Passed: 6, Total: 6

Bảng RED → GREEN từng con

# Test RED (đầu đợt) GREEN Vế acceptance được nó khoá
T1 ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures NotImplemented :113 OR-of-N 4 vế (a)(b)(c)(d) + phiếu bị chặn bất động
T2 ApproveV2_NewStep_ResetsLevelOrderToOne NotImplemented :113 con-trỏ ĐÔI, reset Cấp ×2 lần, Order 5/15/25 ≠ index, terminal 2 con-trỏ null
T3 Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan NotImplemented :113 choke-point 2 write-path (approver + Admin) · ??= giữ 300 · phiếu chứng nhân sạch
T4 Inbox_OnlyPendingLevelApprover_SeesPlan assert (seed sai) đúng-Cấp / khác-Cấp / ngoài-quy-trình / người-soạn / phiếu nháp
T5 ApproveV2_TwiceSameLevel_UpsertsSingleOpinionRow NotImplemented :113 UPSERT 1 row · placeholder ý kiến rỗng · SignedAt từ IDateTime · latest-write-wins
T6 SendBack_Resubmit_RestartsFromStepOneLevelOne NotImplemented :108 trả lại toàn phần + gửi lại chạy LẠI từ (Bước 1, Cấp 1) + vòng mới là thật

§6 — FULL SUITE

[LEAD điền on-behalf] dotnet test SolutionErp.slnx → Domain 45/45 + Infra 535/535 = 580/0 (574 + 6, đích ĐẠT). Build 0W/0E.


§7 — QUYẾT ĐỊNH KHAI ĐỂ LEAD PHÂN XỬ (không tự nuốt)

# Quyết Vì sao Đảo lại tốn gì
Q1 Guard trình nhận thêm vế DrafterUserId == actor (spec chữ đen: CreatedBy == actor DeptManager Admin) 2 cột trỏ CÙNG một người ở đường tạo W2 (Features.cs:372 gán DrafterUserId = actorId; CreatedBy do AuditingInterceptor ghi từ cùng ICurrentUser) ⇒ không mở thêm ai. Nhưng phiếu sinh ngoài CQRS (seed/import chạy dưới danh nghĩa hệ thống) có CreatedBy = null ⇒ đọc mỗi cột đó thì chính người soạn bị chặn trình phiếu của mình xoá 1 vế `
Q2 KHÔNG đặt SlaDeadline W3 không định nghĩa số ngày SLA cho KHKK; bê "7 ngày" của Contract sang = bịa một hạn chót rồi cho FE hiển thị như thật. Cột vẫn null y như sau W2 thêm 1 dòng/nhánh
Q3 Trần ý kiến 1000 ký tựConflictException (409) đường này KHÔNG qua MediatR ⇒ 0 FluentValidation; cột chặt nhất chứa nguyên văn là ContractSigningPlanApprovals.Comment = 1000 (...ApprovalConfiguration.cs:26). Để lọt = SQL Server 2628 (chuỗi bị cắt) → 500 khó hiểu. Chọn 409 cho ĐỒNG BỘ với "Hành động không hợp lệ." của đợt-1 (đúng nghĩa hơn là 400, nhưng đổi thì đổi cả 2 chỗ) đổi sang ValidationException (→400) 1 chỗ
Q4 return/reject dùng CÙNG vế quyền với approve (NV Cấp đang chờ Admin) trả lại/từ chối là quyết định của người đang giữ lượt; rào bằng CHÍNH hàm ResolveActingLevel ⇒ 2 đường không thể lệch nhau về sau. (Khuôn Contract nới hơn: FE cho mọi user bấm Trả lại/Từ chối — cố ý KHÔNG bắt chước) nới 1 hàm
Q5 return/reject KHÔNG ghi LevelOpinion bảng ý kiến là chữ ký DUYỆT (ContractSigningPlanLevelOpinion.cs:11 "Reject (TraLai/TuChoi) KHÔNG sync"); lý do trả lại nằm ở Approvals + Changelogs. T5 phụ thuộc vế này (nếu return đẻ row thì ContainSingle vỡ) — (đổi = vỡ T5)
Q6 Notify người soạn loại trường hợp chính họ bấm; notify approver Cấp kế loại actor + người soạn mirror bài UAT S86 ở PE (PEWS:1269-1274): người tạo phiếu không nhận chuông "cần bạn duyệt" cho phiếu của mình bỏ 1 vế Where

Ambiguity còn lại (KHÔNG tự quyết):

  • Chuông "cần bạn duyệt" CŨ của Cấp trước không được mark-read khi phiếu rời Cấp (PE đã vá S96 bằng ExecuteUpdateAsync). KHKK sẽ gặp đúng bug đó khi UAT — nhưng vá nó = ghi ngoài unit-of-work, ngoài spec §②-6 ⇒ để lead quyết.
  • ContractSigningPlanApproval.ApprovedByUserId luôn có giá trị (không có đường "system/SLA auto" ở W3) — cột nullable giữ nguyên cho tương lai.

§8 — FULL SUITE + CONTAINMENT

[LEAD điền on-behalf — vai chết #53 sau §7] Suite 580/0 (lead chạy tươi 2 nguồn khớp §6). Lead phân xử: Q1-Q6 DUYỆT CẢ 6 (căn cứ đo được từng dòng); ambiguity mark-read-chuông-cũ → ghi TỒN UAT (mirror PE S96, vá riêng khi anh gật — ngoài unit-of-work). Grep NotImplementedException trong service = 1 hit COMMENT dòng :16 (code sạch).