Appearance
Báo cáo Đơn hàng Voucher/Quà tặng
Version: 1.0 Date: 19/03/2026 Author: PO/BA Type: Enhancement Complexity: M Module: report (FE), ecommerce DB (BE)
Changelog
| Version | Date | Author | Thay đổi |
|---|---|---|---|
| 1.0 | 19/03/2026 | PO/BA | Initial — chuyển từ design doc Mode A |
Hướng dẫn đọc (RACI)
| Audience | Đọc sections | Trách nhiệm |
|---|---|---|
| PO/BA | Z, A, B | Approve requirements, UX flows |
| Tech Lead | Z, A, C | Approve architecture, review dev spec |
| FE Dev | B, C (C1-C6) | Implement UI |
| BE Dev | C (C1-C12) | Implement backend (indexes, migration) |
| QA | A (A5), D (D4 = traceability) | Write + execute test cases |
Executive Summary (TL;DR)
Thêm tab "Đơn hàng Voucher/Quà tặng" trong Revenue Report Group để PO/Admin kiểm soát tiêu thụ voucher và quà tặng theo góc nhìn đơn hàng. Mỗi lượt dùng voucher/gift = 1 dòng, có summary bar 4 metrics, filter cascading theo nguồn quà → chiến dịch, hỗ trợ export Excel. Yêu cầu 4 database indexes trước khi deploy (dataset hàng triệu dòng).
Milestones
| Milestone | Target | Owner | Điều kiện |
|---|---|---|---|
| 1. DB Migration (indexes) | T+2 ngày | BE Dev | — |
| 2. GraphQL queries + codegen | T+3 ngày | BE Dev | Sau indexes deploy |
| 3. FE Components (filter + table + summary) | T+7 ngày | FE Dev | Sau codegen |
| 4. QA Testing | T+10 ngày | QA | Sau FE deploy staging |
| 5. Go-Live | T+12 ngày | Tech Lead | QA pass |
Trạng thái Sign-off
| Domain | Người | Status |
|---|---|---|
| Business | PO | Approved |
| Tech | Tech Lead | Approved |
| QA | QA Lead | Approved |
Pending Decisions
Không có Pending Decision — tất cả đã locked trong Decision Log (qua 3 vòng review đa góc nhìn).
Backlog Phase 2 (Out-of-scope)
| # | Tính năng | Lý do defer |
|---|---|---|
| 1 | Top 10 khách hàng sử dụng voucher nhiều nhất | Cần thêm aggregate query phức tạp, ưu tiên MVP trước |
| 2 | Tỷ lệ đơn dùng voucher / tổng đơn (benchmark) | Cần cross-reference với tổng đơn — query thêm |
| 3 | Filter trạng thái voucher (redeemed/restored/expired) | Phức tạp hoá filter, chưa urgent |
| 4 | Multi-column sort (sort theo số tiền giảm) | Các tab hiện có cũng chỉ sort 1 cột |
| 5 | Materialized view cho summary bar | Bắt đầu với indexes, chỉ triển khai nếu aggregate > 3s |
| 6 | Campaign ROI metrics | Thuộc Campaign Report, không phải tab này |
| 7 | Fix branch scoping ở Hasura permission level (RSK-005) | Lỗ hổng hệ thống — cần initiative riêng |
Z) Decision Log
| ID | Category | Quyết định | Lý do | Ngày | Status |
|---|---|---|---|---|---|
| DEC-001 | Business | Thêm tab mới thay vì fix tab hiện có | Giữ nguyên code cũ, thêm tab clean bên cạnh — tránh rủi ro regression | 19/03/2026 | Locked |
| DEC-002 | Business | Chỉ hiển thị đơn có voucher/gift (không hiển thị tất cả đơn) | Ops cần focus — phần lớn đơn không có voucher sẽ gây nhiễu | 19/03/2026 | Locked |
| DEC-003 | Technical | Query ở order_item level, pagination trên order_item | Mỗi lượt dùng voucher = 1 đơn vị kiểm soát. Pagination trên order sẽ sai khi 1 đơn có nhiều items | 19/03/2026 | Locked |
| DEC-004 | UX | 1 tab duy nhất + filter liên động (không tách tab voucher/gift) | Dữ liệu có thể ít, 1 tab + filter đủ linh hoạt. Giữ consistent với pattern Revenue Report | 19/03/2026 | Locked |
| DEC-005 | Business | Phân biệt với Campaign Report: tab này = góc đơn hàng, Campaign Report = góc chiến dịch | Tránh nhầm lẫn cho Admin khi 2 report có dữ liệu chồng chéo | 19/03/2026 | Locked |
| DEC-006 | Technical | Branch scoping ở frontend WHERE builder, không phải Hasura permission | Hasura permission trên order, order_item, user_vouchers đều có filter: {} — không auto-filter branch | 19/03/2026 | Locked |
| DEC-007 | Technical | Dùng order.addresses thay vì order.customer cho thông tin khách hàng | order.customer là remote relationship — gây thêm HTTP call cho mỗi order. addresses cùng DB | 19/03/2026 | Locked |
| DEC-008 | Technical | Filter order_kind IN (service, cosmetic, prepaid) — loại đơn chuyển kho/vật tư | Đơn order_transfer và internal_material không bao giờ có voucher — giảm hàng trăm nghìn rows scan | 19/03/2026 | Locked |
| DEC-009 | UX | Mặc định ẩn đơn hủy, cho phép toggle qua filter "Trạng thái đơn" | Kiểm soát cần cả gross usage (bao gồm đơn hủy) và net usage. Backend có RestoreVouchersOnOrderCancel | 19/03/2026 | Locked |
| DEC-010 | UX | Visual grouping: dim repeated order-level cells khi 1 đơn có nhiều items | Tab này là tab đầu tiên render item-level — cần visual cue để phân biệt | 19/03/2026 | Locked |
| DEC-011 | UX | Gộp cột Tên voucher + Mã voucher thành 1 cell 2 dòng + thêm cột STT (13 cột tổng) | Giảm mật độ bảng. Cột STT consistent với các tab khác (đều có cột index) | 19/03/2026 | Locked |
| DEC-012 | Technical | 4 database indexes BLOCKER — phải deploy trước feature | Không có index → seq scan 5M rows → timeout > 30s trên production | 19/03/2026 | Locked |
| DEC-013 | UX | Tạo VoucherGiftOrderReportFilter.tsx riêng (không dùng chung RevenueReportFilter) | Filter đặc thù (nguồn quà, campaign cascading, loại giảm) hoàn toàn khác filter các tab khác | 19/03/2026 | Locked |
| DEC-014 | Technical | whereOrderAgg phải sync item-level filters (gifted_from, gift_type) | Summary bar phải phản ánh đúng filter đang áp dụng — nếu không, user thấy số không khớp với bảng | 19/03/2026 | Locked |
| DEC-015 | UX | Filter dùng nút "Áp dụng" (không auto-reload) | Consistent với RevenueReportFilter pattern. Tránh multi-request khi đổi cascading filter | 19/03/2026 | Locked |
| DEC-016 | UX | Layout summary 4 cards + export row riêng — khác pattern 1 BoxInfo của tab cũ | Tab này cần 4 metrics (lượt dùng, giá trị giảm, thực thu, tỷ lệ). Khác biệt có chủ đích | 19/03/2026 | Locked |
A) PRD
A1) Blueprint
| Field | Value |
|---|---|
| Feature | Báo cáo Đơn hàng Voucher/Quà tặng |
| Type | Enhancement |
| Platform | Web Admin (diva-admin) |
| Module ảnh hưởng | report (FE), ecommerce DB indexes (BE) |
A2) Context
As-Is
- Revenue Report Group có 5 tab: Tổng đơn hàng, Đơn hàng dịch vụ, Giao dịch dịch vụ, Đơn hàng sản phẩm, Đơn hàng thẻ trả trước
- Không có tab nào tracking voucher/gift consumption
- Discount tính gộp chung (promo + membership), không phân biệt voucher
- Campaign Report hiện có nhưng góc nhìn từ chiến dịch (phát bao nhiêu, claim rate), không từ đơn hàng
- Database đã có đủ fields:
order_item.gifted_id,order_item.voucher_discount_amount,order_item.gifted_from,user_vouchers.* - KHÔNG CÓ INDEX trên các fields trên — performance risk với dataset hàng triệu dòng
To-Be
- Tab mới "Đơn hàng Voucher/Quà tặng" trong Revenue Report Group
- Mỗi lượt dùng voucher/gift = 1 dòng (order_item level)
- Summary bar 4 metrics: lượt dùng, giá trị giảm, thực thu, tỷ lệ giảm TB
- Filter cascading: nguồn quà → chiến dịch/chương trình cụ thể
- Export Excel (client-side, không giới hạn row, confirmation dialog khi > 10K dòng)
A3) Goals & Success Metrics
| Goal | Metric | Target |
|---|---|---|
| Kiểm soát tiêu thụ voucher/gift | PO/Admin có thể xem báo cáo trong < 3s | Page load < 3s với indexes |
| Drill-down theo nguồn/campaign | Filter cascading hoạt động đúng | 5 nguồn × N campaigns |
| So sánh giữa chi nhánh | Branch scoping filter hoạt động | Staff/Manager/Admin đúng scope |
| Export dữ liệu | Excel export thành công | ≤ 10K dòng < 30s, ≤ 50K dòng < 120s |
A4) Personas
| Persona | Vai trò | JTBD | Frequency |
|---|---|---|---|
| Admin | Quản lý hệ thống | Kiểm soát tổng tiêu thụ voucher/gift toàn hệ thống, phát hiện lạm dụng | Hàng tuần |
| Manager | Quản lý chi nhánh | Xem tiêu thụ voucher/gift tại chi nhánh mình, đối chiếu với doanh thu | Hàng tuần |
| PO/BA | Product Owner | Phân tích hiệu quả nguồn quà (lắc xì vs voucher vs bạn bè) | Hàng tháng |
A5) Functional Requirements
FR-001: Hiển thị tab mới trong Revenue Report Group (Ref: DEC-001, DEC-004)
Priority: Must | SCR: SCR-01
AC:
- [ ] Tab "Đơn hàng Voucher/Quà tặng" hiển thị ở cuối danh sách tab trong Revenue Report Group
- [ ] Tab chỉ hiển thị khi user có quyền (
reportRoleschứarevenue_voucher_gift_order_report) - [ ] Click tab → render
VoucherGiftOrderReportTable+VoucherGiftOrderReportFilter - [ ] Các tab hiện có KHÔNG bị ảnh hưởng
FR-002: Summary Bar 4 metrics (Ref: DEC-003)
Priority: Must | SCR: SCR-01
AC:
- [ ] Hiển thị 4
BoxInfocards: Tổng lượt dùng (+ số đơn), Tổng giá trị giảm, Tổng thực thu, Tỷ lệ giảm TB - [ ] Metrics chỉ tính đơn đã xác nhận (
customer_confirmed_at IS NOT NULL) - [ ] Metrics mặc định loại đơn hủy (trừ khi filter chọn "Tất cả")
- [ ] "Tổng lượt dùng" hiển thị cả item count + distinct order count (VD: "256 lượt (128 đơn)")
- [ ] Tỷ lệ giảm TB: khi mẫu số = 0 → hiển thị "—"
- [ ] Loading state:
BoxInfo loading={true}→ QSkeleton
FR-003: Bảng dữ liệu 13 cột (Ref: DEC-003, DEC-010, DEC-011)
Priority: Must | SCR: SCR-01
AC:
- [ ] Bảng hiển thị 13 cột: STT, Mã đơn, Ngày tạo, Khách hàng, Chi nhánh, Voucher/Quà (2 dòng), Loại giảm, Số tiền giảm, Nguồn quà, Campaign, Tổng đơn, Thực thu, Trạng thái
- [ ] Chỉ hiển thị
order_itemcógifted_id IS NOT NULLhoặcvoucher_discount_amount > 0 - [ ] Chỉ hiển thị đơn loại
service,cosmetic,prepaid(Ref: DEC-008) - [ ] Pagination: 20 items/page trên
order_itemlevel - [ ] Sort:
order.created_atmới nhất trước - [ ] Visual grouping: khi nhiều dòng cùng 1 đơn, dim repeated order-level cells (Ref: DEC-010)
- [ ] Click mã đơn → mở chi tiết đơn hàng
- [ ] Cột "Voucher/Quà": tên dòng 1 + mã voucher dòng 2 (grey),
ellipsis+QTooltipkhi text dài - [ ] Cột currency: align right,
minWidth: 120px
FR-004: Filter 6 tiêu chí (Ref: DEC-013, DEC-009)
Priority: Must | SCR: SCR-01
AC:
- [ ] 6 filters: Thời gian, Chi nhánh, Nguồn quà, Chiến dịch/Chương trình, Loại giảm, Trạng thái đơn
- [ ] Filter "Nguồn quà": multi-select, 5 giá trị (
gifted_from_gamification,gifted_from_wheel_of_fortune,gifted_from_voucher,gifted_from_friend,gifted_from_manual) - [ ] Filter "Chiến dịch": disabled khi chưa chọn nguồn quà. Chọn nguồn → load danh sách campaign tương ứng. Reset value khi đổi nguồn
- [ ] Filter "Loại giảm": lọc trên
user_vouchers.gift_typequa relationship:gift: { gift_type: { _in: [...] } } - [ ] Filter "Trạng thái đơn": mặc định "Tất cả (trừ đã hủy)". Có option xem đơn hủy
- [ ] Filter thay đổi → reset pagination về trang 1
- [ ] Filter dùng component riêng
VoucherGiftOrderReportFilter.tsx
FR-005: Export Excel (Ref: DEC-002)
Priority: Must | SCR: SCR-01
AC:
- [ ] Nút export ở row riêng giữa summary bar và bảng
- [ ] Xuất đúng dữ liệu đang hiển thị (sau filter)
- [ ] Không giới hạn row — export toàn bộ dữ liệu sau filter (giống pattern các tab hiện có)
- [ ] Khi dữ liệu > 10,000 dòng → hiển thị confirmation dialog "Dữ liệu có hơn 10,000 dòng, quá trình export có thể mất vài phút. Tiếp tục?" → user chọn Tiếp tục/Hủy
- [ ] Tên file:
don-hang-voucher-qua-tang_DD-MM-YYYY.xlsx - [ ] Dùng
exceljs+createExcelBuildergiống pattern hiện có
FR-006: Database indexes (Ref: DEC-012)
Priority: Must (BLOCKER) | SCR: N/A (backend)
AC:
- [ ] 4 indexes tạo bằng
CREATE INDEX CONCURRENTLY(không lock table) - [ ] Index 1: Partial index trên
order_item(order_id) WHERE (gifted_id IS NOT NULL OR voucher_discount_amount > 0) - [ ] Index 2: Composite index trên
order(created_at DESC, branch_id) WHERE customer_confirmed_at IS NOT NULL - [ ] Index 3: Index trên
order_item(gifted_from) WHERE gifted_from IS NOT NULL - [ ] Index 4: Index trên
user_vouchers(gift_type) WHERE gift_type IS NOT NULL - [ ] Deploy indexes TRƯỚC khi deploy feature FE
- [ ] EXPLAIN ANALYZE trên staging: list query < 500ms, aggregate < 3s
FR-007: Permission + Migration seed (Ref: DEC-006)
Priority: Must | SCR: N/A (backend)
AC:
- [ ] Thêm constant
REVENUE_VOUCHER_GIFT_ORDER_REPORT = "revenue_voucher_gift_order_report"vàotypes.ts - [ ] Cập nhật
REPORT_TREE[REVENUE_REPORT_GROUP] - [ ] Seed data: INSERT report_role record cho report mới
- [ ] Branch scoping: frontend WHERE
order.branch_id IN (...)từglobalStore.account.branches
A6) Assumptions
| ID | Assumption | Owner xác nhận |
|---|---|---|
| ASM-001 | order_item.gifted_id và voucher_discount_amount đã được populate đúng cho tất cả đơn hàng có voucher/gift | BE Dev |
| ASM-002 | user_vouchers.voucher_name và voucher_code luôn có giá trị khi gifted_id IS NOT NULL | BE Dev |
| ASM-003 | Hasura cho phép aggregate query trên order_item với nested order filter | Tech Lead |
| ASM-004 | Report role seeding mechanism đã có sẵn (chỉ cần INSERT record mới) | BE Dev |
A7) Risks
| ID | Risk | Impact | Probability | Mitigation |
|---|---|---|---|---|
| RSK-001 | Query timeout trên production (dataset triệu dòng) | Cao | Cao (nếu không có index) | FR-006: 4 indexes BLOCKER. EXPLAIN ANALYZE trước deploy |
| RSK-002 | Cascading filter load chậm (query gamification/reward_wheel) | Trung bình | Thấp | Chỉ query khi user chọn nguồn quà, lazy load |
| RSK-003 | Export Excel crash browser với dataset rất lớn (> 200K dòng) | Trung bình | Thấp (voucher items chiếm 10-15% tổng, hiếm khi > 100K) | Confirmation dialog khi > 10K dòng cho user biết trước. Follow pattern hiện tại (client-side, không cap) |
| RSK-004 | CREATE INDEX CONCURRENTLY thất bại trên production | Cao | Thấp | Chạy ngoài giờ cao điểm, monitor lock wait |
| RSK-005 | Branch scoping chỉ ở frontend — user kỹ thuật có thể bypass để xem dữ liệu branch khác | Cao | Thấp (cần authenticated user + kỹ thuật) | Lỗ hổng hệ thống — tất cả reports dùng cùng pattern. Cần initiative riêng fix Hasura permission + auth service. Feature này follow pattern hiện tại để consistent. Xem dev-spec C8 |
A8) Metrics (Post-launch)
| Metric | Cách đo | Target | Khi nào đo |
|---|---|---|---|
| Page load time | Browser Performance API | < 3s (list + summary) | Tuần 1 sau launch |
| Aggregate query time | EXPLAIN ANALYZE | < 3s worst case | Trước launch |
| User adoption | Analytics event (tab view count) | > 10 views/tuần | Tháng 1 |
| Export success rate | Analytics event report_voucher_gift_export | > 95% thành công (không crash/abort) | Tháng 1 |
A9) Glossary
| Thuật ngữ (VI) | Thuật ngữ (EN) | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Lượt dùng voucher | Voucher usage | Một order_item có gifted_id IS NOT NULL hoặc voucher_discount_amount > 0 | ≠ Đơn hàng (1 đơn có thể có nhiều lượt dùng) |
| Nguồn quà | Gift source | Nơi voucher/gift đến: lắc xì, vòng quay, campaign, bạn bè, thủ công | Field: order_item.gifted_from |
| Chiến dịch | Campaign/Program | Chương trình phát voucher (voucher_campaigns) hoặc game (gamification) | ≠ Nguồn quà (nguồn là category, chiến dịch là instance cụ thể) |
| Giá trị giảm | Discount amount | Số tiền giảm do voucher/gift tính trên từng order_item | Field: order_item.voucher_discount_amount |
| Thực thu | Net revenue | Số tiền thực nhận sau tất cả giảm giá (voucher + membership + promo) | Field: order.paid_amount |
| Tỷ lệ giảm TB | Average discount rate | voucher_discount / (paid_amount + voucher_discount) × 100 | Chỉ tính phần voucher/gift, không gồm membership/promo |
| Dim repeated cells | Visual grouping | Khi nhiều order_item cùng 1 đơn, ẩn bớt các cột order-level ở dòng 2+ | Pattern mới, chưa có trong các tab khác |
RACI
| Deliverable | PO | TL | FE Dev | BE Dev | QA |
|---|---|---|---|---|---|
| PRD (file này) | A | C | I | I | I |
| UI Spec | C | I | R | I | I |
| Dev Spec | I | A | C | R | I |
| QA Test Plan | C | I | I | I | R |
| Migration + Indexes | I | A | — | R | I |
| FE Components | I | I | R | — | I |
R = Responsible, A = Accountable, C = Consulted, I = Informed
A10) Business Formulas
FORMULA-001: Tỷ lệ giảm trung bình
- Mô tả: Phần trăm giá trị giảm do voucher/gift so với tổng giá trị đơn gốc (trước giảm voucher)
- Công thức:
discount_rate = total_voucher_discount / (total_paid_amount + total_voucher_discount) × 100 - Biến số:
total_voucher_discount= SUM(order_item.voucher_discount_amount) — tổng giá trị giảm từ voucher/gifttotal_paid_amount= SUM(DISTINCTorder.paid_amount) — tổng thực thu (đã trừ voucher + các giảm giá khác)
- Đơn vị: % (1 decimal)
- Ví dụ: Tổng giảm voucher = 45.200.000đ, Tổng thực thu = 320.500.000đ → 45.200.000 / (320.500.000 + 45.200.000) × 100 = 12.4%
- Edge cases:
- Mẫu số = 0 (cả paid_amount và voucher_discount đều 0) → hiển thị "—"
- Chỉ có gift type "Lời chúc" (discount = 0) → tỷ lệ = 0.0%