Appearance
Sales-CSKH Debt Follow-up & Handover — PRD
v2.0 — 15/05/2026 (Major Hardening Release)
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
| Thêm 14 DECs mới (DEC-022..035) — resolve 7 GAPs nghiệp vụ + 5 prod issues + 3 PDs | Z1) Decision Log | All |
Rewrite FORMULA-003 (Doanh thu ròng canonical) — SUM(actual_revenue) exclude wallet | A10) Formulas | BE, QA |
| Thêm FORMULA-006B (customer-level aging bucket — multi-order rollup) | A10) Formulas | BE, QA |
| Thêm FORMULA-007 (Service representative tie-break SCR-02) | A10) Formulas | BE, FE |
| Thêm FORMULA-008 (Weighted % tham gia multi-order) | A10) Formulas | BE, FE, QA |
| Thêm A12.4 (KPI × Role canonical — Manager/BOD raw aggregate, không quy đổi) | A12) Codebase Audit | BE, FE |
FR-009 update — noti trigger >= + 4 re-trigger rules (daily/bucket/partial/settled) | A5) FR-009 | BE, QA |
| Resolved PD-001 (default roles admin+it_leader), PD-002 (cron 7h00 sáng), PD-004 (index-first capacity gate) | Z1) DEC-030/031/034 | DevOps, BE, DBA |
| DEC-035 — PD-003 Hasura permission harden DEFERRED có điều kiện — REVISIT trước GA | Z1) DEC-035 | TL, Security |
Canonical Source of Truth:
SOURCE_OF_TRUTH.md— quy tắc DEC-001: codebase Diva hiện tại là ground truth; HTML intent v4.12 là target state cần đạt sau hardening. Slug: sales-cskh-debt-handover Document type: PRD (Mode B — production-grade, hardening pass) Version: v4.12 (HTML intent) + v2.0 hardening (codebase delta + prod issues resolved) — Updated: 2026-05-15 Spec Index: SOURCE_OF_TRUTH.md · EVIDENCE_PACK.md · ui-spec.md · dev-spec.md · qa-test-plan.md · go-live-checklist.md · handoff.md · prod-issues.md
ⓘ Hardening Strategy (đọc trước tiên)
Feature này đã build ~85% trong Diva codebase rồi. Đây là PRD cho hardening pass — không phải greenfield. Khi đọc PRD:
- A1-A8 mô tả TARGET STATE (intent từ HTML v4.12) — yêu cầu nghiệp vụ tổng thể (FR, persona, KPI, decisions).
- Mỗi FR có cột "Codebase status" — ghi rõ component/route/table đã có → chỉ cần hardening hay extend.
- A12) Codebase Audit Summary ở cuối PRD — bảng tổng kết Reuse 🟢 / Extend 🟡 / Build 🔴 với file path.
- Mapping chi tiết SCR/MOB → existing component: xem
ui-spec.md§0 Codebase Mapping. - Evidence (SQL/Go/TS quotes với line numbers): xem
EVIDENCE_PACK.md.
Khi PRD nói "xây dựng X" / "tạo Y" → đọc lại context: nếu codebase đã có thì hiểu là "đảm bảo X hoạt động đúng spec". Nếu thật sự thiếu, sẽ được đánh dấu 🔴 trong A12.
Sales-CSKH Debt Follow-up & Handover — PRD
Version: v4.12 | Updated: 2026-03-16 Spec Index: README.md | UI Spec: ui-spec.md | Dev Spec: dev-spec.md | QA: qa-test-plan.md | Go-Live: go-live-checklist.md
📌 Changelog v4.12 — Commission Primary Owner (popup hoa hồng + phụ trách chính):
- FR-002 bổ sung rule chọn "Phụ trách chính": 1 Sale → auto set
is_primary_owner(không hiện radio). 2+ Sale → hiện radio bắt buộc chọn. 0 Sale → fallbackin_charge.- 3 wireframes popup hoa hồng: Case 2+ Sale (cần chọn), Case 1 Sale (auto), Case chưa chọn (validation error + disabled submit).
- Validation rule: Khi 2+ Sale chưa chọn phụ trách chính → nút Xác nhận disabled + warning text.
- 4 test cases mới: TC-CP-08c (1 Sale auto), TC-CP-08d (3+ Sale), TC-CP-08e (validation chưa chọn), TC-CP-08f (multi-DV mixed).
- 2 seed data mới: Seed-COMMISSION-02 (multi-DV mixed), Seed-COMMISSION-03 (3 Sale 40/30/30).
Xem changelog đầy đủ tại README.md
Hướng dẫn đọc
| Bạn là | Đọc section |
|---|---|
| PO / BA | Executive Summary + A + B (đầy đủ) |
| Dev | A + C + B4 (Notification) + B-Export |
| QA | A + D + B7 (State) + Edge Cases |
| Mobile Dev | A + B-Mobile (MOB-01…05) + C-Mobile |
| Ops / SRE | A + E |
| Quản lý | Executive Summary + A1 |
Executive Summary (TL;DR)
Mục tiêu: Xây dựng bộ tính năng hợp nhất để đội Sale/CSKH/Telesale quản trị công nợ, theo dõi hiệu suất tư vấn, và bàn giao khách khi nhân sự nghỉ — đúng người, đúng thời điểm, không rơi khách.
3 nhóm tính năng chính (MVP-Lite)
| Tính năng | Mô tả | Người dùng | |
|---|---|---|---|
| 📊 | Dashboard hiệu suất | KPI tư vấn / chốt / nợ / thu hồi nợ | Sale, CSKH, Telesale, Manager |
| 🚨 | Cảnh báo nợ | Job hằng ngày + in-app alert + mobile push | Sale (owner chính), Manager |
| 🔄 | Bàn giao khách | Wizard 3 bước + audit + rollback 24h | HR Ops, Manager |
Platform: Web Admin (Desktop 1280px+) + Diva Partner App (Mobile iOS/Android)Lưu ý: SLA xử lý nợ, Priority scoring P1/P2/P3, Auto-escalation → Phase 2.
Milestones (Cập nhật v4.0)
| Milestone | Target | Nội dung | Owner |
|---|---|---|---|
| M-01 | 03/03/2026 | KPI Definition Freeze + Confirm PD-005/007/008/009 | PM/PO |
| M-02 | 10/03/2026 | UX Freeze — Desktop 8 màn + Mobile 5 màn | UIUX Lead |
| M-03 | 24/03/2026 | Core Build (FR-001…007) | Tech Lead |
| M-04 | 07/04/2026 | Alert + Handover (FR-008…012) + Mobile API | DEV/OPS |
| M-05 | 14/04/2026 | Release Readiness — Web + Mobile | QA Lead + Ops/SRE |
Trạng thái sign-off
| Domain | Owner | Trạng thái | Blocker |
|---|---|---|---|
| Business Fit | PM/PO | ✅ Conditional | — |
| UX Readiness | UIUX Lead | ✅ Conditional | Chờ high-fidelity freeze (Desktop + Mobile) |
| Tech Readiness | Tech Lead | ✅ Conditional | Chờ migration + scheduler runbook + Mobile API spec |
| Quality Readiness | QA Lead | ✅ Conditional | Cần seed data chuẩn cho aging |
| Operational Readiness | Ops/SRE | 🟡 In Progress | Action items OPS-01…05 đã assign — target 10/03 (M-02) |
Action Items — Unblock Ops Readiness
| # | Action | Owner | Deadline | Status |
|---|---|---|---|---|
| OPS-01 | Viết runbook debt scheduler: scenarios (job fail, retry, dedupe check, manual re-trigger) | Ops/SRE | 10/03 (M-02) | ⏳ |
| OPS-02 | Review runbook bởi Tech Lead + QA | Tech Lead | 14/03 | ⏳ |
| OPS-03 | Setup alert dashboard: debt_scheduler_run_failed, push_notification_delivery_rate | Ops/SRE | 17/03 (trước M-03) | ⏳ |
| OPS-04 | Diễn tập dry-run trên staging: job lỗi → retry → không trùng → alert Ops | Ops/SRE + QA | 21/03 | ⏳ |
| OPS-05 | Setup FCM project + service account cho push notification | Ops/SRE + BE | 10/03 (M-02) | ⏳ |
Pending Decisions (Cần confirm trước M-01: 03/03/2026)
| PD-ID | Câu hỏi | Người quyết định | Status | Đề xuất PO/BA |
|---|---|---|---|---|
| PD-005 | Số khách tối đa trong 1 lần handover có giới hạn không? | Tech Lead | ✅ Resolved → DEC-017 | Giới hạn 200 khách/lần. Align với export row logic. Nếu cần nhiều hơn, chia batch. Hiển thị warning khi > 100 khách. |
| PD-007 | Mobile Diva Partner có hỗ trợ Handover Wizard không? | PM/PO | ✅ Resolved → Phase 2 | Phase 2. Handover cần preview diff phức tạp, không phù hợp mobile MVP. Ghi rõ trong Backlog Phase 2. |
| PD-008 | Push notification mobile: Firebase hay APNs riêng? | Tech Lead | ✅ Resolved → DEC-018 | Firebase Cloud Messaging (FCM). Cover cả iOS + Android, Diva Partner đa nền tảng. APNs chỉ cover iOS. |
| PD-009 | Mobile có export XLSX không hay chỉ xem? | PM/PO | ✅ Resolved → DEC-019 | Mobile chỉ xem, không export. Người dùng cần export → dùng web. Giảm complexity mobile MVP. |
RACI
| Deliverable | PM/PO | UIUX | DEV | QA | OPS/SRE |
|---|---|---|---|---|---|
| PRD | A/R | C | C | C | I |
| UI/UX Spec (Web + Mobile) | C | A/R | C | C | I |
| Dev Spec | C | C | A/R | C | C |
| QA Plan | C | C | C | A/R | I |
| Release Plan | A | I | R | C | R |
Backlog Phase 2 (OUT of MVP-Lite)
| Tính năng | Lý do chuyển |
|---|---|
| SLA xử lý nợ (24h/8h/4h) | Tăng complexity scheduler + UI; team chưa có culture SLA |
| Priority scoring P1/P2/P3 | Cần dữ liệu baseline trước khi scoring chính xác |
| Auto-escalation báo quản lý | Phụ thuộc SLA — bỏ SLA thì bỏ luôn auto-escalation |
| Benchmark nâng cao, cohort analysis | Nâng cao, không cần cho MVP |
| Automation rules, predictive scoring | ML phase, chưa có data đủ |
| Drill-down tư vấn trên Mobile | Quá phức tạp cho màn nhỏ, ưu tiên web |
| Handover Wizard trên Mobile | Phase 2 confirmed (PD-007 resolved). Cần preview diff phức tạp, không phù hợp mobile MVP |
Z) Master Decision Log
Z1. Quyết định nghiệp vụ
| DEC-ID | Quyết định | Lý do | Status |
|---|---|---|---|
| DEC-001 | visit_count = lượt theo grain (customer_id, branch_id, date) có consultant; unique_customer_count = khách duy nhất theo customer_id | Tránh nhập nhằng KPI | ✅ Locked |
| DEC-002 | closed_customer_count chỉ tính khi mua + thực thu > 0 | Đồng nhất với dòng tiền thực nhận | ✅ Locked |
| DEC-003 | Owner nợ: is_primary_owner → highest% → in_charge (tie-break kỹ thuật cho dữ liệu legacy) | Chốt owner chính ngay từ popup hoa hồng, giảm mơ hồ case 50/50 | ✅ Locked |
| DEC-004 | Ngưỡng: branch_override ?? global_threshold | MVP đủ linh hoạt | ✅ Locked |
| DEC-005 | Handover: 3 bước + 1 xác nhận cuối | Giảm rủi ro bàn giao sai | ✅ Locked |
| DEC-006 | Handover chuyển cả nhắc nợ + quyền xem lịch sử | Tránh rơi khách sau bàn giao | ✅ Locked |
| DEC-007 | Kênh cảnh báo MVP: in-app + mobile push | Tối ưu tốc độ, phủ rộng nhân viên mobile | ✅ Locked |
| DEC-008 | KPI thu nợ: 2 chỉ số (weighted + full settlement) | Đánh giá tốc độ thu + tất toán | ✅ Locked |
| DEC-009 | actual_revenue column trực tiếp (đã tính sẵn refund + discount) | Hợp nhất canonical 1 lần cho FE/BE/QA | ⚠️ Superseded |
| DEC-010 | Mẫu số 0: giá trị = 0, hiển thị N/A | Tránh lỗi chia 0 | ✅ Locked |
| DEC-011 | Chuẩn hóa debt_start_date và debt_due_date | Tránh nhập nhằng recovery/overdue | ✅ Locked |
| DEC-012 | Mobile scope: Diva Partner App, chỉ xem + hành động cơ bản | Nhân viên cần theo dõi mọi lúc | ✅ Locked |
| DEC-013 | Manager xem report theo branch đang quản lý (không phải tất cả) | Phân quyền đúng scope quản lý | ✅ Locked |
| DEC-014 | Export chỉ XLSX, bỏ CSV | Chuẩn hóa format, giảm maintain | ✅ Locked |
| DEC-015 | Handover rollback: chỉ toàn bộ, không hỗ trợ partial | Giảm complexity MVP | ✅ Locked |
| DEC-016 | Noti handover gửi cho: Creator + Target (người tiếp nhận) | Đảm bảo cả 2 bên đều biết | ✅ Locked |
| DEC-017 | Giới hạn 200 khách/lần handover. Warning khi > 100. Chia batch nếu cần nhiều hơn | Align với export row cap, tránh transaction quá lớn (PD-005) | ✅ Locked |
| DEC-018 | Push notification mobile dùng Firebase Cloud Messaging (FCM) | Cover cả iOS + Android, Diva Partner đa nền tảng (PD-008) | ✅ Locked |
| DEC-019 | Mobile chỉ xem, không export XLSX. Export → dùng web | Giảm complexity mobile MVP (PD-009) | ✅ Locked |
| DEC-020 | Khi đến giờ nhắc (remind_at), gửi notification in-app (web) cho assigned_to. Không push mobile | Giảm complexity scheduler; followup task là nhắc nhẹ, không cần push | ✅ Locked |
| DEC-021 | Màn hình quản lý lịch nhắc: drawer trên web (SCR-03) + section trong MOB-03. Không tạo trang riêng | Gom vào context khách hàng/nợ, không cần navigation riêng | ✅ Locked |
| DEC-022 | Doanh thu ròng canonical = SUM(order.actual_revenue) trong kỳ, loại trừ wallet payment (Ref Pitfalls Map § Payment methods). KHÔNG dùng paid_amount − refund_amount (legacy DEC-009 superseded — actual_revenue đã tính sẵn refund + discount trong column trực tiếp) | Single-source canonical chốt 1 lần cho FE/BE/QA cùng verify; tránh 3 định nghĩa khác nhau gây sai số liệu (Ref: FORMULA-003) | ✅ Locked v1.0 |
| DEC-023 | Customer-level aging bucket = bucket có max(overdue_days) trong tất cả order nợ của khách. Khách có nhiều đơn ở nhiều bucket → tính theo bucket cao nhất. Biểu đồ phân loại + bảng aging count theo customer (1 KH = 1 unit ở bucket cao nhất). Tổng tiền nợ trong bucket = SUM(remaining) tất cả đơn nợ của KH ở bucket đó | Tránh đếm trùng KH ở nhiều bucket; ưu tiên hiển thị rủi ro cao nhất để user hành động | ✅ Locked v1.0 (Ref: FORMULA-006B) |
| DEC-024 | KPI quy đổi (allocated) chỉ hiển thị cho view individual (Sale/CSKH/Telesale). Manager/BOD/Admin ẩn cột "Khách nợ quy đổi" / "Tiền nợ quy đổi" / "Doanh thu ròng quy đổi" — thay bằng raw aggregate (count/sum thực) trong branch scope (Manager) hoặc toàn hệ thống (BOD/Admin). API cần param view_role để backend trả đúng dataset | Manager/BOD không có trong bảng hoa hồng nên không có % để quy đổi; raw aggregate đúng business meaning với role quản lý | ✅ Locked v1.0 (Ref: A12 KPI × Role canonical) |
| DEC-025 | Noti trigger operator: overdue_days >= threshold_days (inclusive — gửi từ chính ngày đạt ngưỡng). Vd: ngưỡng 30 → khách overdue đúng 30 ngày hôm đó nhận noti | Tránh delay 1 ngày khiến khách rơi vào high risk trước khi cảnh báo; QA verify bằng test seed overdue_days = threshold_days | ✅ Locked v1.0 (Ref: FR-009) |
| DEC-026 | Noti re-trigger rules (4 rule canonical): (1) Gửi mỗi ngày cho mỗi (owner, customer, bucket) nếu vẫn quá hạn — KHÔNG dedupe cross-day. (2) Khi customer chuyển bucket (vd watch_list → high_risk) → gửi noti mới ngay trong ngày chuyển bucket với template highlight bucket mới. (3) Khi customer thanh toán 1 phần nhưng vẫn còn nợ → vẫn nằm trong list daily — KHÔNG reset. (4) Khi customer settled hết (paid = total) → drop khỏi list ngay từ ngày sau | Cảnh báo cần daily reminder để giữ khách trong scope hành động; bucket change là sự kiện critical cần biết ngay | ✅ Locked v1.0 (Ref: FR-009 + dev-spec C6.1) |
| DEC-027 | Service representative tie-break cho cột "Dịch vụ" SCR-02: Sort consultation_date DESC, order_service_id DESC. Khách có nhiều DV cùng lượt tư vấn → hiển thị "{tên DV chính} (+N DV khác)" với tooltip liệt kê đầy đủ | Tránh hiển thị random; chọn DV mới nhất để user biết touchpoint cuối cùng | ✅ Locked v1.0 (Ref: FORMULA-007) |
| DEC-028 | Cột "Cuộc gọi" SCR-02 — LOCKED Option E: Source = ecommerce_user.last_contacted_at (denormalized field được app mobile cập nhật khi Sale gọi qua tính năng click-to-call). Format: "Gần nhất: HH:mm DD/MM/YYYY". Icon: phone xanh. Scope: all-time, không filter theo dashboard period. Empty: "Chưa gọi" nếu NULL. Tooltip: "Cuộc gọi gần nhất từ app Diva Staff cho khách này" | Đúng intent business (track cuộc gọi đòi nợ); reuse field denormalized hiện có → query nhanh; app mobile đã ghi sẵn vào debt_contact_log + update ecommerce_user.last_contacted_at qua action recordDebtFollowupContact | ✅ Locked v1.0 (Confirmed 2026-05-14) |
| DEC-029 | % tham gia khi KH có nhiều đơn cùng ngày — weighted avg theo order.total (Ref FORMULA-008). Công thức: weighted_pct = SUM(participation_per_order × order.total) / SUM(order.total). Hiển thị 1 row/KH với % weighted; tooltip drill-down liệt kê chi tiết per-order. KHÔNG split thành nhiều row | Sale gọi đòi nợ ở cấp KHÁCH (1 cuộc gọi cho KH), không gọi từng đơn → 1 row/KH hợp lý. Weighted avg theo nợ cho Sale biết mức độ tham gia tổng → quyết định ưu tiên gọi. Tooltip drill-down giải quyết visibility per-order | ✅ Locked v1.0 (Confirmed 2026-05-14) |
| DEC-030 | Default role grants cho debt_manager modules (Ref PD-001 resolved): cấp cho roles admin + it_leader mặc định khi onboard. Các role khác (branch_manager, sale, cskh, telesale) cấu hình theo Dynamic Permission v2 per-tenant (không seed mặc định) | Admin + IT Leader luôn cần truy cập để vận hành + debug; các role business roles giao PO/Tenant tự cấu hình | ✅ Locked v1.0 (Confirmed 2026-05-15) |
| DEC-031 | Cron daily_debt_alert chạy 7h00 sáng (Asia/Ho_Chi_Minh) — Ref PD-002 resolved (updated 2026-05-15). Gửi alert cho debt_owner trước giờ làm việc để có cả ngày gọi. SLA: 100% noti delivered trước 8h00 (60 phút processing window) | Sale cần biết khách nợ TRƯỚC khi vào ca → có thời gian plan call list trong ngày. 7h sáng kịp Sale check trước giờ làm 8h | ✅ Locked v1.0 (Confirmed 2026-05-15) |
| DEC-032 | Tooltip "Doanh thu ròng" wording rõ ràng (Ref QUESTION-PROD-001 resolved Option A): formula GIỮ NGUYÊN net_revenue = SUM(actual_revenue) (FORMULA-003). Tooltip mới: "Doanh thu ròng (ghi nhận kế toán) = Tổng giá đơn − Chiết khấu − Hoàn trả. ⚠️ Đây là doanh thu đã ghi nhận, KHÔNG phải tiền đã thu về quỹ. Phần khách còn nợ chưa được trừ. Để xem tiền thực thu → KPI 'Đã thu trong kỳ'." Sub-label thêm cảnh báo "≠ tiền thực thu" | Tách rõ "doanh thu ghi nhận" (booked) vs "tiền thực thu" (cash). Codebase đã có sẵn cả 2 KPI ("Doanh thu ròng" + "Đã thu trong kỳ") → không cần thêm formula. Không phá kế toán + commission Sale | ✅ Locked v1.0 (Confirmed 2026-05-15) |
| DEC-033 | Banner "Ưu tiên xử lý nợ" as-of TODAY (Ref QUESTION-PROD-003 resolved Scenario 2): Banner LUÔN dùng NOW() cho COUNT khách + SUM tiền nợ — KHÔNG phụ thuộc filter date của dashboard. Scope branch + Nhân sự VẪN áp dụng (Manager xem branches mình, Sale xem khách mình). Sub-label cảnh báo: "⚡ Dữ liệu hiện tại — không phụ thuộc bộ lọc thời gian" (font 11px italic). Button [Xử lý ngay →] → mở SCR-03 với filter date RESET về TODAY | Banner là CTA actionable, không phải report kỳ. 3 tabs phục vụ report (as-of date_to); banner phục vụ alert hành động hôm nay. Consistency với scheduler 7h00 (DEC-031) — alert daily cũng as-of TODAY | ✅ Locked v1.0 (Confirmed 2026-05-15) |
| DEC-034 | Capacity strategy: Index-first + Gate Day-30 (Ref PD-004 resolved Option D). Day-1: KHÔNG build debt_daily_snapshot materialized. Pre-deploy thêm 4 critical indexes trên order table. Monitor dashboard_query_duration_seconds p95 trong 30 ngày. Gate trigger snapshot build (Phase 2): p95 > 1.5s liên tiếp 7 ngày HOẶC p99 > 3s bất kỳ 24h HOẶC >= 3 manager complaint HOẶC order > 3M rows. Snapshot scope (nếu trigger): chỉ cho tab Thống kê — tab Hiệu suất/Công nợ + Banner vẫn on-the-fly | Tránh over-engineer Day-1. Index thường giải 80% performance issues với cost thấp. Banner as-of NOW (DEC-033) bắt buộc real-time → snapshot không cover hết. Codebase function customer_debt_dashboard_order_scope() đã stable optimized | ✅ Locked v1.0 (Confirmed 2026-05-15) |
| DEC-035 | Hasura permission hardening DEFERRED có điều kiện (Ref PD-003 declined 2026-05-15). Scope hardening pass này KHÔNG bao gồm rewrite permission YAML cho 9 bảng debt_* + customer_handover_log + ecommerce_user. KNOWN RISK accepted: Audit 2026-05-15 phát hiện hiện trạng — chỉ 1 role user với select_permissions.filter: {}, insert_permissions.check: {}, update_permissions.filter: {} → bất kỳ authenticated user có thể query/update TẤT CẢ rows của TẤT CẢ tenant (cross-staff, cross-branch, cross-tenant). Mitigation tạm thời: Trông cậy FE permission check (DEC-011 Dynamic Permission v2) + BE action validate (defense-in-depth) + UAT environment isolated từ production data. GATE BẮT BUỘC revisit trước GA: (1) Pen-test direct GraphQL access từ tài khoản Sale → verify không leak; (2) Nếu confirm leak → MUST harden Tier 1 (5 bảng critical) trước GA. Test cases QA bắt buộc: Document trong qa-test-plan TC-CP-SEC-01..05 (xem ui-spec B-EdgeCases Security) | UAT phase hiện tại với data seed/giả lập, không có production PII; effort hardening 3-5 ngày không kịp release window này; risk có thể mitigate qua FE hide + BE action validate (defense-in-depth). Phải revisit trước GA để tránh production leak | ⚠️ Deferred v1.0 (Risk accepted 2026-05-15) — REVISIT before GA |
Z2. Quyết định UX
| DEC-ID | Quyết định | Lý do | Status |
|---|---|---|---|
| DEC-UX-001 | SCR-03 là trang riêng, không phải tab | Tăng focus tác vụ xử lý nợ | ✅ Locked |
| DEC-UX-002 | Rule owner hiển thị bằng tooltip giải thích | Tránh hiểu sai KPI/alert | ✅ Locked |
| DEC-UX-003 | Config ngưỡng: global + branch override ở MVP | Đồng bộ backend và QA | ✅ Locked |
| DEC-UX-004 | Wizard handover 3 bước + preview bắt buộc | Giảm rủi ro bàn giao sai | ✅ Locked |
| DEC-UX-005 | Filter cascade: Chi nhánh đổi → reset Nhân sự; Vai trò đổi → filter Nhân sự | Tránh chọn nhầm tổ hợp vô nghĩa | ✅ Locked |
| DEC-UX-006 | Mobile chỉ xem data cá nhân, không filter theo nhân sự khác | Đơn giản hóa mobile UX | ✅ Locked |
| DEC-UX-007 | Màu Aging Badge: Xanh/Vàng/Cam/Đỏ theo 4 bucket | Nhận diện rủi ro nhanh | ✅ Locked |
| DEC-UX-008 | Xác nhận handover cần tick 2 checkbox trước khi bật nút | Giảm rủi ro bàn giao nhầm | ✅ Locked |
| DEC-UX-009 | Lịch nhắc xem qua drawer trên web (SCR-03) + section trong chi tiết khách (MOB-03) | Giữ context nợ, không tách trang riêng | ✅ Locked |
Z3. Quyết định kỹ thuật
| DEC-ID | Quyết định | Lý do | Status |
|---|---|---|---|
| DEC-TECH-001 | Owner rule: hoa hồng cấp dịch vụ | Đồng bộ business + scheduler | ✅ Locked |
| DEC-TECH-002 | Threshold rule: branch override | MVP hỗ trợ override chi nhánh | ✅ Locked |
| DEC-TECH-003 | Snapshot debt_owner_id khi phát sinh nợ | Tránh drift lịch sử | ✅ Locked |
| DEC-TECH-004 | Scheduler in-app + push mobile, dedupe theo ngày | Giảm duplicate notification | ✅ Locked |
| DEC-TECH-005 | Handover: transaction + audit + rollback 24h RBAC | Cân bằng tốc độ và an toàn | ✅ Locked |
| DEC-TECH-006 | Export: chỉ XLSX, row cap 200k, chunking tự động | Chuẩn hóa format + tránh timeout | ✅ Locked |
| DEC-TECH-007 | Mobile API: dùng chung BE API với web, RBAC mobile-scoped | Tái sử dụng, giảm surface area | ✅ Locked |
| DEC-TECH-008 | Push notification dùng Firebase Cloud Messaging (FCM) cho cả iOS + Android | FCM đa nền tảng, giảm maintain (PD-008 → DEC-018) | ✅ Locked |
| DEC-TECH-009 | Handover limit 200 khách/transaction. Vượt → chia batch | Tránh transaction timeout + align export cap (PD-005 → DEC-017) | ✅ Locked |
Z4. Quyết định QA
| DEC-ID | Quyết định | Test bắt buộc | Status |
|---|---|---|---|
| DEC-QA-001 | visit_count vs unique phân biệt | 1 khách nhiều sự kiện tư vấn cùng ngày/cùng CN vẫn 1 lượt; khác ngày hoặc khác CN mới tăng lượt | ✅ Locked |
| DEC-QA-002 | Owner rule 3 cấp fallback | Test đủ 3 cấp khi thiếu dữ liệu | ✅ Locked |
| DEC-QA-003 | Threshold branch_override ?? global | Test override và fallback | ✅ Locked |
| DEC-QA-004 | Scheduler dedupe theo ngày | Retry không gửi trùng | ✅ Locked |
| DEC-QA-005 | Handover rollback 24h (toàn bộ) | Success trong 24h + deny ngoài 24h + deny partial | ✅ Locked |
| DEC-QA-006 | 50/50 Sale không chồng chéo KPI | Phân bổ % + tie-break đúng | ✅ Locked |
| DEC-QA-007 | Manager chỉ xem branch mình quản lý | Test RBAC branch scope | ✅ Locked |
| DEC-QA-008 | Mobile push notification deeplink đúng màn | Tap noti → đúng screen | ✅ Locked |
A) Product Requirements Document (PRD)
A1. Module Blueprint
| Module | Mục tiêu | Điểm cốt lõi | Platform |
|---|---|---|---|
| 1. Nguồn phân bổ người phụ trách | Xác định đúng ai chịu trách nhiệm nợ | Nguồn: hoa hồng cấp order_service | Web + Mobile |
| 2. Dashboard KPI | Xem hiệu suất tư vấn/chuyển đổi/nợ | Tách rõ khách đang nợ vs nợ theo tỷ lệ phụ trách | Web (đầy đủ) + Mobile (cá nhân) |
| 3. Danh sách hành động nợ (SCR-03 / MOB-02) | Đội vận hành biết làm gì | Aging bucket + scope chip vai trò (Sale/Telesale/CSKH) + CTA gọi/lịch | Web + Mobile |
| 4. Cảnh báo nợ in-app + push | Nhắc đúng người, đúng lúc | Chỉ gửi owner chính; dedupe; push mobile | Web + Mobile |
| 5. Bàn giao khách | Bàn giao an toàn | Wizard 3 bước + rollback 24h (toàn bộ) + audit | Web only (MVP) |
| 6. Kiểm soát minh bạch | Chống tranh cãi KPI | Hiển thị owner + người tham gia (%) | Web |
Flow thao tác hằng ngày
- Job nợ chạy hằng ngày → tính overdue + aging bucket
- Gửi in-app noti (web) + push notification (Diva Partner App) cho owner chính
- Owner mở SCR-03 (web) hoặc MOB-02 (mobile), xử lý theo độ ưu tiên
- Cập nhật kết quả → KPI và báo cáo tự refresh
- (Nếu có) Nhân sự nghỉ → chạy wizard bàn giao trên web → audit / rollback
A2. Context & Problem Statement
Hiện trạng:
- ✅ Đã có báo cáo vận hành theo nhân sự và báo cáo công nợ đơn hàng
- ✅ Đã có báo cáo doanh thu nhân viên + tổng công nợ phân bổ
- ✅ Đã có job nhắc việc vận hành theo lịch
- ❌ Chưa có job nhắc công nợ quá hạn theo người phụ trách
- ❌ Chưa có nghiệp vụ bàn giao danh sách khách khi nhân viên nghỉ
- ❌ Chưa có mobile app để nhân viên theo dõi nợ khi không ở bàn
A3. Goals / Non-goals / Scope
| G-ID | Mục tiêu |
|---|---|
| G-001 | Danh sách khách đã tư vấn theo ngày/tuần/tháng |
| G-002 | Cảnh báo công nợ quá hạn hằng ngày theo ngưỡng cấu hình (web + mobile push) |
| G-003 | Quy trình bàn giao khách khi nhân sự nghỉ — có audit |
| G-004 | Dashboard KPI chốt + nợ + thu hồi nợ theo nhân sự |
| G-005 | Mobile app (Diva Partner) để nhân viên xem KPI + xử lý nợ cơ bản |
Non-goals:
- ❌ Không thay đổi engine tính commission hiện hữu
- ❌ Không thay thế toàn bộ report legacy trong phase MVP
- ❌ Không triển khai ML dự báo nợ giai đoạn đầu
- ❌ Không hỗ trợ Handover Wizard trên mobile (MVP — pending PD-007)
Roadmap:
| Phase | Nội dung |
|---|---|
| MVP-Lite (hiện tại) | Dashboard + debt alert + manual handover + KPI core + Mobile cơ bản |
| Phase 2 | SLA follow-up + priority ranking + auto-escalation + benchmark + Mobile Handover |
| Phase 3 | Automation rules, predictive scoring |
A4. Personas & Journeys
| Persona | Role | Journey chính | Platform chính |
|---|---|---|---|
| P1 | Sale / CSKH / Telesale | Nhận push → mở app → xem nợ → gọi khách → đánh dấu đã LH | Mobile (hằng ngày) + Web (báo cáo) |
| P2 | Sales/Branch Manager | Dashboard → so sánh conversion và debt ratio theo nhân sự trong branch mình | Web |
| P3 | HR Ops | Tạo handover → chọn nguồn/đích → preview → xác nhận | Web |
A5. Functional Requirements
Nguyên tắc nền tảng: Đơn vị tính chuẩn = order_service. Nguồn attribution = hoa hồng cấp dịch vụ. KPI tổng hệ thống lấy từ bảng gốc — không cộng ngược. So sánh nhân sự chỉ trong cùng role.
FR-001: Bộ lọc thời gian và nhân sự
- Bộ lọc ngày/tuần/tháng + filter theo vai trò, chi nhánh, nhân sự.
- Cascading filter rule (mới — DEC-UX-005):
- Chi nhánh đổi → reset Nhân sự về "Tất cả"
- Vai trò đổi → filter danh sách Nhân sự theo role đã chọn, giữ Chi nhánh
- Preset đổi → không reset dropdown filter
- Default mở lần đầu: Tháng này + Tất cả chi nhánh (theo quyền) + Tất cả vai trò
- Mobile: Không có filter chi nhánh/nhân sự — chỉ filter thời gian cho data cá nhân
- Manager scope: Chỉ thấy branch đang quản lý trong dropdown Chi nhánh (DEC-013)
- AC: Filter đổi giá trị → cập nhật dữ liệu < 2s p95 cho range 30 ngày
FR-002: Danh sách khách đã tư vấn
- Nguồn chuẩn: danh sách người có mặt trong hoa hồng theo role + % hưởng ở cấp order_service.
- Mặc định đo theo lượt (visit-level), có toggle sang khách duy nhất (customer-level).
- Một dịch vụ có thể thuộc nhiều người và nhiều role — mỗi người nhận đúng phần của mình theo %.
Ví dụ phân bổ (Khách A, 1 đơn, 2 dịch vụ):
| Dịch vụ | Role | Nhân sự | % hưởng | Ghi nhận KPI cho |
|---|---|---|---|---|
| DV-A | Sale | Sale 1 | 100% | Sale 1 nhận phần DV-A |
| DV-A | Telesale | Telesale 2 | 100% | Telesale 2 nhận phần DV-A |
| DV-B | Sale | Sale 2 | 100% | Sale 2 nhận phần DV-B |
| DV-B | CSKH | CSKH 1 | 100% | CSKH 1 nhận phần DV-B |
- Cột "% hưởng" là phần trăm hoa hồng của role đó trên dịch vụ đó — không phải tổng giữa các role
Rule chọn "Phụ trách chính" trên popup hoa hồng (per dịch vụ):
| Case | Số Sale trên DV | Behavior | Radio hiện? |
|---|---|---|---|
| 1 Sale | 1 | Auto set is_primary_owner = true cho Sale đó. Không cần user action | ❌ Không hiện (implicit, hiện label "✅ Phụ trách chính" readonly) |
| 2+ Sale | ≥ 2 | Hiện radio "Phụ trách chính" — user BẮT BUỘC chọn đúng 1 người | ✅ Hiện radio, mandatory |
| 0 Sale | 0 (chỉ có CSKH/Telesale) | Không áp dụng rule phụ trách chính Sale. Owner fallback → in_charge | ❌ Không hiện |
- Trường hợp 1 dịch vụ có 2 Sale 50/50: Hiển thị chip "Sale 1 (50%) | Sale 2 (50%)". KPI: mỗi người 50%. Owner chính: ưu tiên người được chọn "Phụ trách chính" trên popup hoa hồng; nếu dữ liệu legacy chưa có cờ này thì fallback tie-break FR-009
- Trường hợp 1 dịch vụ có 3+ Sale (VD: 40/30/30): Hiện radio 3 options, user chọn đúng 1 người làm phụ trách chính
Validation popup hoa hồng:
- Khi có 2+ Sale trên bất kỳ dịch vụ nào mà chưa chọn "Phụ trách chính" → nút "Xác nhận" disabled
- Hiển thị warning: "Vui lòng chọn người phụ trách chính cho Sale trên dịch vụ [tên DV]"
- Đơn hàng có nhiều dịch vụ: phải chọn xong TẤT CẢ dịch vụ có 2+ Sale mới được submit
Wireframe: Popup hoa hồng — Case 2+ Sale (cần chọn)
┌──────────────────────────────────────────────────────────┐
│ Hoa hồng — DV: Chăm sóc da trắng sáng (1.000.000đ) │
├──────────────────────────────────────────────────────────┤
│ │
│ Sale Phụ trách chính │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ◉ Sale 1 — Nguyễn Văn A [50%] ☑ Chính │ │
│ │ ○ Sale 2 — Trần Thị B [50%] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ CSKH │
│ ┌────────────────────────────────────────────────────┐ │
│ │ CSKH 1 — Lê Văn C [100%] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ [Hủy] [Xác nhận] │
└──────────────────────────────────────────────────────────┘Wireframe: Popup hoa hồng — Case 1 Sale (auto, không cần chọn)
┌──────────────────────────────────────────────────────────┐
│ Hoa hồng — DV: Meso HA căng bóng da (3.000.000đ) │
├──────────────────────────────────────────────────────────┤
│ │
│ Sale Phụ trách chính │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Sale 1 — Nguyễn Văn A [100%] ✅ Chính │ │
│ └────────────────────────────────────────────────────┘ │
│ (Tự động gán — chỉ có 1 Sale) │
│ │
│ Telesale │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Telesale 1 — Phạm Thị D [100%] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ [Hủy] [Xác nhận] │
└──────────────────────────────────────────────────────────┘Wireframe: Popup hoa hồng — Case chưa chọn (validation error)
┌──────────────────────────────────────────────────────────┐
│ Hoa hồng — DV: Laser trẻ hóa toàn mặt (6.000.000đ) │
├──────────────────────────────────────────────────────────┤
│ │
│ Sale Phụ trách chính │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ○ Sale 1 — Nguyễn Văn A [40%] │ │
│ │ ○ Sale 2 — Trần Thị B [30%] │ │
│ │ ○ Sale 3 — Lê Văn E [30%] │ │
│ └────────────────────────────────────────────────────┘ │
│ ⚠ Vui lòng chọn người phụ trách chính cho Sale │
│ │
│ [Hủy] [Xác nhận] (disabled)│
└──────────────────────────────────────────────────────────┘- AC: Danh sách tư vấn đúng dữ liệu hoa hồng cấp dịch vụ, đúng quyền người xem
- AC: 1 Sale → auto primary, không hiện radio. 2+ Sale → radio bắt buộc, disabled submit cho đến khi chọn xong
FR-003: KPI tiếp khách
| KPI | Định nghĩa | Ví dụ |
|---|---|---|
| visit_count | Đếm theo grain (customer_id, branch_id, date) có visit_source chứa consultant | 1 khách có 3 lịch hẹn + 1 ghi âm cùng ngày/cùng CN → 1 lượt |
| unique_customer_count | Đếm khách duy nhất theo customer_id trong kỳ lọc | 1 khách phát sinh tư vấn 5 ngày khác nhau → 1 khách |
- AC: Hiển thị đồng thời cả 2 chỉ số, có drill-down (web). Mobile hiển thị cả 2 dạng card.
- Rule ghi nhận
consultant: có ít nhất 1 trong các điều kiện: (1) lịch hẹnconsultanttrạng tháiconfirmed/completed; (2) ghi âm hợp lệ (duration+url) gắn record/lịch hẹn; (3) đơn hàng córeference_appointment_idlàm lịch hẹn chuyểncompleted.
FR-004: KPI mua trong kỳ & chuyển đổi
closed_customer_count= khách có mua hàng VÀ thực thu > 0 (Ref: DEC-002)conversion_rate= closed_customer_count / unique_customer_count- Rule mẫu số: Nếu unique_customer_count = 0 → giá trị = 0, hiển thị N/A (Ref: DEC-010)
- AC: Chỉ tính khách mua trong kỳ khi đủ 2 điều kiện. Nếu unique = 0 → N/A.
FR-005: KPI công nợ
| KPI | Định nghĩa | Kiểu |
|---|---|---|
| debt_customer_count_distinct | Số khách nợ thực (khách duy nhất có outstanding > 0) | Số nguyên |
| debt_customer_equivalent | Số khách nợ quy đổi theo % hưởng | Số lẻ (e.g. 0.5) |
| total_debt_amount | Tổng outstanding_end_of_period | Tiền |
| allocated_debt_amount | Tổng tiền nợ quy đổi theo % hưởng (SUM(allocated_debt)) | Tiền |
| debt_ratio | total_debt_amount / total_revenue | % |
| debt_customer_ratio | debt_customer_distinct / fully_paid_customer | % |
total_revenue= SUM(paid_amount − refund_amount) [giao dịch hợp lệ trong kỳ] (Ref: DEC-009)- Rule mẫu số 0: mẫu số = 0 → hiển thị "N/A" (Ref: DEC-010)
- KPI thu nợ: 2 chỉ số weighted + full settlement (Ref: DEC-008)
allocated_amount= service_amount × commission_percent / 100allocated_debt= service_outstanding × commission_percent / 100- AC: Sai số KPI nợ ≤ 1%. Hiển thị đúng distinct, equivalent và allocated debt. Đúng rule mẫu số 0.
FR-006: KPI thời gian thu nợ
- Mốc: debt_start_date (ngày phát sinh nợ), debt_due_date (hạn thanh toán), days_to_payment_i = payment_date_i − debt_start_date (Ref: DEC-011)
- KPI chính —
avg_recovery_days_weighted= SUM(payment_amount_i × days_to_payment_i) / SUM(payment_amount_i). Càng thấp càng tốt. - KPI phụ —
avg_days_to_full_settlement= SUM(days_from_debt_start_to_full_settlement) / COUNT(fully_settled_debts). Chỉ tính khoản đã tất toán. - AC: Tính đúng timezone Asia/Ho_Chi_Minh cho cả 2 công thức.
FR-007: Aging buckets
Mỗi khoản nợ thuộc đúng 1 bucket — tính từ debt_due_date, timezone Asia/Ho_Chi_Minh:
| Nhóm nợ (bucket nội bộ) | Ngày quá hạn | Mức rủi ro | Màu badge |
|---|---|---|---|
| 0-29 | 0–29 ngày | Mới quá hạn — nguy cơ thấp | 🟢 Xanh lá (#E8F5E9 / #2E7D32) |
| 30-59 | 30–59 ngày | Cần follow-up sát | 🟡 Vàng (#FFF8E1 / #F57F17) |
| 60-89 | 60–89 ngày | Rủi ro cao | 🟠 Cam (#FFF3E0 / #E65100) |
| ≥90 | ≥ 90 ngày | Rủi ro rất cao | 🔴 Đỏ (#FFEBEE / #B71C1C) |
- AC: Không overlap bucket; mỗi khoản nợ thuộc đúng 1 bucket.
FR-008: Cấu hình ngưỡng cảnh báo
threshold_days= branch_override ?? global_threshold (Ref: DEC-004)- VD: global = 30 ngày, branch A override = 45 ngày → branch A dùng 45, branch khác dùng 30
- Validation: threshold_days min=1, max=365. Giờ gửi trong 06:00–10:00 VN timezone.
- AC: Áp dụng đúng precedence; hiệu lực từ lần scheduler kế tiếp sau khi lưu.
FR-009: Cảnh báo nợ hằng ngày
- Job chạy hằng ngày, gửi in-app (web) + push notification (mobile) cho owner chính (Ref: DEC-007)
- Trigger operator (Ref DEC-025): Gửi noti khi
overdue_days >= threshold_days(inclusive — từ chính ngày đạt ngưỡng, không delay 1 ngày) - Re-trigger rules (Ref DEC-026):
- Daily resend: Gửi mỗi ngày cho mỗi tuple
(owner_id, customer_id, current_bucket)nếu khách vẫn quá hạn — KHÔNG dedupe cross-day - Bucket change → priority noti: Khi khách chuyển bucket (vd watch_list → high_risk) → gửi noti mới ngay trong ngày chuyển bucket, với template highlight escalation: "⚠️ Khách đã chuyển sang nhóm "
- Partial payment → vẫn nhắc: Khách thanh toán 1 phần nhưng vẫn còn nợ → vẫn nằm trong list daily, KHÔNG reset
- Settled → drop ngay: Khách settled hết nợ (paid = total) → drop khỏi list noti từ ngày sau ngày settled
- Daily resend: Gửi mỗi ngày cho mỗi tuple
- Rule xác định owner (owner_debt):
commission_is_primary_owner[được chọn từ popup hoa hồng cấp dịch vụ]- →
commission_highest_percent[nếu không có bước 1] - →
in_charge[fallback cuối]
- Tie-break kỹ thuật (chỉ dùng cho dữ liệu legacy chưa có
commission_is_primary_ownervà vẫn hòa):- Ưu tiên
commission_primary_by_role - →
commission_created_at ASC(bản ghi cũ hơn) - →
user_id ASC(nhỏ hơn)
- Ưu tiên
- Noti phải có: số khách nợ quá hạn + nút deep-link vào đúng danh sách (SCR-03 / MOB-02)
- Mobile push: kèm badge count = overdue_count
- Customer aging bucket cho noti payload: dùng FORMULA-006B (customer-level, max bucket), KHÔNG order-level
- AC:
- ≥ 95% cảnh báo gửi đúng lịch
- Không gửi trùng trong cùng tuple (owner, customer, bucket, date)
- Case 50/50 chọn đúng 1 owner
- Khách overdue đúng
threshold_days→ có noti ngày đó (DEC-025) - Khách chuyển bucket → có noti escalation ngày chuyển (DEC-026 rule 2)
- Khách settled → không còn noti từ ngày sau (DEC-026 rule 4)
FR-010: Workflow bàn giao khách
- Wizard 3 bước: Chọn nguồn → Chọn đích + preview → Xác nhận (Ref: DEC-005, DEC-UX-004)
- Cho phép chọn toàn bộ hoặc theo filter
- Handover chuyển cả nhắc nợ + quyền xem lịch sử (Ref: DEC-006)
- Rollback: Chỉ toàn bộ, không hỗ trợ partial (DEC-015)
- Giới hạn 200 khách/lần (Ref: DEC-017, DEC-TECH-009)
- Platform: Web only (MVP)
- AC:
- Handover hoạt động đúng cho cả 2 trường hợp chọn (toàn bộ / theo filter)
- Preview diff phải khớp kết quả thực tế sau xác nhận
- Rollback trong 24h hoàn tác toàn bộ, không partial
- Notification gửi cho cả Creator + Target (Ref: DEC-016)
- Vượt 200 khách → block + error message
FR-011: Audit handover
- 100% thao tác handover có audit trail: who / when / before / after — immutable
- Bao gồm cả thao tác rollback
- AC: Không có thao tác handover/rollback nào thiếu log.
FR-012: Export
- Export XLSX (chỉ XLSX — DEC-014) theo bộ lọc và quyền
- Row cap 200k/job, chunking khi vượt
- Spec cột chi tiết: xem Section B-Export
- AC: Export đúng tập dữ liệu theo filter và quyền; không lộ dữ liệu ngoài quyền.
FR-013: Quản lý lịch nhắc follow-up
- User tạo lịch nhắc (đã có popup) → cần xem lại, đánh dấu hoàn thành, hủy các lịch đã tạo.
- Khi đến giờ
remind_at, hệ thống gửi in-app notification (web) choassigned_to. Không push mobile (DEC-020). - Vị trí xem lịch nhắc:
- Web: Drawer mở từ SCR-03 (nút [📅 Lịch nhắc] trên toolbar hoặc nút bên cạnh từng khách)
- Mobile: Section "Lịch nhắc" trong MOB-03 (chi tiết khách nợ)
- Hiển thị chi tiết:
- Thông tin khách: tên + tiền nợ + quá hạn (ngày)
- Lịch nhắc: ngày giờ nhắc + nội dung + ghi chú + trạng thái (Chờ / Đã xong / Hủy / Quá hạn)
- Lịch sử: timeline các lần tạo lịch nhắc cho cùng 1 khách
- Hành động:
- ✅ Đánh dấu "Đã xong" → chuyển status =
done, ghicompleted_at - ❌ Hủy → chuyển status =
cancelled - 🔄 Tạo lại → mở popup tạo lịch nhắc mới cho cùng khách
- ✅ Đánh dấu "Đã xong" → chuyển status =
- Filter (web drawer):
- Trạng thái: Tất cả / Chờ / Đã xong / Hủy / Quá hạn
- Sắp xếp mặc định:
remind_at ASC(gần nhất lên đầu)
- Quy tắc "Quá hạn":
status = 'pending' AND remind_at < now()→ hiển thị badge đỏ "Quá hạn" - Ref: DEC-020, DEC-021, DEC-UX-009
AC:
- User tạo lịch nhắc → xem lại được trong drawer (web) hoặc section (mobile).
- Khi đến
remind_at,assigned_tonhận in-app notification với nội dung nhắc + deeplink đến khách. - User đánh dấu "Đã xong" → status chuyển
done, hiển thịcompleted_at. - Lịch nhắc quá hạn (chưa xử lý) hiển thị badge "Quá hạn" rõ ràng.
- Xem được timeline tất cả lịch nhắc cho 1 khách (lịch sử follow-up).
FR-014…020: Tab Thống kê công nợ (Admin/Manager)
Nguồn chuẩn chi tiết:
enhancement-analytics-tab.mdv1.5. Parent spec khóa phạm vi tích hợp như sau:
FR-014: TabThống kêtrong SCR-01, chỉ hiển thị cho Manager/Admin, URL hash#analytics.FR-015: 6 KPI cố định (không toggle): Tổng nợ hiện tại, Đã thu trong kỳ, Tỷ lệ thu nợ, Khách còn nợ, Đã tất toán, Tỷ lệ nợ/doanh thu (ròng).FR-016: Chart so sánh có segment[Chi nhánh/Nhân viên], sort chip[Đã thu/Còn nợ], luôn hiển thị cả amount + rate.FR-017: Chart xu hướng 3 line (Tổng nợ,Đã thu,Nợ phát sinh) bật mặc định, có thể bật/tắt bằng legend.FR-018: Leaderboard nhân sự thu nợ (phân trang, sort, top 3 highlight, tie-break theo rule nghiệp vụ).FR-019: Drawer chi tiết nhân sự (720px) gồmChỉ số Công nợ,Phân loại nhóm nợ,Top 5 khách nợ cao nhất.FR-020: Xuất XLSX tabThống kêtheo quyền và bộ lọc.
A6. Assumptions, Constraints & Dependencies
| ID | Loại | Nội dung |
|---|---|---|
| A-001 | Assumption | Lưu snapshot debt_owner_id tại thời điểm phát sinh nợ |
| A-002 | Assumption | Aging tính theo current_date − debt_due_date, timezone VN |
| A-003 | Assumption | RBAC kế thừa cơ chế CRM/report hiện có |
| A-004 | Assumption | Nguồn chuẩn: hoa hồng cấp dịch vụ trong đơn |
| A-005 | Assumption | Diva Partner App đã có infrastructure để nhận push notification |
| C-001 | Constraint | Dữ liệu phân tán giữa order, invoice, commission |
| C-002 | Constraint | Tương thích với query/report legacy |
| C-003 | Constraint | Mobile API dùng chung endpoint với web, RBAC mobile-scoped |
A7. Risks & Mitigations
| R-ID | Rủi ro | Mitigation |
|---|---|---|
| R-001 | Mapping owner không nhất quán | Khóa rule DEC-003 + backfill + audit sample |
| R-002 | Query KPI nặng khi range lớn | Materialized view/cache + index + export chunking |
| R-003 | Handover sai phạm vi | Preview diff bắt buộc + xác nhận + rollback 24h |
| R-004 | Push notification mobile không đến tay nhân viên | Fallback in-app web; alert Ops khi delivery rate < 90% |
| R-005 | Feature flag ff_mobile_push_notification cho FCM rollout |
A8. Success Metrics
| Chỉ tiêu | Mục tiêu |
|---|---|
| Nhân sự có dashboard daily (web) | 100% |
| Nhân sự cài Diva Partner App và bật push | ≥ 80% trong 30 ngày sau go-live |
| Cảnh báo nợ gửi đúng lịch | ≥ 95% |
| Ca nghỉ việc có handover trước ngày hiệu lực | ≥ 90% |
| Giảm tỷ lệ nợ quá hạn > 30 ngày | Baseline đo ở sprint 0 |
A9. Glossary (Thuật ngữ)
| Thuật ngữ (VI) | Thuật ngữ (EN) | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Nợ quá hạn | Overdue debt | Khách có total_debt_amount > 0 và overdue_days > threshold_days | ≠ Khách đang nợ (đang nợ = total_debt_amount > 0, không cần quá hạn) |
| Phụ trách chính | Primary owner | Sale được set is_primary_owner=true trên order_service — sẽ trở thành order.debt_owner_id Day-1 | ≠ Phụ trách phụ (in_charge fallback) |
| Aging bucket | Aging bucket | 4 nhóm new_debt (0-29), watch_list (30-59), high_risk (60-89), very_high_risk (90+) theo overdue_days | — |
| Bàn giao | Handover | Chuyển debt_owner_id từ NV nguồn sang NV đích kèm audit | ≠ Rollback (trả về NV cũ) |
| Hoàn tác bàn giao | Rollback | Trả debt_owner_id về NV cũ trong 24h sau handover | Chỉ khả dụng trong 24h |
| Cảnh báo nợ | Debt alert | Push/in-app notification gửi cho debt_owner khi có nợ quá hạn | ≠ Lịch nhắc follow-up (do staff tự tạo) |
| Lịch nhắc follow-up | Follow-up task | Task debt_followup_task do staff tạo để nhắc bản thân/team chăm sóc khách | ≠ Cảnh báo nợ (system tạo) |
| Đã liên hệ | Marked contacted | Record vào debt_contact_log ghi nhận đã liên hệ khách | ≠ Hoàn thành lịch nhắc (mark task done) |
| Scope chip vai trò | Role scope chip | Filter trong tab Công nợ: Sale / Telesale / CSKH — không phải role permission | ≠ Permission role (admin/manager/staff) |
A10. Công thức nghiệp vụ (canonical)
Single-source rule: A10 là canonical business definition. Dev spec (C3) chỉ ghi SQL implementation delta, ref A10.
FORMULA-001: Số khách đã tư vấn
- Mô tả: Số khách hàng khác nhau (distinct) đã được tư vấn trong kỳ
- Công thức:
COUNT(DISTINCT customer_id) WHERE has_consultation_in_period = true - Nguồn: Function
customer_debt_dashboard_order_scope()— filterconsultant_id IS NOT NULL - Đơn vị: số nguyên (khách)
- Edge cases: Khách tư vấn nhiều lần trong kỳ → chỉ tính 1; tư vấn null → exclude
FORMULA-002: Tỷ lệ chuyển đổi sau tư vấn
- Mô tả: % khách đã tư vấn mà phát sinh đơn mua trong kỳ
- Công thức:
conversion_rate = closed_customer_count / consulted_customer_count × 100 - Biến số:
closed_customer_count: khách cóorder.actual_revenue > 0trong kỳconsulted_customer_count: từ FORMULA-001
- Đơn vị: % (2 decimal)
- Edge cases: denominator = 0 → hiển thị "—"
FORMULA-003: Doanh thu ròng + Tỷ lệ công nợ trên doanh thu (CANONICAL — Ref DEC-022)
Đây là single-source canonical cho "Doanh thu ròng". Mọi nơi (PRD, UI tooltip, dev SQL, QA expected value) phải dùng định nghĩa này. KHÔNG dùng formula khác.
- Mô tả:
net_revenue(Doanh thu ròng) = doanh thu thực tế đã chiết khấu + trừ refund, KHÔNG bao gồm wallet paymentdebt_ratio= % công nợ còn lại trên doanh thu ròng
- Công thức canonical:
net_revenue = SUM(order.actual_revenue) WHERE order.created_at IN period AND order.status != 'cancelled' AND order.deleted_at IS NULL AND order.payment_method_id NOT IN ('wallet', 'wallet_promotion') debt_ratio = total_debt / net_revenue × 100 - Biến số:
order.actual_revenue(TEXT/NUMERIC) — column đã pre-computed bởi BE, đã apply refund + discount (KHÔNG cần JOIN invoice/refund nữa)total_debt:SUM(GREATEST(order.total - order.paid_amount, 0))cùng filter trên
- Đơn vị:
net_revenueVND (integer);debt_ratio% (2 decimal) - Edge cases:
- denominator = 0 → hiển thị "—" (UI), trả
NULL(BE) - Wallet payment exclusion bắt buộc (Ref Pitfalls Map § Payment methods)
- Khách có nhiều đơn, 1 đơn hoàn trả →
actual_revenuecủa đơn đó đã trừ phần hoàn (không cần thêm logic) - View Manager/BOD: dùng formula trên với scope
branch_id IN allowed_branches(Manager) hoặc no filter (BOD) — KHÔNG quy đổi % hoa hồng (Ref DEC-024)
- denominator = 0 → hiển thị "—" (UI), trả
FORMULA-004: Tỷ lệ thu nợ
- Mô tả: % nợ đã thu trong kỳ so với tổng nợ phát sinh
- Công thức:
collection_rate = paid_in_period / total_debt_created × 100 - Đơn vị: % (2 decimal)
- Edge cases: denominator = 0 → "—"
FORMULA-005: Số ngày thu nợ trung bình (avg_days_to_collect)
- Mô tả: Thời gian trung bình từ khi nợ phát sinh đến khi thanh toán đủ
- Công thức:
AVG(settled_at - debt_started_at)trêndebt_order_item_snapshot WHERE is_settled = true - Đơn vị: số ngày (1 decimal)
- Edge cases: chưa có settled record → "—"
FORMULA-006: Nhóm nợ (Aging bucket) — Order-level
- Mô tả: Phân loại 1 ĐƠN HÀNG nợ theo
overdue_days(per-order). Đây là building block; cấp customer rollup ở FORMULA-006B. - Công thức:
order_bucket = CASE WHEN overdue_days <= 29 THEN 'new_debt' WHEN overdue_days <= 59 THEN 'watch_list' WHEN overdue_days <= 89 THEN 'high_risk' ELSE 'very_high_risk' END - TZ:
Asia/Ho_Chi_Minh—overdue_days = (NOW() AT TIME ZONE 'Asia/Ho_Chi_Minh')::date - order.created_at::date - Nguồn:
customer_debt_dashboard_order_scope()migration1776054869987_* - Edge cases:
overdue_days < 0(chưa đến due_date) → exclude; NULL due_date → exclude
FORMULA-006B: Nhóm nợ cấp KHÁCH HÀNG (Customer-level aging) — Ref DEC-023
Quy tắc khách có nhiều đơn ở nhiều bucket — đây là canonical rollup cho biểu đồ phân loại + bảng aging count.
- Mô tả: Khách có nhiều
ordernợ → gán khách vào bucket cao nhất (max risk) - Công thức:sql
WITH customer_orders AS ( SELECT customer_id, order_bucket, -- từ FORMULA-006 overdue_days, GREATEST(order.total - order.paid_amount, 0) AS order_remaining FROM order WHERE order.total - order.paid_amount > 0 -- chỉ đơn còn nợ AND order.status != 'cancelled' AND order.deleted_at IS NULL AND order.payment_method_id NOT IN ('wallet', 'wallet_promotion') ), customer_max AS ( SELECT customer_id, MAX(overdue_days) AS max_overdue_days, SUM(order_remaining) AS total_customer_debt FROM customer_orders GROUP BY customer_id ) SELECT customer_id, CASE WHEN max_overdue_days <= 29 THEN 'new_debt' WHEN max_overdue_days <= 59 THEN 'watch_list' WHEN max_overdue_days <= 89 THEN 'high_risk' ELSE 'very_high_risk' END AS customer_bucket, total_customer_debt FROM customer_max; - Rule quan trọng (cho biểu đồ phân loại + bảng aging):
- Đếm khách (count): mỗi khách = 1 unit, ở
customer_bucketcao nhất duy nhất - Tổng tiền nợ trong bucket:
SUM(total_customer_debt)của tất cả KH trong bucket đó (KHÔNG split theo per-order) - Không double-count: KH có 3 đơn (15d / 45d / 95d) → đếm 1 KH ở
very_high_risk; tổng tiền nợ = SUM của 3 đơn, gán vàovery_high_risk
- Đếm khách (count): mỗi khách = 1 unit, ở
- Edge cases:
- KH có 1 đơn 95d (very_high_risk) + 1 đơn 5d (new_debt) → vẫn xếp vào
very_high_risk(max) - KH đã settled tất cả đơn → exclude khỏi list
- KH có 1 đơn đang nợ + 1 đơn future-dated chưa overdue (
overdue_days < 0) → exclude đơn future, tính bucket trên đơn đang nợ
- KH có 1 đơn 95d (very_high_risk) + 1 đơn 5d (new_debt) → vẫn xếp vào
- UI mapping: Biểu đồ tròn phân loại nhóm nợ (SCR-01 Block C) + Bảng danh sách nợ (SCR-03 cột "Nhóm nợ") đều dùng customer-level
FORMULA-008: Weighted participation percent (cột "% tham gia" SCR-02 khi multi-order) — Ref DEC-029
Đây là canonical formula cho % tham gia khi KH có nhiều đơn nợ trong cùng grain row
(customer_id, branch_id, consulted_date). Giải quyết gap functionsearch_report_debt_performance_consultationhiện tại chỉ pick 1reference_order→ mất visibility các đơn khác.
- Ref: PRD A10 FORMULA-008 + DEC-029
- Mô tả: Khi KH có ≥2 đơn nợ liên quan đến cùng row tư vấn, % tham gia hiển thị là weighted average theo tổng giá trị từng đơn — không chỉ pick 1 đơn duy nhất
- Công thức canonical:
weighted_participation_percent = SUM(participation_percent_per_order × order.total) / SUM(order.total) -- Trong đó participation_percent_per_order = % của debt_owner trên đơn đó -- (computed theo logic order_commission hiện tại — Ref function search_report_debt_performance_consultation) - Đơn vị: % (2 decimal)
- Phạm vi đơn (orders liên quan): tất cả
orderthuộcevidence_logs.order_idcủa row tư vấn ((customer_id, branch_id, consulted_date)) códebt_owner_id IS NOT NULLvàtotal > 0 - Tooltip drill-down format (FE responsibility):
% tham gia: 41.5% (weighted theo tổng giá trị đơn) Chi tiết theo đơn: • Đơn #{order_code_A} ({format_money(total_A)}): {pct_A}% tham gia • Đơn #{order_code_B} ({format_money(total_B)}): {pct_B}% tham gia ... Tổng: {format_money(total_A)}×{pct_A}% + ... = {numerator} / {denominator} = {weighted_pct}% - Edge cases:
- Chỉ có 1 đơn → weighted = participation của đơn đó (no aggregation needed)
- Tất cả đơn có
debt_owner = NULL→ display "—" - Tất cả đơn có
total = 0→ display "—" (denominator = 0) - Đơn bị cancelled (
order_service_status IN ('order_canceled', 'prepaid_canceled')) → exclude khỏi tính
- Why weighted (không phải simple avg): Đơn lớn quan trọng hơn đơn nhỏ → weighted theo
totalcho Sale ưu tiên đúng. Vd: Sale 100% trên đơn 1M + Sale 10% trên đơn 100M → simple avg = 55% (misleading); weighted = (100%×1M + 10%×100M) / 101M = 10.9% (chính xác hơn về exposure)
FORMULA-007: Service representative (cột "Dịch vụ" SCR-02) — Ref DEC-027
- Mô tả: Khách có nhiều dịch vụ trong 1 lượt tư vấn → chọn 1 DV đại diện hiển thị + count phụ
- Rule chọn representative:sql
-- Trong mode "Theo lượt": tie-break trong 1 ngày ORDER BY consultation_date DESC, order_service_id DESC LIMIT 1 -- Trong mode "Theo khách": tie-break trong kỳ ORDER BY consultation_date DESC, order_service_id DESC LIMIT 1 - Format hiển thị:
- 1 DV duy nhất: hiển thị tên DV (vd "Tẩy nốt ruồi")
- ≥ 2 DV cùng lượt: hiển thị "
{tên DV representative}(+{N-1} DV khác)" với tooltip liệt kê đầy đủ - Vd: 3 DV → "Tẩy nốt ruồi (+2 DV khác)"
- Tooltip nội dung: Liệt kê tên tất cả DV của lượt/kỳ đó, mỗi DV 1 dòng, format:
• {tên DV} — {% Của tôi} - Edge cases:
- Khách không có DV nào (vd tư vấn không chốt) → cell trống + dash "—"
- DV có tên null trong DB → hiển thị "Dịch vụ #{order_service_id}"
A11. Traceability tóm tắt
Chi tiết Traceability Matrix (FR ↔ AC ↔ Test Case ↔ Code) xem
dev-spec.mdC12.
| FR-ID | AC | UI surface | Code | Test |
|---|---|---|---|---|
| FR-001 → FR-007 | AC-001..007 | SCR-01 + SCR-02 + SCR-03 | DebtManager* + customer_debt_dashboard_* | TC-CP-01..10 |
| FR-008, FR-009 | AC-008, AC-009 | SCR-04 + NTF-DEBT-DAILY-001 | DebtManagerSetting + daily_debt_alert.go | TC-CP-11..14 |
| FR-010, FR-011 | AC-010, AC-011 | SCR-05..08 wizard | CustomerHandover* + customer_handover_support action | TC-CP-15..20 |
| FR-012 | AC-012 | Export CTA tất cả pages | XExcel + XTable | TC-CP-21 |
| FR-013 | AC-013 | SCR-03-DRAWER + Popup + MOB-04 | debt_followup_task + DebtManagerCreateReminderSchedule | TC-CP-22..24 |
| FR-014..020 | AC-014..020 | SCR-01-TAB-ANALYTICS | DebtManagerStatistics | TC-CP-25..31 |
A12. Codebase Audit Summary 🆕
Mục đích section này: trả lời câu hỏi "với HTML intent v4.12, codebase Diva hiện tại đã đáp ứng được gì?" — giúp dev tránh build trùng.
A12.1 Bảng tổng kết Reuse 🟢 / Extend 🟡 / Build mới 🔴
| Năng lực | Status | Codebase evidence | Delta cần làm |
|---|---|---|---|
Route /dm/debt (3 tabs) | 🟢 Reuse | diva-admin/src/modules/debt-manager/modules.ts | Verify route guard dùng Dynamic Permission v2 (DEC-011) |
| Tab Hiệu suất tư vấn | 🟡 Extend | component/consulting-performance/DebtManagerConsultingPerformance.tsx | Bổ sung KPI cards FR-003, FR-004 nếu thiếu (Khách mua trong kỳ, Tỉ lệ chuyển đổi) |
| Tab Công nợ | 🟡 Extend | component/debt/DebtManagerDebt.tsx | Bổ sung scope chip vai trò Sale/Telesale/CSKH + summary banner + Daily Focus top 5 |
| Tab Thống kê | 🟡 Extend | component/statistics/DebtManagerStatistics.tsx | Bổ sung leaderboard nhân viên + drawer chi tiết NV (720px) |
| Popup tạo lịch nhắc | 🟢 Reuse | DebtManagerCreateReminderSchedule.tsx | Verify validation rule |
| Drawer lịch nhắc của tôi | 🟢 Reuse | DebtManagerActionViewCalendar.tsx + DebtManagerReminderScheduleHistory.tsx | Verify filter + permission scope |
| Cài đặt cảnh báo nợ | 🟢 Reuse | src/modules/settings/pages/DebtManagerSetting.tsx | Verify history view + branch override |
| Wizard bàn giao 5→6→7→8 | 🟢 Reuse | src/modules/user/components/customer-handover/CustomerHandover* | Verify limit 200 khách/lần (DEC-007), rollback 24h |
| Export XLSX | 🟢 Reuse | XExcel.tsx + XTable.tsx export feature | Verify column mapping theo B-Export |
| Filter bar (date+staff+branch) | 🟢 Reuse | DebtManagerFilter.tsx | Verify dùng X-Hasura-Branch-Id |
| Mobile MOB-01 Dashboard | 🟡 Extend | debt_management_screen.dart (2 tabs) | Bổ sung KPI cards layout cho tab Consultation |
| Mobile MOB-02 List nợ | 🟡 Extend | debt_management_debt_tab.extension.dart | Pre-filter overdue + scope chip |
| Mobile MOB-03 Chi tiết | 🟡 Extend | debt_customer_detail_screen.dart | Bổ sung section Lịch sử LH + Lịch nhắc collapsible |
| Mobile MOB-04 Bottom sheet | 🟢 Reuse | debt_create_reminder_sheet.dart (1038 lines) | — |
| Mobile MOB-05 Notification | 🟡 Extend | notification_screen.dart + AppNotificationRoute.dart | Bổ sung notification types: isDebtHandoverCompleted, action card |
| DB function aging | 🟢 Reuse | customer_debt_dashboard_order_scope() migration 1776054869987_* | — |
| Daily alert scheduler | 🟢 Reuse | services/ecommerce-api/scheduler/daily_debt_alert.go | Verify run time DEC-PD-002 (17:05 hay sáng) |
| Handover action BE | 🟢 Reuse | customer_handover_support action + rollback_customer_handover | Verify limit 200 + rollback 24h enforcement |
Hasura permission cho debt_alert_config, debt_alert_log, customer_handover_log, debt_contact_log, debt_followup_task | 🟡 Extend | Tables sẵn — permission YAML | Harden least-data per DEC-012 |
Materialized snapshot debt_daily_snapshot | 🔴 Skip Day-1 | — | Chỉ thêm nếu capacity gate fail (DEC-013) |
A12.2 Số liệu
- Tables sẵn có: 7 bảng (debt_alert_config, debt_alert_config_log, debt_alert_log, customer_handover_log, debt_contact_log, debt_followup_task, debt_followup_notification_schedule, debt_order_item_snapshot) — 0 bảng mới Day-1
- Hasura actions sẵn có: ≥2 (customer_handover_support, rollback_customer_handover) — đếm thêm trong dev-spec
- Schedulers sẵn có: 1 (daily_debt_alert) + 1 sub-pipeline (debt_alert_notification_schedule)
- FE components sẵn có (web): 13 component trong
debt-manager/component/+ 9 component trongcustomer-handover/ - FE components sẵn có (mobile): 6 widget trong
debt_management/widgets/+ screens chính (debt_management, debt_customer_detail, notification) - Migrations debt-related: ~20 migrations (1695033865666 → 1776826519813)
A12.3 Cái KHÔNG có và KHÔNG cần build Day-1
- Auto-assign debt owner bằng AI
- SLA escalation nhiều cấp
- CRM campaign automation
A12.4 KPI × Role canonical (Ref DEC-024) 🆕
Bảng quyết định KPI nào hiển thị cho role nào — giải quyết case Manager/BOD không có % hoa hồng. Mỗi cell cho biết:
<source data>[(hiển thị/ẩn)]. BE phải honorview_roleparam khi trả KPI.
| KPI / Cột | Sale / CSKH / Telesale (individual) | Manager (branch scope) | BOD / Admin (system scope) |
|---|---|---|---|
| Khách đang nợ (count) | KH của mình | COUNT raw trong branches quản lý | COUNT raw toàn hệ thống |
| Khách nợ quy đổi | × % HH của mình | 🚫 ẨN — không quy đổi | 🚫 ẨN |
| Tổng tiền nợ | SUM raw của mình | SUM raw branch | SUM raw toàn hệ thống |
| Tiền nợ quy đổi | × % HH của mình | 🚫 ẨN | 🚫 ẨN |
Doanh thu ròng (net_revenue — Ref FORMULA-003) | SUM(actual_revenue) × % HH (allocated) | SUM raw branch | SUM raw toàn hệ thống |
| Tỉ lệ chuyển đổi (FORMULA-002) | của mình | trung bình team trong branch (weighted by visits) | trung bình toàn hệ thống |
| Tỉ lệ công nợ/doanh thu (FORMULA-003) | của mình | branch ratio (raw total_debt / raw net_revenue) | system ratio |
| Tỉ lệ thu nợ (FORMULA-004) | của mình | branch avg | system avg |
| Số ngày thu nợ trung bình (FORMULA-005) | của mình | branch avg | system avg |
| Aging buckets count (FORMULA-006B) | count KH của mình per bucket | COUNT raw KH branch per bucket | COUNT raw KH toàn hệ thống per bucket |
| Cột "% Của tôi" / "% tham gia" (SCR-02, SCR-03) | hiển thị % của Sale | 🚫 ẨN cột (không meaningful cho aggregate view) | 🚫 ẨN cột |
| Cột "Phụ trách chính" (SCR-03) | hiển thị | hiển thị (tên owner) | hiển thị (tên owner) |
| Cột "Doanh thu ròng quy đổi" hoặc tương tự | hiển thị | 🚫 ẨN | 🚫 ẨN |
| Leaderboard nhân sự (SCR-01-TAB-ANALYTICS) | 🚫 ẨN tab (chỉ Manager/Admin) | hiển thị NV trong branches quản lý | hiển thị NV toàn hệ thống |
Rules thực thi:
- API contract: Mỗi query KPI nhận param
view_role: 'individual' | 'manager' | 'bod'(mặc định resolve từ Hasura role nếu không truyền) - BE permission: Khi
view_role='manager'→ BE ignore filter theouser_idcủa caller, chỉ applybranch_id IN allowed_branches; KHÔNG quy đổi % HH - FE rendering: Component KPI card kiểm tra
view_roletrước khi render cột quy đổi. Ẩn (display: none) thay vì disable - Permission matrix UI: Cập nhật ui-spec B5 — đánh dấu rõ cột nào ẩn/hiện per role
Edge cases:
- Manager kiêm Sale (vừa quản lý vừa được phân khách): API hỗ trợ toggle giữa "View của tôi" (individual) và "View team" (manager) — UI có pill switcher
- BOD không có branch trong allowed list (hệ thống mới chưa setup) → trả empty + banner "Chưa có data"
- Khi Manager filter Nhân sự = 1 staff cụ thể trong team → view SẼ quy đổi theo % HH của staff đó (đây là drill-down individual)
- App/API mobile riêng (reuse staff app)
- Bảng materialized
debt_daily_snapshot(chỉ build nếu capacity gate fail)