Files
solution-erp/.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w3-duyet-3-tram-29-07-2026.md
2026-07-29 18:43:24 +07:00

6.2 KiB
Raw Blame History

SPEC WAVE-3 — Duyệt 3 trạm + finalize chốt giá (GĐ2 KHKK) 🔴 wave nặng nhất

Nguồn RATIFIED: spec cũ §2.1 (bẫy Proposal) + §2.3 service + §③ WAVE-3 (:341-386). Chạy SAU W2. Test-before BẮT BUỘC (critical algorithm OR-of-N).

① TÍNH NĂNG / ĐỀ BÀI

Phiếu KHKK chạy trọn DangSoanThao → ChoDuyet → (PMH → CCM → CEO) → DaDuyet, chốt ApprovedAmount per-winner tại finalize choke-point. Default dry-run: đường THƯỜNG duy nhất — CEO ký thật, KHÔNG bật 2 đường kết thúc sớm (AllowApproverFinalize/CeoApprovalThreshold — anh từng phán "PHÁ VỠ" S155; plan cha default #1).

② CÁCH IMPLEMENT

  1. Gate 5-anchor (§③-A).
  2. Test-before: viết 6 test sàn (bảng §③-B) RED trước.
  3. Infrastructure/Services/ContractSigningPlanWorkflowService.cs — copy ContractWorkflowService.ApproveV2Async:217-394 (🔴 CẤM copy ProposalFeatures — flatten phá OR-of-N, comment tự thú "assume 1 step" ở :424, cơ chế :427-439). Giữ nguyên: con-trỏ ĐÔI StepIndex+LevelOrder · GroupBy Order :246 · OR-of-N :259-260 · LevelOpinions UPSERT :292-316 · skipToFinal :320-352. Adapt 3 chỗ: (a) 🔴 Finalize choke-point — 2 WRITE-PATH, KHÔNG "DUY NHẤT" (review F-B1, lớp #81): nguồn copy có ≥2 nhánh set terminal — nhánh duyệt-cấp-cuối VÀ nhánh admin-override (ContractWorkflowService.cs:190-193 gán contract.Phase = targetPhase QUA BIẾN ⇒ literal-grep KHÔNG thấy). Helper ApplyApprovedValuesOnFinalize (mọi Line ApprovedAmount ??= ProposedAmount) phải gọi ở CẢ HAI. Khuôn RULE-comment ĐÚNG = ContractFeatures.cs:356-360 (LUẬT + grep -rn + "không đếm số") — KHÔNG phải PEWS:1008 (review F-B4: dòng đó CÓ số-đếm "4 site" mà thực 5 call-site, tự mâu thuẫn; và :1009 là helper NS khác helper GIÁ :973-1002). (b) 🔴 KHÔNG PORT 2 nhánh kết thúc sớm (review F-B3 — anh phán "PHÁ VỠ" S155, "port-nhưng-không-kích-hoạt" = containment GIẢ vì Designer type=10 render checkbox thật). W3 chỉ đường THƯỜNG. Containment thật = W1 ẩn 2 checkbox cho type=10. (c) Reject → tối thiểu TraLai toàn phần (4 return-mode per-level cờ sẵn Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:116-125🔴 class ApprovalWorkflowLevel nằm TRONG ApprovalWorkflow.cs:95, KHÔNG có file riêng tên đó).
  4. 🔴 Ghi lịch sử — DÙNG BẢNG KHKK RIÊNG, KHÔNG hàm Contract (review F-B2 = FK-547 runtime): mỗi transition ghi ContractSigningPlanApprovals (#7) + ContractSigningPlanChangelogs (#6) qua method KHKK-riêng (LogSigningPlanTransition tự viết, ghi _db.ContractSigningPlanChangelogs). 🔴 CẤM gọi IChangelogService.LogWorkflowTransitionAsync — signature (Guid contractId,…) ghi ContractChangelogsFK vật lý ContractId Cascade (ContractChangelogConfiguration.cs:22-26) ⇒ truyền plan.Id = FK violation 547 (enum cast được nên compiler KHÔNG chặn).
  5. Guard trình = CreatedBy==actor DeptManager(cùng DepartmentId phiếu) Admin — CỐ Ý khác khuôn role-based Contract để né lớp 403-PMH (spec cũ :193-196).
  6. Notify: mirror LogTransitionAsync (ContractWorkflowService.cs:407-427) + THÊM notify đích danh approver Cấp kế (fix lỗ notify-chỉ-Drafter từ ngày 1).
  7. Endpoint POST {id}/transitions (+ policy KeHoachKyKet.Update).
  8. FE ×2: panel workflow (mirror PE Panel-3 ✓/●/○) + banner "Đến lượt bạn" (khuôn blockedByV2Level) + nút Duyệt/Trả lại/Từ chối + ô ý kiến (UPSERT vào LevelOpinions khi duyệt — Q1=1B sync).
  9. Build ×2 + test GREEN + UAT 1 lượt thật (2 phiếu song sinh) + commit.

③ CHECKLIST

A. Gate 5-anchor: ContractWorkflowService.cs:217-394 (nguồn copy) · :259-260 (OR-of-N) · :292-316 (UPSERT) · ApprovalWorkflow.cs:95+:116-125 (return-mode cờ, class trong file này) · ContractFeatures.cs:356-360 (khuôn RULE choke-point — LUẬT + grep -rn + "không đếm số"; thay PEWS:1008 per F-B4, khớp §②-3(a)). B. Acceptance (spec cũ W3 :348-384 — SÀN 6 test, thiếu 1 tên = TRƯỢT):

  • 🔴 OR-of-N 4 vế: (a) 2 phiếu song sinh — phiếu-1 A duyệt → tiến; phiếu-2 B duyệt → CŨNG tiến; (b) ca ÂM: người ngoài cấp → 403; (c) LevelOpinions WHERE PlanId=@id = 1 row; (d) KHÔNG đòi cả A và B.
  • Con-trỏ đôi: workflow 3 Bước → StepIndex 1→2→3, LevelOrder reset về 1 mỗi Bước mới.
  • Finalize: Lines WHERE PlanId=@id AND ApprovedAmount IS NULL = 0 (🔴AND PlanId=@id) — TEST CẢ 2 write-path: duyệt-cấp-cuối THƯỜNG + admin-override (F-B1).
  • Inbox: approver cấp đang chờ thấy phiếu; cấp khác KHÔNG.
  • UPSERT: duyệt 2 lần cùng cấp → 1 row, SignedByUserId người thật.
  • Trả lại → TraLai → sửa → gửi lại chạy từ Cấp 1 Bước 1.
  • Notify: approver Cấp kế NHẬN thông báo (test-after được phép — ngoài sàn 6).
  • dotnet test PASS 0 fail, số-sau ≥ baseline + 6, đủ 6 TÊN theo khuôn repo (anh chốt @S160 đổi khỏi tên thuần-Việt spec cũ :377-382; English-predicate + danh-từ-VN-không-dấu) trong ContractSigningPlanApprovalTests: ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures (OR-of-N ca (d)) · ApproveV2_NewStep_ResetsLevelOrderToOne (con-trỏ đôi) · Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan (choke-point AND PlanId=@id) · Inbox_OnlyPendingLevelApprover_SeesPlan (ca ÂM cấp khác) · ApproveV2_TwiceSameLevel_UpsertsSingleOpinionRow (UPSERT + SignedByUserId) · SendBack_Resubmit_RestartsFromStepOneLevelOne (vòng trả-lại). (Ý nghĩa 1:1 với 6 acceptance #1-#6; đổi tên KHÔNG đổi phủ.)
  • FE build ×2 PASS; cicd 3-chân-kiềng sau deploy; prod curl: user approver thật vào inbox thấy phiếu test ZZTEST. C. Rủi ro: [ ] copy nhầm Proposal = mất OR-of-N ÂM THẦM (build sạch test cũ xanh — acceptance #1 là lưới duy nhất) · [ ] quên inbox precompute (đã wire W2) = lặp lỗi Contract.