From a0339babe22711c065d7047d9f0653da28ee4b24 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 12 Aug 2026 18:05:11 +0700 Subject: [PATCH] wal: flush 20260812T1805 --- .claude/WAL.md | 8 +- .../run.md | 14 +++ .../spec-soltuon-dieu-chinh-hd-12-08-2026.md | 21 +++- .../sub-style-be-b3.md | 98 ++++++++++++++++- .../sub-style-fe-b3.md | 100 +++++++++++++++++- .../sub-style-review-b4-be1.md | 17 +++ .../sub-style-review-b4-be2.md | 20 ++++ .../sub-style-review-b4-fe1.md | 16 +++ .../sub-style-review-b4-fe2.md | 16 +++ 9 files changed, 293 insertions(+), 17 deletions(-) create mode 100644 .claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-review-b4-be1.md create mode 100644 .claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-review-b4-be2.md create mode 100644 .claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-review-b4-fe1.md create mode 100644 .claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-review-b4-fe2.md diff --git a/.claude/WAL.md b/.claude/WAL.md index 9810d4e..405d361 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -15,9 +15,11 @@ chain: [x] B2 ensemble 3-lane: FAIL-11 + FIXES-12 + FAIL-11 → 2 BLOCKER tiền-đề v1 (bỏ-type=3 · pendingMe-không-tồn-tại) + W5 sai khuôn kép (KHKK cấm approver-delete; anchor code chết S175) + Q1-Q5. SPEC v2 land + b2-review-synthesis.md. Root-cause A+B HELD 3/3. -[!] B3 /fable-real ×2 style: fe + be → spec-list style đồng nhất. wf: wf_f71dc3e3-3ce · - 2 lane tier=fable · artifact sub-style-{fe,be}-b3.md -[ ] B4 /fable-clone ×2 style FE/BE — review lại 2 spec style +[x] B3 style ×2 fable: FE 25-SF+10-bẫy+13-C (#53, đĩa trọn) · BE 20-SB+7-BS+18-C (sạch). + 2 FLAG xử vào spec v2.1: BS-3 policy-khoá-oan → W5(d) đổi owner-check-handler (khớp + chốt (82) W8-hoãn) · SF-17 ContractDetailContent identical-chưa-khai-MIRROR → W5(iv) +[!] B4 /fable-clone ×2 style — 4 lane opus (anchor+khuôn ⟂ ô-đo+Goodhart ×2 vai). + wf: wf_6b7f00c2-c26 · artifact sub-style-review-b4-{fe1,fe2,be1,be2}.md [ ] B5 /fable-real reviewer — tổng hợp cuối → plan wave cụ thể "SOLTUON-12-08-2026" [ ] B6 hmw fan-out Opus 5 MAX execute toàn plan (per-wave gate + cicd 2 app + prod verify) [!] TỒN CŨ — CỬA NGOÀI-GIỜ (anh gật giờ): dọn workspace runner + dispatch verify CI · diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md index 866bccb..dc5f32f 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/run.md @@ -83,3 +83,17 @@ HEAD lúc mở run: ae1dcf4a · HMW-mode ON · lead Opus 5 MAX (default) label `WF be-style b3-speclist s190` (SPEC-LIST STYLE BE — guard-order · exception-map · comment T26 · changelog idiom · policy per-action · test style 680→680+N). Artifact: sub-style-{fe,be}-b3.md. +- **B3 KẾT QUẢ** (73 tool-use · 342K · ~13'): FE #53-return-rỗng nhưng artifact TRỌN + (25 SF + 10 bẫy-đã-đo + 13 ô C — đặc sản: SF-16 window.confirm-không-Dialog · bẫy-1 + KhkkWorkflowPanel xóa = xác-chết-còn-máy · SF-17 ContractDetailContent identical 403/403 + dòng nhưng CHƯA khai MIRROR header) · BE return sạch (20 SB + 7 BS + 18 C — 2 shape + controller-authz song song CÓ CHỦ ĐÍCH · enum-trap 98/99>5 · Reason 3-tầng 2000-char). + **Lead xử 2 FLAG vào spec v2.1:** BS-3 (policy-403-trước-owner-check + ngược chốt (82)) + → W5(d) đổi thành owner-check-handler, policy dời W8 · SF-17 → W5(iv) thêm dòng tự-khai + MIRROR. BS-1 (docs/CLAUDE.md §7 "comment English" stale vs 100% deployed comment Việt) + → NEXT-em bookend, ngoài scope wave. +- **B4 phóng** `wf_6b7f00c2-c26` — /fable-clone ×2 vai style, 4 lane opus: + `WF fe-style b4-anchor s190` · `WF fe-style b4-odo s190` (chạy thử C-1..13 baseline + + 2 biến thể grep/rg) · `WF be-style b4-anchor s190` (soi cả SB-3 policy thành thừa sau + v2.1?) · `WF be-style b4-odo s190` (T-matrix hợp nhất ca test + đo CanDelete LocalDB). + Artifact: sub-style-review-b4-{fe1,fe2,be1,be2}.md. diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md index 7302aa6..656cb3a 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md @@ -73,10 +73,18 @@ Tổng: **W4/W1/W2/W3 FE-only · W5 BE nhỏ + FE (0 migration)**. DEPLOY CHỜ dải còn lại (F-5 lens-3 — HĐ đã gen mã KHÔNG BAO GIỜ xoá được); (c) ghi **`ContractChangelog`** (Action=Delete + PhaseAtChange + Reason) — 🔴 thứ tự **`Remove()` TRƯỚC, `Add(changelog)` SAU** (bẫy T26: đảo là cascade DETACH ⇒ 0 row im lặng); - (d) `[Authorize(Policy = "Contracts.Delete")]` cho DELETE endpoint (`ContractsController.cs:78-83` - hiện `[Authorize]` trần — #82); + (d) 🔄 **v2.1 theo BS-3 (be-style B3):** KHÔNG gắn `[Authorize(Policy="Contracts.Delete")]` đợt + này — policy-403 chạy TRƯỚC owner-check, tiền lệ PE S155 đo 11/13 role `CanDelete=0` ⇒ gắn = + khoá oan Drafter mất nút xoá nháp đang có, và đi NGƯỢC chốt (82) của anh ("phân quyền tất cả + trước, phân quyền sau" — W8 HOÃN). Chốt an toàn = **owner-check trong handler** (mục (a) — đủ + diệt lỗ "user thường xoá HĐ người khác" F-4 lens-3, trả 403 qua `ForbiddenException`); policy + per-action DELETE ghi vào danh mục W8 chờ anh "ổn rồi". Nấc-0 null-guard `DrafterUserId` là + `Guid?` — khuôn `PeSoftDeleteFeatures.cs:56-58` (BS-6); (e) xác nhận sẵn: `Remove` = SOFT-delete qua `AuditingInterceptor.cs:54-62` (H-6 — wave chỉ assert, đừng đo lại). + **STYLE-GATE cho B6:** worker MỌI wave phải theo 2 spec-list style đã land cùng folder — + `sub-style-fe-b3.md` (SF-1..25 + checklist C-1..C-13) + `sub-style-be-b3.md` (SB-1..20 + 18 ô C) + — lệch luật nào phải kèm trích lời anh cho phép, không có = FLAG khi gate. FE: nút "Xóa phiếu" ở **thanh nút đáy `ContractDetailContent`** (port khuôn `KhkkDetailContent.tsx:1077-1096` — KHÔNG lấy `KhkkWorkflowPanel` làm khuôn, phần xoá trong đó là CODE CHẾT S175), hiện khi phase ∈ allow-list + user là Drafter/Admin; mirror ×2 app SHA-pair. @@ -114,9 +122,12 @@ nào phụ thuộc layout mới; W5 cuối — BE + test-before). Drafter xoá được (như cũ) · **ChoDuyet(10) CẤM kể cả Drafter** (khuôn KHKK — NGƯỢC v1) · **DaPhatHanh(9) CẤM mọi đường** · user thường xoá HĐ người khác ⇒ **403** (sửa lỗ cũ — khai rõ) · V1 nháp (2) vẫn xoá được (chống vá quá tay); ✅(ii) changelog: xoá xong đọc-ngược DB ra **đúng 1 - dòng** Action=Delete (0 dòng = dẫm T26); ✅(iii) `grep -rn "Authorize(Policy" src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` - có dòng cho DELETE; ✅(iv) nút Xóa: render đúng điều kiện (phase+vai), `ContractDetailContent` mirror - SHA ×2 app; ✅(v) build 2 app PASS. + dòng** Action=Delete (0 dòng = dẫm T26); ✅(iii) 🔄 v2.1: owner-check trong handler — test + stranger-403 (khuôn `ContractSigningPlanCrudTests` stranger-case) + KHÔNG thêm + `[Authorize(Policy` mới vào DELETE (`grep` DELETE-action giữ nguyên shape — policy = W8); + ✅(iv) nút Xóa: render đúng điều kiện (phase+vai), `ContractDetailContent` mirror + SHA ×2 app **+ thêm dòng tự-khai MIRROR ở header cùng bytes 2 bên (SF-17 — file identical + nhưng chưa khai)**; ✅(v) build 2 app PASS. ## ④ Nấc đo local (BẮT BUỘC — deploy CHỜ, ô hành-vi KHÔNG tick bằng suy luận) diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md index c5c24ac..b98863b 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-be-b3.md @@ -5,16 +5,106 @@ ## Khuôn nguồn BE -(đang đọc — append dần) +Tất cả đều là bản **ĐÃ DEPLOY trên `origin/main`** (luật S176). W5 đụng 3 file production + 1-2 file test; mỗi luật SB-n dưới neo vào ≥1 khuôn ở đây. + +| # | Nguồn (file:line) | Cho luật gì | +|---|---|---| +| K1 | `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs:1740-1807` — `DeleteContractSigningPlanCommand{,Validator,Handler}` | **Khuôn HÀNH-VI chính của W5** (lời anh "tương tự như KHKK"): guard-order, allow-list phase, ForbiddenException owner-check, changelog Remove-trước-Add, comment T26 nén | +| K2 | `src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs:14-166` | Comment đầu-file "Vì sao có file này" + XML-doc `Reason` (:34-36) + T26 bản ĐẦY-ĐỦ (:127-144) + tách-đường policy (:25-29) | +| K3 | `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs:885-902` — `DeleteContractCommandHandler` HIỆN TẠI | Nền W5 sửa: 0 owner-check, chỉ `Phase >= DangInKy` chặn, 0 changelog, 0 `ICurrentUser` | +| K4 | `ContractFeatures.cs:330-356` (khối IDOR + `[W7 S161]`) | Khuôn tag-đợt trong comment + lý-do-đặt-chỗ | +| K5 | `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:11-14` (class `[Authorize]` TRẦN) + `:29-32` (`ListDeleted` per-action `Contracts.Read`) + `:78-83` (DELETE hiện TRẦN) | Shape authz của CHÍNH controller này: class-trần + per-action policy trên endpoint nhạy cảm | +| K6 | `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs:153-156` | Khuôn DELETE endpoint: `[HttpDelete("{id:guid}")]` + `[Authorize(Policy = "KeHoachKyKet.Delete")]` + `[FromQuery] string? reason = null` | +| K7 | `src/Backend/SolutionErp.Domain/Contracts/ContractChangelog.cs:13-44` | Field changelog + `ChangelogEntityType.Contract=1` + `ChangelogAction.Delete=3` (sẵn, 0-mig) | +| K8 | `src/Backend/SolutionErp.Api/Middleware/GlobalExceptionMiddleware.cs:32-40` | Map exception→HTTP: Validation→400 · NotFound→404 · Unauthorized→401 · Forbidden→403 · Conflict→409; `detail = ex.Message` (:52) ⇒ message = chữ user đọc | +| K9 | `src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:14` (`Contracts` const) + `:251` (∈ `All`) + `:278` (`Actions = ["Read","Create","Update","Delete"]`) + `src/Backend/SolutionErp.Api/Program.cs:86-87` (factory `{menu}.{action}`) | `"Contracts.Delete"` được SINH SẴN by-construction — 0 seed mới, 0 key mới | +| K10 | `src/Backend/SolutionErp.Domain/Identity/AppRoles.cs:5-6` (`Admin`/`Drafter`) + `src/Backend/SolutionErp.Application/Common/Interfaces/ICurrentUser.cs:3-10` (`Guid? UserId · Roles · FullName · Email · IsAuthenticated`) | Vật liệu owner-check | +| K11 | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs:26-34` (2 helper reflection) + `:121-144` (`PolicyName_IsActuallyRegisterable`) + `:150-153` ("2 shape KHÁC nhau — KHÔNG ép về cùng 1 kiểu") | Khuôn test authz reflection (Pattern 10) | +| K12 | `tests/.../Application/PeDeleteByApproverTests.cs:32-41` (vì-sao fixture riêng) + `:55-104` (`SoftDeleteFixture` = IdentityFixture + `AuditingInterceptor`) + `:774` (`T26_Delete_WritesChangelogWithDeleteAction_AndCarriesReasonWhenProvided`) | Fixture wire interceptor — chống "assert rỗng nghĩa" | +| K13 | `tests/.../Application/ContractSigningPlanCrudTests.cs:469-523` (`DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds` — đo 3-trong-1) + `:525-539` (`DeleteContractSigningPlan_GuardedByOwnerAndPhase`, stranger→Forbidden) | Khuôn test delete GẦN W5 NHẤT (chính họ KHKK) | +| K14 | Migration `20260423030733_AddContractDetailsAndChangelog.cs:28` — `ContextNote nvarchar(2000)` | Neo `MaximumLength(2000)` cho validator (validator MATCH EF — khuôn K1 `:1748-1750`) | +| K15 | 4/4 csproj `src/Backend/*/*.csproj`: `net10.0` + `enable` + `enable` | Nullable strict là cấu-hình THẬT | +| K16 | `docs/STATUS.md:6` — khuôn pin test-count khai DELTA: `test **668→680**` | Chỗ ghi 680→680+N | +| K17 | Phép đo CẤM (đo 2026-08-12): `grep "catch ("` trong `Api/Controllers` = **0 hit** · `Console.WriteLine` trong `src/Backend` = **0 hit** · `FromSqlRaw\|ExecuteSqlRaw\|SqlQueryRaw` trong `Application` = **0 hit** | Luật CẤM §(g) là hiện-trạng đo được, không phải khẩu hiệu | ## SPEC-LIST STYLE BE (SB-1..) -(append sau khi đọc khuôn) +> Worker viết W5 phải thoả TỪNG SB — mỗi SB có neo khuôn deployed + cách đo. "Khuôn" = copy HÌNH, không copy LỜI KHAI (câu khai sự-thật phải tự đúng trong ngữ-cảnh Contract). + +### (a) Khuôn handler + guard-order + +- **SB-1 — Bộ 3 mảnh cùng file, cùng khối:** `record DeleteContractCommand(Guid Id, string? Reason = null) : IRequest` → `class DeleteContractCommandValidator : AbstractValidator<>` → `class DeleteContractCommandHandler(...) : IRequestHandler<>` nằm LIỀN NHAU trong `ContractFeatures.cs` dưới banner sẵn có `// ========== DELETE (soft) ==========` (:885). KHÔNG tách file mới, KHÔNG tạo folder Commands/. Khuôn: K1 `:1741-1756` (KHKK delete đủ 3 mảnh liền nhau). Hiện Delete THIẾU validator — anh em cùng nghĩa (K1) CÓ ⇒ phải thêm. +- **SB-2 — Handler = primary-constructor `(IApplicationDbContext db, ICurrentUser currentUser)`:** khuôn K1 `:1754-1756` + K2 `:50-52`. KHÔNG inject DbContext cụ thể, KHÔNG service mới. `db.ContractChangelogs` sẵn trên interface (`IApplicationDbContext.cs:40`). +- **SB-3 — Guard-order chuẩn = NotFound → Forbidden → Conflict** (đúng thứ tự dòng trong K1): (1) load `?? throw new NotFoundException("Contract", request.Id)` (:1760-1761, giữ nguyên dạng đang có ở K3 `:893-894`) → (2) owner-check `var isOwner = entity.DrafterUserId == currentUser.UserId; var isAdmin = currentUser.Roles.Contains(AppRoles.Admin); if (!isOwner && !isAdmin) throw new ForbiddenException(...)` (:1763-1766; `Contract.DrafterUserId` có thật — `Contract.cs:16`) → (3) phase allow-list `throw new ConflictException(...)` (:1778-1783). Ngoại lệ CÓ TÊN: chỉ được đảo Conflict lên trước Forbidden khi guard-người PHỤ THUỘC dữ-kiện guard-trạng-thái (khuôn K2 `:63-107` — phải biết phase/step mới tính được "đúng lượt"), và phải comment lý-do. W5 KHÔNG rơi ca này ⇒ theo K1. +- **SB-4 — Allow-list phase viết TƯỜNG MINH một guard:** ngữ nghĩa W5 = cho `{Phase < DangInKy}` ∪ `{TraLai(98), TuChoi(99)}`, chặn hết phần còn lại (bao DaPhatHanh(9) + ChoDuyet(10)). Khuôn cú pháp = pattern-matching `is not (... or ...)` (K1 `:1778-1780`); range+set trộn được nhưng phải 1 guard DUY NHẤT + comment liệt kê đủ trạng-thái cho-phép. 🔴 KHÔNG giữ nguyên `>= DangInKy` rồi vá thêm if thứ hai — 2 if rời là mầm drift. + +### (b) Exception nào cho ca nào + +- **SB-5 — Phân loại theo K8:** sai NGƯỜI (không phải Drafter-của-phiếu/Admin) = `ForbiddenException` → 403; sai TRẠNG-THÁI (phase ngoài allow-list) = `ConflictException` → 409; không thấy = `NotFoundException("Contract", request.Id)` → 404 (ctor: tên-entity-English + key — K3 `:894`). CẤM `InvalidOperationException`/`UnauthorizedAccessException`/return-null. Cả 3 loại từ `SolutionErp.Application.Common.Exceptions` (using sẵn — K2 `:4`). +- **SB-6 — Message = tiếng Việt trọn câu, có đường-thoát:** `detail = ex.Message` đi thẳng ra user (K8 `:52`). Khuôn giọng: Forbidden nói AI được phép ("Chỉ người soạn kế hoạch hoặc Admin được xoá phiếu này." — K1 `:1766`); Conflict nói TRẠNG-THÁI nào được + lối ra ("Chỉ xoá được ... ở trạng thái Nháp, Trả lại hoặc Từ chối. ... đang duyệt phải Trả lại/Từ chối trước." — K1 `:1782-1783`). Viết lại cho danh-từ "hợp đồng"/các phase HĐ (DaPhatHanh cần câu riêng ý "HĐ đã phát hành không bao giờ xoá được" — hình theo K1, lời theo ngữ-cảnh Contract). + +### (c) Comment tiếng Việt tự-khai (khuôn T26 + khuôn khai-lý-do) + +- **SB-7 — Comment allow-list phải tự khai 3 vế** (khuôn K1 `:1768-1777`): (i) VÌ SAO có allow-list này (trích/neo lời anh: *"tương tự như KHKK"* — YC-029 Đ4, spec v2 §②-W5); (ii) vì sao ChoDuyet VẪN CẤM (approver-delete = Q4 chờ anh, KHÔNG mở đợt này); (iii) nhắc 2-tầng hiển-thị⟂API (gotcha #82) vì FE cũng hiện nút theo phase+vai. Tag đợt theo khuôn K4: mở đầu comment bằng `[W5 S190]` (mirror `[W7 S161]` `:353`, `[S159-đợt5]` K5 `:29`). +- **SB-8 — Owner-check phải KHAI là "sửa lỗ sẵn có":** spec v2 W5(a) bắt khai tường minh — comment nói rõ bản cũ (K3) KHÔNG kiểm chủ-sở-hữu, đây là siết mới đồng-khuôn KHKK, không phải hành-vi từ trước. Khuôn "khai vá lệch/lỗ cũ" = K1 `:1772-1775`. +- **SB-9 — Comment T26 chép HÌNH bản NÉN của K1 `:1787-1790`** (4 dòng: thứ-tự Remove-trước-Add + cơ chế `CascadeDeleteTiming.Immediate` DETACH child `Added` ⇒ SaveChanges ghi 0 row im lặng + vì sao thêm-sau thì sống), kèm chữ "bug T26 S155" để grep được. KHÔNG chép nguyên văn 18 dòng bản ĐẦY-ĐỦ của K2 `:127-144` (bản đó dành cho file có thêm rào interceptor — Contract không cần lặp). + +### (d) Changelog — field nào, thứ tự nào + +- **SB-10 — Remove TRƯỚC, Add changelog SAU = LUẬT:** `db.Contracts.Remove(entity);` đứng TRƯỚC `db.ContractChangelogs.Add(new ContractChangelog {...});`, rồi `await db.SaveChangesAsync(ct);` MỘT lần (K1 `:1791-1805`, K2 `:145-164`). Đo: số-dòng của `.Remove(` < số-dòng của `.Add(`. +- **SB-11 — Bộ field ghi delete** (map K1 `:1793-1803` sang K7): `ContractId = entity.Id` · `EntityType = ChangelogEntityType.Contract` (header ⇒ `EntityId` BỎ TRỐNG — K7 `:19` "null nếu là Contract header") · `Action = ChangelogAction.Delete` · `PhaseAtChange = entity.Phase` (phase TẠI LÚC xoá — ghi TRƯỚC khi có gì đổi) · `UserId = currentUser.UserId` · `UserName = currentUser.FullName ?? currentUser.Email` · `Summary = $"Xoá hợp đồng {entity.MaHopDong ?? "(chưa có mã)"}"` (placeholder khuôn K1 `:1801` — đa số HĐ xoá được chưa gen mã nên vế `??` là BẮT BUỘC) · `ContextNote = reason`. KHÔNG dùng `FieldChangesJson` cho delete. +- **SB-12 — `Reason` đi trọn đường 3 tầng theo khuôn:** controller `[FromQuery] string? reason = null` (K6 `:155-156`) → command `string? Reason = null` + XML-doc khai "DeletedBy/DeletedAt do AuditingInterceptor set chỉ trả lời ai+lúc nào, LÝ DO nằm ở changelog" (K2 `:34-36`, K1 `:1740`) → validator `RuleFor(x => x.Reason).MaximumLength(2000)` + comment "MaxLength MATCH EF" neo config (K1 `:1748-1750`; số 2000 = K14) → handler `var reason = string.IsNullOrWhiteSpace(request.Reason) ? null : request.Reason.Trim();` (K1 `:1785`). + +### (e) Policy string + chỗ đặt attribute + +- **SB-13 — `[Authorize(Policy = "Contracts.Delete")]` đặt NGAY TRÊN action Delete, class-level GIỮ `[Authorize]` trần:** shape của CHÍNH controller này (K5: class trần `:13` + per-action `ListDeleted` `:31-32` kèm câu "chặt hơn [Authorize] trần của class — không phá gì"). 🔴 CẤM "sửa cho nhất quán" sang class-level policy kiểu KHKK — 2 shape tồn tại CÓ CHỦ ĐÍCH (K11 `:150-153`); class-level policy trên ContractsController sẽ khoá GET list của mọi role thiếu Contracts.Read (gotcha #44 silent-403). Tên policy hợp lệ by-construction: K9 (KHÔNG sinh key mới, KHÔNG sửa `DbInitializer`, KHÔNG sửa `MenuKeys`). +- **SB-14 — Comment trên endpoint khai lý-do policy** theo khuôn K5 `:29-30` (2 dòng: đợt + vì-sao): nêu `#82` ([Authorize] trần = lỗ), và policy này là tầng-2 ĐỘC LẬP với owner-check trong handler (defence-in-depth như KHKK: K6 có policy VÀ K1 có owner-check). + +### (f) Test style (test-before) + +- **SB-15 — Chỗ đặt + fixture:** file mới `tests/SolutionErp.Infrastructure.Tests/Application/ContractDeleteGuardTests.cs` (hoặc tên `Contract...Tests` cùng dạng; namespace `SolutionErp.Infrastructure.Tests.Application`). 🔴 Fixture PHẢI wire `AuditingInterceptor` — chép hình `SoftDeleteFixture` K12 `:55-104` (private sealed trong file test, KHÔNG sửa `IdentityFixture` dùng chung — lý-do K12 `:41`: 500+ test đang dựa hành-vi cũ). Thiếu interceptor thì `Remove()` = xoá CỨNG ⇒ assert soft-delete/changelog "đúng ở cả hai thế giới" = đo số không (K12 `:32-41`). +- **SB-16 — Đặt tên + cấu trúc:** xUnit `[Fact]` + FluentAssertions; tên method English PascalCase `Hành-động_Kịch-bản_Kỳ-vọng` (khuôn K13 `:482` `DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds`, `:531` `..._GuardedByOwnerAndPhase`); banner section `// ==== (n) ... ====` tiếng Việt khai VẬT ĐO + VÌ SAO (K13 `:469-480`); because-message tiếng Việt trong mọi `.Should()`; assert exception khuôn `(await act.Should().ThrowAsync("...")).WithMessage("*mảnh-tiếng-Việt*")` (K13 `:459-462`). AAA ngầm bằng dòng trống + comment `// ---------- TRƯỚC / ACT / SAU ----------` khi test dài (K12-file `:330-357`). +- **SB-17 — Bộ ca BẮT BUỘC** (spec v2 ✅W5-i, đủ 6): TraLai Drafter xoá được · TuChoi Drafter xoá được · DangSoanThao Drafter xoá được (giữ cũ) · ChoDuyet CẤM kể cả Drafter (409) · DaPhatHanh CẤM mọi đường kể cả Admin (409) · người-lạ xoá HĐ người khác → 403 (khuôn stranger `new TestCurrentUser(Guid.NewGuid(), ...)` K13 `:538`) · +V1-nháp phase 2 vẫn xoá được (chống vá quá tay). Mỗi ca xoá-thành-công phải assert 3-trong-1 theo K13 `:490-509`: (a) khuất query thường + `IgnoreQueryFilters()` row còn `IsDeleted=true` + `DeletedBy` đúng; (c) changelog đọc-ngược DB `Single(l => l.Action == ChangelogAction.Delete)` + `ContextNote`/`PhaseAtChange`/`UserId` đúng — 0 dòng = dẫm T26. +- **SB-18 — Reflection-authz:** THÊM `[Fact]` vào `AuthorizePolicyRegressionTests.cs` (file sẵn — không tạo file mới): `GetActionAuthorize(typeof(ContractsController), nameof(ContractsController.Delete))` → `attr.Should().NotBeNull(...)` + `attr!.Policy.Should().Be("Contracts.Delete", "...")` (khuôn K11 `:73-80`) + giữ nếp kiểm registerable: policy split 2 phần ∈ `MenuKeys.All`/`MenuKeys.Actions` (K11 `:121-144`). Because-message nêu #82 + ngày/đợt. +- **SB-19 — Test-before + pin số:** viết test TRƯỚC (RED trên code hiện tại — ca ChoDuyet-CẤM và 403-người-lạ phải ĐỎ với K3) rồi code W5 (GREEN) — khuôn W3 KHKK "test-before RED→GREEN" (`docs/STATUS.md:11`). Chạy `dotnet test SolutionErp.slnx`; số pin 680→680+N khai DELTA vào row session `docs/STATUS.md` (khuôn `:6` "test **668→680**") — ĐÚNG 1 row, không neo số tuyệt đối chỗ khác (B1). + +### (g) CẤM (đã đo hiện-trạng = 0, giữ 0 — K17) + +- **SB-20:** KHÔNG `try-catch` trong controller (exception bay lên K8) · KHÔNG `Console.WriteLine` (Serilog qua `ILogger`; và theo khuôn K1/K2: handler delete KHÔNG log gì — middleware đã log Warning cho 4xx `K8:42-45`, đừng thêm log thừa) · KHÔNG raw SQL trong Application (`FromSqlRaw`/`ExecuteSqlRaw`) · Nullable strict đang bật 4/4 csproj (K15) — không `!` nén warning, `Guid?` null-check tường minh trước dùng (khuôn K2 `:56-58`) · KHÔNG migration, KHÔNG sửa schema (`ChangelogAction.Delete` sẵn — K7 `:42`), KHÔNG đổi `SolutionErp.slnx` (0 file .cs mới ngoài test — file test nằm trong project test sẵn có). ## Bẫy style BE repo này -(append) +- **BS-1 — Doc nói "comment English", bản deploy nói ngược:** `docs/CLAUDE.md` §7 ghi *"Code + comment: English"*, nhưng TOÀN BỘ khuôn deploy (K1/K2/K4/K5 + cả 2 file test K11-K13) là comment tiếng Việt tự-khai. Luật S176: deployed = CHUẨN ⇒ **theo tiếng Việt**. FLAG cho lead: dòng doc đó stale — KHÔNG tự sửa doc trong wave này (ngoài scope W5). +- **BS-2 — Enum value 98/99 nằm NGOÀI dải tuyến-tính:** `TraLai=98`, `TuChoi=99` LỚN HƠN `DaPhatHanh=9` về số ⇒ guard cũ `Phase >= DangInKy` (K3 `:896`) đang chặn cả 2 phase mà W5 phải MỞ. Mọi so-sánh range mới phải xử lý 98/99 bằng vế TẬP tường minh, không bằng range (đây chính là lý-do SB-4 đòi 1 guard duy nhất). +- **BS-3 — Policy gắn vào đường xoá Drafter đang dùng = nguy-cơ khoá oan (tiền-lệ PE S155):** K2 `:25-29` khai đo DB Dev 11/13 role `CanDelete=0` ⇒ PE phải TÁCH ĐƯỜNG để Drafter không mất quyền xoá nháp chính mình. W5 chọn khuôn KHKK 1-đường (policy + owner-check trên CÙNG endpoint) — đúng khuôn, NHƯNG hệ-quả data: role Drafter thiếu `CanDelete` trên key `Contracts` sẽ 403 TRƯỚC KHI owner-check chạy ⇒ Drafter có thể MẤT nút xoá nháp đang có (hành-vi hiện tại: DELETE trần, ai đăng nhập cũng gọi được). Đây là quyết-định data/quyền, KHÔNG phải style — **FLAG lead ĐO ma-trận quyền prod trước khi ship** + khai trong PR; FE W5 gate nút theo phase+vai (spec) nhưng tầng API gate theo policy ⇒ đúng lớp 2-tầng #82, có thể "bấm được mà 403". +- **BS-4 — Assert-rỗng-nghĩa trong test:** fixture thiếu `AuditingInterceptor` thì soft-delete thành hard-delete, mọi assert "khuất khỏi list" xanh oan (K12 `:32-41`). Neo chống-vacuous khuôn K12 `:352-355`: sau ACT phải assert `IgnoreQueryFilters().Single(...).IsDeleted == true` TRƯỚC khi assert các hệ-quả. +- **BS-5 — Bê khuôn nhưng đừng bê lời khai:** message/comment của K1 nói "kế hoạch", "phiếu PE bị khoá 409" — chép nguyên sang Contract là khai SAI sự-thật trong ngữ-cảnh mới (bài S176). Mọi câu tiếng Việt phải viết lại quanh danh-từ "hợp đồng" + phase HĐ. +- **BS-6 — `DrafterUserId` là `Guid?`:** (`Contract.cs:16`) — HĐ cũ/di-sản có thể null ⇒ owner-check `entity.DrafterUserId == currentUser.UserId` với cả 2 vế nullable: null == null-user là ca không được lọt. Khuôn K2 `:56-58` chặn từ cửa: `if (!currentUser.IsAuthenticated || currentUser.UserId is null) throw new UnauthorizedException();` — thêm nấc-0 này là ĐÚNG khuôn khi handler cần định-danh (K1 bỏ qua vì controller đã `[Authorize]`, nhưng so-sánh nullable của K1 `:1763` chỉ an toàn nhờ `[Authorize]`; W5 nên theo K2 cho kín — 1 dòng, đo được). +- **BS-7 — `SaveChangesAsync` đúng MỘT lần:** khuôn K1/K2 kết bằng 1 lời gọi duy nhất — 2 lần Save (một cho Remove một cho changelog) đẻ nhu-cầu transaction + vỡ atomicity (K2 `:143-144` khai đã cân nhắc và BÁC). ## Checklist style ĐO ĐƯỢC cho worker -(append) +Chạy từ repo root; PASS = đúng vế kỳ-vọng. (Số dòng dưới là của bản HIỆN TẠI — sau khi worker sửa, đo bằng pattern, không bằng số dòng.) + +| # | Lệnh đo | Kỳ vọng | +|---|---|---| +| C1 | `grep -n "record DeleteContractCommand" src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | 1 hit, có `string? Reason = null` | +| C2 | `grep -n "DeleteContractCommandValidator" src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | ≥1 hit + gần đó `MaximumLength(2000)` | +| C3 | `grep -n "ICurrentUser currentUser" src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | có hit trong `DeleteContractCommandHandler` | +| C4 | Trong thân handler delete: dòng `NotFoundException` < dòng `ForbiddenException` < dòng `ConflictException` | thứ tự SB-3 | +| C5 | `grep -n "AppRoles.Admin" ...ContractFeatures.cs` (vùng delete) + `grep -n "DrafterUserId == currentUser.UserId" ...` | owner-check 2 vế có mặt | +| C6 | Dòng `db.Contracts.Remove(` < dòng `db.ContractChangelogs.Add(` ; `grep -c "SaveChangesAsync"` trong handler delete = 1 | SB-10 · BS-7 | +| C7 | `grep -n "ChangelogAction.Delete" ...ContractFeatures.cs` + `grep -n "PhaseAtChange" ...` + `grep -n '(chưa có mã)' ...` | bộ field SB-11 | +| C8 | `grep -n "T26" ...ContractFeatures.cs` | ≥1 (comment SB-9) | +| C9 | `grep -n '\[W5' ...ContractFeatures.cs` | ≥1 (tag đợt SB-7) | +| C10 | `grep -n 'Authorize(Policy = "Contracts.Delete")' src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` | =1, đứng giữa `[HttpDelete("{id:guid}")]` và `public async Task Delete` | +| C11 | Class-level controller: `[Authorize]` TRẦN vẫn nguyên tại `:13` (không Policy) | SB-13 | +| C12 | `grep -n "FromQuery] string? reason" ...ContractsController.cs` | 1 hit trên Delete (SB-12) | +| C13 | `grep -rn "nameof(ContractsController.Delete)" tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs` | ≥1 (SB-18) | +| C14 | File test mới: `grep -n "AuditingInterceptor" tests/.../Application/Contract*Tests.cs` + `grep -n "IgnoreQueryFilters" ...` | ≥1 mỗi cái (SB-15, BS-4) | +| C15 | Đếm ca trong file test mới: TraLai · TuChoi · DangSoanThao · ChoDuyet-409 · DaPhatHanh-409 · stranger-403 · V1-phase2 | 7 ca SB-17 đủ mặt | +| C16 | `dotnet test SolutionErp.slnx` | PASS 680+N; row session `docs/STATUS.md` khai `680→680+N` | +| C17 | `grep -rn "catch (" src/Backend/SolutionErp.Api/Controllers/` · `grep -rn "Console.WriteLine" src/Backend/` · `grep -rnE "FromSqlRaw\|ExecuteSqlRaw" src/Backend/SolutionErp.Application/` | 0 · 0 · 0 (giữ nguyên K17) | +| C18 | `git diff --stat` của wave | KHÔNG đụng `Persistence/**`, KHÔNG file `.cs` mới ngoài `tests/`, KHÔNG sửa `MenuKeys.cs`/`DbInitializer.cs`/`SolutionErp.slnx` | + +== HẾT == diff --git a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md index d7e1278..ec68c94 100644 --- a/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md +++ b/.claude/workflows/runs/2026-08-12-S190-soltuon-dieu-chinh-hd/sub-style-fe-b3.md @@ -5,16 +5,106 @@ ## Khuôn nguồn (file nào là chuẩn cho việc gì) -(đang ghi) +Tất cả đều ĐÃ DEPLOY prod (origin/main, ngoài paths-ignore) — luật S176: đây là CHUẨN, lệch không có lời anh = FLAG. + +| Việc trong wave | Khuôn ĐÃ CHẠY | Anchor | +|---|---|---| +| Host 3-panel + trục danh-tính URL (W2) | `KhkkListPage.tsx` | `fe-user/src/pages/khkk/KhkkListPage.tsx:156,171` — `h-[calc(100vh-4rem)]` + `lg:grid-cols-[400px_1fr_360px]`; trục URL đọc-only :62-68; override 3-trạng-thái :71-77; reset-in-render :81-88 | +| Panel danh sách + Select filter + banner thùng rác (W2 panel-3, Q2 Select) | `KhkkListPanel.tsx` | `fe-user/src/components/khkk/KhkkListPanel.tsx:326-360` — `