Skip to content

Sales-CSKH Debt Follow-up & Handover — PRD

v2.0 — 15/05/2026 (Major Hardening Release)

Thay đổiSectionẢnh hưởng
Thêm 14 DECs mới (DEC-022..035) — resolve 7 GAPs nghiệp vụ + 5 prod issues + 3 PDsZ1) Decision LogAll
Rewrite FORMULA-003 (Doanh thu ròng canonical) — SUM(actual_revenue) exclude walletA10) FormulasBE, QA
Thêm FORMULA-006B (customer-level aging bucket — multi-order rollup)A10) FormulasBE, QA
Thêm FORMULA-007 (Service representative tie-break SCR-02)A10) FormulasBE, FE
Thêm FORMULA-008 (Weighted % tham gia multi-order)A10) FormulasBE, FE, QA
Thêm A12.4 (KPI × Role canonical — Manager/BOD raw aggregate, không quy đổi)A12) Codebase AuditBE, FE
FR-009 update — noti trigger >= + 4 re-trigger rules (daily/bucket/partial/settled)A5) FR-009BE, 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/034DevOps, BE, DBA
DEC-035 — PD-003 Hasura permission harden DEFERRED có điều kiện — REVISIT trước GAZ1) DEC-035TL, 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:

  1. 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).
  2. Mỗi FR có cột "Codebase status" — ghi rõ component/route/table đã có → chỉ cần hardening hay extend.
  3. A12) Codebase Audit Summary ở cuối PRD — bảng tổng kết Reuse 🟢 / Extend 🟡 / Build 🔴 với file path.
  4. Mapping chi tiết SCR/MOB → existing component: xem ui-spec.md §0 Codebase Mapping.
  5. 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):

  1. 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 → fallback in_charge.
  2. 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).
  3. Validation rule: Khi 2+ Sale chưa chọn phụ trách chính → nút Xác nhận disabled + warning text.
  4. 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).
  5. 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 / BAExecutive Summary + A + B (đầy đủ)
DevA + C + B4 (Notification) + B-Export
QAA + D + B7 (State) + Edge Cases
Mobile DevA + B-Mobile (MOB-01…05) + C-Mobile
Ops / SREA + 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ăngMô tảNgười dùng
📊Dashboard hiệu suấtKPI 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 pushSale (owner chính), Manager
🔄Bàn giao kháchWizard 3 bước + audit + rollback 24hHR 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)

MilestoneTargetNội dungOwner
M-0103/03/2026KPI Definition Freeze + Confirm PD-005/007/008/009PM/PO
M-0210/03/2026UX Freeze — Desktop 8 màn + Mobile 5 mànUIUX Lead
M-0324/03/2026Core Build (FR-001…007)Tech Lead
M-0407/04/2026Alert + Handover (FR-008…012) + Mobile APIDEV/OPS
M-0514/04/2026Release Readiness — Web + MobileQA Lead + Ops/SRE

Trạng thái sign-off

DomainOwnerTrạng tháiBlocker
Business FitPM/PO✅ Conditional
UX ReadinessUIUX Lead✅ ConditionalChờ high-fidelity freeze (Desktop + Mobile)
Tech ReadinessTech Lead✅ ConditionalChờ migration + scheduler runbook + Mobile API spec
Quality ReadinessQA Lead✅ ConditionalCần seed data chuẩn cho aging
Operational ReadinessOps/SRE🟡 In ProgressAction items OPS-01…05 đã assign — target 10/03 (M-02)

Action Items — Unblock Ops Readiness

#ActionOwnerDeadlineStatus
OPS-01Viết runbook debt scheduler: scenarios (job fail, retry, dedupe check, manual re-trigger)Ops/SRE10/03 (M-02)
OPS-02Review runbook bởi Tech Lead + QATech Lead14/03
OPS-03Setup alert dashboard: debt_scheduler_run_failed, push_notification_delivery_rateOps/SRE17/03 (trước M-03)
OPS-04Diễn tập dry-run trên staging: job lỗi → retry → không trùng → alert OpsOps/SRE + QA21/03
OPS-05Setup FCM project + service account cho push notificationOps/SRE + BE10/03 (M-02)

Pending Decisions (Cần confirm trước M-01: 03/03/2026)

PD-IDCâu hỏiNgười quyết địnhStatusĐề xuất PO/BA
PD-005Số khách tối đa trong 1 lần handover có giới hạn không?Tech Lead✅ Resolved → DEC-017Giớ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-007Mobile Diva Partner có hỗ trợ Handover Wizard không?PM/PO✅ Resolved → Phase 2Phase 2. Handover cần preview diff phức tạp, không phù hợp mobile MVP. Ghi rõ trong Backlog Phase 2.
PD-008Push notification mobile: Firebase hay APNs riêng?Tech Lead✅ Resolved → DEC-018Firebase Cloud Messaging (FCM). Cover cả iOS + Android, Diva Partner đa nền tảng. APNs chỉ cover iOS.
PD-009Mobile có export XLSX không hay chỉ xem?PM/PO✅ Resolved → DEC-019Mobile chỉ xem, không export. Người dùng cần export → dùng web. Giảm complexity mobile MVP.

RACI

DeliverablePM/POUIUXDEVQAOPS/SRE
PRDA/RCCCI
UI/UX Spec (Web + Mobile)CA/RCCI
Dev SpecCCA/RCC
QA PlanCCCA/RI
Release PlanAIRCR

Backlog Phase 2 (OUT of MVP-Lite)

Tính năngLý 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/P3Cầ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 analysisNâng cao, không cần cho MVP
Automation rules, predictive scoringML phase, chưa có data đủ
Drill-down tư vấn trên MobileQuá phức tạp cho màn nhỏ, ưu tiên web
Handover Wizard trên MobilePhase 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-IDQuyết địnhLý doStatus
DEC-001visit_count = lượt theo grain (customer_id, branch_id, date)consultant; unique_customer_count = khách duy nhất theo customer_idTránh nhập nhằng KPI✅ Locked
DEC-002closed_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-003Owner 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-004Ngưỡng: branch_override ?? global_thresholdMVP đủ linh hoạt✅ Locked
DEC-005Handover: 3 bước + 1 xác nhận cuốiGiảm rủi ro bàn giao sai✅ Locked
DEC-006Handover chuyển cả nhắc nợ + quyền xem lịch sửTránh rơi khách sau bàn giao✅ Locked
DEC-007Kênh cảnh báo MVP: in-app + mobile pushTối ưu tốc độ, phủ rộng nhân viên mobile✅ Locked
DEC-008KPI thu nợ: 2 chỉ số (weighted + full settlement)Đánh giá tốc độ thu + tất toán✅ Locked
DEC-009total_revenue = paid − refund (ròng, trong kỳ)Superseded by DEC-022: dùng 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-010Mẫu số 0: giá trị = 0, hiển thị N/ATránh lỗi chia 0✅ Locked
DEC-011Chuẩn hóa debt_start_date và debt_due_dateTránh nhập nhằng recovery/overdue✅ Locked
DEC-012Mobile scope: Diva Partner App, chỉ xem + hành động cơ bảnNhân viên cần theo dõi mọi lúc✅ Locked
DEC-013Manager 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-014Export chỉ XLSX, bỏ CSVChuẩn hóa format, giảm maintain✅ Locked
DEC-015Handover rollback: chỉ toàn bộ, không hỗ trợ partialGiảm complexity MVP✅ Locked
DEC-016Noti handover gửi cho: Creator + Target (người tiếp nhận)Đảm bảo cả 2 bên đều biết✅ Locked
DEC-017Giới hạn 200 khách/lần handover. Warning khi > 100. Chia batch nếu cần nhiều hơnAlign với export row cap, tránh transaction quá lớn (PD-005)✅ Locked
DEC-018Push notification mobile dùng Firebase Cloud Messaging (FCM)Cover cả iOS + Android, Diva Partner đa nền tảng (PD-008)✅ Locked
DEC-019Mobile chỉ xem, không export XLSX. Export → dùng webGiảm complexity mobile MVP (PD-009)✅ Locked
DEC-020Khi đến giờ nhắc (remind_at), gửi notification in-app (web) cho assigned_to. Không push mobileGiảm complexity scheduler; followup task là nhắc nhẹ, không cần push✅ Locked
DEC-021Mà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êngGom vào context khách hàng/nợ, không cần navigation riêng✅ Locked
DEC-022Doanh 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-023Customer-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-024KPI 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 datasetManager/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-025Noti 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 notiTrá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-026Noti 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 sauCả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-027Service 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-028Cộ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 rowSale 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-030Default 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-031Cron 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-032Tooltip "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-033Banner "Ư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ề TODAYBanner 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-034Capacity 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-flyTrá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-035Hasura 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-IDQuyết địnhLý doStatus
DEC-UX-001SCR-03 là trang riêng, không phải tabTăng focus tác vụ xử lý nợ✅ Locked
DEC-UX-002Rule owner hiển thị bằng tooltip giải thíchTránh hiểu sai KPI/alert✅ Locked
DEC-UX-003Config ngưỡng: global + branch override ở MVPĐồng bộ backend và QA✅ Locked
DEC-UX-004Wizard handover 3 bước + preview bắt buộcGiảm rủi ro bàn giao sai✅ Locked
DEC-UX-005Filter 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-006Mobile 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-007Màu Aging Badge: Xanh/Vàng/Cam/Đỏ theo 4 bucketNhận diện rủi ro nhanh✅ Locked
DEC-UX-008Xác nhận handover cần tick 2 checkbox trước khi bật nútGiảm rủi ro bàn giao nhầm✅ Locked
DEC-UX-009Lị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-IDQuyết địnhLý doStatus
DEC-TECH-001Owner rule: hoa hồng cấp dịch vụĐồng bộ business + scheduler✅ Locked
DEC-TECH-002Threshold rule: branch overrideMVP hỗ trợ override chi nhánh✅ Locked
DEC-TECH-003Snapshot debt_owner_id khi phát sinh nợTránh drift lịch sử✅ Locked
DEC-TECH-004Scheduler in-app + push mobile, dedupe theo ngàyGiảm duplicate notification✅ Locked
DEC-TECH-005Handover: transaction + audit + rollback 24h RBACCân bằng tốc độ và an toàn✅ Locked
DEC-TECH-006Export: chỉ XLSX, row cap 200k, chunking tự độngChuẩn hóa format + tránh timeout✅ Locked
DEC-TECH-007Mobile API: dùng chung BE API với web, RBAC mobile-scopedTái sử dụng, giảm surface area✅ Locked
DEC-TECH-008Push notification dùng Firebase Cloud Messaging (FCM) cho cả iOS + AndroidFCM đa nền tảng, giảm maintain (PD-008 → DEC-018)✅ Locked
DEC-TECH-009Handover limit 200 khách/transaction. Vượt → chia batchTránh transaction timeout + align export cap (PD-005 → DEC-017)✅ Locked

Z4. Quyết định QA

DEC-IDQuyết địnhTest bắt buộcStatus
DEC-QA-001visit_count vs unique phân biệt1 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-002Owner rule 3 cấp fallbackTest đủ 3 cấp khi thiếu dữ liệu✅ Locked
DEC-QA-003Threshold branch_override ?? globalTest override và fallback✅ Locked
DEC-QA-004Scheduler dedupe theo ngàyRetry không gửi trùng✅ Locked
DEC-QA-005Handover rollback 24h (toàn bộ)Success trong 24h + deny ngoài 24h + deny partial✅ Locked
DEC-QA-00650/50 Sale không chồng chéo KPIPhân bổ % + tie-break đúng✅ Locked
DEC-QA-007Manager chỉ xem branch mình quản lýTest RBAC branch scope✅ Locked
DEC-QA-008Mobile push notification deeplink đúng mànTap noti → đúng screen✅ Locked


A) Product Requirements Document (PRD)


A1. Module Blueprint

ModuleMục tiêuĐiểm cốt lõiPlatform
1. Nguồn phân bổ người phụ tráchXác định đúng ai chịu trách nhiệm nợNguồn: hoa hồng cấp order_serviceWeb + Mobile
2. Dashboard KPIXem 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áchWeb (đầ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ịchWeb + Mobile
4. Cảnh báo nợ in-app + pushNhắc đúng người, đúng lúcChỉ gửi owner chính; dedupe; push mobileWeb + Mobile
5. Bàn giao kháchBàn giao an toànWizard 3 bước + rollback 24h (toàn bộ) + auditWeb only (MVP)
6. Kiểm soát minh bạchChống tranh cãi KPIHiển thị owner + người tham gia (%)Web

Flow thao tác hằng ngày

  1. Job nợ chạy hằng ngày → tính overdue + aging bucket
  2. Gửi in-app noti (web) + push notification (Diva Partner App) cho owner chính
  3. Owner mở SCR-03 (web) hoặc MOB-02 (mobile), xử lý theo độ ưu tiên
  4. Cập nhật kết quả → KPI và báo cáo tự refresh
  5. (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-IDMục tiêu
G-001Danh sách khách đã tư vấn theo ngày/tuần/tháng
G-002Cảnh báo công nợ quá hạn hằng ngày theo ngưỡng cấu hình (web + mobile push)
G-003Quy trình bàn giao khách khi nhân sự nghỉ — có audit
G-004Dashboard KPI chốt + nợ + thu hồi nợ theo nhân sự
G-005Mobile 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:

PhaseNội dung
MVP-Lite (hiện tại)Dashboard + debt alert + manual handover + KPI core + Mobile cơ bản
Phase 2SLA follow-up + priority ranking + auto-escalation + benchmark + Mobile Handover
Phase 3Automation rules, predictive scoring

A4. Personas & Journeys

PersonaRoleJourney chínhPlatform chính
P1Sale / CSKH / TelesaleNhận push → mở app → xem nợ → gọi khách → đánh dấu đã LHMobile (hằng ngày) + Web (báo cáo)
P2Sales/Branch ManagerDashboard → so sánh conversion và debt ratio theo nhân sự trong branch mìnhWeb
P3HR OpsTạo handover → chọn nguồn/đích → preview → xác nhậnWeb

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ụRoleNhân sự% hưởngGhi nhận KPI cho
DV-ASaleSale 1100%Sale 1 nhận phần DV-A
DV-ATelesaleTelesale 2100%Telesale 2 nhận phần DV-A
DV-BSaleSale 2100%Sale 2 nhận phần DV-B
DV-BCSKHCSKH 1100%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ụ):

CaseSố Sale trên DVBehaviorRadio hiện?
1 Sale1Auto 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≥ 2Hiện radio "Phụ trách chính" — user BẮT BUỘC chọn đúng 1 người✅ Hiện radio, mandatory
0 Sale0 (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ĩaVí dụ
visit_countĐếm theo grain (customer_id, branch_id, date)visit_source chứa consultant1 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ọc1 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ẹn consultant trạng thái confirmed/completed; (2) ghi âm hợp lệ (duration + url) gắn record/lịch hẹn; (3) đơn hàng có reference_appointment_id làm lịch hẹn chuyển completed.

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ĩaKiểu
debt_customer_count_distinctSố khách nợ thực (khách duy nhất có outstanding > 0)Số nguyên
debt_customer_equivalentSố khách nợ quy đổi theo % hưởngSố lẻ (e.g. 0.5)
total_debt_amountTổng outstanding_end_of_periodTiền
allocated_debt_amountTổng tiền nợ quy đổi theo % hưởng (SUM(allocated_debt))Tiền
debt_ratiototal_debt_amount / total_revenue%
debt_customer_ratiodebt_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 / 100
  • allocated_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ạnMức rủi roMàu badge
0-290–29 ngàyMới quá hạn — nguy cơ thấp🟢 Xanh lá (#E8F5E9 / #2E7D32)
30-5930–59 ngàyCần follow-up sát🟡 Vàng (#FFF8E1 / #F57F17)
60-8960–89 ngàyRủi ro cao🟠 Cam (#FFF3E0 / #E65100)
≥90≥ 90 ngàyRủ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):
    1. 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
    2. 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 "
    3. 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
    4. Settled → drop ngay: Khách settled hết nợ (paid = total) → drop khỏi list noti từ ngày sau ngày settled
  • Rule xác định owner (owner_debt):
    1. commission_is_primary_owner [được chọn từ popup hoa hồng cấp dịch vụ]
    2. commission_highest_percent [nếu không có bước 1]
    3. in_charge [fallback cuối]
  • Tie-break kỹ thuật (chỉ dùng cho dữ liệu legacy chưa có commission_is_primary_owner và vẫn hòa):
    1. Ưu tiên commission_primary_by_role
    2. commission_created_at ASC (bản ghi cũ hơn)
    3. user_id ASC (nhỏ hơ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:
    1. Handover hoạt động đúng cho cả 2 trường hợp chọn (toàn bộ / theo filter)
    2. Preview diff phải khớp kết quả thực tế sau xác nhận
    3. Rollback trong 24h hoàn tác toàn bộ, không partial
    4. Notification gửi cho cả Creator + Target (Ref: DEC-016)
    5. 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) cho assigned_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, ghi completed_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
  • 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:

  1. User tạo lịch nhắc → xem lại được trong drawer (web) hoặc section (mobile).
  2. Khi đến remind_at, assigned_to nhận in-app notification với nội dung nhắc + deeplink đến khách.
  3. User đánh dấu "Đã xong" → status chuyển done, hiển thị completed_at.
  4. Lịch nhắc quá hạn (chưa xử lý) hiển thị badge "Quá hạn" rõ ràng.
  5. 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.md v1.5. Parent spec khóa phạm vi tích hợp như sau:

  • FR-014: Tab Thố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ồm Chỉ số Công nợ, Phân loại nhóm nợ, Top 5 khách nợ cao nhất.
  • FR-020: Xuất XLSX tab Thống kê theo quyền và bộ lọc.

A6. Assumptions, Constraints & Dependencies

IDLoạiNội dung
A-001AssumptionLưu snapshot debt_owner_id tại thời điểm phát sinh nợ
A-002AssumptionAging tính theo current_date − debt_due_date, timezone VN
A-003AssumptionRBAC kế thừa cơ chế CRM/report hiện có
A-004AssumptionNguồn chuẩn: hoa hồng cấp dịch vụ trong đơn
A-005AssumptionDiva Partner App đã có infrastructure để nhận push notification
C-001ConstraintDữ liệu phân tán giữa order, invoice, commission
C-002ConstraintTương thích với query/report legacy
C-003ConstraintMobile API dùng chung endpoint với web, RBAC mobile-scoped

A7. Risks & Mitigations

R-IDRủi roMitigation
R-001Mapping owner không nhất quánKhóa rule DEC-003 + backfill + audit sample
R-002Query KPI nặng khi range lớnMaterialized view/cache + index + export chunking
R-003Handover sai phạm viPreview diff bắt buộc + xác nhận + rollback 24h
R-004Push notification mobile không đến tay nhân viênFallback in-app web; alert Ops khi delivery rate < 90%
R-005PD-007/008/009 chưa confirmResolved v4.1: PD-007=Phase 2, PD-008=FCM, PD-009=Mobile chỉ xemFeature flag ff_mobile_push_notification cho FCM rollout

A8. Success Metrics

Chỉ tiêuMụ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àyBaseline đo ở sprint 0

A9. Glossary (Thuật ngữ)

Thuật ngữ (VI)Thuật ngữ (EN)Định nghĩaPhân biệt với
Nợ quá hạnOverdue debtKhách có total_debt_amount > 0overdue_days > threshold_days≠ Khách đang nợ (đang nợ = total_debt_amount > 0, không cần quá hạn)
Phụ trách chínhPrimary ownerSale đượ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 bucketAging bucket4 nhóm new_debt (0-29), watch_list (30-59), high_risk (60-89), very_high_risk (90+) theo overdue_days
Bàn giaoHandoverChuyể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 giaoRollbackTrả debt_owner_id về NV cũ trong 24h sau handoverChỉ khả dụng trong 24h
Cảnh báo nợDebt alertPush/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-upFollow-up taskTask 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 contactedRecord 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 chipFilter 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() — filter consultant_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 > 0 trong 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 payment
    • debt_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_revenue VND (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_revenue củ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)

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ên debt_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_Minhoverdue_days = (NOW() AT TIME ZONE 'Asia/Ho_Chi_Minh')::date - order.created_at::date
  • Nguồn: customer_debt_dashboard_order_scope() migration 1776054869987_*
  • 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 order nợ → 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_bucket cao 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ào very_high_risk
  • 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ợ
  • 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 function search_report_debt_performance_consultation hiện tại chỉ pick 1 reference_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ả order thuộc evidence_logs.order_id của row tư vấn ((customer_id, branch_id, consulted_date)) có debt_owner_id IS NOT NULLtotal > 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 total cho 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.md C12.

FR-IDACUI surfaceCodeTest
FR-001 → FR-007AC-001..007SCR-01 + SCR-02 + SCR-03DebtManager* + customer_debt_dashboard_*TC-CP-01..10
FR-008, FR-009AC-008, AC-009SCR-04 + NTF-DEBT-DAILY-001DebtManagerSetting + daily_debt_alert.goTC-CP-11..14
FR-010, FR-011AC-010, AC-011SCR-05..08 wizardCustomerHandover* + customer_handover_support actionTC-CP-15..20
FR-012AC-012Export CTA tất cả pagesXExcel + XTableTC-CP-21
FR-013AC-013SCR-03-DRAWER + Popup + MOB-04debt_followup_task + DebtManagerCreateReminderScheduleTC-CP-22..24
FR-014..020AC-014..020SCR-01-TAB-ANALYTICSDebtManagerStatisticsTC-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ựcStatusCodebase evidenceDelta cần làm
Route /dm/debt (3 tabs)🟢 Reusediva-admin/src/modules/debt-manager/modules.tsVerify route guard dùng Dynamic Permission v2 (DEC-011)
Tab Hiệu suất tư vấn🟡 Extendcomponent/consulting-performance/DebtManagerConsultingPerformance.tsxBổ 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ợ🟡 Extendcomponent/debt/DebtManagerDebt.tsxBổ sung scope chip vai trò Sale/Telesale/CSKH + summary banner + Daily Focus top 5
Tab Thống kê🟡 Extendcomponent/statistics/DebtManagerStatistics.tsxBổ sung leaderboard nhân viên + drawer chi tiết NV (720px)
Popup tạo lịch nhắc🟢 ReuseDebtManagerCreateReminderSchedule.tsxVerify validation rule
Drawer lịch nhắc của tôi🟢 ReuseDebtManagerActionViewCalendar.tsx + DebtManagerReminderScheduleHistory.tsxVerify filter + permission scope
Cài đặt cảnh báo nợ🟢 Reusesrc/modules/settings/pages/DebtManagerSetting.tsxVerify history view + branch override
Wizard bàn giao 5→6→7→8🟢 Reusesrc/modules/user/components/customer-handover/CustomerHandover*Verify limit 200 khách/lần (DEC-007), rollback 24h
Export XLSX🟢 ReuseXExcel.tsx + XTable.tsx export featureVerify column mapping theo B-Export
Filter bar (date+staff+branch)🟢 ReuseDebtManagerFilter.tsxVerify dùng X-Hasura-Branch-Id
Mobile MOB-01 Dashboard🟡 Extenddebt_management_screen.dart (2 tabs)Bổ sung KPI cards layout cho tab Consultation
Mobile MOB-02 List nợ🟡 Extenddebt_management_debt_tab.extension.dartPre-filter overdue + scope chip
Mobile MOB-03 Chi tiết🟡 Extenddebt_customer_detail_screen.dartBổ sung section Lịch sử LH + Lịch nhắc collapsible
Mobile MOB-04 Bottom sheet🟢 Reusedebt_create_reminder_sheet.dart (1038 lines)
Mobile MOB-05 Notification🟡 Extendnotification_screen.dart + AppNotificationRoute.dartBổ sung notification types: isDebtHandoverCompleted, action card
DB function aging🟢 Reusecustomer_debt_dashboard_order_scope() migration 1776054869987_*
Daily alert scheduler🟢 Reuseservices/ecommerce-api/scheduler/daily_debt_alert.goVerify run time DEC-PD-002 (17:05 hay sáng)
Handover action BE🟢 Reusecustomer_handover_support action + rollback_customer_handoverVerify limit 200 + rollback 24h enforcement
Hasura permission cho debt_alert_config, debt_alert_log, customer_handover_log, debt_contact_log, debt_followup_task🟡 ExtendTables sẵn — permission YAMLHarden least-data per DEC-012
Materialized snapshot debt_daily_snapshot🔴 Skip Day-1Chỉ 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 trong customer-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 honor view_role param khi trả KPI.

KPI / CộtSale / CSKH / Telesale (individual)Manager (branch scope)BOD / Admin (system scope)
Khách đang nợ (count)KH của mìnhCOUNT 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ìnhSUM raw branchSUM 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 branchSUM raw toàn hệ thống
Tỉ lệ chuyển đổi (FORMULA-002)của mìnhtrung 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ìnhbranch ratio (raw total_debt / raw net_revenue)system ratio
Tỉ lệ thu nợ (FORMULA-004)của mìnhbranch avgsystem avg
Số ngày thu nợ trung bình (FORMULA-005)của mìnhbranch avgsystem avg
Aging buckets count (FORMULA-006B)count KH của mình per bucketCOUNT raw KH branch per bucketCOUNT 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:

  1. 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)
  2. BE permission: Khi view_role='manager' → BE ignore filter theo user_id của caller, chỉ apply branch_id IN allowed_branches; KHÔNG quy đổi % HH
  3. FE rendering: Component KPI card kiểm tra view_role trước khi render cột quy đổi. Ẩn (display: none) thay vì disable
  4. 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)