wal: flush 20260728T1112

This commit is contained in:
pqhuy1987
2026-07-28 11:12:16 +07:00
parent f086698e8e
commit 1d6d7b9df3
6 changed files with 455 additions and 6 deletions

View File

@ -158,9 +158,20 @@ Lead trình 2 điểm ở §1.5 và §1.4; anh chốt **cả hai**. Đây là **
propose-only; spec-file do LEAD ghi sau khi verify (H21 ① honest-note c)
```
### Wave 2 — `/fable-clone reviewer` ensemble (đóng gói C2: ≤3 file/lane · ép khung rỗng lượt 1-2 · trần 25)
### Wave 2 — `/fable-clone reviewer` ensemble 4 lane (đóng gói C2: ≤3 file/lane · ép khung rỗng lượt 1-2 · trần 25)
*(4 lăng kính — label neo tại đây vì panel ephemeral; nội dung chốt sau khi đọc output wave 1)*
**Label C3 4-thành-phần — NEO TẠI ĐÂY** (panel ephemeral, không neo thì không tái lập/audit được):
| Label (neo C3) | Lăng kính | File được đọc (≤3) |
|---|---|---|
| `Gate reviewer lens-wave s157` | Wave-plan có khả thi không · acceptance có **ĐO ĐƯỢC thật** không hay chỉ nghe như đo được · wave có deploy độc lập thật không | `spec-ke-hoach-ky-ket-hd-28-07-2026.md` · `runs/2026-07-27-S155-pe-delete-approver/run.md` |
| `Gate reviewer lens-fidelity s157` | Spec có trung thực với **3 nguồn gốc** không — bịa bước / rơi bước / gán sai vai / nống biên | `spec-*.md` · `src-QT-TRINH-KY-HD.txt` · `src-FO-002.01-bang-kiem-tra-HD.txt` |
| `Gate reviewer lens-schema s157` | **7 bảng** thừa/thiếu · loose-Guid vs FK · UNIQUE filtered · enum mới có đúng không · biến thể 5 bảng | `spec-*.md` · `ApprovalWorkflow.cs` · `PurchaseEvaluation.cs` |
| `Gate reviewer lens-claims s157` | Verify ĐỘC LẬP mọi claim `file:line` trong §2 — có dương-giả không | `spec-*.md` · `ProposalFeatures.cs` · `MenuKeys.cs` |
**taskList snapshot (wave 2):** 4 lane · `role: reviewer` ×4 · `tier: 'opus'` explicit từng lane (belt-and-suspenders
theo GAP#6 — worker-tier-pin S124 đã che, nhưng lệnh này tự định nghĩa là "ensemble tier-2") · `maxTurns: 25` ·
lane RETURN-only, lead = single-writer refute+synthesize @P3/P4.
### Wave 3 — lead synthesize → `spec-ke-hoach-ky-ket-hd-28-07-2026.md` + **PLAN WAVE thi công trình anh**
@ -172,10 +183,28 @@ Lead trình 2 điểm ở §1.5 và §1.4; anh chốt **cả hai**. Đây là **
- [x] S1 — lead grounding: đọc TRỌN `ContractPhase.cs`**phát hiện A** (9 ISO ↔ 9 enum khớp 1:1) +
**phát hiện B** (khoảng trống nằm giữa ISO-1 và ISO-2, thiếu ở CẢ doc lẫn code)
- [x] S2 — scaffold run-folder + `run.md` (file này)
- [ ] S3 — `/fable-real investigator-codebase` deep-pass
- [ ] S4 — lead verify output S3 trên ĐĨA (byte + đối chiếu claim load-bearing)
- [ ] S5 — `/fable-clone reviewer` ensemble review
- [ ] S6 — lead refute + synthesize → spec cuối + plan wave → trình anh
- [x] S3 — `/fable-real investigator-codebase` deep-pass**3 lượt, 2 sự-cố, PARTIAL-HONEST**:
· lượt 1 **`skeleton-ruột-rỗng`**: 202K tok / 35 tool-use, đĩa chỉ **1.661 B khung rỗng 4 mục `[PENDING]`**
(return garble `#53` = mẩu "Đọc tiếp 3 file S156"). **Ghi-đĩa-trong-lúc-làm = CẦN KHÔNG ĐỦ** — phải
verify RUỘT có chữ. Header ĐÃ hấp thụ O-1/O-2 ⇒ SendMessage giữa chừng ăn.
· lượt 2 (resume, **cắt còn 2 mục** theo khuôn cứu S156) → **§A + §B CLEAN**, đĩa 1.661→**23.085 B**,
return khớp đĩa.
· lượt 3 (resume §C/§D) — **tiến-trình CLI thoát giữa chừng**, agent stopped no-completion-record.
Đĩa **KHÔNG đổi** (23.085 B, mtime 10:55) ⇒ §C/§D **chưa từng được ghi**. Mất = phần chưa làm, **0 mất
dữ-liệu đã có**. **KHÔNG resume lần 3** (nguyên-nhân ngoại-cảnh, nhưng H21 ① đã cho phép: engine
propose-only, **spec do LEAD ghi**; §A/§B CHÍNH LÀ phần propose).
- [x] S4 — lead verify ĐỘC LẬP 4 claim load-bearing trên ĐĨA (không tin return):
· **B.1 Proposal-lite phá OR-of-N** → ✅ THẬT: `ProposalFeatures.cs` `SelectMany` flatten `:427-429` +
`ElementAtOrDefault` **1 row** `:433` + so đúng **1** `ApproverUserId` `:439`; comment code TỰ THÚ
*"Lite version: assume 1 step per workflow"*.
· **slot enum 10 trống** → ✅ THẬT: `ApprovalWorkflow.cs:53-67` dừng ở `TravelRequest = 9`.
· **`Off_DeXuat` 4 key ĐỀU trong `All`** → ✅ THẬT `MenuKeys.cs:171`; **`Pe_*` sinh bằng factory
`:150`/`:154` NẰM NGOÀI `All`** ⇒ policy per-action không tồn tại — bẫy claim đúng.
· **Mig cuối = 68** → ✅ khớp `docs/STATUS.md` CURRENT STATE.
- [x] S5 — lead ghi spec `spec-ke-hoach-ky-ket-hd-28-07-2026.md` (3 heading H21 ①) — §A/§B từ invest
(đã verify) + **§C wave-plan + §D do LEAD viết** (đúng H21: propose ⟂ spec).
- [ ] S6 — `/fable-clone reviewer` ensemble chấm spec
- [ ] S7 — lead refute + synthesize → trình anh
> 🔴 **Luật của run này (vết S156):** CẤM điền sẵn kết-quả cho stage chưa chạy. Byte-count · verdict ·
> TOTAL chỉ được ghi SAU khi đo thật. Mọi stage chưa chạy giữ `[ ]`.

View File

@ -0,0 +1,340 @@
# SPEC — "KẾ HOẠCH KÝ KẾT HỢP ĐỒNG" (khúc còn thiếu giữa Duyệt NCC và HĐ NTP/NCC)
> **Run:** `2026-07-28-S157-ke-hoach-ky-ket-hd` · lead-written sau verify (H21 ①: engine propose-only ⟂ lead ghi spec)
> **Nguồn propose:** `sub-invest-kehoach-1.md` §A+§B (23.085 B, `/fable-real investigator-codebase` Fable)
> **Lead verify:** 4 claim load-bearing đo lại độc lập trên đĩa — xem `run.md` S4. §C+§D dưới đây **lead viết**.
> **Trạng thái:** CHỜ `/fable-clone reviewer` chấm + **5 câu owner** chưa trả lời (không chặn Wave 1-2).
---
## ① TÍNH NĂNG / ĐỀ BÀI
### 1.1 Vấn đề
Giữa **Duyệt NCC** (module PE, kết thúc `PE.DaDuyet`) và **Hợp đồng NTP/NCC** (module Contract, bắt đầu
`DangSoanThao`) có một khúc quy trình **KHÔNG TỒN TẠI Ở CẢ HAI TẦNG**:
- **Không có trong code** — repo 0-hit `shopdrawing|TVGS|duyệt mẫu`.
- **Không có trong quy trình ISO đang ban hành** — `QT TRINH KY HOP DONG TP-NCC.docx` bảng 2 đi THẲNG
bước 1 *"Lựa chọn NTP/NCC"* → bước 2 *"Soạn thảo hợp đồng"*, **0 bước trung gian**.
- **Chỉ có trên sơ đồ PDF** `01- Quy trinh hien tai --- 14_2026` (bản mới), các bước **b.7→12**.
🔴 ⇒ **Spec này vừa là spec phần mềm, vừa là ĐỀ XUẤT BỔ SUNG QUY TRÌNH ISO.** Anh cần duyệt ở cả 2 tư cách.
### 1.2 Tên + mục đích (owner đặt)
> **anh:** *"Bước này là bước **'đề xuất giá trị ký kết hợp đồng'**"* · sau đó gọi là **"Kế hoạch ký kết Hợp đồng"**.
Đây là phát biểu **MỤC ĐÍCH**: đầu ra của khúc này = **MỘT CON SỐ CÓ PHÊ DUYỆT** (giá trị ký kết đề xuất).
b.8-9 (duyệt mẫu / shopdrawing với TVGS) = **CĂN CỨ**; b.10-12 = **chuỗi duyệt chính con số đó**.
⇒ Vật mang trong hệ thống = **một PHIẾU**, không phải thư mục đính kèm.
### 1.3 Bối cảnh kiến trúc — 2 phát hiện nền (lead đo, invest xác nhận từ nguồn gốc)
**(A) 7 phase `[LEGACY]` của `ContractPhase` CHÍNH LÀ quy trình ISO 9 bước, mã-hoá cứng.** Khớp 1:1 đúng thứ tự:
`DangChon=1`↔Lựa chọn NTP/NCC · `DangSoanThao=2`↔Soạn thảo · `DangGopY=3`↔Góp ý · `DangDamPhan=4`↔Đàm phán ·
`DangInKy=5`↔In HĐ · `DangKiemTraCCM=6`↔Kiểm tra CCM · `DangTrinhKy=7`↔Duyệt BOD · `DangDongDau=8`↔Đóng dấu ·
`DaPhatHanh=9`↔Phát hành. Post-Mig 21/S17 thay bằng `ChoDuyet=10` + con-trỏ chạy trên workflow V2 admin-config
⇒ chuyển từ *cứng 9 bước* sang *cấu-hình N bước*. **Khung V2 sinh ra CHÍNH ĐỂ biểu diễn quy trình ISO này.**
**(B) Khoảng trống nằm ĐÚNG giữa ISO-1 và ISO-2** (xem 1.1).
### 1.4 🔴 OWNER ĐÃ CHỐT (2026-07-28) — ràng buộc cứng của spec
| # | Anh nói (verbatim) | Hệ quả spec |
|---|---|---|
| **O-1** | *"Đúng chính xác, cấu trúc trình ký giống, chỉ khác nội dụng thôi."* | **TÁI DÙNG khung duyệt V2** (`ApprovalWorkflow`>Step=Phòng>Level=NV, OR-of-N, `LevelOpinions`). **0 bảng approval mới.** Dựng mới = **NỘI DUNG phiếu**. ⇒ trục tranh luận Q2 hôm qua **ĐÓNG**. |
| **O-2** | *"Cái này để tham khảo thôi, ko vấn đề j, đa số là trễ."* (SLA) | **KHÔNG SLA-engine**, không auto-approve-on-timeout, không cảnh báo chặn, **không wave nào cho SLA**. Giữ hiển thị deadline. Lỗ `AddDays(7)` hardcode = ghi nhận, ngoài scope. |
| **O-3** | *"Chỗ hợp đồng cứ từ từ nhé."* | Lỗ hổng an ninh `ContractWorkflowService.cs:48-66` + controller HĐ class-trần: **NGOÀI SCOPE**, treo có chủ đích. Wave nào chạm module Contract phải hỏi lại anh. |
### 1.5 Quy trình chi tiết — 6 bước (b.7→12)
Role hệ thống có thật (`AppRoles.cs:5-16`): `Procurement` (PMH) · `CostControl` (CCM) · `Director`+`AuthorizedSigner`
(BOD/NĐUQ) · `ProjectManager`/`Drafter` (BCH) · `HrAdmin` · `Admin`.
🔸 **"BCH dự án X" KHÔNG có model** (`Department.ManagerUserId` 0-row) ⇒ né bằng: **BCH = người tạo phiếu**
(`CreatedBy`); PMH/CCM/CEO = **đích danh user trong Level** của workflow V2 (đúng triết lý V2, **0 model mới**).
| b | Việc | Ai | Đầu vào | Đầu ra | Chuyển bước | Nhánh lỗi |
|---|---|---|---|---|---|---|
| 7 | PMH chuyển thông tin cho BCH | Procurement | PE `DaDuyet` (winners `IsWinner`, giá `SUM Quote.IsSelected`, HoSoLink) | BCH biết việc | **Pull-model**: màn *"PE đã duyệt — chờ lập KH"* lọc `Phase==DaDuyet && chưa có plan` (khuôn `CreateContractFromEvaluationFeatures.cs:173`) + notify in-app. **Email ra ngoài = ngoài scope** (SMTP TODO toàn hệ thống) | PE thiếu winner → không hiện |
| 8 | Duyệt mẫu + shopdrawing với TVGS | BCH **nhập hộ** (TVGS ngoài hệ thống) | Mẫu + shopdrawing từ NTP/NCC (offline) | Danh mục **CĂN CỨ** trong phiếu: {Kind, NCC, Status, TvgsName, ngày, scan} | Vòng đời **per-item**: `ChuaNop→DaNop→TvgsDuyet\|TvgsBac`; `TvgsBac→DaNop` (nộp lại) — 🔴 **sơ đồ KHÔNG vẽ vòng lặp này, thực tế có** | Item bị bác = đổi status + note, **KHÔNG đụng state machine phiếu** |
| 9 | Tổng hợp + so sánh giá → **ĐỀ XUẤT giá trị** | BCH (Drafter phiếu) | Căn cứ b.8 + báo giá PE (**snapshot** `PeReferenceAmount` lúc tạo — nguyên tắc freeze Mig 67) | Phiếu đủ dòng giá đề xuất per-winner + file so sánh → **TRÌNH** | Guard trình: đủ dòng giá per-winner (mirror completeness-guard PE `PurchaseEvaluationWorkflowService.cs:180-212`); căn cứ chưa đủ `TvgsDuyet`**cảnh báo mềm** (tiền lệ S62) — **O-Q1 owner quyết mềm/cứng** | Ghi chú sơ đồ *"hồ sơ sai sót → gửi mail làm rõ"* = comment + item `TvgsBac` |
| 10 | PMH kiểm tra, xác nhận giá trị | Procurement (đích danh Level) | Phiếu `ChoDuyet` Bước 1 | Duyệt → Bước 2; ý kiến vào `LevelOpinions` (UPSERT khuôn `ContractWorkflowService.cs:292-316`) | OR-of-N trong Cấp (`ApprovalWorkflow.cs:81-94`) | Trả lại → `TraLai`, hoặc 4 return-mode per-level (cờ CÓ SẴN `ApprovalWorkflowLevel.cs:116-125`) |
| 11 | CCM kiểm tra | CostControl | Bước 2 | Duyệt → Bước 3 | như trên | như trên |
| 12 | **CEO ký HOẶC CCM đóng dấu** | Director / cơ chế kết thúc sớm | Bước 3 (hoặc kết thúc tại Bước 2) | 🔴 **GIÁ TRỊ KÝ KẾT CHỐT** per-winner (`ApprovedAmount`) ghi tại **finalize choke-point** (mirror `ApplyApprovedPriceOnFinalize` PE `:973-1002` + RULE `:1008` *mọi nhánh set DaDuyet phải gọi helper*) | Terminal `DaDuyet` | Từ chối → `TuChoi`; trả lại → `TraLai` |
**Rẽ nhánh b.12 "hoặc" — 4 đường ĐỀU CÓ SẴN trong khung V2, 0 migration:**
(i) `CeoApprovalThreshold` + CCM tích "duyệt done miễn CEO" (PE `:892-929`, fail-closed: đòi threshold + role
`CostControl` + giá < ngưỡng) · (ii) `AllowApproverFinalize` per-slot (PE `:870-890`) · (iii)
`AllowApproverSkipToFinal` per-slot, CEO vẫn thật (Contract `:320-352`) · (iv) 2 workflow (/không Step CEO)
admin pin. 🔸 **Điều kiện rẽ THẬT: cả 3 nguồn IM LẶNG** **Q3 owner**; chọn đường nào **cũng không đổi schema**.
### 1.6 b.8-9 là **bảng con trong phiếu** — không phải phiếu con, không phải module riêng
do: (1) owner phát biểu b.8-9 = **căn cứ**, không phải đối tượng duyệt riêng; (2) PDF chỉ vẽ **MỘT** chuỗi
duyệt (10-12) cho cả khúc phiếu con = đẻ chuỗi duyệt thứ 2 không trên giấy; (3) vòng đời mẫu/shopdrawing
**per-item status**, không phải state machine phiếu; (4) TVGS ngoài hệ thống chỉ cần **ghi vết**.
Module riêng chỉ đáng khi mẫu/shopdrawing cần quản **độc lập với gói thầu** chưa yêu cầu.
### 1.7 Mâu thuẫn / im lặng giữa 3 nguồn (bảng tra — KHÔNG tự quyết)
| # | Điểm | ISO doc | PDF | FO-002.01 | Xử |
|---|---|---|---|---|---|
| 1 | Khúc b.712 | **KHÔNG CÓ** | (duy nhất) | không nói | Spec = đề xuất bổ sung ISO |
| 2 | SLA khúc 712 | im lặng | im lặng | im lặng | **O-2**: tham khảo, không hoà giải |
| 3 | SLA khúc | 07/07/07/07/03/01 ngày | 3d + 7-10d | 🔴 *"mỗi bộ phận 01 ngày, quá hạn **XEM NHƯ ĐÃ THÔNG QUA**"* | 3 nguồn 3 kiểu; auto-pass **mâu thuẫn mô hình duyệt-chặn V2**. O-2 chỉ ghi nhận |
| 4 | Điều kiện rẽ b.12 | | "hoặc", 0 điều kiện | không nói | **Q3 owner** |
| 5 | Nhánh trả lại 10/11/12 | không vẽ | không vẽ | không vẽ | Hệ thống sẵn `TraLai` + 4 return-mode |
| 6 | Bypass Chủ đầu | không nói | không nói | | = `Contract.BypassProcurementAndCCM` (đã ) **KHÔNG áp phiếu KH** |
| 7 | Checklist kiểm tra per-phòng (PRO: điều khoản/thanh toán/rủi ro pháp · CCM: giá vs NS) | tả lời | không | (checkbox từng mục) | Hệ thống chỉ `LevelOpinions.Comment` tự do **GAP**, **O-Q2 owner** số hoá checkbox không |
| 8 | Tên phòng/vai | PRO/CCM/BOD/NĐUQ/HRA | Procurement/CCM/CEO | PB-DA/PRO/CCM/ | Khớp `AppRoles` **0 mâu thuẫn** |
| 9 | Chuỗi FO-002.01 (ĐỀ XUẤTPROCCM) | ISO 5-7 | b.9101112 **CÙNG HÌNH** | | **Đối chứng GIẤY cho O-1** |
---
## ② CÁCH IMPLEMENT
### 2.1 ⚠️ BẪY SỐ MỘT — CẤM copy "Đề xuất" (Proposal) cho phần DUYỆT
`ProposalFeatures.cs` twin **nhỏ nhất** (849 LOC BE) nên rất cám dỗ. **Nhưng phần duyệt của nó phá OR-of-N:**
`SelectMany` flatten mọi Level xuyên Steps `:427-429` `ElementAtOrDefault(order-1)` lấy **1 row** `:433`
so đúng **1** `ApproverUserId` `:439`. Comment trong chính file **tự thú**: *"Lite version: assume 1 step per
workflow"*. Entity Proposal cũng **chỉ có `CurrentApprovalLevelOrder`, KHÔNG có `CurrentWorkflowStepIndex`**.
2 người cùng Cấp (OR-of-N hợp lệ theo `ApprovalWorkflow.cs:81-94`) bị biến thành **2 nấc TUẦN TỰ (AND)**.
nhiều người cùng cấp **chính là thứ tờ FO-002.01 cần**.
🔴 **Service PHẢI copy `ContractWorkflowService.ApproveV2Async:217-394`** (con-trỏ ĐÔI StepIndex+LevelOrder ·
`GroupBy` Order `:246` · OR-of-N `:259-260` · `LevelOpinions` UPSERT `:292-316` · skipToFinal `:320-352`).
**Proposal chỉ làm khuôn cho CRUD / DTO / menu / FE.**
> ✅ *Lead đã tự đọc `ProposalFeatures.cs:420-445` xác minh — claim ĐÚNG, không phải suy diễn.*
### 2.2 Schema — Mig **69** `AddContractSigningPlans` · **7 CreateTable, 0 ALTER bảng cũ**
Down = `DropTable ×7`, reversible sạch. Convention: PascalCase EN · **loose-Guid không FK vật lý** sang
PE/Supplier (convention PE, ghi Mig 49) · FK vật chỉ nội bộ module + sang bảng V2 dùng chung.
| # | Bảng | Cột chính | Ràng buộc |
|---|---|---|---|
| 1 | `ContractSigningPlans` (header) | `MaKeHoach?` · `PurchaseEvaluationId` (loose+IX) · `ProjectId` (denorm lọc) · `Phase int` · `ApprovalWorkflowId Guid?` · `CurrentWorkflowStepIndex int?` · `CurrentApprovalLevelOrder int?` · `DrafterUserId` · `SlaDeadline?` · `HoSoLink nvarchar(1000)?` · `GhiChu` | `AuditableEntity` + `HasQueryFilter(!IsDeleted)` (khuôn `PurchaseEvaluationConfiguration.cs:84`) |
| 2 | `ContractSigningPlanLines` (giá per-winner) | `PlanId` FK Cascade · `SupplierId` loose · **`PeReferenceAmount` decimal(18,2) SNAPSHOT lúc tạo** · `ProposedAmount` · `ApprovedAmount?` (chỉ ghi tại finalize) · `Note` | UNIQUE filtered `(PlanId, SupplierId) WHERE IsDeleted=0` (gotcha #57) |
| 3 | `ContractSigningPlanDossierItems` (căn cứ b.8-9) | `PlanId` FK Cascade · `Kind int` (MauVatLieu=1/Shopdrawing=2/Khac=99) · `SupplierId?` · `Name` · `Status int` (ChuaNop=0/DaNop=1/TvgsDuyet=2/TvgsBac=3) · `TvgsName?` · `TvgsResultAt?` · `Note` | IX (PlanId) |
| 4 | `ContractSigningPlanLevelOpinions` | `PlanId` + `ApprovalWorkflowLevelId` · `Comment nvarchar(2000)` · `SignedAt` · `SignedByUserId` · `SignedByFullName` | UNIQUE (PlanId, LevelId); FK **Cascade Plan + Restrict Level** (mirror `ContractLevelOpinions` Mig 33) |
| 5 | `ContractSigningPlanAttachments` | `PlanId` FK Cascade · FileName/Path/Size/ContentType · `Purpose int` · `DossierItemId?` | khuôn `ProposalAttachment.cs` |
| 6 | `ContractSigningPlanChangelogs` | `PlanId` · Action · PhaseAtChange · UserId · Summary · ContextNote | **có từ ngày 1** tiền lệ PE: Changelogs nguồn transition-marker/backfill (Mig 60/61) |
| 7 | `ContractSigningPlanCodeSequences` | `Prefix` PK · `LastSeq` | khuôn `ProposalCodeSequence.cs`; format đề xuất **`KHKK/{YYYY}/{Seq:D3}`** **owner chốt format** |
**Biến thể tối thiểu 5 bảng** (bỏ #6 + #7) **KHÔNG khuyến nghị**: mất audit-trail transition (Mig 60/61 chứng
minh cần) mất phiếu atomic. Nếu owner muốn cắt, cắt #7 trước ( tay), giữ #6.
**Enum:**
- `ApprovalWorkflowApplicableType` **+`ContractSigningPlan = 10`** append-only. *Lead verify: `ApprovalWorkflow.cs:53-67` dừng ở `TravelRequest = 9`, **slot 10 TRỐNG THẬT**.*
- `ContractSigningPlanPhase` **MỚI**, giá trị sạch: `DangSoanThao=1 / ChoDuyet=2 / DaDuyet=3 / TraLai=98 / TuChoi=99`.
**KHÔNG copy 7/98 của PE** (sẹo lịch sử enum PE phiếu mới không nợ data ); giữ 98/99 cho quen mắt FE badge.
- 🔴 **KHÔNG thêm cột `SigningPlanId` lên PE** tra ngược bằng `Plans.Where(PurchaseEvaluationId==x)`.
Thêm cột = đẻ *"cột có mà 0 ai đọc"* (lớp lỗi đã cắn **6 lần** repo này).
### 2.3 CQRS + Service + Controller
**`Application/ContractSigningPlans/ContractSigningPlanFeatures.cs`** (mega-file, khuôn `ProposalFeatures.cs`):
- `CreateContractSigningPlanCommand(PeId, ApprovalWorkflowId, GhiChu?)` validator: PE tồn tại + `Phase==DaDuyet`
(mirror cầu `CreateContractFromEvaluationFeatures.cs:53-54`) + **chưa có plan sống**
(`AnyAsync(p.PeId==x && p.Phase!=TuChoi)` Conflict; cho tạo lại sau `TuChoi`) + workflow
`ApplicableType==ContractSigningPlan` (mirror Proposal `:258-265`).
Handler **auto-sinh Lines từ winners**: `pe.Suppliers.Where(IsWinner)`, `PeReferenceAmount = SUM Quote.IsSelected`
per winner (copy đúng phép tính cầu `:56-62`, `:88-90`) **snapshot 1 lần**.
- `UpdateContractSigningPlanDraftCommand` guard `Phase ∈ {DangSoanThao, TraLai}` + **cho re-pin
`ApprovalWorkflowId`** (mirror PE; tránh vết Contract UpdateDraft KHÔNG đường re-pin).
- `Get` / `List` / `pendingMe` detail Include Lines+DossierItems+Attachments+LevelOpinions + workflow-tree
(khuôn Proposal `:161-228`); 🔴 **inbox V2 precompute TỪ NGÀY 1** (mirror PE `ResolveV2InboxIdsAsync`)
bẫy đã cắn Contract: thiếu cái này **approver không mở được phiếu đang chờ chính mình**
(`ContractFeatures.cs:363-372` legacy-only, `grep ChoDuyet` = 0 hit).
- `UpsertDossierItemCommand` / `DeleteDossierItemCommand` chỉ mở `DangSoanThao|TraLai` (căn cứ **input**
của con số đang duyệt). Attachment mở mọi phase (triết PE S147: BE không phase-guard attachment).
**`Infrastructure/Services/ContractSigningPlanWorkflowService.cs`** copy `ApproveV2Async:217-394` 2.1), adapt 3 chỗ:
1. Terminal `Phase=DaDuyet` + **`ApplyApprovedValuesOnFinalize`**: mọi Line `ApprovedAmount ??= ProposedAmount`
tại **choke-point duy nhất** + RULE grep-4-site (khuôn `PurchaseEvaluationWorkflowService.cs:1008`).
2. Port 2 nhánh kết thúc sớm: `AllowApproverFinalize` `:870-890` + `finalizeByCcmDelegation`+`CeoApprovalThreshold`
`:892-929` (tổng so ngưỡng = `SUM ProposedAmount`).
3. Reject 4 return-mode per-level (cờ sẵn `ApprovalWorkflowLevel.cs:116-125`, logic mirror PE
`ApplyReturnModeAsync:368-547`) **tối thiểu `TraLai` toàn phần** nếu muốn gọn wave đầu.
🔴 **Guard trình = `CreatedBy==actor DeptManager Admin`** **CỐ Ý KHÁC** khuôn role-based
(`ContractWorkflowService.cs:70-79` đòi `Drafter|DeptManager`) để **né lớp 403-PMH**: PMH mang role `Procurement`
nên bị chặn b.13/b.17 đúng lớp *"cơ-chế đúng, vai đi qua không có"*. Nếu owner muốn đồng nhất role-based thì
BCH users **phải được gán role `Drafter`**.
🔴 **Notify:** mirror `LogTransitionAsync` báo Drafter (`ContractWorkflowService.cs:407-427`) **+ THÊM notify đích
danh approver của Cấp kế** fix lỗ *notify-chỉ-Drafter* **ngay từ ngày 1**, đừng chép nợ.
**`Api/Controllers/ContractSigningPlansController.cs`** 🔴 **authz 2 TẦNG** (bài học #82 / S118):
class `[Authorize]` **+ per-action** `[Authorize(Policy = "ContractSigningPlans.{Read|Create|Update|Delete}")]`.
**KHÔNG chép kiểu `ContractsController.cs:12-13` class-trần** (đó chính lỗ đang treo). Policy TỰ SINH khi key
vào `MenuKeys.All` (`Program.cs:82-89` foreach `All × Actions`).
Endpoints: `GET` list/inbox · `GET {id}` · `POST` · `PUT {id}` · `POST {id}/transitions` · dossier-items CRUD ·
attachments · `DELETE {id}` (allow-list `DangSoanThao|TuChoi`, mirror PE `:1404`).
### 2.4 Menu + Permission + FE 2 app (Pattern 16-bis 4-place mirror)
- **BE `MenuKeys.cs`:** +4 const `Csp` / `Csp_List` / `Csp_Create` / `Csp_Inbox` **VÀ đưa CẢ 4 vào mảng `All`**.
*Lead verify `MenuKeys.cs:171`: `OffDeXuat`+3 con **đều trong `All`**; còn `Pe_*` sinh bằng **factory**
`:150`/`:154` **NẰM NGOÀI `All`** per-action policy **không tồn tại**.* **Theo khuôn `Off_DeXuat`, KHÔNG theo `Pe_*`.**
2 row canonical `docs/STATUS.md` (Menu keys · Policies = |All|×4) phải bump cùng lúc.
- **Seed `DbInitializer.cs`:** 4 dòng menu (khuôn Off_DeXuat `:1817-1820`, label VN *"Kế hoạch ký kết HĐ"* /
*"Danh sách"* / *"Tạo mới"* / *"Inbox duyệt"*) + admin-permission (`:2383` **grep MỌI site seed-permission**,
S155 ghi nhận 2 site cho vài nhóm). **Dark-launch được bằng `IsVisible=0`** rồi bật sau.
- **FE ×2 app** (duplicate CHỦ ĐÍCH), mỗi app 4 chỗ: `App.tsx` routes · `Layout.tsx` staticMap ·
`lib/menuKeys.ts` · `pages/` ×3 (khuôn bộ Proposal ~978 LOC/app: List 252 + Create 245 + Detail 386 + types 95).
Detail thêm: bảng **Lines** (đề xuất/chốt per-NCC) · bảng **DossierItems** (status chip + TVGS + scan) ·
**panel workflow** (mirror PE Panel-3 ✓/●/○) · **banner "Đến lượt bạn"** (khuôn PE `blockedByV2Level`).
- **Designer admin nhận type 10:** fe-admin `ApprovalWorkflowsV2Page.tsx` map typeCodeint `:143` + các site BE
`ApprovalWorkflowV2AdminFeatures.cs`. **Invest tự khai KHÔNG verify được** path `fe-user` matrix-view
(`WorkflowMatrixViewPage.tsx` grep 0 hit) **PHẢI grep lại lúc làm**, đừng tin line .
### 2.5 Cầu tiếp giáp b.12→13 (sửa 1 file) — ⚠️ chạm module Contract
`CreateContractFromEvaluationFeatures.cs`:
(a) đọc plan `DaDuyet` của PE thì `giaTri = line.ApprovedAmount` khớp `w.SupplierId` **thay** `SUM` `:88-90`,
ghi **ContextNote lệch** *"giá KH x vs SUM-PE y"* vào changelog `:121-140` (khuôn audit-note D4 S134);
không plan giữ `SUM` + **cảnh báo mềm** (**Q11** owner nâng cứng sau).
(b) `+param ApprovalWorkflowId?` **pin V2 cho HĐ** hiện pin V1-only `:68-71`,`:108` V2-less trình xong
**KẸT CỨNG** `ConflictException` `ContractWorkflowService.cs:115-116`.
🔴 **Đây là "chỗ hợp đồng"** anh chốt *"cứ từ từ"* **wave này phải hỏi lại anh trước khi chạy**.
---
## ③ CHECKLIST — CHIA WAVE (lead viết)
**Nguyên tắc chia:** mỗi wave **deploy độc lập được**, không để nửa tính năng chết trên prod (bài học PE nút-xóa
S155: §F.0 chứng minh đợt-1-một-mình dụng phải chia theo *"đẩy riêng thì người dùng thấy gì"*).
| Wave | Tên | Chặn bởi owner? | Deploy riêng được? |
|---|---|---|---|
| **W1** | Schema + permission (dark-launch) | không | ( hình với user) |
| **W2** | CRUD phiếu nháp + căn cứ b.8-9 | không | (bật menu, tạo được phiếu) |
| **W3** | Duyệt 3 trạm + finalize chốt giá | **Q3** (chọn chế rẽ b.12) nhưng chạy được với mặc định | (phiếu chạy trọn vòng) |
| **W4** | Gate + checklist per-phòng | **O-Q1, O-Q2** | (siết dần) |
| **W5** | Cầu sang Hợp đồng | **Q6, Q11 + anh cho phép chạm HĐ** | |
---
### WAVE 1 — Schema + permission scaffold (dark-launch)
- **Mục tiêu:** đặt nền DB + quyền, **không đổi gì người dùng thấy**.
- **Vai:** `implementer-backend` · review `database-agent` (design-review migration trước apply).
- **File:** 7 entity + `ContractSigningPlanPhase.cs` + 7 EF Configuration + `ApplicationDbContext`/`IApplicationDbContext`
DbSets + Mig 69 (3-file rule) + `ApprovalWorkflow.cs` enum +slot 10 + `MenuKeys.cs` +4 const **vào `All`** +
`DbInitializer` menu 4 dòng `IsVisible=0` + admin-permission.
- **Acceptance ĐO ĐƯỢC:**
1. `dotnet ef migrations list` `69_AddContractSigningPlans` cuối.
2. `sqlcmd -Q "SELECT COUNT(*) FROM sys.tables"` **89 → 96** (+7).
3. `dotnet test SolutionErp.slnx` **562 PASS giữ nguyên, 0 fail** (chưa test mới).
4. `sqlcmd` đếm policy: `|MenuKeys.All| × 4` tăng đúng **+16**; grep `ContractSigningPlans.Read` trong list policy.
5. Menu **KHÔNG hiện** trên cả 2 FE (IsVisible=0) screenshot hoặc `GET /api/menus` không chứa `Csp`.
6. `dotnet ef migrations script 68 69` + script Down chạy sạch trên DB copy (reversible thật, không chỉ nhìn code).
- **Rủi ro:** seed-permission **>1 site** (S155) ⇒ grep hết trước khi kết luận. · Enum slot 10 nếu ai đó chèn
trước ⇒ re-verify `ApprovalWorkflow.cs` ngay trước khi viết.
### WAVE 2 — CRUD phiếu nháp + căn cứ b.8-9 (bật menu)
- **Mục tiêu:** BCH **tạo được phiếu nháp**, điền dòng giá đề xuất + danh mục căn cứ, **chưa trình được**.
- **Vai:** `implementer-backend` (Features + Controller) · `implementer-frontend` (3 page × 2 app) ·
`test-specialist` (test-after).
- **File:** `ContractSigningPlanFeatures.cs` (Create/Update/Get/List + DossierItem CRUD + attachment) ·
`ContractSigningPlansController.cs` (**2-tầng authz**) · FE ×2 app 4 chỗ · `DbInitializer` bật `IsVisible=1`.
- **Acceptance ĐO ĐƯỢC:**
1. `POST /api/contract-signing-plans` từ PE `DaDuyet`**201**, response có `id`; DB có **N Lines = số winner**
của PE đó (`sqlcmd` đếm đối chiếu `PurchaseEvaluationSuppliers WHERE IsWinner=1`).
2. `PeReferenceAmount` mỗi Line == `SUM(Quotes WHERE IsSelected)` của đúng NCC đó (`sqlcmd` 2 câu, so khớp).
3. Tạo lần 2 trên **cùng PE****409 Conflict**.
4. Tạo từ PE **chưa `DaDuyet`****400/409** (không phải 500).
5. 🔴 **Authz 2 tầng:** user thiếu quyền gọi `POST`**403** (không phải 200/401). User có quyền → 201.
*(Đây là phép chặn lỗ #82 tái phát — bắt buộc chạy, không được bỏ.)*
6. FE 2 app: `npm run build` PASS ×2 · SHA256 3 file mirror **IDENTICAL**.
7. Menu hiện đúng 4 mục tiếng Việt trên cả 2 app.
- **Rủi ro:** quên `HasQueryFilter(!IsDeleted)` ⇒ phiếu xóa mềm vẫn hiện. · UNIQUE filtered thiếu
`WHERE IsDeleted=0` ⇒ tạo lại sau xóa bị chặn oan (gotcha #57).
### WAVE 3 — Duyệt 3 trạm + finalize chốt giá 🔴 **wave nặng nhất**
- **Mục tiêu:** phiếu chạy trọn `DangSoanThao → ChoDuyet → DaDuyet`, chốt `ApprovedAmount`.
- **Vai:** `implementer-backend` (service) · `implementer-frontend` (panel + banner) · `test-specialist`
(**test-before cho OR-of-N** — đây là critical algorithm) · `reviewer` (adversarial trước commit).
- **File:** `ContractSigningPlanWorkflowService.cs` (copy `ApproveV2Async:217-394`) · `transitions` endpoint ·
inbox V2 precompute · notify · FE workflow panel + banner "Đến lượt bạn".
- **Acceptance ĐO ĐƯỢC:**
1. 🔴 **OR-of-N còn sống** *(phép chặn bẫy §2.1)*: workflow 1 Bước có **1 Cấp 2 người** → **người thứ hai
duyệt được** và phiếu **tiến sang Bước kế** (không đòi cả 2 ký). Test unit + 1 lượt UAT thật.
2. **Con-trỏ đôi:** workflow **3 Bước** → sau mỗi lượt duyệt, `CurrentWorkflowStepIndex` **và**
`CurrentApprovalLevelOrder` đều đổi đúng (`sqlcmd` đọc 2 cột sau mỗi bước).
3. **Finalize choke-point:** duyệt cấp cuối → **mọi** Line có `ApprovedAmount != NULL`
(`sqlcmd COUNT(*) WHERE ApprovedAmount IS NULL` = **0**). Test cả 3 đường tới `DaDuyet`
(thường · `AllowApproverFinalize` · `CeoApprovalThreshold`) — **grep 4-site RULE**.
4. **Inbox:** approver Cấp đang chờ gọi `GET /inbox`**thấy phiếu**; approver cấp khác → **không thấy**.
*(Phép chặn đúng lỗ Contract đang mắc.)*
5. `LevelOpinions` UPSERT: duyệt 2 lần cùng cấp → DB **1 row** (không đúp), `SignedByUserId` đúng người thật.
6. Trả lại → `TraLai`, sửa rồi gửi lại chạy **từ Cấp 1 Bước 1**.
7. Notify: approver Cấp kế **nhận** thông báo (không chỉ Drafter).
8. `dotnet test`**562 + N mới**, 0 fail.
- **Rủi ro:** 🔴 **copy nhầm Proposal** ⇒ mất OR-of-N **âm thầm** (build sạch, test cũ vẫn xanh) — acceptance #1
là lưới duy nhất bắt được. · Quên inbox precompute ⇒ lặp lại đúng lỗi Contract.
### WAVE 4 — Gate trình + checklist per-phòng ✅ **CHẶN: O-Q1, O-Q2**
- **Mục tiêu:** siết chất lượng đầu vào theo ý anh.
- **Chờ anh:** O-Q1 căn cứ b.8 chưa đủ `TvgsDuyet`**cảnh báo mềm hay chặn cứng**? · O-Q2 checkbox
FO-002.01 (PRO: điều khoản/thanh toán/rủi ro pháp lý · CCM: giá vs NS) → **số hoá thành checklist** hay
giữ **comment tự do**?
- **Acceptance (khi có câu trả lời):** trình thiếu căn cứ → đúng hành vi anh chọn (toast cảnh báo **hoặc** 409);
checklist (nếu số hoá) lưu + hiện lại đúng trên phiếu đã duyệt.
### WAVE 5 — Cầu sang Hợp đồng ✅ **CHẶN: Q6, Q11 + anh cho phép chạm HĐ**
- 🔴 **Wave này chạm module Contract — anh đã chốt *"chỗ hợp đồng cứ từ từ"*. KHÔNG tự chạy.**
- **Chờ anh:** Q6 giá vào HĐ = `ApprovedAmount` của KH (mặc định thiết kế: **KH thắng** + audit lệch) hay
vẫn `SUM`-PE? · Q11 chưa có KH duyệt thì tạo HĐ = **chặn** hay **cảnh báo**?
- **Acceptance:** HĐ tạo từ PE có KH `DaDuyet``GiaTri == SUM(ApprovedAmount)`; changelog có ContextNote lệch;
HĐ pin được workflow V2 (không còn `ConflictException` kẹt cứng).
---
### Ngoài mọi wave (ghi nhận, KHÔNG làm trong run này)
- **SLA** — O-2: chỉ hiển thị. `AddDays(7)` hardcode vẫn sai nhưng **không ai enforce** ⇒ không đáng 1 wave.
- **Lỗ hổng an ninh HĐ** (`ContractWorkflowService.cs:48-66` Reject-trước-guard + controller class-trần) —
**vẫn SỐNG trên prod**, anh chốt *"từ từ"*. Nhắc đúng 1 dòng cho khỏi quên.
- **FO-002.01 auto-pass** (*"quá hạn xem như đã thông qua"*) — mâu thuẫn mô hình duyệt-chặn V2. O-2 ⇒ ghi nhận.
---
## ④ §D — HẤP THỤ 6 việc sửa của `review-synthesis.md` §G (S156)
| # | Việc review yêu cầu | Xử lý trong spec này |
|---|---|---|
| 1 | Claim SAI *"hardcoded policy fallback"* → thật là `ConflictException:115-116` (HĐ **kẹt cứng** `ChoDuyet`) | ✅ **Đã sửa** — §2.5(b) ghi đúng: pin V1-only ⇒ HĐ V2-less **KẸT CỨNG**, hỏng **cứng** không degrade êm. Lead tự xác minh trên đĩa. |
| 2 | Siết biên `b.14→19`, không phải `b.13→21` | ✅ Spec này **không nhắc lại biên cũ**; phạm vi khai rõ = **b.7→12**, khúc 13→21 để nguyên verdict `LAI` S156. |
| 3 | Thiếu Q **vai người TRÌNH** (PMH `Procurement` bị 403 `:70-79`) | ✅ **Đã xử ở thiết kế**, không đẩy thành câu hỏi: §2.3 guard trình = `CreatedBy DeptManager Admin` **cố ý khác** khuôn role-based, kèm lối thoát nếu owner muốn đồng nhất. |
| 4 | Thêm cảnh báo lỗ hổng reject — *việc RIÊNG* | ✅ Giữ đúng 1 dòng ở "Ngoài mọi wave"; **không nhét vào wave** (anh chốt *"từ từ"*). |
| 5 | Q5 thiếu phương án **(d) `skipToFinal`** đã wire | ✅ Có mặt — §1.5 liệt **4 đường rẽ b.12**, trong đó (iii) `AllowApproverSkipToFinal` `Contract:320-352`. |
| 6 | Viết lại Q2 theo **2 trục + 3 số đo cùng đơn vị từ twin thật** | ⚠️ **Trục cơ-chế ĐÓNG bởi O-1** (anh chốt tái dùng khung) ⇒ chỉ còn **trục NỘI DUNG**. Bảng dưới. |
### Trục NỘI DUNG — 3 nhánh, 3 số cùng đơn vị
> Số twin **đo thật**: `Proposal` = **4 bảng · 849 LOC BE · 1.956 LOC FE** (invest `wc`/`grep` trực tiếp).
> Số cho từng nhánh = **ngoại suy** từ twin đó — khai rõ đâu là đo, đâu là ước.
| Nhánh | Bảng mới | LOC BE (ước) | LOC FE (ước) | Đánh giá |
|---|---|---|---|---|
| **(A) Phiếu độc lập** ⬅ *spec này chọn* | **7** (5 nếu cắt) | ~1.1001.400 | ~2.0002.400 | Vòng đời riêng, xóa/soft-delete riêng, workflow riêng type 10. **Đắt nhất về bảng, rẻ nhất về rủi ro.** |
| **(B) Mở rộng PE** (thêm cột + bảng con vào phiếu PE) | ~3 | ~600800 | ~9001.200 | Rẻ hơn, **nhưng**: PE `DaDuyet`**terminal thành công** — mở lại vòng duyệt thứ 2 trên phiếu đã đóng ⇒ phá bất-biến PE, đụng cả `budgetFrozen` snapshot Mig 67. **Rủi ro cao nhất.** |
| **(C) Entity con của Contract** | ~4 | ~700900 | ~1.1001.400 | Kéo khúc 7→12 vào module HĐ — mà anh vừa chốt **"chỗ hợp đồng cứ từ từ"**; đồng thời HĐ chưa tồn tại ở b.7-12 (HĐ mới sinh ở b.13) ⇒ **sai thứ tự nghiệp vụ**. |
**Lý do chọn (A)** — không phải vì rẻ, mà vì: nó là **PHIẾU** theo đúng phát biểu mục đích của anh (có đối tượng
được duyệt + 3 trạm + người chốt); (B) phá bất-biến của PE đang chạy ổn; (C) sai thứ tự nghiệp vụ và chạm vùng
anh vừa yêu cầu để yên. **Nếu owner ưu tiên cắt chi phí thì cắt trong (A)** (7→5 bảng), đừng đổi sang (B)/(C).
---
<!-- END · 5 WAVE · 7 BANG · 3 CHAN-BOI-OWNER (W3-mem/W4/W5) · 5 CAU-HOI-OWNER -->

View File

@ -0,0 +1,23 @@
# LĂNG KÍNH 4/4 — VERIFY ĐỘC LẬP CLAIM file:line (reviewer, S157)
Đích chấm: `spec-ke-hoach-ky-ket-hd-28-07-2026.md` §② — mọi claim `file:line` load-bearing được mở-file-thật đối chiếu.
## Verdict
(đang đo)
## Claim ĐÚNG
(đang đo)
## Claim SAI
(đang đo)
## Claim KHÔNG KIỂM ĐƯỢC
(đang đo)
## Điểm CỦNG CỐ
(đang đo)

View File

@ -0,0 +1,13 @@
# LĂNG KÍNH 2/4 — TRUNG THỰC VỚI NGUỒN GỐC
## Verdict
## Bịa
## Rơi
## Sai vai
## Nống biên
## Điểm CỦNG CỐ

View File

@ -0,0 +1,27 @@
# LĂNG KÍNH 3/4 — SCHEMA 7 BẢNG (reviewer, S157)
> Khung RỖNG ghi TRƯỚC khi đọc (chống #53 return-garble). Nội dung điền dần trong lúc soi.
## Verdict
_(chưa chấm)_
## Bảng THỪA
_(chưa soi)_
## Bảng THIẾU
_(chưa soi)_
## Cột sai
_(chưa soi)_
## Ràng buộc sai
_(chưa soi)_
## Điểm CỦNG CỐ
_(chưa soi)_

View File

@ -0,0 +1,17 @@
# LĂNG KÍNH 1/4 — WAVE-PLAN + ACCEPTANCE (reviewer đối kháng)
> Đích chấm: `spec-ke-hoach-ky-ket-hd-28-07-2026.md` §③ CHIA WAVE.
> Đối chứng: `runs/2026-07-27-S155-pe-delete-approver/run.md` (tiền lệ chia 2 đợt đã chạy THẬT).
> 4 trục tấn công: (a) acceptance đo được thật? (b) wave deploy độc lập thật? (c) thứ tự wave? (d) cờ CHẶN sai?
## Verdict
_(đang soi — điền cuối)_
## Điểm HIGH
## Điểm MED
## Điểm LOW
## Điểm CỦNG CỐ