wal: flush 20260728T1058

This commit is contained in:
pqhuy1987
2026-07-28 10:58:47 +07:00
parent 5507262753
commit f086698e8e
2 changed files with 181 additions and 2 deletions

View File

@ -8,6 +8,7 @@ Read-only INTERNAL audit SOLUTION_ERP. Tools: Read/Grep/Glob/Bash + 5 RAG MCP. O
- NOT: external docs/CVE/lib investigator-api · write implementer · test test-specialist · architecture decision em main
## 📌 Recent invest (1-line digest; full detail → archive/runs/git)
- **S157 (07-28) KH-ký-kết-HĐ b.712 AB đĩa, §CD PENDING lượt sau, `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/sub-invest-kehoach-1.md`]:** Owner chốt O-1 *"cấu trúc trình ký GIỐNG, khác nội dung"* tái dùng khung V2, dựng 7 bảng NỘI-DUNG `ContractSigningPlan*` (Mig 69 thuần CreateTable; mig cuối=68 verify ls) + `ApplicableType=10` (slot trống `ApprovalWorkflow.cs:53-67`). 🔴 Bẫy twin MỚI: **Proposal-lite ApproveV2 PHÁ OR-of-N** `ProposalFeatures.cs:427-439` flatten global-level + `ElementAtOrDefault` match 1-user, entity không StepIndex (`Proposal.cs:27`) 2-row-cùng-Order thành AND tuần tự; KH service PHẢI copy `ContractWorkflowService.ApproveV2Async:217-394`, Proposal chỉ làm khuôn CRUD/menu/FE. Twin đo thật: Proposal BE 849 · FE 1.956 LOC · 4 bảng (Mig 38 grep CreateTable) · PE 6.5K/14.9K cận trên. `ConflictException :115-116` tự xác minh đĩa (skill contract-workflow stale fallback V1 chỉ sống đường display). ISO doc 9 bước `ContractPhase` 1-9 khớp 1:1 (7 LEGACY = ISO mã-hoá cứng); khúc b.7-12 thiếu CẢ ISO-doc lẫn code; FO-002.01 luật auto-pass-quá-01-ngày (mâu thuẫn V2 no-auto-approve; O-2 SLA=tham-khảo). #53: coordinator chặn skeleton-rỗng @202K-token cắt còn §AB, Edit-liền-tay từng mục. Tag `[s157, ke-hoach-ky-ket, proposal-lite-breaks-or-of-n, iso-9-steps-eq-contractphase]`
- **S156 (07-27) BCH post-CEO flow bước 721 0-§36 đĩa, §4-§5 PENDING lượt sau, `runs/2026-07-27-S156-bch-post-ceo-flow/sub-invest-bch-1.md`]:** Verdict **LAI** 1321 TÁI DÙNG Contract (7 dòng `[LEGACY]` `ContractPhase.cs:11-13` chính trạm 13-21 đã gỡ CỐ Ý Mig21/S17; V2 `ContractWorkflowService.cs:217-394` cookie-cutter sẵn, terminal gen-mã+DaPhatHanh) · 7-12 DỰNG MỚI (0-hit shopdrawing/TVGS, khuôn 12-bis + `ApplicableType=10` append-only). 🔴 Chuỗi "cơ-chế đúng, thứ đi qua KHÔNG " phía HĐ: cầu PE pin V1 (`CreateContractFromEvaluationFeatures.cs:68-71,108`) không nhận V2 + UpdateDraft không re-pin · `GetEligiblePhases` (`ContractFeatures.cs:334-351`) + inbox `PhaseActorRoles` (`:363-372`) 100% LEGACY-phase approver V2/BCH KHÔNG xem/inbox được ChoDuyet · `CeoApprovalThreshold` 0-consumer (bước-18 ">5 tỷ" chưa có máy) · SLA V2 hardcode `AddDays(7)` 3 site, Step không có SlaDays · notify chỉ Drafter (`:407`) · `AttachmentPurpose` thiếu ký-nháy (enum-extend 0-mig tiền lệ PE=4) · HĐ TraLai reset-toàn-phần không 4-mode. Mig 68 `AddPeAllowApproverDelete` ĐÃ land 07-27 ⇒ mig kế = 69. Bắt lead sai grounding: 12 enum/7 LEGACY (lead ghi 9/6) — lead nhận. #53 strike-1 (return bốc hơi, đĩa nguyên → lead đọc từ đĩa, không redo). Tag `[s156, bch-post-ceo, verdict-lai, eligible-phases-legacy-only, bridge-pins-v1]`
- **S155 (07-27) PE xóa-phiếu màn DUYỆT [24 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-be-1.md`]:** DELETE `/api/purchase-evaluations/{id}` ĐÃ CÓ (`PurchaseEvaluationsController.cs:146`, authz = class `[Authorize]` TRẦN) nhưng allow-list `{DangSoanThao,TuChoi}` (`PurchaseEvaluationFeatures.cs:1404`) chặn ChoDuyet. `Remove()`=**SOFT** gián tiếp qua `AuditingInterceptor.cs:56-62` — lớp #81-EXT indirect. Lũy kế: `PeBudgetAccumulator.cs` 2 hàm/4 call-site DUY NHẤT; ChoDuyet ăn số **2 chỗ** (`ComputeAsync:54` PrevSubmitted + `ComputePendingAsync:114` PendingSelected); `TraLai` KHÔNG triệt tiêu; `TuChoi` rơi khỏi 100% phép cộng. Query 0 dòng `IsDeleted` → dựa global `HasQueryFilter` (`PurchaseEvaluationConfiguration.cs:84`) ⇒ soft-del TỰ rớt, không sửa accumulator. Snapshot Mig67 KHÔNG re-compute. 🔴 Cascade 6 FK (`:75-80`) **không bao giờ chạy** vì soft ⇒ con `BaseEntity` ở lại = rác ẩn. **Git khảo cổ TRÚNG:** "chỗ cho hủy" = nút Từ chối, gỡ `6db195d` 12-06 S60; guard `PurchaseEvaluationWorkflowService.cs:101-106` còn sống tự trỏ "Xóa phiếu khi còn Bản nháp" = chính cái lỗ. 0 tiền lệ hủy chứng-từ in-flight; khuôn gần nhất `CancelMeetingBookingHandler` (`MeetingFeatures.cs:457`, Status=Cancelled NOT IsDeleted). Tag `[s155, pe-delete-approver, s60-tuchoi-removal-hole, soft-delete-no-cascade]`
- **S155 (07-27) PE xóa-phiếu FE/authz-slice [18 finding, `runs/2026-07-27-S155-pe-delete-approver/sub-invest-fe-2.md`]:** Khối HÀNH ĐỘNG = `components/pe/PeWorkflowPanel.tsx:458-506` **byte-identical 2 app** (md5 `02671be6…`, cùng PeDetailTabs/PeListPanel/ListPage) ⇒ sửa-1-bên-là-sót. Gate 3 tầng: `?pendingMe=1``readOnly={!pendingMe}` (`ListPage:590`/`:676`) · `next=workflow.nextPhases` BE-source (`:257`) · `blockedByV2Level` (`:99-103` isAdmin ∥ actor∈currentApproval.approvers). 🔴 Plumbing xóa ĐÃ NỐI SẴN vào màn duyệt: `onDelete={del.mutate}` truyền ở `ListPage:573`+`:668` (`api.delete('/purchase-evaluations/:id')` `:90`) nhưng nút chết vì `PeDetailTabs:445` đòi `mode==='workspace' && canEditPhase && !readOnly`; nút hiện chỉ ở `:460` `phase===DangSoanThao`. **Authz 2 tầng:** PE vùng FE = **0-hit** `usePermission|PermissionGuard` (guard chỉ sống ở Master+Users, khuôn `DepartmentsPage:101 action="Delete"`); `menuKeys.ts` KHÔNG có const `Pe_*` (chỉ root `:23`) — `Pe_*` sinh BE `MenuKeys.cs:134-141`, seed `DbInitializer:1870-1877`, FE khớp bằng regex `Layout.tsx:120` (fe-admin `:107` **LỆCH**, thiếu WfView). 🔴 `Pe_*` **KHÔNG ∈ `MenuKeys.All`**`Program.cs:82-89` không đăng-ký policy `Pe_*.{action}` (0 dynamic provider) ⇒ dùng `[Authorize(Policy="Pe_…")]` = policy-chưa-tồn-tại; **dùng được ngay = `PurchaseEvaluations.Delete`** (root ∈ All, + `GetMyMenuTreeQuery:66-72` cho `Pe_*` KẾ THỪA cờ CRUD từ root ⇒ 0 key mới, 0 migration). Ma trận enumerate `db.MenuItems` (`PermissionFeatures.cs:20`) nên ô Delete đã hiện sẵn. Q6: thêm-nút KHÔNG kích 4-place (0 route/page/staticMap `Layout.tsx:56-106`); `Pe_*` resolve nhánh regex `:119-134` KHÔNG qua staticMap. Tag `[s155, pe-delete-fe-authz, two-app-byte-identical, pe-keys-not-in-All]`

View File

@ -12,13 +12,191 @@
> KHÔNG auto-approve-on-timeout, KHÔNG wave SLA; giữ hiển thị deadline; `AddDays(7)` hardcode = 1 dòng ghi nhận.
## §A — QUY TRÌNH CHI TIẾT khúc còn thiếu (hoà giải 3 nguồn)
[PENDING]
### A.0 Khung — khoảng trống nằm giữa ISO-1 và ISO-2 (xác nhận Phát hiện B của lead từ nguồn gốc)
Em tự đối chiếu: ISO doc bảng 2 (`src-QT-TRINH-KY-HD.txt:60-61`) đi THẲNG "Lựa chọn NTP/NCC" → "Soạn thảo
hợp đồng", **0 bước trung gian**. PDF sơ đồ (bản 14/2026, `runs/...S156/run.md` bảng 21 bước) chèn b.7→12
đúng chỗ đó. FO-002.01 là tờ cover của KHÚC HĐ (ISO bước 5-7), không nói gì khúc 7→12.
**PDF là nguồn DUY NHẤT có khúc này** — spec ra từ đây vừa là spec phần mềm vừa là đề xuất bổ sung ISO.
Vật mang trong hệ thống = **1 PHIẾU "Kế hoạch ký kết Hợp đồng"** (owner đặt tên, đầu ra = CON SỐ có phê
duyệt), vòng đời 5-trạng-thái mirror PE, duyệt bằng **khung V2 có sẵn** (O-1).
### A.1 Bảng bước b.7→12 → ngôn ngữ hệ thống
Role hệ thống có thật (`AppRoles.cs:5-16`): Procurement (PMH/PRO) · CostControl (CCM) · Director+AuthorizedSigner
(BOD/NĐUQ) · ProjectManager/Drafter (phía BCH) · HrAdmin · Admin. "BCH dự án X" **không có model** (Q1 S156 —
`Department.ManagerUserId` 0-row, PositionLevel NULL) → phiếu KH 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 (role thật) | Đầu vào | Đầu ra | Điều kiện chuyển | Nhánh lỗi/trả lại |
|---|---|---|---|---|---|---|
| 7 | PMH mail chuyển thông tin BCH/NTP/NCC | Procurement | PE chạm `DaDuyet` (winners `IsWinner`, giá `SUM Quote.IsSelected`, HoSoLink NAS) | BCH biết việc → vào lập KH | Hệ thống = **pull-model**: màn "PE đã duyệt — chờ lập KH" lọc `Phase==DaDuyet && chưa có plan` (khuôn `CreateContractFromEvaluationFeatures.cs:173` filter `DaDuyet && ContractId==null`) + notification in-app. Email ra NGOÀI (NTP/NCC) = ngoài scope (SMTP TODO toàn hệ thống) | PE thiếu winner → không hiện trong màn (guard sẵn ở PE) |
| 8 | Duyệt mẫu + shopdrawing với TVGS | BCH nhập; TVGS = người NGOÀI hệ thống, **BCH nhập hộ kết quả** (tên TVGS + ngày + scan) — hướng (a) Q7 S156, sơ đồ không đòi account TVGS | Mẫu vật liệu + shopdrawing từ TP/NCC (offline) | Danh mục CĂN CỨ trong phiếu KH: per-item {Kind: MauVatLieu\|Shopdrawing, NCC nào, Status, TvgsName, ngày, file 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**, hệ thống phải có vì thực tế TVGS bác | Item bị bác = đổi status + note, KHÔNG đụng state machine phiếu |
| 9 | Tổng hợp hồ sơ + so sánh giá → ĐỀ XUẤT giá trị ký HĐ | BCH (= Drafter phiếu KH; role thường ProjectManager) | Căn cứ b.8 + báo giá PE (snapshot `PeReferenceAmount` per-winner lúc tạo phiếu — nguyên tắc snapshot Mig 67) | Phiếu KH điền đủ: dòng giá trị ĐỀ XUẤT per-winner + file so sánh giá → **TRÌNH** (`DangSoanThao→ChoDuyet`) | Guard trình: dòng giá đề xuất đủ per-winner (mirror completeness-guard S60 PE `PurchaseEvaluationWorkflowService.cs:180-212`); căn cứ b.8 chưa đủ TvgsDuyet → **soft-warn** (tiền lệ S62 vượt-NS-cảnh-báo-mềm) — cứng/mềm = owner quyết (O-Q1 dưới) | Ghi chú sơ đồ: "hồ sơ sai sót thiếu khối lượng, sai spec, thiếu phạm vi → gửi mail làm rõ" = comment + item TvgsBac |
| 10 | PMH kiểm tra hồ sơ so sánh giá, xác nhận giá trị | Procurement (đích danh user trong Level) | Phiếu ChoDuyet Bước 1 | Duyệt → Bước 2; ý kiến vào `LevelOpinions` (UPSERT khi duyệt — khuôn `ContractWorkflowService.cs:292-316`) | OR-of-N trong Cấp (`ApprovalWorkflow.cs:81-94`) | Trả lại → `TraLai` (reset) hoặc 4 return-mode per-level (cờ CÓ SẴN `ApprovalWorkflowLevel.cs:116-125`, service mirror PE `:421-424`) |
| 11 | CCM kiểm tra | CostControl (đích danh trong Level) | Bước 2 | Duyệt → Bước 3 | như trên | như trên |
| 12 | **CEO ký HOẶC CCM đóng dấu approval** | Director; nhánh "CCM đóng dấu" = 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 "mọi nhánh set DaDuyet phải gọi helper" `:1008` | Terminal `DaDuyet` | Từ chối → `TuChoi` (terminal); trả lại → `TraLai` |
**Cơ chế rẽ b.12 "hoặc" — 4 đường ĐỀU CÓ SẴN trong khung V2 (O-1), 0 migration:**
(i) `CeoApprovalThreshold` + CCM tích "duyệt done miễn CEO" — PE service `:892-929` (fail-closed: đòi threshold
+ role CostControl + giá < ngưỡng); (ii) `AllowApproverFinalize` per-slot opt-out 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 (ngưỡng? loại gói?) = **cả 3 nguồn im lặng** Q3 owner còn treo,
nhưng chọn đường nào cũng không đổi schema.
**Tiếp giáp ra (b.12→13):** phiếu KH `DaDuyet` cầu `POST /purchase-evaluations/{id}/create-contract` hiện
(`CreateContractFromEvaluationFeatures.cs`) tạo draft `DangSoanThao` sửa cầu đọc `ApprovedAmount` từ KH
thay `SUM Quote.IsSelected` (`:88-90`) + audit lệch PE-vs-KH + gate "chưa KH duyệt thì cảnh báo/chặn"
(Q11 mềm/cứng). Khúc b.1321 = verdict LAI S156, KHÔNG bàn lại.
### A.2 b.8-9 là GÌ: **bảng con trong phiếu KH** — không phải phiếu con, không phải module riêng
Repo 0-hit `shopdrawing|TVGS|duyệt mẫu` (verify S156 §0, em không đo lại không để đo). Chọn **satellite
table `DossierItems` + attachments trong phiếu KH** vì: (1) owner phát biểu b.8-9 = **CĂN CỨ** của con số, 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 riêng = đẻ chuỗi duyệt
thứ 2 không trên đồ; (3) vòng đời mẫu/shopdrawing per-item status (nộp/duyệt/bác/nộp lại), không phải
state machine phiếu; (4) TVGS ngoài hệ thống chỉ cần ghi vết, không cần workflow. 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).
### A.3 Mâu thuẫn / im lặng giữa 3 nguồn (bảng tra)
| # | Điểm | ISO doc | PDF đồ | FO-002.01 | Xử |
|---|---|---|---|---|---|
| 1 | Khúc b.712 | **KHÔNG CÓ** (12 thẳng) | (duy nhất) | không nói | Spec = đề xuất bổ sung ISO |
| 2 | SLA khúc 712 | im lặng | im lặng (10-14d b.1-4; 3d + 7-10d b.13-18) | im lặng | **O-2: tham khảo** không hoà giải, không engine |
| 3 | SLA khúc (1319) | 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"** (auto-pass!) | 3 nguồn 3 kiểu; auto-pass mâu thuẫn hình duyệt chặn V2 (V2 không auto-approve; V1 từng System AutoApprove) O-2 chỉ ghi nhận |
| 4 | Điều kiện rẽ b.12 CEO vs CCM | không bước | "hoặc" KHÔNG điều kiện | không nói | Q3 owner; 4 chế sẵn (A.1) |
| 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 per-level |
| 6 | Bypass Chủ đầu trực tiếp | không nói | không nói | ("chuyển trực tiếp Giám đốc") | = `Contract.BypassProcurementAndCCM` (đã trong code) KHÔNG áp phiếu KH (KH chỉ sinh từ PE = TP/NCC) |
| 7 | Nội dung kiểm tra cấu trúc per-phòng (PRO: điều khoản/thanh toán/rủi ro pháp · CCM: giá vs NS/rủi ro) | tả lời | không | (checkbox từng mục) | Hệ thống chỉ `LevelOpinions.Comment` tự do checklist cấu trúc = GAP ghi nhận, không blocking (owner quyết số hoá checkbox không O-Q2) |
| 8 | Tên phòng/vai | PRO/CCM/BOD/NĐUQ/HRA | Procurement/CCM/CEO | PB-DA/PRO/CCM/ | Khớp `AppRoles`: Procurement/CostControl/Director+AuthorizedSigner/HrAdmin nhất quán, 0 mâu thuẫn |
| 9 | Chuỗi FO-002.01 (ĐỀ XUẤTPROCCM) | bước 5-7 | b.9101112 CÙNG HÌNH | | Đối chứng giấy cho O-1: khung Step(Phòng)×Level+Opinions gánh được cả 2 khúc |
**Câu hỏi owner còn treo sau run này (rút từ 14 câu S156, trừ đã chốt O-1/O-2/Q7a/Q8-ngoài-scope):**
O-Q1 gate căn cứ b.8 khi trình: mềm (khuyến nghị, tiền lệ S62) hay cứng? · O-Q2 checkbox FO-002.01 số hoá
hay comment tự do? · Q3 điều kiện rẽ b.12 (chọn chế i-iv) · Q6 xác nhận giá KH thay SUM-PE khi tạo
(mặc định thiết kế: KH thắng + audit lệch) · Q11 gate tạo-HĐ khi chưa KH duyệt: mềm/cứng.
## §B — CÁCH WIRE vào codebase (twin THẬT, file:line)
[PENDING]
> O-1 chốt: **tái dùng khung duyệt V2** (`ApprovalWorkflows`/Steps/Levels + cột dùng chung CeoApprovalThreshold
> `ApprovalWorkflow.cs:43` + 9 cờ per-slot `ApprovalWorkflowLevel.cs:116-169`) — **0 bảng approval mới**.
> Dựng mới = NỘI DUNG phiếu. Twin đo thật: **Proposal** (BE 849 LOC · FE 1.956 LOC · 4 bảng Mig 38 — em wc/grep
> trực tiếp) = khuôn CRUD/menu; **Contract ApproveV2Async** (`ContractWorkflowService.cs:217-394`) = khuôn service;
> **PE** = khuôn finalize/giá-chốt (BE ~6.5K · FE ~14.9K = cận trên, không cần tới).
### B.1 ⚠️ Bẫy twin đo được lượt này — CẤM copy Proposal-lite cho phần DUYỆT
`ProposalFeatures.cs` ApproveProposalHandler đi **global-level-order**: flatten mọi Level xuyên Steps
(`:427-429` `SelectMany`), match `ElementAtOrDefault(order-1)` **1 row** (`:433`) + so đúng **1 user**
(`:439` `currentSlot.Level.ApproverUserId != currentUser.UserId`). 2 row cùng Order (OR-of-N hợp lệ theo
`ApprovalWorkflow.cs:81-94`) bị biến thành 2 nấc TUẦN TỰ (AND) **phá ngữ nghĩa OR-of-N**, không con trỏ
Step (entity Proposal `:27` chỉ `CurrentApprovalLevelOrder`, KHÔNG `CurrentWorkflowStepIndex`). Phiếu KH cần
3 Bước (PMHCCMCEO) **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.
### B.2 Schema — Mig **69** `AddContractSigningPlans` (mig cuối hiện tại = 68 `20260727033522_AddPeAllowApproverDelete.cs`, em ls trực tiếp)
**7 CreateTable, 0 ALTER bảng cũ ⇒ Down = DropTable ×7, reversible sạch.** Convention: PascalCase EN, loose-Guid
không FK vật sang PE/Supplier (convention PE `PurchaseEvaluation` ProjectId/SelectedSupplierId đều loose,
ghi Mig 49 note), FK vật chỉ trong 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 cây), Phase int, ApprovalWorkflowId Guid?, CurrentWorkflowStepIndex int?, CurrentApprovalLevelOrder int?, DrafterUserId, SlaDeadline?, SlaWarningSent, HoSoLink nvarchar(1000)? (mirror PE Mig 52), 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** (nguyên tắc freeze Mig 67 không để PE sửa sau làm trôi baseline), ProposedAmount decimal(18,2), ApprovedAmount decimal(18,2)? (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? loose, Name, Status int (ChuaNop=0/DaNop=1/TvgsDuyet=2/TvgsBac=3), TvgsName nvarchar(200)?, 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 / `ProposalLevelOpinionConfiguration.cs`) |
| 5 | `ContractSigningPlanAttachments` | PlanId FK Cascade, FileName/Path/Size/ContentType, Purpose int (MauVatLieu=1/Shopdrawing=2/SoSanhGia=3/Khac=99), DossierItemId? | khuôn `ProposalAttachment.cs` (20 dòng) |
| 6 | `ContractSigningPlanChangelogs` | PlanId, Action, PhaseAtChange, UserId, Summary, ContextNote | tiền lệ PE: Changelogs = nguồn transition-marker/backfill (Mig 61) từ ngày 1 |
| 7 | `ContractSigningPlanCodeSequences` | Prefix PK, LastSeq | khuôn `ProposalCodeSequence.cs` (13 dòng); format đề xuất `KHKK/{YYYY}/{Seq:D3}` **owner chốt format** |
Biến thể tối thiểu 5 bảng (bỏ #6 Changelogs + #7 CodeSequences) KHÔNG khuyến nghị: mất audit-trail transition
(tiền lệ Mig 60/61 cho thấy cần) mất phiếu atomic.
**Enum ngoài bảng:** `ApprovalWorkflowApplicableType` **+`ContractSigningPlan = 10`** append-only vào
`ApprovalWorkflow.cs:53-67` (slot 10 trống, em đọc trực tiếp; tiền lệ Mig 37 extend enum). `ContractSigningPlanPhase`
enum 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 ; 98/99 giữ cho quen mắt FE badge).
**KHÔNG thêm cột `SigningPlanId` lên PE** tra ngược bằng query `Plans.Where(PurchaseEvaluationId==x)`; thêm cột
= đẻ "cột 0 ai đọc" (lớp lỗi 6-lần của repo).
DbSets: `ApplicationDbContext` + `IApplicationDbContext` (khuôn mọi module). 3-file rule mig (Up/Down + Designer
+ Snapshot).
### B.3 CQRS + service + controller
- **`Application/ContractSigningPlans/ContractSigningPlanFeatures.cs`** (mega-file khuôn `ProposalFeatures.cs` 556 dòng):
- `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` match (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 `PurchaseEvaluationFeatures.cs` re-pin chỉ Nháp/TraLai; tránh vết Contract UpdateDraft KHÔNG đường
re-pin S156 §2-bis).
- `Get`/`List`/`pendingMe` detail Include Lines+DossierItems+Attachments+LevelOpinions + workflow-tree JOIN
(khuôn Proposal `:161-228`); **inbox V2 precompute từ ngày 1** (mirror PE `ResolveV2InboxIdsAsync` bẫy S156:
Contract THIẾU cái này nên approver không inbox `ContractFeatures.cs:363-372` legacy-only).
- `UpsertDossierItemCommand` / `DeleteDossierItemCommand` BCH nhập hộ TVGS; mở cả khi ChoDuyet? **KHÔNG**
chỉ 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).
- Attachment upload/download/delete khuôn `ProposalAttachment` + PE attachment features.
- **`Infrastructure/Services/ContractSigningPlanWorkflowService.cs`** copy `ContractWorkflowService.ApproveV2Async:217-394`
(B.1) + adapt 3 chỗ: (1) terminal `Phase=DaDuyet` + **`ApplyApprovedValuesOnFinalize`**: mọi Line
`ApprovedAmount = ApprovedAmount ?? ProposedAmount` (người duyệt cấp cuối được sửa trước khi duyệt nếu bật
`AllowApproverEditBudget`-tương-tự; khuôn choke-point + RULE grep-4-site `PurchaseEvaluationWorkflowService.cs:1008`);
(2) port 2 nhánh kết thúc sớm PE: `AllowApproverFinalize` `:870-890` + `finalizeByCcmDelegation`+`CeoApprovalThreshold`
`:892-929` (tổng so ngưỡng = `SUM ProposedAmount` các Line); (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-Drafter
(`PurchaseEvaluationWorkflowService.cs:172-178` + `ContractWorkflowService.cs:70-79`) để lớp 403-PMH
(S156 §G3: role không khớp = kẹt cứng); nếu owner muốn đồng nhất role-based thì BCH users phải mang role Drafter.
**Notify:** mirror `LogTransitionAsync` notify Drafter (`ContractWorkflowService.cs:407-427`) + THÊM notify
đích danh approvers 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 (lỗ A1 S156). 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 (body {targetPhase, decision, comment, returnMode?, skipToFinal?, applyLevelFinalize?,
finalizeByCcmDelegation?, approvedLines?}) · dossier-items CRUD · attachments ·
DELETE {id} (allow-list DangSoanThao|TuChoi mirror PE `PurchaseEvaluationFeatures.cs:1404`).
### B.4 Menu + permission + FE 2 app (Pattern 16-bis, twin Off_DeXuat đo thật)
- **BE:** `MenuKeys.cs` +4 const kiểu `Csp`/`Csp_List`/`Csp_Create`/`Csp_Inbox` (khuôn `OffDeXuat*` `:109-112`)
**+ đưa cả 4 vào `All` `:156-174`** (khuôn `:168` Off_DeXuat 4 key ĐỀU trong All policy sinh đủ; ĐỪNG theo
kiểu `Pe_*` factory-ngoài-All `:142-144` per-action policy sẽ chưa tồn tại bẫy S155 "policy-chưa-tồn-tại").
2 row canonical `docs/STATUS.md` (Menu keys · Policies DERIVED |All|×4) PHẢI cập nhật cùng lúc.
- **Seed:** `DbInitializer.cs` menu 4 dòng (khuôn Off_DeXuat `:1817-1820`: root parent + 3 leaf, label VN
"Kế hoạch kết "/"Danh sách"/"Tạo mới"/"Inbox duyệt") + admin-permission list (`:2383` grep đủ MỌI site
seed-permission khi làm, S155 ghi nhận 2 site cho vài nhóm); dark-launch được bằng `IsVisible=0` (cột Mig 27)
rồi bật sau.
- **FE ×2 app (duplicate CHỦ ĐÍCH):** mỗi app 4 chỗ: (1) routes `App.tsx` (khuôn fe-user `:77-79` /proposals ×3
route); (2) `components/Layout.tsx` staticMap (khuôn `:95-97` `Off_DeXuat_List: '/proposals'`…); (3) `lib/menuKeys.ts`
(khuôn `:54-57`); (4) 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 Steps/Levels ✓/●/○) + 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` đọc ApplicableType (grep "ApplicableType" toàn file khi làm S155 F6 liệt
~5 dây BE + fe-user matrix-view; **path file matrix-view fe-user em grep lượt này KHÔNG thấy
`fe-user/src/pages/WorkflowMatrixViewPage.tsx` vị trí thật cần grep lại khi implement, đừng tin line S155**).
- **Cầu tiếp giáp (sửa 1 file):** `CreateContractFromEvaluationFeatures.cs` (a) đọc plan DaDuyet của PE:
`giaTri = line.ApprovedAmount` khớp `w.SupplierId` thay SUM `:88-90`, ghi ContextNote lệch "giá KH x vs SUM-PE y"
vào 2 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 (hiện pin V1-only `:68-71,:108` V2-less
trình xong KẸT CỨNG `ConflictException` `ContractWorkflowService.cs:115-116`, em tự xác minh trên đĩa).
- **1 dòng bắt buộc nhắc (ngoài scope, treo theo ý anh):** lỗ Reject-trước-guard
`ContractWorkflowService.cs:48-66` + controller class-trần vẫn SỐNG trên prod anh chốt 07-28 "từ từ".
## §C — CHIA WAVE THI CÔNG + CHECKLIST ĐO ĐƯỢC
[PENDING]
## §D — HẤP THỤ 6 việc sửa review-synthesis §G
[PENDING]
<!-- TRẠNG THÁI GIỮA CHỪNG (chưa END): §A=XONG (6 bước + 9 điểm hoà giải + 5 Q treo) · §B=XONG (7 bảng Mig 69,
0 ALTER, bẫy Proposal-lite B.1) · §CD=[PENDING] lượt sau theo lệnh coordinator. TOTAL-line chỉ ghi khi đủ 4 mục. -->