Files
solution-erp/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/sub-invest-kehoach-1.md
2026-07-28 10:58:47 +07:00

203 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# sub-invest-kehoach-1 — "Kế hoạch ký kết Hợp đồng" (PDF b.7→12) — quy trình + wire + wave
> **Run:** `2026-07-28-S157-ke-hoach-ky-ket-hd` · agent: investigator-codebase (/fable-real, effort max)
> **Trạng thái file:** GHI TRONG LÚC LÀM — mục chưa điền ghi `[PENDING]`. Propose-only; lead ghi spec sau verify.
> **Nguồn:** run.md S157 (§1 grounding lead) · src-QT-TRINH-KY-HD.txt · src-FO-002.01 · run.md S156 · sub-invest-bch-1.md · review-synthesis.md §G.
> **Luật đo:** mọi claim load-bearing neo file:line TỰ ĐỌC. Doc/skill = giả thuyết. Đĩa thắng.
> **🔴 OWNER CHỐT GIỮA CHỪNG (run.md §1.7, nhận qua lead lúc đang điều tra):**
> **(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"* ⇒ phiếu KH TÁI DÙNG khung duyệt V2
> (`ApprovalWorkflow`>Step>Level OR-of-N + LevelOpinions UPSERT); DỰNG MỚI = NỘI DUNG phiếu. Trục cơ-chế Q2 ĐÓNG;
> option-space §D(6) chỉ còn trục NỘI DUNG (phiếu độc lập ⟂ mở rộng PE ⟂ entity con Contract).
> **(O-2)** *"Cái này để tham khảo thôi, ko vấn đề j, đa số là trễ"* ⇒ SLA = hiển thị tham khảo; KHÔNG SLA-engine,
> 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)
### 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)
> 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. -->