Appearance
Decision Brief — Prepaid Card Analytics Tab
Date: 2026-05-04 · Phase: 1 (MVP) Purpose: Tóm tắt các quyết định kiến trúc/business quan trọng + lý do, để team mới onboard hiểu nhanh "tại sao chọn cách này".
Detail decisions trong prd.md Z1-Z4. File này nhóm + giải thích why, không lặp lại WHAT.
1. Architecture decisions
🏛️ A1. Component-level QTabPanels — KHÔNG dùng child route
Decision (DEC-T06): Sub-tabs render qua QTabPanels với direct component, NOT <router-view> + child routes.
Why:
- Khớp pattern hiện có trong codebase (
ServiceReport.tsx,WalletReport.tsx) - Lazy load qua
defineAsyncComponent— vẫn tách bundle được - Khi user switch tab nhanh, không phải mount/unmount route → giữ filter state rõ ràng
- Tránh debug router edge cases (back button, deep link conflict)
Rejected alternative: Child routes — sẽ bắt mỗi sub-tab thành route riêng. Phức tạp hơn cho 1 module 4 tab.
🏛️ A2. 4 Materialized Views Phase 1 (split order vs card-level)
Decision: 4 MVs deploy Phase 1 — mv_prepaid_order_daily, mv_prepaid_card_daily, mv_prepaid_customer_stats, mv_prepaid_finance_daily.
Why:
- 70 CN × 365 ngày × 1M+ giao dịch — query real-time sẽ > 10s, không acceptable
- Tách order_daily và card_daily sau review L4 phát hiện single MV với granularity
(date, branch, product_id)gây double counttotal_collectedkhi 1 order có nhiềuorder_item - Order-level metrics (Tiền thu, Tổng nạp ví, KH unique) → query từ
order_daily - Card-level metrics (Top thẻ, Phân bố mệnh giá) → query từ
card_daily
Rejected alternative: 1 MV duy nhất với grouping sets — phức tạp logic, dễ aggregate nhầm bằng frontend.
🏛️ A3. Defer Marketing + Nhân viên sub-tab Phase 3+ (TBD)
Decision (DEC-B05): Phase 1 = 4 sub-tab (Tổng quan + Giao dịch + Khách hàng + Tài chính). Marketing + Nhân viên defer indefinitely.
Why:
- Phase 1 ưu tiên cấp thiết: Kế toán đối soát + Quản lý dashboard
- Sub-tab Khách hàng cover phần lớn nhu cầu Marketing (segment, bulk SMS/ZNS, behavior bar) cho self-service cơ bản
- Sub-tab Tài chính tab Hoa hồng cover nhu cầu kế toán lương NV — không cần Sub-tab Nhân viên ranking phức tạp
- Defer xa hơn cho phép focus vào quality MVP thay vì rush 6 sub-tab cùng lúc
Re-prioritize trigger: Marketing yêu cầu báo cáo CD chuyên sâu / Quản lý vùng cần coaching data-driven.
🏛️ A4. Bỏ Compare Mode toggle
Decision (DEC-U04): Filter bar chỉ 3 element (Chi nhánh · Khoảng thời gian · Tìm kiếm). KHÔNG có toggle Tổng hợp / So sánh KV / So sánh CN.
Why:
- 3 chế độ → dev phải build 3 layouts cho mỗi sub-tab → effort cao + maintain phức tạp
- Chart "So sánh khu vực" + bảng có cột Khu vực vẫn giữ — KHÔNG phụ thuộc toggle
- Mới đã có simple filter — đơn giản hóa state management
Rejected: Giữ compare mode — quá đắt cho Phase 1 MVP.
🏛️ A5. AOV thay CLV trong Behavior Bar (Phase 1)
Decision: Section 5.5 "Chỉ số hành vi khách hàng" hiển thị 3 metrics: Giá trị đơn TB (AOV) · Tỷ lệ tái nạp · Chu kỳ trung bình. KHÔNG có CLV thuần.
Why:
- CLV cần data ≥ 6-12 tháng tích lũy mới meaningful → Phase 1 launch chưa đủ
- AOV đơn giản, actionable (Marketing tăng AOV bằng upsell), tính được ngay từ ngày 1
- Section 5 đã có 4 layer (Segment + Behavior + Table + Expanded) — đủ behavior context
Re-prioritize: Có thể bổ sung CLV Phase 3+ khi có data tích lũy.
🏛️ A6. RBAC v2 fine-grained (Phase 1) — 3 actions
Decision (review L4 fix): Phân quyền Phase 1 = 3 actions per module report.prepaid_analytics:
view— bật/tắt xem báo cáo (sub-tab visibility)export— bật/tắt export Excel (compliance)view_full_phone— bật/tắt unmask SDT (PII compliance)
Why:
- Module có PII (SDT) + dữ liệu tài chính + export — module-level toggle (chỉ
view) KHÔNG đủ compliance - Kế toán cần xem SDT đầy đủ để gọi nợ, Marketing chỉ cần xem segment → SDT phải tách action riêng
- Export bulk có rủi ro (data leak) → cần action permission riêng để có thể grant/revoke per-user
Implementation:
- BE enforce ở Hasura permission rule + export endpoint guard (KHÔNG chỉ trust FE hide button)
- Default grants seed qua migration: admin có cả 3, kế toán có cả 3, branch_manager có view+export, marketing chỉ view
- Audit log mỗi export action vào
export_jobtable vớiphone_unmaskflag
Rejected (v2.0 module-level only): Quá đơn giản cho compliance. PII không enforce được qua role-only.
2. Calculation decisions (CRITICAL — review L4)
📊 C1. prepaid_value_into_wallet đã là line total — KHÔNG × quantity
Decision (Section 0.2 Rule 1): Mọi formula liên quan Nạp ví / Ví Diva / Ví KM / % Đã dùng ví / KM đã nạp / LN gộp KHÔNG được nhân với quantity.
Why:
- Codebase
PrepaidOrderCreate.tsx:252setprepaid_value_into_wallet= giá trị nạp của cả dòng item (đã nhân quantity ở FE) - Codebase
PrepaidOrderPayments.tsx:55cộng trực tiếp, không nhân thêm - Spec gốc dùng
× quantity→ phóng đại N lần khi quantity > 1 → kế toán không tin số
Evidence: Xem EVIDENCE_PACK.md E2.
📊 C2. Tách order-level và item-level aggregation
Decision (Section 0.2 Rule 2): Order-level metrics (total_collected, KH unique) phải aggregate ở granularity (date, branch, region) KHÔNG có product_id. Item-level metrics aggregate riêng.
Why:
- 1 đơn có thể có nhiều
order_item(mua 2 thẻ trong 1 đơn) → group byproduct_idrồi SUM order metric → cộng trùng N lần - Tách 2 MVs riêng đảm bảo không nhầm
Evidence: Xem EVIDENCE_PACK.md E3.
📊 C3. Single source A10 FORMULA — không re-define trong dev-spec
Decision: PRD A10 FORMULA-001..018 là canonical. Dev-spec C3 chỉ ref FORMULA-ID, KHÔNG tự định nghĩa công thức.
Why:
- Tránh drift khi spec evolve — đổi formula 1 chỗ, các SQL implementation cùng update
- Khớp CLAUDE.md project rule: "PRD A10 = business definition (canonical). Dev-spec C3 = SQL implementation delta only, ref A10"
3. Phase / Scope decisions
🚦 P1. Search strategy Phase 1: single source ở shared filter (DEC-U12)
Decision: Phase 1 search = single shared search ở filter top, scope auto-switch theo sub-tab active. BỎ HẲN local search trong từng sub-tab. Phase 2 Global Search dropdown (suggestion list) là enhancement của shared search hiện tại, KHÔNG tạo input mới.
Why:
- 1 ô search top = 1 source state, persist cross-tab → mental model đơn giản
- Local filter của mỗi sub-tab chỉ giữ structured filter (dropdown / chip) đặc thù
- Backend dùng
ILIKE %term%qua_or4 trường (Transactions: order_code · customer.name · customer.phone_search · prepaid_card.code) — KHÔNG cầnpg_trgmGIN index Phase 1 - Kế toán + Quản lý dùng search xuyên các tab → keyword giữ nguyên khi user đổi tab giúp họ diagnose nhanh
Rejected:
Local search riêng cho Transactions— Review L8 + DEC-U12: gây drift state, duplicate UX, FE phức tạpDefer hết search Phase 2— kế toán cần tra cứu cấp thiết
🚦 P2. Defer write actions từ Phase 1.x
Decision: Bulk SMS/ZNS, Gán NV, Xác nhận thanh toán → defer Phase 1.x (sau MVP read-only). Phase 1 = pure read-only analytics.
Why:
- Write actions cần thêm: API endpoints, RBAC action permission (
bulk_send,confirm_payment), audit log, error/retry handling, idempotency - Build đầy đủ trong Phase 1 sẽ đẩy timeline lên 12+ tuần
- Read-only MVP delivers core value (đối soát, monitor) trước, write actions là follow-up
Re-prioritize: Phase 1.x ngay sau Phase 1 stable (1-2 sprint).
🚦 P3. Reuse region_branch existing — KHÔNG tạo mới
Decision (Schema Mapping): Dùng region_branch table + branch.region_id đã có sẵn. KHÔNG tạo migration branch_region.
Why:
- Migration
1678865967129_region_branch/up.sqlđã chạy → schema đã sẵn sàng - Chỉ cần data check (70 CN có đủ
region_idchưa) trước deploy - Tránh duplicate / conflict naming
4. UX decisions
🎨 U1. Sub-tab order: Khách hàng TRƯỚC Tài chính
Decision (DEC-U07): Tổng quan → Giao dịch → Khách hàng → Tài chính → Marketing* → Nhân viên*.
Why:
- Luồng đọc của Quản lý: nhìn tổng → tra đơn → xem KH → mới đến số tiền
- Marketing sự dụng tab Khách hàng sớm — đặt sớm trong tab bar tăng discoverability
🎨 U2. Date presets gộp vào dropdown
Decision (DEC-U05): Khoảng thời gian là 1 dropdown chứa cả presets (Hôm nay / 7 ngày / Tháng này / ...) + custom range. KHÔNG tách button rời.
Why:
- Filter bar gọn (3 element) thay vì 5
- Click 1 lần thay vì 2 step (chọn preset rồi chọn range)
🎨 U3. Rename "Doanh thu & Công nợ" → "Tài chính"
Decision (DEC-U06): Tên ngắn gọn cho tab bar.
Why:
- 6 tabs ngang → tên dài làm tab bar wrap → UX kém
- "Tài chính" cover đủ ý (DT + Công nợ + Hoa hồng + PTTT)
5. Risks accepted (knowingly tolerated)
| Risk | Why accepted | Mitigation |
|---|---|---|
| Phase 1 thiếu Global Search dropdown (suggestion list) | UX gap nhẹ — shared search top ở filter bar (DEC-U12) đã apply scope auto theo sub-tab, đủ tra cứu cấp thiết. Phase 2 = thêm dropdown gợi ý + xuyên entity (KH/đơn/thẻ/CN) | Phase 2 ramp up sau MVP stable |
| RBAC fine-grained 3 actions phụ thuộc Dynamic Permission v2 ready | Phase 1 cần PII compliance + export audit — không thể module-level toggle | V6 BLOCKER — Security Lead confirm v2 module API trước implement |
| 8 verification points (V1-V8) đều BLOCKER | Schema/security/data có rủi ro cao nếu giả định sai | Implementation HOLD HOÀN TOÀN cho đến khi cả 8 V đều ✅ Confirmed kèm evidence (xem SOURCE_OF_TRUTH Section 4) |
| Reconciliation chênh lệch ≤ 0.1% với báo cáo cũ | Tab cũ có bug (skip 1000 rows + exclude flexible + có thể double count parent/sub invoice) → spec đúng có thể chênh nhẹ | Document expected delta; reconcile lại trên data sạch |
| Bulk SMS/ZNS/Gán NV/multi-select trong tab Khách hàng | Pure read-only Phase 1 — không có write surface | OUT OF SCOPE Phase 1+ (kể cả P1.x) — dùng module Marketing/CRM riêng (DEC-U09) |
6. Decision log full
Xem chi tiết per-decision tại:
- prd.md Z1 — Business Decisions: DEC-B01..B05
- prd.md Z2 — UX Decisions: DEC-U01..U07
- prd.md Z3 — Technical Decisions: DEC-T01..T06
- prd.md Z4 — QA Decisions: DEC-Q01..Q02
Reference:
- SOURCE_OF_TRUTH.md — canonical spec mapping
- EVIDENCE_PACK.md — bằng chứng codebase/business
- prd.md · dev-spec.md