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:
- Approver THẤY — List
ContractFeatures.cs:311-318:x.c.Phase != DangSoanThao && myWorkflowIds.Contains(x.c.ApprovalWorkflowId); helper:403-411gom mọi workflow user có chânApproverUserId. Detail:624-637:isV2Approverhỏi theoc.ApprovalWorkflowId(không theo role) + chặn Nháp:631-636. - INBOX —
:532-534v2InboxIds; where:543; thoát-sớm:538xét CẢ 2 nguồn (user 0 role legacy vẫn có hộp thư).ResolveV2InboxIdsAsync:559-593đòiPhase==ChoDuyet && AWId!=null && StepIndex!=null && LevelOrder!=null— trình sẽ setStepIndex=0(ContractWorkflowService.cs:94) vàLevelOrder=1đúng vìApprovalWorkflowId is not null(:97). Admin đi nhánh phaseAdminInboxPhases:503-513(đã cóChoDuyet). - TRÌNH được —
ContractWorkflowService.cs:82-92:isCreatorxétCreatedBy || 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/contractsKHÔNG có policy:ContractsController.cs:13class[Authorize]trần; action:49-50trống. Y hệtList:16-17,Inbox:41-42,Get:45-46,Transition:64-65. ChỉListDeleted:31-32mang[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
ContractSigningPlansControllerthì policy CỘNG DỒN: class:28[Authorize(Policy="KeHoachKyKet.Read")]+ actionContracts.Create⇒ hiệu lực = AND 2 policy, không phải "KHÔNG phảiKeHoachKyKet.*" như spec:54viết. - Hệ quả: (i) B9
:122mô 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ọnKeHoachKyKet.*) có thể không được cấpContracts.Create⇒ mất nút, trong khi chính người đó vẫn tạo HĐ tay được quaPOST /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=*.cs→ duy nhấtCreateContractFromEvaluationFeatures.cs:145pe.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ộcContractAttachment / ContractDetail* / ContractChangelog / ContractComment+DbInitializer+ column-def migration; 0 hit gán choContractSigningPlanLine. ⇒ "assignment ĐẦU TIÊN toàn codebase" (:50) đúng.ContractSigningPlanWorkflowService.cschỉ 1 hit:54và đú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-1140chặn không-owner/không-admin →:1145-1148allow-list{DangSoanThao, TuChoi}không có ngoại lệ admin ⇒DaDuyetbất-khả-xoá qua API kể cả Admin. ĐÚNG. - Đường thoát sqlcmd soft-delete có căn cứ:
ContractSigningPlanConfiguration.cs:44HasQueryFilter(x => !x.IsDeleted)⇒ rào:342-344không đếm phiếu đã xoá mềm. ĐÚNG. - Lỗ suy luận: rào
:342-344trên đĩa HÔM NAY làAnyAsync(p.PurchaseEvaluationId == pe.Id && p.Phase != TuChoi)— per-PE, KHÔNG cóApprovalGroup. B15:131viế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 ZZTESTDaDuyetkhoá mọi nhóm của PE thậtPE/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:140về 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ấyCode. Bridge cũ nạpproject:65-66+supplierMap:63-64kèm 2NotFoundException(:66,:85-86). Khối "Build Contract (map field)" spec:46chỉ nêuProjectId/SupplierId— không nêu bước nạp + 2 guard NotFound.SlaDeadlinevắng trong field-map (bridge cũ set:109-110; create tay setContractFeatures.cs:110). Không chí mạng (trình sẽ set +7dContractWorkflowService.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 minhContractFeatures.cs:105-109"Cả 2 set ok — Service ApproveV2Async branch dispatch theo ApprovalWorkflowId trước". Hệ quả đo được:GetContractQueryHandler:647-662khiWorkflowDefinitionId == nullrơi vềWorkflowPolicyRegistry.ForContractWithOverrides(policy hardcode) ⇒ khốiworkflowtrong 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 đọcworkflow.nextPhases".- Chốt nhỏ: guard
:384thực tế làstring.IsNullOrEmpty(contract.MaHopDong)(spec ghiis 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-82 có T1…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 VÀ 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:279form.append('purpose', String(SEALED_COPY)); BE nhậnAttachmentPurpose Purpose(ContractAttachmentFeatures.cs:22/51/80); cờ đọcContractFeatures.cs:348-355(EXISTS, cố ý không có!IsDeleted— đã giải thích tại chỗ). - QUYẾT-2 (
:103) đúng mã:ContractCodeGenerator.cs:23HopDongNhaCungCap => "NCC"; "MB":25tồn tại nhưng né là hợp lý. - Neo FE
:66chính xác: khốiactions={củafe-user/src/pages/khkk/KhkkDetailPage.tsxbắt đầu:291(nút Xoá:303-313, Danh sách:314-317); bảnfe-admintồ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 != nullkhô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.