diff --git a/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-database-reviewer-style-3.md b/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-database-reviewer-style-3.md index 49b28cc8..b6779754 100644 --- a/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-database-reviewer-style-3.md +++ b/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-database-reviewer-style-3.md @@ -4,7 +4,11 @@ > KHÔNG chấm thiết-kế schema (FK strategy / index perf / concurrency = `database-agent`). > Địa-phận: `src/Backend/SolutionErp.Infrastructure/Persistence/**`. -## 0. Thước phạm-vi (untracked-aware) +**VERDICT: `G5-DB-STYLE: PASS-WITH-FINDINGS` — 3-file ĐỦ, 1 finding + 1 note out-of-diff.** + +--- + +## 0. Thước phạm-vi (untracked-aware — bắt được migration mới) ``` { git diff --name-only origin/main...HEAD @@ -13,18 +17,90 @@ } | sort -u | grep -i 'Persistence/' ``` -(kết quả + đo đạc append phía dưới) +4 hit — đúng 4 file, không hơn: -## 1. 3-file rule +| file | trạng thái git | +|---|---| +| `Persistence/DbInitializer.cs` | ` M` (+45/−6) | +| `Persistence/Migrations/20260813204332_AddContractSealedAt.cs` | `??` UNTRACKED | +| `Persistence/Migrations/20260813204332_AddContractSealedAt.Designer.cs` | `??` UNTRACKED | +| `Persistence/Migrations/ApplicationDbContextModelSnapshot.cs` | ` M` (+6/−0) | -## 2. Tên migration +🔴 2/3 file migration là **UNTRACKED** ⇒ thước `git diff` đơn thuần sẽ **0 hit** — đúng cái bẫy S179. Vế `git ls-files -o` là thứ bắt được. -## 3. Cột SealedAt / SealedByUserId +`ContractConfiguration.cs` **KHÔNG** trong diff ⇒ `DraftData` (nơi chứa thân HĐ) đã có sẵn từ trước (`ContractConfiguration.cs:21` `HasColumnType("nvarchar(max)")`) ⇒ vế 2 của YC-032 **không nợ migration**. Đây là điểm CỘNG, không phải sót. -## 4. Down() reversible +## 1. 3-file rule — ĐỦ 3/3 -## 5. DbInitializer seed +- `ls 20260813204332*` → 2 file (`.cs` 1134 B + `.Designer.cs` 268 739 B), cùng mtime `Aug 14 03:43`. +- `ApplicationDbContextModelSnapshot.cs` (file dùng chung) **CÓ đổi**: `git diff --stat` = `1 file changed, 6 insertions(+)`. +- grep `Sealed` trong snapshot → 2 hit: `:866 b.Property("SealedAt")` · `:869 b.Property("SealedByUserId")`. +- 🔴 **CONTROL DƯƠNG** (chống 0-hit giả): `grep -c Sealed` trong Designer MỚI = **4**; trong Designer của Mig 73 (`20260812141501_...EndedByLevelFinalize.Designer.cs`) = **0** ⇒ thước sống, và vắng-mặt ở bản trước là vắng THẬT. +- Designer có attribute đúng id: `[Migration("20260813204332_AddContractSealedAt")]` (`...Designer.cs:15`). -## 6. has-pending-model-changes +## 2. Tên migration — KHỚP khuôn -## 7. VERDICT +`AddContractSealedAt` = `Add` PascalCase, khuôn `Add`. +**Neo bản ĐÃ APPLY PROD:** Mig 73 `20260812141501_AddContractEndedByLevelFinalize` — cùng bảng `Contracts`, cùng dạng `Add` + `Contract` + tên tính năng. Timestamp `20260813204332` > `20260812141501` ⇒ thứ tự apply đúng, không chèn ngược. + +## 3. Cột `SealedAt` / `SealedByUserId` — KHỚP họ `edAt` + `edByUserId` + +`Mig.cs:14-24`: `AddColumn("SealedAt", "Contracts", "datetime2", nullable: true)` + `AddColumn("SealedByUserId", "Contracts", "uniqueidentifier", nullable: true)`. + +| trục | bản mới | bản ĐÃ APPLY PROD (neo) | khớp? | +|---|---|---|---| +| hậu-tố `...ByUserId` | `SealedByUserId` | 11 cột toàn repo: `SignedByUserId` ×7, `ApprovedByUserId`, `BookedByUserId`, `UploadedByUserId` (snapshot `:692/:1245/:4304/:4334/:4776/…`) | ✅ | +| kiểu | `uniqueidentifier` + `datetime2` | `AddContractLevelOpinions.cs:22-23` (Mig 33) `SignedAt datetime2` + `SignedByUserId uniqueidentifier` | ✅ | +| nullable | cả 2 `null` | `ApprovedByUserId` `Guid?` (Mig 69 KHKK, snapshot `:422`) — mốc chưa xảy ra = null | ✅ | +| loose-Guid KHÔNG FK vật lý | không FK | Mig 69 `AddContractSigningPlans.cs`: grep `ByUserId` ∩ `ForeignKey` = **0 hit**, mà CONTROL DƯƠNG cùng file = **8** `ForeignKey` ⇒ họ `*ByUserId` cố ý loose. Cùng lớp: `WorkItemId` (Mig 49), `Department.ParentId` (Mig 51) | ✅ | +| index | KHÔNG index | 1/11 cột `*ByUserId` có index (chỉ `BookedByUserId` — `MeetingBooking`, là khoá truy vấn). `SealedBy` là con-dấu audit, không phải khoá lọc ⇒ theo đa số 10/11 | ✅ | +| khai trong `Configurations/` | không khai | `ContractConfiguration.cs` chỉ khai maxlength/precision/enum-conversion/index; cột `Guid?`/`DateTime?` trần đi theo convention (`SlaDeadline`, `CurrentApprovalLevelOrder` cũng 0 dòng `Property`) ⇒ **không khai mới là đúng khuôn** | ✅ | + +🔸 Không có `SealedByFullName` denorm (LevelOpinions có `SignedByFullName`) — **không tính lệch**: neo đúng của con-dấu-trên-chính-entity là bộ audit `BaseEntity.cs:7-9` (`CreatedBy`/`UpdatedBy` = Guid trần, không denorm tên), còn `SignedByFullName` thuộc bảng vệ-tinh in ra hộp ý-kiến. + +## 4. `Down()` — reversible THẬT + +`Mig.cs:28-37` = `DropColumn("SealedAt") + DropColumn("SealedByUserId")` — nghịch-đảo ĐÚNG của `Up`, không `Sql()` backfill nên không có vế data không đảo được. +**Neo:** Mig 73 `20260812141501_...cs:22-27` cùng hình-dạng `AddColumn → DropColumn`, không thừa không thiếu. + +## 5. `has-pending-model-changes` — TỰ CHẠY, không tin lời lead + +``` +dotnet ef migrations has-pending-model-changes --project ...Infrastructure --startup-project ...Api +→ Build succeeded. +→ No changes have been made to the model since the last migration. +``` +Khớp số lead khai. ⇒ snapshot ⟂ model đồng bộ, migration kế tiếp sẽ không sinh duplicate column (gotcha #17 không dính). + +## 6. `DbInitializer.cs` — seed template + menu-row + +**ĐÚNG khuôn (3 trục):** +- Menu-row 2 loại mới **tự sinh**, không hardcode: `MenuKeys.cs:44-45` `ContractTypeCodes = ["ThauPhu", "ThauPhuVatTu", "ThauPhuThietBi", "GiaoKhoan", …]` (chèn giữa ⇒ thứ tự 01 · 01A · 01B · 02…), tiêu thụ tại `DbInitializer.cs:2198 / :2218 / :2591 / :2627`. +- Nhãn đi đủ 3 nhà: `typeLabels` (`:2055-2056`), leaf `Hdc_*` (`:2382-2383`), group (`:2399-2400`). +- Vòng `reconciled` (`:3904-3921`) **idempotent + cùng khuôn `labelBackfill`** (`:2408-2422`): so-sánh → chỉ ghi khi lệch → đếm → `SaveChanges` chỉ khi >0 → log. Chạy lần 2 = 0 ghi. `existingCodes` chụp TRƯỚC vòng add (`:3872`) nên reconcile chỉ đụng row cũ, không đụng row vừa insert. + +**🔴 FINDING F-1 — reconcile giành quyền ghi với admin UI trên 2 cột admin sửa được (seed-convention, không phải schema-design).** + +`DbInitializer.cs:3913-3914` +```csharp +if (row.IsActive != shouldBeActive) { row.IsActive = shouldBeActive; reconciled++; } +if (row.ContractType != type) { row.ContractType = type; reconciled++; } +``` +Comment ngay trên (`:3900-3902`) tự đặt luật: *"KHÔNG đụng `Name`/`Description` — admin có thể đã sửa tay qua UI, ghi đè là xoá công người dùng"*. Nhưng **cả 2 cột đang bị đụng ĐỀU do chính admin UI ghi**: +- `FormFeatures.cs:283` `entity.ContractType = request.ContractType;` (`UpdateContractTemplateCommandHandler`) — admin trỏ lại loại HĐ cho mẫu ⇒ **restart kế tiếp bị lật về hằng-số**. +- `FormFeatures.cs:307` — **DELETE mẫu = `entity.IsActive = false`** (`DeleteContractTemplateCommandHandler`, comment *"simpler MVP: just flip IsActive"*). File `.docx` vẫn nằm trên đĩa ⇒ `shouldBeActive = true` ⇒ **mẫu admin vừa xoá SỐNG LẠI mỗi lần khởi động API**. + +Ungated: `SeedContractTemplatesAsync` gọi ở `DbInitializer.cs:119`, **NGOÀI** rào `if (!demoSeedDisabled)` (`:117`) ⇒ chạy mọi env kể cả prod. Đây đúng lớp bài học repo *"seeder ungated re-add mỗi lần restart"* (#75/#76) — luật của em: seed/backfill mới **phải khai rõ gate**. +Vá rẻ nhất, giữ nguyên ý đồ: chỉ reconcile đúng 2 `FormCode` đang re-trỏ (`002.02` / `002.03`), hoặc chỉ cho phép chiều `false → true` **một lần** khi file mới xuất hiện. Quyết ở lead/owner — em không sửa code. + +**🔸 NOTE N-1 (NGOÀI diff, pre-existing = CHUẨN theo luật lõi, KHÔNG tính finding G5):** `ContractTemplateConfiguration.cs:24` `HasIndex(x => x.FormCode).IsUnique()` **không filter** `IsDeleted`, trong khi entity là soft-delete (`ContractTemplate : AuditableEntity` + `HasQueryFilter(x => !x.IsDeleted)` `:26`). Repo đã 6× vá đúng lớp này (Mig 43/45/47, gotcha #57). Phơi-nhiễm hiện **bằng 0** vì đường DELETE lật `IsActive` chứ không lật `IsDeleted` (`FormFeatures.cs:307`) ⇒ ghi lại để khỏi quên, KHÔNG chặn G5. + +## 7. Neo THÁCH-CLEAN (cho các trục em tuyên khớp) + +- `20260813204332_AddContractSealedAt.cs:14-18` ⇄ `20260812141501_AddContractEndedByLevelFinalize.cs:13-19` — cùng `AddColumn` trên bảng `Contracts`, cùng cặp `Up`/`Down` thuần, 0 `Sql()`. +- `20260813204332_AddContractSealedAt.cs:20-24` ⇄ `20260522052240_AddContractLevelOpinions.cs:23` — `SealedByUserId uniqueidentifier` ⇄ `SignedByUserId uniqueidentifier`, **cả hai KHÔNG FK vật lý**. +- `DbInitializer.cs:3904-3921` ⇄ `DbInitializer.cs:2408-2422` — cùng khuôn backfill hội-tụ (so-sánh → ghi-khi-lệch → đếm → save-if->0). + +## 8. Ghi chú phép đo không lấy được + +`sqlcmd -S .\SQLEXPRESS -d SolutionErp -E -Q "SELECT TOP 3 MigrationId FROM __EFMigrationsHistory"` → **không chạy được ở máy này** (`Error Locating Server/Instance Specified` — instance prod nằm trên VPS). Không ảnh hưởng kết luận: mọi neo "đã apply prod" em dùng đều là migration cũ (26 / 33 / 49 / 51 / 69 / 73) đã có lịch sử prod-verified trong `CLAUDE.md` + skill `ef-core-migration`. Em KHÔNG ssh vào prod (read-only reviewer). diff --git a/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-reviewer-5.md b/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-reviewer-5.md index 1ecf47b8..62ec6114 100644 --- a/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-reviewer-5.md +++ b/.claude/workflows/runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-reviewer-5.md @@ -1,37 +1,39 @@ -# sub-reviewer-5 — G5 vòng chấm cuối YC-032 (HĐ Thầu phụ tách 2 form) +# sub-reviewer-5 — G5 ADVERSARIAL GATE (YC-032, trước deploy prod) -> Vai: reviewer adversarial (read-only). Ngày 2026-08-13/14. Trần lượt ~40. -> Diện: 38 file sửa (+984/−85) + 11 file/thư-mục mới (chưa `git add`). -> Nhiệm vụ: soi 6 trục, mỗi trục tự đo bằng lệnh, 0-hit kèm control-dương. +> Lượt CHẠY LẠI đã-cắt-gọt (ghi đè khung rỗng 880 B của lượt trước). +> Kỷ luật: mọi số từ lệnh vừa chạy · 0-hit kèm control-dương · append SAU MỖI trục, không gom cuối. -## 0. Trạng thái cây (đo) +## 0. Phạm vi đo (diff + untracked) + +`git status --porcelain` = **49 dòng** (38 ` M` tracked + 11 `??` untracked). +`git diff --stat` (tracked only) = **38 file, +1009 / −92**. +🔴 11 mục untracked mà `git diff` thuần BỎ SÓT: +`fe-{admin,user}/src/components/contracts/ContractBodyView.tsx` · `fe-{admin,user}/src/lib/contractBody/` (thư mục) · `fe-{admin,user}/src/lib/contractDraftData.ts` · 2 file `.docx` template (002.02 / 002.03) · `20260813204332_AddContractSealedAt{.cs,.Designer.cs}` · `tests/.../ContractSealTests.cs`. + +## 1. Trục 1 — Nhánh câm thứ 5 (switch/ternary nuốt 8/9) (đang đo) -## 1. Trục 1 — nhánh câm còn sót (switch trên ContractType) +## 2. Trục 2 — Menu mất im lặng (map code↔int, staticMap `Hdc_*`) (đang đo) -## 2. Trục 2 — menu biến mất im lặng (gotcha #50) +## 3. Trục 3 — `paymentTerms` có bị xoá không (3 đường ghi) (đang đo) -## 3. Trục 3 — lược đồ DraftData, 3 đường ghi +## 4. Trục 4 — Khoá bản cứng có RĂNG thật (`SealedAt`) (đang đo) -## 4. Trục 4 — khoá bản cứng có răng thật không +## 5. Trục 5 — Key ô trùng trong `body002_0*.ts` (đang đo) -## 5. Trục 5 — key ô trùng trong body002_0*.ts +## 6. Trục 6 — Pre-fill khớp `slot.key` + N+1 query (đang đo) -## 6. Trục 6 — pre-fill từ KHKK khớp key + N+1 +## 7. VERDICT -(đang đo) - -## 7. Bảng finding tổng hợp - -(đang đo) +(chờ)