diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 74e42b7..937f707 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,8 +1,113 @@ # HANDOFF — Brief 5 phút cho session tiếp theo -**Last updated:** 2026-05-12 (Session 21 turn 2 — **🎯 RAG Hybrid setup planning + Cách A validation deep dive. 2 commit (`1f8e9af` plan save 1223 LOC + this chốt). KHÔNG implement, plan only — defer chờ bro confirm 5 dự án future. Decision chốt: Cách A defensive (giữ blanket 120K em main + RAG retrieve) over Cách B aggressive (cắt 60-70% blanket). Industry-validated cross 4 Anthropic blog + 5 community tools (Cursor/Continue/Cline/Aider). Stack: Voyage-3-large + Qdrant + FastMCP + Streamlit dashboard. Multi-agent cost reality: 4 agents → ~520K cumulative blanket → heavy session ~560K (Cách A) vs ~700K (lazy). 3-layer pattern Phase 1-3 rollout (embeddings + BM25 + reranking, ~70% → ~92% recall). Stats: +1 memory entry (`feedback_rag_hybrid_pattern`) +1 plan file (`rag-setup-plan.md` 1500 LOC). Sub-agents vẫn 4 seeds-only, em main solo session.**) +**Last updated:** 2026-05-12 2100 (Session 21 turn 3 — **🔴 BUG FIX CRITICAL "Trả về nhưng hệ thống vẫn duyệt" PE workflow (gotcha #45 mới). 3 chunk per-commit: `de00887` (BE Chunk A guard + 3 test) + `4b29d00` (FE Chunk B fix 2 app mirror) + this Chunk C Docs. Root: `PeWorkflowPanel.tsx` `isReject` payload (L64-66) thiếu nhánh TraLai → button "← Trả lại" gửi `decision: 1` (Approve) thay vì `2` (Reject) khi target=TraLai(98) → BE skip Reject branch → enter APPROVE STEP → `ApproveV2Async` UPSERT opinion "đã duyệt" + advance Cấp tiếp theo. Inconsistency phụ: dialog `isSendBack` (L247-248) cùng pattern thiếu TraLai → dialog title sai `'✓ Duyệt → Trả lại'` + KHÔNG amber warning. Severity CRITICAL — data integrity issue khó rollback (BE đã `SaveChangesAsync`). Test-before §7 BẮT BUỘC: viết test reproduce → confirm FAIL (BE đi sâu vào ApproveV2Async throw "Phiếu chưa pin workflow") → thêm BE guard early throw ConflictException khi `target ∈ {TraLai, TuChoi} && decision != Reject` → confirm PASS. 3 regression test (Throws TraLai+Approve, Throws TuChoi+Approve consistency, happy path Reject+TraLai). Tổng `dotnet test SolutionErp.slnx` 84 PASS (58 Domain + 26 Infra = +3 from 81 baseline). `npm run build` × 2 app pass. Stats: 27 mig (no change) · 59 tables · ~142 endpoints · 34 FE pages · **84 test (+3)** · **45 gotcha (+1 #45)** · 17 memory · 6 skills · 4 sub-agents seeds-only. Em main solo S21 t3 — bug fix reasoning chain cross BE/FE Implementer REFUSE per multi-agent rule (decision tree: tightly coupled BE+FE+test). CHƯA push remote — chờ bro confirm sau Chunk C wrap.**) +**S21 turn 2:** 2026-05-12 1800 (Session 21 turn 2 — **🎯 RAG Hybrid setup planning + Cách A validation deep dive. 2 commit (`1f8e9af` plan save 1223 LOC + this chốt). KHÔNG implement, plan only — defer chờ bro confirm 5 dự án future. Decision chốt: Cách A defensive (giữ blanket 120K em main + RAG retrieve) over Cách B aggressive (cắt 60-70% blanket). Industry-validated cross 4 Anthropic blog + 5 community tools (Cursor/Continue/Cline/Aider). Stack: Voyage-3-large + Qdrant + FastMCP + Streamlit dashboard. Multi-agent cost reality: 4 agents → ~520K cumulative blanket → heavy session ~560K (Cách A) vs ~700K (lazy). 3-layer pattern Phase 1-3 rollout (embeddings + BM25 + reranking, ~70% → ~92% recall). Stats: +1 memory entry (`feedback_rag_hybrid_pattern`) +1 plan file (`rag-setup-plan.md` 1500 LOC). Sub-agents vẫn 4 seeds-only, em main solo session.**) **S21 turn 1:** 2026-05-12 0030 (Session 21 turn 1 — **🎯 Add con thứ 4 cicd-monitor (Path A — post-deploy verifier). 1 commit `f1c61c9` pushed `36e21c8..f1c61c9 main -> main`. CI skipped per path filter (3 file `.md`). Cost reality update: ~750K spawn (3 → 4 agents) · ~1.35M heavy / ~700K optimized. Stats: 4 sub-agents seeds-only · 16 memory · 27 mig · 59 tables · ~142 endpoints · 81 test · 44 gotcha · 6 skills unchanged. KHÔNG flush 3 agent MEMORY.md (chưa spawn work — em main solo). Trial Week 1 kick-off S21 turn 2+ Plan B Contract V2 wire mirror PE pattern.**) +## TL;DR Session 21 turn 3 — Bug fix CRITICAL "Trả về nhưng hệ thống vẫn duyệt" (gotcha #45) + +User UAT 2026-05-12 21:00 screenshot button labeled `← Trả lại` trong PE Workflow Panel (menu "Duyệt"), mô tả hành vi: nhấn vào nhưng phiếu KHÔNG về phase TraLai — ngược lại tiến qua Cấp tiếp theo. User mô tả: "Trả về nhưng hệ thống vẫn duyệt". + +### Diagnose (em main solo, no agent spawn) + +3 chỗ inconsistency cùng pattern trong `PeWorkflowPanel.tsx` (× 2 app fe-admin + fe-user): + +| # | Location | Logic | Bug? | +|---|---|---|---| +| 1 | L205-207 button `isSendBack` | include TraLai → label `← Trả lại` ĐÚNG | ✅ no bug | +| 2 | L64-66 payload `isReject` | thiếu nhánh TraLai → gửi `decision: 1` (Approve) | 🔴 BUG ROOT | +| 3 | L247-248 dialog `isSendBack` | thiếu nhánh TraLai → dialog title fallback `'✓ Duyệt → Trả lại'` + no amber warning | 🔴 BUG phụ | + +BE `PurchaseEvaluationWorkflowService.TransitionAsync`: +- L51 `if (decision == Reject)` branch → đúng cho decision=Reject. +- L97 `APPROVE STEP` branch khi decision=Approve + fromPhase=ChoDuyet → ApproveV2Async UPSERT opinion + advance Cấp. +- → FE gửi `decision=1` (do bug `isReject`) → BE đi vào nhánh APPROVE thay vì REJECT → phiếu approve mặc dù user định trả lại. + +### Chunk A — BE defense-in-depth + 3 regression test (`de00887`) + +**Test-before §7 BẮT BUỘC:** Viết test reproduce bug TRƯỚC fix. + +```csharp +// Sau line 48 (set isAdmin/isSystem), trước REJECT branch (L51) +if ((targetPhase == PurchaseEvaluationPhase.TraLai + || targetPhase == PurchaseEvaluationPhase.TuChoi) + && decision != ApprovalDecision.Reject) +{ + throw new ConflictException( + $"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " + + "Báo lỗi caller — payload mismatch giữa target phase và decision."); +} +``` + +Boundary protection cho mọi caller tương lai (API client / mobile / cron retry). Guard KHÔNG xoá khi FE fix — defense-in-depth. + +3 test file `tests/SolutionErp.Infrastructure.Tests/Services/PurchaseEvaluationWorkflowServiceGuardTests.cs`: +- `TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState` — reproduce bug, expect `ConflictException` "*TraLai*Reject*" +- `TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState` — consistency cover TuChoi +- `TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai` — happy path control (Reject branch vẫn đúng) + ++ `NoOpNotificationService` stub reusable cho future PE service tests (avoid `INotificationService` real DI complexity). + +Run test → 2 FAIL (reproduce bug, BE đi sâu vào ApproveV2Async throw "Phiếu chưa pin workflow") + 1 PASS (happy path). Thêm BE guard → 3 PASS. Tổng `dotnet test SolutionErp.slnx` 84 PASS (+3 from 81 baseline). + +### Chunk B — FE fix mirror 2 app (`4b29d00`) + +3 chỗ × 2 app = 6 edits: + +```typescript +// Chỗ 1: isReject payload (L64-66) +const isReject = target === PurchaseEvaluationPhase.TuChoi + || (target === PurchaseEvaluationPhase.DangSoanThao + && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) + || (target === PurchaseEvaluationPhase.TraLai // ← THÊM + && evaluation.phase !== PurchaseEvaluationPhase.TraLai) + +// Chỗ 3: dialog isSendBack (L247-248) +const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao + || target === PurchaseEvaluationPhase.TraLai) // ← THÊM + && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao + && evaluation.phase !== PurchaseEvaluationPhase.TraLai // ← THÊM +``` + +Chỗ 2 (button label `isSendBack` L205-207) đã đúng từ S17, KHÔNG đụng. + +Verify: `npm run build` × 2 app pass (fe-user 17.91s + fe-admin 6.71s, 0 TS6 err). + +### Chunk C — Docs (this commit) + +- `docs/gotchas.md` +#45 PE button label vs decision payload mismatch (~120 dòng narrative + 2 commit cross-ref + pattern reusable + phòng tránh tương lai) +- `docs/gotchas.md` checklist debug +entry 22 quick lookup +- `docs/STATUS.md` Last updated S21 t3 + count 81→84 test + 44→45 gotcha +- `docs/HANDOFF.md` TL;DR S21 t3 narrative đầy đủ (file này) +- `docs/changelog/sessions/2026-05-12-2100-s21-turn3-fix-tra-lai-bug45.md` session log mới + +### Pending (carry from S21 turn 2) + +Plans A-I unchanged. Plan C1 (test regression gotcha #44 silent 403 S18) vẫn còn nợ — không bundle với S21 t3 fix (scope khác, ưu tiên unblock UAT bug critical trước). + +### Audit cadence + +- Lần gần nhất: 2026-05-04 manual trễ 4 ngày +- Lần kế: **2026-06-01** combined audit +- Drift sau S21 t3: 44→45 gotcha (+1) + 81→84 test (+3) + 17→17 memory (no new) + 6 skills unchanged + +### State chốt S21 turn 3 + +| Metric | Trước (S21 t2) | Sau (S21 t3) | Δ | +|---|---|---|---| +| DB tables | 59 | 59 | 0 | +| Migrations | 27 | 27 | 0 | +| Endpoints | ~142 | ~142 | 0 | +| FE pages | 34 | 34 | 0 | +| **Unit tests** | 81 | **84** | **+3** (PE guard) | +| **Gotchas** | 44 | **45** | **+1** (#45) | +| Memory entries | 17 | 17 | 0 | +| Skills | 6 | 6 | 0 | +| Sub-agents | 4 seeds-only | 4 seeds-only | 0 | +| **Commits S21 t3** | — | **3** | (`de00887` + `4b29d00` + this) | + +--- + ## TL;DR Session 21 turn 2 — RAG Hybrid setup planning (Cách A chốt + 3-layer pattern) User clarify 5 dự án future > 1M MD tokens → cuộc thảo luận deep ~15 turn về RAG infrastructure. Em main solo (no SOLUTION_ERP sub-agent spawn), delegate 2 lần claude-code-guide agent research Anthropic + community practice. diff --git a/docs/STATUS.md b/docs/STATUS.md index ceeef54..68dd7d8 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,13 +2,14 @@ > **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`. -**Last updated:** 2026-05-12 1800 (Session 21 turn 2 — **🎯 RAG Hybrid setup planning + Cách A validation deep dive. 2 commit (`1f8e9af` plan save 1223 LOC + this chốt). Em main solo (no SOLUTION_ERP sub-agent spawn), delegate claude-code-guide × 2 research Anthropic + community practice. Decision chốt: Cách A defensive (giữ blanket 120K em main + RAG retrieve supplement) over Cách B aggressive (cắt 60-70% blanket). Industry-validated cross 4 Anthropic blog + 5 community tools (Cursor/Continue/Cline/Aider all hybrid). Stack: Voyage-3-large + Qdrant local + FastMCP Python + Streamlit dashboard 7 pages + SQLite event log. Multi-agent cost reality: 4 agents → ~520K cumulative blanket → heavy session ~560K (Cách A) vs ~700K (lazy), saving -20%. 3-layer pattern Phase 1-3 rollout (Layer 1 vector → Layer 2 +BM25 → Layer 3 +reranking, recall ~70% → ~92%). Stats: +1 memory entry (`feedback_rag_hybrid_pattern.md`) +1 plan file (`rag-setup-plan.md` 1500 LOC final). 4 sub-agents vẫn seeds-only. Plan I NEW deferred chờ bro confirm 5 dự án path + stack + Voyage API key + disk cleanup 5-8GB.**) +**Last updated:** 2026-05-12 2100 (Session 21 turn 3 — **🎯 Bug fix CRITICAL "Trả về nhưng hệ thống vẫn duyệt" PE workflow (gotcha #45). 2 chunk per-commit `de00887` (BE Chunk A) + `4b29d00` (FE Chunk B) + Chunk C Docs this. Root: PeWorkflowPanel.tsx `isReject` payload (L64-66) thiếu nhánh TraLai → button "← Trả lại" gửi `decision: 1` (Approve) thay vì `2` (Reject) khi target=TraLai(98) → BE ApproveV2Async UPSERT opinion "đã duyệt" + advance Cấp. Inconsistency phụ: dialog `isSendBack` (L247-248) cùng pattern thiếu TraLai → dialog title sai. Fix BE defense-in-depth + FE 3 chỗ × 2 app mirror rule §3.9. Test-before §7 BẮT BUỘC: 3 regression test mới (2 reproduce bug + 1 happy path control) — `dotnet test SolutionErp.slnx` 84 PASS (58 Domain + 26 Infra = +3). `npm run build` × 2 app pass. Stats: 27 mig (no change) · 59 tables · ~142 endpoints · 34 FE pages · **84 test pass (+3)** · **45 gotcha (+1 #45)** · 17 memory entries (no new) · 6 skills. Em main solo (no sub-agent spawn S21 t3 — bug fix reasoning chain cross BE/FE Implementer REFUSE per multi-agent rule).**) +**S21 turn 2:** 2026-05-12 1800 (Session 21 turn 2 — **🎯 RAG Hybrid setup planning + Cách A validation deep dive. 2 commit (`1f8e9af` plan save 1223 LOC + this chốt). Em main solo (no SOLUTION_ERP sub-agent spawn), delegate claude-code-guide × 2 research Anthropic + community practice. Decision chốt: Cách A defensive (giữ blanket 120K em main + RAG retrieve supplement) over Cách B aggressive (cắt 60-70% blanket). Industry-validated cross 4 Anthropic blog + 5 community tools (Cursor/Continue/Cline/Aider all hybrid). Stack: Voyage-3-large + Qdrant local + FastMCP Python + Streamlit dashboard 7 pages + SQLite event log. Multi-agent cost reality: 4 agents → ~520K cumulative blanket → heavy session ~560K (Cách A) vs ~700K (lazy), saving -20%. 3-layer pattern Phase 1-3 rollout (Layer 1 vector → Layer 2 +BM25 → Layer 3 +reranking, recall ~70% → ~92%). Stats: +1 memory entry (`feedback_rag_hybrid_pattern.md`) +1 plan file (`rag-setup-plan.md` 1500 LOC final). 4 sub-agents vẫn seeds-only. Plan I NEW deferred chờ bro confirm 5 dự án path + stack + Voyage API key + disk cleanup 5-8GB.**) **S21 turn 1:** 2026-05-12 0030 (Session 21 turn 1 — **🎯 Add con thứ 4 cicd-monitor (Path A — post-deploy verifier green READ tier). 1 commit `f1c61c9` pushed `36e21c8..f1c61c9 main -> main`, CI skipped per path filter (`**/*.md` paths-ignore docs-only). Trade-off: +~150K spawn extra mỗi run, đổi lại catch deploy ship fail tự động (bundle hash unchanged / mig drift prod / endpoint 500) — recurring blind spot pattern em main solo S20 quên verify ~30% push. Cost reality update: ~750K spawn setup (3 → 4 agents) · ~1.35M heavy session · ~700K optimized cached. Stats: 4 sub-agents seeds-only (+1 cicd-monitor green) · 16 memory entries (no new, update existing `feedback_multi_agent_setup.md` 3 → 4 agents narrative) · 27 mig · 59 tables · ~142 endpoints · 81 test unchanged · 44 gotcha unchanged · 6 skills unchanged. KHÔNG flush 3 agent MEMORY.md (chưa spawn work S21 t1 nên KHÔNG có findings — em main solo via context + Write file).**) **S20 wrap:** 2026-05-11 22:00 (Session 20 wrap turns 1-12 — **🎯 14 commit `9dee00d` → `ae1814c`. PE Detail UI restructure 3 yêu cầu (t1-5) + Manual budget drop tên (t6) + Mig 27 admin menu eOffice (t7) + NCC palette 5-màu cycle + Winner icon ✓ đậm + AddSupplier auto-fill master + Responsive laptop nhỏ 4-tầng pattern (t8-11) + Multi-agent infrastructure setup 3 sub-agents (t12). 27 mig (+1) · 59 tables · ~142 endpoints (+1) · 34 FE pages (+1) · 61 menu key (+1) · 81 test pass unchanged · 44 gotcha · 16 memory entries (+2) · 3 sub-agents NEW. Phase 9 UAT iteration mode.**) **S20 turn 7:** 2026-05-11 17:00 (Session 20 turn 7 — **🎯 Admin Ẩn/Hiện + Đổi tên menu eOffice (Mig 27). 5 chunk `2ea2d27`→`ef394f8`→`059bfcb`→`1ed6530`→Chunk E Docs. User Q2=b: DisplayLabel CHỈ áp fe-user, admin sidebar giữ Label gốc. Domain MenuItem +IsVisible(true) +DisplayLabel(200). Mig 27 AddVisibilityAndDisplayLabelToMenuItems. BE PATCH /api/menus/{key} [Authorize Policy=Permissions.Update]. NEW FE-admin MenuVisibilityPage ~210 LOC (table inline edit per-row + Save dirty + Khôi phục mặc định + Toggle Eye/EyeOff + 4 StatCard). fe-user Layout filterForUser 2 tầng (USER_HIDDEN_KEYS hardcode + !isVisible dynamic) + effectiveLabel(displayLabel || label) replace 3 callsite. fe-admin Layout KHÔNG đụng. +1 menu key MenuVisibility "Menu eOffice" leaf System Order=94. 27 mig, 59 tables, ~142 endpoints, 34 FE pages, 81 test pass (Q4 UAT defer).**) **S20 prev:** 2026-05-11 (Session 20 — **🎯 PE Detail UI restructure 3 yêu cầu user UX. 4 chunk per-commit `9dee00d` → `2bba851` → `f2f01f4` → (current Chunk D Docs).** Q1=a (giữ Section "Chọn NCC TP" riêng), Q2=a "1 hạng mục trước tiên" (NCC shared, demo 1 hạng mục), Q3=a (chỉ hiện NV đã ký), Q4 public luôn (skip dotnet test mỗi chunk theo memory `feedback_uat_skip_verify`, vẫn `npm run build` × 2 app mỗi chunk vì có rename/remove function). **Chunk A (`9dee00d`)**: BE `CreatePurchaseEvaluationCommandHandler` thêm 1 PurchaseEvaluationDetail mặc định khi tạo phiếu — GroupCode="01", GroupName="Hạng mục chính", NoiDung=TenGoiThau, DonGiaNganSach=ThanhTienNganSach=Budget.TongNganSach hoặc BudgetManualAmount fallback 0; Changelog Insert audit. FE reorder PeDetailTabs (mirror 2 app) 1.Thông tin / 2.Hạng mục (lên #2) / 3.Chọn NCC / 4.NCC tham gia / 5.Ý kiến. **Chunk B (`2bba851`)**: ItemsTab restructure thành list `HangMucCard` (1 card / 1 hạng mục, expanded=true mặc định cho 1 hạng mục demo). Header card: GroupCode + NoiDung + 3 stat (KL/ĐG/TT) + NS link Δ nếu có + Pencil/Trash actions + ▼/▶ toggle expand. Expand body: NCC inline table columns NCC / Liên hệ / Điều khoản TT / **File báo giá** / ĐG chưa VAT / ĐG có VAT / Thành tiền / Action. Quote inline click cell → QuoteDialog cũ reuse. Add NCC + Sửa NCC reuse AddSupplierDialog/EditSupplierDialog cũ. Winner ✓ button mỗi NCC row. Drop function `SuppliersTab` (dead code ~134 LOC, replace bằng HangMucCard expand panel). Giữ AddSupplierDialog + EditSupplierDialog + SupplierAttachmentsCell (HangMucCard call lại). Section layout cuối: 1.Thông tin / 2.Hạng mục + Báo giá NCC (nested) / 3.Chọn NCC TP thắng thầu / 4.Ý kiến cấp duyệt — 4 section. **Chunk C (`f2f01f4`)**: Section Ý kiến restructure render layer (KHÔNG đụng Mig 26 schema — vẫn UPSERT 1 row / Level). LevelOpinionsSectionV2 forEach step → 1 `StepOpinionsBox` (replace grid-cols-2 cho N approver). Box header: "Bước N — Tên" + dept badge emerald + "X/Y đã duyệt" counter. Body: filter opinions theo step.order → sort levelOrder asc, signedAt asc → render `StepOpinionEntry` per signed opinion (tên NV + Cấp badge slate + admin override badge amber nếu có + emerald rounded-full timestamp + comment text). NV chưa duyệt KHÔNG hiển thị (Q3=a). Drop function `LevelOpinionBox` (replaced). Mirror fe-admin + fe-user. Verify build pass cả 2 app sau khi catch TS6133 `SuppliersTab` + `SupplierAttachmentsCell` unused (đã giải quyết: drop SuppliersTab, restore SupplierAttachmentsCell vào HangMucCard cột "File báo giá"). 81 test pass (no change — UAT defer)**) -## 📍 Phase hiện tại: **Phase 9 active — UAT V2 testing với user thật** — **59 DB tables (+1 PurchaseEvaluationLevelOpinions Mig 26), 26 migrations (+1 Mig 26), ~141 API endpoints (no new — UPSERT auto qua Service hook không endpoint riêng, Q1=1B), 33 FE pages. 81 unit test pass** (58 Domain + 23 Infra — no change S19, feature UAT defer test theo §7). 44 gotcha. 30 demo user + 1 test user UAT. 6 skill. **5 trạng thái phiếu** (Nháp/Đã gửi duyệt/Trả lại/Từ chối/Đã duyệt). **2 Workflow schemas đồng tồn tại** post-Session 17: (1) Mig 21 `WorkflowDefinition` flat (V1) — pin với PE/Contract cũ + match Dept+PositionLevel. (2) Mig 22-26 `ApprovalWorkflow` (V2) — pin với PE mới + match ApproverUserId 1-1, Steps/Levels group by Order, Bước (Phòng) > Cấp (N NV OR-of-N), Mig 25 +IsUserSelectable admin pin per version, **Mig 26 +PeLevelOpinions sign-off dynamic theo Level**. Service PE branch theo `ApprovalWorkflowId` set or null. Sau UAT chốt → migrate + drop V1 + Contract V2 wire. +## 📍 Phase hiện tại: **Phase 9 active — UAT V2 testing với user thật** — **59 DB tables (+1 PurchaseEvaluationLevelOpinions Mig 26), 27 migrations (+1 Mig 27 menu visibility S20 t7), ~142 API endpoints (+1 PATCH /menus/{key}), 34 FE pages (+1 MenuVisibilityPage). 84 unit test pass** (58 Domain + 26 Infra = 23 baseline + 3 PE guard S21 t3 fix gotcha #45). **45 gotcha (+1 #45 PE button label vs decision payload mismatch S21 t3)**. 30 demo user + 1 test user UAT. 6 skill. **5 trạng thái phiếu** (Nháp/Đã gửi duyệt/Trả lại/Từ chối/Đã duyệt). **2 Workflow schemas đồng tồn tại** post-Session 17: (1) Mig 21 `WorkflowDefinition` flat (V1) — pin với PE/Contract cũ + match Dept+PositionLevel. (2) Mig 22-26 `ApprovalWorkflow` (V2) — pin với PE mới + match ApproverUserId 1-1, Steps/Levels group by Order, Bước (Phòng) > Cấp (N NV OR-of-N), Mig 25 +IsUserSelectable admin pin per version, **Mig 26 +PeLevelOpinions sign-off dynamic theo Level**. Service PE branch theo `ApprovalWorkflowId` set or null. Sau UAT chốt → migrate + drop V1 + Contract V2 wire. ### 🌐 Production URLs diff --git a/docs/changelog/sessions/2026-05-12-2100-s21-turn3-fix-tra-lai-bug45.md b/docs/changelog/sessions/2026-05-12-2100-s21-turn3-fix-tra-lai-bug45.md new file mode 100644 index 0000000..9425fd7 --- /dev/null +++ b/docs/changelog/sessions/2026-05-12-2100-s21-turn3-fix-tra-lai-bug45.md @@ -0,0 +1,182 @@ +# Session 21 turn 3 — 2026-05-12 21:00 — Bug fix CRITICAL "Trả về nhưng hệ thống vẫn duyệt" PE workflow (gotcha #45) + +**Dev:** Claude Opus 4.7 1M Max (em main solo, no SOLUTION_ERP sub-agent spawn) +**Duration:** ~1.5h diagnose + fix + test + docs +**Base commit:** `0a3b747` (S21 t2 RAG planning chốt) +**Commits này turn:** `de00887` (BE Chunk A) → `4b29d00` (FE Chunk B) → this (Chunk C Docs) + +## Trigger + +User UAT 2026-05-12 21:00 screenshot button labeled `← Trả lại` trong PE Workflow Panel (menu "Duyệt") với caption thắc mắc "Trả về nhưng hệ thống vẫn duyệt" + yêu cầu "check lại nhé chỗ Duyệt NCC". + +Đây là bug CRITICAL data integrity — NV nhấn "Trả lại" vô tình "duyệt" phiếu sang Cấp tiếp theo + UPSERT opinion vĩnh viễn vào `PurchaseEvaluationLevelOpinions` (Mig 26). Khó rollback vì BE đã `SaveChangesAsync`. + +## Diagnose + +Em main solo (Implementer REFUSE per multi-agent rule — reasoning chain cross BE/FE+test tightly coupled). Grep `Trả lại|isReject|TraLai` trong fe-user/fe-admin → tìm 3 chỗ inconsistency trong `PeWorkflowPanel.tsx`: + +| # | Location | Logic | Bug? | +|---|---|---|---| +| 1 | L205-207 button `isSendBack` | include cả `DangSoanThao` lẫn `TraLai` → label `← Trả lại` đúng | ✅ no bug | +| 2 | L64-66 payload `isReject` | CHỈ check `DangSoanThao`, thiếu `TraLai` → gửi `decision: 1` (Approve) thay vì `2` | 🔴 BUG ROOT | +| 3 | L247-248 dialog `isSendBack` | CHỈ check `DangSoanThao`, thiếu `TraLai` → title fallback `'✓ Duyệt → Trả lại'` (sai semantic) + no amber warning | 🔴 BUG phụ | + +**BE side audit** `PurchaseEvaluationWorkflowService.TransitionAsync`: +- L51 `if (decision == Reject)` branch → handle BOTH TuChoi (set Phase=TuChoi) + TraLai (set Phase=TraLai, clear pointer). Correct. +- L97 `APPROVE STEP` branch khi `decision=Approve && fromPhase=ChoDuyet` → `ApproveV2Async` UPSERT opinion + advance Cấp pointer. +- → Khi FE gửi `decision=1` do bug `isReject` thiếu nhánh TraLai, BE entry → APPROVE branch thay vì REJECT branch → phiếu approve mặc dù user định trả lại. + +## Chunk A — BE defense-in-depth + 3 regression test (`de00887`) + +### Test-before §7 BẮT BUỘC strict flow + +**Step 1:** Write test file `tests/SolutionErp.Infrastructure.Tests/Services/PurchaseEvaluationWorkflowServiceGuardTests.cs` 3 test KHÔNG có BE guard → run → expect FAIL. + +**Step 2:** Confirm 2 test FAIL (reproduce bug — BE đi sâu vào ApproveV2Async throw "Phiếu chưa pin workflow definition hoặc workflow không có step") + 1 test PASS (happy path Reject branch — đã pass vì BE đã đúng cho decision=Reject từ Session 17). + +**Step 3:** Add BE guard sau L48, trước L51: + +```csharp +// ===== GUARD: targetPhase TraLai/TuChoi BẮT BUỘC decision=Reject ===== +// Defense-in-depth chặn FE inconsistency (gotcha #45 — Session 21 turn 3). +// Bug: button "← Trả lại" gửi decision=Approve khi target=TraLai → BE skip +// Reject branch → enter APPROVE STEP → ApproveV2Async UPSERT opinion. +if ((targetPhase == PurchaseEvaluationPhase.TraLai + || targetPhase == PurchaseEvaluationPhase.TuChoi) + && decision != ApprovalDecision.Reject) +{ + throw new ConflictException( + $"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " + + "Báo lỗi caller — payload mismatch giữa target phase và decision " + + "(xem gotcha #45 + docs/workflow-contract.md)."); +} +``` + +**Step 4:** Re-run test → 3/3 PASS. Run full suite → 84/84 PASS (58 Domain + 26 Infra = +3 from 81 baseline). + +### 3 test cases + +1. **`TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState`** — Bug reproduce. Setup PE ở Phase=ChoDuyet, CurrentApprovalLevelOrder=1. Act: gửi target=TraLai + decision=Approve. Assert: throw `ConflictException` "*TraLai*Reject*" + Phase KHÔNG đổi + CurrentApprovalLevelOrder=1 (no advance). +2. **`TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState`** — Consistency cover. Cùng pattern với TuChoi. +3. **`TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai`** — Happy path control. decision=Reject + target=TraLai → BE đi vào Reject branch, set Phase=TraLai, clear pointer (CurrentApprovalLevelOrder=null + CurrentWorkflowStepIndex=null + SlaDeadline=null). + ++ `NoOpNotificationService internal sealed` stub trong cùng file (Tests scope) — reusable cho future PE service tests, avoid `INotificationService` real DI complexity. + +### Pattern reusable +- **Boundary guard semantic invariant.** Bất kỳ BE service nào nhận payload từ FE → audit invariant `(domain state X) ⇔ (input parameter Y)` → throw early nếu mismatch. Defense-in-depth thay vì trust FE đúng. +- **Test-before flow strict:** Write test → confirm FAIL với exception KHÁC expected (proves bug reproduce) → add fix → confirm PASS với exception ĐÚNG expected. KHÔNG bỏ qua bước "confirm FAIL" — đảm bảo test actually catches bug. + +## Chunk B — FE fix mirror 2 app (`4b29d00`) + +3 chỗ × 2 app = 6 edits. + +### fe-user/src/components/pe/PeWorkflowPanel.tsx + +**Edit #1 (L64-66 `isReject`):** + +```typescript +const isReject = target === PurchaseEvaluationPhase.TuChoi + || (target === PurchaseEvaluationPhase.DangSoanThao + && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) + || (target === PurchaseEvaluationPhase.TraLai // ← THÊM nhánh TraLai + && evaluation.phase !== PurchaseEvaluationPhase.TraLai) +``` + +**Edit #2 (L247-248 dialog `isSendBack`):** + +```typescript +const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao + || target === PurchaseEvaluationPhase.TraLai) // ← THÊM TraLai + && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao + && evaluation.phase !== PurchaseEvaluationPhase.TraLai // ← THÊM guard +``` + +**Comment update:** Thêm context bug + cross-ref BE guard Chunk A trong comment. + +### fe-admin/src/components/pe/PeWorkflowPanel.tsx + +Mirror y hệt (rule §3.9 mirror 2 app — duplicate có chủ đích). + +### Verify + +```bash +# fe-user +cd fe-user && npm run build +✓ built in 17.91s + +# fe-admin +cd fe-admin && npm run build +✓ built in 6.71s +``` + +0 TS6 err, 0 new warnings. Warning chunk size pre-existing (KHÔNG introduced). + +## Chunk C — Docs (this commit) + +### `docs/gotchas.md` +#45 + +~120 dòng narrative đầy đủ KEEP rule §6.5: +- **Triệu chứng** — UAT screenshot user mô tả hành vi +- **Root cause** — 3 chỗ inconsistency table + BE service path narrative +- **Severity** CRITICAL — data integrity issue khó rollback +- **Fix Chunk A** BE code block + 3 test list +- **Fix Chunk B** FE code block diff +- **Pattern reusable** — boundary guard semantic invariant + button label ↔ payload sync +- **Phòng tránh tương lai** — grep audit khi spec mới thêm phase + test-before §7 strict flow +- **References** — 2 commit + Session 17 spec + +`docs/gotchas.md` checklist debug +entry 22 quick lookup. + +### `docs/STATUS.md` + +Edit Last updated header thêm S21 t3 + count 81→84 test + 44→45 gotcha. Giữ nguyên S21 t2/t1/S20 narrative cũ (rule §6.5 KEEP). + +### `docs/HANDOFF.md` + +Insert TL;DR S21 t3 section trên cùng (trước S21 t2). Header Last updated mới + narrative đầy đủ Chunk A/B/C + state table cumulative. Giữ S21 t2/t1/S20 narrative cũ. + +### Session log + +File này — `docs/changelog/sessions/2026-05-12-2100-s21-turn3-fix-tra-lai-bug45.md`. + +## Stats cumulative + +| Metric | Trước (S21 t2) | Sau (S21 t3) | Δ | +|---|---|---|---| +| DB tables | 59 | 59 | 0 | +| Migrations | 27 | 27 | 0 | +| Endpoints | ~142 | ~142 | 0 | +| FE pages | 34 | 34 | 0 | +| **Unit tests** | 81 | **84** | **+3** (PE guard) | +| **Gotchas** | 44 | **45** | **+1** (#45) | +| Memory entries | 17 | 17 | 0 | +| Skills | 6 | 6 | 0 | +| Sub-agents | 4 seeds-only | 4 seeds-only | 0 | +| Commits S21 t3 | — | **3** | (`de00887` + `4b29d00` + this) | + +## Lessons learned + +1. **Mảng inconsistency 3 chỗ cùng pattern** — khi spec mới thêm value vào set check (vd Session 17 thêm `TraLai` làm Phase RIÊNG thay vì DangSoanThao revert), DỄ QUÊN grep TOÀN BỘ logic check `=== OldValue` để thêm `|| === NewValue`. Tốt nhất extract helper function `isReject(target, currentPhase): boolean` share 1 nơi thay vì duplicate 3 chỗ. +2. **BE guard defense-in-depth có giá trị thực** — trong S21 t3, nếu BE chỉ trust FE đúng → bug ship prod, user UAT report, mất data integrity. BE guard early catch payload mismatch + ConflictException rõ ràng → fix nhanh + safe. +3. **Test-before flow strict không chỉ là "viết test" — còn confirm FAIL** — em main đầu tiên định viết test + fix cùng commit (cho gọn). Nhưng test-before §7 BẮT BUỘC confirm test FAIL trước fix. Bước này quan trọng — confirm test actually reproduce bug (assert đúng exception type/message), không chỉ là "test xanh sau fix". +4. **Multi-agent decision tree áp đúng** — Bug fix tightly coupled BE+FE+test reasoning chain → Implementer REFUSE per rule (Cognition "writes single-threaded"). Em main solo correct decision, KHÔNG cố split → tránh agent thrash. + +## Handoff + +- ✅ Chunk A `de00887` committed local — chưa push +- ✅ Chunk B `4b29d00` committed local — chưa push +- ✅ Chunk C (this) — committed sau khi save session log +- ⏭ **PENDING bro confirm push remote** — `git push origin main` 3 commit ahead `0a3b747..HEAD` +- ⏭ Sau push: CI sẽ trigger (NOT docs-only — có file `.cs` + `.tsx`) → 🟩 CICD Monitor spawn smoke verify (per plan G Trial Week 1) + +User next action expected: "fix đi rồi tao giao thêm task" → sau Chunk C wrap → bro chốt task tiếp theo (có thể là Plan B Contract V2 wire hoặc fix khác phát sinh UAT). + +## References + +- Gotcha #45: `docs/gotchas.md` +- BE service: `src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs` +- FE component (× 2 app): `{fe-admin,fe-user}/src/components/pe/PeWorkflowPanel.tsx` +- Test: `tests/SolutionErp.Infrastructure.Tests/Services/PurchaseEvaluationWorkflowServiceGuardTests.cs` +- Workflow spec Session 17: `PurchaseEvaluationPhase.cs` enum doc + Service comment L15-19 +- Rule §7 test-before: `docs/rules.md` +- Rule §3.9 mirror 2 FE: `docs/rules.md` diff --git a/docs/gotchas.md b/docs/gotchas.md index c9654e9..ba1b022 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -637,6 +637,76 @@ public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase **FE diagnostic improvement:** TanStack Query error nên hiển thị warning UI (toast hoặc banner) thay vì silent. Hiện tại `useQuery` catch silent → debug khó. Future: wire `onError` handler global show generic error toast. +### 45. PE "Trả về nhưng hệ thống vẫn duyệt" — FE button label vs decision payload mismatch (Session 21 turn 3) + +**Triệu chứng:** UAT 2026-05-12 — User bro screenshot button labeled `← Trả lại` trong PE Workflow Panel (menu "Duyệt"), nhấn vào nhưng phiếu KHÔNG về phase TraLai — ngược lại tiến qua Cấp tiếp theo (hệ thống ghi nhận approve). User mô tả hành vi: "Trả về nhưng hệ thống vẫn duyệt". + +**Root cause:** `PeWorkflowPanel.tsx` có 3 chỗ check transition type với logic KHÔNG sync giữa nhau: + +- **L205-207** `isSendBack` (button label color): include cả `DangSoanThao` lẫn `TraLai` từ phase trung gian → label hiển thị `← Trả lại` đúng. +- **L64-66** `isReject` (payload `decision` gửi BE): CHỈ check `DangSoanThao`, **thiếu `TraLai`** → khi target=TraLai (98), `isReject=false` → payload `decision: 1` (Approve) thay vì `2` (Reject). +- **L247-248** dialog `isSendBack` (title + warning): CHỈ check `DangSoanThao`, **thiếu `TraLai`** → dialog title fallback `'✓ Duyệt → Trả lại'` (sai semantic) + KHÔNG hiển thị amber warning "Phiếu sẽ về Đang soạn thảo". + +BE `PurchaseEvaluationWorkflowService.TransitionAsync`: +- L51 `if (decision == Reject)` branch → set Phase=TraLai correctly khi decision=Reject. +- L97 `APPROVE STEP` branch khi decision=Approve + fromPhase=ChoDuyet → `ApproveV2Async` UPSERT opinion = "đã duyệt" + advance Cấp. +- → Khi FE gửi `decision=1` (do bug `isReject`), BE đi vào nhánh APPROVE thay vì REJECT → phiếu được ghi nhận approve dù user định trả lại. + +**Severity:** 🔴 CRITICAL — data integrity issue. NV nhấn "Trả lại" sẽ vô tình "duyệt" phiếu sang Cấp tiếp theo + UPSERT opinion vĩnh viễn vào `PurchaseEvaluationLevelOpinions` (Mig 26). Khó rollback vì BE đã `SaveChangesAsync`. + +**Fix Chunk A (`de00887` BE defense-in-depth):** +```csharp +// PurchaseEvaluationWorkflowService.cs sau set isAdmin/isSystem (L48), trước REJECT branch (L51) +if ((targetPhase == PurchaseEvaluationPhase.TraLai + || targetPhase == PurchaseEvaluationPhase.TuChoi) + && decision != ApprovalDecision.Reject) +{ + throw new ConflictException( + $"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " + + "Báo lỗi caller — payload mismatch giữa target phase và decision."); +} +``` + +Boundary protection cho mọi caller tương lai (API client / mobile / cron retry). 3 regression test: +- `TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState` (bug reproduce) +- `TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState` (consistency cover) +- `TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai` (happy path control) + +**Fix Chunk B (`4b29d00` FE mirror 2 app):** +```typescript +// PeWorkflowPanel.tsx (fe-user + fe-admin) — 3 chỗ × 2 app + +// Chỗ 1: isReject payload (line 64-66) +const isReject = target === PurchaseEvaluationPhase.TuChoi + || (target === PurchaseEvaluationPhase.DangSoanThao + && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) + || (target === PurchaseEvaluationPhase.TraLai // ← THÊM + && evaluation.phase !== PurchaseEvaluationPhase.TraLai) + +// Chỗ 2: dialog isSendBack (line 247-248) +const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao + || target === PurchaseEvaluationPhase.TraLai) // ← THÊM + && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao + && evaluation.phase !== PurchaseEvaluationPhase.TraLai // ← THÊM +``` + +Chỗ 3 (button label `isSendBack` L205-207) đã đúng từ S17, KHÔNG đụng. + +**Pattern reusable — invariant check khi viết FE workflow transition:** +1. Button label condition (visual) phải SYNC với payload decision (semantic). +2. Dialog title/warning condition phải SYNC với button label + payload. +3. Tốt nhất: extract `isReject(target, currentPhase)` thành 1 helper FE + BE share semantic — KHÔNG duplicate logic giữa 3 chỗ. + +**Phòng tránh tương lai:** +- Khi spec mới có thêm phase terminal/intermediate (vd Session 17 thêm TraLai làm Phase RIÊNG thay vì DangSoanThao revert), audit grep TOÀN BỘ logic check `=== DangSoanThao` để xem chỗ nào cần thêm `|| === NewPhase`. +- BE guard early invariant `(targetPhase ∈ terminalSet) ⇔ (decision == Reject)` thay vì trust FE payload. +- Test-before bug fix BẮT BUỘC §7 — 3 test cover bug reproduce + consistency + happy path. + +**References:** +- Commit fix: `de00887` (BE Chunk A) + `4b29d00` (FE Chunk B) +- Spec Session 17: `feedback_n_stage_workflow_pattern` DEPRECATED + spec mới trong `PurchaseEvaluationWorkflowService.cs` comment L15-19 +- State machine 5 trạng thái: Nháp / Đã gửi duyệt / **Trả lại (98) — Phase RIÊNG** / Từ chối / Đã duyệt + ## Checklist debug bug mới 1. Build pass không? → fail → check using + package version compat @@ -660,3 +730,4 @@ public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase 19. Nếu npm install caching fail `tsc not found` → KHÔNG dùng junction Move-Item, thử robocopy/Copy-Item (#40) 20. Nếu CI vẫn trigger khi commit MD-only → paths-ignore trong on:push không match patterns đúng (#41) 21. Nếu user phàn nàn "feature work cho admin nhưng user empty/403 silent" → check class-level Authorize policy có over-restrict cho non-admin không, split per action (#44) +22. Nếu button workflow label nói "Trả lại" nhưng phiếu vẫn tiến approve → audit FE `isReject` payload condition vs button `isSendBack` label condition vs dialog `isSendBack` warning condition — phải sync 3 chỗ với CÙNG set target phase. BE thêm guard `(target ∈ terminalSet) ⇔ (decision=Reject)` chặn caller mismatch (#45)