Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c3-l3.md
2026-07-31 14:00:32 +07:00

15 KiB
Raw Blame History

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 FE can('Contracts','Create'); SPEC:122 B9 "Người có Contracts.Create".
  • Đo seeder: DbInitializer.cs:2222 nối ContractMenuKeys() (:2165-2178, phần tử đầu = MenuKeys.Contracts) vào reviewKeys; vòng cấp :2251-2258 ghi CanCreate = 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-2218 nói thẳng: "HĐ (Contracts+Ct_) … CanRead-only mọi role"*.
  • Đo endpoint đích: POST /api/contracts KHÔNG có action-policyContractsController.cs:13 class [Authorize] trần, :49 [HttpPost] trống (đối chứng dương: :32Contracts.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.Create kèm việc cấp quyền tường minh; (b) B0 pre-flight thêm SELECT 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.ReadContracts.Create); spec + FE chỉ khai 1 vế. [Q1 403-path]

  • ContractSigningPlansController.cs:28 class [Authorize(Policy = "KeHoachKyKet.Read")] (quy ước 2 tầng ghi ở :14-16); mọi action hiện hữu đều KeHoachKyKet.* (: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.Create mà thiếu KeHoachKyKet.Read sẽ 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-24 UNIQUE filtered (ContractSigningPlanId, SupplierId); entity comment ContractSigningPlanLine.cs:9-11 xác nhận ⇒ 1 plan KHÔNG thể có 2 line cùng NCC.
  • Máy OG-1 GỘP của K7 (SPEC:31 LineIds[], SPEC:41 guard "tất cả cùng SupplierId", SPEC:76 T2, SPEC:80 T6) 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òn is_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:780 if (entity.Phase >= ContractPhase.DangInKy) throw ConflictException(...)không phải :632-633 như SPEC:130 ghi (vùng :625-638 là 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, verified ContractSigningPlanFeatures.cs:1145-1148 allow-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ẫn ContractId != 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 (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-340 xác nhận global filter loại phiếu xoá mềm khỏi rào hasLivePlan :342-347.)

M6 — Acceptance A1 PASS được với impl ghi ĐÈ CẢ PLAN. [Q2]

  • SPEC:86 chỉ SELECT ContractId ... WHERE Id IN (…). Impl set ContractId cho 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ữ ContractId null".

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-NN riê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 ContractId non-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ên ContractId — 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ó 3tests/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 (+ mirror fe-admin/...:2202-2237) = dialog "Tạo HĐ từ phiếu Duyệt NCC": Dialog + Label/Select typeOptions 7 enum (:2193-2201) + Input tên HĐ + footer Hủy/Tạo + toast + navigate('/contracts/{id}') + disabled={mut.isPending}.
  • SPEC:68 chỉ trỏ "mirror dropdown ContractCreatePage"typeOptions 7 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:18 class [Authorize] trần, [HttpGet] :21 không có action-policy (Workflows.Create chỉ từ :29 trở đi) ⇒ mọi user đăng nhập list được workflow ApplicableType=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)