Skip to content

Bug Fix: Báo cáo Doanh thu thực tế (Actual Revenue Report)

v1.2 — 11/05/2026 — QA review iteration

Thay đổiSectionẢnh hưởng
Downgrade BUG-06 → NEEDS DISCOVERY (verified: mẫu số là fund f chứ không phải fund.payment như em đoán; cần reconciliation invoicefund trước khi chốt fix)Section 2BE, DBA
Rollback Q7 từ "CHỐT Option B" → "PENDING DISCOVERY" — đính kèm 3 giả thuyết H1/H2/H3 + reconciliation SQLSection 6BE, DBA, PO
Split BUG-05 AC theo lazy-load: initial load (overview+3 chart) ≠ expanded table (sau click)BUG-05BE, QA
Thêm TC-11b, TC-11c — lazy-load verifySection 5QA
Cập nhật TC-18/19/20 — đánh dấu PENDING DISCOVERYSection 5QA
Status banner per-bug — table rõ ràng cho từng bug (Ready / Blocked / Discovery)Banner topAll
Fix UI Spec typo — DD/YYYYMM/YYYY cho monthly compareSection 4BDesigner, FE

v1.1 — 11/05/2026

Thay đổiSectionẢnh hưởng
Thêm BUG-06telesale_amount_rate mismatch dimension (initial claim — đã downgrade ở v1.2)Section 2BE, DBA, QA
Thêm Q7 + Q8 — chốt fix Option B (rollback ở v1.2) + audit data quality telesaleSection 6All
Thêm TC-18/19/20 — telesale rate testsSection 5QA
Sửa anchor links (QA review P1) — bỏ emoji/em-dash khỏi headingsToàn fileAll
Sửa TC-10 — single expected khớp Q4 (ẩn hẳn ngày tương lai)Section 5QA, FE
Đổi status banner — BE READY, FE BLOCKED đến Designer sign-offBanner topFE, Designer
Đổi complexity M → S + ghi rõ "Quick Fix compact package"index.mdAll

🔒 STATUS theo từng bug:

BugTrạng tháiNgười phụ tráchCó thể bắt đầu?
BUG-01 (compare period)✅ ReadyBENgay
BUG-02 (Δ% / Δ abs)✅ ReadyBENgay
BUG-03 (AOV add cột)✅ ReadyBE + DBANgay
BUG-04 (visual cột)⏳ BlockedFE + DesignerChờ Designer sign-off Section 4B
BUG-05 (performance)✅ ReadyBE + FENgay
BUG-06 (telesale rate)🟠 NEEDS DISCOVERYBE + DBAChờ reconciliation SQL invoicefund

🎨 UI/UX: Section 4B đang ở trạng thái DRAFT — cần Designer review + điền các chỗ ??? trước khi FE bắt đầu BUG-04.

Đã chốt 6 quyết định nghiệp vụ + 2 quyết định mới (Q7 pending, Q8 chốt) — xem Section 6.

Mục lục


Quick Reference cho QA

Thông tinGiá trị
Số test cases22 TCs (chi tiết Section 5)
Phân nhóm TCs5 Logic compare · 3 AOV correctness · 2 Visual · 9 Performance (gồm 3 lazy-load tests) · 3 Telesale (1 regression + 2 pending discovery)
Acceptance CriteriaMỗi BUG có AC riêng — xem Section 2
Definition of Donexem Section 7
SeverityHigh — báo cáo dùng trong họp với cấp quản lý
Components ảnh hưởng3 chart + 1 bảng trong Báo cáo Doanh thu thực tế
URL test/r/reports/actual_revenue_report_group
Mong đợi performanceP95 toàn report < 5s; mỗi chart/bảng < 2s

🧪 Test environment

  • Staging URL: (FE dev điền sau khi deploy staging)
  • Test data: Cần data có đủ 3 loại đơn (service / cosmetic / prepaid) trong nhiều tháng, ít nhất 1 branch có data và 1 branch all-zero (vd VP Cần Thơ)
  • Test viewport: Desktop (1440px+) — chính. Mobile/tablet — secondary
  • Tools: Chrome DevTools (Network, Performance), Vietnamese locale

🚨 Critical regressions cần check

Sau khi fix BUG-03, 3 chart Pie + 1 Line khác dùng chung SQL function. QA phải verify chúng KHÔNG THAY ĐỔI giá trị:

  • ✅ Line "Biến động thực thu" — payment_amount_by_month không đổi
  • ✅ Pie "Tỉ lệ thực thu Dịch vụ" — revenue_service_group không đổi
  • ✅ Pie "Tỉ lệ thực thu Mỹ phẩm" — revenue_cosmetic_group không đổi
  • ✅ Pie "Tỉ lệ thực thu Thẻ" — revenue_prepaid_group không đổi

1. Tóm tắt

Báo cáo Doanh thu thực tế (URL /r/reports/actual_revenue_report_group) đang có 6 lỗi ảnh hưởng nhiều thành phần:

Thành phầnBugs ảnh hưởngTrạng thái
Bar "Biến động trung bình theo đơn hàng"BUG-01, 02, 03, 04, 05Ready (trừ BUG-04 chờ Designer)
Line "Biến động thực thu"BUG-01, 02, 05Ready
Line "Biến động khách hàng"BUG-01, 02, 05Ready
Bảng "Chi tiết hoạt động kinh doanh"BUG-05 (handler riêng reportSalesRevenueByBranchlazy-load)Ready
Mục "Hiệu quả" — KPI "Tỉ lệ đóng góp thực thu Telesale"BUG-06 (nghi vấn mismatch dimension invoice vs fund)🟠 NEEDS DISCOVERY

→ 4 bug logic số liệu (ready) + 1 bug performance (ready, 2 handler) + 1 bug nghi vấn cần verify trước fix → khiến số liệu sai, gây hiểu nhầm cho cấp quản lý và load chậm. Cần fix gấp vì báo cáo đang được sử dụng trong cuộc họp.

Screenshots lỗi:

  • Monthly view: chart hiển thị 01/2025 → 05/2026, tooltip "10/2025 vs 10/2025"
  • Daily view: chart hiển thị 01/05 → 16/05/2026, tooltip "07/05/2026 vs 07/05/2026"

→ Bug xảy ra ở cả 2 view mode (day/month), cùng root cause backend.

Tech context (đã verify trong codebase):

LayerPath
FE — 3 chart componentsdiva-admin/src/modules/report/components/actual-revenue-report/Charts/ (BarChartActualRevenue.tsx, LineChartActualRevenue.tsx)
FE — section wrapperActualRevenueReportVolatility.tsx:187
BE — handler 1 (overview + chart)diva-backend/services/ecommerce-api/action/report_sales_revenue.go (functions: reportSalesRevenue, GetOrderAndInvoiceBase, CaculateData, SetValueObject, generateLabels)
BE — handler 2 (bảng chi tiết)diva-backend/services/ecommerce-api/action/report_sales_revenue_by_branch.goreportSalesRevenueByBranch()
DBfunctions search_dashboard_sales_revenue_chart() + search_dashboard_sales_revenue_by_branchs() — migrations 1752116891000 + 1777431629000
GraphQLHasura actions reportSalesRevenue + reportSalesRevenueByBranch — fields: avg_order_revenue_by_month, payment_amount_by_month, customer_visits_by_month, etc.

⚠️ Lưu ý quan trọng: DB function search_dashboard_sales_revenue_chart() còn được dùng bởi 4 chart khác (Line "Biến động thực thu" + 3 Pie "Tỉ lệ thực thu *") → KHÔNG được thay cột SUM hiện tại, chỉ ADD cột AOV mới.


2. Danh sách lỗi (6 BUG)

⚠️ REVIEW UPDATE (2026-05-11): Sau khi verify codebase chi tiết, đã phát hiện:

  • BUG-02 thực chất là BE bug (FE chỉ display, không compute) → reassign cho BE dev
  • BUG-03 KHÔNG được sửa bằng cách replace SUM trong SQL function (sẽ phá 4 chart khác chia sẻ cùng function) → strategy đúng: thêm 4 cột AOV mới, giữ nguyên cột SUM cũ
  • BUG-05 root cause mô tả lại: 4 query là 4 hàm DB khác nhau (không phải 4 variant của cùng 1 hàm). Khi có compare period → 8 query sequential

🔴 BUG-01: Tooltip header "10/2025 vs 10/2025" khi user chưa chọn compare period

Hiện trạng (Actual):

Tooltip header: "10/2025 vs 10/2025"   (khi user mở report mà KHÔNG set compare period)

→ Đang so sánh period với chính nó → vô nghĩa.

Scope ảnh hưởng (verified — KHÔNG chỉ 1 chart):

#ChartFile component
1"Biến động thực thu" (Line)LineChartActualRevenue.tsx
2"Biến động khách hàng" (Line)LineChartActualRevenue.tsx
3"Biến động trung bình theo đơn hàng" (Bar) — chart sếp phát hiện đầu tiênBarChartActualRevenue.tsx

3 chart cùng root cause — fix 1 chỗ ở backend, cả 3 chart hết bug.

Root cause (verified):

File report_sales_revenue.go — function generateLabels() (dòng 593–601):

go
// HIỆN TẠI (SAI):
if input.Data.CompareStartDate == nil {
    startCompare = start    // ❌ fallback bằng main period → "X vs X"
    endCompare = end
}
// Title được build từ startCompare/endCompare → label.Time + " vs " + label.CompareTime

Mong đợi (Expected):

Khi user không set compare:

  • Tooltip header chỉ hiển thị period hiện tại: "10/2025" (bỏ vế "vs ...")
  • Δ% và Δ abs hiển thị (em-dash) thay vì giá trị mâu thuẫn

Khi user có set compare:

  • Header hiển thị cả 2: "10/2025 vs 9/2025" (MoM) hoặc "10/2025 vs 10/2024" (YoY) tùy user chọn

Acceptance Criteria:

  • [ ] Trong generateLabels(): khi CompareStartDate == nil → set CompareTime = "" (rỗng) thay vì gán = Time
  • [ ] FE tooltip render: nếu CompareTime == "" → chỉ hiện Time, không hiện vs ...
  • [ ] Áp dụng đồng nhất cho cả 3 chart (Bar + 2 Line)
  • [ ] Khi user CÓ set compare → header hiển thị đúng 2 period khác nhau

🔴 BUG-02: Δ% và Δ abs mâu thuẫn — BE bug, không phải FE

Hiện trạng (Actual):

Dịch vụ:   705,21 Triệu — 0% (+705,21 Triệu)
Mỹ phẩm:    70,66 Triệu — 0% (+70,66 Triệu)
Nạp thẻ:       10 Triệu — 0% (+10 Triệu)
Tổng:      785,87 Triệu — 0% (+785,87 Triệu)

Lỗi: Nếu Δ% = 0% thì Δ abs phải = 0. Không thể vừa "không thay đổi" vừa "tăng +705 triệu".

Root cause (verified — KHÔNG phải FE bug):

File report_sales_revenue.go — function SetValueObject() (dòng 583–588):

go
// HIỆN TẠI (THIẾU GUARD):
func SetValueObject(valueObject *ValueObject, main float32, compare float32) {
    valueObject.Value = main
    valueObject.ValueChange = main - compare    // → main - 0 = main (sai!)
    if compare != 0 {
        valueObject.Percent = valueObject.ValueChange / compare
    }
    // ❌ Khi compare = 0: Percent giữ default = 0, NHƯNG ValueChange = main → mâu thuẫn
}

→ FE chỉ display giá trị BE trả về — không có lỗi ở FE. Bug nằm hoàn toàn ở Go handler.

Scope ảnh hưởng: Cùng 3 chart như BUG-01 (Bar + 2 Line) — đều dùng chung SetValueObject().

Cách fix (BE):

go
// ✅ ĐÚNG: guard cả 2 field cùng nhau
func SetValueObject(valueObject *ValueObject, main float32, compare float32) {
    valueObject.Value = main
    if compare == 0 {
        valueObject.ValueChange = nil    // trả null
        valueObject.Percent = nil
        return
    }
    valueObject.ValueChange = main - compare
    valueObject.Percent = valueObject.ValueChange / compare
}

Nếu struct hiện tại không cho phép nil (do dùng float32 thay vì *float32), cần đổi 2 field này sang pointer + update FE check null thay vì 0.

Cách fix (FE — small):

ts
// Trong BarChartActualRevenue + LineChartActualRevenue tooltip render:
const percentText = percent === null ? '—' : `${(percent * 100).toFixed(1)}%`
const deltaText = priceVolatility === null ? '—' : formatMoney(priceVolatility)

Acceptance Criteria:

  • [ ] Sửa SetValueObject() để khi compare = 0 thì cả PercentValueChange đều null (hoặc field nào BE convention)
  • [ ] FE tooltip render khi nhận null
  • [ ] Khi compare > 0: cả 2 field phải cùng dấu (không thể Δ% = 0Δ abs ≠ 0)
  • [ ] Áp dụng cho cả 3 chart (Bar + 2 Line)
  • [ ] Sau khi fix BUG-01 (label) + BUG-02 (data), tooltip sẽ tự nhất quán

🔴 BUG-03: Title "trung bình theo đơn hàng" nhưng backend đang trả về TỔNG doanh thu

Hiện trạng (Actual):

Title: "Biến động trung bình theo đơn hàng"
GraphQL field: avg_order_revenue_by_month   ← tên field nói "AVG"
Giá trị tháng 10/2025:
  - Dịch vụ:   705,21 Triệu                  ← thực chất là SUM doanh thu tháng
  - Mỹ phẩm:    70,66 Triệu
  - Nạp thẻ:       10 Triệu
  - Tổng:      785,87 Triệu

Bằng chứng từ codebase (đã verify):

Lỗi #1 — SQL function chỉ tính SUM:

Trong DB function public.search_dashboard_sales_revenue_chart() (migration 1752116891000 + 1777431629000), CTE time_group_full (dòng 339–353) chỉ có SUM(...), không có / COUNT(...):

sql
SUM(invoice_amount)::bigint AS payment_amount,
SUM(CASE WHEN order_kind = 'service' THEN invoice_amount END)::bigint AS service_amount,
SUM(CASE WHEN order_kind = 'cosmetic' THEN invoice_amount END)::bigint AS cosmetic_amount,
SUM(CASE WHEN order_kind = 'prepaid' THEN invoice_amount END)::bigint AS prepaid_amount

Lỗi #2 — Go code gán nhầm SUM cho field Avg*:

File report_sales_revenue.go dòng 420–428:

go
// HIỆN TẠI (SAI):
output.AvgOrderRevenueByMonth[idx].ValueObject.Total.Value    = float32(revenueByTimeGroup.PaymentAmount)
output.AvgOrderRevenueByMonth[idx].ValueObject.Service.Value  = float32(revenueByTimeGroup.ServiceAmount)
output.AvgOrderRevenueByMonth[idx].ValueObject.Cosmetic.Value = float32(revenueByTimeGroup.CosmeticAmount)
output.AvgOrderRevenueByMonth[idx].ValueObject.Prepaid.Value  = float32(revenueByTimeGroup.PrepaidAmount)
// ❌ Gán PaymentAmount (SUM) vào field tên Avg* → AOV hiển thị thực chất là TỔNG

⚠️ Cảnh báo strategy fix (REVIEW UPDATE):

KHÔNG được thay SUM(...) thành SUM(...) / COUNT(...) trong CTE — vì 4 chart khác trên cùng report cũng dùng function này và CẦN giá trị TỔNG:

ChartField DB dùngCần
Line "Biến động thực thu"payment_amountTỔNG ✅
Pie "Tỉ lệ thực thu DV"revenue_service_group (build từ service_amount)TỔNG ✅
Pie "Tỉ lệ thực thu MP"revenue_cosmetic_group (build từ cosmetic_amount)TỔNG ✅
Pie "Tỉ lệ thực thu thẻ"revenue_prepaid_group (build từ prepaid_amount)TỔNG ✅
Bar "Biến động trung bình theo đơn hàng"service_amount, cosmetic_amount, prepaid_amountAVG ← chart đang fix

→ Replace SUM sẽ phá 4 chart kia.


✅ Strategy đúng: THÊM 4 cột AOV mới, GIỮ NGUYÊN cột SUM cũ

Bước 1 — Sửa SQL (migration mới, ADD cột):

sql
-- Trong CTE time_group_full — GIỮ nguyên 4 cột SUM cũ, THÊM 4 cột AVG mới:
time_group_full AS (
  SELECT
    label,
    -- Cũ (giữ nguyên — cho Line/Pie):
    SUM(invoice_amount)::bigint AS payment_amount,
    SUM(CASE WHEN order_kind = 'service'  THEN invoice_amount END)::bigint AS service_amount,
    SUM(CASE WHEN order_kind = 'cosmetic' THEN invoice_amount END)::bigint AS cosmetic_amount,
    SUM(CASE WHEN order_kind = 'prepaid'  THEN invoice_amount END)::bigint AS prepaid_amount,
    -- 🆕 Mới (cho Bar Chart AOV):
    (SUM(invoice_amount) / NULLIF(COUNT(DISTINCT order_id), 0))::bigint AS avg_payment_amount,
    (SUM(CASE WHEN order_kind = 'service'  THEN invoice_amount END) 
        / NULLIF(COUNT(DISTINCT order_id) FILTER (WHERE order_kind = 'service'), 0))::bigint AS avg_service_amount,
    (SUM(CASE WHEN order_kind = 'cosmetic' THEN invoice_amount END) 
        / NULLIF(COUNT(DISTINCT order_id) FILTER (WHERE order_kind = 'cosmetic'), 0))::bigint AS avg_cosmetic_amount,
    (SUM(CASE WHEN order_kind = 'prepaid'  THEN invoice_amount END) 
        / NULLIF(COUNT(DISTINCT order_id) FILTER (WHERE order_kind = 'prepaid'), 0))::bigint AS avg_prepaid_amount
  FROM invoice_base_data
  WHERE payment_method_id NOT IN ('wallet', 'wallet_promotion')
  GROUP BY label
  ORDER BY label
)

Bước 2 — Sửa Go struct + mapping:

go
// Trong RevenueByTimeGroup struct: thêm 4 field mới
type RevenueByTimeGroup struct {
    Label          string
    PaymentAmount  int64    // giữ nguyên
    ServiceAmount  int64    // giữ nguyên
    CosmeticAmount int64    // giữ nguyên
    PrepaidAmount  int64    // giữ nguyên
    // 🆕 4 field mới
    AvgPaymentAmount  int64
    AvgServiceAmount  int64
    AvgCosmeticAmount int64
    AvgPrepaidAmount  int64
}

// Trong CaculateData() dòng 420-428: đổi gán
output.AvgOrderRevenueByMonth[idx].ValueObject.Total.Value    = float32(revenueByTimeGroup.AvgPaymentAmount)   // 🆕
output.AvgOrderRevenueByMonth[idx].ValueObject.Service.Value  = float32(revenueByTimeGroup.AvgServiceAmount)   // 🆕
output.AvgOrderRevenueByMonth[idx].ValueObject.Cosmetic.Value = float32(revenueByTimeGroup.AvgCosmeticAmount)  // 🆕
output.AvgOrderRevenueByMonth[idx].ValueObject.Prepaid.Value  = float32(revenueByTimeGroup.AvgPrepaidAmount)   // 🆕
// PaymentAmountByMonth (cho LineChart) vẫn dùng PaymentAmount cũ — không đổi

Acceptance Criteria:

  • [ ] PO confirm: chart "Biến động trung bình theo đơn hàng" cần hiển thị AOV (trung bình/đơn), KHÔNG phải tổng
  • [ ] Migration thêm 4 cột AOV mới trong SQL function, KHÔNG xóa cột SUM cũ
  • [ ] Go struct + Go mapping cập nhật để đọc cột AOV mới cho AvgOrderRevenueByMonth
  • [ ] LineChart "Biến động thực thu" và 3 PieChart "Tỉ lệ thực thu *" hiển thị giá trị KHÔNG ĐỔI trước và sau fix
  • [ ] BarChart "Biến động trung bình theo đơn hàng" hiển thị AOV: Dịch vụ ~ 500K – 50 Triệu/đơn (realistic spa)
  • [ ] Cross-check: AOV × COUNT(orders) ≈ Tổng doanh thu (sai số do rounding < 1%)
  • [ ] Đơn vị hiển thị: dynamic — "Nghìn" nếu < 1 triệu, "Triệu" nếu ≥ 1 triệu

🟡 BUG-04: Cột 05/2026 hiển thị partial data không chú thích

Hiện trạng (Actual):

  • Cột 5/2026 ~140 Triệu
  • Trung bình các tháng khác > 600 Triệu
  • Hôm nay là 2026-05-11 → tháng 5 mới qua ~36% thời gian

→ Cột đang hiển thị data của tháng đang diễn ra nhưng không có chú thích → người xem hiểu nhầm doanh thu giảm mạnh.

Mong đợi (Expected) — chọn 1 trong 3:

OptionCách xử lýƯu/nhược
ALabel cột bằng badge "đang diễn ra" + dashed barTrực quan, giữ data
BExclude cột tháng hiện tại khỏi chartSạch nhưng mất context
CNormalize theo run-rate (× ngày-trong-tháng / ngày-đã-qua)Dự báo nhưng dễ gây hiểu nhầm khác

Đề xuất: Option A — giữ data, thêm visual cue.

Acceptance Criteria:

  • [ ] Cột tháng đang diễn ra có visual khác biệt (dashed, opacity 50%, hoặc pattern)
  • [ ] Hover tooltip hiển thị thêm dòng: "Data đến ngày dd/mm/yyyy — tháng chưa kết thúc"
  • [ ] Logic detect tháng hiện tại dùng Asia/Ho_Chi_Minh timezone

🔴 BUG-05: Toàn report load rất chậm (Performance) — gồm 2 handler

Hiện trạng (Actual):

PO/BA phản ánh report /r/reports/actual_revenue_report_group load rất chậm — ảnh hưởng trải nghiệm khi mở trong cuộc họp.

ℹ️ Note (QA reviewed): FE đang lazy-load bảng "Chi tiết hoạt động kinh doanh" — query pause: true cho đến khi user click "Hiển thị chi tiết" (xem TableDetailActualRevenue.tsx:58). Nghĩa là:

  • Initial page load chỉ gọi handler 1 reportSalesRevenue (overview + 3 chart) → đây là phần user phản ánh chậm
  • Sau khi click expand mới gọi handler 2 reportSalesRevenueByBranch (bảng)

Fix cần tách AC cho 2 case: initial load và expanded table.

Root cause (đã verify codebase) — 2 handler khác nhau, cùng pattern sequential:

Handler 1: reportSalesRevenue (Overview + 3 chart)

File report_sales_revenue.go — function GetOrderAndInvoiceBase() (dòng 179–354):

Call 1 (dòng 199-204): search_dashboard_sales_revenue        → totals
Call 2 (dòng 209-214): search_dashboard_customer_visits      → customer counts
Call 3 (dòng 219-227): search_dashboard_telesale_amount      → telesale
Call 4 (dòng 229-237): search_dashboard_sales_revenue_chart  → time series (3 chart)

Nếu user CÓ set compare period (dòng 242-281):
Call 5-8: 4 query thêm cho compare period
→ Tổng 8 queries sequential, ước tính 4–24s

Handler 2: reportSalesRevenueByBranch (Bảng chi tiết) — MỚI PHÁT HIỆN

File report_sales_revenue_by_branch.go — function reportSalesRevenueByBranch() (dòng 80–165):

Call 1 (dòng 104): search_dashboard_sales_revenue_by_branchs  → 6 CTE × N chi nhánh
Call 2 (dòng 114-122): store.QueryProductGroup()              → product master
→ Tổng 2 queries sequential, ước tính 300–700ms

DB function search_dashboard_sales_revenue_by_branchs đã có 6 CTE song song (good design) nhưng FE vẫn phải đợi handler trả về tuần tự.

Frontend (cần verify thêm):

Hai handler trên gọi Hasura action khác nhau → FE có thể gọi parallel (Promise.all hoặc URQL parallel queries) hoặc sequential. Cần verify cách FE đang dùng. Nếu sequential → tổng thời gian load = (4–24s) + (0.3–0.7s).

Vấn đề chính:

  1. Handler 1 sequential calls — 4–8 query Hasura mỗi cái 1–3s → tổng 4–24s (vấn đề chính)
  2. Handler 2 sequential calls — 2 query → 0.3–0.7s (vấn đề phụ, vẫn cải thiện được)
  3. FE có thể sequential 2 handler — cần verify (Promise.all vs await tuần tự)
  4. Thiếu index? — chưa có composite index trên (paid_at, status, parent_id, branch_id). Cần EXPLAIN ANALYZE để confirm planner
  5. Không có cache — re-query data các tháng đã hoàn thành (immutable)

Mong đợi (Expected) — tách 2 case theo lazy-load:

  • Initial load (overview + 3 chart, KHÔNG có bảng chi tiết): P95 < 5 giây
  • Mỗi chart visible: P95 < 2 giây sau khi click vào report
  • Expanded table (sau khi user click "Hiển thị chi tiết"): P95 < 2 giây thêm

Giải pháp đề xuất (theo thứ tự ưu tiên):

#Giải phápLayerEffortImpact
1Parallel 4–8 Hasura calls trong handler reportSalesRevenue bằng errgroupBES (~1h)Giảm 50-70% thời gian (4 calls → ~1 call worth of time)
2Parallel 2 calls trong handler reportSalesRevenueByBranch (function call + product group)BES (~30p)Giảm 30-40% thời gian bảng
3Verify FE gọi 2 handler parallel (URQL parallel queries / Promise.all), không sequentialFES (~30p)Tránh waterfall — toàn page load song song
4EXPLAIN ANALYZE query thực tế → decide add composite index invoice(paid_at, status, parent_id, branch_id)BE+DBAS (~2h)Tùy plan analyzer — có thể giảm 30-60%/query hoặc không cải thiện
5Cache kết quả các tháng đã hoàn thành (immutable) ở Redis/app_setting với TTL 24hBEM (~1 ngày)Lần load thứ 2 trở đi gần như instant
6Materialized view cho aggregation, refresh nightlyDBAL (~2-3 ngày)Nhanh nhất nhưng phức tạp

→ Đề xuất làm Option 1 + 2 + 3 trước (quick wins chắc chắn). Option 4 chỉ làm SAU KHI đo EXPLAIN ANALYZE — không add index mù. Option 5-6 tách ticket riêng nếu vẫn chậm.

Acceptance Criteria:

  • [ ] 4–8 Hasura calls trong GetOrderAndInvoiceBase() (handler 1) chạy parallel qua errgroup (verify trong code)
  • [ ] 2 calls trong reportSalesRevenueByBranch() (handler 2) chạy parallel qua errgroup (verify trong code)
  • [ ] FE verify: 2 action reportSalesRevenue + reportSalesRevenueByBranch được fetch parallel (không await tuần tự)
  • [ ] Chạy EXPLAIN ANALYZE cho 2 query chính (search_dashboard_sales_revenue_chart + search_dashboard_sales_revenue_by_branchs) với range 18 tháng — output đính kèm vào ticket trước/sau
  • [ ] Nếu EXPLAIN cho thấy Seq Scan trên invoice → add composite index → re-run EXPLAIN xác nhận chuyển sang Index Scan
  • [ ] Đo P95 load time TRƯỚC và SAU fix (timestamp các call qua DevTools Network tab hoặc backend tracing)
  • [ ] Đạt:
    • Initial load (overview + 3 chart, không có bảng vì lazy): P95 < 5s
    • Mỗi chart visible: P95 < 2s
    • Expanded table (sau click): P95 < 2s thêm
    • QA verify: bảng vẫn lazy-load (không bị eager bug regression)
  • [ ] Thêm log/metric tracking thời gian cả 2 handler (cho monitoring tương lai)

🟠 BUG-06: Tỉ lệ đóng góp thực thu Telesale (telesale_amount_rate) — NEEDS DISCOVERY

⚠️ STATUS: NEEDS DISCOVERY — KHÔNG ready-for-dev. Cần BE/DBA chạy reconciliation invoicefund trước khi chốt fix strategy.

Hiện trạng (Actual):

Trong mục "Hiệu quả" của Báo cáo Doanh thu thực tế, KPI "Tỉ lệ đóng góp thực thu Telesale" đang hiển thị thấp bất thường. Tooltip công thức (intent) ghi:

Tỉ lệ đóng góp thực thu Telesale
  = Tổng doanh số hoa hồng của NV thuộc nhóm Telesale
    ─────────────────────────────────────────────────
    Tổng thực thu phát sinh

Description: "Tỉ lệ số tiền mà Telesale đã mang về trên tổng số tiền mà công ty thu được trong kỳ"


Verified evidence từ codebase (KHÔNG đầy đủ — cần discovery):

VếField hiện tạiSource SQL (verified)Đơn vị thực tế?
Tử sốtelesale_amountSUM(invoice.amount × commission_ratio) — file 1761037948000_*/up.sql:43Cần verify (gross invoice, nhưng có thể đại diện cash nếu invoice = paid)
Mẫu sốTotalPaymentAmountSUM(f.amount) FROM fund f WHERE f.type='incoming' — file 1761037948000_*/up.sql:183Cash từ bảng fund (KHÔNG phải fund.payment)

→ Em đã sai khi giả định mẫu số là fund.payment. Mẫu số THỰC TẾ là bảng fund (cash transaction log), với filter type='incoming', exclude wallet.


Vì sao chưa thể chốt là bug:

3 giả thuyết cần verify, mỗi cái dẫn tới fix khác nhau:

#Giả thuyếtNếu đúng → Fix
H1Tử số gross invoice, mẫu cash từ fund → mismatch dimension thậtĐổi tử về cash của đơn telesale (cần biết fund.order_id hoặc relationship)
H2invoice.amount thực ra LUÔN BẰNG cash đã thu (invoice chỉ tạo khi pay) → không có mismatchKHÔNG phải bug — cần kiểm tra nghiệp vụ Diva về flow invoice/payment
H3fund f không cover hết payment của invoice (vd: missing some payment_method) → mẫu số bị thiếuBug nằm ở mẫu số, không phải tử số

Không thể đề xuất SQL fix mà chưa biết relationship giữa invoicefund.


Discovery Questions (BE/DBA cần verify trước khi fix):

#Câu hỏiCách verify
D1Schema bảng fund có cột nào link tới invoice hoặc order? (vd: invoice_id, order_id, reference_id)\d+ fund trên prod
D2Có invoice nào KHÔNG có row trong fund không? (case: tạo invoice nhưng chưa thu tiền — công nợ)LEFT JOIN + COUNT
D3Có row fund nào KHÔNG link tới invoice không? (case: tiền vào không phải từ order — refund, internal transfer)Tương tự D2
D4Trong kỳ test, tổng SUM(i.amount) từ invoice_cte vs tổng SUM(f.amount) từ fund_cte chênh bao nhiêu?Cross-query
D5Nghiệp vụ: 1 invoice có thể có nhiều row fund (trả nhiều lần) không? Có thể có 0 row fund (chưa trả) không?Hỏi BE/PO
D6Field name telesale_amount đặt là "amount" — nghĩa là gross hay cash? Có document/comment nào không?Grep + interview BE

Reconciliation SQL cần chạy (trước khi quyết định fix):

sql
-- Reconciliation: invoice (gross) vs fund incoming (cash) trong cùng 1 kỳ
WITH inv AS (
  SELECT order_id, SUM(amount) AS invoice_total
  FROM ecommerce.invoice
  WHERE parent_id IS NULL
    AND (status = 'invoice_completed' OR status IS NULL)
    AND created_at BETWEEN '2026-04-01' AND '2026-04-30'
  GROUP BY order_id
),
cash AS (
  -- TODO: cần biết fund link với order_id thế nào — câu D1
  SELECT ??? AS order_id, SUM(amount) AS cash_total
  FROM fund
  WHERE type = 'incoming'
    AND date::timestamp BETWEEN '2026-04-01' AND '2026-04-30'
    AND payment_method_id NOT IN ('wallet','wallet_promotion')
  GROUP BY ???
)
SELECT 
  COUNT(*) AS total_orders,
  COUNT(*) FILTER (WHERE inv.invoice_total > cash.cash_total) AS orders_with_debt,
  ROUND(AVG(cash.cash_total::numeric / NULLIF(inv.invoice_total, 0)) * 100, 2) AS avg_payment_ratio_pct
FROM inv
LEFT JOIN cash USING (order_id);

Output cần báo cáo:

  • total_orders: tổng đơn có invoice
  • orders_with_debt: số đơn invoice > cash
  • avg_payment_ratio_pct: % thực thu trung bình so với invoice

Nếu avg_payment_ratio_pct = 100% → H2 đúng → KHÔNG phải bug, BUG-06 đóng. Nếu avg_payment_ratio_pct < 95% → H1 đúng → confirmed bug, cần fix theo strategy mới (sẽ design sau khi biết D1).


Note — Metric 2 (telesale_conversion_rate) ĐÃ ĐÚNG, KHÔNG cần fix:

sql
COUNT(DISTINCT đơn telesale) / COUNT(*) toàn đơn

Cả 2 vế cùng đơn vị "đơn" → logic chuẩn. Dev KHÔNG được động vào.


Acceptance Criteria (sau khi DISCOVERY xong):

  • [ ] BE/DBA hoàn thành reconciliation SQL → output đính kèm vào ticket
  • [ ] PO/BA review output → quyết định: BUG (H1/H3) hay KHÔNG (H2)
  • [ ] Nếu BUG → spec lại fix strategy với CTE join đúng (dựa trên D1)
  • [ ] Nếu KHÔNG → đóng BUG-06, ghi rõ lý do "data hợp lý: invoice = cash do nghiệp vụ"
  • [ ] Verify Metric 2 telesale_conversion_rate KHÔNG ĐỔI (regression)

SQL audit (cho ticket riêng Operations — Q8):

sql
-- Audit: % đơn có gán telesale commission
SELECT 
  CASE WHEN EXISTS(
    SELECT 1 FROM ecommerce.order_commission oc 
    JOIN ecommerce.ecommerce_user_role eur ON eur.user_id = oc.user_id
    WHERE oc.order_id = o.id 
      AND eur.role_id IN ('telesales_leader','telesales_staff','telesales_app')
      AND oc.amount > 0
  ) THEN 'Có telesale' ELSE 'Không' END AS has_telesale,
  COUNT(*) AS so_don,
  ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) AS pct
FROM ecommerce.order o
WHERE o.created_at BETWEEN '2026-04-01' AND '2026-04-30'
  AND o.deleted_at IS NULL
  AND o.status = 'completed'
GROUP BY 1;

→ Audit này độc lập với BUG-06 — chạy luôn để hiểu data quality, không chờ discovery.


3. Scope & Out of Scope

In scope (6 bug — ảnh hưởng 3 chart + 1 bảng + 1 KPI):

  • BUG-01 + BUG-02: fix BE → ảnh hưởng cả 3 chart (Bar AOV + 2 Line)
  • BUG-03 (AOV): chỉ ảnh hưởng Bar chart — strategy ADD cột mới để không phá Line + Pie khác
  • BUG-04 (visual cột hiện tại): chỉ ảnh hưởng Bar chart
  • BUG-05 (performance): ảnh hưởng CẢ 2 handler — reportSalesRevenue (overview + chart) + reportSalesRevenueByBranch (bảng chi tiết) — toàn report load nhanh
  • BUG-06 (telesale_amount_rate): chỉ ảnh hưởng 1 KPI ở mục "Hiệu quả" — cùng codebase + cùng team fix
  • Áp dụng cho tất cả filter (toàn hệ thống / theo chi nhánh / khoảng thời gian / by_day/month/year)

Out of scope:

  • 3 Pie chart "Tỉ lệ thực thu DV/MP/Thẻ" — chỉ verify regression (giá trị không đổi), không refactor
  • telesale_conversion_rate (KPI Metric 2 — "Tỉ lệ khách phát sinh doanh số từ Telesale") — logic ĐÃ ĐÚNG, KHÔNG động vào, chỉ verify regression
  • Các efficiency metrics khác (avg_*_per_visit, crm_*_rate, etc.) — audit riêng (xem Section 8 follow-up)
  • Tab 2 "Tỉ lệ khách hàng" (CustomerCycleRateReport) — khác data source, không bị ảnh hưởng
  • Audit data quality "telesale có gán đúng/sai vào đơn không" — ticket riêng cho Operations team (xem SQL audit trong BUG-06)
  • Thay đổi data model bảng invoice / order / order_commission
  • Thêm filter mới hay segment mới
  • Materialized view / Redis cache (tách ticket P2 nếu fix Option 1+2 của BUG-05 vẫn chưa đủ)
  • i18n cho hardcoded title (tech debt, không phải bug)

4. Affected Module / Component

💡 QA tip: Section này dev-focused (file paths, function names). QA có thể skip xuống thẳng Section 5 — Test Cases. Click để xem chi tiết kỹ thuật:

Click để xem chi tiết file paths + DB functions + indexes (dev/DBA reference)

Frontend (Vue 3 + Quasar + TS)

LayerFileMô tả
Pagediva-admin/src/modules/report/pages/ActualRevenueReport.tsxPage wrapper, route /r/reports/actual_revenue_report_group
TabActualRevenueReportDetailTab "Báo cáo" (ACTUAL_REVENUE_REPORT_DETAIL)
SectionActualRevenueReportVolatility.tsx:187Chứa 3 chart "Biến động *"
Chart BarCharts/BarChartActualRevenue.tsx:125"Biến động trung bình theo đơn hàng" — BUG-03, BUG-04 hiển thị
Chart Line × 2Charts/LineChartActualRevenue.tsx:49–82"Biến động thực thu" + "Biến động khách hàng" — cùng pattern BUG-01, BUG-02
GraphQL querydiva-admin/src/modules/report/graphql/report_actual_revenue.graphqlFields: payment_amount_by_month, customer_visits_by_month, avg_order_revenue_by_month

Bar Chart — 3 series color (verify):

  • service (Dịch vụ) — #3479E9
  • cosmetic (Mỹ phẩm) — #91C3FD
  • prepaid (Nạp thẻ) — #BEDBFE

Backend (Go + Hasura + PostgreSQL)

LayerPathMô tả
Hasura actionreportSalesRevenueDefined trong diva-backend/services/controller/ metadata
Go handlerreport_sales_revenue.go:162reportSalesRevenue()Entry point handle action
Query batchreport_sales_revenue.go:179–354GetOrderAndInvoiceBase()BUG-05 — 4–8 calls sequential, cần parallel hóa
Compute Δreport_sales_revenue.go:583–588SetValueObject()BUG-02 — thiếu guard khi compare = 0
Build labelsreport_sales_revenue.go:593–601generateLabels()BUG-01 — fallback CompareTime = Time khi user không set compare
Map AOVreport_sales_revenue.go:420–428CaculateData()BUG-03 — gán PaymentAmount (SUM) vào field AvgOrderRevenueByMonth
DB function (chart)public.search_dashboard_sales_revenue_chart() migration 1752116891000 + 1777431629000BUG-03 — cần ADD 4 cột AOV mới, GIỮ NGUYÊN cột SUM cũ (vì share với 4 chart khác)
CTE chínhtime_group_full dòng 339–353Aggregation, GROUP BY time period (by_day / by_month / by_year)
Handler 2 (bảng)report_sales_revenue_by_branch.go:80–165reportSalesRevenueByBranch()BUG-05 — handler riêng cho bảng "Chi tiết hoạt động kinh doanh", cũng cần parallel hóa
DB function (bảng)public.search_dashboard_sales_revenue_by_branchs() migration 1777431629000_improve_func_sales_revenue6 CTE song song (good design), 1 row/chi nhánh
DB function (telesale)public.search_dashboard_telesale_amount() migration 1761037948000_update_func_search_dashboard_sales_revenue:318BUG-06 — cần ADD CTE telesale_payment_cte tính thực thu của đơn telesale
Map TelesaleAmountRatereport_sales_revenue.go:318BUG-06 — đổi divisor từ TelesaleAmount (doanh số) → TelesalePaymentAmount (thực thu)

⚠️ Function share — cảnh báo ripple effect

public.search_dashboard_sales_revenue_chart() được sử dụng bởi 5 chart:

  • Bar "Biến động trung bình theo đơn hàng" (chart fix)
  • Line "Biến động thực thu" (cùng share payment_amount)
  • Pie "Tỉ lệ thực thu DV/MP/Thẻ" × 3 (build từ service_amount/cosmetic_amount/prepaid_amount)

KHÔNG được thay cột SUM hiện tại — phải ADD cột mới (xem strategy BUG-03).

Source data

  • Bảng chính: invoice JOIN order
  • Filter (CTE invoice_filtered):
    • parent_id IS NULL (chỉ parent invoices, không lấy child)
    • status = 'invoice_completed' OR status IS NULL
    • paid_at BETWEEN _from AND _to
    • payment_method_id NOT IN ('wallet', 'wallet_promotion')
    • branch_id filter tùy chọn

Index hiện tại (verify trong migrations)

  • idx_invoice_parent_id — single column trên parent_id
  • idx_completed_at — single column trên completed_at
  • idx_order_item_id — trên bảng order_item
  • KHÔNG có composite index trên (paid_at, status, parent_id, branch_id) — cần EXPLAIN ANALYZE để quyết định

4B. UI Spec cho Designer

🎨 Status: Đang chờ Designer review. PO/BA đã draft 6 mục B1–B6 dưới đây. Designer chỉ cần:

  • Điền câu trả lời ở các chỗ ???
  • Confirm/chỉnh các phương án em đề xuất
  • Đính kèm Figma link (nếu vẽ thêm) vào B-Ref

B1 — State Matrix cho Bar Chart (BUG-04)

Chart hiển thị 4 state khác nhau tùy thời điểm + data. Designer chọn visual cho mỗi state:

#StateKhi nào xuất hiệnVisual đề xuấtDesigner chốt
1NormalTháng/ngày đã hoàn thành, có dataSolid bar, 100% opacity, màu hệ thống Diva✅ giữ nguyên hiện tại
2OngoingNgày/tháng đang diễn ra (vd 11/05 hôm nay) — partial dataDashed pattern (4px dash + 2px gap), opacity 50%, viền nét đứt??? Confirm hoặc đề xuất khác
3FutureNgày sau hôm nay (đã chốt Q4: ẨN HẲN)Không render cột✅ ẩn — không cần design
4No dataBranch/period không có giao dịch (vd VP Cần Thơ all-zero)Cột nhỏ chiều cao = 0 + label "—" dưới trục X??? Confirm hoặc đổi (vd ẩn cột?)
5HoverMouseover bất kỳ cộtSáng lên + tooltip xuất hiện??? Pattern hover Diva đang dùng?
6SelectedClick cột (nếu có chức năng drill-down)N/A — chart hiện tại không có drill-down✅ skip

Câu hỏi cho Designer: State #2 (Ongoing) là quan trọng nhất — pattern visual phải đủ khác biệt với state Normal để user nhận biết "data chưa đủ" nhưng không quá nổi bật gây phân tán.


B2 — Tooltip Copy Text (BUG-01, BUG-02, BUG-04)

Wording cho tooltip — Designer chốt theo design system của Diva:

#Trường hợpOption em đề xuấtDesigner chốt
1Header — CÓ compare periodMonthly view: "10/2025 vs 09/2025" (MM/YYYY); Daily view: "11/05/2026 vs 10/05/2026" (DD/MM/YYYY)??? Confirm format
2Header — KHÔNG có compare periodChỉ "10/2025" (bỏ vế "vs")??? Confirm hay muốn thêm hint "Chọn period so sánh để xem biến động"?
3Δ% khi compare = null"—" (em-dash, font weight regular, màu muted gray)??? Hay "N/A" / "Chưa có" / khác?
4Δ abs khi compare = null"—" (giống #3)??? Phải đồng bộ với #3
5Đơn vị — giá trị < 1 triệu"680 Nghìn" (k VND không dùng)???
6Đơn vị — 1 triệu ≤ giá trị < 1 tỷ"2,15 Triệu" (Vietnamese decimal comma)???
7Đơn vị — giá trị ≥ 1 tỷ"1,82 Tỷ"???
8Note "cột đang diễn ra" (BUG-04)"Data đến 11/05/2026 — tháng/ngày chưa kết thúc"??? Wording chuẩn?
9Note "không có giao dịch""Chưa có giao dịch trong kỳ này"???
10Icon trending Δ% dương↗ + màu xanh #22C55E??? Hệ thống có icon riêng?
11Icon trending Δ% âm↘ + màu đỏ #EF4444???
12Icon trending Δ% = 0→ (ngang) + màu gray???

B3 — Tooltip Dictionary (info icon ℹ️ giải thích nghiệp vụ)

User mới có thể không hiểu "AOV" / "trung bình đơn hàng" — cần info icon giải thích:

Màn hìnhElementTooltip textĐiều kiện hiện
Bar AOVIcon ℹ️ cạnh title"Trung bình doanh thu mỗi đơn hàng = Tổng thực thu / Số đơn hàng. Giúp đánh giá giá trị mỗi giao dịch."Luôn hiện (hover)
Line "Biến động thực thu"Icon ℹ️ cạnh title"Tổng doanh thu thực tế thu được (đã trừ wallet/promotion)."Luôn hiện
Line "Biến động khách hàng"Icon ℹ️ cạnh title"Số lượt khách đến (visits), không phải số khách unique."Luôn hiện
Bảng Chi tiết HĐKDCột "Telesales" header"Khách hàng đến do telesale gọi/tư vấn — track qua tag CRM."Hover header
Bảng Chi tiết HĐKDCột "Tự đến" header"Khách hàng walk-in, không qua telesale."Hover header
Bảng Chi tiết HĐKDCột "Công nợ""Số tiền khách còn nợ chưa thanh toán."Hover header

Câu hỏi cho Designer: Vị trí icon ℹ️ (trước/sau title? size? color?) — cần thống nhất với design system.


B4 — Empty State cho bảng "Chi tiết HĐKD"

Screenshot phát hiện: VP Cần Thơ hiển thị tất cả 0 ở mọi cột — UX hiện tại confusing.

Đề xuất 3 phương án (Designer chọn):

OptionCách hiển thịƯuNhược
AGiữ row, thay 0 bằng "—" (em-dash) ở tất cả cellVẫn cho biết branch tồn tại trong filterVẫn chiếm 1 row table
BẨn hẳn row branch không có dataBảng sạch, focus data thậtUser không biết branch đó tồn tại
CGiữ row + badge "Chưa có giao dịch" ở cột "Chi nhánh"Rõ ràng nhấtTốn space

Em đề xuất Option C cho rõ ràng, nhưng Designer quyết định cuối.


B5 — Loading State (BUG-05)

Hiện tại report load 5–9s mà không có feedback → user nghĩ trang lỗi/treo. Designer cần spec loading pattern:

LayerHiện tạiĐề xuấtDesigner chốt
Overview tiles (8 metric boxes)??? (chưa rõ — Designer check current)Skeleton — block xám 60×24px nhấp nháy???
3 Charts (Bar + 2 Line)???Chart skeleton: 6–8 thanh xám placeholder???
3 Pie charts???Vòng tròn xám với segments???
Bảng Chi tiết HĐKD???5–10 skeleton rows???
Toàn page???Header + sidebar render ngay; content area skeleton???

Pattern animation: Designer chốt — shimmer effect (gradient di chuyển) hay pulse (opacity 50%→100%)?


B6 — ASCII Wireframe Tooltip mới (BUG-01 + BUG-02)

┌─────────────────────────────────┐         ┌─────────────────────────────────┐
│ 10/2025 vs 09/2025              │         │ 10/2025                         │
│                                 │         │                                 │
│ Trung bình đơn hàng             │         │ Trung bình đơn hàng             │
│ ● Dịch vụ:  2,15 Triệu          │         │ ● Dịch vụ:  2,15 Triệu          │
│   ↗ 16,2% (+0,30 Triệu)         │         │   —                             │
│ ● Mỹ phẩm:  680 Nghìn           │         │ ● Mỹ phẩm:  680 Nghìn           │
│   ↗ 5,1% (+33 Nghìn)            │         │   —                             │
│ ● Nạp thẻ:  1,20 Triệu          │         │ ● Nạp thẻ:  1,20 Triệu          │
│   ↘ 8,3% (-110 Nghìn)           │         │   —                             │
│ ─────────────────               │         │ ─────────────────               │
│ Tổng:       4,03 Triệu          │         │ Tổng:       4,03 Triệu          │
│   ↗ 7,9% (+295 Nghìn)           │         │   —                             │
└─────────────────────────────────┘         └─────────────────────────────────┘
   [State 1: User CÓ set compare]              [State 2: User KHÔNG set compare]

Câu hỏi cho Designer:

  1. Layout này OK chưa? Có muốn đổi thứ tự (vd Tổng lên đầu)?
  2. Khoảng cách padding/margin?
  3. Width tooltip cố định hay auto?
  4. Position tooltip (top/bottom của cột)?
  5. Trên mobile/tablet có thay đổi gì không?

B-Ref — Designer deliverables

Sau khi Designer review xong:

  • [ ] Figma file link (nếu vẽ thêm) — paste vào đây: ???
  • [ ] Export PNG mockup các state (B1) — đính kèm vào ticket
  • [ ] Confirm color codes (đặc biệt B2 #10-12 — icon trending) đã match design system
  • [ ] Confirm tất cả copy text (B2 + B3) phù hợp với tone Diva

5. Test Cases (QA verify)

TC nhóm Logic so sánh (BUG-01 + BUG-02) — apply cho CẢ 3 chart

#StepsExpected
TC-01Mở report, KHÔNG set compare period, hover bất kỳ điểm nào trên Bar chart AOVHeader: chỉ hiện "10/2025", KHÔNG có vs ...; Δ% và Δ abs hiển thị "—"
TC-02Tương tự TC-01 nhưng trên Line "Biến động thực thu"Same as TC-01 — logic chung qua SetValueObject()
TC-03Tương tự TC-01 nhưng trên Line "Biến động khách hàng"Same as TC-01
TC-04Set compare period (MoM), hover tháng 10/2025 (có data cả current + previous)Header: "10/2025 vs 9/2025"; Δ% và Δ abs cùng dấu (cùng dương / cùng âm / cùng = 0)
TC-05Set compare nhưng previous period không có data (vd compare = 12/2024 khi BE return rỗng)Δ% và Δ abs đều hiện "—", không hiện 0%

TC nhóm AOV correctness (BUG-03)

#StepsExpected
TC-06Sau fix, hover Bar chart tháng 10/2025 (Dịch vụ)AOV trong khoảng 500K – 50 Triệu/đơn (realistic spa)
TC-07Cross-check AOV × COUNT(orders) ≈ Tổng doanh thuSai số do rounding < 1% so với chart "Biến động thực thu" cùng tháng
TC-08Verify Line "Biến động thực thu" và 3 Pie "Tỉ lệ thực thu *"Giá trị KHÔNG ĐỔI trước và sau fix (regression check)

TC nhóm Visual (BUG-04)

#StepsExpected
TC-09Mở chart hôm nay (2026-05-11), xem cột tháng/ngày hiện tạiCột có visual khác biệt (dashed/opacity 50%); tooltip thêm dòng "Data đến 11/05/2026 — chưa kết thúc"
TC-10Filter range bao gồm ngày tương lai (vd 01/05 → 20/05) khi hôm nay là 11/05Ngày tương lai (12/05–20/05) KHÔNG render — chart kết thúc tại ngày hôm nay (đã chốt Q4 = Option A)

TC nhóm Performance (BUG-05)

#StepsExpected
TC-11Mở report lần đầu (cold cache), đo thời gian click → initial render xong (overview + 3 chart, KHÔNG có bảng vì lazy-load)P95 ≤ 5 giây
TC-11bSau initial load, click "Hiển thị chi tiết" → đo thời gian đến khi bảng visibleP95 ≤ 2 giây thêm
TC-11cVerify bảng VẪN LAZY-LOAD — không bị bug regression eager-fetchDevTools Network: handler 2 KHÔNG fire cho đến khi click
TC-12Đo thời gian visible riêng từng phần (chart, bảng) sau clickP95 ≤ 2 giây mỗi phần
TC-13Verify cả 2 action reportSalesRevenue + reportSalesRevenueByBranch được FE fetch parallel (DevTools Network)Request gần như cùng start time
TC-14Verify internal calls trong mỗi handler chạy parallel (BE log/tracing)4–8 calls của handler 1, 2 calls của handler 2 — không waterfall sequential
TC-15EXPLAIN ANALYZE 2 query chính BEFORE/AFTER (search_dashboard_sales_revenue_chart + search_dashboard_sales_revenue_by_branchs), range 18 thángOutput đính kèm; nếu có index mới: chuyển Seq Scan → Index Scan
TC-16Thay đổi filter chi nhánh, đo lại perfKhông regression so với TC-11
TC-17Mở report với compare period ON, đo lại perfVẫn ≤ 5s (vì calls đã parallel) — không phải 2× thời gian khi compare

TC nhóm Telesale rate (BUG-06) — ⚠️ PENDING DISCOVERY

⚠️ Các TC sau chỉ áp dụng SAU KHI BUG-06 discovery xong và confirm là bug + chốt fix strategy. Trước đó, QA chưa thực hiện được các TC này.

#StepsExpectedĐiều kiện áp dụng
TC-18(Sau khi fix BUG-06) Mở report → xem KPI "Tỉ lệ đóng góp thực thu Telesale"Giá trị thay đổi so với trước fix (theo strategy đã chốt sau discovery); ratio realistic theo nghiệp vụBUG-06 confirmed + fixed
TC-19(Sau khi fix BUG-06) Cross-check: công thức mới = công thức từ discovery outputMatch 100% (sai số rounding < 0.01%)BUG-06 confirmed + fixed
TC-20Regression check (luôn chạy, ngay cả khi BUG-06 chưa fix): KPI "Tỉ lệ khách phát sinh doanh số từ Telesale" (Metric 2 telesale_conversion_rate)Giá trị KHÔNG ĐỔI trước/sau bất kỳ change nàoLuôn

Discovery deliverable cần verify trước khi assign TC-18/19:

  • [ ] BE/DBA chạy reconciliation SQL trong BUG-06 → đính kèm output
  • [ ] PO/BA confirm: H1 (bug thật) hay H2 (không bug) hay H3 (bug ở mẫu)
  • [ ] Nếu H1/H3 → spec lại fix strategy + thêm TC mới phù hợp

6. Confirmed Decisions

#Câu hỏiQuyết định cuối
Q1BUG-01: Mode so sánh khi user không set compare periodCHỐT — Khi không set compare: FE chỉ hiện period hiện tại (không có vế "vs"). Khi có set: hiện đầy đủ "X vs Y". Δ% và Δ abs hiển thị khi không có compare.
Q2BUG-03: Chart hiển thị AOV hay tổng?CHỐT — AOV (trung bình/đơn). Implement bằng ADD 4 cột mới trong SQL function, KHÔNG xóa cột SUM cũ.
Q3BUG-04: Cột ĐANG DIỄN RA hiển thị thế nào?CHỐT — Option A — Dashed bar + opacity 50% + tooltip note "Data đến dd/mm/yyyy — chưa kết thúc"
Q4BUG-04: Cột TƯƠNG LAI hiển thị thế nào?CHỐT — Option A — Ẩn hẳn (chart kết thúc tại ngày hôm nay)
Q5BUG-05: Thứ tự fix performanceCHỐT — Parallel hóa trước (Option 1) → đo EXPLAIN ANALYZE → quyết định có add index (Option 2) sau. KHÔNG add index mù.
Q6Audit avg_payment_amount_per_visit + avg_download_app_per_visitCHỐT — Yes. Sau khi fix ticket này xong, mở ticket P2 riêng để audit 2 field nghi vấn AOV tương tự.
Q7BUG-06 (telesale_amount_rate): fix strategy?⚠️ PENDING DISCOVERY — chưa chốt fix vì chưa verify relationship invoicefund. BE/DBA cần chạy reconciliation SQL trước. Em đã rollback claim "Option B" trước đó vì giả định sai về schema fund. Xem BUG-06 mới (status NEEDS DISCOVERY).
Q8Audit data quality "telesale có gán đúng/sai vào đơn không"?CHỐT — Yes, ticket riêng cho Operations team. Chạy luôn, độc lập với BUG-06 discovery.

🔒 Các quyết định trên đã được PO/BA chốt — KHÔNG cần re-confirm trước khi dev bắt đầu. Mọi thay đổi sau ngày 2026-05-11 phải qua PO/BA approval.


7. Definition of Done

Phần Ready-for-dev (5 bug + lazy-load split):

  • [ ] BUG-01, 02, 03, 05 — code merged (1 PR backend) — Ready
  • [ ] BUG-04 — code merged (1 PR frontend) — sau khi Designer sign-off Section 4B
  • [ ] PO đã confirm Q1-Q5, Q8 và dev implement đúng quyết định
  • [ ] Designer đã review + sign-off Section 4B (B1-B6) trước khi FE bắt đầu
  • [ ] QA pass 19 test cases ready (5 logic + 3 AOV + 2 visual + 9 performance + 1 regression Metric 2 telesale)
  • [ ] Screenshot trước/sau cho 3 chart (Bar + 2 Line):
    • Bar: AOV thực sự realistic (500K–50 Triệu/đơn)
    • 2 Line: tooltip không còn "X vs X", không còn "0% (+X)"
  • [ ] Regression: 3 Pie chart "Tỉ lệ thực thu *" + KPI telesale_conversion_rate giá trị KHÔNG ĐỔI trước/sau
  • [ ] Cross-check: AOV × COUNT(orders) ≈ Tổng doanh thu cùng tháng (sai số < 1%)
  • [ ] Đo perf trước/sau cho BUG-05:
    • Initial load (overview + 3 chart) P95 < 5s
    • Mỗi chart < 2s
    • Expanded table (sau click) < 2s thêm
    • Bảng vẫn lazy-load (không bị regression eager)
  • [ ] EXPLAIN ANALYZE output trước/sau cho 2 DB function
  • [ ] Deploy production và sếp đã review lại biểu đồ

Phần Discovery (BUG-06):

  • [ ] BE/DBA chạy reconciliation SQL invoicefund (xem BUG-06)
  • [ ] Output đính kèm vào ticket
  • [ ] PO/BA review → quyết định BUG-06: H1 / H2 / H3
  • [ ] Nếu H1/H3 → spec lại fix strategy → assign TC-18/19 cho QA
  • [ ] Nếu H2 → đóng BUG-06, ghi rõ lý do "không phải bug"

Audit follow-up (Q6 + Q8):

  • [ ] Tạo ticket riêng cho avg_*_per_visit audit (tech)
  • [ ] Tạo ticket riêng cho data quality order_commission (ops — chạy SQL trong BUG-06)

8. Reference

  • Reported by: PO/BA — sontho22@gmail.com
  • Reported date: 2026-05-11
  • Screenshots:
    • Monthly view: image-cache/d565d5f7-7ade-4b09-b3be-846d5a403de3/1.png
    • Daily view: image-cache/d565d5f7-7ade-4b09-b3be-846d5a403de3/2.png
  • Report URL: /r/reports/actual_revenue_report_group (Báo cáo Doanh thu thực tế → tab "Báo cáo")
  • Hasura action: reportSalesRevenue
  • GraphQL fields affected: avg_order_revenue_by_month (chính), payment_amount_by_month, customer_visits_by_month (cùng pattern BUG-01, 02)
  • Related codebase paths: xem Section 4
  • Scope ảnh hưởng: 3 chart trong cùng section "Biến động" (Bar AOV + 2 Line)

Suggested assignee

BugNgười làmVùng code
BUG-01BE devreport_sales_revenue.gogenerateLabels() (~593)
BUG-02BE devreport_sales_revenue.goSetValueObject() (~583) + tiny FE change render
BUG-03BE dev + DBASQL migration (add 4 cột AOV) + CaculateData() mapping (~420)
BUG-04FE devBarChartActualRevenue.tsx chart config + tooltip
BUG-05BE dev + FE devBE: GetOrderAndInvoiceBase() (~179) + reportSalesRevenueByBranch() (~80) parallel hóa. FE: verify 2 action reportSalesRevenue + reportSalesRevenueByBranch được gọi parallel. EXPLAIN ANALYZE → có thể add index
BUG-06BE + DBA (DISCOVERY first)Phase 1 (Discovery): Chạy reconciliation SQL invoicefund (xem BUG-06). Phase 2 (Fix): Spec lại sau khi có output.

→ Đề xuất 1 BE dev chính + 1 FE dev support + 1 Designer (parallel với BE dev). BUG-01, 02, 03, 05 cùng file report_sales_revenue.go → giao 1 PR backend gộp sẽ tối ưu (test integration cùng nhau).

Designer workload: Review + chốt Section 4B (B1-B6) — ước tính 1-2 ngày song song với BE dev.

Migration & deploy order

Song song:
  ├── BE dev: PR Backend (gộp 4 bug BE) — bắt đầu ngay khi spec ready-for-dev
  └── Designer: review + chốt Section 4B (B1-B6) — bắt đầu ngay khi spec ready-for-dev

Tuần tự sau đó:
  └── FE dev: PR Frontend — CHỜ Designer sign-off Section 4B + BE merge xong staging

       └── Deploy: BE trước, FE sau
  1. PR Backend (gộp 4 bug BE): SQL migration add cột → Go struct update → handler logic (CaculateData mapping) → SetValueObject guard → generateLabels guard → parallel calls (handler 1) → parallel calls (handler 2 bảng chi tiết)
  2. Designer: Hoàn thiện B1-B6 trong Section 4B → đính Figma + confirm copy text
  3. PR Frontend: Tooltip render theo B2/B3/B6, state visual theo B1, empty state theo B4, loading skeleton theo B5, verify 2 action gọi parallel
  4. Deploy thứ tự: Backend trước, FE sau (FE backward-compatible với BE cũ thì OK gộp 1 lần)

Audit follow-up (mở ticket riêng nếu cần)

  • avg_payment_amount_per_visit (Efficiency metric) — có thể cùng pattern bug "SUM gán nhầm cho AVG" (Q6)
  • avg_download_app_per_visit (Efficiency metric) — cùng nghi vấn (Q6)
  • Operations ticket — Data quality order_commission: % đơn có gán telesale commission, có over-claim không, có thiếu không (Q8 — chạy SQL audit ở BUG-06)
  • Field naming *_by_month nhưng chứa data by_day/by_year (tùy filter) — không phải bug nhưng confusing
  • i18n cho hardcoded title (tech debt)