wal: flush 20260731T1400
This commit is contained in:
@ -49,10 +49,16 @@ next: 5/6 OG CHỐT (1 schema-GỘP+UI-1-1 · 2 đội-hình-cũ · 7 N-phiếu+
|
|||||||
B0-B16, B1→KHKK-N4, type NhaCungCap né hố-MB, đội hình từ K3-LIVE [Dev 0 QT-DN-V2-001]).
|
B0-B16, B1→KHKK-N4, type NhaCungCap né hố-MB, đội hình từ K3-LIVE [Dev 0 QT-DN-V2-001]).
|
||||||
🔴 HỐ-1-KHKK MỚI: phiếu KHKK DaDuyet KHÔNG XÓA nổi kể cả Admin (:1145-1148 allow-list SAU nhánh admin) +
|
🔴 HỐ-1-KHKK MỚI: phiếu KHKK DaDuyet KHÔNG XÓA nổi kể cả Admin (:1145-1148 allow-list SAU nhánh admin) +
|
||||||
chiếm slot (PeId,group) VĨNH VIỄN — dry-run trên PE thật = phiếu-test nằm lại, dọn = sqlcmd ⇒ CÂU (V) cho anh.
|
chiếm slot (PeId,group) VĨNH VIỄN — dry-run trên PE thật = phiếu-test nằm lại, dọn = sqlcmd ⇒ CÂU (V) cho anh.
|
||||||
wf: B2-r cụm-3 wf_24ab6bc3-abd ĐANG CHẠY (3 lane c3-l{1,2,3}; l3 = lượt bù lens-chết-c2). Về → synthesize
|
B2-r cụm-3 ✅ 3/3 (L1 7/7-ĐÚNG · L2 FAIL-5H K8-chưa-chạy-được [12/14 bước 0 người thật · QĐ9 đo-0-bit ·
|
||||||
→ B3 fable-real review chốt → bảng tổng trình anh → B4.
|
ZZTEST không hiện list · rollback-HĐ no-op] · L3 FAIL-4H [authz HỘI-2-khóa + Contracts.Create 0-VAI ĐƯỢC
|
||||||
CHỜ ANH: (I) bản-15-trang · (II) B2-14-dòng+"/" · (III) ai bấm Đưa-vào-HĐ · (V) dry-run PE-thật-A/049
|
CẤP ⇒ nút chết 403 câm — 3-lane hội tụ]) → spec-cum3-chi-tiet-31-07-2026.md FINAL 16 vá.
|
||||||
[phiếu test nằm lại] vs PE-ZZTEST-riêng · OG-6 soát bảng+8-nhãn. Sổ lane-chết: fable 3/7 · opus 1/9.
|
B2 ĐÓNG TRỌN: 3 spec final + 43 vá tổng. wf: B3 wf_567c4b38-405 ĐANG CHẠY (1 lane reviewer tier:FABLE
|
||||||
|
đối chiếu chéo 4 spec → A mâu-thuẫn-liên-spec · B vá-đè-vá · C build-order · E GO/NO-GO · F digest-owner).
|
||||||
|
Về → lead tổng → BẢNG TỔNG trình anh → B4 build K1 (anh pre-authorize "chốt kỹ rồi thì dry-run luôn";
|
||||||
|
seed-land vẫn chờ OG-6+câu-II · K8 chờ câu-V).
|
||||||
|
CHỜ ANH: (I) bản-15-trang · (II) B2-14-dòng+"/" · (III) ai-bấm-Đưa-vào-HĐ [default Drafter+Procurement+
|
||||||
|
Admin] · (V) PE-thật vs ZZTEST-riêng [nghiêng (b): trước-K2 rào per-PE khóa TOÀN BỘ A/049] · OG-6 soát.
|
||||||
|
Sổ lane-chết: fable 3/8 · opus 1/12.
|
||||||
|
|
||||||
verify:
|
verify:
|
||||||
grep -c 'END spec v2' .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md
|
grep -c 'END spec v2' .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/spec-4gd-khkk-tong-quat-31-07-2026.md
|
||||||
|
|||||||
@ -15,8 +15,9 @@
|
|||||||
| 2.c1-r | Review cụm-1 — 3 lane `WF reviewer c1-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ 3/3 sạch · L1 2-blocker+GỠ-(i)-:342 · L2 FAIL 3H (menu-ngoài-All chết · ép-false ≥3 site sót · KhkkCreatePage mâu thuẫn) · L3 PWB 5H → **spec-cum1-final 12 vá + OG-9** |
|
| 2.c1-r | Review cụm-1 — 3 lane `WF reviewer c1-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ 3/3 sạch · L1 2-blocker+GỠ-(i)-:342 · L2 FAIL 3H (menu-ngoài-All chết · ép-false ≥3 site sót · KhkkCreatePage mâu thuẫn) · L3 PWB 5H → **spec-cum1-final 12 vá + OG-9** |
|
||||||
| 2.c2 | Cụm-2 K4+K5+K6 — `WF invest-fable b2c2 s164` | /fable-real invest | ✅ SẠCH 5/5 (K4 13v·K5 6v·K6 4v, 0-mig; BÁC 2 tiền-đề lead có chứng) |
|
| 2.c2 | Cụm-2 K4+K5+K6 — `WF invest-fable b2c2 s164` | /fable-real invest | ✅ SẠCH 5/5 (K4 13v·K5 6v·K6 4v, 0-mig; BÁC 2 tiền-đề lead có chứng) |
|
||||||
| 2.c2-r | Review cụm-2 — `WF reviewer c2-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ l1 PWF 2-MAJOR · l2 PWF 3H/7M · 🔴 l3 CHẾT-RUỘT 188B (opus đầu tiên gãy — phủ bù lead-note có khai) → **spec-cum2-final 15 vá** |
|
| 2.c2-r | Review cụm-2 — `WF reviewer c2-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ l1 PWF 2-MAJOR · l2 PWF 3H/7M · 🔴 l3 CHẾT-RUỘT 188B (opus đầu tiên gãy — phủ bù lead-note có khai) → **spec-cum2-final 15 vá** |
|
||||||
| 2.c3 | Cụm-3 K7+K8 — `WF invest-fable b2c3 s164` wf_a21c8ff1-bc7 | /fable-real invest | ⏳ |
|
| 2.c3 | Cụm-3 K7+K8 — `WF invest-fable b2c3 s164` | /fable-real invest | ✅ SẠCH 22.9KB (K7 8v/8test BÁC tiền-đề 3-lỗ-S156-đã-vá · K8 17 bước · HỐ-1-KHKK mới) |
|
||||||
| 3 | Chốt spec cuối | /fable-real review | ☐ |
|
| 2.c3-r | Review cụm-3 — `WF reviewer c3-{evi,cov,seq} s164` | /fable-clone (opus ×3) | ✅ 3/3 sạch · L1 7/7-ĐÚNG PWF 4M · L2 FAIL 5H (K8 chưa-chạy-được-như-viết) · L3 FAIL 4H (authz hội-2-khóa · Contracts.Create 0-vai) → **spec-cum3-final 16 vá** |
|
||||||
|
| 3 | Chốt spec cuối — `WF review-fable b3-chot s164` wf_567c4b38-405 | /fable-real review | ⏳ (đối chiếu chéo 4 spec: mâu-thuẫn liên-spec · vá-đè-vá · build-order · GO/NO-GO · digest owner) |
|
||||||
| 4 | Fan-out build bám spec+checklist | Opus 5 MAX (hmw) | ☐ |
|
| 4 | Fan-out build bám spec+checklist | Opus 5 MAX (hmw) | ☐ |
|
||||||
| 5 | Dry-run E2E toàn trình | theo spec wave cuối | ☐ |
|
| 5 | Dry-run E2E toàn trình | theo spec wave cuối | ☐ |
|
||||||
|
|
||||||
@ -25,7 +26,7 @@
|
|||||||
- [x] B0 — draft tổng hợp 10 lời chốt + model 4GĐ + 8 nhóm + scope 5 mảnh + ràng buộc khuôn + nền kỹ thuật + 8 treo
|
- [x] B0 — draft tổng hợp 10 lời chốt + model 4GĐ + 8 nhóm + scope 5 mảnh + ràng buộc khuôn + nền kỹ thuật + 8 treo
|
||||||
- [x] B1a — att-1 wf_22e8949b-afa CHẾT ngược-#53 (207K tok, 0 byte) · att-2 wf_bc04352b-6a4 A-D đầy + spec-file lead kết tinh `spec-4gd-khkk-tong-quat-31-07-2026.md` (8 verdict · 8 wave K1-K8 · 3 cụm · OG-1/2/3/5)
|
- [x] B1a — att-1 wf_22e8949b-afa CHẾT ngược-#53 (207K tok, 0 byte) · att-2 wf_bc04352b-6a4 A-D đầy + spec-file lead kết tinh `spec-4gd-khkk-tong-quat-31-07-2026.md` (8 verdict · 8 wave K1-K8 · 3 cụm · OG-1/2/3/5)
|
||||||
- [x] B1b — wf_8eb13087-5f9 3/3 lane sạch (l1 FAIL 5H · l2 6/6 chịu-lực ĐÚNG +l2-F1 rào-1-PE · l3 FAIL 6H + bác 6 nghi) → `b1b-review-synthesis.md` 14 vá + OG-6/OG-7 → spec v2 (hội tụ 2-lane: admin-hide tập-key tường minh; ApprovalGroup K2-chủ + backfill=1; 8 designer có phép đo; nguồn K1 = 7 ảnh [file đĩa 6-trang không bảng])
|
- [x] B1b — wf_8eb13087-5f9 3/3 lane sạch (l1 FAIL 5H · l2 6/6 chịu-lực ĐÚNG +l2-F1 rào-1-PE · l3 FAIL 6H + bác 6 nghi) → `b1b-review-synthesis.md` 14 vá + OG-6/OG-7 → spec v2 (hội tụ 2-lane: admin-hide tập-key tường minh; ApprovalGroup K2-chủ + backfill=1; 8 designer có phép đo; nguồn K1 = 7 ảnh [file đĩa 6-trang không bảng])
|
||||||
- [ ] B2 — cụm invest sâu + review (số cụm = theo spec B1)
|
- [x] B2 — 3 cụm TRỌN: c1 (33KB + 12 vá) · c2 (5/5 + 15 vá; l3 chết-ruột phủ-bù) · c3 (22.9KB 7/7-đúng + 16 vá) = 43 vá tổng, 3 spec final
|
||||||
- [ ] B3 — fable-real review chốt
|
- [ ] B3 — fable-real review chốt
|
||||||
- [ ] B4 — fan-out build
|
- [ ] B4 — fan-out build
|
||||||
- [ ] B5 — dry-run E2E
|
- [ ] B5 — dry-run E2E
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
# SPEC CỤM-3 FINAL — K7 + K8 (lead kết tinh sau B2-r cụm-3, @S164)
|
||||||
|
|
||||||
|
## ① Đề bài
|
||||||
|
K7 cầu KHKK→HĐ (bridge 8v/8test — đường ống S156 ĐÃ vá @W6-S161, verified 3 chân) · K8 dry-run E2E 17 bước. NỀN = `sub-invest-fable-b2-cum3.md` (22.9KB; L1 chấm **7/7 claim ĐÚNG**) — **16 VÁ dưới thắng khi xung đột** (từ `sub-reviewer-c3-l{1,2,3}.md`: L1 PWF 4M · L2 FAIL 5H · L3 FAIL 4H).
|
||||||
|
|
||||||
|
## ② 16 vá
|
||||||
|
|
||||||
|
**Trục AUTHZ bridge (3-lane hội tụ — nặng nhất):**
|
||||||
|
1. 🔴 **[L1-F2 ⨯ L2-H1 ⨯ L3-H1/H2] Gate nút bridge = HỘI 2 policy** (`KeHoachKyKet.Read` class-level ∧ `Contracts.Create` action-level) — spec/FE phải khai ĐỦ 2 vế; và 🔴 **`Contracts.Create` hiện CHƯA CẤP vai nào** (seeder `CanCreate=isPe` ⇒ false cho `Contracts`) ⇒ thêm việc: **grant `Contracts.Create` cho vai được bấm** (đề xuất: Drafter + Procurement + Admin — [chờ câu III anh; default người-có-quyền-tạo-HĐ]) + **test authz 2 chiều** (vai có quyền → 201 · vai thiếu 1 trong 2 vế → 403) + FE gate đúng hội-2-khóa (không lỏng hơn server). Ghi chú: endpoint `POST /api/contracts` hiện `[Authorize]` trần (#82 treo chủ-đích — KHÔNG sửa đợt này, chỉ khai).
|
||||||
|
2. **[L1-F9] B0 pre-flight picker đủ 2 vế**: workflow type-3 phải `IsActive` **∧ `IsUserSelectable`** (dropdown lọc cả 2 — `ContractCreatePage.tsx:334-341`); B0 thêm SQL check cả 2 cờ kẻo dialog rỗng.
|
||||||
|
3. **[L1-F11] Guard bridge re-check `IsActive`/`IsUserSelectable`** của workflow được pin (khuôn `ContractFeatures.cs:74-88` chỉ check tồn-tại+type — forge POST pin được quy trình retired; thêm 2 điều kiện, rẻ).
|
||||||
|
|
||||||
|
**Trục dữ liệu/cây:**
|
||||||
|
4. 🔴 **[L1-F6] `pe.ContractId ??=` có chiều NGƯỢC** — bridge-PE-cũ chạy trước thì `??=` no-op im lặng ⇒ HĐ mới **vô hình trên cây** (cây đi qua `pe.contractId` ĐƠN `usePipelineStages.ts:214-218`). Vá 2 tầng: (a) K7 ghi `Line.ContractId` luôn (nguồn thật) + `pe.ContractId ??=` chỉ là fallback hiển thị; (b) **K5-liên-đới: builder GĐ3/GĐ4 nối qua TẬP `lines[].contractId`** của các phiếu KHKK thuộc gói (∪ `pe.contractId` legacy) — không thì OG-1 UI-1-1 (N HĐ/PE) chỉ hiện HĐ đầu.
|
||||||
|
5. **[L3-H3] T2 (gộp 2 line cùng NCC) phụ thuộc Mig 71 DROP-unique** — thêm vào danh sách phụ-thuộc K7←K2 (hiện sót); test T2 chạy SAU K2.
|
||||||
|
6. **[L1-F7] Bridge bổ sung theo tiền lệ**: nạp `Project.Code` + `Supplier.Code` (+2 guard NotFound, khuôn bridge-cũ `:63-66/:85-86`) cho codegen `GenerateAsync` · field-map += `SlaDeadline` · **`WorkflowDefinitionId` pin như 2 tiền lệ** (`ContractFeatures.cs:105-109` "Cả 2 set ok") — không để null rơi về policy hardcode `:647-662`.
|
||||||
|
7. **[L1-F10] Acceptance B6 đổi neo**: `EndedByLevelFinalize` phía KHKK là vật K3 MỚI ĐẺ (hiện 0 hit) — B6 neo vào {phase DaDuyet + `ApprovedAmount` frozen per-line + K3-cột-runtime} tùy nhánh; khai rõ bảng nào cột nào.
|
||||||
|
8. **[L1-F8] Sửa nhãn "7 case" → 8** (bảng T1-T8).
|
||||||
|
|
||||||
|
**Trục K8 dry-run (L2 — "chưa chạy được như viết"):**
|
||||||
|
9. 🔴 **[L2-H5] B0 = FORM NGƯỜI THẬT fail-closed**: bảng 14 dòng {bước · người · email · tài khoản} PHẢI ĐIỀN ĐỦ trước giờ-G (12/14 bước đang trống); nguồn = K3-acceptance LIVE; thiếu 1 dòng = KHÔNG chạy.
|
||||||
|
10. 🔴 **[L2-H2] Phép đo QĐ9 phải TRƯỢT ĐƯỢC**: sau finalize → **sửa NS/giá nguồn** (PE hoặc catalog) → reload phiếu → số **KHÔNG đổi** = PASS (đổi = FAIL); đo **CẢ 2 nhánh** (a-Chương finalize-CCM VÀ lên-CEO-Trường duyệt) — hiện phép đo 0-bit.
|
||||||
|
11. 🔴 **[L2-H3] Dấu ZZTEST cho phiếu KHKK**: mã phiếu auto-gen (`KHKK/2026/xxx`) KHÔNG mang ZZTEST — bản ghi sống-vĩnh-viễn lại không dấu. Vá: nhét `ZZTEST` vào field hiển thị được trên list (tên kế hoạch/ghi chú — soi DTO chọn field) + **rollback theo DANH SÁCH ID** ghi lại từng bước, không theo prefix.
|
||||||
|
12. 🔴 **[L2-H4 ⨯ L1-F5] Câu giờ-G (V) trình anh ĐỦ 2 hệ quả**: (i) phiếu KHKK DaDuyet trên PE thật **không xóa được kể cả Admin** (sqlcmd là lối duy nhất); (ii) 🔴 **TRƯỚC K2 rào là per-PE** ⇒ phiếu ZZTEST khóa **TOÀN BỘ** `PE/2026/A/049` (không phải chỉ nhóm-4) — nghiêng mạnh về **phương án (b) PE ZZTEST riêng**; nếu anh chọn (a) PE thật thì bắt buộc chạy SAU K2.
|
||||||
|
13. **[L3-H4] B16 rollback HĐ = NO-OP hiện tại** (numeric guard chặn xóa cả khi TuChoi) — rollback plan ghi rõ: HĐ test dọn bằng **sqlcmd soft-delete** (lệnh cụ thể per bảng: Contracts + ContractApprovals + Changelogs + Line.ContractId=NULL + pe.ContractId=NULL), thứ tự ngược.
|
||||||
|
14. **[L2-M10] Câu III treo → acceptance "ai bấm" để [CHỜ-ANH]** — không chốt trong DoD tới khi anh trả; default dialog + người-có-quyền.
|
||||||
|
15. **[L2-M12] DoD cardinality đo được**: sau bridge UI-1-1 — đếm `SELECT COUNT(DISTINCT ContractId) FROM Lines WHERE PlanId=X AND ContractId IS NOT NULL` == số lần bấm; gộp (sau này) = N line 1 ContractId.
|
||||||
|
16. **[L3-Q5] Dialog bridge tái dùng khuôn sẵn** (dialog confirm shadcn/ui đã dùng ở Xóa-phiếu S155 + select khuôn `ContractCreatePage` picker) — 0 khuôn mới (lời 9).
|
||||||
|
|
||||||
|
## ③ Checklist final cụm-3
|
||||||
|
- [ ] K7 (SAU cụm-1 K2+K3): bridge 8v nền + vá 1-8 · test 8+2-authz+idempotency-double-click · acceptance 7 nền + hội-2-khóa 403-path + cây hiện HĐ qua lines[].contractId + `giaTri == Σ ApprovedAmount` line chọn (null → message) · baseline ≥590+k.
|
||||||
|
- [ ] K8 (CUỐI, sau MỌI wave land + câu V anh chốt): B0-FORM 14 người fail-closed + pre-flight SQL (workflow LIVE 2-cờ + KHKK-N4 + catalog N-row) · 17 bước mỗi bước {người thật · hành động · kỳ vọng · chụp} · QĐ9 đo 2 nhánh trượt-được · ZZTEST field-hiển-thị + rollback theo ID (sqlcmd cả HĐ lẫn phiếu) · notification dặn team trước.
|
||||||
|
|
||||||
|
END spec-cum3-final — nền 7/7-claim-đúng + 16 vá · authz-hội-2-khóa đóng · K8 từ "kịch bản" thành "chạy được".
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
# sub-review-fable-b3 — B3 REVIEW CHỐT CUỐI 4 spec (tổng v2 + cụm-1 + cụm-2 + cụm-3)
|
||||||
|
|
||||||
|
> Engine /fable-real · reviewer adversarial · đối chiếu CHÉO liên-spec → GO/NO-GO + build-order cuối cho B4.
|
||||||
|
> Luật đan xen: đọc 1 spec → ghi section NGAY.
|
||||||
|
|
||||||
|
## A. Mâu thuẫn LIÊN-SPEC
|
||||||
|
(ĐANG LÀM)
|
||||||
|
|
||||||
|
## B. Vá-đè-vá (vá cụm sau có phá vá cụm trước?)
|
||||||
|
(ĐANG LÀM)
|
||||||
|
|
||||||
|
## C. Build-order cuối + gate
|
||||||
|
(ĐANG LÀM)
|
||||||
|
|
||||||
|
## D. Rủi ro còn hở
|
||||||
|
(ĐANG LÀM)
|
||||||
|
|
||||||
|
## E. GO/NO-GO per wave
|
||||||
|
(ĐANG LÀM)
|
||||||
|
|
||||||
|
## F. Digest trình owner
|
||||||
|
(ĐANG LÀM)
|
||||||
@ -1,11 +1,113 @@
|
|||||||
# c3-l1
|
# 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).
|
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
|
## Findings
|
||||||
|
|
||||||
(ĐANG LÀM)
|
### 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 được** — `ContractWorkflowService.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=*.cs` → **duy 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ệ admin** ⇒ `DaDuyet` 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`/`SupplierId` — **khô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-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: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
|
## Verdict
|
||||||
|
|
||||||
(ĐANG LÀM)
|
**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.
|
||||||
|
|||||||
@ -1,13 +1,101 @@
|
|||||||
# c3-l2
|
# c3-l2
|
||||||
|
|
||||||
LANE 2/3 review cụm-3 — LENS ĐỦ-vs-SPEC + OG (out-of-gate / owner-gate).
|
LANE 2/3 review cụm-3 — LENS ĐỦ-vs-SPEC + OG (owner-gate).
|
||||||
Target: `sub-invest-fable-b2-cum3.md` (K7 bridge 8v/8test + K8 dry-run 17 bước B0-B16).
|
Target: `sub-invest-fable-b2-cum3.md` (K7 bridge 8v/8test + K8 dry-run 17 bước B0-B16).
|
||||||
Spec đối chiếu: `spec-4gd-khkk-tong-quat-31-07-2026.md` §②③④ + `spec-cum1-chi-tiet-31-07-2026.md`.
|
Spec đối chiếu: `spec-4gd-khkk-tong-quat-31-07-2026.md` §②③④ + `spec-cum1-chi-tiet-31-07-2026.md`.
|
||||||
|
Mọi neo dưới đây ĐO LẠI TRÊN ĐĨA hôm nay (không mượn số của lane khác).
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|
||||||
(ĐANG LÀM)
|
### HIGH
|
||||||
|
|
||||||
|
**H1 — Bridge đặt trên controller class-Policy ⇒ tập khóa hiệu lực là 2, spec khai 1; FE gate lỏng hơn server; 0 test authz.**
|
||||||
|
- Claim của cum3 `:54`: "🔴 đúng-key-policy-endpoint-đích (gotcha #85) … key `Contracts.Create`, KHÔNG phải `KeHoachKyKet.*`".
|
||||||
|
- Đĩa: `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs:26` = `[Authorize(Policy = "KeHoachKyKet.Read")]` **class-level**; comment `:12-19` khai luật nhà: "tầng 1 class `KeHoachKyKet.Read` (mọi endpoint tối thiểu Read) + tầng 2 per-action `KeHoachKyKet.{Create|Update|Delete}` — GHI ĐỦ TRÊN MỌI action".
|
||||||
|
- ASP.NET Core: action-`[Authorize(Policy=X)]` KHÔNG thay class-level, hai cái **AND**. ⇒ tập khóa THẬT của bridge = `{KeHoachKyKet.Read ∧ Contracts.Create}` — chưa ai khai. Đây là action ĐẦU TIÊN trên controller mang tiền tố khóa khác (khuôn nhà cùng hình dạng: `EmployeesController` class `Hrm_HoSo.Read` + per-action cùng tiền tố — `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs:208-256`).
|
||||||
|
- Hệ quả đo được: (a) FE gate `can('Contracts','Create')` (cum3 `:67`) LỎNG HƠN server 1 khóa — cùng lớp bệnh gotcha **#85/#82** tôi đã ghi @S162 (gate query phải = ĐÚNG tập khóa endpoint); (b) nếu câu III trả lời "người phòng Mua hàng bấm" mà vai đó KHÔNG có `KeHoachKyKet.Read` thì nút chết 403 im lặng (#44), và A1 chạy bằng admin (bypass) sẽ KHÔNG bắt được; (c) T1-T8 (`:73-82`) **0 case authz**.
|
||||||
|
- Sev: HIGH. Đề nghị: spec khai TƯỜNG MINH tập khóa hiệu lực 2 vế + 2 test âm (thiếu `KeHoachKyKet.Read` → 403; thiếu `Contracts.Create` → 403) + FE gate khớp ĐÚNG tập đó; nếu muốn 1-khóa thì phải nói rõ endpoint chuyển sang `ContractsController` (quyết của lead, không phải của tôi).
|
||||||
|
|
||||||
|
**H2 — QĐ9 "khóa budget" có bước ĐO nhưng phép đo KHÔNG THỂ TRƯỢT (0-bit), và nhánh trigger thứ 2 (CEO) không được đo.**
|
||||||
|
- Nghĩa vụ: `spec-4gd:19` QĐ9 — freeze khi **finalize tại CCM HOẶC CEO duyệt final**; "phiếu DaDuyet đọc SNAPSHOT, không đọc live".
|
||||||
|
- cum3 có B6 (`:119`, kỳ vọng "ApprovedAmount ghi tại choke-point + FREEZE", sqlcmd 2 cột) và B8 (`:121`, "mở lại detail: số khóa 🔒; sửa line bị phase-guard chặn; sqlcmd số không đổi").
|
||||||
|
- Lỗ: **không bước nào làm NHIỄU nguồn live** rồi đo số đã-freeze có đứng yên không. B8 chặn sửa bằng phase-guard ⇒ số không đổi là chuyện đương nhiên **kể cả khi display vẫn đọc LIVE**. Hỏi theo sàn-sự-thật: "phép kiểm nào khiến kết luận *đã freeze* TRƯỢT nếu freeze hỏng?" — hiện KHÔNG có.
|
||||||
|
- Lỗ 2: nhánh CEO (`:120` B7) nằm trong nhánh B "*nếu còn giờ*" (`:105`) và kỳ vọng chỉ là "Phase=DaDuyet đường chuẩn" — **0 assert freeze**. ⇒ 1 trong 2 trigger của QĐ9 không có mặt trong kịch bản chính.
|
||||||
|
- Sev: HIGH. Đề nghị: thêm bước "sau DaDuyet, đổi nguồn live (giá PE / NS tham chiếu / PeReferenceAmount) → mở lại phiếu KHKK: số HIỂN THỊ phải KHÔNG đổi, sqlcmd snapshot ≠ live" + ép nhánh CEO vào phần bắt buộc (hoặc khai thẳng "QĐ9 chỉ được chứng ở nhánh CCM, nhánh CEO nợ").
|
||||||
|
|
||||||
|
**H3 — DoD "mọi bản ghi mới mang tiền tố ZZTEST" KHÔNG đạt được cho phiếu KHKK; và đúng bản ghi sống-vĩnh-viễn lại là bản ghi không mang dấu.**
|
||||||
|
- Nghĩa vụ `spec-4gd:35`. cum3 thực hiện bằng: B1 `:114` "GhiChu tiền tố **ZZTEST**" (KHKK) + B10 `:123` "TenHopDong tiền tố ZZTEST" (HĐ).
|
||||||
|
- Đĩa: `ContractSigningPlanFeatures.cs:34-57` `ContractSigningPlanListItemDto` — 22 field, **KHÔNG có `GhiChu`** (entity có `ContractSigningPlan.cs:45 GhiChu`, nhưng list không trả). Phiếu KHKK cũng **không có field tên tự do** nào khác; detail header đọc `k.peTenGoiThau` = tên gói thầu THẬT (`fe-user/src/pages/khkk/KhkkDetailPage.tsx:288`).
|
||||||
|
- ⇒ Trên màn Danh sách/Đang duyệt/Đã duyệt KHKK, phiếu test hiện: `KHKK/2026/xxx` + tên gói thầu thật + dự án thật + NCC thật — **không phân biệt được với phiếu thật**. Mà theo chính cum3 `:131` (HỐ-1-KHKK) đây là bản ghi **không xoá được kể cả Admin** ⇒ nó nằm lại lâu nhất và mang dấu ít nhất.
|
||||||
|
- Sev: HIGH. Đề nghị: hoặc (a) khai thẳng "ZZTEST chỉ hiện ở GhiChu, KHÔNG hiện trên list — chấp nhận", hoặc (b) đây là lập luận MẠNH NHẤT cho phương án "bộ ZZTEST riêng" ở K8.2 (Project ZZTEST ⇒ `ProjectName` hiện ZZTEST trên list) — hiện K8.2 **không nêu lập luận này**.
|
||||||
|
|
||||||
|
**H4 — Câu giờ-G (K8.2) thiếu 2 hệ quả nặng nhất trên bản ghi THẬT, trong khi default là "im lặng = chạy trên data thật".**
|
||||||
|
- K8.2 `:140` liệt cái giá: phiếu/HĐ ZZTEST nằm vĩnh viễn + cháy 1 seq. **Thiếu:**
|
||||||
|
1. **Chiếm slot**: phiếu KHKK DaDuyet của A/049 khoá luôn việc lập phiếu nhóm-4 THẬT cho PE đó tới khi sqlcmd (cum3 tự khai ở `:131` nhưng KHÔNG đưa vào câu hỏi cho anh). Đĩa xác nhận rào: `ContractSigningPlanFeatures.cs:341-347` `AnyAsync(p.PurchaseEvaluationId == pe.Id && p.Phase != TuChoi)` → 409.
|
||||||
|
2. **Chiếm `pe.ContractId`**: cum3 `:52` quyết `pe.ContractId ??= contract.Id`. Đĩa: `CreateContractFromEvaluationFeatures.cs:59-60` `if (pe.ContractId is not null) throw new ConflictException("Phiếu này đã tạo HĐ rồi.")` ⇒ bridge PE→HĐ CŨ **409 vĩnh viễn** cho PE thật A/049 — và vì bridge cũ tạo **N HĐ cho N winner trong 1 call** (loop `foreach (var w in winners)` `:81+`), 1 lần bridge KHKK khoá luôn đường tạo HĐ cho **mọi winner còn lại** của phiếu đó. cum3 gọi đây là "đúng ý, khóa đường cũ tự nhiên" — nhưng đó là **rút một đường đang dùng được trên bản ghi thật**, thuộc quyền anh, không phải hệ quả kỹ thuật trung tính.
|
||||||
|
- Sev: HIGH. Đề nghị: viết lại K8.2 thành 2 phương án A/B, mỗi phương án 1 dòng "cái mất" gồm đủ 4 mục (2 mục trên + phiếu nằm lại + seq cháy); bỏ "im lặng = A" hoặc đổi default sang B.
|
||||||
|
|
||||||
|
**H5 — 12/14 bước không có người thật, 0/14 có tài khoản; và KHÔNG có cửa nào bắt điền trước giờ-G.**
|
||||||
|
- Bảng `:112-127`: cột là {Ai · Hành động · Kỳ vọng · Bằng chứng} — **không có cột email/tài khoản**. Chỉ B6 (`TP.CCM Phan Văn Chương`) và B7 (`CEO Trường`) là người thật; B1/B2/B3 "BCH drafter [K3-bảng]", B4 "Trạm-1 [K3-bảng]", B5 "Trạm-2 [K3-bảng]", B9 "Người có Contracts.Create [⏳ câu III]", B11 "Approver HĐ [đội hình type-3]", B12 "HRA/CCM", B13 "Bất kỳ + Admin", B14 "Lead".
|
||||||
|
- cum3 `:104` giải thích hợp lý vì sao chưa điền (Dev không có `QT-DN-V2-001`) — **nhưng** B0 (`:110`) có 5 mục và KHÔNG mục nào là "điền đủ 14 dòng cột Ai từ bảng K3 + verify từng tài khoản login 200 + xác nhận người có mặt khung giờ X". Checklist `:166` cũng chỉ đòi "B0 đủ 5 mục + câu giờ-G".
|
||||||
|
- Hệ quả: tới giờ-G mới phát hiện thiếu người ⇒ hoặc hoãn, hoặc **chạy bằng Admin bypass** (V2 Admin skip mọi check) — mà chạy bằng admin thì dry-run mất đúng thứ nó định chứng (gating trạm-by-trạm; `SignedByUserId` sẽ ra "Admin duyệt thay"). Spec không nói mode nào được phép.
|
||||||
|
- Sev: HIGH. Đề nghị: B0 += mục (vi) "bảng 14 dòng đã điền tên+tài khoản, mỗi tài khoản đã thử login 200 trong 24h trước" + 1 dòng chính sách "được/không được dùng Admin thay trạm".
|
||||||
|
|
||||||
|
### MED
|
||||||
|
|
||||||
|
**M6 — B0(ii) tự-tham-chiếu: không bắt được lỗi mà OG-2 sinh ra để bắt.**
|
||||||
|
`:110` B0(ii) verify "…`KHKK-N4` active + roster khớp **bảng K3**". Bảng K3 và seed `KHKK-N4` cùng một nguồn (K3 sinh ra cả hai) ⇒ nếu K3 seed nhầm từ bản `QT-DN-V2-001` **archived**, B0 vẫn PASS. OG-2 (`spec-4gd:44`) yêu cầu đúng cái này: "verify bản **LIVE (không archived)**". Tên `QT-DN-V2-001` không xuất hiện trong B0. Đề nghị: B0 đo trực tiếp nguồn (`ApprovalWorkflows` Code=`QT-DN-V2-001` + cờ active/version) rồi mới so xuống `KHKK-N4`.
|
||||||
|
|
||||||
|
**M7 — QUYẾT-1 (PVC → nhóm B1 → `KHKK-N4`) dựng trên bảng CHƯA được anh soát, và 3/3 trích dẫn vào bảng đó đều lệch.**
|
||||||
|
- OG-6 còn ⏳ (`spec-4gd:47`: "anh soát bảng seed trước land"). QUYẾT-1 (`cum3:102`) chốt nhóm dựa trên bảng đó mà không khai phụ thuộc này ⇒ anh sửa bảng thì nhóm/mã workflow của cả buổi dry-run đổi theo.
|
||||||
|
- Trích dẫn (đo bằng `sed -n 'Np'` trên `danh-muc-sp002-transcribe.md`):
|
||||||
|
| cum3 nêu | nội dung THẬT tại dòng đó | neo ĐÚNG |
|
||||||
|
|---|---|---|
|
||||||
|
| `:52` = "B1 · Vật tư / Materials (23) → N4" | `\| A3-07 \| Điện tạm \| Temporary power supply \|` | **`:65`** |
|
||||||
|
| `:75` = "B1-23 Vật tư chính khác" | `\| B1-10 \| Nylon \| Nylon sheets \|` | **`:88`** |
|
||||||
|
| `:20` = "A1 = thiết bị/vật tư PHỤ → N1" | `\| # \| Nhãn menu đề xuất \|` (header bảng) | **`:33`** (hoặc `:22` dòng nhãn N1) |
|
||||||
|
- Sev: MED (kết luận nghiệp vụ có thể vẫn đúng, nhưng neo không dẫn tới bằng chứng — người soát bấm vào sẽ thấy thứ khác và mất niềm tin đúng chỗ cần nhất).
|
||||||
|
|
||||||
|
**M8 — Neo `ContractFeatures.cs:632-633` (HỐ-1 guard numeric) SAI, và sai theo kiểu "chép từ sổ S160" — trái chính luật K7.0 tự đặt.**
|
||||||
|
`cum3:130` viết: "`DeleteContract` guard numeric `Phase >= DangInKy` (`ContractFeatures.cs:632-633`, S160)". Đĩa: guard thật ở **`:780`** `if (entity.Phase >= ContractPhase.DangInKy)` trong `DeleteContractCommandHandler` (`:773`, record `:771`). `:632` nằm trong khối authz của GetDetail (`isHardCopyActor`/`ForbiddenException("Bạn không có quyền xem HĐ này.")`). Nội dung claim ĐÚNG, neo lệch ~148 dòng. K7.0 mở đầu bằng "đo lại hôm nay, không tin sổ" (`:8`) — nguyên tắc áp cho K7 nhưng không áp cho K8.
|
||||||
|
|
||||||
|
**M9 — "4 file test SigningPlan" là 3; và module KHKK hiện KHÔNG có regression authz nào — K7 cũng không thêm.**
|
||||||
|
`cum3:14` liệt "(Approval/Crud/Schema/AuthorizePolicy)". Đĩa: `tests/…/Application/ContractSigningPlanApprovalTests.cs`, `…/ContractSigningPlanCrudTests.cs`, `…/Common/ContractSigningPlanSchemaTests.cs` = **3**; `tests/…/Api/AuthorizePolicyRegressionTests.cs` là file CHUNG (`grep -n "SigningPlan"` → **0 hit**), phủ ApprovalWorkflowsV2/HrmConfigs/Employees. ⇒ ContractSigningPlansController chưa từng bị test authz khoá, và T1-T8 không thêm (nối H1).
|
||||||
|
|
||||||
|
**M10 — Câu III (ai bấm) đánh dấu ⏳ nhưng KHÔNG có cửa chặn, trong khi acceptance đã nướng sẵn đáp án.**
|
||||||
|
`cum3:56` "⏳ PENDING-OWNER"; B9 `:122` ghi "[⏳ câu III]". Nhưng: không có dòng OG mới trong `spec-4gd §④`; không nằm trong B0 5 mục (`:110`); checklist `:166` chỉ đòi câu giờ-G. Ngược lại checklist `:163` **chốt cứng** "nút chỉ `DaDuyet && can('Contracts','Create')`". ⇒ câu treo mà đáp án đã thành tiêu chí nghiệm thu; anh trả lời khác thì acceptance sai. Đề nghị: hoặc nâng thành OG-10 chặn K7-FE, hoặc bỏ ⏳ và khai "đã tự chốt theo l1-F15, anh phủ quyết sau được".
|
||||||
|
|
||||||
|
**M11 — QUYẾT-2 chọn `HopDongNhaCungCap` bằng lý do KỸ THUẬT (né hố mã "MB"), không phải nghiệp vụ.**
|
||||||
|
`cum3:103`: "abbr 'NCC' CÓ trong RG-001 v02 — né hố 'MB' không gốc quy định, S160". Hệ quả: (a) dry-run không đi qua loại HĐ mà nghiệp vụ mua vật tư có thể dùng thật (`HopDongMuaBan`); (b) hố "MB" tiếp tục không lộ. Đề nghị: khai 1 dòng cho anh — "chọn NCC vì mã MB chưa có trong quy định; nếu nghiệp vụ là MuaBan thì cần chốt token mã trước" — và để anh quyết, đừng để lựa chọn tài liệu pháp lý bị lái bởi codegen.
|
||||||
|
|
||||||
|
**M12 — DoD "HĐ đúng số lượng OG-1" mất phép đo; 2 định nghĩa "gộp" đang chỏi nhau.**
|
||||||
|
`spec-4gd:11` QĐ1: gộp theo **cặp (NCC × dòng-danh-mục)**. `cum3:41` guard-3: gộp chỉ theo **SupplierId** (không ràng buộc dòng-danh-mục). Hai định nghĩa cho ra số HĐ khác nhau khi 1 NCC trúng nhiều hạng mục. A1 (`:86`) đo "đúng N dòng **chọn**" = đo lại chính input của caller ⇒ không phép nào chứng được quy tắc cardinality. (UI 1-1 làm rủi ro thực tế thấp, nhưng DoD đang tự khai một thứ nó không đo.) Đề nghị: khai rõ "khoá gộp = SupplierId trong phạm vi 1 phiếu" và sửa DoD thành phát biểu đo được.
|
||||||
|
|
||||||
|
### LOW
|
||||||
|
|
||||||
|
- **L13** Guard-list 6 mục (`cum3:38-44`) thiếu load + NotFound cho `Project`/`Supplier`, trong khi codegen cần `project.Code`/`supplier.Code` (`ContractWorkflowService.cs:390`) và khuôn gốc CÓ 2 throw (`CreateContractFromEvaluationFeatures.cs` `?? throw NotFoundException("Project"…)` + `TryGetValue → NotFoundException("Supplier"…)`). "Copy trọn khuôn" mà rơi 2 throw.
|
||||||
|
- **L14** Checklist `:167` đòi `ss-k8-01..13` nhưng `ss-k8-07` chỉ sinh ra ở nhánh B "nếu còn giờ" (`:105`, `:120`) ⇒ nhánh A (nhánh CHÍNH) luôn thiếu 1 chứng ⇒ checklist không bao giờ tick sạch.
|
||||||
|
- **L15** Cùng 1 HĐ mang 2 con số khác thời-điểm: `GiaTri = Σ ApprovedAmount` (chốt lúc finalize) vs `BudgetManualAmount = Σ PeReferenceAmount` (snapshot lúc lập KHKK) — `cum3:46`, không acceptance nào phân biệt. Cùng lớp bệnh mà cụm-1 vá-8 đang lo (Σ×3, `spec-cum1:15`).
|
||||||
|
- **L16** Hệ quy chiếu test lệch giữa 2 cụm: cum3 dùng "≥ baseline+8" (`:92`, `:164`), spec-4gd/cum1 dùng "≥590+k" (`spec-4gd:24-26`, `spec-cum1:25`). Không quy đổi được nếu wave khác land xen giữa.
|
||||||
|
- **L17** `:110` B0(iv) khẳng định "KHÔNG có cơ chế tắt notification per-record" — khẳng-định-VẮNG-MẶT không kèm lệnh đo/0-hit + control dương. Rẻ: dán 1 grep.
|
||||||
|
- **L18** QĐ9 dặn né **#81-EXT** (grep MỌI Phase-assignment-site kể cả admin-override/seeder — `spec-4gd:19`). K8 là cửa cuối nhưng không có bước thử đường bypass (ví dụ admin đổi Phase trực tiếp → hook freeze có chạy?). Thuộc K2/K3 về mặt code, nhưng K8 nên có 1 dòng "đã xác nhận sweep, evidence: …".
|
||||||
|
|
||||||
|
### Claim ĐÃ THỬ BÁC MÀ ĐỨNG (giữ nguyên, đừng sửa)
|
||||||
|
|
||||||
|
- **P1** `cum3:48` "terminal V2 có guard `if (MaHopDong is null)` (`ContractWorkflowService.cs:384`)" — **ĐÚNG NGUYÊN DÒNG**: `:384 if (string.IsNullOrEmpty(contract.MaHopDong))` (gen ở `:390`). Thiết kế "gen mã ngay lúc bridge" an toàn khỏi double-gen.
|
||||||
|
- **P2** `cum3:131` "global filter làm rào `:342-344` giải phóng slot khi soft-delete" — **ĐÚNG**: rào dùng `db.ContractSigningPlans.AnyAsync(...)` KHÔNG `IgnoreQueryFilters` (cả file chỉ 1 hit `:767`, thuộc query khác). Lối thoát sqlcmd đứng.
|
||||||
|
- **P3** HỐ-1-KHKK **ĐÚNG**: `:1142-1145` nhánh owner/admin CHẠY TRƯỚC, `:1148` allow-list `{DangSoanThao, TuChoi}` áp cho **tất cả** ⇒ phiếu DaDuyet không xoá được kể cả Admin. Phát hiện mới, có giá trị.
|
||||||
|
- **P4** BÁC tiền đề K7(b) (`:16-21`) — đúng phương pháp (re-đo thay vì thi hành đề bài cũ), và việc CẤM đụng `GetEligiblePhases` (`:162`) là bảo vệ đúng chỗ.
|
||||||
|
- **P5** Mirror 2 app: `fe-user` và `fe-admin` `pages/khkk/KhkkDetailPage.tsx` cùng 26927 B / 687 dòng; neo `:291-318` đúng là khối `actions` của PageHeader (`:288-300` xác nhận) ⇒ K7.3 chỉ đúng chỗ.
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
(ĐANG LÀM)
|
**FAIL (chưa cho build) — 18 finding: 5 HIGH · 7 MED · 6 LOW; 5 claim chịu được phản-biện.**
|
||||||
|
|
||||||
|
Chia 2 nửa rất khác nhau:
|
||||||
|
- **K7 (bridge)**: nền CHẮC — re-đo 3 nấc thật, bác được tiền đề lỗi thời, khuôn/atomicity/codegen đúng, 5/5 claim tôi thử bác đều đứng. Chặn duy nhất là **H1** (tập khóa authz 2 vế chưa khai + FE gate lỏng + 0 test authz) và **M10/M12** (câu III treo mà acceptance đã chốt; DoD cardinality không đo được). Vá 3 chỗ này là K7 chạy được.
|
||||||
|
- **K8 (dry-run)**: **chưa chạy được như viết**. 12/14 bước không có người thật và không cửa nào bắt điền (**H5**); phép đo QĐ9 không thể trượt (**H2**); dấu ZZTEST không hiện trên list KHKK nên DoD tự khai là không đạt (**H3**); và câu hỏi trình anh thiếu đúng 2 hệ quả nặng nhất trên bản ghi thật (**H4**).
|
||||||
|
|
||||||
|
Ưu tiên vá: H4 → H2 → H1 → H5 → H3 → M6/M10.
|
||||||
|
|
||||||
|
END sub-reviewer-c3-l2 — TOTAL: 18 finding (5 HIGH · 7 MED · 6 LOW) + 5 positive.
|
||||||
|
|||||||
@ -1,12 +1,103 @@
|
|||||||
# c3-l3
|
# 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Ự.
|
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`
|
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
|
## Findings
|
||||||
|
|
||||||
(ĐANG LÀM)
|
### 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-policy** — `ContractsController.cs:13` class `[Authorize]` trần, `:49 [HttpPost]` trống (đối chứng dương: `:32` có `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.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.Read` ∧ `Contracts.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 **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-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ó **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` (+ 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
|
## Verdict
|
||||||
|
|
||||||
(ĐANG LÀM)
|
**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)
|
||||||
|
|||||||
Reference in New Issue
Block a user