From eb7473f6c8fffce82c50fb672b8d750a08e1a2fb Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 29 Jul 2026 23:48:08 +0700 Subject: [PATCH] wal: flush 20260729T2348 --- .claude/WAL.md | 3 +- .../sub-implementer-backend-0.md | 158 ++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 .claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-implementer-backend-0.md diff --git a/.claude/WAL.md b/.claude/WAL.md index 538e1e5..82ff633 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -35,7 +35,8 @@ chain: - [x] commit W6 `761d818` (3 file +811/−6) PUSHED - [x] **W6 ĐÓNG — cicd VERDICT=PASS** (Stage 1-2 cicd [CI 587 delta+7 · restart chứng-đến-giây] + 3-6 lead [MIGTOP giữ · 96 · 142 · **inbox admin 200-`[]`-hợp-lệ không-500** · CỜ giữ]). Synthesis `runs/2026-07-29-S161-khkk-w6-duongong/w6-synthesis.md` - [x] gate 5-anchor W7 PASS 5/5 (A2 content-found drift+6; A5 xác nhận 0-type) -- [!] wf: W7 đợt-1 `wf_0ae8cc6d-6e2` ĐANG CHẠY — {BE nhỏ: +param type + DTO hasSealedCopy append-cuối · FE: HardCopiesPage ×2 + 4-place 7 leaf Hdc + upload 1-purpose SealedCopy qua endpoint Contract CÓ SẴN} theo spec RÚT-1-MỐC @S161 — run-folder `runs/2026-07-29-S161-khkk-w7-bancung/`. Sau: test 3-PIN → reviewer → ship +- [x] W7 đợt-1 `wf_0ae8cc6d-6e2` XONG: BE **CLEAN PASS 5/5 gate** (+type param + HasSealedCopy derive KHÔNG-IsDeleted — **lane bắt memory-pack slice của LEAD SAI**: ContractAttachment=BaseEntity hard-delete, viết theo slice là không compile; runtime-proof qua test W6 chạy chính handler) · FE #53 nhưng XONG HẾT (HardCopiesPage ×2 PAIR + 7 leaf re-point 0-coming-soon + marker bundle 1/1 — diagnostics toàn stale #68); suite 587/0 giữ +- [!] wf: W7 đợt-2 `wf_1893244d-f15` test 3-PIN ĐANG CHẠY (đích ≥590) → reviewer → ship → còn W5-chờ-anh-chốt-người-duyệt - [ ] 🔴 W5 CHỜ ANH: chốt NGƯỜI DUYỆT 3 trạm (PRO/CCM/CEO = ai trong 18 user prod) để dựng 2 workflow type-3/type-10 qua Designer — em không tự chọn người thật; xong mới chạy W5 (cầu KHKK→HĐ) + E2E - [ ] W5 (cầu KHKK→HĐ — cần setup WF tay TRƯỚC: type-3 + type-10 3-trạm Designer) → W7 (bản cứng 1-mốc @S161) - [ ] W6 → W5 → W7 theo đồ thị plan-cha (sau W3) diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-implementer-backend-0.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-implementer-backend-0.md new file mode 100644 index 0000000..101be6b --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-implementer-backend-0.md @@ -0,0 +1,158 @@ +# sub-implementer-backend-0 — W7 BE (bản cứng `Hdc_*`, RÚT-1-MỐC) + +> Vai: implementer-backend · Run: `2026-07-29-S161-khkk-w7-bancung` · Ngày 2026-07-29 +> Task: (1) +param `type` cho `ListContractsQuery` + controller · (2) `ContractListItemDto` append-CUỐI `HasSealedCopy` derive từ attachments · (3) build 0 err + suite 587/0 GIỮ. +> LUẬT: KHÔNG commit/push/slnx · KHÔNG enum-extend · KHÔNG policy mới · KHÔNG đụng vùng O-A. + +--- + +## §0 — Ghi-đĩa-từng-mục (trạng thái sống) + +- [x] §1 Đo tiền-đề từ ĐĨA (trước khi sửa 1 dòng nào) +- [x] §2 Sửa DTO (`ContractDtos.cs`) +- [x] §3 Sửa query + filter + projection (`ContractFeatures.cs`) +- [x] §4 Sửa controller (`ContractsController.cs`) +- [x] §5 Build +- [x] §6 Test suite +- [x] §7 Tự đối-chứng chéo hợp-đồng FE (nghi thức W3đ1 §6) +- [x] §8 Bàn giao / caveat + +--- + +## §1 — Đo tiền-đề từ ĐĨA (KHÔNG tin spec mù) + +### 1.1 `AttachmentPurpose` — enum SẴN, 0 extend +`src/Backend/SolutionErp.Domain/Contracts/ContractAttachment.cs:5-11` +``` +DraftExport = 1 · ScannedSigned = 2 · SealedCopy = 3 · Other = 99 +``` +⇒ ĐÚNG gate 5-anchor: `SealedCopy=3` có sẵn ⇒ **0 enum-extend** (khớp spec §②-2 RÚT-1-MỐC). + +### 1.2 🔴 ContractAttachment KHÔNG CÓ soft-delete — MEMORY-PACK slice SAI ở điểm này +- `ContractAttachment : BaseEntity` (`ContractAttachment.cs:13`), KHÔNG phải `AuditableEntity`. +- `BaseEntity` (`src/Backend/SolutionErp.Domain/Common/BaseEntity.cs:3-10`) = `Id · CreatedAt · UpdatedAt · CreatedBy · UpdatedBy` — **KHÔNG có `IsDeleted`**. +- `ContractAttachmentConfiguration` (`.../Configurations/ContractConfiguration.cs:78-93`) — **KHÔNG có `HasQueryFilter`** (đối lập `ContractConfiguration.cs:44` `HasQueryFilter(x => !x.IsDeleted)` của `Contract`). +- Delete attachment = **HARD delete**: `db.ContractAttachments.Remove(att)` — `ContractAttachmentFeatures.cs:143`. + +⇒ Slice gợi ý `a.Purpose == SealedCopy && !a.IsDeleted` sẽ **KHÔNG COMPILE** (`bool ContractAttachment.IsDeleted` không tồn tại). +⇒ Projection ĐÚNG = `x.c.Attachments.Any(a => a.Purpose == AttachmentPurpose.SealedCopy)` — hard-delete ⇒ row còn = file còn, không cần lọc. +(Đây đúng là ca "verify EF/entity là source-of-truth, KHÔNG trust spec blindly" — bài S35.) + +### 1.3 Nav `Contract.Attachments` có thật +`ContractConfiguration.cs:42`: `b.HasMany(x => x.Attachments).WithOne(a => a.Contract).HasForeignKey(a => a.ContractId).OnDelete(Cascade)` ⇒ `x.c.Attachments.Any(...)` dịch được sang `EXISTS(...)`. + +### 1.4 Tiền-lệ param `type` — đã có ở query anh em +`ContractFeatures.cs:408-409` `ListDeletedContractsQuery(ContractType? Type = null)` + filter `:434` +`ContractsController.cs:31` `[FromQuery] ContractType? type = null` +⇒ W7 chỉ cookie-cutter đúng khuôn đó sang `ListContractsQuery`, 0 sáng-tạo. + +### 1.5 3 call-site dựng `ContractListItemDto` (đếm bằng grep, KHÔNG bằng trí nhớ) +- `ContractFeatures.cs:335` List (**MỚI derive ở đây**) +- `ContractFeatures.cs:450` ListDeleted (giữ nguyên 12 arg ⇒ nhận default) +- `ContractFeatures.cs:529` Inbox (giữ nguyên 12 arg ⇒ nhận default) +⇒ Vì thế param mới **BẮT BUỘC có default** (`= false`), nếu không 2 site kia vỡ compile. + +### 1.6 Endpoint upload đã có sẵn (FE lane cần — đo hộ, KHÔNG sửa) +`ContractsController.cs:85-101` `POST /api/contracts/{id}/attachments` +- multipart field: `file` (IFormFile, **tên field = `file`**) · `purpose` (`[FromForm]`, int enum, mặc định `Other=99`) · `note` (`[FromForm]`, optional) +- guard: `[RequestSizeLimit(25_000_000)]` + validator 20 MB + MIME whitelist (`ContractAttachmentFeatures.cs:31-41`: pdf/doc/docx/xls/xlsx/png/jpeg/webp) +- authz: **KHÔNG có `[Authorize(Policy=...)]` riêng** → chỉ `[Authorize]` trần cấp class (`:13`). Đây là vùng **O-A để mở nguyên trạng** theo lệnh lead ⇒ KHÔNG đụng. +⇒ FE gửi `purpose=3` để đánh dấu bản cứng. + +--- + +## §2 — DTO: append-CUỐI `HasSealedCopy` + +File `src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs` +Append sau `DateTime CreatedAt` (vị trí CUỐI, có default `= false`). + +Lý do default: xem §1.5 (2 call-site kia giữ 12 arg). Hệ quả **KHAI THẲNG**: ở màn "Đã xóa" + "Hộp thư" cờ luôn `false` (không derive) — đúng chỉ-thị "KHÔNG đổi gì khác"; badge W7 chỉ sống trên `GET /api/contracts`. + +## §3 — Query + filter + projection + +File `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` +1. `ListContractsQuery` — positional-append `ContractType? Type = null` (KHÔNG chèn giữa: `new ListContractsQuery(phase, supplierId, projectId)` ở controller là positional). +2. Filter đặt **cùng khối** 3 filter cũ (`Phase/SupplierId/ProjectId`), sau khối IDOR ⇒ không đụng scope W6. +3. Projection List: `x.c.Attachments.Any(a => a.Purpose == AttachmentPurpose.SealedCopy)`. + +## §4 — Controller + +File `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` — `List` (`:16-24`): `[FromQuery] ContractType? type = null` (đặt cạnh `projectId`, TRƯỚC `CancellationToken`) + truyền vào ctor thứ 4. + +--- + +## §5 — BUILD (SỐ ĐO THẬT, chạy SAU khi 4 edit đã land) + +``` +$ dotnet build SolutionErp.slnx -v minimal +Build succeeded. + 0 Warning(s) + 0 Error(s) +Time Elapsed 00:00:44.12 +``` +6 assembly link OK (Domain · Domain.Tests · Application · Infrastructure · Api · Infrastructure.Tests). **0 Error / 0 Warning.** + +## §6 — TEST (SỐ ĐO THẬT) + +``` +$ dotnet test SolutionErp.slnx --no-build +Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 - SolutionErp.Domain.Tests.dll (net10.0) +Passed! - Failed: 0, Passed: 542, Skipped: 0, Total: 542 - SolutionErp.Infrastructure.Tests.dll (net10.0) +``` +**TOTAL 587 passed / 0 failed** ⇒ baseline 587 (45D + 542I) **GIỮ NGUYÊN**, 0 regression, 0 skip. + +### 6.1 🟢 Bằng chứng RUNTIME (không chỉ compile) — may mà có sẵn +`tests/SolutionErp.Infrastructure.Tests/Application/ContractV2PipelineTests.cs:165-166` +```csharp +=> new ListContractsQueryHandler(db, actor) + .Handle(new ListContractsQuery(Phase: phase) { PageSize = 200 }, CancellationToken.None); +``` +2 điều rút ra: +1. Test dựng query bằng **named-arg `Phase:`** ⇒ positional-append `Type` KHÔNG phá call-site test (đó là lý do append CUỐI, không chèn giữa). +2. Handler này CHẠY THẬT trên SQLite fixture trong suite ⇒ projection mới `Attachments.Any(...)` đã được **provider dịch + thực thi thành công**, không phải chỉ "qua compiler". Đây là mức chứng mạnh hơn build-only. + +## §7 — Tự đối-chứng chéo hợp-đồng FE (5/5, TRƯỚC bàn giao — nghi thức W3đ1 §6) + +| # | Lead định-nghĩa (LITERAL) | BE trên đĩa (dòng SAU khi sửa) | Khớp | +|---|---|---|---| +| 1 | `GET /api/contracts?type={int}` param MỚI `type` (ContractType? 1..7) | `ContractsController.cs:25` `[FromQuery] ContractType? type = null` → `:27` truyền vị-trí thứ 4 | ✅ | +| 2 | `&phase=9` vẫn chạy (DaPhatHanh) | `ContractsController.cs:20` `[FromQuery] ContractPhase? phase` GIỮ NGUYÊN, 0 dòng đụng | ✅ | +| 3 | DTO append-CUỐI `bool HasSealedCopy`, FE đọc `hasSealedCopy` | `ContractDtos.cs:25` field CUỐI. Wire-name: `Program.cs:33-34` `AddJsonOptions` CHỈ add `UtcDateTimeJsonConverter`, **KHÔNG override `PropertyNamingPolicy`** ⇒ giữ default ASP.NET Core = camelCase ⇒ JSON = `hasSealedCopy` | ✅ | +| 4 | Cờ = có ≥1 attachment `purpose=3` | `ContractFeatures.cs:355` `x.c.Attachments.Any(a => a.Purpose == AttachmentPurpose.SealedCopy)` | ✅ | +| 5 | Upload dùng endpoint CÓ SẴN, FE không cần endpoint mới | `ContractsController.cs:85-101` (`POST {id}/attachments`, field `file` + `purpose` + `note`) — **0 dòng thay đổi**, xem §1.6 | ✅ | + +Điểm 3 là chỗ dễ "giả-định thay phép-đo" nhất (mặc định camelCase ai cũng *tưởng* đúng) ⇒ đã đi đọc `Program.cs` xác nhận không có override, thay vì tin quy ước. + +### 7.1 Vị trí 4 thay đổi (grep xác nhận sau sửa) +- `ContractFeatures.cs:289` — `ContractType? Type = null` trong `ListContractsQuery` +- `ContractFeatures.cs:327` — `if (request.Type is not null) q = q.Where(x => x.c.Type == request.Type);` +- `ContractFeatures.cs:355` — derive `HasSealedCopy` (chỉ ở projection List) +- `ContractsController.cs:25` + `:27` — param + truyền +- `ContractDtos.cs:25` — `bool HasSealedCopy = false` +KHÔNG đụng: `:450` (filter Type của ListDeleted, đã có từ S159) · `:466` + `:545` (2 projection còn lại). + +## §8 — Bàn giao / caveat (khai thẳng, không giấu) + +1. **`hasSealedCopy` = `false` cứng ở 2 màn khác** (Đã xóa `:450` · Hộp thư `:529`) vì giữ literal "KHÔNG đổi gì khác". Nếu FE về sau muốn badge ở 2 màn đó → phải mở thêm 1 dòng derive mỗi site (rẻ, nhưng là quyết-định của lead, KHÔNG phải mình tự mở). +2. **Cờ không phân biệt ai upload / phase nào**: bất kỳ attachment `purpose=3` là true (kể cả HĐ chưa `DaPhatHanh`). Đúng tinh thần 1-MỐC "Upload file cứng lên → xem như xong", nhưng FE nên chỉ hiển thị badge trên list đã lọc `phase=9`. +3. **KHÔNG có guard chặn upload `purpose=3` sai phase** — đây là vùng O-A (attachment-guard) lead dặn để mở nguyên trạng. Không tự thêm. +4. **0 migration · 0 enum-extend · 0 policy mới · 0 seed/menu** ⇒ `sys.tables` + số menu 142 + authz O-A không đổi. +5. KHÔNG commit / KHÔNG push / KHÔNG sửa `SolutionErp.slnx` (0 file `.cs` MỚI ⇒ slnx không cần đụng). + +### 8.1 Isolation audit (git status cuối lượt) — có 1 file KHÔNG PHẢI của tôi, khai luôn +``` + M src/Backend/SolutionErp.Api/Controllers/ContractsController.cs ← của tôi + M src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs ← của tôi + M src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs ← của tôi +?? .claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-implementer-backend-0.md ← của tôi (run-folder) + M .claude/governance/.session-counter.json ← 🔴 KHÔNG PHẢI TÔI +``` +`.session-counter.json` = hook `session-counter-tick.ps1` bump `counter 34→35 · S160→S161 · head febe6b1→8f37910` (đọc diff xác nhận: chỉ 4 field + 1 history entry, đúng khuôn tick). Tôi KHÔNG có tool nào chạm file đó trong lượt này. Ghi ra đây để em main audit diff sau P2 không quy nhầm cho lane này. + +Tổng diff code của tôi: **3 file, +35/−4 dòng** (phần lớn là comment giải-thích "vì sao", không phải logic). + +### 8.2 Cái tôi CỐ Ý KHÔNG làm (để lead khỏi phải hỏi) +- KHÔNG thêm test (`tests/**` = test-specialist) — chỉ đảm bảo compile + suite cũ xanh. +- KHÔNG thêm `[Authorize(Policy=...)]` cho `List` (giữ `[Authorize]` trần như cũ) — thêm policy = đổi authz O-A, ngoài lệnh. +- KHÔNG migration / KHÔNG enum / KHÔNG seed / KHÔNG menu-key. +- KHÔNG đụng `ListDeletedContractsQueryHandler` dù nó là hàng-xóm gần nhất (W6 đã ghi rõ đó là bề-mặt IDOR nhạy cảm — `ContractFeatures.cs:426-431`).