Files
solution-erp/.claude/agent-memory/implementer-backend/archive/2026-06.md
2026-07-13 17:02:40 +07:00

47 lines
32 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.

# Implementer-Backend — Archive 2026-06 (S41-S55 verbose, curated S? Harness-9)
> **Archived:** 2026-06-17 Harness-9 Stage B curate (em main proxy). 14 verbose Recent-activity bullets moved BYTE-EXACT from MEMORY.md (L87-L104) to keep L1 HOT slim < 25KB.
> **Scope:** S55 master-import → S41 P11-A Wave 1 schema (+ 3 already-condensed Archived-stubs pointing git d2f52ba / earlier q4). FIFO chronological NEWEST-first as they appeared in L1.
> **FROZEN:** entries below are verbatim copies — do NOT reflow/edit. Re-grounding of stale counts is a separate pass (additive-only).
> **KEEP in MEMORY.md:** header+role+split boundary + BE Patterns 1-4/7-9/12-bis/12-ter + anti-patterns+BE conventions + Curate trigger + 6 newest Recent-activity (06-17 Off_Dashboard → S56 GOLIVE-HARDEN).
---
## Moved Recent-activity entries (verbatim, FIFO newest-first as in L1)
- **S55 master-data import (Mig 48 `AddProjectMasterFields` 4 AddColumn no-table + `SeedRealMasterDataAsync` 62 Project+71 WorkItem+3 Supplier) [proxy by em main — agent return truncated gotcha #53 before MEMORY step]:** Project entity +4 prop (`Year int?`, `Investor/Location/Package string?`, maxlen 250/500/300 ProjectConfiguration). `ProjectFeatures.cs` DTO+CreateCmd+UpdateCmd+validators+handlers+List/Get projections +4 (all nullable, appended end). **`SeedRealMasterDataAsync`** = 3 tuple-loop per-code idempotent (mirror `SeedDemoMasterDataAsync:2185` `existingCodes.Contains→skip`) wired UNGATED line 118 AFTER `SeedCatalogsAsync` → reaches prod (DemoSeed:Disabled=true KHÔNG gate, by-design như SeedDemoMasterData/Catalogs). Project Name=Code khi Excel blank. WorkItem 4 Category (Vật tư16/Thầu phụ30/MEP9/Thiết bị16, gen Code VT/TP/MEP/TB-NN; divider "THIẾT BỊ" dropped). Supplier NTP→NhaThauPhu/NCC→NhaCungCap, extras→Note. **FLOCK01 collision** demo↔real → per-code skip (demo thắng, real code+year only, OK). Compile-fix `MasterCatalogFilteredUniqueTests.cs` +4 null args CreateProjectCommand (necessary build-green). **Runtime Dev proof (em main):** app-start seeded 62proj/71wi/3sup landed, CAL01.Investor col populates, 0 overflow/dup. Build 0/0, test 216. Data spec `scripts/master-import-data.generated.md`. Tag `[s55, master-import, mig48, seed-real-ungated, project-4field]`.
- **S54 ItTicket reassign cross-stack — IT-staff self-service (NO migration, 2 BE file edit):** NEW `GetAssignableItStaffQuery`+`AssignableStaffResult(CanReassign,Staff)`+`AssignableStaffDto(Id,FullName)` capability endpoint (REGION 5 WorkflowAppsFeatures.cs) + MODIFIED `AssignItTicketHandler`: authz Admin-OR-dept-IT → `ForbiddenException`; assignee-must-be-IT → `ConflictException`. Controller `/assign` hạ `[Authorize(Roles="Admin")]``[Authorize]` (handler enforce fine-grained data-driven) + NEW `GET /assignable-staff`. Predicate IT = reuse round-robin S52 `Departments.Where(Code=="IT" && !IsDeleted)`. `ICurrentUser` KHÔNG có DepartmentId → query `db.Users.Where(Id==cu.UserId).Select(DepartmentId)`. 2 pattern split (em main reconciled từ stray src/Backend/.claude — cwd-relative Write mishap): [[pattern-controller-lower-authorize-handler-finegrained]] + [[pattern-scoped-capability-endpoint-anti-silent-403]]. Build 0/0, test 203→216 (test-specialist +13 authz), reviewer PASS (role-string "Admin" chain-verified real: AppRoles→SeedRoles→JWT ClaimTypes.Role→cu.Roles). Tag `[s54, it-ticket-reassign, capability-endpoint, authz-handler, no-mig]`.
- **S54 Task D BE — promote AttendanceReport to sidebar menu leaf (NO migration, 2 file edit):** Case 1 mechanical, menu = DbInitializer idempotent seed (not schema). 3 insert: (1) MenuKeys.cs const `OffAttendanceReport = "Off_AttendanceReport"` after OffChamCong:124 · (2) MenuKeys.cs All[] Off-group line +`OffAttendanceReport` after OffChamCong:158-159 · (3) DbInitializer.cs menu tuple `(OffAttendanceReport, "Báo cáo chấm công", Off, 8, "FileBarChart")` after OffChamCong:1787 (Order 8, parent Off, mirror Vehicle/Driver S51). **adminPermAutoViaAll=TRUE verified 2-point:** `SeedAdminPermissionsAsync` DbInitializer:1916 iterates `MenuKeys.All` → full-CRUD Permission row per missing key (idempotent `existingMenuKeys.Contains`); `Program.cs:78` iterates All × Actions → policy registration. +All[] = both auto, NO manual grant. **Idempotent-add verified:** menu upsert loop DbInitializer:1845-1862 `existingItems.TryGetValue(key)` miss → `MenuItems.Add` (existing prod gets leaf on restart, existing rows only Order-reconciled — same as S51). Build 0 err (2 pre-existing DocxRenderer warn). KHÔNG touch FE (menuKeys.ts/Layout = implementer-frontend) / tests / commit. Tag `[s54, task-d, menu-leaf, no-mig, admin-perm-via-all]`.
- **S53 gotcha #57 EXT BE — filter 3 Master Code unique indexes + Mig 46 local catch-up (Mig 47 `FilterMasterCatalogUniqueIndexesByIsDeleted`, index-only no-table):** Test-before RED→GREEN driven (test-specialist `MasterCatalogFilteredUniqueTests`, 3 FAIL on unfiltered → must turn GREEN). gotcha #57 4th+5th+6th cumulative (S45 Holiday Mig 43, S51 HRM×3 Mig 45 → now Department/Project/Supplier). Edit 3 config Code unique: `b.HasIndex(x=>x.Code).IsUnique()``+.HasFilter("[IsDeleted] = 0")`. **KEY: copied EXACT filter string byte-for-byte from HolidayConfiguration:18 + LeaveTypeConfiguration:19** (spaces around `=`, NOT guessed) → snapshot+SQL Server consistent with 13 existing filtered indexes. SupplierConfiguration: ONLY Code index filtered, `HasIndex(x=>x.Type)` :25 LEFT untouched (verified snapshot 3590 Type no-filter). Mig diff CLEAN: Up=3×DropIndex+3×CreateIndex filtered, Down=3×reverse unfiltered (no drift, no extra table/col). Master entities HAVE global `HasQueryFilter(!IsDeleted)` (unlike HRM) — app-check `db.X.AnyAsync(Code==req.Code)` filters soft-deleted → passed; then bare DB index counted it → UNIQUE violation 500. Filter aligns DB index with app-check. **Mig 46 local catch-up:** S52 left local LocalDB stuck at Mig 45 (prod had 46 via CI, local gap). `database update` to BOTH DBs applied Mig 46 (`AddSlaFieldsToItTicket`) THEN Mig 47 — residual closed. Dev override `--connection SolutionErp_Dev`; Design factory-default `SolutionErp_Design` (both `(localdb)\MSSQLLocalDB`). Build 0 err (2 pre-existing DocxRenderer warn). Full suite 203 GREEN (58 Domain + 145 Infra, +3 new). KHÔNG touch FE/test/commit (em main commits). Tag `[s53, gotcha-57-ext, mig47, mig46-catchup, filter-byte-for-byte]`.
- **S52 P11-D Wave2 BE — ItTicket round-robin + SLA (Mig 46 `AddSlaFieldsToItTicket`, 3 AddColumn no-table) [proxy by em main: agent killed session-limit trước MEMORY step]:** Entity +SlaDueAt/SlaWarnedSent/SlaBreached. `CreateItTicketHandler`: `SlaWindow` static map (Urgent4/High8/Medium24/Low72h) **shared với `ItTicketSlaJob`** (single-source) → `e.SlaDueAt=CreatedAt+window`. Round-robin least-loaded: `db.Users.Where(DepartmentId==itDeptId && IsActive).OrderBy(db.ItTickets.Count(assigned && !Closed && !Resolved)).ThenBy(Id).First()` (itDept = `Departments.Code=="IT"`); no IT-staff → unassigned. NEW `ItTicketSlaJob:BackgroundService` mirror SlaExpiryJob (30s warmup/15min loop/scope) NHƯNG **KHÔNG auto-transition** — chỉ breach (SlaDueAt<now & !breached & open SlaBreached=true + notify assignee) + warning (≤20% window notify + SlaWarnedSent), idempotent qua guard. DI `AddHostedService<ItTicketSlaJob>` sau SlaExpiryJob. `AssignItTicketCommand` + PUT `/{id}/assign` `[Authorize(Roles=Admin)]`. DTO +SlaDueAt/SlaBreached + projection. **DbInitializer `SeedItDepartmentStaffAsync` KEY ordering: PHẢI chạy SAU `SeedDemoUsersAsync`** (method đó reconcile 2 user dept về PRO/CCM mỗi boot override về IT sau, end-state deterministic) + dept "IT" thứ 10 + gán nv.cao/nv.truong (sample user, idempotent). Build 0-err. Tag `[s52, p11-d, mig46, round-robin, sla-job, seed-ordering]`.
- **S52 P11-E+F Wave1 BE migration-FREE (4 file new + 3 edit, NO mig):** Case 1/2 deterministic ~98% em main. **P11-F** (2 LOC): `CreateItTicketHandler` set `e.MaTicket = WorkflowAppCodeGen.GenerateMaDonTuAsync(db,"IT",clock.Now.Year,clock,ct)` TRƯỚC `db.ItTickets.Add` gen lúc Create (kanban no-workflow, khác Leave/OT gen lúc Submit). Helper = `internal static` cùng ns Office, gọi trực tiếp no-using. Format `IT/2026/001`. **P11-E** report chấm công: NEW `AttendanceReportFeatures.cs` (DTO 3 record EXACT + GetAttendanceReportHandler) + NEW `IAttendanceReportExcelExporter`(App/Reports/Services) + NEW Infra `AttendanceReportExcelExporter`(ClosedXML mirror ContractExcelExporter, sync `Export(dto)` no-DB) + DI scoped + Controller +2 endpoint `[Authorize(Roles=Admin)]`. **KEY gotcha day-type:** DayOfWeek+holidaySet KHÔNG EF-translate `.ToListAsync()` rồi group/classify IN-MEMORY C#. **KEY gotcha type:** `Holiday.Date`=**DateOnly** KHÔNG DateTime (spec viết HashSet<DateTime> nhầm) → `HashSet<DateOnly>` + so `DateOnly.FromDateTime(a.AttendanceDate.Date)`. OtPolicy active fallback 1.5/2.0/3.0. Day-type prio: holiday→weekend(Sat/Sun)→weekday. OtWeighted=Σ(cấp×hệ số). FullName denorm ưu tiên `Attendance.UserFullName` rồi User.FullName. DeptName LEFT JOIN. RenderResult=(Content,FileName,ContentType) ns Forms.Services. Controller inject exporter ctor. Build 0 err (2 pre-existing DocxRenderer warn). KHÔNG touch FE/test/mig/ItTicket-khác. Routes: GET `/api/attendances/report?year&month&departmentId` + `/report/excel`. Tag `[s??, p11-e-f, wave1, no-mig, day-type-in-memory, dateonly-holiday]`.
- **S51 P11-C HMW W1 — Vehicle+Driver catalogs HrmConfigs (Mig 44 `AddVehicleAndDriverCatalogs`, 9 add-point ~12 file/edit):** Pattern 12-bis catalog-mega 4th cumulative. 2 entity (`Vehicle`/`Driver`:AuditableEntity Domain/Hrm) + 2 EF config (mirror `HolidayConfiguration` filtered, NOT buggy bare LeaveType) + 2 DbSet (IAppDbContext+ApplicationDbContext) + Mig 3-file + HrmConfigFeatures Region5/6 (DTO+List/Create/Update/Delete CQRS mirror Region1 EXACT) + Controller +2 route group (8 endpoint, GET public + POST/PUT/DELETE `[Authorize(Roles=Admin)]`) + MenuKeys +2 const +All array + DbInitializer (menu 2 leaf + SeedHrmConfigsAsync guard&seed 2 veh+2 drv). **gotcha #57 KEY:** Code UNIQUE `.HasFilter("[IsDeleted]=0")` — Mig diff verified CLEAN 2 CreateTable + 2 filtered IX no drift. Validator MaxLength = em main schema (Code50/Name200/Plate20/Phone20/LicNum50/LicClass20/Desc500), SeatCount GreaterThanOrEqualTo(0). Admin perm AUTO-grant: `SeedAdminPermissionsAsync` + `Program.cs:78` both iterate `MenuKeys.All` → +All array = 8 policy + Admin row auto (no manual grant code). HRM no HasQueryFilter → `.Where(!IsDeleted)` manual. Applied BOTH DB. Build 0 err (2 pre-existing DocxRenderer warn). RAG/Qdrant DOWN → all Read/Grep on-disk. Spec deterministic ~98% em main → ACCEPT Case 1. KHÔNG touch FE/test/commit. Tag `[s51, p11-c, mig44, vehicle-driver, catalog-mega]`.
- **S43 P11-B Wave 1 — LeaveBalance business logic (Mig 42 `AddLeaveBalances`, 7 file: 1 entity + 1 config + 2 DbSet edit + Mig 3-file + 1 hook edit + 1 Features + 1 Controller):** Case 1/3 deterministic ~98% em main spec. Pattern 12-ter-adjacent single-entity: entity `LeaveBalance:AuditableEntity` (UserId/LeaveTypeId/Year + EntitledDays/UsedDays/AdjustmentDays decimal(5,2), nav LeaveType). Config FK LeaveType WithMany() **Restrict** (catalog no cascade) + UNIQUE composite (UserId,LeaveTypeId,Year) + IX UserId. Mig diff CLEAN: 1 CreateTable + 3 IX, no drift. Applied BOTH DB (Dev `SolutionErp_Dev` + Design default). **Deduction hook:** insert in `ApproveLeaveRequestHandler` terminal else (DaDuyet branch) ONLY — UPSERT LeaveBalance, `bal.UsedDays += p.NumDays`, exactly-once guaranteed by early guard `Status != DaGuiDuyet throw`. OtRequest/Travel/Vehicle UNTOUCHED (only Leave has balance). CQRS `Application.Hrm`: DTO RemainingDays=Entitled+AdjustmentUsed COMPUTED (not stored) + GetMy/GetUser lazy-merge (load active LeaveTypes + balances → in-memory merge, synth default when no row — KHÔNG EF LEFT JOIN translate) + AdjustLeaveBalanceCommand admin upsert (HasValue-gated). **Policy resolved:** HRM admin convention = `[Authorize(Roles="Admin")]` NOT menu policy (verified HrmConfigsController write endpoints) — used on GET-by-user + PUT /adjust; /my = `[Authorize]`. Controller injects IDateTime for year default `clock.Now.Year` (thin, no DateTime.Now hardcode). HRM no HasQueryFilter → `.Where(!IsDeleted)` manual everywhere. KHÔNG touch FE/test/commit. Build 0 err (2 pre-existing DocxRenderer warn). Tag `[s43, p11-b-w1, mig42, leave-balance, single-entity]`.
- **S42 P11-A SEED — 4 sample ApprovalWorkflow V2 for WorkflowApps (DbInitializer.cs only, +4 method ~210 LOC + 4 call):** Case 1 mechanical mirror `SeedSampleProposalWorkflowV2Async` EXACT × 4 (Leave5/Ot6/Travel9/Vehicle7). Each: idempotent `AnyAsync(ApplicableType==X)` guard → resolve approver `binh.le@solutions.com.vn` (SAME user as Proposal/Contract seed, null→LogWarning+return) → 1 ApprovalWorkflow (Version=1, IsActive+IsUserSelectable=true, ActivatedAt=UtcNow) + 1 Step (Order=1, Name="Cấp duyệt", DepartmentId=CCM?.Id) + 1 Level (Order=1, ApproverUserId). Codes QT-NP/OT/CT/XE-V2-001. Wired 4 calls after SeedSampleProposalWorkflowV2Async (NOT gated DemoSeed, gotcha #51 infra seed). Enum verified Grep. Build 0 err 0 warn. Bash tool = bash NOT PowerShell despite env hint (use `cd && cmd | grep`). Spec deterministic 100% → ACCEPT Case 1. NOT touched App/Controller/FE/test/Mig. Tag `[s42, p11-a, seed, mirror-proposal-exact]`.
- **S42 P11-A Wave 2b APP — wire ApproveV2 CQRS Travel+Vehicle (`TravelVehicleApprovalFeatures.cs` ~830 LOC + 2 controller edit):** Cookie-cutter mirror Wave 2a / ProposalFeatures Region 2. 1 new file ns `Application.Office`: 2 module × (DetailDto + LevelOpinionDto + GetById JOIN Step/Level metadata + UpdateDraft + Submit + Approve UPSERT+advance + Reject TuChoi + Return TraLai+RejectedFromStatus) + 1 shared `internal static TravelVehicleCodeGen.GenerateMaDonTuAsync` (Serializable tx, `WorkflowAppCodeSequences` Prefix-keyed, prefix `DT/CT/{year}` Travel & `DX/XE/{year}` Vehicle, format `{prefix}/{seq:D3}` — D3 no year segment per spec). KEY gotcha: WorkflowAppStatus enum DIFFERS from ProposalStatus int values (DaGuiDuyet=2 not 1, TraLai=3) → mirror by SEMANTIC enum member not literal. Owner = `RequesterUserId` (not DrafterUserId). Submit verify wf.ApplicableType==Travel9/Vehicle7 else Conflict. 2 controller +6 route each (GET{id}/PUT/submit/approve/reject/return) nested body records, CreatedAtAction. KHÔNG sửa WorkflowAppsFeatures.cs/Leave/Ot/FE/test/seed. Build 0 err. Spec deterministic ~98% em main → ACCEPT Case 1/2. Tag `[s42, p11-a, wave-2b, mirror-proposal-region2]`.
- **Archived S42 P11-A Wave 2a APP (LeaveOt ApproveV2 CQRS) → git history (S65b FIFO trim):** KEY absorbed Pattern 4 + WorkflowAppCodeGen Serializable-tx Prefix-keyed `{prefix}/{year}/{seq:D3}` + WorkflowAppStatus enum DIFFERS ProposalStatus (mirror SEMANTIC member not literal) + Owner=RequesterUserId. Detail in Wave 2b entry above + S56 em-main Serializable note.
- **S41 P11-A Wave 1 SCHEMA — wire ApproveV2+LevelOpinions 4 WorkflowApps (Mig 41 `WireWorkflowAppsApprovalV2`):** Pattern 12-bis cookie-cutter mirror Proposal Mig 38, 13× cumulative. 11 file: 5 entity (4 `{Leave,Ot,Travel,Vehicle}RequestLevelOpinion` + shared `WorkflowAppCodeSequence` Prefix-PK) + 5 EF config (auto-discover ApplyConfigurationsFromAssembly, no manual register) + edit 4 parent (nav LevelOpinions + `WorkflowAppStatus? RejectedFromStatus`) + edit enum (`TravelRequest=9`) + 2 DbSet (IAppDbContext+ApplicationDbContext, 5 each). Mig diff CLEAN: 5 CreateTable + 4 AddColumn (no drift). FK Cascade parent + Restrict Level + UNIQUE composite ({Parent}Id, ApprovalWorkflowLevelId). Applied BOTH DB (Dev + Design). Build 0 err. Wave 2 (App/Controller) + Wave 4 (test) deferred per spec. Spec deterministic 100% (em main chose schema) → ACCEPT Case 1. Tag `[s41, p11-a, 12-bis-13x, mig41]`.
- **Archived S35 G-H2 BE-CRUD-4-catalog + S29 Plan-B-Chunk-C Contract-V2-mirror → `archive/2026-05-q4.md` (S65 FIFO trim):** KEY absorbed Pattern 12-bis foundation above (catalog-mega + HRM `.Where(!IsDeleted)` manual + Validator MaxLength=EF-source).
- **Archived FE/test + older BE entries → `archive/2026-05-q4.md` + git d2f52ba (S40 curate):** S35 FE inline forms 5 satellite (→ frontend domain) · S34 test bundle +10 [Fact] 130 PASS (→ test-specialist domain) · S33 Task 5 EmployeesListPage · S32 wrap/startup. KEY absorbed in Patterns above + split pointers.
<!-- FIFO'd from MEMORY.md at S112 (2026-07-12): Recent-activity S77 → S56, dates 2026-06-10..19 -->
- **2026-06-19 (S77 PE +2 ghi-chú giá đề xuất BE — Mig 57 `AddPeSuggestedPriceNotes` 3-file, 6 edit/0 new file, COOKIE-CUTTER S73 suggested-price + S74 CcmNote, em-main CONTRACT-locked names 100% → ACCEPT Case 1):** PRO dải Min/Max + CCM 1 giá (S73) +ô GIẢI THÍCH vì-sao min vs max (anh Kiệt FDC + Tra Sol). 2 cột nullable, no table/index/backfill. (1) `PurchaseEvaluation.cs` +`string? ProSuggestedPriceNote`/`CcmSuggestedPriceNote` ngay sau CcmSuggestedPrice. (2) `PurchaseEvaluationConfiguration.cs` +`HasMaxLength(1000)` ×2 sau ApprovedPriceSource (KHÔNG index — free-text). (3) Mig: Up=2 AddColumn nvarchar(1000) nullable, Down=2 DropColumn, snapshot ×2 verified — clean mirror Mig 55. (4a) `UpdatePeSuggestedPriceProCommand` +trailing `string? Note=null` + validator `MaximumLength(1000)` MATCH-EF (S35) + handler **absolute-set** `pe.ProSuggestedPriceNote=request.Note` (null=clear, gia-đình text-field) + changelog part khi đổi. (4b) `UpdatePeSuggestedPriceCcmCommand` mirror + changelog suffix "(kèm ghi chú)". Role-gate KHÔNG đụng (Note gated y giá: Forbidden fail-closed TRƯỚC side-effect đã có). (5) Controller `SuggestedPriceProBody`/`CcmBody` +`string? Note` + pass `body.Note` vào cmd (4th/3rd positional). (6) `PurchaseEvaluationDetailBundleDto` +`ProSuggestedPriceNote`/`CcmSuggestedPriceNote` sau CcmSuggestedPrice + projection PEFeatures.cs positional-insert ĐÚNG order (DTO positional → order khít projection BẮT BUỘC). **Call-site grep verify (S65b lesson):** `new UpdatePeSuggestedPrice(Pro|Ccm)Command\(` repo-wide → 2 controller (đã sửa) + 18 test named-arg/positional-dừng-trước-Note → trailing-optional fully backward-compat, 0 test edit. FE 0 ref (đúng — implementer-frontend next). Build SolutionErp.slnx (2 test project, gotcha #65) **0 warn 0 err**. KHÔNG apply DB/FE/test/commit. Route: `note` camelCase qua PUT `/suggested-price/{pro,ccm}` body + GET detail bundle. Tag `[s77, pe-suggested-price-note, mig57, two-column-no-table, absolute-set, trailing-optional-callsite-safe, positional-dto-order]`.
- **2026-06-17 (S? Off_Dashboard menu leaf BE — NO migration, 3 edit/2 file, idempotent seed mirror S53/S54-TaskD, em-main spec deterministic 100% → ACCEPT Case 1):** +1 menu key `Off_Dashboard` ("Bảng điều khiển Văn phòng số"), pattern = S53 Off_AttendanceReport EXACT. 3 insert: (1) `MenuKeys.cs` const `OffDashboard = "Off_Dashboard"` ngay sau root `Off:99` · (2) `MenuKeys.cs` All[] line `Off, OffDanhBa``Off, OffDashboard, OffDanhBa` · (3) `DbInitializer.cs` SeedMenuTreeAsync tuple `(OffDashboard, "Bảng điều khiển Văn phòng số", Off, **0**, "LayoutDashboard")` trước OffDanhBa=1 (Order 0 = landing đầu nhóm, KHÔNG renumber children 1-7 hiện có). **KEY recon — Off_* leaves ARE IN All[] (NOT factory-excluded):** task hint "leaf may be excluded+granted-via-factory" KHÔNG áp Off (chỉ Pe_* leaf sinh động). Off_AttendanceReport :160 in All → tôi follow SAME = +All. Admin auto 2-point verified: `SeedAdminPermissionsAsync:2001` + `Program.cs:78` both iterate `MenuKeys.All` → +All = 4 policy {Read/Create/Update/Delete} + Admin Permission row auto, NO manual grant. **Revoke verified KHÔNG sửa:** `RevokeTemporarilyHiddenModulesAsync:2170` `p.MenuKey.StartsWith("Off")` → Off_Dashboard tự nằm trong scope ẩn-non-Admin. `InReviewScope:2070` chỉ match Catalog*/Master-keys/Pe_* → Off_Dashboard KHÔNG re-grant non-admin. Idempotent: upsert loop :1909 `existingItems.TryGetValue(key)` miss→Add / hit→chỉ reconcile Order (prod DB cũ nhận leaf next boot, re-run no-op). Build SolutionErp.slnx (gồm 2 test project, gotcha #65) **0 warn 0 err**. KHÔNG touch FE (menuKeys.ts/Layout=implementer-frontend)/test/mig/commit. Tag `[s?, off-dashboard, menu-leaf, no-mig, admin-perm-via-all, order-0-landing]`.
- **2026-06-16 (S65b PE +HoSoLink BE — Mig 51 `AddHoSoLinkToPurchaseEvaluation` 3-file, 6 file edit + 0 new file, em-main CHỐT spec 100% → ACCEPT Case 1):** Phiếu PE +1 cột `HoSoLink` = 1 hyperlink tới thư mục hồ sơ NAS (anh Kiệt paste link, FE render bấm-mở). KHÔNG entity con/bảng mới — 1 cột nullable. (1) `PurchaseEvaluation.cs` +`string? HoSoLink` sau MoTa. (2) `PurchaseEvaluationConfiguration.cs` +`HasMaxLength(1000)` (KHÔNG index — hyperlink free-text, không filter/join). (3) Mig via `dotnet ef migrations add` → Up=1 `AddColumn<string> nvarchar(1000) maxLength:1000 nullable` NO table NO index, Down=1 DropColumn; snapshot HoSoLink nvarchar(1000) verified. (4a) `CreatePurchaseEvaluationCommand` +trailing `string? HoSoLink = null` (sau WorkItemId — optional-param-after-required rule) + validator `MaximumLength(1000)` MATCH EF (S35 lesson) + handler `HoSoLink = request.HoSoLink`. (4b) `UpdatePurchaseEvaluationDraftCommand` +trailing `string? HoSoLink = null` + validator 1000 + handler **absolute-set** `entity.HoSoLink = request.HoSoLink` (Section-1 text-field family MoTa/DiaDiem pattern → null=clear, KHÔNG null-safe-keep như WorkItemId picker; deliberate: hyperlink user cần clear được). (5) `PurchaseEvaluationDetailBundleDto` +`string? HoSoLink` sau MoTa + projection `e.HoSoLink` positional-insert đúng vị trí. **RANG-CUNG grep verify (bài học CreateDepartmentCommand CS7036):** `Grep CreatePurchaseEvaluationCommand|UpdatePurchaseEvaluationDraftCommand` repo-wide gồm tests → 0 manual `new ...Command(...)` call-site (controller bind `[FromBody]` model-binding, KHÔNG manual ctor; tests dùng NAMED-ARG dừng ở WorkItemId) → trailing-optional-default fully backward-compat, KHÔNG sửa call-site nào. List DTO KHÔNG đụng (spec chỉ Detail/Get). `.slnx` KHÔNG update (chỉ 2 mig-file trong project có sẵn). KHÔNG apply DB/FE/test/commit. Build SolutionErp.slnx (gồm 2 test project) **0 warn 0 err** (DocxRenderer warn cleared). Route: `hoSoLink` camelCase qua POST/PUT body + GET detail. Tag `[s65b, pe-hosolink, mig51, one-column-no-table, trailing-optional-param, named-arg-callsite-safe]`.
- **2026-06-16 (S65 Department hierarchy BE — Mig `AddDepartmentParentId` 3-file, 4 file edit + 0 new file, em-main schema CHỐT → ACCEPT Case 1):** Cây tổ chức nền trang Hồ sơ Nhân sự. (1) `Department.cs` +`Guid? ParentId` loose-Guid KHÔNG physical FK (convention PE.ProjectId/WorkItemId/SelectedSupplierId). (2) `DepartmentConfiguration.cs` +`HasIndex(x=>x.ParentId)` only — **KHÔNG `HasOne` self-FK** (em main chốt loose). (3) `DepartmentFeatures.cs` +`GetDepartmentTreeQuery`+`DepartmentTreeNodeDto`+Handler (append existing file, NO new .cs → `.slnx` KHÔNG cần update; slnx lists projects-not-files). (4) `DepartmentsController.cs` +`[HttpGet("tree")]`. **KEY recon finding (spec asked verify):** `EmployeeProfile` has **NO `DepartmentId`** — links via `UserId`; org-chart dept field nằm trên **`User.DepartmentId`** (Mig 11) → GROUP BY `db.Users.Where(DepartmentId!=null && IsActive).GroupBy(DepartmentId).ToDictionary` = DirectEmployeeCount (recon NOT schema-decision). Tree ráp in-mem: roots=ParentId-null OR orphan-parent (safe-root); TotalEmployeeCount=Direct+Σ(Children.Total) đệ quy rollup via `record with{}`; **cycle-guard HashSet<Guid> visited** (node đã thăm→return null cắt vòng); Children sort `OrderBy(Code, StringComparer.Ordinal)` ổn định. **Authz copy-từ-đâu:** `[HttpGet]` List = CHỈ class-level `[Authorize]` (no per-action attr) → `/tree` cũng vậy (verified read controller). Mig diff CLEAN: AddColumn ParentId nullable + CreateIndex IX_Departments_ParentId, NO new table, Down DropIndex→DropColumn (SQL 5074 order). KHÔNG apply (prod/CI). Build SolutionErp.slnx 0/0. KHÔNG touch FE/test/seed-parent/commit (em main). Route `GET /api/departments/tree``List<DepartmentTreeNodeDto>`. Tag `[s65, dept-hierarchy, loose-guid-no-fk, in-mem-tree-rollup, cycle-guard, user-departmentid-source]`.
- **2026-06-11 (S57bis PE WorkItemId BE slice — PARTIAL, on-behalf em main ghi hộ, H2-proposed):** Return-truncated #53 TRƯỚC Mig 49 + projection-3 → em main solo hoàn tất (fix CS7036 + CS8019, Mig 49 `AddWorkItemToPurchaseEvaluation` 3-file, projection ListItemDto ×3 LEFT-join WorkItems, UpdateDraft null-safe `if (request.WorkItemId is not null)` chống null-hóa bug-class S42). LEARNED: FK-guard loose-Guid `AnyAsync(w.Id==x && w.IsActive)`→Conflict (mirror S43); validator `NotEmpty` create-only, DB nullable backward-compat 4 phiếu cũ; `NotEmpty()` trên `Guid?` KHÔNG chặn `Guid.Empty` → handler guard bắt (defense-in-depth). SURPRISE: truncate 2 session liên tiếp (S55, S57bis) ở slice lớn cross-layer → cắt stage nhỏ hơn (entity+config / mig / projection tách spawn). Tag `[s57bis, truncated-53, on-behalf]`.
- **2026-06-10 (S57-resume spawn-test H4.8 — Harness-4 two-tier):** Mình bị DEMOTE pin `model: claude-opus-4-8` (deterministic-scaffold class, double-gate reviewer+test+cicd sau lưng). Spawn-test echo model NGAY sau edit → self-report `claude-fable-5[1m]` = SE env (CCD harness) KHÔNG fresh-read frontmatter → pin ăn SAU restart CLI. Post-restart mình chạy Opus 4.8 (effort Max giữ env-wide); task hệ-trọng giao mình qua hmw có thể override `tier:'fable'`. Tag [h4-demote, spawn-test, pending-restart].
_(S56 GOLIVE-HARDEN 3 BE fix — ExecuteUpdate-atomic LeaveBalance + fail-closed AssignItTicket authz + DocxRenderer CS8602 → FIFO'd S77; verbatim git/archive. em-main post-review bumped tx → `IsolationLevel.Serializable` per database-agent. Test 228 green.)_
<!-- FIFO'd from MEMORY.md at S116 (2026-07-13): Recent-activity 2026-06-25..19 (S88 · PE Section-B Mig 59 · PE chuông) -->
- **2026-06-25 (S88 PE stability cleanup — FROZEN-module mechanical relocation + 1 stale-comment fix BE — NO migration, 3 edit/2 file, em-main spec deterministic 100% file/line/boundary given → ACCEPT Case 1):** Pure mechanical move ZERO behavior-change. **T1 misplaced-command relocate:** `SetPeCcmBudgetPeriodCommand`+Validator+Handler (BUDGET setter writes `pe.CcmBudgetPeriodAmount` Section B "NS kỳ này" CCM, Mig 59) sống NHẦM trong suggested-PRICE file → move về budget file. (a) REMOVE `PeSuggestedPriceFeatures.cs` lines 144-199 (blank + `===== CCM NS kỳ này =====` header → `SetPeCcmBudgetPeriodCommandHandler` closing `}`); file end clean tại line 143 (`UpdatePeSuggestedPriceCcmCommandHandler` `}`), suggested-price PRO/CCM trên giữ nguyên. (b) APPEND `PeWorkItemBudgetFeatures.cs` END sau line 200 (`UpdatePeBudgetCcmCommandHandler` `}`) → cạnh budget setters `UpdatePeBudgetPro/Ccm`. Code BYTE-IDENTICAL (logic/authz Admin\|CostControl fail-closed/validation GTE(0).When(HasValue)/absolute-set null=clear/changelog UNCHANGED); CHỈ prepend 1 comment line `// [S88] Chuyển từ PeSuggestedPriceFeatures.cs về đây (đúng nhóm budget setter).`. Usings 8-header IDENTICAL 2 file → KHÔNG add duplicate. Namespace `SolutionErp.Application.PurchaseEvaluations` SAME → MediatR type-resolution transparent, controller `:125` KHÔNG đụng. **T2 stale-comment fix:** `Dtos/PurchaseEvaluationDtos.cs` ~250-254 comment trên `decimal WinnerQuoteTotal` ghi SUM "của SelectedSupplierId" (OLD single-winner) → verified actual compute `PurchaseEvaluationFeatures.cs:1093-1099` = `Suppliers.Where(s=>s.IsWinner)` (multi, Mig 58) → sửa CHỈ comment-text "MỌI đơn vị IsWinner (multi-winner Mig 58)". Field/code UNCHANGED. **LESSON:** frozen-relocation = read BOTH file FIRST exact boundary + verify usings-superset target + verify stale-comment claim vs real compute-site TRƯỚC sửa (KHÔNG trust comment blind, mirror S35 EF-source-of-truth). Build `SolutionErp.Application.csproj -v q` **0 warn 0 err** (4s). KHÔNG test (test-specialist)/FE/mig/commit. Tag `[s88, frozen-relocation, byte-identical-move, mediatr-type-resolve-transparent, stale-comment-verify-compute-site, no-behavior-change]`.
- **2026-06-24 (PE Section B 3-cột THỰC HIỆN — CCM "NS kỳ này" cột riêng BE — Mig 59 `AddCcmBudgetPeriodToPurchaseEvaluation` 3-file, 5 edit/0 new file, em-main CHỐT spec+API-contract 100% → ACCEPT Case 1):** Section B tách 3 cột Dự án|PRO|CCM giống Section A. PRO=`BudgetPeriodAmount` sẵn có; CCM nhập cột riêng độc-lập. COOKIE-CUTTER mirror S77 suggested-price-Ccm + S65b HoSoLink one-column. (1) `PurchaseEvaluation.cs` +`decimal? CcmBudgetPeriodAmount` sau ExpectedRemainingAmount. (2) `PurchaseEvaluationConfiguration.cs` +`HasColumnType("decimal(18,2)")` sau ExpectedRemaining (spec asked literal decimal(18,2); siblings dùng HasPrecision(18,2) NHƯNG emit identical column — tôi follow spec verbatim HasColumnType). (3) Mig: Up=1 AddColumn decimal(18,2) nullable NO table/index, Down=1 DropColumn, snapshot verified — clean mirror Mig 52. (4) DTO `PurchaseEvaluationDetailBundleDto` +`decimal? CcmBudgetPeriodAmount` sau ExpectedRemainingAmount TRƯỚC BudgetSummary + projection PEFeatures.cs:1110 positional-insert đúng order (positional record → projection order BẮT BUỘC khít). (5) Setter `SetPeCcmBudgetPeriodCommand(Guid Id, decimal? CcmBudgetPeriodAmount):IRequest` append `PeSuggestedPriceFeatures.cs` (same concern PE role-gate setters) + Validator `GreaterThanOrEqualTo(0).When(HasValue)` (budget=0 hợp lệ) + Handler fail-closed Forbidden Admin|CostControl TRƯỚC side-effect (S56 #5) + absolute-set null=clear (gotcha #73) + changelog Header (sibling-consistency; spec min=SaveChanges). (6) Controller `[HttpPatch("{id:guid}/budget/ccm-period")]` + `CcmBudgetPeriodBody` (spec EXPLICIT PATCH dù siblings PUT — follow spec). **Call-site verify:** `new PurchaseEvaluationDetailBundleDto(` repo-wide → EXACTLY 1 (PEFeatures:1100, đã sửa) → positional-safe. Submit-guard `BudgetPeriodAmount` PRO KHÔNG đụng. Build SolutionErp.slnx (6 proj incl 2 test, gotcha #65) **0 warn 0 err**. KHÔNG apply DB/FE/test/commit. Route FE/test mirror: DTO field `ccmBudgetPeriodAmount` camelCase + PATCH `/api/purchase-evaluations/{id}/budget/ccm-period` body `{ ccmBudgetPeriodAmount }`. Tag `[pe-section-b-3col, ccm-budget-period, mig59, one-column-no-table, absolute-set-fail-closed, positional-dto-order, patch-per-spec]`.
- **2026-06-19 (PE chuông báo approver BE — NO migration, 1 edit/1 file, em-main spec deterministic 100% → ACCEPT Case 1):** Tra Sol (Zalo) "không thấy chuông — việc có hồ sơ cần duyệt": approver Cấp hiện tại KHÔNG nhận notify khi phiếu ENTER/ADVANCE tới ChoDuyet (chỉ drafter báo terminal). FIX = +1 block trong `LogTransitionAsync` (PurchaseEvaluationWorkflowService.cs ~line 1058) NGAY SAU drafter-notify, KHÔNG endpoint mới (Pattern 4 — notify = side-effect của Submit/Approve-advance, cả 2 đã gọi LogTransitionAsync + end ChoDuyet). **Resolution mirror EXACT canonical** `EnsureActorInLevel` (line ~301): `db.ApprovalWorkflows.AsNoTracking().Include(Steps).ThenInclude(Levels)``stepsOrdered[CurrentWorkflowStepIndex]` (bounds-check) → `.Levels.Where(Order==CurrentApprovalLevelOrder && ApproverUserId!=Guid.Empty && !=actorUserId).Select(ApproverUserId).Distinct()``NotifyManyAsync(ids, Generic, "Phiếu cần bạn duyệt: {MaPhieu??TenGoiThau}", "Có phiếu Duyệt NCC đang chờ bạn duyệt.", /purchase-evaluations/{Id}, refId:Id, ct)`. **Recon:** (1) `ApprovalWorkflowLevel.ApproverUserId` = non-null `Guid` (NOT Guid?) → "exclude null" = `!=Guid.Empty` defensive (OR-of-N rows mỗi 1 NV). (2) `NotifyManyAsync` sig verified = `(IEnumerable<Guid>, NotificationType, title, string? desc, string? href, Guid? refId, CT)` khớp urgent-feature ref. (3) actor-exclude qua `!= actorUserId` (Guid != Guid? lift OK). **Guard V2-only:** `ApprovalWorkflowId is Guid && CurrentWorkflowStepIndex is int && CurrentApprovalLevelOrder is int` → V1/no-workflow skip. **Best-effort try/catch** nuốt lỗi (phiếu đã SaveChanges; notify fail KHÔNG rollback). Block đọc pointer SAU set → Submit (idx0/lvl1) + Approve-advance (pointer advance rồi LogTransition) đều fire đúng. Build SolutionErp.slnx (2 test project, gotcha #65) **0 warn 0 err**. KHÔNG test/FE/mig/commit. Tag `[pe-approver-bell, notify-many, log-transition-hook, no-mig, v2-only, best-effort-trycatch, pattern4-side-effect]`.