[CLAUDE] App: golive harden — LeaveBalance concurrency + ItTicket authz-order + DocxRenderer + Travel/Vehicle tests
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m19s

Pre-golive verification (S56) surfaced 4 issues; all fixed + verified (228 test pass, 0 build warning).

#3 LeaveBalance lost-update (DB11 concurrency): terminal-approve deduction was an in-memory read-modify-write (UsedDays += NumDays) under a bare SaveChanges, so two concurrent terminal approvals of the same (user,type,year) lost an update. Fix: atomic server-side ExecuteUpdateAsync (UsedDays = UsedDays + n) inside an explicit Serializable transaction (matches the codegen/Proposal/TravelVehicle convention; serializes the auto-create-row race too). Exactly-once guard (Status != DaGuiDuyet) intact. No migration.

#5 ItTicket reassign existence-oracle: AssignItTicketHandler checked ticket-NotFound before the Admin-OR-dept-IT Forbidden guard. Reordered so authorization runs first -> fail-closed (a non-IT/non-admin caller gets Forbidden for any ticketId, existent or not).

#6 DocxRenderer CS8602: null-guard MainDocumentPart + Document with clear exceptions (cleared 2 build warnings -> 0).

#4 Travel/Vehicle ApproveV2: added smoke tests (Submit->Approve terminal + outsider-Forbidden) — previously zero coverage.

Tests 216 -> 228 (+12). database-agent DB-layer review PASS; em-main cross-stack review clean (reviewer workflow stage did not emit StructuredOutput -> em-main covered the cross-stack review by reading every diff). Bundles agent-memory harvest (S56).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-09 17:51:38 +07:00
parent bef582594e
commit a20cde89fb
13 changed files with 555 additions and 19 deletions

View File

@ -70,6 +70,10 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 Recent activity (FIFO — older → archive/git)
- **2026-06-09 (S56 pre-golive verify — 4 logic streams, all PASS):** Audited P11-B/D/E/F + ApproveV2 + catalogs + S55 master-wiring. **LeaveBalance** deduction exactly-once (terminal DaDuyet, guard `Status!=DaGuiDuyet` :296 blocks re-approve), FK guard Create+UpdateDraft→Conflict; **AttendanceReport** classify day-type IN-MEMORY (Holiday DateOnly HashSet), OtPolicy multiplier; **MaTicket** gen-on-Create Serializable IT/2026/NNN. Tests cover (LeaveBalance 9 + AttReport 2 + codegen 3 = 29 green). ApproveV2 4-module flatten Steps→Levels correct; **Travel/Vehicle ApproveV2 = 0 test** (cookie-cutter of tested Leave/OT — add 2 smoke post-golive). master-data **idempotency PROVEN** (DbInitializer re-run → counts identical, per-code guard :2310/:2404/:2422). **⚠️ PROD FACT (corrects stale S52 mem):** dept "IT"/Phòng CNTT DOES exist (Id 65CC6307…) but has **0 active users** on prod → ItTicket auto-assign no-ops, reassign dropdown empty, SLA job no notify-target. Pre-golive ops fix: assign ≥1 real user to dept IT (1 UPDATE, no code). Tag [s56, pre-golive-verify, logic-pass, dept-IT-empty-prod, travel-vehicle-untested].
- **2026-06-09 (S56 Phase 2 FE-redesign RECON — 25 page audit, on-disk):** ⭐ **NOT a rewrite** — S55 already redesigned ui-primitives + DataTable + shell → any page importing `ui/{Button,Input}`+`DataTable` AUTO-inherits density. **Hover-hidden quick-win nearly absent:** repo-wide grep `opacity-0`×`group-hover` = **only 1 real site** `ContractCreatePage.tsx:196` (EXCLUDED scope) + DataTable.tsx:15 is a comment forbidding it (good). In-scope = **0 hover-hidden fixes**. **DataTable adoption split:** only 5/25 use `<DataTable>` (Suppliers/Projects/Departments/Users/Forms); 12 pages roll RAW `<table>` (MeetingRooms/Catalogs/HrmConfigs/EmployeesList/Proposals/WorkflowApps/Attendance×2/MenuVisibility/Roles) → custom density pass needed. **Drawer ≥8-field candidates = 3:** Suppliers (9 fld, Dialog), Projects (10 fld, Dialog), Users-CREATE (~8 fld w/ roles multiselect, 4 Dialogs total but only create is big). All currently big-Dialog → convert. **NO `Drawer.tsx` exists** (`ui/` = Button/Dialog/Input/Label/Select/Textarea only) → build first. **Bậc-thang reference ALREADY EXISTS:** `EmployeesListPage.tsx` (1200L) = canonical inline add/edit-row for 5 satellites (`setEditing{X}Id` + `addingX` mutex, :256-356) → extract `InlineEditRow` pattern from here, reuse for Catalogs/HrmConfigs/MeetingRooms (these 3 currently edit via Dialog :251/:316/:232, ≤7 cols → bậc-thang candidates). **Modal-detail = NONE:** ProposalDetail:275 + WorkflowAppDetail:424 Dialogs are tiny action-confirm (just `<Textarea>` ý kiến), detail body already inline 2-col grid (:181) → NOT convert. **fe-user mirrors** office/master/hrm (SHA256-identical per comments) but NO system/forms/reports mirror. **Custom-layout heavy:** InternalDirectory (card-grid :124 not table), MeetingCalendar (693L FullCalendar), EmployeesList (2-panel), HrmConfigs (declarative KIND_CONFIG :45). Effort: Master 3×Drawer=M, Catalogs/HrmConfigs/MeetingRooms bậc-thang=M, Users Drawer=M, rest S (auto-inherit polish). Surprise: hover-hidden NAMGROUP-win essentially pre-solved (team already avoids opacity-0 pattern, DataTable comment enforces) → quick-win section nearly empty. Tag `[fe-redesign-p2, recon, drawer-3, basc-thang-ref-exists, s56]`.
- **2026-06-09 (S55 master-data Excel-import recon — 3 master + seed mechanism, on-disk):** ⭐ **"Hạng mục"/WorkItem master TỒN TẠI** — `Domain/Master/Catalogs/WorkItem.cs:6-14` (Code(50)UNIQUE-filtered/Name(200)/Category(100,idx)/DefaultUnit(50)/Description/IsActive), config `CatalogsConfiguration.cs:60-74`, full CRUD `CatalogsFeatures.cs:260-324` → group(VẬT TƯ/THẦU PHỤ/MEP)→Category, "1 Mat"→Code, item→Name. KHÔNG cần table/migration mới. **PE detail = pure free-text** (`PurchaseEvaluationDetail.cs` GroupCode/GroupName/ItemCode/NoiDung strings, NO FK→WorkItem) → load WorkItems non-breaking. **Project** (`Project.cs:5-14`, cfg `:14-21`): Code(50,UNIQUE `[IsDeleted]=0` Mig47)+Name(200) REQUIRED, StartDate/EndDate/BudgetTotal(18,2)/Note(1000)/ManagerUserId optional. ❌ **THIẾU Year/Investor/Location/Package** — chỉ Note free-text catch-all. Create cmd `ProjectFeatures.cs:67` dup-check `:87 AnyAsync(Code==)`. **Supplier** (`Supplier.cs:5-16`, cfg `:14-27`): Code/Name req + Type enum + TaxCode(20)/Phone/Email/Address/ContactPerson/Note. `SupplierType.cs`: NhaCungCap=1/NhaThauPhu=2/ToDoi=3/DonViDichVu=4/ChuDauTu=5. ❌ **THIẾU Status/TinhTrang (KHÔNG có field/enum nào)** + bank-acct + legal-rep (≠ContactPerson) + quality-score; "Cả hai" PHÂN LOẠI unmappable (Type single-valued). Create `CreateSupplierCommand.cs:10` dup `:45`. **Seed = idempotent `existingCodes.Contains→skip`** (`DbInitializer.SeedDemoMasterDataAsync:2149`, today 18 supplier `:2155` + 8 project `:2222`; WorkItems 15 rows tuple-loop `SeedCatalogsAsync:576-599`). **NO bulk import** — Master chỉ single CRUD; Import/Upload hits = Forms/PE/Employees attachment only; POST one-at-a-time. **Seed→prod:** `DbInitializer.InitializeAsync` chạy MỌI startup (`Program.cs:197` unless `--no-db-init`) → `MigrateAsync` THEN seed; demo gated `config.GetValue<bool>("DemoSeed:Disabled")` (`:80`) NHƯNG SeedDemoMasterData+SeedCatalogs chạy BẤT KỂ flag (ngoài if-block :108/:115) → seed method mới auto-reach prod next deploy. Rec: idempotent DbInitializer mirror (NOT API loop). Surprise: real+demo data sẽ trộn chung Suppliers/Projects/WorkItems (18/8/15 demo rows) → cân nhắc gate demo off prod. Tag `[master-import, workitem-exists, seed-idempotent, s55]`.
- **2026-06-08 (S52 Phase 11-D/E/F product-close recon — 6 gap, on-disk):** ⭐ **GAP1 IT-pool KHÔNG TỒN TẠI:** AppRoles.All=13 role (`AppRoles.cs:23`) NO "IT"; 9 dept seed (`DbInitializer.cs:2066` PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) NO dept IT; MenuKeys NO It_* group (chỉ `OffItTicket="Off_ItTicket"` 1 leaf :123). → round-robin pool PHẢI tạo signal mới: option (a) +AppRoles.ItStaff const + seed user, (b) +dept "IT" code, (c) per-user flag `User.IsItStaff`. Least-loaded query = `Users.Where(pool).OrderBy(u => Tickets.Count(AssignedToUserId==u.Id && Status!=Closed))``ItTicket.AssignedToUserId Guid?` SẴN (`ItTicket.cs:21`). **GAP2 HostedService:** đăng ký tại `Infrastructure/DependencyInjection.cs:46 AddHostedService<SlaExpiryJob>()` (KHÔNG Program.cs — grep Program.cs rỗng). Pattern `SlaExpiryJob.cs`: `BackgroundService` + ctor `(IServiceProvider sp, ILogger)` + `ExecuteAsync` Task.Delay(30s warmup)+while loop Interval 15min → `_sp.CreateAsyncScope()` resolve scoped `IApplicationDbContext`+`IDateTime`+`INotificationService` (:61-65). ItTicketSlaJob mirror: thêm dòng :47 + clone file. **GAP3 OtPolicy (`Hrm/OtPolicy.cs`):** 3 multiplier decimal(4,2) `MultiplierWeekday/Weekend/Holiday` (:21-23, seed 1.5/2.0/3.0) + 3 cap int `MaxHoursPer Day/Month/Year` (:26-28) + `Code` UNIQUE + `IsActive` (1 default công ty). `Attendance.OtHours decimal?` (`Office/Attendance.cs:37`) per-row, KHÔNG link OtPolicyId → join thủ công qua IsActive=true; công thức OT-pay = `OtHours × multiplier(dayType) × hourlyRate`, dayType phân loại từ AttendanceDate (Holiday tra Hrm_Holiday, Sat/Sun=Weekend, else Weekday). **GAP4 Excel reuse:** `IContractExcelExporter.ExportAsync→RenderResult` record `(byte[] Content, string FileName, string ContentType)` (`IFormRenderer.cs:3`); impl `ContractExcelExporter.cs` ClosedXML `XLWorkbook`+`Worksheets.Add`+`MemoryStream→ToArray()` (:103-109 content-type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`); DI scoped :40; controller stream `return File(result.Content, result.ContentType, result.FileName)` (`ReportsController.cs:35`, mirror Forms/PE/Contracts). AttendanceExporter = clone + đổi columns + new CQRS command (mẫu `ExportContractsToExcelCommand`). **GAP5 Attendance API:** `AttendancesController.cs` 3 endpoint check-in/check-out/me (`[Authorize]` ko role); CQRS inline `Office/WorkflowAppsFeatures.cs` REGION 6 (:401-490) — `CheckInCommand`/`CheckOutCommand`/`GetMyAttendanceQuery(Year,Month)` chỉ trả LIST cá nhân (1 user/tháng). ❌ CHƯA có aggregate/monthly-report/all-users query → P11-E phải +`GetAttendanceReportQuery(year,month,deptId?)`. ItTicket CQRS cũng inline cùng file (:354 GetItTicketsQuery + CreateItTicketCommand + UpdateItTicketStatusCommand, controller `ItTicketsController.cs`). **GAP6 FE state:** ItTicketsPage + MyAttendancePage TỒN TẠI cả 2 app (fe-admin+fe-user, comment "MIRROR SHA256 identical"), routes `/it-tickets`+`/attendance` (`App.tsx:101-102`), menuKeys `OffItTicket`+`OffChamCong` (:65-66), Layout map :84-85. ❌ THIẾU: ItTicket = SKELETON read-only kanban (banner :32-34 "Form tạo + auto-assign + SLA timer defer Phase 11"), NO create form/assign-UI/SLA-badge; Attendance = check-in/out OK nhưng NO admin report page / Excel export button / OT-pay column. NO menuKey `Attendance_Report`/`It_Assign`. Surprise: ItTicket+Attendance KHÔNG dùng Workflow V2 (kanban status flow, comment `ItTicket.cs:6`) — khác Leave/OT/Travel/Vehicle (LevelOpinion). Tag `[p11-def-recon, it-pool-absent, otpolicy-multiplier, excel-reuse, s52]`.