[CLAUDE] Docs: S87 closeout — PE multi-winner + budget=0 + cấp-KẾT-THÚC (Mig 58) + Section B 3-cột (Mig 59)

STATUS + HANDOFF S87 narrative + count flush (Migrations 57->59, test 377->402,
gotcha 73 unchanged) + session log 2026-06-24-S87 + agent-memory harvest
(cicd-monitor #344 / implementer-backend / investigator-codebase / reviewer /
test-specialist — spawn-record on-disk).

State THẬT: Mig 59 · 88 bảng (cicd flag 89 drift, re-ground monthly) · 402 test
(45D+357I) · gotcha 73 · menu 54 · bundle admin CXCKlNay/user Di40ewrF
(Run #344; #345 cicd-verifying Section B). Docs-only -> CI skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-24 16:14:54 +07:00
parent 9b1ee0c3a5
commit 121f5cc128
8 changed files with 50 additions and 9 deletions

View File

@ -32,7 +32,7 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
- **Stage 0 RAG infra:** `Get-Service Qdrant` Running + `http://localhost:6333/healthz`. Collection `proj_solution_erp`.
- **Stage 1 Push+filter:** `git log -1 --format='%H %s'` + `git log origin/main..HEAD` empty + diff vs paths-ignore (docs-only SKIPPED-DOCS return).
- **Stage 2 Gitea poll** (max 10 iter × 60s): API `.../actions/tasks?limit=5` (NOT `/runs` 404). Match `head_sha` (NOT run_number cancelled runs shift numbering). `updated_at` stale ~2min (gotcha #46) cross-check VPS mtime. Foreground-sleep BLOCKED Windows Monitor-style busy-wait-curl-spin ~30s/iter.
- **Stage 3 Test gate:** baseline **354 PASS** (45 Domain + 309 Infra). CI runs both proj BEFORE build/deploy status=success test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK.
- **Stage 3 Test gate:** baseline **402 PASS** (45 Domain + 357 Infra; S88 #345 395 + 7 new `PeCcmBudgetPeriodSetterAuthzTests`; prior S87 #344 395, S86 377). CI runs both proj BEFORE build/deploy status=success test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK.
- **Stage 4 Post-deploy** (if SUCCESS): auth login bearer (admin + nv.test gotcha #44; token=`accessToken` route `/api/auth/login`) 3-5 endpoint smoke 2XX (incl new NEW-endpoint bodyless-POST returns **411 IIS Length-Required pre-auth NOT 404**; re-probe WITH body 401 confirms wired) FE bundle hash 2 app (per #69 = real-size-vs-fake not hash-delta) SignalR negotiate (gotcha #25) EF mig prod==repo.
- **Stage 4.6 (S29 CRITICAL):** sqlcmd seed sample verify post-deploy (NOT chỉ schema). Seed=runtime-row-insert (no history/tables advance) verify via DB-query (e.g. Permissions count), NOT bundle-frozen-alone.
- Discovery: ASP.NET 10 record enum needs numeric input unless `JsonStringEnumConverter` (SOL has NO converter FE sends numeric). sqlcmd ssh Windows-auth `\\\\SQLEXPRESS` 4-backslash. INFRASTRUCTURE seed MUST run (NOT inside `if(!demoSeedDisabled)`); DEMO seed gated gotcha #51.
@ -50,9 +50,9 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
- **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore `['docs/**','**/*.md','.claude/skills/**']`. Anon API works (public repo) when GITEA_TOKEN absent.
- **Prod:** api/admin/eoffice `.solutions.com.vn` · SSH `ssh vietreport-vps` (Administrator, id_ed25519) · IIS phys paths: API `C:\inetpub\solution-erp\api` · admin `\fe-admin` · user `\fe-user`. DB `.\SQLEXPRESS`/`SolutionErp`/`vrapp` SQL-auth. **Conn key = `ConnectionStrings.Default` (NOT `DefaultConnection`)**. pw `vrapp/buKL3TGBkD0wDDbYVw65QeX9` read from prod `appsettings.Production.json` when `$env:PROD_DB_PASSWORD` empty. skill-doc path `C:\inetpub\apps\SolutionErp\Api` STALE.
- **SSHPS quoting (S42):** nested bashsshpowershell mangles `$var`/`\"` use `iconv UTF-16LE | base64` `powershell -EncodedCommand $B64`; OR write ps1-file + `powershell -File $(cygpath -w)`. sqlcmd over SSH direct `-W -h -1` pw-inline. sys-catalog string-concat `COLLATE DATABASE_DEFAULT` (collation conflict).
- **Tests baseline:** **354 PASS** (45 Domain + 309 Infra · 0 fail/skip; S77 Run #329 sha e823694). Phase 9 UAT skip per chunk OK.
- **Mig latest repo:** **Mig 57 `20260619070051_AddPeSuggestedPriceNotes`** (S77 #323; PE +2 note-cols nvarchar(1000) VERIFIED-APPLIED-PROD). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. **`sys.tables` (is_ms_shipped=0) = 88** (S61 Budget-replace DROPPED 9388 narrative-93 STALE; when commit touches no schema, 88 correct, don't FAIL on 88. Always cross-ref COMMIT scope vs ambient count). enum-additive (e.g. `ApprovalAttachment=5`) = int-stored NO-mig expect history NOT-advance + empty `git diff -- '*Migrations*'`.
- **Bundle hash live (S85 Run #341/#342):** admin js `aKuNip19`/css `BKB2NsgQ` · user js `BUsL0HeQ`/css `BaqeI4iG` (FE frozen since #339, BE/docs-only deploys after). **PER #69: hash MAY rotate per deploy OR stay frozen if 0 fe-src diff (byte-identical) — SNAPSHOT for THIS run, NOT a frozen baseline.** Real FE ship = diff `fe-*/src` in commit/range + real-size-vs-fake (1.6/1.5MB vs ~919b SPA-trap), NOT hash delta. ASYMMETRIC-deploy framing (S66) RETIRED by #69.
- **Tests baseline:** **402 PASS** (45 Domain + 357 Infra · 0 fail/skip; S88 Run #345 sha 9b1ee0c 395 + 7 CCM-budget-period setter authz; prior S87 #344 395). Phase 9 UAT skip per chunk OK.
- **Mig latest repo:** **Mig 59 `20260624084608_AddCcmBudgetPeriodToPurchaseEvaluation`** (S88 #345; PE +1 AddColumn `CcmBudgetPeriodAmount` decimal(18,2) NULL no-backfill VERIFIED-APPLIED-PROD history-HEAD + col-exists 0-nonnull). Prev Mig 58 `AddPeMultiWinnerAndLevelFinalize` (S87 #344). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. **`sys.tables` (is_ms_shipped=0) = 88** (S61 Budget-replace DROPPED 9388 narrative-93 STALE; when commit touches no schema, 88 correct, don't FAIL on 88. Always cross-ref COMMIT scope vs ambient count). enum-additive (e.g. `ApprovalAttachment=5`) = int-stored NO-mig expect history NOT-advance + empty `git diff -- '*Migrations*'`.
- **Bundle hash live (S88 Run #345):** admin js `C8lWxEQK`/css `BFfMO5l8` · user js `Kz5cSW2u`/css `B11h2RoV` (BOTH js+css ROTATED from #344 `CXCKlNay`/`BKB2NsgQ` + `Di40ewrF`/`BaqeI4iG` by real fe-src PE Section-B 3-col rewrite css rotated too = new utility classes for table grid). **PER #69: hash MAY rotate per deploy OR stay frozen if 0 fe-src diff (byte-identical) — SNAPSHOT for THIS run, NOT a frozen baseline.** Real FE ship = diff `fe-*/src` in commit/range + real-size-vs-fake (1.6/1.5MB vs ~919b SPA-trap), NOT hash delta. ASYMMETRIC-deploy framing (S66) RETIRED by #69.
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
## 🔑 Critical config (flag commit nếu tái xuất)
@ -70,6 +70,9 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
## 📅 Recent runs (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **S88 #345 `9b1ee0c` PASS ~4m (FULL-STACK PE Section-B 3-col + Mig 59, anh Kiệt FDC/CCM)** — Section B (THỰC HIỆN) rewrite 1-col-list → 3-col table (per-col kỳ-này) + new setter endpoint `PATCH /purchase-evaluations/{id}/budget/ccm-period` (`SetPeCcmBudgetPeriodCommand`, fail-closed Admin|CostControl else Forbidden) + DTO field `ccmBudgetPeriodAmount`. Files 14: BE 4 (Controller/DTO/Domain/Config) + 2 App-features (PeSuggestedPriceFeatures + PurchaseEvaluationFeatures) + FE **both apps** 4 (PeDetailTabs + types ×2) + Mig 3-file + 1 new test. **Mig 59 `20260624084608_AddCcmBudgetPeriodToPurchaseEvaluation`** = single AddColumn `PurchaseEvaluations.CcmBudgetPeriodAmount` decimal(18,2) NULL **no backfill** — VERIFIED-APPLIED-PROD (history top==`AddCcmBudgetPeriod`==repo HEAD; sys.columns COL=1 type `decimal(18,2) null=1`; NONNULLrows=0 as designed). Test gate=success ⟹ passed (**baseline 402** = 45 Domain + 357 Infra = 395 + **7 `PeCcmBudgetPeriodSetterAuthzTests`**: Admin-set/CostControl-set/Drafter-forbidden/Procurement-forbidden/Zero-valid/Null-clears/Unknown-NotFound). **Bundle BOTH js+css ROTATE (real fe-src Section-B rewrite): admin js `CXCKlNay`→`C8lWxEQK` css `BKB2NsgQ`→`BFfMO5l8`, user js `Di40ewrF`→`Kz5cSW2u` css `BaqeI4iG`→`B11h2RoV`** (css rotated = new table-grid utility classes, NOT logic-only). Real-ship proven: admin js 1,627,505b Last-Modified 09:10:47Z=16:10+07 in-window (16:08→16:12) · user js 1,531,406b 09:11:40Z in-window · SPA-trap control `ZZfake0.js`→200 but 919b text/html (size-check meaningful). Endpoint live: `PATCH .../budget/ccm-period` unauth→**401** bodyless AND with-json-body (wired: NOT 404=route-exists, NOT 500=no-crash; PATCH auth short-circuits before IIS Length-check so 401 not 411). Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus both 401. ⚠️ ambient sys.tables=88 (NOT 89 as run-spec hedged — sqlcmd direct COUNT WHERE is_ms_shipped=0 AND name<>'__EFMigrationsHistory' = 88; Mig 59 AddColumn-only 0-new-table, consistent w/ S62 re-ground baseline). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`. gotcha #69 inverse re-confirmed (real fe-src ⟹ hash rotates).
- **S87 #344 (id=458) `f1b10e9` PASS ~5m11s (FULL-STACK PE CCM 3-fix + Mig 58, anh Kiệt FDC)** — multi-winner SUM (SelectWinner→SupplierIds[] array body) + budget=0 valid-submit + per-level auto-finalize (`ApproveV2Async if matchingLevel.AllowApproverFinalize`). Files: BE 13 (Controller/App/Domain/Infra/Service + Mig 3-file) + FE **BOTH apps** 7 (PeDetailTabs/PeWorkflowPanel/types ×2 + fe-admin ApprovalWorkflowsV2Page Designer) + 6 test. **Mig 58 `20260624071509_AddPeMultiWinnerAndLevelFinalize`** = 2 AddColumn bit (`PurchaseEvaluationSuppliers.IsWinner` default0 + `ApprovalWorkflowLevels.AllowApproverFinalize` default0) + 1 backfill `UPDATE ... SET IsWinner=1 WHERE pes.SupplierId=pe.SelectedSupplierId` — VERIFIED-APPLIED-PROD (history top==`AddPeMultiWinnerAndLevelFinalize`==repo HEAD; both cols sys.columns=1; **backfill ran: winners=13 == pe_with_SelectedSupplierId=13 / 30 total supplier rows** — exactly 1 winner per pre-existing selected PE). Test gate=success ⟹ passed (**baseline 395** = 45 Domain + 350 Infra per spec; was 397 2 because per-level-finalize moved runtime-flag→auto-config, NOT regression). **Bundle BOTH ROTATE (real fe-src ⟹ new content-hash, contrasts #69-REFINE BE-only): admin js `aKuNip19`→`CXCKlNay` (css FROZEN `BKB2NsgQ`, logic-only diff no new chunk), user js `BUsL0HeQ`→`Di40ewrF` (css FROZEN `BaqeI4iG`).** Real-ship proven: admin js 1,621,660b Last-Modified 08:26:32Z=15:26+07 in-window · user js 1,525,569b 08:27:26Z in-window · control fake-hash `ZZfakehash0.js`→200 but 919b text/html (SPA-trap confirms 200-alone meaningless). Endpoint live: `POST /api/purchase-evaluations/{id}/select-winner` bodyless→411 (IIS Length-Required pre-auth), +json-body→401 (wired, NOT 404/400/500; `SelectWinnerBody(IReadOnlyList<Guid>? SupplierIds)``SelectPurchaseEvaluationWinnerCommand` array contract live). Health api ready/live + admin + eoffice 4×200. ⚠️ **ambient sys.tables=89 (was 88) — NOT from Mig 58** (AddColumn-only, 0 new table; newest table `PeWorkItemBudgets` 06-13 Mig 50, none created 06-24); 88 baseline last re-grounded S62, +1 pre-existing drift in Mig 50-57 range, harmless. ⚠️ prod path `C:\inetpub\solution-erp\api` (skill-doc `\apps\SolutionErp\Api` STALE — re-confirmed); conn key `ConnectionStrings.Default`; base64 EncodedCommand + 1 path-retry. gotcha #69-REFINE inverse confirmed (real fe-src ⟹ hash DOES rotate).
- **S86 #343 (id=457) `ae4f106` PASS ~5m00s (BE-ONLY notify-filter)** — PE "cần duyệt" notify EXCLUDE drafter + 1 new test (`PeApproverNotifyExcludesDrafterTests`) + STATUS + agent-memory. Only-BE-src: 1 file `PurchaseEvaluationWorkflowService.cs`, ZERO `fe-*/src`, ZERO `*Migrations*`. status=success ⟹ test gate passed (**baseline 377** = 45 Domain + 332 Infra per spec; S85 374 +3; reviewer's 2 transient parallel-collection flake did NOT surface red in CI — gate green). **Ship-proof #69-REFINE (BE-only) CONFIRMED via dll Last-Modified in run-window (16:37:38→16:42:35+07 = 09:37:38→09:42:35Z):** Api.dll 09:40:04Z · **Application.dll 09:38:12Z · Infrastructure.dll 09:38:34Z** (PurchaseEvaluationWorkflowService lives in Infrastructure → recompiled fresh) — all 3 in-window. **FE bundle FROZEN (correct, 0 fe-src diff ⟹ byte-identical): admin `aKuNip19`/`BKB2NsgQ`, user `BUsL0HeQ`/`BaqeI4iG` = S85 baseline.** Mig FROZEN 57 (`AddPeSuggestedPriceNotes`, prod head==repo) + tables 88. Prod 4×200 (api ready/live, admin, eoffice) + PE wired (GET /api/purchase-evaluations + /api/menus both 401 unauth, NOT 404/500). Notify-filter BE-internal no-new-endpoint → test 377 covers behavior (no curl-probe of notify logic). gotcha #69-REFINE reaffirmed (BE-only: dll Last-Modified = sole ship-proof, hash-delta N/A). ⚠️ SSH transient drop ×3 mid-session (`Connection closed by 103.124.94.38`) — base64 EncodedCommand + retry-loop recovered; not a deploy issue.
- **S85 #342 `d4f6021` PASS ~5m00s (DOCS+SQL NO-OP, INVERTED criteria)** — 6×.md + `scripts/s85-wipe-uat-data.sql` (only non-ignored path → CI triggers; whole-workflow still builds FE+publish). **PASS-when-FROZEN, NOT rotate.** Verified: status=success ⟹ test gate passed (baseline **374** = 45 Domain + 329 Infra per STATUS.md canonical, logs API 404 anon so count not log-scraped — trusted gate+baseline) + **bundle FROZEN admin `aKuNip19`/user `BUsL0HeQ` (correct: 0 fe-src diff ⟹ byte-identical ⟹ same hash, gotcha #69-REFINE)** but DID ship (admin js Last-Modified 08:56:34Z=15:56+07 IN-window 15:53→15:58, real 1.62MB/user 1.52MB vs SPA-trap 919b, 2nd-fetch byte-stable). Prod 4×200 (ready/live/admin/eoffice) + endpoint smoke 4×200 (PE/awf-v2/menus/projects). **Prior S85 deploys still live: R6 TZ fix `49ac086` → `createdAt:"…7331140Z"` trailing-Z CONFIRMED; R5 UpdateDraft `1263c0d` → both ancestors of d4f6021, shipped via #341 (#340 cancelled but #341 success built later commit incl TZ).** SignalR negotiate 411 (bodyless-POST IIS, wired, notif untouched). gotcha #73 (TZ) + #69-REFINE reaffirmed.
- **S83 #333 `fc1f19d` PASS ~4m59s** — BE-only PE-guard (require ApprovalWorkflowId V2 at create-validator + submit-branch ConflictException; close FE-only-validation gap → phiếu reached "Đã gửi duyệt" w/ null workflow = V1-legacy-banner trap). +2 validator tests → **test 356** (gate=success ⟹ pass). NO-mig (frozen 57, prod head `AddPeSuggestedPriceNotes`==repo, no-pending) + ZERO fe-*/src diff. **Bundle hash UNCHANGED (admin `CsJetgZH`/user `BVS0ApIm` = #330) BUT shipped — Last-Modified 17:10:35/17:11:27 in-window** (real js 1.62/1.52MB vs SPA-trap 919b) → **gotcha #69-REFINE: 0-FE-src ⟹ byte-identical ⟹ same-hash; Last-Modified is true ship-proof.** Guard BE-only no-endpoint → no curl-probe (356 tests cover RED→GREEN). Health api/ready+live 200, admin/eoffice 200, api-root 404 (no `/` route, expected).
- **S78 #330 `7886fd0` PASS ~4m56s** — cross-stack NO-MIG enum `ApprovalAttachment=5` (attach-on-approve) + FE 2-app PeWorkflowPanel modal-upload. Verified: empty-migrations-diff + Mig-frozen-57 + tables88 + bundle BOTH rotate js+css (real 1.6/1.5MB vs fake-919b) + endpoint bodyless-POST-411→re-probe-body-401-wired + health 4×200. → _INDEX.

View File

@ -74,6 +74,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i
## 📅 Recent activity (FIFO — older → archive/git)
- **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]`.
- **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]`.

View File

@ -70,6 +70,8 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **2026-06-24 PE 3-change recon (Lưu Trần/anh Kiệt CCM — multi-winner + budget=0-valid + per-step terminal):** ⭐ (A1 winner) cardinality=**SINGLE** scalar `PE.SelectedSupplierId Guid?` (`PurchaseEvaluation.cs:28`); cmd `SelectPurchaseEvaluationWinnerCommand(PEId,Guid?)` `PurchaseEvaluationDetailFeatures.cs:392` handler `:398-423` (null=clear S85-C5); ctrl `/select-winner` `PurchaseEvaluationsController.cs:167-172`. winnerQuoteTotal=**DERIVED** SUM `Quote.ThanhTien WHERE SupplierId==Selected` 3 sites: read `PurchaseEvaluationFeatures.cs:1089` + submit-guard `PurchaseEvaluationWorkflowService.cs:191` + ccm-delegate `:868`. ⚠️ Quote ALREADY has `IsSelected bool` (`PurchaseEvaluationQuote.cs:15` per-item winner, UNUSED by total). Multi-winner = NEW junction OR reuse IsSelected + change predicate `==Selected``IN selectedSet`. FE single `<Select>` `PeDetailTabs.tsx:961-991` (NccSelectorRow). (A2 budget=0) BLOCKED 2 layers: validator `.GreaterThan(0).When(HasValue)` create `:55` + UpdateDraft `:233` + budget-adjust `:339`; submit-guard `effectiveBudget<=0`→missing `PurchaseEvaluationWorkflowService.cs:213-216` (effective=`BudgetPeriodAmount??ProInitialAmount` S85-D3). FE `effBudget<=0` `PeDetailTabs.tsx:211,237` canSubmit `:242`. To allow 0: relax `>0``>=0` validator + submit `<0` + FE. winnerQuoteTotal-gate SEPARATE `:194`. (A3 terminal) `ApprovalWorkflowLevel` already has 6 per-slot bool flags (`ApprovalWorkflow.cs:104-131`: AllowReturnOneLevel/OneStep/ToAssignee/ToDrafter/EditDetails/EditBudget/**SkipToFinal**); `CeoApprovalThreshold decimal?` on WORKFLOW `:43`. Finalize branches `ApproveV2Async`: terminal `nextIdx>=steps.Count` `:905-916` + ccm-delegate opt-in `:854-891` + skipToFinal `:803-844`. "Approve-ends-here" = NEW per-Level bool mirror SkipToFinal but set Phase=DaDuyet (like ccm-delegate but no threshold). Designer FE = `fe-admin/ApprovalWorkflowsV2Page.tsx`. **schema-decision forks→em-main:** (1) multi-winner storage: reuse Quote.IsSelected vs new PE↔Supplier winner-junction (2) budget=0 vs budget-null distinction post-relax (3) terminal flag scope per-Level vs per-Step + interaction w/ CeoApprovalThreshold (both finalize → ordering). → _INDEX.
- **2026-06-23 PE UAT recon D2/D4/D5 (anh Kiệt FDC go-live):** ⭐ (D2-urgent authz) `PurchaseEvaluationUrgentFeatures.cs:49-50` BẤT-ĐỐI-XỨNG `isPro=IsUrgent?hasPro:(hasPro&&isDeptManager)` — GẮN=role-only / GỠ=DeptManager-only. NO DrafterUserId match (handler chỉ load entity, never reads DrafterUserId). Để drafter gỡ cờ mình: sửa :49-50 OR `actor.Id==entity.DrafterUserId`. Endpoint `PUT /purchase-evaluations/{id}/urgent` (controller:91). ⚠️ cờ KHÔNG split-by-drafter (PRO/CCM = role, drafter ≠ tất nhiên PRO/CCM). (D5) FE PE-create dropdown=`PeWorkspaceCreateView.tsx:94-104` GET `/approval-workflows-v2?applicableType={1|2}` filter `isUserSelectable`. **Dev DB: PE type1/type2 selectable=0** (6 selectable đều type3-9 Contract/Proposal/Leave/OT/Xe/Ticket NOT PE) → prod UNVERIFIED (SSH-paren-quote-trap). Validator REQUIRE workflow CẢ 2 tầng: `PurchaseEvaluationFeatures.cs:48` create NotEmpty (S83) + submit-guard `PurchaseEvaluationWorkflowService.cs:225` ConflictException. (D4 required) create-validator `:35-57`: Type/TenGoiThau≤500/ProjectId/WorkItemId/ApprovalWorkflowId NotEmpty + BudgetPeriodAmount>0-when-present. Submit-guard `:174-228`: SelectedSupplierId + winnerQuoteTotal>0 + BudgetPeriodAmount>0 + Bảng-so-sánh-attachment + ApprovalWorkflowId. → _INDEX.
- **2026-06-20 governance-landing map (RC-sig + User-Mark H12/13 + objective-criteria):** ⭐ WHERE-to-land 3 AI_INFRA gov broadcasts. `harness-11-engine.md` = CANONICAL engine (D5/D6/D7 safety-tier + D8 one-direction-lock + D9 single-writer + D10 Bash-residual + CAVEAT no-OS-hook). `error-ledger.md` = §L.a AS-1..13 + §L.b 7-step. adap-report FORMAT richest = `2026-06-18-Governance-harness-11.md`. rules.md §6.6 = objective-criteria. **report-before-stamp ⊂ D7 (extend, đừng duplicate → C3 vocab-fork).** → _INDEX.
- **2026-06-20 Harness-14 Eval/Budget/Outcome audit:** ⭐ H-14 = time/age/recency-decay KHÔNG được làm căn-cứ cắt feature. (1) BUDGET aligned: `memory-budget.json` 0 decay/TTL/age hit; `keep_floor=5`=newest-protection (drains OLDEST), cap seed-by-MEASURE. (2) detectors = canonical-anchor-vs-STATUS + disk-cross-check, ZERO age-window. (3) EVAL genuine: `eval/golden-set-solution_erp.jsonl` (14q) + `evaluator.md` recall@5≥0.7 — weekly-manual no-automation. (4) anti-downgrade = Harness-8 "all-inherit chất-lượng>chi-phí" PARTIAL (no generic rule in rules.md, grep 0). → _INDEX.

View File

@ -62,6 +62,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **S86 (2026-06-24) PE Section B THỰC HIỆN tách 3 cột Dự án|PRO|CCM (Mig 59 `CcmBudgetPeriodAmount`) — lane [authz] PASS:** role-gate đúng — handler `SetPeCcmBudgetPeriod` byte-mirror `UpdatePeSuggestedPriceCcm` (NotFound→Forbidden order; fail-closed `!Admin && !CostControl``ForbiddenException`→403 via GlobalExceptionMiddleware TRƯỚC mọi set/changelog). FE gate `bs.canEditCcm` (BE-computed `PurchaseEvaluationFeatures.cs:812 isAdmin||CostControl`) KHỚP BIT-EXACT BE endpoint gate → no FE-show-BE-block mismatch + no FE-hide-BE-allow hole. PRO cột giữ `drafterEditable` (/budget-adjust unchanged) · Dự-án cột hardcode `—` (chưa wire, an toàn). Budget=0 hợp-lệ (`GTE(0).When(HasValue)`, test `ZeroValue_IsValid`). **Submit-guard PROOF untouched:** `grep CcmBudgetPeriod` trong Infrastructure/Services = ZERO; `missingForApproval` chỉ build trên `BudgetPeriodAmount`(PRO)+ProInitial fallback (Mig 58 "0 valid only null block"). Class `[Authorize]` any-auth controller (gotcha #44 N-A: write-endpoint fine-grained Forbidden). 7/7 new authz test PASS (Admin/CCM allow · Drafter/Procurement deny · null-clear absolute-set · NotFound-before-authz). Full suite 45+357=**402 PASS** (395+7). fe-admin+fe-user tsc 0 + 2-app byte-identical (§3.9) + Mig 3-file OK. **Lane [math] (sibling, kept):** per-cột độc-lập đúng — `colCalc(periodAmount,colFull,remaining)` đóng-gói r3/r5/r7/r8/r9+3 cmp KHÔNG cross-cột (`pro=colCalc(draftRow3,proFull,draftRow8)` · `ccm=colCalc(draftCcmRow3,ccmFull,null)`); số chung row1/2/4/6 KHÔNG nhân per-cột (r5=row1+r3 cột, cmpPeriod=r3-row4 chung, cmp56=r5-row6 chung). **Div-by-0 safe:** `fmtPct(num,denom)=denom>0?...:null` + consumer `{pct&&...}` guard → cột full=0/null KHÔNG NaN/Infinity; Dự-án cột hardcode "—" KHÔNG vào colCalc. CCM r8=default(r7) đúng spec (chưa có field CCM-remaining). **#70 note:** new ccmPeriodMut single-field body (no sibling echo) + thêm `||peFetching` gate = clean; PRO adjustMut 2-field-echo KHÔNG peFetching-gate = PRE-EXISTING (base HEAD unchanged, spec "PRO giữ luồng hiện có") không phải regression. Authz mirror byte-exact `UpdatePeSuggestedPriceCcm` (NotFound→Forbidden order, fail-closed Admin|CostControl). Build slnx 0-err + test-proj rebuild 0-err (MSB3030/3026 = Dropbox bin-lock flake, benign) + 7/7 new authz test PASS + fe-admin tsc 0 + 2 app byte-identical (mirror §3.9). Mig 3-file OK. DTO positional slot line-up OK.
- **S82 (2026-06-21) Harness-15-v2 adopt review (governance delta, 0 code) — 3/3 lane PASS:** floor-completeness + faithfulness + honesty-regression, all clean ("genuine PASS not courtesy"). Caught 3 MINOR (đã fix): volume "~42K" stale trong mark · mark What-cell mang nội-dung S82 mà status ghi "anh-confirm S81" → flag pending-confirm · `_index` pending. **Memory-note (chống false-alarm):** `broadcasts/_index.md` sha = notify's **self-declared `content_sha256`** (frontmatter), KHÔNG recompute full-file/body → đừng flag index-vs-file "mismatch" trước khi đọc frontmatter field. **Stamped-mark mid-session edit OK** khi chỉ refresh What-cell content của decision đã-confirm + status flag pending (KHÔNG mint RC-sig mới, KHÔNG đổi tier) = accuracy-keeping, không phải frozen-record breach.
- **S76 (2026-06-19) PE budget ma-trận 3 cột Mig 56 + badge display-only — PASS:** MAJOR race = 2 PRO cell cùng-cột share mutation + echo sibling từ `bs` + fire-and-forget `invalidate()` → data-loss window (fixed **gotcha #70** useIsFetching gate). Badge role-set MUST mirror gate bit-for-bit (đảo-chiều set-lookup = forward Roles.Contains). **SURPRISE:** spec "KHÔNG migration" FALSE (diff bundle Part1+2+3) — đừng tin scope-framing em-main, đọc changed-set thật. → _INDEX S76 Part1/Part2+3.
- **S72/S72bis/S72ter/Q2 (2026-06-18) Mig 54 PE giá-đề-xuất + CCM-finalize OPT-IN — financial go-live PASS:** fail-closed guard ORDER = throw TRƯỚC mọi set `Phase=DaDuyet`; no-deadlock proof = submit-guard invariant (`winnerQuoteTotal>0`) forward-trace tới finalize candidate-set (FE `length===0` branch unreachable); finalize-bypass = 3 gate trực-giao (approver-match ∩ role=CostControl ∩ amount<threshold) + server-recompute amount KHÔNG trust-client; isSystem-exempt = dead-branch single-caller-proof. _INDEX S72*.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,34 @@
# S87 (2026-06-24) — PE multi-winner + budget=0 + cấp-KẾT-THÚC (Mig 58) + Section B 3-cột (Mig 59)
**HMW-mode ON · ~14 spawn (nhiều #53 truncate → recover-disk + reviewer cross-verify + em-main self-gate) · 2 commit prod-verified.**
## Bối cảnh
Anh forward PDF `CCM - 4 ý can sua PM.pdf` (OneNote export) — 4 ý CCM muốn sửa, anh dặn **BỎ mục 3** ("CCM sửa Ngân sách-kỳ-này", nói riêng sau). Rồi anh giao thêm Section B 3-cột qua screenshot + "workflow chạy review test kỹ".
## (1) CCM 3-fix — Mig 58 `AddPeMultiWinnerAndLevelFinalize` (`f1b10e9`, Run #344 PASS ~5m11s)
- **① Multi-winner:** chọn NHIỀU NTP/NCC/ĐVDV cùng trúng → tổng CP = SUM. Thêm `PurchaseEvaluationSupplier.IsWinner` (nguồn-sự-thật mới, backfill từ `SelectedSupplierId` 13/13 phiếu cũ). `SelectWinnerCommand(Guid? SupplierId)``(IReadOnlyList<Guid> SupplierIds)` multi-toggle; `winnerQuoteTotal` 3 site đổi predicate `==SelectedSupplierId``IsWinner`. `SelectedSupplierId` giữ làm "primary" (= 1 winner ? đó : null). FE bảng so sánh tick-nhiều-ô + "Tổng chi phí đã chọn (N đơn vị)".
- **② Budget=0 hợp lệ:** submit-guard nới `effectiveBudget is null || <= 0``is null` only (0 cố-ý đi qua, null chặn). Fallback ProInitial CHỈ khi null (bảo-vệ 0-cố-ý khỏi bị override→null-block). 3 validator `GreaterThan(0)``GreaterThanOrEqualTo(0)`.
- **④ Cấp KẾT THÚC quy trình:** `ApprovalWorkflowLevel.AllowApproverFinalize`. **Diễn-tiến thiết-kế:** ban đầu em làm opt-in checkbox (an-toàn S73) → anh "cho bật trong quy trình thì hay hơn" → đổi **AUTO** theo cấu-hình-cấp: nhánh `if (matchingLevel.AllowApproverFinalize)` trong ApproveV2Async tự DaDuyet bỏ CEO, GỠ runtime-flag `finalizeByApprover` (revert 9 site plumbing). FE bỏ checkbox → banner "Cấp này KẾT THÚC quy trình" + bắt chọn giá-chốt. Designer per-level checkbox.
- **⚠️ Self-review bắt 8 chỗ FE keyed `selectedSupplierId`** (null khi ≥2 winner → vỡ submit-guard/checklist/`computeGiaChaoThau`/`canCreateContract`/display) → đổi `isWinner`. → bài học `feedback_cardinality_change_grep_consumers`.
- Test 377→**395** (+20 2 finalize-auto-rework). Bundle admin `aKuNip19``CXCKlNay`/user `BUsL0HeQ``Di40ewrF`. cicd #344: Mig 58 applied, backfill 13/13, 2 bundle rotate, select-winner 401 wired.
## (2) Section B 3-cột — Mig 59 `AddCcmBudgetPeriodToPurchaseEvaluation` (`9b1ee0c`, Run #345 cicd-verifying)
- Anh muốn Section B (THỰC HIỆN) tách 3 cột Dự án|PRO|CCM giống Section A. **AskUserQuestion** chốt scope → **"mỗi cột nhập kỳ-này RIÊNG"**.
- BE: `PurchaseEvaluation.CcmBudgetPeriodAmount decimal(18,2)?` + `SetPeCcmBudgetPeriodCommand` (role-gate Admin|CostControl fail-closed, GTE0, absolute-set) + endpoint `PATCH /budget/ccm-period` + DTO. (⚠️ command rơi `PeSuggestedPriceFeatures.cs` — lệch chỗ, dọn monthly.)
- FE: Section B `<BudgetRow>` list → `<table>` 3 cột. Helper `colCalc(period, colFull, remaining)` tính per-cột (PRO: budgetPeriodAmount/proFull · CCM: ccmBudgetPeriodAmount/ccmFull · Dự án —). Số chung row1/2/4/6 `colSpan=3`.
- **Workflow `wf_aafb18fe-d9d`** (anh yêu cầu "review test kỹ"): BE→FE+test→3-lane review. math PASS (traced colCalc no-column-crossing) · authz PASS (setter persist+role-gate, 402 test) · **mirror+FE return NULL** (#53) → em-main self-gate: mirror byte-identical 2 app + 402 test + 2 npm build.
- Test 395→**402** (+7 `PeCcmBudgetPeriodSetterAuthzTests`).
## Lessons
- **Cardinality single→multi = grep MỌI consumer** (`feedback_cardinality_change_grep_consumers`) — impl sửa write-site, sót dependent read-site, vỡ feature vừa thêm; build+test+reviewer PASS vẫn miss (lỗi logic-runtime case multi).
- **Design-dialog tránh rework:** item-4 auto-vs-optin (anh chốt auto qua workflow-config) + Section B scope (AskUserQuestion per-cột-kỳ-này) — hỏi-trước-build chỗ ambiguous-high-rework.
- **Workflow review-lane value dù lane NULL:** 2/5 lane (FE-impl + mirror-review) return NULL, nhưng math+authz lane verify disk độc-lập → trust PASS + em-main self-gate phần NULL. Workflow vẫn hữu-ích cho review (≠ phải mọi-lane-return).
- **#53 truncate dày-đặc session này** — impl-backend, test-spec, impl-frontend ×2, workflow FE+mirror; TẤT CẢ recover-disk OK (git/build/test = source-of-truth). Carry `feedback_agent_kill_recovery` + `feedback_workflow_fanout_reliability`.
## State THẬT cuối S87
**Mig 59 · 88 bảng (cicd flag 89 — drift cũ KHÔNG do Mig 58/59 AddColumn-only) · 402 test (45D+357I) · gotcha 73 · menu 54 · bundle admin `CXCKlNay`/user `Di40ewrF` (Run #344; #345 rotate pending).**
## NEXT
- **Anh:** restart CLI (carry). UAT (Ctrl+F5): multi-winner tick-nhiều + tổng · budget=0 submit · Designer bật "Duyệt là kết thúc" cấp anh Chương · Section B cột CCM nhập kỳ-này (tài khoản CCM).
- **Em:** curate L1 over-cap carry · monthly 2026-07-01 (dọn CCM-setter lệch-chỗ · re-ground sys.tables 88→89 · STATUS/HANDOFF re-tier · ef-core SKILL Mig→59).
- **Pending product:** Section B cột Dự-án wire (chờ anh) · Ngưỡng giá CEO Mig 54 Designer · "C" chuyển phiếu→dự án chờ spec.