Skip to content

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 count total_collected khi 1 order có nhiều order_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_job table với phone_unmask flag

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:252 set prepaid_value_into_wallet = giá trị nạp của cả dòng item (đã nhân quantity ở FE)
  • Codebase PrepaidOrderPayments.tsx:55 cộ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 by product_id rồ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 _or 4 trường (Transactions: order_code · customer.name · customer.phone_search · prepaid_card.code) — KHÔNG cần pg_trgm GIN 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ạp
  • Defer 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_id chư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)

RiskWhy acceptedMitigation
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 readyPhase 1 cần PII compliance + export audit — không thể module-level toggleV6 BLOCKER — Security Lead confirm v2 module API trước implement
8 verification points (V1-V8) đều BLOCKERSchema/security/data có rủi ro cao nếu giả định saiImplementation 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àngPure read-only Phase 1 — không có write surfaceOUT 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:


Reference: