Appearance
Bug Fix: Báo cáo Doanh thu thực tế (Actual Revenue Report)
v1.2 — 11/05/2026 — QA review iteration
| Thay đổi | Section | Ả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 invoice ↔ fund trước khi chốt fix) | Section 2 | BE, DBA |
| Rollback Q7 từ "CHỐT Option B" → "PENDING DISCOVERY" — đính kèm 3 giả thuyết H1/H2/H3 + reconciliation SQL | Section 6 | BE, DBA, PO |
| Split BUG-05 AC theo lazy-load: initial load (overview+3 chart) ≠ expanded table (sau click) | BUG-05 | BE, QA |
| Thêm TC-11b, TC-11c — lazy-load verify | Section 5 | QA |
| Cập nhật TC-18/19/20 — đánh dấu PENDING DISCOVERY | Section 5 | QA |
| Status banner per-bug — table rõ ràng cho từng bug (Ready / Blocked / Discovery) | Banner top | All |
Fix UI Spec typo — DD/YYYY → MM/YYYY cho monthly compare | Section 4B | Designer, FE |
v1.1 — 11/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Thêm BUG-06 — telesale_amount_rate mismatch dimension (initial claim — đã downgrade ở v1.2) | Section 2 | BE, DBA, QA |
| Thêm Q7 + Q8 — chốt fix Option B (rollback ở v1.2) + audit data quality telesale | Section 6 | All |
| Thêm TC-18/19/20 — telesale rate tests | Section 5 | QA |
| Sửa anchor links (QA review P1) — bỏ emoji/em-dash khỏi headings | Toàn file | All |
| Sửa TC-10 — single expected khớp Q4 (ẩn hẳn ngày tương lai) | Section 5 | QA, FE |
| Đổi status banner — BE READY, FE BLOCKED đến Designer sign-off | Banner top | FE, Designer |
| Đổi complexity M → S + ghi rõ "Quick Fix compact package" | index.md | All |
🔒 STATUS theo từng bug:
Bug Trạng thái Người phụ trách Có thể bắt đầu? BUG-01 (compare period) ✅ Ready BE Ngay BUG-02 (Δ% / Δ abs) ✅ Ready BE Ngay BUG-03 (AOV add cột) ✅ Ready BE + DBA Ngay BUG-04 (visual cột) ⏳ Blocked FE + Designer Chờ Designer sign-off Section 4B BUG-05 (performance) ✅ Ready BE + FE Ngay BUG-06 (telesale rate) 🟠 NEEDS DISCOVERY BE + DBA Chờ reconciliation SQL invoice↔fund🎨 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
- 1. Tóm tắt
- 2. Danh sách lỗi (5 BUG)
- 3. Scope & Out of Scope
- 4. Affected Module / Component
- 4B. UI Spec cho Designer
- 5. Test Cases (QA) — QA bắt đầu từ đây
- 6. Confirmed Decisions
- 7. Definition of Done
- 8. Reference & Assignee
Quick Reference cho QA
| Thông tin | Giá trị |
|---|---|
| Số test cases | 22 TCs (chi tiết Section 5) |
| Phân nhóm TCs | 5 Logic compare · 3 AOV correctness · 2 Visual · 9 Performance (gồm 3 lazy-load tests) · 3 Telesale (1 regression + 2 pending discovery) |
| Acceptance Criteria | Mỗi BUG có AC riêng — xem Section 2 |
| Definition of Done | xem Section 7 |
| Severity | High — báo cáo dùng trong họp với cấp quản lý |
| Components ảnh hưởng | 3 chart + 1 bảng trong Báo cáo Doanh thu thực tế |
| URL test | /r/reports/actual_revenue_report_group |
| Mong đợi performance | P95 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_monthkhông đổi - ✅ Pie "Tỉ lệ thực thu Dịch vụ" —
revenue_service_groupkhông đổi - ✅ Pie "Tỉ lệ thực thu Mỹ phẩm" —
revenue_cosmetic_groupkhông đổi - ✅ Pie "Tỉ lệ thực thu Thẻ" —
revenue_prepaid_groupkhô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ần | Bugs ảnh hưởng | Trạng thái |
|---|---|---|
| Bar "Biến động trung bình theo đơn hàng" | BUG-01, 02, 03, 04, 05 | Ready (trừ BUG-04 chờ Designer) |
| Line "Biến động thực thu" | BUG-01, 02, 05 | Ready |
| Line "Biến động khách hàng" | BUG-01, 02, 05 | Ready |
| Bảng "Chi tiết hoạt động kinh doanh" | BUG-05 (handler riêng reportSalesRevenueByBranch — lazy-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):
| Layer | Path |
|---|---|
| FE — 3 chart components | diva-admin/src/modules/report/components/actual-revenue-report/Charts/ (BarChartActualRevenue.tsx, LineChartActualRevenue.tsx) |
| FE — section wrapper | ActualRevenueReportVolatility.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.go — reportSalesRevenueByBranch() |
| DB | functions search_dashboard_sales_revenue_chart() + search_dashboard_sales_revenue_by_branchs() — migrations 1752116891000 + 1777431629000 |
| GraphQL | Hasura 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
SUMtrong 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):
| # | Chart | File 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ên | BarChartActualRevenue.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.CompareTimeMong đợ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(): khiCompareStartDate == nil→ setCompareTime = ""(rỗng) thay vì gán= Time - [ ] FE tooltip render: nếu
CompareTime == ""→ chỉ hiệnTime, không hiệnvs ... - [ ] Á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ùngfloat32thay vì*float32), cần đổi 2 field này sang pointer + update FE checknullthay 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()để khicompare = 0thì cảPercentvàValueChangeđềunull(hoặc field nào BE convention) - [ ] FE tooltip render
—khi nhậnnull - [ ] Khi
compare > 0: cả 2 field phải cùng dấu (không thểΔ% = 0màΔ 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ệuBằ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_amountLỗ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:
| Chart | Field DB dùng | Cần |
|---|---|---|
| Line "Biến động thực thu" | payment_amount | TỔ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_amount | AVG ← 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 đổiAcceptance 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:
| Option | Cách xử lý | Ưu/nhược |
|---|---|---|
| A | Label cột bằng badge "đang diễn ra" + dashed bar | Trực quan, giữ data |
| B | Exclude cột tháng hiện tại khỏi chart | Sạch nhưng mất context |
| C | Normalize 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_Minhtimezone
🔴 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: truecho đến khi user click "Hiển thị chi tiết" (xemTableDetailActualRevenue.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–24sHandler 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–700msDB 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:
- Handler 1 sequential calls — 4–8 query Hasura mỗi cái 1–3s → tổng 4–24s (vấn đề chính)
- Handler 2 sequential calls — 2 query → 0.3–0.7s (vấn đề phụ, vẫn cải thiện được)
- FE có thể sequential 2 handler — cần verify (Promise.all vs await tuần tự)
- Thiếu index? — chưa có composite index trên
(paid_at, status, parent_id, branch_id). Cần EXPLAIN ANALYZE để confirm planner - 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áp | Layer | Effort | Impact |
|---|---|---|---|---|
| 1 | Parallel 4–8 Hasura calls trong handler reportSalesRevenue bằng errgroup | BE | S (~1h) | Giảm 50-70% thời gian (4 calls → ~1 call worth of time) |
| 2 | Parallel 2 calls trong handler reportSalesRevenueByBranch (function call + product group) | BE | S (~30p) | Giảm 30-40% thời gian bảng |
| 3 | Verify FE gọi 2 handler parallel (URQL parallel queries / Promise.all), không sequential | FE | S (~30p) | Tránh waterfall — toàn page load song song |
| 4 | EXPLAIN ANALYZE query thực tế → decide add composite index invoice(paid_at, status, parent_id, branch_id) | BE+DBA | S (~2h) | Tùy plan analyzer — có thể giảm 30-60%/query hoặc không cải thiện |
| 5 | Cache kết quả các tháng đã hoàn thành (immutable) ở Redis/app_setting với TTL 24h | BE | M (~1 ngày) | Lần load thứ 2 trở đi gần như instant |
| 6 | Materialized view cho aggregation, refresh nightly | DBA | L (~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 quaerrgroup(verify trong code) - [ ] 2 calls trong
reportSalesRevenueByBranch()(handler 2) chạy parallel quaerrgroup(verify trong code) - [ ] FE verify: 2 action
reportSalesRevenue+reportSalesRevenueByBranchđược fetch parallel (không await tuần tự) - [ ] Chạy
EXPLAIN ANALYZEcho 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
invoice↔fundtrướ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 sinhDescription: "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ại | Source SQL (verified) | Đơn vị thực tế? |
|---|---|---|---|
| Tử số | telesale_amount | SUM(invoice.amount × commission_ratio) — file 1761037948000_*/up.sql:43 | Cần verify (gross invoice, nhưng có thể đại diện cash nếu invoice = paid) |
| Mẫu số | TotalPaymentAmount | SUM(f.amount) FROM fund f WHERE f.type='incoming' — file 1761037948000_*/up.sql:183 | Cash 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:
Có 3 giả thuyết cần verify, mỗi cái dẫn tới fix khác nhau:
| # | Giả thuyết | Nếu đúng → Fix |
|---|---|---|
| H1 | Tử 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) |
| H2 | invoice.amount thực ra LUÔN BẰNG cash đã thu (invoice chỉ tạo khi pay) → không có mismatch | KHÔNG phải bug — cần kiểm tra nghiệp vụ Diva về flow invoice/payment |
| H3 | fund f không cover hết payment của invoice (vd: missing some payment_method) → mẫu số bị thiếu | Bug 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 invoice và fund.
Discovery Questions (BE/DBA cần verify trước khi fix):
| # | Câu hỏi | Cách verify |
|---|---|---|
| D1 | Schema 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 |
| D2 | Có 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 |
| D3 | Có 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 |
| D4 | Trong 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 |
| D5 | Nghiệ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 |
| D6 | Field 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ó invoiceorders_with_debt: số đơn invoice > cashavg_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 đơnCả 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_rateKHÔ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)
| Layer | File | Mô tả |
|---|---|---|
| Page | diva-admin/src/modules/report/pages/ActualRevenueReport.tsx | Page wrapper, route /r/reports/actual_revenue_report_group |
| Tab | ActualRevenueReportDetail | Tab "Báo cáo" (ACTUAL_REVENUE_REPORT_DETAIL) |
| Section | ActualRevenueReportVolatility.tsx:187 | Chứa 3 chart "Biến động *" |
| Chart Bar | Charts/BarChartActualRevenue.tsx:125 | "Biến động trung bình theo đơn hàng" — BUG-03, BUG-04 hiển thị |
| Chart Line × 2 | Charts/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 query | diva-admin/src/modules/report/graphql/report_actual_revenue.graphql | Fields: payment_amount_by_month, customer_visits_by_month, avg_order_revenue_by_month |
Bar Chart — 3 series color (verify):
service(Dịch vụ) —#3479E9cosmetic(Mỹ phẩm) —#91C3FDprepaid(Nạp thẻ) —#BEDBFE
Backend (Go + Hasura + PostgreSQL)
| Layer | Path | Mô tả |
|---|---|---|
| Hasura action | reportSalesRevenue | Defined trong diva-backend/services/controller/ metadata |
| Go handler | report_sales_revenue.go:162 — reportSalesRevenue() | Entry point handle action |
| Query batch | report_sales_revenue.go:179–354 — GetOrderAndInvoiceBase() | BUG-05 — 4–8 calls sequential, cần parallel hóa |
| Compute Δ | report_sales_revenue.go:583–588 — SetValueObject() | BUG-02 — thiếu guard khi compare = 0 |
| Build labels | report_sales_revenue.go:593–601 — generateLabels() | BUG-01 — fallback CompareTime = Time khi user không set compare |
| Map AOV | report_sales_revenue.go:420–428 — CaculateData() | BUG-03 — gán PaymentAmount (SUM) vào field AvgOrderRevenueByMonth |
| DB function (chart) | public.search_dashboard_sales_revenue_chart() migration 1752116891000 + 1777431629000 | BUG-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ính | time_group_full dòng 339–353 | Aggregation, GROUP BY time period (by_day / by_month / by_year) |
| Handler 2 (bảng) | report_sales_revenue_by_branch.go:80–165 — reportSalesRevenueByBranch() | 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_revenue | 6 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:318 | BUG-06 — cần ADD CTE telesale_payment_cte tính thực thu của đơn telesale |
| Map TelesaleAmountRate | report_sales_revenue.go:318 | BUG-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:
invoiceJOINorder - Filter (CTE
invoice_filtered):parent_id IS NULL(chỉ parent invoices, không lấy child)status = 'invoice_completed' OR status IS NULLpaid_at BETWEEN _from AND _topayment_method_id NOT IN ('wallet', 'wallet_promotion')branch_idfilter tùy chọn
Index hiện tại (verify trong migrations)
idx_invoice_parent_id— single column trênparent_ididx_completed_at— single column trêncompleted_atidx_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:
| # | State | Khi nào xuất hiện | Visual đề xuất | Designer chốt |
|---|---|---|---|---|
| 1 | Normal | Tháng/ngày đã hoàn thành, có data | Solid bar, 100% opacity, màu hệ thống Diva | ✅ giữ nguyên hiện tại |
| 2 | Ongoing | Ngày/tháng đang diễn ra (vd 11/05 hôm nay) — partial data | Dashed pattern (4px dash + 2px gap), opacity 50%, viền nét đứt | ??? Confirm hoặc đề xuất khác |
| 3 | Future | Ngày sau hôm nay (đã chốt Q4: ẨN HẲN) | Không render cột | ✅ ẩn — không cần design |
| 4 | No data | Branch/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?) |
| 5 | Hover | Mouseover bất kỳ cột | Sáng lên + tooltip xuất hiện | ??? Pattern hover Diva đang dùng? |
| 6 | Selected | Click 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ợp | Option em đề xuất | Designer chốt |
|---|---|---|---|
| 1 | Header — CÓ compare period | Monthly view: "10/2025 vs 09/2025" (MM/YYYY); Daily view: "11/05/2026 vs 10/05/2026" (DD/MM/YYYY) | ??? Confirm format |
| 2 | Header — KHÔNG có compare period | Chỉ "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ỷ" | ??? |
| 8 | Note "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? |
| 9 | Note "không có giao dịch" | "Chưa có giao dịch trong kỳ này" | ??? |
| 10 | Icon trending Δ% dương | ↗ + màu xanh #22C55E | ??? Hệ thống có icon riêng? |
| 11 | Icon trending Δ% âm | ↘ + màu đỏ #EF4444 | ??? |
| 12 | Icon 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ình | Element | Tooltip text | Điều kiện hiện |
|---|---|---|---|
| Bar AOV | Icon ℹ️ 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ĐKD | Cộ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ĐKD | Cột "Tự đến" header | "Khách hàng walk-in, không qua telesale." | Hover header |
| Bảng Chi tiết HĐKD | Cộ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):
| Option | Cách hiển thị | Ưu | Nhược |
|---|---|---|---|
| A | Giữ row, thay 0 bằng "—" (em-dash) ở tất cả cell | Vẫn cho biết branch tồn tại trong filter | Vẫn chiếm 1 row table |
| B | Ẩn hẳn row branch không có data | Bảng sạch, focus data thật | User không biết branch đó tồn tại |
| C | Giữ row + badge "Chưa có giao dịch" ở cột "Chi nhánh" | Rõ ràng nhất | Tố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:
| Layer | Hiện tại | Đề xuất | Designer 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:
- Layout này OK chưa? Có muốn đổi thứ tự (vd Tổng lên đầu)?
- Khoảng cách padding/margin?
- Width tooltip cố định hay auto?
- Position tooltip (top/bottom của cột)?
- 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
| # | Steps | Expected |
|---|---|---|
| TC-01 | Mở report, KHÔNG set compare period, hover bất kỳ điểm nào trên Bar chart AOV | Header: chỉ hiện "10/2025", KHÔNG có vs ...; Δ% và Δ abs hiển thị "—" |
| TC-02 | Tương tự TC-01 nhưng trên Line "Biến động thực thu" | Same as TC-01 — logic chung qua SetValueObject() |
| TC-03 | Tương tự TC-01 nhưng trên Line "Biến động khách hàng" | Same as TC-01 |
| TC-04 | Set 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-05 | Set 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)
| # | Steps | Expected |
|---|---|---|
| TC-06 | Sau fix, hover Bar chart tháng 10/2025 (Dịch vụ) | AOV trong khoảng 500K – 50 Triệu/đơn (realistic spa) |
| TC-07 | Cross-check AOV × COUNT(orders) ≈ Tổng doanh thu | Sai số do rounding < 1% so với chart "Biến động thực thu" cùng tháng |
| TC-08 | Verify 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)
| # | Steps | Expected |
|---|---|---|
| TC-09 | Mở chart hôm nay (2026-05-11), xem cột tháng/ngày hiện tại | Cộ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-10 | Filter range bao gồm ngày tương lai (vd 01/05 → 20/05) khi hôm nay là 11/05 | Ngà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)
| # | Steps | Expected |
|---|---|---|
| TC-11 | Mở 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-11b | Sau initial load, click "Hiển thị chi tiết" → đo thời gian đến khi bảng visible | P95 ≤ 2 giây thêm |
| TC-11c | Verify bảng VẪN LAZY-LOAD — không bị bug regression eager-fetch | DevTools 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 click | P95 ≤ 2 giây mỗi phần |
| TC-13 | Verify cả 2 action reportSalesRevenue + reportSalesRevenueByBranch được FE fetch parallel (DevTools Network) | Request gần như cùng start time |
| TC-14 | Verify 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-15 | EXPLAIN ANALYZE 2 query chính BEFORE/AFTER (search_dashboard_sales_revenue_chart + search_dashboard_sales_revenue_by_branchs), range 18 tháng | Output đính kèm; nếu có index mới: chuyển Seq Scan → Index Scan |
| TC-16 | Thay đổi filter chi nhánh, đo lại perf | Không regression so với TC-11 |
| TC-17 | Mở report với compare period ON, đo lại perf | Vẫ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.
| # | Steps | Expected | Đ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 output | Match 100% (sai số rounding < 0.01%) | BUG-06 confirmed + fixed |
| TC-20 | Regression 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ào | Luô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ỏi | Quyết định cuối |
|---|---|---|
| Q1 | BUG-01: Mode so sánh khi user không set compare period | ✅ CHỐ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. |
| Q2 | BUG-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ũ. |
| Q3 | BUG-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" |
| Q4 | BUG-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) |
| Q5 | BUG-05: Thứ tự fix performance | ✅ CHỐ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ù. |
| Q6 | Audit avg_payment_amount_per_visit + avg_download_app_per_visit | ✅ CHỐT — Yes. Sau khi fix ticket này xong, mở ticket P2 riêng để audit 2 field nghi vấn AOV tương tự. |
| Q7 | BUG-06 (telesale_amount_rate): fix strategy? | ⚠️ PENDING DISCOVERY — chưa chốt fix vì chưa verify relationship invoice ↔ fund. 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). |
| Q8 | Audit 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_rategiá trị KHÔNG ĐỔI trước/sau - [ ] Cross-check:
AOV × COUNT(orders) ≈ Tổng doanh thucù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
invoice↔fund(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_visitaudit (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
- Monthly view:
- 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
| Bug | Người làm | Vùng code |
|---|---|---|
| BUG-01 | BE dev | report_sales_revenue.go — generateLabels() (~593) |
| BUG-02 | BE dev | report_sales_revenue.go — SetValueObject() (~583) + tiny FE change render — |
| BUG-03 | BE dev + DBA | SQL migration (add 4 cột AOV) + CaculateData() mapping (~420) |
| BUG-04 | FE dev | BarChartActualRevenue.tsx chart config + tooltip |
| BUG-05 | BE dev + FE dev | BE: GetOrderAndInvoiceBase() (~179) + reportSalesRevenueByBranch() (~80) parallel hóa. FE: verify 2 action reportSalesRevenue + reportSalesRevenueByBranch được gọi parallel. EXPLAIN ANALYZE → có thể add index |
| BUG-06 | BE + DBA (DISCOVERY first) | Phase 1 (Discovery): Chạy reconciliation SQL invoice ↔ fund (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- 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)
- Designer: Hoàn thiện B1-B6 trong Section 4B → đính Figma + confirm copy text
- 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
- 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_monthnhưng chứa databy_day/by_year(tùy filter) — không phải bug nhưng confusing - i18n cho hardcoded title (tech debt)