Skip to content

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

VersionDateAuthorThay đổi
1.019/03/2026PO/BAInitial — chuyển từ design doc Mode A

Hướng dẫn đọc (RACI)

AudienceĐọc sectionsTrách nhiệm
PO/BAZ, A, BApprove requirements, UX flows
Tech LeadZ, A, CApprove architecture, review dev spec
FE DevB, C (C1-C6)Implement UI
BE DevC (C1-C12)Implement backend (indexes, migration)
QAA (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

MilestoneTargetOwnerĐiều kiện
1. DB Migration (indexes)T+2 ngàyBE Dev
2. GraphQL queries + codegenT+3 ngàyBE DevSau indexes deploy
3. FE Components (filter + table + summary)T+7 ngàyFE DevSau codegen
4. QA TestingT+10 ngàyQASau FE deploy staging
5. Go-LiveT+12 ngàyTech LeadQA pass

Trạng thái Sign-off

DomainNgườiStatus
BusinessPOApproved
TechTech LeadApproved
QAQA LeadApproved

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ăngLý do defer
1Top 10 khách hàng sử dụng voucher nhiều nhấtCần thêm aggregate query phức tạp, ưu tiên MVP trước
2Tỷ lệ đơn dùng voucher / tổng đơn (benchmark)Cần cross-reference với tổng đơn — query thêm
3Filter trạng thái voucher (redeemed/restored/expired)Phức tạp hoá filter, chưa urgent
4Multi-column sort (sort theo số tiền giảm)Các tab hiện có cũng chỉ sort 1 cột
5Materialized view cho summary barBắt đầu với indexes, chỉ triển khai nếu aggregate > 3s
6Campaign ROI metricsThuộc Campaign Report, không phải tab này
7Fix branch scoping ở Hasura permission level (RSK-005)Lỗ hổng hệ thống — cần initiative riêng

Z) Decision Log

IDCategoryQuyết địnhLý doNgàyStatus
DEC-001BusinessThê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 regression19/03/2026Locked
DEC-002BusinessChỉ 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ễu19/03/2026Locked
DEC-003TechnicalQuery ở order_item level, pagination trên order_itemMỗ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 items19/03/2026Locked
DEC-004UX1 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 Report19/03/2026Locked
DEC-005BusinessPhân biệt với Campaign Report: tab này = góc đơn hàng, Campaign Report = góc chiến dịchTránh nhầm lẫn cho Admin khi 2 report có dữ liệu chồng chéo19/03/2026Locked
DEC-006TechnicalBranch scoping ở frontend WHERE builder, không phải Hasura permissionHasura permission trên order, order_item, user_vouchers đều có filter: {} — không auto-filter branch19/03/2026Locked
DEC-007TechnicalDùng order.addresses thay vì order.customer cho thông tin khách hàngorder.customer là remote relationship — gây thêm HTTP call cho mỗi order. addresses cùng DB19/03/2026Locked
DEC-008TechnicalFilter order_kind IN (service, cosmetic, prepaid) — loại đơn chuyển kho/vật tưĐơn order_transferinternal_material không bao giờ có voucher — giảm hàng trăm nghìn rows scan19/03/2026Locked
DEC-009UXMặ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ó RestoreVouchersOnOrderCancel19/03/2026Locked
DEC-010UXVisual grouping: dim repeated order-level cells khi 1 đơn có nhiều itemsTab này là tab đầu tiên render item-level — cần visual cue để phân biệt19/03/2026Locked
DEC-011UXGộ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/2026Locked
DEC-012Technical4 database indexes BLOCKER — phải deploy trước featureKhông có index → seq scan 5M rows → timeout > 30s trên production19/03/2026Locked
DEC-013UXTạ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ác19/03/2026Locked
DEC-014TechnicalwhereOrderAgg 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ảng19/03/2026Locked
DEC-015UXFilter dùng nút "Áp dụng" (không auto-reload)Consistent với RevenueReportFilter pattern. Tránh multi-request khi đổi cascading filter19/03/2026Locked
DEC-016UXLayout 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ủ đích19/03/2026Locked

A) PRD

A1) Blueprint

FieldValue
FeatureBáo cáo Đơn hàng Voucher/Quà tặng
TypeEnhancement
PlatformWeb Admin (diva-admin)
Module ảnh hưởngreport (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

GoalMetricTarget
Kiểm soát tiêu thụ voucher/giftPO/Admin có thể xem báo cáo trong < 3sPage load < 3s với indexes
Drill-down theo nguồn/campaignFilter cascading hoạt động đúng5 nguồn × N campaigns
So sánh giữa chi nhánhBranch scoping filter hoạt độngStaff/Manager/Admin đúng scope
Export dữ liệuExcel export thành công≤ 10K dòng < 30s, ≤ 50K dòng < 120s

A4) Personas

PersonaVai tròJTBDFrequency
AdminQuản lý hệ thốngKiểm soát tổng tiêu thụ voucher/gift toàn hệ thống, phát hiện lạm dụngHàng tuần
ManagerQuản lý chi nhánhXem tiêu thụ voucher/gift tại chi nhánh mình, đối chiếu với doanh thuHàng tuần
PO/BAProduct OwnerPhâ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 (reportRoles chứa revenue_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 BoxInfo cards: 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_itemgifted_id IS NOT NULL hoặc voucher_discount_amount > 0
  • [ ] Chỉ hiển thị đơn loại service, cosmetic, prepaid (Ref: DEC-008)
  • [ ] Pagination: 20 items/page trên order_item level
  • [ ] Sort: order.created_at mớ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 + QTooltip khi 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_type qua 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 + createExcelBuilder giố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ào types.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

IDAssumptionOwner xác nhận
ASM-001order_item.gifted_idvoucher_discount_amount đã được populate đúng cho tất cả đơn hàng có voucher/giftBE Dev
ASM-002user_vouchers.voucher_namevoucher_code luôn có giá trị khi gifted_id IS NOT NULLBE Dev
ASM-003Hasura cho phép aggregate query trên order_item với nested order filterTech Lead
ASM-004Report role seeding mechanism đã có sẵn (chỉ cần INSERT record mới)BE Dev

A7) Risks

IDRiskImpactProbabilityMitigation
RSK-001Query timeout trên production (dataset triệu dòng)CaoCao (nếu không có index)FR-006: 4 indexes BLOCKER. EXPLAIN ANALYZE trước deploy
RSK-002Cascading filter load chậm (query gamification/reward_wheel)Trung bìnhThấpChỉ query khi user chọn nguồn quà, lazy load
RSK-003Export Excel crash browser với dataset rất lớn (> 200K dòng)Trung bìnhThấ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-004CREATE INDEX CONCURRENTLY thất bại trên productionCaoThấpChạy ngoài giờ cao điểm, monitor lock wait
RSK-005Branch scoping chỉ ở frontend — user kỹ thuật có thể bypass để xem dữ liệu branch khácCaoThấ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)

MetricCách đoTargetKhi nào đo
Page load timeBrowser Performance API< 3s (list + summary)Tuần 1 sau launch
Aggregate query timeEXPLAIN ANALYZE< 3s worst caseTrước launch
User adoptionAnalytics event (tab view count)> 10 views/tuầnTháng 1
Export success rateAnalytics 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ĩaPhân biệt với
Lượt dùng voucherVoucher usageMộ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 sourceNơi voucher/gift đến: lắc xì, vòng quay, campaign, bạn bè, thủ côngField: order_item.gifted_from
Chiến dịchCampaign/ProgramChươ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ảmDiscount amountSố tiền giảm do voucher/gift tính trên từng order_itemField: order_item.voucher_discount_amount
Thực thuNet revenueSố tiền thực nhận sau tất cả giảm giá (voucher + membership + promo)Field: order.paid_amount
Tỷ lệ giảm TBAverage discount ratevoucher_discount / (paid_amount + voucher_discount) × 100Chỉ tính phần voucher/gift, không gồm membership/promo
Dim repeated cellsVisual groupingKhi 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

DeliverablePOTLFE DevBE DevQA
PRD (file này)ACIII
UI SpecCIRII
Dev SpecIACRI
QA Test PlanCIIIR
Migration + IndexesIARI
FE ComponentsIIRI

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/gift
    • total_paid_amount = SUM(DISTINCT order.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%