[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:
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user