Lo hong (reviewer S118 phat hien khi phan quyen PRO tra.bui): ReportsController chi co
[Authorize] tran (khong policy) -> MOI user dang nhap goi duoc GET /reports/dashboard (tong gia
tri HD dang chay, top NCC/du an theo gia tri, xu huong 12 thang) + GET /reports/contracts/export
(xuat toan bo HD ra Excel). Nguoc quyet dinh S92 "HD chi Admin thay" (menu an nhung API van mo,
go URL/API truc tiep lay duoc).
Fix (owner chot: chi Admin qua Reports.Read policy -- linh hoat mo role khac qua ma tran sau):
- Dashboard() + ExportContracts() -> them [Authorize(Policy = "Reports.Read")] per-method. Hien
chi Admin co Reports.Read -> non-admin 403. Admin bypass moi policy nhu cu.
- MyDashboard() GIU MO (chi [Authorize] class-level) -- scope theo currentUser, moi user chi thay
so cua CHINH MINH (draft/pending/SLA/gia tri draft cua minh), khong lo toan cong ty.
Test: +4 reflection authz regression (AuthorizePolicyRegressionTests, mirror gotcha #44 S45):
dashboard/export co Reports.Read; my-dashboard KHONG co action-policy (guard chong ai khoa nham
dashboard ca nhan). 505 -> 509. Gotcha #82 (menu-flag grant != quyen API that).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hai chỉnh từ buổi UAT anh Kiệt FDC (S117):
1) Fix hiển thị lũy kế "duyệt rồi không bắt" — root cause là DISPLAY-LIE, không phải
lũy kế bug. endsBeforeCeo cho phiếu CHƯA DaDuyet nay POINTER-AWARE: chỉ báo "kết
thúc trước CEO" khi con-trỏ còn ở/trước cấp finalize. Phiếu đã đi QUA cấp finalize
(approver bỏ tick "Cấp này KẾT THÚC" → trình tiếp CEO) → false: sơ đồ hết làm mờ
Bước CEO + hết nói dối "Kết thúc tại Cấp X" khi phiếu đang chờ CEO. 3 site (detail
in-memory + list/inbox EF-subquery). Filter lũy kế GIỮ nguyên (đúng nguyên tắc
"duyệt rồi mới bắc"). BE-only — FE là consumer thuần nên tự render đúng.
2) Feature "duyệt theo KHOẢNG Min–Max": khi PRO có cả Min & Max, người duyệt cấp cuối
chọn CẢ HAI (radio "PRO — cả Min và Max", source=ProMinMax) → Min ở ApprovedPriceAmount
+ Max ở ApprovedPriceMaxAmount (Mig 66 AddColumn nullable, no backfill, phiếu cũ
nguyên vẹn). Bất-biến Max⟺ProMinMax, Min≤Max (guard ở service + validator). HĐ.GiaTri
KHÔNG đọc giá chốt (= SUM báo giá NCC được chọn) → range thuần record-of-decision
hiển thị, 0 tác động HĐ. FE 2-app SHA-mirror (Panel + DetailTabs).
Test 495 → 501 (+1 pointer-aware repro, +5 ProMinMax). Cả 2 điều tra qua /fable-real
engine-đắt (investigator-codebase deep-pass) theo yêu cầu anh Kiệt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Go-live request anh Kiệt FDC (Zalo "user chưa tự đổi pass được hả em"): trước đây chỉ admin
Reset hộ (POST users/{id}/reset-password), user KHÔNG tự đổi được. Thêm self-service:
- BE: ChangePasswordCommand + POST /api/auth/change-password ([Authorize]). Lấy user qua
ICurrentUser.UserId; UserManager.ChangePasswordAsync verify mật khẩu HIỆN TẠI; sai →
ValidationException field CurrentPassword "Mật khẩu hiện tại không đúng."; rule mật khẩu mới
≥ 12 + khác mật khẩu cũ (validator); sau đổi vô hiệu refresh token (mirror ResetPassword).
Qualify SolutionErp...ValidationException (tránh CS0104 clash với FluentValidation).
- FE 2 app (SHA-mirror): ChangePasswordDialog (3 ô current/new/confirm + validate client:
≥12, khớp, khác cũ) wire vào menu tài khoản TopBar — "Đổi mật khẩu" trên "Đăng xuất".
- Test: +ChangePasswordCommandTests (đúng/sai pass cũ keyed-field + msg, <12 boundary, ==cũ,
chưa-auth/UserId-null/user-not-found, RefreshToken cleared). Full suite 431 PASS (45D + 386I).
Build: BE 0 warn/0 err · fe-user + fe-admin build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the Mig 58 multi-winner (>=2 cung trung) wiring — the audit's joint-winner
dead-ends, CEO-approved scope ("1 HD per winner"):
- create-contract 1->N: one draft HD per IsWinner supplier (was 409 dead-end for >=2).
Command -> List<Guid>. ContractCodeGenerator self-commits its sequence per call, so
gen all N codes first (contracts built local) then atomic add + single final
SaveChanges (no partial create on failure). GiaTri: single = details ThanhTienNganSach
sum (legacy preserved); >=2 = per-winner quote total (DangSoanThao draft, editable —
anh Kiet UAT the basis). pe.ContractId = first (idempotency). Controller -> {contractIds}.
- list-card winner names: PurchaseEvaluationListItemDto +WinnerSupplierNames (EF
collection subquery over IsWinner suppliers; translates + runs on SQLite — test-verified).
FE PeListPanel shows joined names for >=2 (was blank). Dialog -> contractIds[] + "Da tao N HD".
Tests 413 -> 419 (+6: multi-contract 5 [per-winner GiaTri 140/260 vs single legacy 800,
idempotency, 0-winner, not-DaDuyet] + EF-subquery translation smoke). Full suite GREEN.
FE 2-app SHA-identical. reviewer PASS (codegen-sequencing, positional-record arg-order,
no-partial-create all independently verified).
UAT (anh Kiet): open a >=2-winner phieu -> "Tao HD" creates N draft contracts (1/winner);
confirm per-winner contract GiaTri (quote total) is the wanted basis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CCM (Lưu Trần / anh Kiệt FDC) — làm 3/4 mục (mục 3 "CCM sửa ngân sách" để riêng):
1. Multi-winner: chọn NHIỀU NTP/NCC/ĐVDV cùng trúng → winnerQuoteTotal = SUM
ThanhTien MỌI đơn vị isWinner (vd 3 ông 37.8+9+10 = 56.8tr). Cột mới
PurchaseEvaluationSupplier.IsWinner (Mig 58, backfill từ SelectedSupplierId).
SelectWinner → multi-toggle SupplierIds[]. FE: bảng so sánh tick nhiều ô +
dòng "Tổng chi phí đã chọn"; mọi guard/checklist/computeGiaChaoThau/tạo-HĐ
đổi sang isWinner (selectedSupplierId = null khi liên-danh ≥2).
2. Ngân sách = 0 hợp lệ: nới submit-guard + 3 validator (chỉ chặn khi NULL,
số 0 cố ý đi qua; fallback ProInitial chỉ khi null). Mirror BE + FE.
3. Cấp KẾT THÚC quy trình: ApprovalWorkflowLevel.AllowApproverFinalize (Mig 58)
— admin bật per-cấp trong Workflow Designer; duyệt tới cấp đó TỰ DaDuyet,
bỏ qua CEO (AUTO theo cấu hình quy trình, không ô-tích runtime — anh Kiệt
chốt). FE: banner cảnh báo + bắt chọn giá chốt.
Mig 58 AddPeMultiWinnerAndLevelFinalize (2 AddColumn bool + backfill IsWinner,
additive/reversible). Test 377→395 (+PeMultiWinnerTests +PeApproverFinalizeTests
+budget=0; auto-model gọn bớt 2 test bypass/opt-out). FE 2 app SHA-mirror.
Build sạch BE + 2 FE local.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Theo Tra Sol + anh Kiet FDC (Zalo): A) o nhap tien VndInlineEdit + BudgetCell nhay phan cach vi-VN (300.000.000) on-keystroke (o dialog da co san). B) them o ghi chu PRO + CCM canh nut Luu trong khoi Gia de xuat (giai thich vi sao chon Min/Max) — Mig 57 AddPeSuggestedPriceNotes (+ProSuggestedPriceNote +CcmSuggestedPriceNote nvarchar(1000) null, additive no-backfill no-table); 2 setter command +Note absolute-set rides role-gate PRO/CCM/Admin; DTO +2 field; controller body +note. C) sua chinh ta 'd. Ban so sanh' -> 'd. Bang so sanh gia' (2 app). GUARD gotcha #70: o gia + ghi chu echo nhau absolute-set -> them peFetching khoa nut Luu toi khi pe-detail refetch land, tranh stale-echo mat du lieu (mirror bang ngan sach S76; em-main review bat impl-frontend sot guard). BE slnx 0-warn 0-err; FE build PASS x2; test 344->351 (+7); PeDetailTabs/PeWorkspaceCreateView 2 app SHA256-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
S76 (anh Kiet FDC + chi Tra Sol) — form ngan sach + hien thi quyen nhap NS trong flow:
- Part 1: form ngan sach -> MA TRAN 3 cot, moi phong nhap+dieu chinh cot minh
(PRO canEditPro / CCM canEditCcm / Du an FE hien-thi-only). Mig 56
+ProInitialAmount/ProAdjustmentAmount (additive-nullable + data-migrate
ProEstimate->ProInitial). full moi cot = ban hanh + hieu chinh.
- Part 2: Workflow Designer (fe-admin) +badge "NS PRO/CCM" canh approver
(suy tu role Admin|Procurement / Admin|CostControl, hien-thi-only no-authz).
- Part 3: flow quy trinh fe-user/fe-admin (Duyet NCC) +badge tuong tu.
- Fix race mat-du-lieu Part 1 (useIsFetching khoa Luu khi refetch — dong
cua-so stale-echo, reviewer Part2/3 bat).
- Test 339->344 (+5). 2 workflow review (Part 1 PASS + Part 2/3 PASS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Theo note anh Kiệt FDC (go-live so-sánh-giá thứ Hai):
- (1) Giá chào thầu thêm giá đề xuất NGOÀI giá NCC: PRO nhập dải Min/Max +
CCM nhập 1 giá (2 lệnh role-gate Procurement/CostControl, fail-closed).
Khi duyệt cấp cuối, người duyệt CHỌN 1 giá chốt (Ncc/ProMin/ProMax/Ccm)
-> luu ApprovedPriceAmount/Source (bind tai moi nhanh DaDuyet, bat buoc
chon; auto-approve he thong mien).
- (3) CCM duyet-done mien CEO: DOI tu AUTO-threshold (S69) sang O-TICH-TAY
(finalizeByCcmDelegation) -- CCM chu dong tich, fail-closed theo nguong
+ role + gia goi. An toan hon (khong vo tinh bo CEO).
- Mig 54 additive-nullable (5 cot PE) - FE 2 app SHA-mirror - test 306->334
(+28: opt-in 6->11, +10 gia chot, +13 setter authz).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Yêu cầu anh Kiệt FDC (sau họp sếp). Mig 53 AddPeUrgentAndCeoApprovalThreshold — 3 AddColumn, no new table (Mig 52→53). Rollout an toàn: cột nullable, ngưỡng null = giữ luồng duyệt cũ 100% cho tới khi admin set.
B — CCM duyệt-final theo NGƯỠNG GIÁ TRỊ ("gói CEO phân quyền theo giá trị"):
- ApprovalWorkflow += CeoApprovalThreshold (decimal?, admin nhập trong Workflow Designer).
- ApproveV2Async: actor role CostControl (CCM) + winnerQuoteTotal (tổng giá NCC được chọn) < ngưỡng → DaDuyet luôn (bỏ CEO); ≥ ngưỡng → đẩy lên CEO như cũ. Ngưỡng null = luồng tuyến tính cũ. Q4 chốt nhận diện theo ROLE người duyệt.
- reviewer PASS 0 blocker: cascade-safe (Off/role không lan), tested load-bearing (CCM dưới ngưỡng → DaDuyet skip CEO).
A — cờ gấp per-vai (visibility-only, Q3 KHÔNG đổi luồng):
- PE += IsUrgentByPro (PRO đỏ) / IsUrgentByCcm (CCM xanh).
- Endpoint PUT /purchase-evaluations/{id}/urgent role-gated (Procurement→ByPro, CostControl→ByCcm, Admin→cả 2, khác→Forbidden) + notify CEO (Director) khi MỚI bật (best-effort).
FE ×2 app: Workflow Designer ô "Ngưỡng giá trị gói CEO" (fe-admin) + PE detail nút bật/tắt cờ gấp đỏ/xanh theo role + badge GẤP + hint "giá trị gói vs ngưỡng → CCM duyệt-final/cần CEO" + PE list badge gấp.
DTO: PE detail += isUrgentByPro/Ccm + winnerQuoteTotal + ceoApprovalThreshold; list += isUrgentByPro/Ccm; workflow V2 += ceoApprovalThreshold.
+14 test (292→306): PeCcmThresholdFinalizeTests 5 (B routing) + PeUrgentToggleAuthzTests 9 (A authz). Build slnx 0/0 · npm build ×2 0 err · dotnet test 306 PASS.
C (sau duyệt xong chuyển phiếu đến dự án) — chờ anh Kiệt làm chi tiết form, CHƯA làm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nền cây tổ chức cho trang Hồ sơ Nhân sự (anh: bố trí giống NamGroup; chốt phân
cấp thật + tự gán phòng cha trong quản lý phòng ban).
- Mig AddDepartmentParentId: +ParentId Guid? loose-Guid (no FK vật lý, convention
PE) + IX_Departments_ParentId. AddColumn+CreateIndex, no new table, Down reversible
(DropIndex->DropColumn). Chưa apply (CI/prod seed apply).
- GET /api/departments/tree: ráp cây in-memory + đếm NV active theo User.DepartmentId
(EmployeeProfile KHÔNG có DepartmentId — link qua User, field phòng ban ở User Mig 11)
+ rollup TotalEmployeeCount + cycle-guard HashSet. Authz = class [Authorize] (= GET list).
- Create/Update +ParentId (write vẫn khóa Admin,CatalogManager). Update có cycle-guard:
chặn tự-làm-cha + đi ngược chuỗi cha gặp lại Id (chống vòng A->B->A).
Build PASS (0 warn/err full solution). Test-after (test-specialist).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: bang moi PeWorkItemBudgets (1 record/cap Du an x Hang muc, UNIQUE filtered [IsDeleted]=0) + drop 5 bang Budget cu + PE/Contracts drop BudgetId + backfill BudgetManualAmount->BudgetPeriodAmount TRUOC DropColumn (phieu UAT giu so) + DELETE menu/permission Bg_* IN-list children-first
- BE: PUT {id}/budget/pro (role Procurement) + {id}/budget/ccm (role CostControl, Adjustment cho phep AM) fail-closed Forbidden-truoc-side-effect + EnsureTrackedAsync race-safe (catch unique -> re-fetch winner, loi khac rethrow) + auto-create record khi tao phieu + budgetSummary DTO (luy ke trinh-truoc/chon-thau-truoc/de-xuat-ky-nay + full fallback du-tru-PRO + canEdit flags) + submit-guard (3) doi predicate BudgetPeriodAmount -> "chua nhap Ngan sach ky nay" + PATCH budget-adjust absolute-set 2 field moi + Contract GIU BudgetManual* (HD nhap tay khong doi) + ke thua HD map BudgetPeriodAmount
- FE x2 app SHA256 identical: bang "TONG HOP NGAN SACH TRINH KY" block A (full dam + ban hanh + V0 hieu chinh + du tru PRO + ghi chu, editable theo canEditPro/canEditCcm) + block B 9 dong cong thuc Excel (5=1+3, 6=2+4, 7=full-5, 8 tu nhap default 7, 9=4+8) + to mau vuot ngan sach #C00000 / am do / red-soft row8>row7 + "Chua chon" khi count=0 + banner phieu chua gan Hang muc + o "Ngan sach ky nay" o create/header + XOA pages/components/types budgets + routes + menuKeys + Layout staticMap 4-place
- Tests: +22 PeWorkItemBudgetTests (auto-create x3, ensure/race x2, authz matrix PRO x5 + CCM x3, budgetSummary aggregates x5, adjust x4) - 14 BudgetPolicyTests xoa theo module - 1 test via-BudgetId -> 263 PASS (45 Domain + 218 Infra, 0 fail)
- database-agent advise adopted: khong FK vat ly PE/Contracts->Budgets (DropColumn khong can DropForeignKey) + DropIndex truoc DropColumn (SQL 5074) + IN-list thay LIKE Bg_% (underscore wildcard + miss root) + khong Serializable wrap (nested-tx conflict codegen)
- Reviewer PASS-with-minor 0 blocker (verdict-first survived); 2 minor da sua truoc commit (comment adjustMut absolute-set + dead key budgetId); note: F4 approver-edit-budget UI entry tam drafter-only, BE van cho approver scope - cho UAT anh Kiet
- Scaffold-bug caught: EF tu sinh RenameColumn BudgetManualAmount->ExpectedRemainingAmount (SAI semantics) -> thay bang Add+UPDATE+Drop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PeWorkflowPanel x2: nguoi duyet == nguoi soan (drafterUserId == currentUser.id)
-> an ca "Tra lai" + "Tu choi" (anh chot: tra cho chinh minh vo nghia, huy phieu
= nho cap khac tu choi / xoa phieu Nhap).
- SuppliersController: POST tao NCC mo cho moi user dang nhap (anh chot - nghiep vu
di thau phat sinh NTP moi lien tuc); PUT/DELETE van khoa Admin+CatalogManager (S57).
- PeDetailTabs AddSupplierDialog x2: Select -> SearchableSelect (go-tim bo dau,
sort A-Z theo ma) + nut "+ NCC moi" quick-create (Ma/Ten/Loai/SDT/Email) ->
POST /suppliers -> auto-select vao phieu.
- Upload file bao gia + bang so sanh x2: input multiple + upload tuan tu tung file
(UAT "moi lan chi chon duoc 1 file").
- SHA256 mirror x2 app, build tsc+vite x2 PASS, BE 0 err, test 240/240 PASS local.
Double-check chất lượng P11-A ở Max (agents trước chạy High + truncate 3×) →
phát hiện 2 bug THẬT trong workflow-picker FE của WorkflowAppDetailPage (core
approve/reject/return ĐÚNG, chỉ sub-flow chọn quy trình hỏng):
Bug #1 (HIGH) — pinWorkflow PUT /{id} chỉ gửi {approvalWorkflowId} → UpdateDraft
validator (Reason NotEmpty, NumDays>0...) fail → 400. Nút "Lưu quy trình" vỡ.
Bug #2 (HIGH) — fetch workflow expect flat array nhưng endpoint trả
AwAdminOverviewDto {types:[...]} → picker rỗng/crash. FE copy nhầm pattern hỏng
của ProposalCreatePage thay vì PE/Contract proven.
Fix:
- BE: thêm endpoint chuyên dụng PUT /{id}/workflow + Set{Module}WorkflowCommand/Handler
cho 4 module — chỉ set ApprovalWorkflowId trên draft Nhap/TraLai (verify ApplicableType
per module), KHÔNG validate field khác. Single-responsibility, bulletproof.
- FE: sửa fetch mirror PE/Contract (data.types.find(t=>t.applicableType===X)?.history
.filter(isUserSelectable)) + pin gọi endpoint mới. fe-admin+fe-user SHA256 identical.
- Test: +3 SetWorkflow (happy no-status-change / wrong ApplicableType Conflict / submitted
guard) → 141→144 PASS.
Verify: BE build 0 error · 144 test PASS · FE build ×2 · SHA256 identical.
Bonus phát hiện: ProposalCreatePage (S37) có bug #2 có sẵn (latent, chưa exercise UAT)
→ flag spawn task riêng, KHÔNG fix trong commit này.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CICD Monitor Run #202 Plan O verify catch CRITICAL caveat:
PurchaseEvaluationsController.cs:267 `TransitionPeBody` record CHỈ có
3 fields (TargetPhase, Decision, Comment) — MISSING 3 fields có trong
Command record `TransitionPurchaseEvaluationCommand`:
- ReturnMode (F1 mode Trả lại)
- ReturnTargetUserId (F1 Assignee target)
- SkipToFinal (F2 duyệt thẳng Cấp cuối)
Mediator.Send line 70 cũng drop 3 field. → FE × 2 app SEND ĐÚNG 7 fields
qua `api.post(/transitions)` body (Investigator audit confirm wire OK) →
ASP.NET Core deserialization silently DROP 3 fields ở Controller layer →
Handler nhận ReturnMode=null + SkipToFinal=false → fallback default Drafter
mode + F2 không trigger.
Bug present 2 NGÀY PROD từ Mig 28 deploy 2026-05-13 — gây TẤT CẢ F1+F2
wire fail từ FE side. Plan N (S23 t4) + Plan O (S23 t5) fix 5 lookup sites
discrimination NHƯNG controller body record bug block flow TRƯỚC KHI đến
lookup site. Em main + Reviewer + Implementer + Investigator all MISS bug
này xuyên 4 plan vì:
1. Mig 28 Command extend 3 fields (S21 t4) nhưng Controller body NOT extended
2. Plan K K2 add `skipToFinal` 8th param Service nhưng Controller NOT extended
3. Bug silent — no error, no compile fail, no test fail, FE call OK,
BE return 204 nhưng handler nhận default args → wrong behavior
Plan P fix BE-only ~10 LOC 1 file `PurchaseEvaluationsController.cs`:
1. Add `using SolutionErp.Application.PurchaseEvaluations.Services` cho
WorkflowReturnMode enum import (line ~7)
2. Extend `TransitionPeBody` record line 267 thêm 3 fields default:
```csharp
public record TransitionPeBody(
PurchaseEvaluationPhase TargetPhase,
ApprovalDecision Decision,
string? Comment,
WorkflowReturnMode? ReturnMode = null,
Guid? ReturnTargetUserId = null,
bool SkipToFinal = false);
```
3. Update `mediator.Send` line 70 pass 7 fields:
```csharp
await mediator.Send(new TransitionPurchaseEvaluationCommand(
id, body.TargetPhase, body.Decision, body.Comment,
body.ReturnMode, body.ReturnTargetUserId, body.SkipToFinal), ct);
```
Investigator (FE wire audit) verify:
- fe-user/src/components/pe/PeWorkflowPanel.tsx:113-124 + fe-admin mirror —
api.post send ĐẦY ĐỦ 7 fields qua body
- KHÔNG cần fix FE
- Mig 28/31 Domain test đã cover handler logic — không cần test mới
Verify:
- dotnet build SolutionErp.slnx clean (0 err, 2 warn pre-existing DocxRenderer)
- dotnet test SolutionErp.slnx **111/111 PASS** unchanged (no regression)
Docs update:
- docs/STATUS.md Last updated S23 t6
- docs/HANDOFF.md TL;DR S23 t6 ngắn gọn
- .claude/agent-memory/cicd-monitor/MEMORY.md drift (Run #202 entry pre-existing)
Pattern reinforced cross-project:
- Controller body record MUST mirror Command record fields khi Command thêm
optional params. Silent drop bug class — không test/build catch được.
- Investigator pre-flight audit FE wire trước khi fix BE (Plan P scope
verify) tránh em main fix sai assumption.
Pending: CICD Monitor verify Plan P deploy + UAT test bro real.
Pending Bug 2 F2 đến Phan Văn Chương: verify workflow v14 DB structure
sau khi Plan P unblock F2 flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Feature 1 (attachment preview):
- NEW `GET /api/purchase-evaluations/{id}/attachments/{attId}/view`
- Cùng handler download, override `Content-Disposition: inline` để FE nhúng iframe
- Permission: same scope GET phiếu (Plan E V2 strict scope)
Feature 2 (điều chỉnh ngân sách):
- NEW `AdjustPurchaseEvaluationBudgetCommand` + Handler + Validator
- NEW `PATCH /api/purchase-evaluations/{id}/budget-adjust` body
`{budgetId, budgetManualName, budgetManualAmount}`
- Phase + actor scope guard:
* DangSoanThao/TraLai → chỉ Drafter của phiếu
* ChoDuyet → Approver currentLevel (match ApproverUserId) — V2 only
* Admin → bypass tất cả
- Audit changelog với diff narrative: "Điều chỉnh ngân sách: link X→Y, số tiền A→Bđ [Drafter/Approver Bước/Cấp/Admin]"
- Tách riêng KHÔNG dùng UpdatePeDraft vì Approver scope KHÔNG nên được edit
Section 1 fields (TenGoiThau/DiaDiem/MoTa/PaymentTerms)
Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Test defer carry Plan C (UAT mode §7) — guard logic critical, ưu tiên cho S23+
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hai yêu cầu UAT 2026-05-08:
1. Bỏ "(clone)" auto-append khi clone version mới — version đã đủ phân biệt.
2. Thêm pin toggle để admin chọn workflows nào cho user pick lúc tạo phiếu.
Migration 25 AddIsUserSelectableToApprovalWorkflows:
- ALTER ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0
- UPDATE backfill SET IsUserSelectable=1 WHERE IsActive=1 (giữ behavior cũ
cho active versions, archived = false default — admin tự pin nếu cần)
BE:
- Domain ApprovalWorkflow +property IsUserSelectable
- DTO AwDefinitionDto +field
- CreateAwDefinitionCommandHandler set default true cho version mới
- New SetAwUserSelectableCommand + Handler
- API PATCH /api/approval-workflows-v2/{id}/user-selectable (Workflows.Create policy)
- DbInitializer SeedSampleApprovalWorkflowsV2Async set IsUserSelectable=true
FE Designer (fe-admin):
- DefinitionDto +isUserSelectable
- Badge amber "Pin Cho user chọn" khi true (cạnh Đang áp dụng/Archived)
- Button "Pin/PinOff Ghim cho user / Bỏ ghim" trong action group + mutation toggle
- Auto-fill name khi clone: bỏ "(clone)" suffix → giữ nguyên name
FE Workspace (fe-admin + fe-user):
- approvalWorkflows query filter w.isUserSelectable === true
- User dropdown chỉ thấy workflows admin đã pin
Verify: dotnet build pass · 81 test pass · npm build × 2 pass · Mig 25 apply LocalDB OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug UAT 2026-05-08: user Drafter (nv.test) login Workspace tạo phiếu B,
dropdown "Quy trình duyệt" empty silent. Sample seed B đã chạy đúng
(Designer admin hiển thị sample + clone v02 active) nhưng Workspace empty.
Root cause: class-level [Authorize(Policy = "Workflows.Read")] →
non-admin role 403 Forbidden khi GET /api/approval-workflows-v2.
TanStack Query catch error silent → dropdown empty không có warning.
Fix:
- Class-level [Authorize] only (any authenticated)
- GET inherit class policy (Drafter cần list workflow để pick — read-only)
- POST + DELETE giữ [Authorize(Policy = "Workflows.Create")] — admin-only Designer
Workflow data không nhạy cảm — chỉ là cấu hình quy trình. Validate
ApplicableType match PE.Type ở Create command đã có.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User báo:
- Filter "Tất cả quy trình duyệt" hiện chỉ ở Danh sách → muốn ở cả 2
- Filter chọn quy trình → không thấy phiếu V2 trong Duyệt (Inbox)
BE — wire filter vào Inbox:
- GetMyPurchaseEvaluationInboxQuery +ApprovalWorkflowId? param
- Handler thêm filter `q.Where(x => x.e.ApprovalWorkflowId == awId)`
- PurchaseEvaluationsController.Inbox +approvalWorkflowId query param
FE (cả 2 app mirror):
- PurchaseEvaluationsListPage: bỏ điều kiện `!pendingMe` ở Select dropdown
→ hiển thị filter quy trình duyệt CẢ Duyệt + Danh sách
- Inbox API call: pass approvalWorkflowId từ URL param
Verify: BE 0 error · 2 FE builds OK.
Test luồng eoffice:
1. Vào "Duyệt NCC > Duyệt" → 2 dropdown filter hiện đầy đủ
2. Chọn 1 quy trình V2 từ dropdown → list filter chỉ phiếu pin quy trình đó
3. Vào "Duyệt NCC > Danh sách" → 2 dropdown vẫn show, filter cũng work
User báo: "Phiếu chưa thấy lên trong danh sách duyệt — chắc do chưa ăn
vào flow. Tách thành 2 cái dropdown là list quy trình duyệt và list
trạng thái. Debug trước, phân quyền rút gọn lại sau."
BE — V2-aware permission + filter (Application/PurchaseEvaluations/
PurchaseEvaluationFeatures.cs):
- ListPurchaseEvaluationsQuery +ApprovalWorkflowId? Guid? param +
IDOR loose: phiếu pin V2 → mọi authenticated user thấy được (UAT)
- GetMyPurchaseEvaluationInbox V2-aware: ResolveV2InboxIdsAsync helper
precompute Set<Guid> phiếu Phase=ChoDuyet pin V2 + actor ∈ Cấp hiện
tại approvers (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder
match Step.Order + Level.Order). Inbox where = eligiblePhases.Contains
|| v2InboxIds.Contains. eligiblePhases admin +ChoDuyet.
- GetById Detail loose: V2 pin → cho non-Drafter xem (skip
eligiblePhases check).
API Controller:
- PurchaseEvaluationsController.List +approvalWorkflowId query param
FE — 2 dropdown filter (cả 2 app mirror):
- PurchaseEvaluationsListPage: +URL param `awId` filter quy trình
- useQuery `approval-workflows-v2-filter` load list V2 active+history
theo applicableType=typeFilter (chỉ enabled khi có type)
- Render Select riêng "Tất cả quy trình duyệt" (chỉ show !pendingMe vì
Inbox dùng API endpoint khác) + Select "Tất cả trạng thái" giữ
- Display option: "QT-DN-V2-001 v01 — Tên quy trình"
Verify: BE build 0 error · 2 FE builds OK.
Test luồng eoffice:
1. Drafter trình phiếu V2 → Phase=ChoDuyet
2. Login NV X (approver Cấp 1) vào "Duyệt NCC > Duyệt"
(?pendingMe=1) → phiếu hiện trong list
3. Login NV Y (không phải approver) → list rỗng (đúng spec)
4. Vào "Duyệt NCC > Danh sách" (không pendingMe) → 2 dropdown:
- Quy trình duyệt: filter theo workflow specific
- Trạng thái: filter theo Phase
Mở rộng 2-stage logic từ PE sang Contract workflow (Migration 16 đã có schema):
BE Service:
- ContractWorkflowService thêm UserManager<User> DI
- Mirror logic 2-stage từ PurchaseEvaluationWorkflowService.TransitionAsync
Sau policy guard, trước gen mã HĐ:
- User.DepartmentId != null + actor không admin/system + KHÔNG resume
- DeptManager (TPB) → Stage=Confirm trực tiếp
- CanBypassReview=true → Stage=Confirm + IsBypassed=true
- Else (NV) → Stage=Review only, BLOCK transition
- Insert ContractDepartmentApproval row (UPSERT theo UNIQUE)
- Block transition khi chưa có Stage=Confirm:
- Insert ContractApproval (FromPhase=ToPhase=fromPhase, [Review NV] comment)
- Insert ContractChangelog "đã review, chờ TPB confirm"
- Notify TPB cùng dept (UserManager filter DeptManager role)
- Return early — phase KHÔNG đổi
App + Api:
- ContractDepartmentApprovalFeatures.cs (List query mirror PE)
- ContractsController endpoint GET /contracts/{id}/department-approvals
FE (cả fe-admin + fe-user):
- types/contracts.ts thêm ApprovalStage const + ContractDepartmentApproval type
- WorkflowHistoryPanel section "Tiến trình duyệt 2-cấp phòng ban":
- Group by phase × dept, show Review NV + Confirm TPB
- Highlight amber "chờ TPB confirm" khi current phase có Review chưa Confirm
- Badge fuchsia "bypass" khi NV.CanBypassReview=true
- Insert giữa WorkflowSummaryCard và Lịch sử duyệt
- Mirror cả 2 app (rule §3.9)
Use case mirror PE: HĐ ở phase DangGopY (P.CCM) — nv.cao (NV) duyệt thì
phase KHÔNG đổi (Review only), chờ ccm.tran (TPB) confirm mới sang DangXetDuyet.
Build: BE pass + FE pass cả 2 + 77 test pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>