[CLAUDE] Docs: Chunk C — chốt Session 21 turn 3 fix gotcha #45 PE button "Trả lại" mismatch

Add gotcha #45 narrative đầy đủ ~120 dòng KEEP rule §6.5:
- Triệu chứng UAT screenshot + user mô tả "Trả về nhưng hệ thống vẫn duyệt"
- Root cause 3 chỗ inconsistency table + BE service path
- Severity CRITICAL data integrity
- Fix Chunk A BE code + 3 test list
- Fix Chunk B FE code diff × 2 app
- Pattern reusable (boundary guard semantic invariant) + phòng tránh tương lai
- References 2 commit + Session 17 spec

+ gotchas.md checklist debug entry 22 quick lookup.

Update STATUS.md Last updated header + count 81→84 test + 44→45 gotcha.
Insert HANDOFF.md TL;DR S21 t3 đầy đủ Chunk A/B/C + state cumulative.
New session log docs/changelog/sessions/2026-05-12-2100-s21-turn3-fix-tra-lai-bug45.md.

Verify:
- 84 test PASS (dotnet test SolutionErp.slnx — Chunk A persisted)
- npm run build × 2 app pass (Chunk B persisted)
- KHÔNG paraphrase / KHÔNG cắt narrative cũ S21 t1/t2/S20 (rule §6.5 KEEP)

Pending: bro confirm push remote `0a3b747..HEAD` 3 commits ahead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 09:46:52 +07:00
parent 4b29d00716
commit 6d30ba42d1
4 changed files with 362 additions and 3 deletions

View File

@ -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)