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

32 KiB
Raw Blame History

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 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.

  • 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, OffDanhBaOff, 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 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/treeList<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.)

  • 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].