16 KiB
sub-review-verdict-1 — reviewer ADVERSARIAL tấn công verdict LAI
Khung ghi trước khi đọc (lượt 1), nội dung append theo từng phép đo. Đối tượng công:
sub-invest-bch-1.md§1 — "b.13→21 TÁI DÙNG Contract V2 (không dựng mới, không hồi sinh LEGACY enum) · b.7→12 DỰNG MỚI". Ranh: read-only. Không sửa code-tree, không commit. Mọi số liệu đo lại từ đĩa.
0. Meta + phạm vi đo
- Run:
2026-07-27-S156-bch-post-ceo-flow - 3 file Read đầy đủ (đúng trần):
ContractPhase.cs(29 dòng) ·ContractWorkflowService.cs(443 dòng, đường thậtsrc/Backend/SolutionErp.Infrastructure/Services/— KHÔNG phảiApplication/Contracts/như spec ghi) ·CreateContractFromEvaluationFeatures.cs(203 dòng). - 4 lệnh grep xác minh (không phải Read — dùng để chấm claim của invest trỏ ra ngoài 3 file, đúng mandate "đo, đừng tin"): fallback-policy ·
WorkflowPolicyRegistryconsumers ·ChoDuyettrongContractFeatures.cs·AuthorizetrongContractsController.cs. - LENS-VERDICT: LUNG-LAY — 11 điểm (chi tiết §5-§6). Lõi verdict SỐNG, nhãn "TÁI DÙNG" SAI CHI PHÍ, 1 sub-claim SAI SỰ THẬT trên đĩa.
1. Q1 — "phase bị gỡ = trạm của b.13→21": ĐỨNG, và đứng vững hơn cả cách invest chứng
Kết luận: KHÔNG phải trùng hợp tên gọi. Đây là tương ứng CẤU TRÚC — chứng bằng ACTOR, không bằng tên.
Invest chỉ trích ContractPhase.cs:3-13 (comment). Chứng mạnh hơn nằm ở bảng vai còn sống trong ContractFeatures.cs, invest KHÔNG dùng:
| Legacy phase (đã gỡ) | Vai được map trên đĩa | Trạm sơ đồ | Khớp? |
|---|---|---|---|
DangGopY=3 |
ContractFeatures.cs:366 ProjectManager+Procurement+CostControl+Finance+Accounting+Equipment |
b.14 BCH bổ sung/góp ý | ✅ |
DangDamPhan=4 |
:367 Drafter, DeptManager, ProjectManager |
b.15 đàm phán TP | ✅ |
DangInKy=5 |
:368 Drafter, DeptManager, ProjectManager |
b.16 ký nháy / in ký | ✅ |
DangKiemTraCCM=6 |
:369 CostControl |
b.17 CCM kiểm tra | ✅ |
DangTrinhKy=7 |
:370 Director, AuthorizedSigner |
b.18 CEO ký | ✅ |
DangDongDau=8 |
:371 HrAdmin |
b.19 HR đóng dấu | ✅ |
DangChon=1 |
(không map trong PhaseActorRoles) |
TIỀN-13 (chọn NCC = PE) | ❌ ngoài dải |
Chuỗi vai CostControl → Director/AuthorizedSigner → HrAdmin (:369-371) trùng khít chuỗi CCM→CEO→HR của b.17→18→19. Xác suất trùng ngẫu nhiên 3 vai liên tiếp đúng thứ tự ≈ 0 ⇒ hệ thống ĐÃ TỪNG mô hình hoá đúng dải này rồi gỡ. Lập luận invest ĐỨNG.
3 khiếm khuyết về độ chính xác (không lật kết luận):
- Trích dẫn nống (điểm 2). Invest §0:18-21 viết "
ContractPhase.cs:3-13ghi thẳng" rồi gán cho dải đó cảCurrentApprovalLevelOrder+ "workflow V2 admin-config (ApprovalWorkflowsSteps=Phòng > Levels=Cấp OR-of-N)". Đọc thậtContractPhase.cs:3-13: chỉ có state-machine 5 trạng thái + câu "LEGACY values (...) deprecated post-Mig 21 — giữ enum cho data cũ". Không một chữ nào về V2 / ApprovalWorkflows / Cấp / OR-of-N;CurrentWorkflowStepIndexnằm ở:25(ngoài dải 3-13),CurrentApprovalLevelOrdernằm ở file KHÁC (ContractWorkflowService.cs:84), mô tả Steps/Levels ở:204-216. File chỉ nói ĐÃ GỠ, không nói GỠ ĐỂ THAY BẰNG GÌ — phần "vì sao" là suy luận của invest, không phải trích dẫn. - Lệch biên dải (điểm 3). Mệnh đề "các trạm của bước 13→21 chính là những phase đã bị gỡ" chỉ đúng cho b.14→19. b.13 chạy trên
DangSoanThao=2(CÒN SỐNG,ContractPhase.cs:17) và b.20-21 chạy trênDaPhatHanh=9(CÒN SỐNG,:24). Chính §3 của invest map đúng (khúc 2 và khúc 4) — tức §0 phát biểu rộng hơn §3 tự đo. Mâu thuẫn nội bộ nhẹ. - "Đi ngược kiến trúc" = quy ước, KHÔNG phải rào chắn (điểm bổ sung tại §4-5). Không có gì trong mã CẤM gán lại legacy:
ContractWorkflowService.cs:191-199nhánh admin override setcontract.Phase = targetPhasebất kỳ, không lọc legacy. Hồi sinh enum là rẻ mà dở, không phải bất khả. Nếu owner hỏi "sao không dùng lại?", câu trả lời phải là chi phí vận hành (2 mô hình song song), không phải "hệ thống chặn".
2. Q2 — ApproveV2Async gánh được CCM→CEO→HR không?
Trả lời: gánh được KHUNG (thứ tự trạm), KHÔNG gánh được ĐƯỜNG SỐNG (nhìn thấy / được gọi / rẽ nhánh theo tiền).
2.1 Cái CÓ (invest đúng)
- 3 trạm = 3 Step:
:234steps = aw.Steps.OrderBy(Order),:246group Levels theo Order = Cấp,:259-260OR-of-N matchApproverUserId. - Tiến trạm:
:355-361(cấp kế trong Bước) →:365-392(Bước kế, Cấp 1) →:366-385terminalDaPhatHanh+ gen mã. - Gen mã không đúp cho HĐ sinh từ phiếu:
:371if (string.IsNullOrEmpty(contract.MaHopDong))— HĐ từ phiếu đã có mã tạiCreateContractFromEvaluationFeatures.cs:113⇒ giữ nguyên. ✅ - UPSERT ý kiến cấp duyệt
:292-316+SignedByUserId:306/:314. ✅
2.2 Cái THIẾU — 5 cơ chế, trong đó 3 cái invest KHÔNG nêu
| # | Thiếu gì | Bằng chứng | Hệ quả cho b.17→19 | Invest có nêu? |
|---|---|---|---|---|
| a | Rẽ nhánh CEO theo GiaTri | grep CeoApprovalThreshold|AllowApproverFinalize trên ContractWorkflowService.cs = 0 hit |
b.18 ">5 tỷ mới CEO ký" không tự động được | ✅ có (§2 + Q5) |
| b | Tín hiệu bàn giao trạm kế | :407 if (contract.DrafterUserId is Guid drafterId && drafterId != actorUserId) — notify chỉ Drafter |
CCM duyệt xong, CEO không được báo | ✅ nêu §2-bis, ❌ không hạ cấp verdict |
| c | Người duyệt XEM được phiếu | grep ChoDuyet src/.../ContractFeatures.cs = 0 hit; view-guard :343-349 map 100% legacy |
CCM/CEO/HR mở HĐ đang chờ chính mình → chặn | ✅ nêu §2-bis, ❌ không hạ cấp verdict |
| d | PMH trình được HĐ | :70-79 — trình DangSoanThao/TraLai → ChoDuyet đòi role Drafter HOẶC DeptManager; role Procurement không nằm trong đó → ForbiddenException :77 |
b.13/b.17 "PMH trình ký" 403 nếu PMH chỉ mang role Procurement | ❌ BỎ SÓT |
| e | Guard cho Trả lại / Từ chối | :49-66 nhánh Reject nằm TRƯỚC MỌI kiểm tra: không check role, không check approver-hiện-tại, không check fromPhase. Handler chỉ chặn chưa-đăng-nhập (ContractFeatures.cs:224 UnauthorizedException), controller chỉ [Authorize] trần (ContractsController.cs:13, 0 per-action policy) |
Mọi user đã đăng nhập POST reject → set HĐ bất kỳ, phase bất kỳ kể cả DaPhatHanh terminal → TuChoi/TraLai + SlaDeadline=null |
⚠️ invest chỉ ghi "không guard theo cấp" — nhẹ hơn sự thật nhiều |
⇒ Trả lời độc ác cho Q2: chuỗi CCM→CEO→HR chạy được trên bàn thí nghiệm (admin bấm, hoặc user được pin đúng Level và tự mò vào URL), không chạy được trong vận hành — người duyệt không thấy phiếu (c), không được báo (b), người trình có thể bị chặn (d), và bất kỳ ai cũng có thể giết phiếu (e). Nhãn "TÁI DÙNG — không dựng mới" do đó sai về chi phí: tối thiểu 4 hạng mục phải wire mới (visibility, inbox, notify-approver-kế, guard reject) trước khi 17→19 dùng được, chưa kể (a).
3. Q3 — b.15-16 map vào attachment: mất 3 vết, invest chỉ bắt 1
Invest bắt: AttachmentPurpose không phân biệt ký-nháy vs ký-chính (Q12). Đúng nhưng là cái nhẹ nhất. Hai cái nặng hơn bị bỏ:
- Mất TRẠNG THÁI máy-đọc-được. Upload attachment KHÔNG đổi
Phase; suốt b.13→16 HĐ đứng nguyênDangSoanThao. Hệ thống không phân biệt được "chưa soạn" / "đang đàm phán" / "chờ TP ký nháy" / "đã ký nháy, chờ trình". Hệ quả dây chuyền: Q11 (gate lane ⑥ "duyệt mẫu xong mới ký HĐ") mất luôn đối tượng để chấm — muốn chặn cứng thì phải có cờ trạng thái, mà mô hình attachment không sinh ra cờ nào. Enum cũ (DangDamPhan/DangInKy) từng cho đúng thứ đó. - Mất ĐỒNG HỒ SLA đúng chỗ sơ đồ đặt.
ContractWorkflowService.cs:31-32GetPhaseSlatrả 7 ngày CHỈ choChoDuyet,nullcho mọi phase khác. Khúc b.13→16 nằm ngoàiChoDuyet⇒ chỉ có 1 deadline tĩnh cấp lúc tạo (CreateContractFromEvaluationFeatures.cs:109-110, fallback 7 ngày), và:62Trả lại setSlaDeadline = null⇒ sau 1 lần trả lại thì HĐ hết đồng hồ vĩnh viễn. Mà chính khúc này sơ đồ ghi "BCH 3 ngày" + "13→18 hết 7-10 ngày". Invest Q10 chỉ hỏi SLA per-step trong V2 — bỏ đúng khúc mà sơ đồ ghi số. - Mất định danh chữ ký (invest đã bắt — Q12): ai ký nháy, ngày nào, bản nào — chỉ còn tên file + comment tự do.
⇒ Mapping "trạm offline → attachment" hợp triết lý Mig 21 (đúng như invest nói) nhưng không miễn phí: đổi 3 thứ đo được lấy 1 thứ linh hoạt. Verdict nên khai giá này, không nên trình bày như tương đương.
4. Q4 — nhánh invest BỎ QUA
4.1 🔴 Sub-claim SAI SỰ THẬT (nặng nhất). Invest §1:51-55 (Điểm LAI thứ 3): "HĐ sinh từ phiếu sẽ rơi nhánh V1 legacy (hardcoded policy fallback nếu V1 không có active def)".
Đo trên đĩa: grep "WorkflowPolicyRegistry|WorkflowTypeAssignment" trên ContractWorkflowService.cs = 0 hit. Đường duyệt thật: :98 không có ApprovalWorkflowId → V1 → :108-113 nạp WorkflowDefinitions → :115-116 if (def == null || def.Steps.Count == 0) throw new ConflictException("HĐ chưa pin workflow definition hoặc workflow không có step.").
⇒ KHÔNG có fallback nào. Nếu activeWfId null tại CreateContractFromEvaluationFeatures.cs:68-71 (không có V1 def active cho ContractType đó) thì HĐ sinh ra trình được nhưng KHÔNG AI DUYỆT ĐƯỢC — 409 ngay lần approve đầu, HĐ kẹt cứng ở ChoDuyet. Đây là hỏng cứng, không phải "degrade êm" như invest mô tả. (WorkflowPolicyRegistry chỉ còn sống ở ContractFeatures.cs:448-455 và DbInitializer.cs:720 — xem 4.4.)
4.2 Bỏ sót đường bỏ-qua-CEO ĐÃ WIRE. Q5 của invest liệt 3 phương án cho ngưỡng b.18 và khẳng định "cơ chế ngưỡng phía HĐ CHƯA TỒN TẠI". Đúng cho tự động theo tiền, nhưng thiếu phương án (d) 0 dòng code BE: ContractWorkflowService.cs:318-352 skipToFinal + matchingLevel.AllowApproverSkipToFinal (:322) cho phép CCM ở Bước 1 nhảy thẳng tới Bước cuối/Cấp cuối (:339-340) — tức bỏ qua Step CEO, HR vẫn duyệt thật để về DaPhatHanh. Cơ chế này Contract V2 ĐÃ CÓ (khác AllowApproverFinalize vốn PE-only). Nhược: thủ công (người bấm), không tự chấm theo 5 tỷ. Phải đưa vào bảng phương án để owner cân.
4.3 Bỏ sót gate vai người TRÌNH — xem §2.2 mục (d), :70-79.
4.4 Bỏ sót lệch DISPLAY ⟂ GUARD. ContractFeatures.cs:448-449 vẫn dựng policy hiển thị bằng WorkflowPolicyRegistry.FromDefinition(def) / .ForContract(c) và :455 .ForContractWithOverrides(...) — tức phần trả về FE (workflow.nextPhases) sinh từ mô hình legacy 9-phase, trong khi TransitionAsync chỉ chấp nhận mô hình phẳng V1-step/V2-level. Đây là lớp lỗi "FE bày nút mà BE không nhận" (hoặc ngược lại) — cần đo trước khi hứa "tái dùng UI sẵn có" cho b.17→19.
4.5 Bỏ sót hệ quả multi-winner ở khúc duyệt. CreateContractFromEvaluationFeatures.cs:145 pe.ContractId = contracts[0].Id + guard :59-60 if (pe.ContractId is not null) throw ⇒ (i) N HĐ liên-danh phải chạy N chuỗi duyệt độc lập (CEO ký N lần cho 1 gói thầu) — nhân đôi tải cho b.17→19 mà §3 bảng không ghi; (ii) cầu không tái chạy được: thêm winner sau khi đã tạo HĐ là bất khả qua API này. Q4 của invest bắt đúng phần ngưỡng per-HĐ vs per-gói, nhưng bỏ phần tải vận hành + khoá idempotent.
4.6 (ủng hộ invest — đo lại thấy ĐÚNG). 2 claim nặng nhất của invest đứng vững: CeoApprovalThreshold 0-hit phía HĐ ✅; ChoDuyet 0-hit trong ContractFeatures.cs ✅ (⇒ view-guard :343-349 + inbox :365-371 legacy-only là THẬT). Cầu pin V1 :108 WorkflowDefinitionId = activeWfId, 0 tham số V2 ✅.
5. Bảng điểm công
| # | Điểm | Mức | Ảnh hưởng verdict |
|---|---|---|---|
| 1 | "hardcoded policy fallback" SAI — thật là ConflictException :115-116, HĐ kẹt cứng |
🔴 HIGH | nâng nặng "Điểm LAI thứ 3" từ nên sửa → chặn |
| 2 | Trích dẫn nống ContractPhase.cs:3-13 (V2/Cấp không nằm trong dải) |
🟡 MED | không lật, hạ độ tin cite |
| 3 | Lệch biên: chỉ b.14→19 mới là phase-đã-gỡ (13, 20-21 dùng phase sống) | 🟢 LOW | không lật |
| 4 | Q1 dưới-chứng: bỏ bảng vai :347-349/:369-371 = chứng mạnh nhất |
🟡 MED | củng cố verdict (invest có lợi) |
| 5 | Notify chỉ Drafter :407 — đo đúng nhưng không hạ cấp nhãn "tái dùng" |
🟡 MED | sai chi phí |
| 6 | Bỏ sót: gate role trình :70-79 chặn PMH thuần Procurement |
🔴 HIGH | thêm việc bắt buộc cho b.13/17 |
| 7 | Bỏ sót: Reject :49-66 0-guard + [Authorize] trần ⇒ mọi user giết HĐ mọi phase (kể cả DaPhatHanh) |
🔴 HIGH | lỗ bảo mật, phải vá trước khi mở luồng |
| 8 | Bỏ sót: mất đồng hồ SLA khúc 15-16 (:31-32 + :62) |
🟡 MED | Q10 hỏi thiếu chỗ |
| 9 | Bỏ sót: mất trạng thái máy-đọc-được ⇒ Q11 gate cứng vô đối tượng | 🟡 MED | ràng buộc thiết kế |
| 10 | Bỏ sót: AllowApproverSkipToFinal :318-352 = phương án (d) 0-code cho b.18 |
🟡 MED | thiếu lựa chọn cho owner |
| 11 | Bỏ sót: display dựng từ WorkflowPolicyRegistry (ContractFeatures.cs:448-455) ⟂ guard phẳng |
🟡 MED | rủi ro FE khi tái dùng |
3 HIGH · 7 MED · 1 LOW = 11 điểm.
6. LENS-VERDICT
LUNG-LAY (không BÁC).
- Giữ: thế chia đôi
b.7→12 dựng mới ⟂ b.13→21 tái dùng Contractsống. Q1 đứng, và đứng chắc hơn cách invest chứng (bảng vaiContractFeatures.cs:347-349,:369-371khớp CCM→CEO→HR). - Lung lay: cụm chữ "TÁI DÙNG (không dựng mới)" sai về chi phí. Đúng hơn: "tái dùng KHUNG duyệt V2, nhưng phải WIRE MỚI 4 đường sống (visibility
ChoDuyet· inbox · notify approver kế · guard reject) + 1 rẽ nhánh tiền (b.18) trước khi 17→19 vận hành được". Nếu owner đọc "tái dùng" thành "gần như không tốn công", ước lượng sẽ hụt. - Phải sửa trong bản invest trước khi trình owner: (i) bỏ chữ "hardcoded policy fallback" — thay bằng
ConflictException :115-116+ nâng "Điểm LAI thứ 3" lên mức CHẶN; (ii) thêm Q về vai người TRÌNH (:70-79); (iii) thêm cảnh báo bảo mật Reject:49-66; (iv) bổ sung phương án (d)AllowApproverSkipToFinalvào Q5; (v) siết lại câu §0 cho khớp §3 (b.14→19, không phải 13→21). - Không đề nghị sửa mã (ranh vai): trên đây là mô tả lỗi + tiêu chí nghiệm thu, thực thi thuộc em main / implementer.
END · TOTAL=11 DIEM