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

15 KiB

c3-l1

LANE 1/3 review cụm-3 — LENS QUYẾT-vs-CODE. Đối tượng: sub-invest-fable-b2-cum3.md (K7 bridge 8v/8test + K8 dry-run 17 bước B0-B16). Mọi verify = grep/read trên ĐĨA hôm nay (2026-07-31), không mượn số của lane khác. Đường dẫn gốc repo = D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP.

Findings

Bảng chấm 7 claim chịu lực (đề bài a-g)

# Claim của cụm-3 Phán Dòng grep/read
a 3 lỗ S156 ĐÃ vá @W6-S161 (list/detail · inbox · guard trình) ĐÚNG — và ĐỦ cả 3 chân cho HĐ sinh-từ-KHKK (F-1) ContractFeatures.cs:306-318, :621-637, :530-543, ContractWorkflowService.cs:73-92
b Cấm sửa GetEligiblePhases (nhét ChoDuyet = rò HĐ xoá-mềm sang màn "Đã xoá") ĐÚNG comment ContractFeatures.cs:381-387; call-site thứ 3 :441 ListContractsQueryHandler.GetEligiblePhases; :442-447 cố ý không ghép vế V2
c ContractSigningPlanLine.ContractId write-site = 0 ĐÚNG, và chắc hơn lời khai (F-3) grep repo-wide → 0 assignment
d HỐ-1-KHKK: allow-list {Nháp,TuChoi} SAU nhánh admin ⇒ DaDuyet không xoá nổi kể cả Admin ĐÚNG; hệ quả "chiếm slot" có 1 điều-kiện ẩn (F-5) ContractSigningPlanFeatures.cs:1137-1148; rào :342-347; filter ContractSigningPlanConfiguration.cs:44
e type-3 = any-ContractType ⇒ buộc thêm input ApprovalWorkflowId ĐÚNG; picker lọc còn 1 vế nữa spec không nêu (F-9) ApprovalWorkflow.cs:57; khuôn guard ContractFeatures.cs:74-88; picker ContractCreatePage.tsx:334-341
f pe.ContractId ??= giữ cây GĐ3 (hook đi pe.contractId đơn) ĐÚNG nhưng THIẾU 1 chiều (F-6) usePipelineStages.ts:158-160, :214-218 (2 app sha256 khớp 7144eeb787a8…)
g Mã HĐ gen lúc bridge mirror PE :113; terminal chống double-gen :384 ĐÚNG; spec thiếu 2 tiền-đề + 1 lệch-khuôn (F-7) CreateContractFromEvaluationFeatures.cs:113; ContractWorkflowService.cs:384-391

Tổng: 7/7 ĐÚNG về sự-kiện · 0 SAI · 4 claim hở ở phần SUY-LUẬN/hệ-quả · +2 lỗ mới không nằm trong 7 câu hỏi (F-2 policy, F-9 picker).


F-1 [PASS — control dương] BÁC-tiền-đề của cụm-3 đứng vững: 3 chân ĐỦ cho HĐ sinh-từ-KHKK

Giả định bridge pin ApprovalWorkflowId type-3 + DrafterUserId = người bấm:

  1. Approver THẤY — List ContractFeatures.cs:311-318: x.c.Phase != DangSoanThao && myWorkflowIds.Contains(x.c.ApprovalWorkflowId); helper :403-411 gom mọi workflow user có chân ApproverUserId. Detail :624-637: isV2Approver hỏi theo c.ApprovalWorkflowId (không theo role) + chặn Nháp :631-636.
  2. INBOX:532-534 v2InboxIds; where :543; thoát-sớm :538 xét CẢ 2 nguồn (user 0 role legacy vẫn có hộp thư). ResolveV2InboxIdsAsync:559-593 đòi Phase==ChoDuyet && AWId!=null && StepIndex!=null && LevelOrder!=null — trình sẽ set StepIndex=0 (ContractWorkflowService.cs:94) và LevelOrder=1 đúng vì ApprovalWorkflowId is not null (:97). Admin đi nhánh phase AdminInboxPhases:503-513 (đã có ChoDuyet).
  3. TRÌNH đượcContractWorkflowService.cs:82-92: isCreator xét CreatedBy || DrafterUserId ⇒ người bấm bridge luôn trình được dù không mang 3 role kia.

⇒ K7(b) co lại thành V3 test-khoá là kết luận đúng; "làm cho xong theo đề bài cũ" đúng là sẽ tạo lỗ.

F-2 [MEDIUM] "policy Contracts.Create = đúng-key-endpoint-đích (#85)" — sai về sự-thật đo được; FE-gate lệch server 2 chiều

Spec :54 :67 :161 :163 chốt key = Contracts.Create, viện gotcha #85. Đo:

  • Endpoint đích thật POST /api/contracts KHÔNG có policy: ContractsController.cs:13 class [Authorize] trần; action :49-50 trống. Y hệt List:16-17, Inbox:41-42, Get:45-46, Transition:64-65. Chỉ ListDeleted:31-32 mang [Authorize(Policy="Contracts.Read")]. ⇒ "đúng key policy của endpoint đích" theo nghĩa đen = [Authorize] trần. (Đúng lớp S162: 2 endpoint cùng module cho kết luận NGƯỢC nhau ⇒ phải đo TỪNG cái, không suy từ tên module.)
  • Gắn action lên ContractSigningPlansController thì policy CỘNG DỒN: class :28 [Authorize(Policy="KeHoachKyKet.Read")] + action Contracts.Create ⇒ hiệu lực = AND 2 policy, không phải "KHÔNG phải KeHoachKyKet.*" như spec :54 viết.
  • Hệ quả: (i) B9 :122 mô tả actor = "Người có Contracts.Create" là điều kiện THIẾU; (ii) chiều ngược nặng hơn — BCH drafter (trọn KeHoachKyKet.*) có thể không được cấp Contracts.Create ⇒ mất nút, trong khi chính người đó vẫn tạo HĐ tay được qua POST /api/contracts (0 policy). Gate chặt hơn năng-lực-thật = chặn nhầm, không phải bảo mật.

Đề nghị (mô tả, không sửa code): spec khai hiệu-lực KeHoachKyKet.Read AND Contracts.Create; B0 thêm 1 dòng kiểm actor B9 đủ CẢ HAI; và bỏ/viết lại câu viện #85 vì bằng chứng đang chỉ chiều ngược.

F-3 [PASS — chắc hơn lời khai] write-site = 0, xác nhận ở phạm vi RỘNG hơn spec đo

Spec :13 chỉ grep 2 file. Tôi grep repo-wide 2 lượt:

  • grep -rn "\.ContractId\s*=[^=]" src/Backend --include=*.csduy nhất CreateContractFromEvaluationFeatures.cs:145 pe.ContractId = contracts[0].Id (là PurchaseEvaluation.ContractId).
  • Mở rộng bắt object-initializer grep -rn "ContractId\s*=[^=]" → mọi hit còn lại thuộc ContractAttachment / ContractDetail* / ContractChangelog / ContractComment + DbInitializer + column-def migration; 0 hit gán cho ContractSigningPlanLine. ⇒ "assignment ĐẦU TIÊN toàn codebase" (:50) đúng. ContractSigningPlanWorkflowService.cs chỉ 1 hit :54 và đúng là COMMENT.

F-4 [LOW] Neo :381 trong chú thích của MÃ là neo tự-trỏ (không phải lỗi cụm-3)

ContractFeatures.cs:384 viết "…hàm DÙNG CHUNG với :381 ListDeleted", nhưng :381 chính là dòng mở khối chú thích đó; handler ở :427, call-site :441. Cụm-3 :17 trích đúng (:441). Nêu để implementer đọc mã không bị neo sai dẫn lạc.

F-5 [MEDIUM] HỐ-1-KHKK đúng, nhưng "chiếm slot (PE, nhóm-4)" là hệ quả CÓ ĐIỀU KIỆN — K2 chưa land thì thiệt hại RỘNG hơn

  • Cơ chế: :1137-1140 chặn không-owner/không-admin → :1145-1148 allow-list {DangSoanThao, TuChoi} không có ngoại lệ adminDaDuyet bất-khả-xoá qua API kể cả Admin. ĐÚNG.
  • Đường thoát sqlcmd soft-delete có căn cứ: ContractSigningPlanConfiguration.cs:44 HasQueryFilter(x => !x.IsDeleted) ⇒ rào :342-344 không đếm phiếu đã xoá mềm. ĐÚNG.
  • Lỗ suy luận: rào :342-344 trên đĩa HÔM NAY là AnyAsync(p.PurchaseEvaluationId == pe.Id && p.Phase != TuChoi)per-PE, KHÔNG có ApprovalGroup. B15 :131 viết "chiếm slot (A/049, nhóm-4)" = trạng thái GIẢ ĐỊNH sau K2. Nếu K8 chạy khi K2 chưa merge (hoặc K2 đổi phạm vi), phiếu ZZTEST DaDuyet khoá mọi nhóm của PE thật PE/2026/A/049, không riêng nhóm-4. Đề nghị: B0 thêm dòng kiểm điều-kiện "rào :342 đã là per-(PeId, ApprovalGroup)?" — nếu chưa thì hoãn hoặc chuyển vòng-1 sang bộ ZZTEST riêng. Đây là lý do KỸ THUẬT (không phải cảm tính) nghiêng câu giờ-G :140 về phương án bộ ZZTEST riêng.

F-6 [MEDIUM] pe.ContractId ??= — spec khai 2 chiều hệ quả, thiếu chiều thứ 3 (bridge cũ chạy TRƯỚC)

Hook xác nhận GĐ3 chỉ đi qua pe.contractId: usePipelineStages.ts:158-160 (chú thích) + :214-218 (for (const p of peItems) { if (!p.contractId …), 2 app byte-identical (sha256 7144eeb787a8ff8b…). Spec :52 liệt (i) khoá bridge cũ, (ii) liên-danh chỉ HĐ[0]. Thiếu: thứ tự NGƯỢC. Guard #5 của spec (:43) chỉ xét line.ContractId == null, không xét pe.ContractId ⇒ PE đã tạo HĐ bằng đường cũ (pe.ContractId != null) vẫn bridge tiếp bằng KHKK → ??= no-op im lặng ⇒ HĐ mới vô hình trên cây GĐ3 + 2 HĐ song song cho cùng gói, 0 cảnh báo. Đúng lớp "vắng-mặt trông giống ổn". Đề nghị: K7.1 phải chốt 1 trong 2 — chặn 409 ("PE này đã có HĐ tạo từ đường cũ"), hoặc cho phép nhưng bắt buộc changelog PE nêu "cây chỉ hiển thị HĐ đầu" + thêm case test T9. Không để ??= quyết định im lặng.

F-7 [LOW→MEDIUM] Gen mã lúc bridge: thiếu 2 tiền-đề của codegen, bỏ trống SlaDeadline, và WorkflowDefinitionId=null lệch CẢ HAI tiền lệ

  • codeGenerator.GenerateAsync(contract, project.Code, supplier.Code, ct) (CreateContractFromEvaluationFeatures.cs:113, ContractWorkflowService.cs:390) cần entity Project + Supplier để lấy Code. Bridge cũ nạp project :65-66 + supplierMap :63-64 kèm 2 NotFoundException (:66, :85-86). Khối "Build Contract (map field)" spec :46 chỉ nêu ProjectId/SupplierIdkhông nêu bước nạp + 2 guard NotFound.
  • SlaDeadline vắng trong field-map (bridge cũ set :109-110; create tay set ContractFeatures.cs:110). Không chí mạng (trình sẽ set +7d ContractWorkflowService.cs:98) nhưng HĐ ở Nháp trống SLA — khác HĐ tạo tay; nên khai là CỐ Ý.
  • WorkflowDefinitionId = null (spec :46) lệch cả 2 tiền lệ: bridge cũ pin V1 :108; create tay pin CẢ HAI với chú thích tường minh ContractFeatures.cs:105-109 "Cả 2 set ok — Service ApproveV2Async branch dispatch theo ApprovalWorkflowId trước". Hệ quả đo được: GetContractQueryHandler:647-662 khi WorkflowDefinitionId == null rơi về WorkflowPolicyRegistry.ForContractWithOverrides (policy hardcode) ⇒ khối workflow trong DTO detail của HĐ sinh-từ-KHKK khác HĐ V2 tạo tay. Spec đang phá luật tự đặt ("GIỐNG NHAU — CHỈ KHÁC FORM — ĐỪNG CHẾ THÊM" :25) mà không chứng "FE V2 không đọc workflow.nextPhases".
  • Chốt nhỏ: guard :384 thực tế là string.IsNullOrEmpty(contract.MaHopDong) (spec ghi is null) — mạnh hơn, không sai nghĩa.

F-8 [LOW] Nhãn số trong chính spec lệch: "7 case" vs bảng 8 dòng

:71 "K7.4 (d) Test — 7 case" nhưng bảng :75-82T1…T8 = 8 dòng; :92 A7 "baseline+8", :96 liệt V1-V8, :164 "T1-T8", :171 END "8 test" ⇒ 1 nhãn sai / 5 chỗ đúng. Đếm B0-B16 thì ĐÚNG: B0 + (B1…B14 = 14 dòng bảng) + B15 + B16 = 17.

F-9 [MEDIUM] Picker quy trình lọc 2 vế, spec + B0 chỉ nêu 1 ⇒ B0 xanh mà dialog B9 vẫn RỖNG

Dropdown mà spec bảo mirror lọc cả hai: fe-user/src/pages/contracts/ContractCreatePage.tsx:334-341 — gọi /approval-workflows-v2?applicableType=3 rồi .filter(w => w.isUserSelectable) (:341; chú thích :332 "IsUserSelectable=true (admin ghim cho user pick)"). BE có cột + toggle riêng: ApprovalWorkflowV2AdminFeatures.cs:68/129-131/1164-1175, độc lập IsActive. Spec K7.3(3) :68 chỉ ghi "filter ApplicableType=3"; K7.2 :62 và B0(ii) :110 chỉ đòi "≥1 ApprovalWorkflow ApplicableType=3 ACTIVE". ⇒ Nếu workflow type-3 trên prod có IsUserSelectable=false, B0 vẫn PASS nhưng tới B9 dialog rỗng, không tạo được HĐ — hỏng giữa dry-run (đúng thứ B0 sinh ra để chặn). Đề nghị: sửa B0(ii) thành "ApplicableType=3 IsUserSelectable=true" (sqlcmd đọc cả 2 cột), và K7.3(3) khai đủ 2 vế lọc.

F-10 [LOW] Bằng chứng B6 neo vào cột CHƯA tồn tại + sai bảng

B6 :119 đòi sqlcmd EndedByLevelFinalize,ApprovedAmount. Đo: grep -rn "Finalize\|ApprovedAmount" src/Backend/SolutionErp.Domain/ContractSigningPlans/*.cs → chỉ ContractSigningPlanLine.cs:23 ApprovedAmount ("CHỈ ghi tại choke-point finalize (W3)") + ContractSigningPlanPhase.cs:12. 0 hit EndedByLevelFinalize phía KHKK (cột này hiện chỉ có ở PE, Mig 60/61). Ngoài ra ApprovedAmount nằm ở bảng ContractSigningPlanLines, không cùng bảng với cờ header như câu sqlcmd gợi ý. Đề nghị: B6 khai tường minh "cột do K3 đẻ — nếu K3 không đẻ thì bằng chứng đổi sang X", và tách 2 câu sqlcmd đúng bảng. (Lớp "acceptance literal tự-lão-hoá".)

F-11 [LOW] Guard workflow copy nguyên khuôn ⇒ kế thừa lỗ: không re-check IsActive/IsUserSelectable phía server

Khuôn ContractFeatures.cs:74-88 (và EnsureWorkflowTypeAsync ContractSigningPlanFeatures.cs:209-220) chỉ kiểm tồn-tại + ApplicableType. Với endpoint MỚI, POST forge ApprovalWorkflowId của quy trình type-3 đã retired/unpinned vẫn pin được ⇒ HĐ chạy roster cũ. Không chặn merge (bằng đúng mặt bằng hiện tại), nhưng spec nên khai là CHẤP NHẬN có ý thức, hoặc thêm 1 dòng guard vào K7.1(6).

F-12 [PASS — control dương, chống dương-giả]

  • B12 khả thi thật: fe-{user,admin}/src/pages/contracts/HardCopiesPage.tsx:279 form.append('purpose', String(SEALED_COPY)); BE nhận AttachmentPurpose Purpose (ContractAttachmentFeatures.cs:22/51/80); cờ đọc ContractFeatures.cs:348-355 (EXISTS, cố ý không có !IsDeleted — đã giải thích tại chỗ).
  • QUYẾT-2 (:103) đúng mã: ContractCodeGenerator.cs:23 HopDongNhaCungCap => "NCC"; "MB" :25 tồn tại nhưng né là hợp lý.
  • Neo FE :66 chính xác: khối actions={ của fe-user/src/pages/khkk/KhkkDetailPage.tsx bắt đầu :291 (nút Xoá :303-313, Danh sách :314-317); bản fe-admin tồn tại (26.927 B).
  • Rào (i)/(iii)/(iv) mà spec trích đúng vị trí: :330-332, :342-347, :349-350.

Verdict

PASS-WITH-FLAGS — spec cụm-3 không có claim SAI (7/7 đúng về sự-kiện, kể cả cú BÁC-tiền-đề đề-bài là đúng và có lợi). Nhưng 4 MEDIUM phải vá TRƯỚC khi implementer khởi công / trước khi chốt lịch K8:

  • F-2 policy cộng-dồn + viện #85 sai chiều (đổi câu chữ + B0 kiểm 2 quyền).
  • F-9 B0 thiếu vế IsUserSelectable ⇒ B0 xanh mà B9 rỗng (chặn được bằng 1 dòng sqlcmd).
  • F-6 chiều ngược pe.ContractId != null không có guard ⇒ HĐ mới vô hình trên cây.
  • F-5 "chiếm slot nhóm-4" chỉ đúng SAU K2; chưa K2 thì khoá cả PE thật.

LOW (F-4, F-7 phần WorkflowDefinitionId, F-8, F-10, F-11) nên vá trong cùng lượt sửa spec vì đều là 1-2 dòng. Không có blocker nào thuộc loại "phải build lại spec".

END sub-reviewer-c3-l1 — TOTAL: 12 finding (0 SAI-claim · 4 MEDIUM · 6 LOW · 2 PASS-control-dương) · 7/7 claim đề-bài đã chấm.