6.2 KiB
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
- Gate 5-anchor (§③-A).
- Test-before: viết 6 test sàn (bảng §③-B) RED trước.
Infrastructure/Services/ContractSigningPlanWorkflowService.cs— copyContractWorkflowService.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-193gáncontract.Phase = targetPhaseQUA BIẾN ⇒ literal-grep KHÔNG thấy). HelperApplyApprovedValuesOnFinalize(mọi LineApprovedAmount ??= 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ảiPEWS:1008(review F-B4: dòng đó CÓ số-đếm "4 site" mà thực 5 call-site, tự mâu thuẫn; và:1009là 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ểuTraLaitoàn phần (4 return-mode per-level cờ sẵnDomain/ApprovalWorkflowsV2/ApprovalWorkflow.cs:116-125— 🔴 classApprovalWorkflowLevelnằm TRONGApprovalWorkflow.cs:95, KHÔNG có file riêng tên đó).- 🔴 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 (LogSigningPlanTransitiontự viết, ghi_db.ContractSigningPlanChangelogs). 🔴 CẤM gọiIChangelogService.LogWorkflowTransitionAsync— signature(Guid contractId,…)ghiContractChangelogscó FK vật lýContractIdCascade (ContractChangelogConfiguration.cs:22-26) ⇒ truyềnplan.Id= FK violation 547 (enum cast được nên compiler KHÔNG chặn). - 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). - 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). - Endpoint
POST {id}/transitions(+ policyKeHoachKyKet.Update). - 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). - 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 (🔴 có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,
SignedByUserIdngườ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 testPASS 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) trongContractSigningPlanApprovalTests:ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures(OR-of-N ca (d)) ·ApproveV2_NewStep_ResetsLevelOrderToOne(con-trỏ đôi) ·Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan(choke-pointAND 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.