# Session 25 — 2026-05-19 — PE History Visibility cumulative fix (Plan AB → AF)

**Dev:** Claude Opus 4.7 1M (em main + 4 sub-agent spawns Investigator+Implementer+Reviewer+CICD)
**Duration:** ~5h
**Base commit:** `e23f51c` (Docs Session 24 wrap)
**Final HEAD:** `506cada` Plan AF FE userMap fallback
**Total commits S25:** **7** (`cdfd542`, `8c05947`, `a734bf2`, `25837b6`, `0aaf2df`, `9ea62be`, `506cada`)
**CICD Runs:** **7** (#215 FAIL test gate → #216-#221 ALL PASS post-fix)

## 🎯 Trigger session

Bro UAT 2026-05-19 sau S24 Plan AA deploy phát hiện 2 bug critical phiếu **PE/2026/A/032** (`3248f2f9-c6e9-43ff-a4ca-067ffecf9f36`):

**Bug 1 — Budget Adjust không show "Lịch sử thay đổi":**
- Bro click "Điều chỉnh ngân sách" 2× (5.300.000.000 → 5.200.000.000 đ)
- Section "Lịch sử thay đổi" empty placeholder "Chưa có lịch sử trả lại / gửi duyệt lại"

**Bug 2 — Trả lại Người chỉ định không log:**
- Bro role Approver Phan Văn Chương click "Trả lại Người chỉ định" → Bùi Lê Thủy Trà
- Section "Lịch sử thay đổi" KHÔNG show entry return assignee

Sau Plan AB deploy bro phát hiện thêm 4 issues UAT iterative — em main solo 5 follow-up plans (AC→AF) cross-stack reasoning.

## 🌳 Plan execution 7 commits Plan AB → AF

### Plan AB — Bug 1 + Bug 2 root fix

#### Investigator pre-flight audit (🟦 Investigator, ~28K)

5Q audit confirm root cause:

| Q | Finding |
|---|---|
| Q1 DB state Changelogs cho PE | SSH auth fail, fallback grep code BE |
| Q2 BE AdjustBudget handler | `PurchaseEvaluationFeatures.cs:379-387` ĐÃ log `EntityType=Header + Action=Update + Summary="Điều chỉnh ngân sách..."` ✓ |
| Q3 BE ApplyReturnModeAsync | line 215-378 **ZERO** `Changelog.Add()` cho 4 mode. Caller `TransitionAsync:100` chỉ log phase transition qua `LogTransitionAsync` ❌ |
| Q4 Schema PurchaseEvaluationChangelog | `EntityType` enum {Header=1, Supplier=2, Detail=3, Quote=4, Workflow=5, Attachment=6}. Missing Kind discriminator (Header+Update collision Budget vs Detail) |
| Q5 FE filter HistoryTab | Hypothesis FE filter omit Header+Workflow types (unconfirmed Investigator scope) |

**Conclusion:**
- Bug 1: BE log OK + FE filter strict TraLai-only loại Header+ChoDuyet
- Bug 2: BE Service hook ROOT GAP — KHÔNG log Changelog cho 4 mode return

#### Em main verify FE filter logic

`PeDetailTabs.tsx:2037-2046` HistoryTab filter:
```ts
const PE_PHASE_TRALAI = 98
const PE_ENTITY_WORKFLOW = 5
const filtered = (logs.data ?? []).filter(l => {
    if (l.entityType === PE_ENTITY_WORKFLOW) {
        if (l.phaseAtChange === PE_PHASE_TRALAI) return true
        if (l.summary?.includes('TraLai →')) return true
        return false
    }
    return l.phaseAtChange === PE_PHASE_TRALAI  // ← Header Budget Adjust ChoDuyet bị loại
})
```

Filter chỉ pass khi phase=TraLai. Header Budget Adjust ChoDuyet → loại. Workflow Reject 3/4 mode giữ ChoDuyet → loại.

#### Plan AB Chunk A — `cdfd542` (🟨 Implementer Case 1 ~12K, 🟥 Reviewer ~22K)

**BE refactor `ApplyReturnModeAsync`:**
- Drafter early return (line 282-287) → if/else common path
- Single `Changelog.Add()` cuối hàm cover 4 mode uniform với `modeName` switch
- `actorName` resolve via `userManager.FindByIdAsync` mirror `LogTransitionAsync` pattern

**FE filter relax × 2 app:**
- ADD `PE_ENTITY_HEADER = 1` + `summary?.toLowerCase().includes('ngân sách')` cho Bug 1
- ADD `summary?.includes('Trả lại')` Workflow entity cho Bug 2
- Update placeholder text + comment 5 rule

3 files +146/-95 LOC. Reviewer PASS 0 blocker.

#### Run #215 FAIL — gotcha #48 NEW (🟩 CICD Monitor)

`test_infra` 51/53 PASS, 2 FAIL Plan M edge case:
- `ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet`
- `ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet`

Error: `Expected changelog.ContextNote not to be <null>`

**Root cause:** SQLite frozen test clock + 2 Changelog.Add() trong cùng SaveChangesAsync → identical CreatedAt → `OrderByDescending.FirstAsync()` non-deterministic → pick Plan AB row (Workflow+Update+ContextNote=null) thay LogTransition row.

#### Plan AB Chunk A2 — `8c05947` (em main solo)

Test fix Option A: filter `Where(c => c.Summary!.Contains("Chuyển phase"))` pick đúng LogTransition entry. Plan AB BE code stays clean.

```diff
- .Where(c => c.PurchaseEvaluationId == pe.Id)
+ .Where(c => c.PurchaseEvaluationId == pe.Id && c.Summary!.Contains("Chuyển phase"))
```

111/111 PASS local. Run #216 PASS deploy success.

### Plan AC — Bug 3 Lịch sử duyệt missing Trả lại + Vượt cấp

Bro UAT post Plan AB: "Lịch sử duyệt" panel 6 entries TẤT CẢ "Đã gửi duyệt → Đã gửi duyệt" (Approve only). Trả lại + Vượt cấp KHÔNG hiện.

**Root cause:**
- `PurchaseEvaluationApprovals.Add()` chỉ ở Approve branch (line 472 V2 + 660 V1)
- Reject branch line 75-103 NEVER add row → Lịch sử duyệt missing
- skipToFinal advance line 532-572 dùng existing line 472 row nhưng comment KHÔNG distinct

#### Plan AC commit `a734bf2` (em main solo)

**BE Reject branch add Approval row:**
```csharp
// Capture pre-call (ApplyReturnModeAsync mutates pointer)
var fromStepIdx = evaluation.CurrentWorkflowStepIndex;
var fromLevelOrder = evaluation.CurrentApprovalLevelOrder;

// ... mutate via ApplyReturnModeAsync ...

// Add Approval row (Decision=Reject)
db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval {
    PurchaseEvaluationId = evaluation.Id,
    FromPhase = fromPhase,
    ToPhase = evaluation.Phase,
    ApproverUserId = actorUserId,
    Decision = ApprovalDecision.Reject,
    Comment = $"{fromPos}{comment ?? ""}".Trim(),
    ApprovedAt = dateTime.UtcNow,
});
```

**BE skipToFinal enrich comment** line 479 `Comment = $"{skipPrefix}[Bước X — Cấp Y] {comment}"`.

**FE Decision badge × 2 app:**
```tsx
function decisionBadge(decision, toPhase): { label, cls } {
  if (decision === PE_DECISION_REJECT) {
    if (toPhase === 99) return { label: 'Từ chối', cls: 'bg-rose-100 text-rose-700 ...' }
    return { label: 'Trả lại', cls: 'bg-amber-100 text-amber-700 ...' }
  }
  return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 ...' }
}
```

Run #217 PASS, bundle rotate × 2 app.

### Plan AC2 — Bug FE merge synthetic Reject from Changelog historical

Bro: "phiếu cũ PE/2026/A/032 ko thể hiện đc lịch sử trả duyệt và gửi vượt cấp ?"

Plan AC chỉ forward fix — entries pre-deploy chỉ có Changelog (LogTransition), Approval table missing Reject rows.

#### Plan AC2 commit `25837b6` (em main solo, Option 2A bro chốt)

FE merge view: ApprovalsTab fetch changelogs + reconstruct synthetic Reject rows:
- Filter: `entityType=5 Workflow + (summary "→ TraLai"/"→ TuChoi" OR contextNote "Trả về"/"không lùi được")`
- Parse phase từ Summary regex `Chuyển phase X → Y` → enum map (DangSoanThao=1, ChoDuyet=10, DaDuyet=20, TraLai=98, TuChoi=99)
- Synthetic ID prefix `syn-{changelog.id}`, decision=2
- Dedupe: `${approverUserId}-${Math.floor(approvedAt / 5000)}` 5s bucket key
- Merge sort by `approvedAt`

KHÔNG DB write, reversible historical recovery. Run #218 PASS, 8 Workflow entries verified.

### Plan AD — Bug 4 dual-phase badges gây nhầm

Bro: "các bước này chưa thể hiện đc là đã gửi duyệt cho ai ? Trả lại cũng vậy (Đã trả lại thì là trả lại chứ hiển thị đã gửi duyệt thì lại gây hiểu nhầm)"

3/4 mode Reject (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → badge `Đã gửi duyệt → Đã gửi duyệt` identical Approve.

#### Plan AD commit `0aaf2df` (em main solo, Option A bro chốt)

**Drop fromPhase→toPhase badges entirely.**

**Add `extractNextTargetHint(decision, toPhase, comment)` helper:**
```ts
// Approve patterns:
//   "sang Cấp X" → "→ Cấp X"
//   "sang Bước Y" → "→ Bước Y (Cấp 1)"
//   "[Duyệt vượt cấp tới Cấp cuối]" → "→ Vượt cấp tới Cấp cuối"
//   toPhase=DaDuyet(20) → "→ Đã duyệt hoàn tất"
// Reject patterns:
//   "Người chỉ định" + Bước/Cấp regex → "→ Trả về Người chỉ định (Bước X Cấp Y)"
//   "Người soạn thảo"/"Drafter" → "→ Trả về Người soạn thảo"
//   "không lùi được" → "→ Không lùi được"
//   "Trả về 1 Cấp"/"Trả về Cấp X" → "→ Lùi về Cấp X"
//   "Trả về 1 Bước"/"Trả về Bước X" → "→ Lùi về Bước X"
//   toPhase=TuChoi(99) → "→ Từ chối hoàn toàn"
```

Cleanup unused `PurchaseEvaluationPhaseColor` import. Mirror × 2 app §3.9. Run #219 PASS, bundle rotate × 2 app. **Gotcha #49 NEW**.

### Plan AE — Bug 5 Changelog UserName show "Hệ thống"

Bro: "chỗ điều chỉnh ngân sách em bắc luôn user nào điều chỉnh luôn em nhé"

Audit phát hiện systemic gap — 9 `Changelog.Add()` sites trong PE features MISSING UserName field.

#### Plan AE commit `9ea62be` (em main solo, preventive batch)

**9 sites batch fix:**
- `PurchaseEvaluationFeatures.cs` 4 sites: Create + Default detail + UpdateDraft + AdjustBudget
- `PurchaseEvaluationDetailFeatures.cs` 5 sites: Detail Insert/Update/Delete + Quote Insert/Update/Delete + Select Winner (1 inside if-block 16-space indent)

**Pattern:**
```csharp
UserName = currentUser.FullName ?? currentUser.Email
```

ICurrentUser interface đã có FullName + Email từ JWT claims — KHÔNG inject userManager mới.

`Edit replace_all=true` 1 pass cover 8 sites cùng indent + 1 site manual fix indent.

111/111 PASS local. Run #220 PASS, bundle UNCHANGED (BE-only commit).

### Plan AF — Bug 6 historical entries pre-Plan AE missing name

Bro: "phiếu cũ ko thể hiện đc người edit nhỉ ?"

Plan AE chỉ forward — entries pre-deploy có `userName=""` empty.

#### Plan AF commit `506cada` (em main solo, Option A bro chốt)

FE ApprovalsTab + HistoryTab build `userMap useMemo` từ embedded PeDetailBundle data:

```tsx
const userMap = useMemo(() => {
  const m = new Map<string, string>()
  if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName)
  ev.approvals.forEach(a => {
    if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName)
  })
  ev.approvalFlow?.steps?.forEach(s =>
    s.levels?.forEach(l =>
      l.approvers?.forEach(ap => {
        if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName)
      })
    )
  )
  ev.levelOpinions?.forEach(o => {
    if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName)
  })
  ev.departmentOpinions?.forEach(o => {
    if (o.userId && o.userName) m.set(o.userId, o.userName)
  })
  return m
}, [ev])

const resolveUserName = (l: PeChangelog): string => {
  if (l.userName && l.userName.trim() !== '') return l.userName
  if (l.userId) {
    const name = userMap.get(l.userId)
    if (name) return name
  }
  return 'Hệ thống'
}
```

NO extra API fetch (`/api/users` admin permission). Cover gần hết users tham gia phiếu. Mirror × 2 app §3.9.

Run #221 PASS, bundle rotate × 2 app (admin `DR95zKWg → C8TvDy7r`, user `BAj_Yaj5 → BvcWrq2z`).

## 📊 Stats final S25 chốt cuối

| Metric | Pre-S25 (S24) | Post-S25 | Δ |
|---|---:|---:|---:|
| Migrations | 31 | 31 | 0 (no schema change) |
| DB tables | 59 | 59 | 0 |
| Endpoints | ~146 | ~146 | 0 |
| FE pages | 35 | 35 | 0 |
| Unit tests | 111 | 111 | 0 (UAT defer test-after per §7) |
| Gotchas | 47 | **49** | **+2 (#48 SQLite tie-break + #49 dual-phase confusion)** |
| Memory user-level | 21 | **23** | **+2 (NEW: feedback_fe_merge_synthetic_audit + feedback_fe_usermap_fallback)** |
| Skills | 6 | 6 | 0 |
| Sub-agents active | 4 | 4 | 0 |
| **Commits S25** | — | **7** | `cdfd542`, `8c05947`, `a734bf2`, `25837b6`, `0aaf2df`, `9ea62be`, `506cada` |
| **CICD Runs** | — | **7** | #215 FAIL → #216-#221 PASS |
| Bundle rotate × 2 app | — | **6×** | (Run #216,217,218,219,221) — Run #220 BE-only unchanged |

## 🎯 Multi-agent ROI S25

| Owner | Cost | Verdict | Value catch |
|---|---|---|---|
| 🟦 Investigator Plan AB Bug 1+2 | ~28K | ✅ | 5Q audit confirm BE Service hook gap (Bug 2) + FE filter strict (Bug 1). Saved em main blind cross-stack fix |
| 🟨 Implementer Plan AB Chunk A | ~12K | ✅ | 3 files +146/-95 LOC cookie-cutter mirror BE refactor + FE 2 app §3.9 |
| 🟥 Reviewer Plan AB pre-commit | ~22K | ✅ | 5-category + 8 adversarial deep checks PASS 0 blocker (1 minor V1 fallback non-block) |
| 🟩 CICD Monitor Run #215 catch FAIL | ~10K | ⚠️ | **VALUE CAUGHT** — SQLite tie-break gotcha #48, test gate blocked prod from broken state |
| 🟩 CICD Monitor Run #216-#221 PASS | ~60K (5 spawns × ~12K) | ✅ | Bundle rotate verify + wire end-to-end + Mig 31 unchanged + Plan AC2 changelogs data source confirm |
| 👤 Chủ trì Solo Plan AB Chunk A2 fix tests | ~3K | ✅ | Test filter discriminator surgical fix, 111/111 PASS |
| 👤 Chủ trì Solo Plan AC | ~12K | ✅ | Cross-stack BE Reject + FE Decision badge × 2 app |
| 👤 Chủ trì Solo Plan AC2 | ~10K | ✅ | FE merge synthetic Reject + dedupe (Option 2A historical recovery) |
| 👤 Chủ trì Solo Plan AD | ~8K | ✅ | UI redesign drop dual-phase badges + extractNextTargetHint regex parse |
| 👤 Chủ trì Solo Plan AE | ~6K | ✅ | BE preventive batch 9 sites UserName |
| 👤 Chủ trì Solo Plan AF | ~10K | ✅ | FE userMap fallback từ embedded domain data (Option A historical name resolve) |

**Total cost cumulative S25:** Agents ~132K + Em main ~210K = ~342K (cache leverage 70-90% per session). ROI ~25% solo equiv.

## 📋 Patterns reusable saved S25 (8 NEW)

1. **Multi-Changelog.Add() SQLite frozen-clock tie-break (gotcha #48)** — Test query audit table cần filter EntityType/Summary discriminator thay raw OrderByDescending(CreatedAt). Cross-ref Contract V2 test setup tương lai.

2. **CICD Monitor catch UAT skip test risk validated** — Cho BE refactor > 100 LOC + signature change, UAT mode `feedback_uat_skip_verify` skip `dotnet test` RISK. Em main resumed local test verify post Plan AB Chunk A2 fix.

3. **Test fix Option A over BE refactor preservation** — Khi test fail post-refactor do test logic assumption broken, ưu tiên tests get more specific selector (filter discriminator) thay vì revert BE code change. Code semantic stays clean, tests adapt to new schema reality.

4. **Capture pre-call mutation state cho audit row from-position** — Khi Service mutate entity state AND write audit row simultaneously (vd Plan AC ApplyReturnModeAsync mutate pointer + log Approval), snapshot OLD Step/Level BEFORE mutation. Pattern reusable Contract V2 audit + Budget V2 audit.

5. **FE merge synthetic rows từ Changelog historical (Plan AC2 Option 2A)** — Reversible recovery KHÔNG DB write. ApprovalsTab fetch changelogs + reconstruct synthetic rows từ existing audit data + dedupe timestamp bucket key vs real rows. Reusable Contract V2 + Budget V2 audit visualization without DB migration.

6. **Drop misleading dual-phase badges + parse semantic next-target hint (Plan AD, gotcha #49)** — UI audit history KHÔNG nên render dual-phase badge khi state machine self-loop (ChoDuyet → ChoDuyet là advance pointer trong cùng phase). Thay bằng Decision badge + semantic hint parse từ structured comment via regex + keyword detect.

7. **Changelog UserName preventive systemic batch fix (Plan AE)** — Khi audit phát hiện 1 site bug pattern, grep enumerate ALL similar sites. `Edit replace_all=true` với context-aware key (UserId + Summary) cover N sites idempotent. ICurrentUser.FullName fallback Email mirror LogTransitionAsync pattern.

8. **FE userMap fallback từ embedded domain data (Plan AF Option A)** — Build composite lookup map từ data có sẵn trong response bundle (drafter + approvals + approvalFlow + levelOpinions + departmentOpinions), KHÔNG cần extra API fetch admin permission. Trust entry.userName non-empty → userMap.get(entry.userId) → 'Hệ thống' fallback.

## ⏭ Pending S26+

- 🔴 **Memory curate cicd-monitor PRIORITY MAX** — ~72KB strongly over 50KB hard threshold. DEDICATED CURATION SESSION REQUIRED — archive Run #186-#210 + S22-S24 verbose entries to `archive/2026-05-runs-186-210.md`. FIFO ~15 entries.
- 🟡 **Memory curate Investigator (32KB) + Implementer (35.7KB) + Reviewer (32.3KB)** cùng session với cicd-monitor.
- 🟡 **Plan B Contract V2 wire (Mig 32+33)** — HIGH priority pre-allocated S23 HANDOFF tree. 5-6 chunk: Investigator pre-flight + Mig 32 schema (Contract +ApprovalWorkflowId) + Service ApproveV2Async branch + Mig 33 ContractLevelOpinions + FE Workspace × 2 app + FE Detail × 2 app.
- 📝 **Memory user-level** `feedback_responsive_laptop_breakpoint` stale "3-panel" → 2-panel correction (S24 Investigator surprise note).
- 🔍 **Discovery #3 anomaly CI trigger docs-only** (4× reinforced) — defer Investigator follow-up.
- 🔍 **Discovery #4 ASP.NET enum body deserialization numeric input requirement** (Plan P S23 side effect) — LOW priority register JsonStringEnumConverter Program.cs.

## References

- Files: [PurchaseEvaluationWorkflowService.cs](../../../src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs) · [PurchaseEvaluationFeatures.cs](../../../src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs) · [PurchaseEvaluationDetailFeatures.cs](../../../src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDetailFeatures.cs) · [PeDetailTabs.tsx fe-user](../../../fe-user/src/components/pe/PeDetailTabs.tsx) · [PeDetailTabs.tsx fe-admin](../../../fe-admin/src/components/pe/PeDetailTabs.tsx)
- Rules: §3.9 mirror 2 FE app · §6.5 KEEP narrative · §7 test timing (UAT defer test-after)
- Cross-ref skill `contract-workflow` (Approve/Reject decision pattern) + `permission-matrix` (approverUserId per-NV slot)
- Memory cross-ref `feedback_per_nv_permission_scope.md` (per-NV wire 10 surface points) + `feedback_service_hook_vs_endpoint.md` (audit log derived state) + NEW `feedback_fe_merge_synthetic_audit.md` + NEW `feedback_fe_usermap_fallback.md`
- CICD Run URLs: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{215..221}
