15 KiB
c3-l3
Lane 3/3 review cụm-3 (K7 bridge 8 việc/8 test + K8 dry-run 17 bước) — LENS ĐO-ĐƯỢC + THỨ TỰ.
Target: .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum3.md (viết tắt SPEC:)
Mọi neo dưới đây đã mở file trên đĩa xác nhận (không tin số của spec).
Findings
HIGH
H1 — Contracts.Create chưa được cấp cho vai nào ⇒ nút K7 chết + 403 câm; nhãn "đúng-key-endpoint-đích" sai tiền đề. [Q1 403-path · Q2]
- SPEC:54 đặt
[Authorize(Policy = "Contracts.Create")]và tự dán nhãn "đúng-key-policy-endpoint-đích (gotcha #85)"; SPEC:67 gate FEcan('Contracts','Create'); SPEC:122 B9 "Người cóContracts.Create". - Đo seeder:
DbInitializer.cs:2222nốiContractMenuKeys()(:2165-2178, phần tử đầu =MenuKeys.Contracts) vàoreviewKeys; vòng cấp:2251-2258ghiCanCreate = isPe(:2256) màIsPeKey("Contracts") = false(:2163) ⇒ CanCreate=false cho MỌI role. Nhánh key non-Pe là skip-existing (:2249-2250) ⇒ row prod cũ cũng không được nâng. Comment:2217-2218nói thẳng: "HĐ (Contracts+Ct_) … CanRead-only mọi role"*. - Đo endpoint đích:
POST /api/contractsKHÔNG có action-policy —ContractsController.cs:13class[Authorize]trần,:49 [HttpPost]trống (đối chứng dương::32cóContracts.Read) ⇒ "key của endpoint đích" là không có key; bridge sẽ CHẶT HƠN chính chỗ tạo HĐ. - Hệ quả đo được: B9 chỉ chạy được bằng Admin (bypass) ⇒ dry-run không chứng được luồng vai thật; và câu III (SPEC:56) "đổi answer chỉ đổi điều kiện hiện nút FE, máy BE không đổi" là sai — đổi answer phải đổi cả quyền được cấp.
- Đề nghị: (a) chọn 1 trong 2 —
[Authorize]trần + guard nghiệp vụ (bằng đích) HOẶC giữContracts.Createkèm việc cấp quyền tường minh; (b) B0 pre-flight thêmSELECT COUNT(*) FROM Permissions WHERE MenuKey='Contracts' AND CanCreate=1≥1; (c) test 2 chiều 200/403.
H2 — Gate thật là HỘI 2 policy (KeHoachKyKet.Read ∧ Contracts.Create); spec + FE chỉ khai 1 vế. [Q1 403-path]
ContractSigningPlansController.cs:28class[Authorize(Policy = "KeHoachKyKet.Read")](quy ước 2 tầng ghi ở:14-16); mọi action hiện hữu đềuKeHoachKyKet.*(:36,:84,:93,:103,:208).- ASP.NET Core AND 2 attribute ⇒ action mới đòi cả hai. SPEC:54/:56/:67 chỉ nói
Contracts.Create⇒ FE-BE lệch: ai cóContracts.Createmà thiếuKeHoachKyKet.Readsẽ thấy nút, ăn 403 câm (lớp #44). - Đề nghị: spec khai công thức hội; FE
can('KeHoachKyKet','Read') && can('Contracts','Create'); 1 test 403 cho từng vế thiếu.
H3 — T2 "gộp 2 line CÙNG NCC" BẤT KHẢ THI trên schema hiện tại; phụ-thuộc THẬT vào cụm-1 bị sót khỏi 3 lý do. [Q3]
ContractSigningPlanLineConfiguration.cs:22-24UNIQUE filtered(ContractSigningPlanId, SupplierId); entity commentContractSigningPlanLine.cs:9-11xác nhận ⇒ 1 plan KHÔNG thể có 2 line cùng NCC.- Máy OG-1 GỘP của K7 (
SPEC:31LineIds[],SPEC:41guard "tất cả cùng SupplierId",SPEC:76T2,SPEC:80T6) chỉ có nghĩa SAU khi unique bị bỏ.spec-cum1-chi-tiet-31-07-2026.md:16(vá-9) mới là chỗ khai: "unique cũ bị DROP trong chính Mig 71". - SPEC:154 liệt 3 lý do "K7 sau cụm-1" (TenHangMuc · phase-guard · ApprovedAmount) — không có lý do DROP-unique, tức lý do khiến T2 dựng được. Viết T2 trước Mig 71 = fixture vỡ unique.
- Đề nghị: thêm phụ thuộc (iv) + acceptance "sau Mig 71: index
(Plan,Supplier)không cònis_unique=1" chạy TRƯỚC khi khởi công T2/guard-3.
H4 — B16 rollback cho HĐ là NO-OP: về TuChoi vẫn KHÔNG xóa được; neo citation sai chỗ. [Q4]
ContractFeatures.cs:780if (entity.Phase >= ContractPhase.DangInKy) throw ConflictException(...)— không phải:632-633như SPEC:130 ghi (vùng:625-638là authz của Detail).ContractPhase.cs: DangInKy=5 (:20), DaPhatHanh=9 (:24), ChoDuyet=10 (:25), TraLai=98 (:26), TuChoi=99 (:27) ⇒ mọi phase ≥5 bị chặn, kể cả TuChoi.- SPEC:136 B16 đặt "HĐ kẹt ChoDuyet → admin Reject→TuChoi" song song với vế KHKK ("TuChoi → xóa được qua UI
:1145" — vế KHKK ĐÚNG, verifiedContractSigningPlanFeatures.cs:1145-1148allow-list nằm SAU nhánh admin:1138-1140) ⇒ người đọc suy ra tính đối xứng không tồn tại. - Cửa sổ dọn sạch DUY NHẤT của HĐ = Phase
DangSoanThao(2) < 5, tức trước B10 (trình). - Đề nghị: B16 ghi rõ "HĐ đã trình = bản ghi vĩnh viễn, chỉ soft-delete bằng sqlcmd" + đánh dấu abort-point cuối cùng còn dọn sạch = ngay sau B9.
MED
M5 — Rollback thiếu 2 loại bản ghi + lệnh dọn không chạy được như-là. [Q4]
- SPEC:134 chỉ nêu
pe.ContractId.line.ContractId— chính write-site MỚI của K7 (SPEC:50) — không có lệnh dọn ⇒ sau soft-delete HĐ, line vẫnContractId != null⇒ guard 5 (SPEC:43) khoá line vĩnh viễn + badge "Đã có HĐ" trỏ HĐ đã xóa. - B12 (SPEC:125) upload scan bản cứng ⇒ sinh row
ContractAttachments+ file vật lý; B15 (SPEC:129-134) không liệt. - SPEC:131 viết
UPDATE ContractSigningPlans SET IsDeleted=1,…— dấu "…" + thiếu WHERE ⇒ không chạy được; và B1/B9 chỉ chụp MÃ (SPEC:114/:122), không có bước ghi lại Guid để dựng WHERE. - (Đối chứng dương: đường thoát "soft-delete giải phóng slot" ĐÚNG — comment
ContractSigningPlanFeatures.cs:338-340xác nhận global filter loại phiếu xoá mềm khỏi ràohasLivePlan:342-347.)
M6 — Acceptance A1 PASS được với impl ghi ĐÈ CẢ PLAN. [Q2]
- SPEC:86 chỉ
SELECT ContractId ... WHERE Id IN (…). Impl setContractIdcho mọi line của plan vẫn ra "đúng N dòng". Thiếu vế bù:SELECT COUNT(*) WHERE ContractSigningPlanId=@p AND ContractId=@c== N. - T1/T2 (SPEC:75-76) cũng không assert "line KHÔNG chọn giữ
ContractIdnull".
M7 — "1 SaveChanges cuối" tự mâu thuẫn trong 1 đoạn ⇒ impl 2-save vẫn PASS cả 8 test. [Q2]
- SPEC:50: "sau khi Add contract + SaveChanges chung 1 lần:
line.ContractId = ..." rồi kết "Atomic: 1 SaveChanges cuối". Vế đầu đọc ra 2 lần save ⇒ khe hở: HĐ tồn tại mà line chưa gắn (line vẫn bridge được lần nữa). - Không test nào bắt: T3 (SPEC:77) chỉ chứng atomic ở nhánh guard (fail TRƯỚC mọi save).
- Khuôn có sẵn câu chuẩn để copy nguyên văn:
CreateContractFromEvaluationFeatures.cs:76-79.
M8 — Baseline test bỏ ngỏ + dấu "≥" không khoá được việc xoá test. [Q1]
- SPEC:92 (A7) và SPEC:164: "≥ baseline+8 (khai k con số)". Baseline canonical có sẵn:
docs/STATUS.md:472= 590 PASS (45 Domain + 545 Infra, đo tươi @S162). - Nhưng K7 land sau cụm-1 (SPEC:156) mà cụm-1 tự khai "~5 test mới" (
spec-cum1:27) ⇒ baseline của K7 ≠ 590 và spec không nói đo ở commit nào. "≥" cho phép xoá 1 test cũ + thêm 9 vẫn xanh. - Đề nghị: ghi 2 số đo tươi (trước/sau) trên đúng commit rebase, dùng
==, kèm 8 tên test.
M9 — Danh sách "wave phải land trước K8" thiếu K1 + K2; B0 không đếm danh mục. [Q3]
- SPEC:157 liệt K4/K5/K6/K3/K7. Nhưng B2 (SPEC:115) chạy "gán hạng mục B1-xx" = danh mục K1 (86 row) và bị "submit-guard vá-5" = K2; B8 (SPEC:121) dựa "phase-guard vá-6" = K2.
- B0 (SPEC:110) verify workflow + PE nhưng không đếm danh mục trên prod ⇒ dry-run chết ở B2 nếu seeder danh mục chưa chạy/bị gate (bài #75/#76: seed gated ≠ migration).
- (Đối chứng dương — hạ mức: migration không cần verify tay,
DbInitializer.cs:64 await db.Database.MigrateAsync()tự apply lúc khởi động API. Chỉ SEED mới cần đếm.)
M10 — Bộ bằng chứng K8 tự mâu thuẫn với nhánh mặc định + B0 thiếu 2/5 bằng chứng. [Q2]
- SPEC:105 chốt "nhánh chính = A"; B7 (SPEC:120) là "chỉ nhánh B" nhưng vẫn mang số
ss-k8-07; checklist SPEC:167 đòi "ss-k8-01..13" ⇒ chạy đúng default sẽ thiếu 07 vĩnh viễn ⇒ checklist hoặc luôn FAIL hoặc bị bỏ qua (bỏ qua = mất răng). - Bằng chứng B0 (SPEC:110) chỉ "ảnh backup + sqlcmd 3 dòng" — không có bằng chứng cho (iv) dặn team và (v) trả lời câu giờ-G, dù checklist SPEC:166 đòi đủ 5 mục.
- Ghi nhận dương: B1-B13 mỗi bước có
ss-k8-NNriêng ⇒ KHÔNG dồn cuối (đúng yêu cầu).
M11 — Thiếu 3 ca test đo được: đua-2-tab · Σ null-mix · nhánh ??=. [Q1]
- (a) Double-POST đồng thời: index
ContractIdnon-unique (ContractSigningPlanLineConfiguration.cs:26), guard 5 đọc-rồi-ghi không transaction ⇒ 2 HĐ + cháy 2 seq (seq không thu hồi được, SPEC:132). Khuôn FE cũ chỉ chặn double-click 1 tab (PeDetailTabs.tsx:2209 disabled={mut.isPending}) và SPEC:68 không nhắc. Đề nghị: re-check trong transaction (KHÔNG đặt unique trênContractId— sẽ cấm luôn máy gộp N line/1 HĐ), hoặc khai thẳng "không chống được đua đa-tab". - (b) Σ null-mix: T3 (SPEC:77) là "1 line ApprovedAmount=null" đơn lẻ; thiếu ca 2 line {có, null} trong CÙNG call ⇒ impl
Sum(x => x.ApprovedAmount ?? 0)bỏ guard vẫn xanh. - (c)
pe.ContractId ??=(SPEC:52): 0 test cho nhánh pe đã có ContractId (liên-danh / bridge cũ) — đúng chỗ spec tự khai hố W9. Không khoá ⇒ lần sau ai đổi??=thành=(đè) sẽ không ai biết.
LOW
L12 — Đếm test không khớp trong cùng file: tiêu đề SPEC:71 "7 case" · bảng SPEC:73-82 có 8 dòng T1-T8 · SPEC:96 V7="T1-T7" + V3=T8 · SPEC:164/:171 "8 test". Sửa tiêu đề → "8 (7 mới + 1 khóa)" kẻo implementer làm 7.
L13 — Citation lệch: SPEC:130 ghi ContractFeatures.cs:632-633 cho guard xoá HĐ; thực tế :780. Nội dung guard đúng, neo sai (xem H4).
L14 — Đếm file test sai (ảnh hưởng H1/H2): SPEC:14 nói "4 file SigningPlan (Approval/Crud/Schema/AuthorizePolicy)"; đĩa chỉ có 3 — tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs, .../Application/ContractSigningPlanCrudTests.cs, .../Common/ContractSigningPlanSchemaTests.cs. File authz duy nhất là tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs (S118, không có ca SigningPlan) ⇒ khuôn test-403 mà spec ngầm dựa vào không có sẵn cho controller này. (Kết luận "0 test bridge" của SPEC:14 vẫn ĐÚNG.)
L15 — Field-map bridge thiếu so với khuôn: SPEC:46 không mang SlaDeadline (khuôn cũ CreateContractFromEvaluationFeatures.cs:111 có set), NoiDung, DraftData. Bán kính nhỏ vì ContractWorkflowService.cs:99 set lại SLA lúc trình ⇒ chỉ draft-trước-trình trống SLA. Nên khai "cố ý bỏ + lý do" thay vì im (§① đòi khai chỗ khác khuôn).
L16 — [Q5] Khuôn dialog CÓ SẴN không được trỏ tới ⇒ nguy cơ chế lại lần 3.
- Khuôn thật:
fe-user/src/components/pe/PeDetailTabs.tsx:2202-2237(+ mirrorfe-admin/...:2202-2237) = dialog "Tạo HĐ từ phiếu Duyệt NCC":Dialog+Label/SelecttypeOptions7 enum (:2193-2201) +Inputtên HĐ + footer Hủy/Tạo +toast+navigate('/contracts/{id}')+disabled={mut.isPending}. - SPEC:68 chỉ trỏ "mirror dropdown ContractCreatePage" ⇒
typeOptions7 enum sẽ bị chép lần thứ 3 (hiện là hằng cục bộ trong PeDetailTabs, không export). - 3 phần CHẾ THÊM có lý do chính đáng (radio chọn line UI-1-1 · Select workflow V2 · badge "Đã có HĐ") nhưng spec không có bảng "khác khuôn ở đâu + vì sao" như §① đòi.
- Đối chứng dương (không phải lỗi): nguồn dropdown workflow KHÔNG dính #44 —
ApprovalWorkflowsV2Controller.cs:18class[Authorize]trần,[HttpGet]:21không có action-policy (Workflows.Createchỉ từ:29trở đi) ⇒ mọi user đăng nhập list được workflowApplicableType=3.
L17 — Phân tích rebase trỏ nhầm file: SPEC:156 nói K7 "thêm code vào ContractSigningPlanFeatures.cs" trong khi SPEC:27 khai file MỚI CreateContractFromSigningPlanFeatures.cs. File đụng thật với cụm-1 = ContractSigningPlansController.cs (+ KhkkDetailPage.tsx ×2 — vế này spec ghi đúng).
Neo đã verify ĐÚNG (control dương — chống "bác cũng phải có chứng")
ContractSigningPlanLine.cs:23 (ApprovedAmount "CHỈ ghi tại choke-point finalize") · :25 (ContractId "[C6 review-schema]") · LineConfiguration.cs:26 index non-unique · ContractSigningPlanFeatures.cs:67 DTO Guid? ContractId · :209-220 EnsureWorkflowTypeAsync trọn 2 nhánh · :330-332 rào phase · :342-347 rào 1-PE-1-plan-sống · :1145-1148 allow-list SAU nhánh admin (HỐ-1-KHKK CÓ THẬT) · ContractWorkflowService.cs:97 CurrentApprovalLevelOrder = AWId is not null ? 1 : null · :111-113 dispatch V2 · :384 if (string.IsNullOrEmpty(contract.MaHopDong)) chống double-gen · ContractFeatures.cs:381-387 comment CẤM nhét ChoDuyet vào GetEligiblePhases (⇒ khuyến nghị "KHÔNG SỬA" của SPEC:17/:162 là ĐÚNG) · ContractFeatures.cs:74-86 guard ApplicableType=Contract (khuôn cho T7) · fe-{user,admin}/src/pages/khkk/KhkkDetailPage.tsx tồn tại cả 2 app, khối actions PageHeader ở :290-318.
Verdict
FAIL-cần-vá-trước-khi-build — 4 HIGH đều là lỗi đo được, không phải khẩu vị:
H1 quyền Contracts.Create = 0 vai được cấp (seeder CanCreate=isPe) ⇒ nút chết/403 ngoài Admin;
H2 gate thật là hội 2 policy, spec+FE khai 1 vế;
H3 T2/guard-gộp bất khả thi tới khi Mig 71 DROP unique — phụ thuộc này KHÔNG có trong danh sách lý do;
H4 rollback HĐ là no-op (numeric guard chặn cả TuChoi).
Phần đúng: bằng chứng K8 per-bước (không dồn cuối), HỐ-1-KHKK có thật, việc "KHÔNG sửa GetEligiblePhases" là đúng, dropdown workflow không dính #44.
END sub-reviewer-c3-l3 — TOTAL: 17 finding (4 HIGH · 7 MED · 6 LOW)