Skip to content

QA Test Plan — Nâng cấp Voucher Management: Kiểm soát, Thống kê & Đối tác

Version: 1.0 Ngày: 2026-03-19 PRD version: v1.1 Tham chiếu: prd.md · ui-spec.md · dev-spec.mdComplexity: Large — 4 phases, 22 FRs, 76 test cases


D1. Test Coverage Summary

1.1 Phạm vi test

PhasePriorityMô tảSố TCRisk Level
Phase 1P0 — UrgentKiểm soát thời gian kích hoạt → sử dụng + Override + Settings28High — ảnh hưởng flow tạo ĐH
Phase 2P1 — HighThống kê nhân viên phát voucher + Export + Partner API16Medium
Phase 3P1 — HighĐối tác Affiliate — gán, quota, badge16High — race condition quota
Phase 4P2 — MediumChu kỳ sử dụng — KPI, bucket chart, so sánh16Medium
Tổng76

1.2 Loại test

LoạiMô tảPhase áp dụngTỉ lệ
FunctionalVerify FRs + ACs đúng yêu cầuP1-P455%
Permission / RBACVerify role × action × branch scopeP1-P415%
Edge CaseBoundary, concurrent, timezone, NULLP1-P415%
Formula / CalculationVerify FORMULA-001 đến FORMULA-006P1-P410%
NFR / PerformanceResponse time, async export, scaleP2, P45%

1.3 Ngoài phạm vi

Nội dungLý do
UI/UX visual design (color, font, spacing)QA chỉ verify layout + state, không verify pixel-perfect
Load testing > 100k concurrent usersDùng performance test riêng (staging environment)
Cross-browser testingCovered bởi QA standard regression suite
Module Affiliate CRUDModule hiện có, không thuộc feature này
Notification delivery (push/email)Test notification trigger + content, không test delivery infrastructure

D2. Requirements Traceability Matrix

Phase 1 — Kiểm soát thời gian

FR IDMô tảScreenAPI / HandlerTest Case IDsFormula
FR-P1-01Config min_activation_hours per campaignVoucherConfigFormvoucher_campaigns mutationTC-P1-01, TC-P1-02
FR-P1-02Chặn tạo ĐH khi voucher chưa đủ thời gianVoucherTimeBlockDialogvalidateOrderVouchers()TC-P1-03, TC-P1-04, TC-P1-05, TC-P1-06FORMULA-001
FR-P1-03Manager overrideVoucherTimeBlockDialogapprove_voucher_overrideTC-P1-07, TC-P1-08, TC-P1-09, TC-P1-10
FR-P1-04Dashboard giám sát overrideVoucherOverrideDashboardGetVoucherOverrides queryTC-P1-11, TC-P1-12, TC-P1-13
FR-P1-05Config mặc định thời gian chờ SettingsVoucherSettingsSectionapp_setting mutationTC-P1-14, TC-P1-15
FR-P1-06Thời hạn voucher manual— (backend)activate_offline_voucherTC-P1-16, TC-P1-17, TC-P1-18
FR-P1-07Hiển thị hạn sử dụng Partner app/POSPartner appuser_vouchers queryTC-P1-19
Permission tests P1All P1 screensTC-P1-20, TC-P1-21, TC-P1-22, TC-P1-23
Edge cases P1TC-P1-24, TC-P1-25, TC-P1-26, TC-P1-27, TC-P1-28FORMULA-001

Phase 2 — Thống kê nhân viên

FR IDMô tảScreenAPI / FunctionTest Case IDsFormula
FR-P2-01Báo cáo thống kê per NVVoucherStaffStatisticsTabget_voucher_staff_statisticsTC-P2-01, TC-P2-02FORMULA-002
FR-P2-02Drill-down chi tiết per NVStaffDrillDownDialogGetStaffVoucherDetail queryTC-P2-03, TC-P2-04
FR-P2-03Ranking NVVoucherStaffStatisticsTabget_voucher_staff_statistics (ORDER BY)TC-P2-05
FR-P2-04API thống kê cá nhân Partner appPartner appget_voucher_staff_statistics (self)TC-P2-06, TC-P2-07FORMULA-002
FR-P2-05Export ExcelVoucherStaffStatisticsTabexport-apiTC-P2-08, TC-P2-09
Permission tests P2TC-P2-10, TC-P2-11, TC-P2-12
Edge cases P2TC-P2-13, TC-P2-14, TC-P2-15, TC-P2-16FORMULA-002

Phase 3 — Đối tác Affiliate

FR IDMô tảScreenAPI / HandlerTest Case IDsFormula
FR-P3-01Gán đối tác vào campaignAffiliateAssignStepvoucher_campaign_affiliates mutationTC-P3-01, TC-P3-02
FR-P3-02Quota per đối tác— (backend)activate_offline_voucher + quota checkTC-P3-03, TC-P3-04, TC-P3-05FORMULA-006
FR-P3-03Phân biệt NV vs đối tác trong reportVoucherStaffStatisticsTabget_voucher_staff_statisticsTC-P3-06, TC-P3-07
FR-P3-04Badge "Voucher do đối tác X phát"AffiliateSourceBadgeuser_vouchers queryTC-P3-08
FR-P3-05Log gán/gỡ đối tác— (backend)affiliate_action_log insertTC-P3-09
Permission tests P3TC-P3-10, TC-P3-11
Edge cases P3TC-P3-12, TC-P3-13, TC-P3-14, TC-P3-15, TC-P3-16FORMULA-006

Phase 4 — Chu kỳ sử dụng

FR IDMô tảScreenAPI / FunctionTest Case IDsFormula
FR-P4-01KPI tổng quanVoucherUsageCycleTabget_voucher_usage_cycle_statisticsTC-P4-01, TC-P4-02FORMULA-003, FORMULA-004
FR-P4-02Phân bổ theo bucketCycleBucketChartget_voucher_usage_cycle_distributionTC-P4-03, TC-P4-04FORMULA-005
FR-P4-03So sánh chiến dịchVoucherUsageCycleTabget_voucher_usage_cycle_statistics (multi)TC-P4-05, TC-P4-06
FR-P4-04Filter + Group byVoucherUsageCycleTabfunction paramsTC-P4-07, TC-P4-08
FR-P4-05Export Excel chu kỳVoucherUsageCycleTabexport-apiTC-P4-09
Permission tests P4TC-P4-10, TC-P4-11
Edge cases P4TC-P4-12, TC-P4-13, TC-P4-14, TC-P4-15, TC-P4-16FORMULA-003, FORMULA-004, FORMULA-005

D3. Seed Data

3.1 Campaigns

ID (alias)Tênmin_activation_hoursTrạng tháiVouchersAffiliatesPhase
CAMP-ATết Kim Cương 202624Active5003 (KOL Hương 100, PK Đông Y 200, KOL Minh unlimited)P1-P4
CAMP-BSinh nhật Diva 202648Active2000P1-P2
CAMP-CƯu đãi VIP0 (no check)Active500P1 edge case
CAMP-DHè Rực Rỡ (ended)24Ended1000P1 edge case
CAMP-ETest Chu Kỳ24Active3002P4 so sánh

3.2 Chi nhánh (Branches)

ID (alias)TênGhi chú
BR-01Q.1 HCMBranch chính
BR-02Q.3 HCMBranch phụ
BR-03Đà NẵngBranch khác vùng

3.3 Users (Staff + Manager + Admin)

ID (alias)TênRoleBranchGhi chú
USR-ADMINLê AdminITLeaderAllFull access
USR-BODTrần BODBODAllFull access, no Settings config
USR-MGR1Nguyễn Manager Q1ManagerCNBR-01Override quyền cho BR-01
USR-MGR2Phạm Manager Q3ManagerCNBR-02Override quyền cho BR-02
USR-ACCVũ Kế ToánAccLeaderAllXem report, export, không override
USR-NV1Nguyễn AStaffBR-01Phát 120 voucher (45 redeemed, 15 expired)
USR-NV2Trần BStaffBR-01Phát 80 voucher (30 redeemed)
USR-NV3Lê CStaffBR-02Phát 50 voucher (10 redeemed)
USR-NV4Hoàng DStaff (deactivated)BR-01Đã nghỉ việc, có 30 voucher lịch sử
USR-NV5Đỗ EStaffBR-03Phát 0 voucher (test empty)

3.4 Affiliates

ID (alias)TênQuota trong CAMP-Adistributed_countGhi chú
AFF-01KOL Hương10098Gần hết quota (test boundary)
AFF-02PK Đông Y20050Còn nhiều quota
AFF-03KOL MinhNULL (unlimited)75Unlimited quota

3.5 Vouchers (user_vouchers) — key records

ID (alias)CampaignStaffStatusactivated_atMục đích test
VC-01CAMP-AUSR-NV1activated2026-03-18 10:00Chặn (< 24h)
VC-02CAMP-AUSR-NV1activated2026-03-17 08:00Đủ thời gian (> 24h)
VC-03CAMP-BUSR-NV2activated2026-03-18 14:00Chặn (< 48h)
VC-04CAMP-CUSR-NV1activated2026-03-19 09:00min=0, không chặn
VC-05CAMP-AUSR-NV1redeemed2026-03-01 10:00Cycle = 8 ngày
VC-06CAMP-AUSR-NV2redeemed2026-03-05 10:00Cycle = 3 ngày
VC-07CAMP-AUSR-NV3expired2026-02-28 10:00Expired voucher
VC-08NULLUSR-NV1activated2026-03-18 15:00Voucher manual (no campaign)
VC-09CAMP-AAFF-01activated2026-03-17 10:00Affiliate voucher
VC-10CAMP-AAFF-02redeemed2026-03-10 10:00Affiliate, cycle test
VC-11CAMP-AUSR-NV1activated2026-03-19 23:30Timezone boundary (midnight)
VC-12CAMP-DUSR-NV1activated2026-03-18 10:00Campaign ended, voucher active
VC-13CAMP-AUSR-NV1redeemed2026-03-15 10:00Redeemed → restored → redeemed
VC-14NULLUSR-NV2activated2026-03-01 10:00Manual, sắp hết hạn

3.6 Override Records

ID (alias)VoucherApproved Byreason_codehours_remainingMục đích test
OVR-01VC-01USR-MGR1vip_customer22.15Override chuẩn
OVR-02VC-03USR-MGR2special_event40.50Override khác branch
OVR-03VC-01USR-ADMINmanager_directive18.00Double override cùng voucher
OVR-04VC-12USR-MGR1system_error12.30Campaign ended
OVR-05VC-08USR-BODother20.00Manual voucher override

3.7 Voucher Logs (cho cycle testing)

VoucherActioncreated_atGhi chú
VC-05voucher_redeemed2026-03-09 14:00Cycle = 8.17 ngày
VC-06voucher_redeemed2026-03-08 10:00Cycle = 3.0 ngày
VC-13voucher_redeemed2026-03-16 12:00Lần redeem đầu
VC-13voucher_restored2026-03-16 14:00ĐH hủy → restore
VC-13voucher_redeemed2026-03-18 10:00Redeem lại — lấy lần cuối
VC-10voucher_redeemed2026-03-14 10:00Affiliate cycle = 4 ngày

3.8 App Setting (VoucherSetting)

json
{
  "AppSettings": {
    "VoucherSetting": {
      "DefaultMinActivationHours": 24,
      "DefaultManualVoucherExpiryDays": 30
    }
  }
}

D4. Test Cases

Phase 1 — Kiểm soát thời gian (28 TCs)

Nhóm 1.1: Config min_activation_hours (FR-P1-01)


TC-P1-01: Tạo campaign với min_activation_hours hợp lệ

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P1-01
PreconditionLogin USR-ADMIN. Mở tạo chiến dịch voucher
Steps1. Vào step "Cấu hình"
2. Nhập "Thời gian chờ tối thiểu (giờ)" = 48
3. Hoàn thành wizard → Submit
ExpectedCampaign tạo thành công. voucher_campaigns.min_activation_hours = 48. Hint hiển thị: "Voucher phải chờ 48 giờ sau khi kích hoạt mới được dùng trong đơn hàng"
DataCAMP-B (min_activation_hours=48)
RefDEC-B01, DEC-B02

TC-P1-02: Validation input min_activation_hours

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-01
PreconditionLogin USR-ADMIN. Mở tạo chiến dịch voucher, step "Cấu hình"
Steps1. Nhập min_activation_hours = -1 → verify validation
2. Nhập min_activation_hours = 721 → verify validation (max 720)
3. Nhập min_activation_hours = 0 → verify chấp nhận
4. Nhập min_activation_hours = "abc" → verify chặn ký tự
5. Bỏ trống → verify dùng default 24
Expected-1: lỗi "Giá trị phải >= 0". 721: lỗi "Giá trị tối đa 720 giờ". 0: OK. "abc": không nhập được (input number). Bỏ trống: default 24
Data
RefDEC-B02

Nhóm 1.2: Chặn tạo ĐH (FR-P1-02, FORMULA-001)


TC-P1-03: Chặn ĐH khi voucher chưa đủ thời gian — happy path

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P1-02
PreconditionLogin USR-NV1 (Staff, BR-01). VC-01: activated 2h trước, CAMP-A min=24h
Steps1. Tạo đơn hàng mới
2. Chọn voucher VC-01
3. Submit tạo ĐH
ExpectedHệ thống chặn. VoucherTimeBlockDialog hiển thị:
- Mã voucher: [VC-01 code]
- Kích hoạt: 18/03/2026 10:00
- Thời gian chờ: 24 giờ
- Còn lại: ~22 giờ
- "Voucher có thể sử dụng từ: 19/03/2026 10:00"
- Nút "Yêu cầu Manager duyệt" ẩn (Staff không có quyền)
DataVC-01, CAMP-A
FormulaFORMULA-001: hours_elapsed = (NOW() - 2026-03-18 10:00) / 3600 ≈ 2.0 < 24 → REJECT. remaining = 24 - 2 = 22h
RefDEC-T01, DEC-B01

TC-P1-04: Cho phép ĐH khi voucher đủ thời gian

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P1-02
PreconditionLogin USR-NV1. VC-02: activated > 24h trước, CAMP-A min=24h
Steps1. Tạo đơn hàng mới
2. Chọn voucher VC-02
3. Submit tạo ĐH
ExpectedĐH tạo thành công. Không hiện dialog chặn. FORMULA-001: hours_elapsed > 24 → PASS
DataVC-02, CAMP-A
FormulaFORMULA-001: hours_elapsed = (NOW() - 2026-03-17 08:00) / 3600 > 24 → PASS
RefDEC-T01

TC-P1-05: Voucher min_activation_hours = 0 — bỏ qua check

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P1-02
PreconditionLogin USR-NV1. VC-04: activated vừa mới, CAMP-C min=0
Steps1. Tạo đơn hàng mới
2. Chọn voucher VC-04
3. Submit
ExpectedĐH tạo thành công. Không chặn. min_activation_hours = 0 → skip check hoàn toàn
DataVC-04, CAMP-C
RefEC-P1-02, DEC-B01

TC-P1-06: Hiển thị thời gian còn lại chính xác (FORMULA-001)

Thuộc tínhGiá trị
PriorityP1
TypeFormula
FRFR-P1-02
PreconditionVC-03: activated 2026-03-18 14:00, CAMP-B min=48h. Hiện tại: 2026-03-19 10:00
Steps1. Tạo ĐH với voucher VC-03
Expectedhours_elapsed = 20.0h. Remaining = 48 - 20 = 28h. Dialog hiện: "Còn lại: 28 giờ 0 phút". Available from: "20/03/2026 14:00"
DataVC-03, CAMP-B
FormulaFORMULA-001: EXTRACT(EPOCH FROM ('2026-03-19 10:00' - '2026-03-18 14:00')) / 3600 = 20.0. 20.0 < 48 → REJECT. Remaining = 28.0h
RefDEC-T01

Nhóm 1.3: Manager Override (FR-P1-03)


TC-P1-07: Manager override thành công — happy path

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P1-03
PreconditionLogin USR-MGR1 (ManagerCN BR-01). VC-01 bị chặn (< 24h). Voucher thuộc BR-01
Steps1. Tạo ĐH → chọn VC-01 → bị chặn
2. Click "Yêu cầu Manager duyệt"
3. Chọn lý do: "Khách VIP"
4. Nhập ghi chú: "Khách VIP thân thiết 5 năm, yêu cầu đặc biệt" (>= 10 ký tự)
5. Click "Xác nhận duyệt"
ExpectedOverride tạo thành công. voucher_activation_overrides record mới: approved_by = USR-MGR1, reason_code = 'vip_customer', branch_id = BR-01. Toast: "Override đã được duyệt". ĐH có thể tạo tiếp
DataVC-01, USR-MGR1, OVR-01
RefDEC-B01, DEC-T02

TC-P1-08: Override validation — ghi chú < 10 ký tự

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-03
PreconditionLogin USR-MGR1. VC-01 bị chặn. Dialog override mở
Steps1. Chọn lý do: "Khách VIP"
2. Nhập ghi chú: "OK" (2 ký tự)
3. Click "Xác nhận duyệt"
ExpectedValidation error: ghi chú phải >= 10 ký tự. Override không tạo
DataVC-01
Refdev-spec C5.1 — reason_note min 10 chars

TC-P1-09: Staff không thể override

Thuộc tínhGiá trị
PriorityP0
TypePermission
FRFR-P1-03
PreconditionLogin USR-NV1 (Staff). VC-01 bị chặn
Steps1. Tạo ĐH → chọn VC-01 → bị chặn
2. Verify nút "Yêu cầu Manager duyệt"
ExpectedNút "Yêu cầu Manager duyệt" ẩn. Chỉ hiện nút "Đóng". Staff không có quyền override
DataVC-01, USR-NV1
RefB5 Permission Matrix — Dialog chặn: Override → Staff ❌

TC-P1-10: Manager override khác branch — chặn

Thuộc tínhGiá trị
PriorityP0
TypePermission
FRFR-P1-03
PreconditionLogin USR-MGR2 (ManagerCN BR-02). VC-01 thuộc BR-01 bị chặn
Steps1. Gọi API approve_voucher_override với user_voucher_id = VC-01
ExpectedError OVERRIDE_UNAUTHORIZED. Manager BR-02 không thể override voucher BR-01. Branch scope: voucher.branch_id NOT IN ctx.Access.BranchIds
DataVC-01, USR-MGR2
RefC8.1 — Branch scope

Nhóm 1.4: Override Dashboard (FR-P1-04)


TC-P1-11: Dashboard hiển thị danh sách override

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-04
PreconditionLogin USR-ADMIN. 5 override records trong seed data
Steps1. Mở Override Dashboard
2. Verify bảng hiển thị
ExpectedBảng hiện 5 records với cột: Người duyệt, Chi nhánh, Mã Voucher, Lý do, Ghi chú, Thời gian. Sắp xếp theo created_at DESC. Tổng summary line hiện đúng
DataOVR-01 đến OVR-05
RefDEC-T02

TC-P1-12: Dashboard filter theo chi nhánh + thời gian

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-04
PreconditionLogin USR-ADMIN. Dashboard mở, 5 override records
Steps1. Filter chi nhánh = "Q.1 HCM" (BR-01)
2. Verify kết quả
3. Filter thời gian: 15/03 - 19/03
4. Verify kết quả
ExpectedFilter CN Q.1 HCM: hiện OVR-01, OVR-03, OVR-04 (branch_id = BR-01). Filter thời gian: hiện records trong khoảng
DataOVR-01 đến OVR-05
RefFR-P1-04

TC-P1-13: Dashboard — Manager chỉ thấy branch mình

Thuộc tínhGiá trị
PriorityP0
TypePermission
FRFR-P1-04
PreconditionLogin USR-MGR1 (ManagerCN BR-01)
Steps1. Mở Override Dashboard
ExpectedChỉ hiện override records có branch_id = BR-01 (OVR-01, OVR-03, OVR-04). KHÔNG hiện OVR-02 (BR-02). Nút "Xuất Excel" ẩn (ManagerCN không có quyền export)
DataOVR-01 đến OVR-05
RefB5 Permission Matrix — Override Dashboard: ManagerCN ✅(Branch), Export ❌

Nhóm 1.5: Settings (FR-P1-05, FR-P1-06)


TC-P1-14: Cấu hình thời gian chờ mặc định

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-05
PreconditionLogin USR-ADMIN. Settings > Voucher
Steps1. Sửa "Thời gian chờ mặc định (giờ)" từ 24 → 12
2. Lưu
3. Verify app_setting.VoucherSetting.DefaultMinActivationHours = 12
ExpectedLưu thành công. app_setting JSONB cập nhật đúng. Voucher manual (không thuộc campaign) áp dụng 12h thay vì 24h
Dataapp_setting
RefDEC-T08

TC-P1-15: ITStaff không được config Settings

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P1-05
PreconditionLogin USR (role ITStaff)
Steps1. Truy cập Settings > Voucher
ExpectedRedirect + toast "Bạn không có quyền". Chỉ ITLeader, BOD được config
Data
RefB5 Permission Matrix — Settings: Config → ITStaff ❌

TC-P1-16: Thời hạn voucher manual — set expired_at khi kích hoạt

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P1-06
PreconditionDefaultManualVoucherExpiryDays = 30. NV kích hoạt voucher manual
Steps1. NV tặng voucher manual cho khách ngày 01/04/2026
2. Verify user_vouchers.expired_at
Expectedexpired_at = 01/05/2026 (01/04 + 30 ngày). Voucher có hạn sử dụng 30 ngày kể từ khi tặng
DataVC-08 pattern
RefDEC-B05

TC-P1-17: Cron expired_vouchers đánh hết hạn voucher manual

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-06
PreconditionVC-14: voucher manual, activated_at = 01/03, expired_at = 31/03. Hiện tại > 31/03
Steps1. Trigger cron expired_vouchers
2. Verify voucher status
ExpectedVC-14 chuyển status = 'expired'. Cron check expired_at < NOW() AND status = 'activated' → đánh expired
DataVC-14
RefDEC-B05, C6.1

TC-P1-18: Notification voucher manual sắp hết hạn (3 ngày trước)

Thuộc tínhGiá trị
PriorityP2
TypeFunctional
FRFR-P1-06
PreconditionVC-14: expired_at = 2026-04-01. Hiện tại: 2026-03-29
Steps1. Trigger cron expired_vouchers
ExpectedNotification noti_voucher_manual_expiring gửi tới customer. Template: "Voucher {name} ({code}) sẽ hết hạn vào 01/04/2026. Hãy sử dụng trước khi quá hạn!". Dedupe: 1 lần/voucher
DataVC-14
RefB4 Notification Spec

TC-P1-19: Hiển thị hạn sử dụng trên Partner app (FR-P1-07)

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P1-07
PreconditionNV vừa tặng voucher manual, expired_at đã set
Steps1. NV tặng voucher → verify hiển thị
2. Khách mở app xem voucher
ExpectedHiển thị: "Hết hạn: 01/05/2026". Cả NV trên POS/Partner app và khách trên customer app đều thấy ngày hết hạn
DataVC-08 pattern
RefDEC-B05

Nhóm 1.6: Permission Tests P1


TC-P1-20: AccLeader xem Override Dashboard nhưng không override

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P1-03, FR-P1-04
PreconditionLogin USR-ACC (AccLeader)
Steps1. Mở Override Dashboard → verify xem được
2. Tạo ĐH → voucher bị chặn → verify không có nút override
3. Click "Xuất Excel" trên Dashboard → verify export được
ExpectedDashboard: xem all branches OK. Dialog chặn: nút "Yêu cầu duyệt" ẩn. Export: OK (AccLeader có quyền export)
Data
RefB5 Permission Matrix P1

TC-P1-21: Staff không truy cập Override Dashboard

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P1-04
PreconditionLogin USR-NV1 (Staff)
Steps1. Navigate trực tiếp đến URL Override Dashboard
ExpectedRedirect về /voucher + toast "Bạn không có quyền". Menu Override Dashboard ẩn
Data
RefB5 Permission Matrix — Staff ❌

TC-P1-22: BOD override không branch scope

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P1-03
PreconditionLogin USR-BOD (BOD). VC-03 thuộc BR-02 bị chặn
Steps1. Tạo ĐH → chọn VC-03 → bị chặn
2. Override với lý do + ghi chú hợp lệ
ExpectedOverride thành công. BOD có quyền override tất cả branches (không branch scope)
DataVC-03, USR-BOD
RefB5 Permission Matrix — BOD ✅

TC-P1-23: Notification override gửi đúng recipients

Thuộc tínhGiá trị
PriorityP2
TypeFunctional
FRFR-P1-03
PreconditionOverride tạo thành công (OVR-01)
Steps1. Verify notification trigger sau override
Expectednoti_voucher_override_created gửi tới BOD + ITLeader. Template: "Voucher {code} được duyệt sử dụng sớm bởi {approved_by_name} tại {branch_name}. Lý do: {reason_note}". Dedupe: 1 lần/override
DataOVR-01
RefB4 Notification Spec

Nhóm 1.7: Edge Cases P1


TC-P1-24: Boundary time — 23h59m vs 24h01m

Thuộc tínhGiá trị
PriorityP0
TypeEdge Case
FRFR-P1-02
PreconditionVC activated lúc 10:00. min=24h. NOW() = 09:59 ngày hôm sau (23h59m)
Steps1. Tạo ĐH lúc 09:59 (23h59m elapsed) → verify chặn
2. Chờ đến 10:01 (24h01m elapsed) → tạo ĐH lại → verify cho phép
Expected23h59m < 24h → CHẶN (dialog hiện "Còn lại: 0 giờ 1 phút"). 24h01m > 24h → CHO PHÉP. So sánh chính xác, không round
Data
FormulaFORMULA-001 boundary
RefEC-P1-01, DEC-Q01

TC-P1-25: Timezone midnight — kích hoạt 23:30, chờ 24h

Thuộc tínhGiá trị
PriorityP0
TypeEdge Case
FRFR-P1-02
PreconditionVC-11: activated_at = 2026-03-19 23:30 (UTC+7). min=24h
Steps1. Tạo ĐH lúc 2026-03-20 22:00 (22h30m elapsed) → verify chặn
2. Tạo ĐH lúc 2026-03-20 23:30 (24h elapsed) → verify cho phép
Expected22:00 next day: 22.5h < 24h → CHẶN. 23:30 next day: 24.0h = 24h → CHO PHÉP (>= check). Timezone Vietnam (UTC+7), không lẫn UTC
DataVC-11
RefEC-P1-05, DEC-Q01

TC-P1-26: Campaign đã kết thúc nhưng voucher chưa expired

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P1-02
PreconditionVC-12: campaign CAMP-D đã ended. Voucher status = 'activated'
Steps1. Tạo ĐH → chọn VC-12
ExpectedVẫn check min_activation_hours. Campaign kết thúc KHÔNG bỏ qua time check. min_activation_hours vẫn áp dụng vì voucher còn active
DataVC-12, CAMP-D
RefEC-P1-03

TC-P1-27: Double override cùng voucher

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P1-03
PreconditionVC-01 đã có override OVR-01 (ĐH đầu bị hủy). Voucher quay lại status 'activated'
Steps1. Tạo ĐH mới với VC-01 → vẫn bị chặn (override cũ cho ĐH đã hủy)
2. Manager override lần 2
ExpectedOverride lần 2 tạo thành công. Mỗi override là record riêng biệt (OVR-03). ĐH mới có thể tạo
DataVC-01, OVR-01, OVR-03
RefEC-P1-04

TC-P1-28: Voucher manual — dùng system default khi không có campaign

Thuộc tínhGiá trị
PriorityP0
TypeEdge Case
FRFR-P1-02, FR-P1-05
PreconditionVC-08: voucher manual (campaign_id = NULL). DefaultMinActivationHours = 24
Steps1. Tạo ĐH với VC-08 (activated < 24h trước)
ExpectedChặn. Dùng AppSettings.VoucherSetting.DefaultMinActivationHours = 24 thay cho campaign config. Dialog hiện thời gian còn lại đúng
DataVC-08, app_setting
RefEC-P1-06, DEC-T08

Phase 2 — Thống kê nhân viên (16 TCs)

Nhóm 2.1: Báo cáo thống kê (FR-P2-01, FORMULA-002)


TC-P2-01: Bảng thống kê NV — hiển thị đúng dữ liệu

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P2-01
PreconditionLogin USR-ADMIN. Mở campaign CAMP-A → Tab "Thống kê NV"
Steps1. Verify bảng hiển thị
2. Check dữ liệu USR-NV1: đã phát 120, đã dùng 45, tỉ lệ 37.50%, DT 22.5 triệu
ExpectedBảng hiện: NV
DataCAMP-A, USR-NV1 đến USR-NV4
FormulaFORMULA-002: 45 / 120 × 100 = 37.50%
RefDEC-U02

TC-P2-02: Filter chi nhánh + thời gian + loại người phát

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P2-01
PreconditionLogin USR-ADMIN. Tab Thống kê NV mở
Steps1. Filter CN = "Q.1 HCM" → verify chỉ USR-NV1, USR-NV2, USR-NV4
2. Filter loại = "Đối tác" → verify chỉ AFF-01, AFF-02, AFF-03
3. Filter thời gian 01/03 - 15/03 → verify data trong khoảng
ExpectedFilter CN: 3 NV thuộc BR-01. Filter đối tác: chỉ hiện affiliates (distributor_type = 'affiliate'). Filter thời gian: chỉ voucher phát trong khoảng
DataCAMP-A
RefFR-P2-01, DEC-T03

Nhóm 2.2: Drill-down (FR-P2-02)


TC-P2-03: Drill-down chi tiết per NV

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P2-02
PreconditionLogin USR-ADMIN. Tab Thống kê NV, CAMP-A
Steps1. Click hàng USR-NV1 (Nguyễn A)
2. Verify StaffDrillDownDialog
ExpectedDialog hiện danh sách voucher của USR-NV1: mã VC, tên khách, ngày kích hoạt, trạng thái, DT. 120 vouchers phân trang. Cột: Mã voucher
DataUSR-NV1 vouchers
RefDEC-U03

TC-P2-04: Drill-down — voucher chưa có ĐH

Thuộc tínhGiá trị
PriorityP2
TypeEdge Case
FRFR-P2-02
PreconditionVoucher status = 'activated' (chưa redeem, chưa có ĐH)
Steps1. Drill-down NV → xem voucher activated
ExpectedCột DT hiện "—" (không hiện 0đ). Trạng thái hiện "Đã kích hoạt"
DataVC-01, VC-02
RefB6 State Matrix — Partial

Nhóm 2.3: Ranking (FR-P2-03)


TC-P2-05: Ranking NV — sắp xếp đa tiêu chí

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P2-03
PreconditionLogin USR-ADMIN. Tab Thống kê NV, CAMP-A
Steps1. Click header "Đã phát" → verify sort DESC
2. Click header "Tỉ lệ %" → verify sort DESC
3. Click header "Doanh thu" → verify sort DESC
ExpectedSort Đã phát: USR-NV1 (120) > USR-NV2 (80) > ... Sort Tỉ lệ: NV có conversion cao nhất lên đầu. Sort DT: NV có doanh thu cao nhất lên đầu. Top 10 highlight
DataCAMP-A staff
RefFR-P2-03

Nhóm 2.4: Partner App API (FR-P2-04)


TC-P2-06: Partner app — NV xem thống kê cá nhân

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P2-04
PreconditionLogin USR-NV1 trên Partner app
Steps1. Mở thống kê voucher cá nhân
ExpectedHiện: "Tháng này bạn phát 25 voucher, 8 đã dùng (32%), DT 4 triệu". Chỉ data của USR-NV1 (staff_id = X-Hasura-User-Id)
DataUSR-NV1
FormulaFORMULA-002 (self scope)
RefDEC-B03

TC-P2-07: Partner app — NV chưa phát voucher

Thuộc tínhGiá trị
PriorityP2
TypeEdge Case
FRFR-P2-04
PreconditionLogin USR-NV5 (chưa phát voucher nào) trên Partner app
Steps1. Mở thống kê voucher cá nhân
ExpectedEmpty state: "Bạn chưa phát voucher nào"
DataUSR-NV5
RefEC-P2-06, B6 State Matrix

Nhóm 2.5: Export (FR-P2-05)


TC-P2-08: Export Excel thống kê NV — sync (< 5000 rows)

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P2-05
PreconditionLogin USR-ADMIN. Tab Thống kê NV, kết quả < 5000 rows
Steps1. Click "Xuất Excel"
ExpectedFile Excel tải về ngay. Nội dung gồm: NV, CN, Đã phát, Đã dùng, Tỉ lệ %, DT, TB ngày dùng. Data khớp bảng hiển thị
DataCAMP-A staff
RefFR-P2-05

TC-P2-09: Export Excel — async (> 5000 rows)

Thuộc tínhGiá trị
PriorityP1
TypeNFR
FRFR-P2-05
PreconditionDataset > 5000 rows (hoặc mock)
Steps1. Click "Xuất Excel"
ExpectedTrigger async < 1s. Toast: "Đang xử lý, bạn sẽ nhận thông báo khi file sẵn sàng". File ready < 30s. Notification khi hoàn thành
DataLarge dataset mock
RefC9.1 NFR — export async

Nhóm 2.6: Permission Tests P2


TC-P2-10: ManagerCN chỉ thấy NV trong branch

Thuộc tínhGiá trị
PriorityP0
TypePermission
FRFR-P2-01
PreconditionLogin USR-MGR1 (ManagerCN BR-01)
Steps1. Mở Tab Thống kê NV
ExpectedChỉ hiện NV thuộc BR-01: USR-NV1, USR-NV2, USR-NV4. KHÔNG hiện USR-NV3 (BR-02). Nút "Xuất Excel" ẩn
DataCAMP-A
RefB5 Permission Matrix — Tab Thống kê NV: ManagerCN ✅(Branch), Export ❌

TC-P2-11: Staff không thấy Tab Thống kê NV

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P2-01
PreconditionLogin USR-NV1 (Staff)
Steps1. Mở campaign CAMP-A detail
ExpectedTab "Thống kê NV" ẩn trong tab list. Không hiện cho Staff
Data
RefB5 Permission Matrix — Tab Thống kê NV: Staff ❌, B6 — Tab ẩn

TC-P2-12: AccLeader xem all branches + export

Thuộc tínhGiá trị
PriorityP2
TypePermission
FRFR-P2-01, FR-P2-05
PreconditionLogin USR-ACC (AccLeader)
Steps1. Mở Tab Thống kê NV → verify xem all branches
2. Click "Xuất Excel"
ExpectedBảng hiện NV tất cả branches. Export OK
Data
RefB5 Permission Matrix — AccLeader ✅ All, Export ✅

Nhóm 2.7: Edge Cases P2


TC-P2-13: NV đã nghỉ việc vẫn hiện trong report

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P2-01
PreconditionUSR-NV4 đã deactivated nhưng có 30 voucher lịch sử
Steps1. Mở Tab Thống kê NV, CAMP-A
ExpectedUSR-NV4 (Hoàng D) vẫn hiện trong bảng với tên + data đầy đủ. Không xóa data lịch sử
DataUSR-NV4
RefEC-P2-01

TC-P2-14: NV chuyển branch mid-campaign

Thuộc tínhGiá trị
PriorityP2
TypeEdge Case
FRFR-P2-01
PreconditionNV phát 50 voucher ở BR-01, sau đó chuyển sang BR-02, phát thêm 20
Steps1. Filter BR-01 → verify 50 voucher
2. Filter BR-02 → verify 20 voucher
ExpectedReport theo branch_id tại thời điểm phát (lưu trong user_vouchers), KHÔNG theo branch hiện tại. BR-01 hiện 50, BR-02 hiện 20
Data
RefEC-P2-02

TC-P2-15: staff_id NULL — voucher online tự claim

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P2-01
PreconditionMột số voucher có staff_id = NULL (voucher online tự claim)
Steps1. Mở Tab Thống kê NV
ExpectedVoucher staff_id = NULL KHÔNG hiện trong report NV. SQL filter: staff_id IS NOT NULL. Tổng đã phát trên report < tổng voucher campaign
Data
RefEC-P2-03

TC-P2-16: Doanh thu = 0 cho voucher tặng miễn phí

Thuộc tínhGiá trị
PriorityP2
TypeEdge Case
FRFR-P2-01
PreconditionVoucher tặng dịch vụ miễn phí, DT = 0
Steps1. Xem thống kê NV phát voucher miễn phí
ExpectedCột DT hiện "0đ" (KHÔNG ẩn). Voucher miễn phí vẫn tính vào conversion rate. total_distributed, total_redeemed đều đếm voucher này
Data
FormulaFORMULA-002: 0đ vẫn tính vào total_redeemed nếu status='redeemed'
RefEC-P2-05

Phase 3 — Đối tác Affiliate (16 TCs)

Nhóm 3.1: Gán đối tác (FR-P3-01)


TC-P3-01: Gán đối tác vào campaign — happy path

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P3-01
PreconditionLogin USR-ADMIN. Tạo chiến dịch mới, step "Đối tác"
Steps1. Tick "Cho phép đối tác phát voucher chiến dịch này"
2. Tìm kiếm "KOL Hương" → chọn → set quota 200
3. Tìm "PK Đông Y" → chọn → set quota 500
4. Submit wizard
ExpectedCampaign tạo thành công. voucher_campaign_affiliates có 2 records: AFF-01 (quota=200), AFF-02 (quota=500). distributed_count = 0 cho cả 2. affiliate_action_log ghi 2 records action campaign_affiliate_assigned
DataAFF-01, AFF-02
RefDEC-B04, DEC-T04

TC-P3-02: Gán đối tác với quota unlimited

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P3-01
PreconditionLogin USR-ADMIN. Wizard step Đối tác
Steps1. Chọn KOL Minh (AFF-03)
2. Tick "Không giới hạn" (quota = NULL)
3. Submit
Expectedvoucher_campaign_affiliates.quota = NULL. UI hiện "Không giới hạn" thay vì số. Đối tác phát không bị chặn bao giờ
DataAFF-03
RefFORMULA-006 — quota IS NULL

Nhóm 3.2: Quota (FR-P3-02, FORMULA-006)


TC-P3-03: Quota check — phát đúng giới hạn

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P3-02
PreconditionAFF-01 trong CAMP-A: quota=100, distributed_count=98
Steps1. AFF-01 phát voucher thứ 99 → verify OK
2. AFF-01 phát voucher thứ 100 → verify OK
3. AFF-01 phát voucher thứ 101 → verify chặn
ExpectedPhát 99: OK (99/100). Phát 100: OK (100/100). Phát 101: CHẶN — error "Đối tác đã hết quota cho chiến dịch này". AFFILIATE_QUOTA_EXCEEDED
DataAFF-01, CAMP-A
FormulaFORMULA-006: distributed_count (100) >= quota (100) → REJECT
RefDEC-T04

TC-P3-04: Quota check — unlimited (NULL)

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P3-02
PreconditionAFF-03 trong CAMP-A: quota=NULL, distributed_count=75
Steps1. AFF-03 phát voucher liên tục (76, 77, ...)
ExpectedKhông bao giờ chặn. quota IS NULL → skip check. distributed_count tăng nhưng không có upper limit
DataAFF-03
FormulaFORMULA-006: quota IS NULL → unlimited
RefDEC-T04

TC-P3-05: Race condition — 2 requests concurrent khi quota còn 1

Thuộc tínhGiá trị
PriorityP0
TypeEdge Case
FRFR-P3-02
PreconditionAFF-01: quota=100, distributed_count=99. 2 requests phát voucher đồng thời
Steps1. Gửi 2 request phát voucher song song (concurrent) cho AFF-01
ExpectedChỉ 1 request thành công (distributed_count=100). Request còn lại nhận AFFILIATE_QUOTA_EXCEEDED. Atomic UPDATE: WHERE distributed_count < quota → chỉ 1 row affected
DataAFF-01
FormulaFORMULA-006 — atomic increment
RefDEC-Q02, EC-P3-02

Nhóm 3.3: Report phân biệt NV vs đối tác (FR-P3-03)


TC-P3-06: Cột "Loại" phân biệt NV vs đối tác

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P3-03
PreconditionTab Thống kê NV, CAMP-A (có cả NV + affiliate)
Steps1. Mở Tab Thống kê NV
2. Verify cột "Loại"
ExpectedUSR-NV1: Loại = "Nhân viên". AFF-01: Loại = "Đối tác". Cột distributor_type: 'internal_staff' → "Nhân viên", 'affiliate' → "Đối tác"
DataCAMP-A
RefDEC-T03

TC-P3-07: Filter "Chỉ xem đối tác"

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P3-03
PreconditionTab Thống kê NV, CAMP-A
Steps1. Filter "Loại người phát" = "Đối tác"
ExpectedChỉ hiện AFF-01, AFF-02, AFF-03. Ẩn tất cả NV nội bộ
DataCAMP-A affiliates
RefFR-P3-03

Nhóm 3.4: Badge (FR-P3-04)


TC-P3-08: Badge "Voucher do đối tác X phát" khi tạo ĐH

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P3-04
PreconditionLogin USR-NV1. Tạo ĐH, chọn voucher VC-09 (phát bởi AFF-01 KOL Hương)
Steps1. Tạo ĐH → chọn voucher VC-09
ExpectedBadge hiển thị: "Voucher do KOL Hương phát". AffiliateSourceBadge component render đúng tên đối tác
DataVC-09, AFF-01
RefDEC-B03

Nhóm 3.5: Log gán/gỡ (FR-P3-05)


TC-P3-09: Log gán/gỡ đối tác vào affiliate_action_log

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P3-05
PreconditionAdmin tạo campaign + gán 2 đối tác, sau đó gỡ 1
Steps1. Gán AFF-01, AFF-02 → verify log
2. Gỡ AFF-02 (soft delete) → verify log
Expectedaffiliate_action_log:
- campaign_affiliate_assigned × 2 (với metadata campaign_id, affiliate_user_id)
- campaign_affiliate_removed × 1. Mỗi log có user_id, created_at
DataAFF-01, AFF-02
RefDEC-T04

Nhóm 3.6: Permission Tests P3


TC-P3-10: AccLeader không truy cập Wizard Đối tác

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P3-01
PreconditionLogin USR-ACC (AccLeader)
Steps1. Mở tạo/sửa campaign wizard
ExpectedStep "Đối tác" ẩn trong wizard. AccLeader không có quyền gán đối tác. Chỉ ITLeader, ITStaff, BOD thấy step
Data
RefB5 Permission Matrix — Wizard Đối tác: AccLeader ❌

TC-P3-11: Affiliate không thấy badge đối tác

Thuộc tínhGiá trị
PriorityP2
TypePermission
FRFR-P3-04
PreconditionLogin user role Affiliate (nếu có truy cập tạo ĐH)
Steps1. Verify badge visibility
ExpectedBadge "Voucher do đối tác X phát" ẩn cho Affiliate role. Tất cả role khác (ITLeader → Staff) đều thấy
Data
RefB5 Permission Matrix — Badge đối tác: Affiliate ❌

Nhóm 3.7: Edge Cases P3


TC-P3-12: Đối tác bị deactivated giữa chiến dịch

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P3-02
PreconditionAFF-01 đang active trong CAMP-A. Admin set is_active=false
Steps1. AFF-01 cố phát voucher sau khi bị deactivated
ExpectedChặn: "Đối tác đã ngừng hoạt động". Voucher đã phát trước đó vẫn hợp lệ (không recall). Report vẫn hiện AFF-01 với data lịch sử
DataAFF-01
RefEC-P3-01

TC-P3-13: Admin tăng quota giữa chiến dịch

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P3-02
PreconditionAFF-01: quota=100, distributed_count=100 (hết quota)
Steps1. Admin sửa quota AFF-01 → 150
2. AFF-01 phát voucher thứ 101
ExpectedPhát thành công. distributed_count = 101 < 150. Upsert update quota ngay lập tức, đối tác phát tiếp không cần chờ
DataAFF-01
RefEC-P3-03, DEC-T04

TC-P3-14: Soft delete đối tác — voucher đã phát vẫn giữ

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P3-05
PreconditionAFF-02 đã phát 50 voucher trong CAMP-A
Steps1. Admin gỡ AFF-02 khỏi CAMP-A
2. Verify voucher đã phát + report
Expectedvoucher_campaign_affiliates.deleted_at set. 50 voucher đã phát vẫn giữ staff_id = AFF-02. Report vẫn hiện AFF-02 với 50 voucher lịch sử. Wizard không hiện AFF-02 (đã gỡ)
DataAFF-02
RefEC-P3-04

TC-P3-15: Campaign không gán đối tác — flow bình thường

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P3-01
PreconditionCAMP-B không gán đối tác nào
Steps1. NV phát voucher CAMP-B bình thường
ExpectedFlow hoạt động bình thường. Không có affiliate check. distributor_type = 'internal_staff'. Tab Thống kê NV không hiện đối tác
DataCAMP-B
RefEC-P3-05

TC-P3-16: Đối tác phát voucher cho campaign KHÔNG được gán

Thuộc tínhGiá trị
PriorityP0
TypeEdge Case
FRFR-P3-02
PreconditionAFF-01 được gán CAMP-A nhưng KHÔNG gán CAMP-B
Steps1. AFF-01 cố phát voucher cho CAMP-B
ExpectedTừ chối: "Đối tác không được phép phát voucher chiến dịch này". Check voucher_campaign_affiliates WHERE campaign_id AND affiliate_user_id → not found → reject
DataAFF-01, CAMP-B
RefEC-P3-06

Phase 4 — Chu kỳ sử dụng (16 TCs)

Nhóm 4.1: KPI tổng quan (FR-P4-01, FORMULA-003, FORMULA-004)


TC-P4-01: KPI cards hiển thị đúng — happy path

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P4-01
PreconditionLogin USR-ADMIN. CAMP-A → Tab "Chu kỳ sử dụng". Seed data: vouchers với cycle 1, 3, 5, 8, 9, 14, 20, 30 ngày
Steps1. Mở Tab Chu kỳ sử dụng
2. Verify 4 KPI cards
ExpectedTB: 11.3 ngày (avg). Nhanh nhất: 1 ngày (min). Trung vị: 8.5 ngày (median giữa 8 và 9). Tỉ lệ dùng: tính từ total_redeemed/total_activated
DataCAMP-A voucher_logs
FormulaFORMULA-003: cycle_days per voucher. FORMULA-004: PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY cycle_days)
RefDEC-T05, DEC-T06

TC-P4-02: Verify FORMULA-003 — cycle days tính từ voucher_logs

Thuộc tínhGiá trị
PriorityP0
TypeFormula
FRFR-P4-01
PreconditionVC-05: activated 01/03 10:00, redeem log 09/03 14:00. VC-13: activated 15/03 10:00, redeem → restore → redeem 18/03 10:00
Steps1. Query get_voucher_usage_cycle_statistics cho CAMP-A
2. Verify cycle calculation
ExpectedVC-05: cycle = (09/03 14:00 - 01/03 10:00) / 86400 = 8.17 → 8 ngày. VC-13: cycle = (18/03 10:00 - 15/03 10:00) / 86400 = 3.0 → 3 ngày (lấy lần redeem CUỐI CÙNG, không phải lần đầu 16/03)
DataVC-05, VC-13, voucher_logs
FormulaFORMULA-003: DISTINCT ON voucher_id, ORDER BY created_at DESC WHERE action='voucher_redeemed'
RefDEC-T05

Nhóm 4.2: Bucket Distribution (FR-P4-02, FORMULA-005)


TC-P4-03: Bucket chart hiển thị đúng phân bổ

Thuộc tínhGiá trị
PriorityP0
TypeFunctional
FRFR-P4-02
PreconditionCAMP-A: 200 vouchers, phân bổ cycle biết trước
Steps1. Mở Tab Chu kỳ sử dụng
2. Verify horizontal bar chart
Expected7 buckets hiển thị: 0-3 ngày, 4-7, 8-14, 15-30, 31-60, 60+, Chưa dùng. Mỗi bar hiện count + percentage. Tổng tất cả bars = 200 (total vouchers)
DataCAMP-A
FormulaFORMULA-005: percentage = bucket_count / total_count × 100 (1 decimal)
RefDEC-T07, DEC-U04

TC-P4-04: Verify FORMULA-005 — bucket boundary chính xác

Thuộc tínhGiá trị
PriorityP1
TypeFormula
FRFR-P4-02
PreconditionVouchers với cycle_days: 0, 3, 3.9, 4, 7, 14, 30, 31, 60, 61
Steps1. Verify bucket assignment cho từng voucher
Expectedcycle=0 → "0-3". cycle=3.9 → "0-3". cycle=4 → "4-7". cycle=14 → "8-14". cycle=30 → "15-30". cycle=31 → "31-60". cycle=60 → "31-60". cycle=61 → "60+". SQL CASE WHEN: BETWEEN 0 AND 3, BETWEEN 4 AND 7, etc.
DataCustom voucher_logs
RefFORMULA-005, dev-spec bucketed CTE

Nhóm 4.3: So sánh chiến dịch (FR-P4-03)


TC-P4-05: So sánh 2 chiến dịch — chart chồng

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P4-03
PreconditionLogin USR-ADMIN. Tab Chu kỳ, cả CAMP-A và CAMP-E có data
Steps1. Chọn CAMP-A + CAMP-E để so sánh
2. Verify chart
ExpectedBar chart hiện 2 series chồng nhau (CAMP-A vs CAMP-E). Mỗi bucket hiện 2 bars. VD: CAMP-A "0-3 ngày" = 28%, CAMP-E "0-3 ngày" = 15%. Legend hiện tên chiến dịch
DataCAMP-A, CAMP-E
RefFR-P4-03

TC-P4-06: So sánh chiến dịch — 1 có data, 1 không

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P4-03
PreconditionCAMP-A có data, CAMP-C không có voucher redeemed (chỉ 50 activated)
Steps1. Chọn CAMP-A + CAMP-C so sánh
ExpectedCAMP-A: hiện data bình thường. CAMP-C: KPI hiện "—" cho avg/min/median. Bar chart: CAMP-C tất cả buckets = 0% trừ "Chưa dùng" = 100%
DataCAMP-A, CAMP-C
RefEC-P4-05

Nhóm 4.4: Filter + Group by (FR-P4-04)


TC-P4-07: Filter theo chiến dịch + chi nhánh + thời gian

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P4-04
PreconditionTab Chu kỳ sử dụng
Steps1. Filter chiến dịch = CAMP-A
2. Filter CN = BR-01
3. Filter thời gian: 01/03 - 15/03
4. Verify KPI + chart cập nhật
ExpectedKPI + chart chỉ tính vouchers khớp filter (CAMP-A, BR-01, activated trong khoảng thời gian). Số liệu thay đổi so với không filter
DataCAMP-A, BR-01
RefFR-P4-04

TC-P4-08: Group by dimension (campaign / branch / staff / overall)

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P4-04
PreconditionTab Chu kỳ, không filter
Steps1. Group by "Tổng quan" → verify 1 row tổng
2. Group by "Chiến dịch" → verify N rows (per campaign)
3. Group by "Chi nhánh" → verify rows per branch
4. Group by "Nhân viên" → verify rows per staff
ExpectedMỗi group_by tạo rows khác nhau. KPI cards hiện dữ liệu tổng quan. Bảng breakdown hiện theo dimension chọn
DataAll data
RefFR-P4-04

Nhóm 4.5: Export (FR-P4-05)


TC-P4-09: Export Excel chu kỳ sử dụng

Thuộc tínhGiá trị
PriorityP1
TypeFunctional
FRFR-P4-05
PreconditionLogin USR-ADMIN. Tab Chu kỳ với data
Steps1. Click "Xuất Excel"
ExpectedFile Excel chứa: Sheet 1 — Summary (KPI cards data). Sheet 2 — Chi tiết (bảng breakdown theo filter). Async nếu data lớn
DataCAMP-A
RefFR-P4-05

Nhóm 4.6: Permission Tests P4


TC-P4-10: ManagerCN xem Tab Chu kỳ — branch scope

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P4-01
PreconditionLogin USR-MGR1 (ManagerCN BR-01)
Steps1. Mở CAMP-A → Tab Chu kỳ sử dụng
ExpectedKPI + chart chỉ tính vouchers thuộc BR-01. Nút "Xuất Excel" ẩn. So sánh chiến dịch hoạt động bình thường (scope branch)
DataCAMP-A, BR-01
RefB5 Permission Matrix — Tab Chu kỳ: ManagerCN ✅(Branch), Export ❌

TC-P4-11: Staff không thấy Tab Chu kỳ

Thuộc tínhGiá trị
PriorityP1
TypePermission
FRFR-P4-01
PreconditionLogin USR-NV1 (Staff)
Steps1. Mở campaign detail
ExpectedTab "Chu kỳ sử dụng" ẩn trong tab list
Data
RefB5 Permission Matrix — Tab Chu kỳ: Staff ❌

Nhóm 4.7: Edge Cases P4


TC-P4-12: 100% voucher pending (chưa ai dùng)

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P4-01, FR-P4-02
PreconditionCampaign mới, tất cả voucher status='activated'
Steps1. Mở Tab Chu kỳ
ExpectedKPI avg/min/median hiện "—". Tỉ lệ dùng = 0%. Bar chart: "Chưa dùng" = 100%, tất cả bucket khác = 0%
Data
RefEC-P4-01, B6 State Matrix — Partial

TC-P4-13: Voucher redeem → restore → redeem (net cycle)

Thuộc tínhGiá trị
PriorityP0
TypeEdge Case
FRFR-P4-01
PreconditionVC-13: activated 15/03 10:00. Logs: redeem 16/03 12:00, restore 16/03 14:00, redeem 18/03 10:00
Steps1. Query cycle stats cho VC-13
ExpectedCycle = 18/03 10:00 - 15/03 10:00 = 3.0 ngày. Dùng lần redeem CUỐI CÙNG (18/03), KHÔNG dùng lần đầu (16/03). DISTINCT ON (voucher_id) WHERE action='voucher_redeemed' ORDER BY created_at DESC
DataVC-13, voucher_logs
FormulaFORMULA-003 — net redeem từ voucher_logs
RefEC-P4-02, DEC-T05

TC-P4-14: Cycle = 0 (kích hoạt và dùng cùng ngày)

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P4-01, FR-P4-02
PreconditionVoucher activated 10:00, redeemed 10:30 cùng ngày (campaign min=0)
Steps1. Verify cycle calculation
2. Verify bucket assignment
ExpectedCycle = 0 ngày (hợp lệ). Nằm trong bucket "0-3 ngày". KPI min = 0. Không báo lỗi
Data
RefEC-P4-03

TC-P4-15: Performance — > 100k vouchers

Thuộc tínhGiá trị
PriorityP2
TypeNFR
FRFR-P4-04
PreconditionDataset > 100k vouchers (staging environment)
Steps1. Query get_voucher_usage_cycle_statistics
2. Query get_voucher_usage_cycle_distribution
3. Đo response time
ExpectedStatistics: < 3s. Distribution: < 3s. FE chỉ nhận kết quả đã aggregate (~10-50 rows), không nhận raw data. DB-level aggregation
DataLarge dataset (staging)
RefEC-P4-04, C9.1 NFR

TC-P4-16: PERCENTILE_CONT trả NULL khi không có redeem

Thuộc tínhGiá trị
PriorityP1
TypeEdge Case
FRFR-P4-01
PreconditionFilter kết quả: tất cả voucher pending (không có redeem nào)
Steps1. Query cycle statistics
ExpectedPERCENTILE_CONT trả NULL (không có giá trị cycle_days). Median hiện "—" trên UI. Không lỗi SQL, không hiện NaN
Data
FormulaFORMULA-004 — empty dataset → NULL
RefEC-P4-06, DEC-T06

D5. Entry / Exit Criteria

5.1 Entry Criteria (bắt đầu test)

#Tiêu chíKiểm traPhase
EN-01Migration deploy thành công (không lỗi rollback)hasura migrate status — no pendingMỗi phase
EN-02Hasura metadata apply thành cônghasura metadata apply — no errorsMỗi phase
EN-03Go handler compile + unit test passgo test ./... — 0 failuresP1, P3
EN-04Seed data load thành công (D3)SQL seed script chạy không lỗiAll
EN-05FE build thành công (no TS errors)npm run build — exit 0Mỗi phase
EN-06Dev self-test pass (happy path chính)Dev xác nhận đã test localMỗi phase
EN-07PERF-FIX deploy thành côngExpression index + merge CTEs appliedP2, P4
EN-08P2 deploy thành côngStaff statistics function availableP3 (dependency)

5.2 Exit Criteria (kết thúc test)

#Tiêu chíNgưỡngPhase
EX-01Tất cả TC P0 PASS100% (0 fail)Mỗi phase
EX-02Tất cả TC P1 PASS>= 95% (cho phép 1 known issue với workaround)Mỗi phase
EX-03Tất cả TC P2 PASS>= 90% (known issues documented)Mỗi phase
EX-04Không có bug Blocker / Critical mở0 Blocker, 0 CriticalMỗi phase
EX-05Permission test PASS 100%Tất cả role × action đều verifyMỗi phase
EX-06Formula verification PASSFORMULA-001 đến FORMULA-006 tất cả ví dụ đúngAll
EX-07NFR performance đạt targetTime validation < 50ms, Statistics < 2s, Cycle < 3sP1, P2, P4
EX-08Edge case race condition verifyTC-P3-05 (concurrent quota) PASSP3
EX-09Regression test passFlow tạo ĐH hiện có không bị ảnh hưởngP1
EX-10Export test passSync + async export hoạt động đúngP2, P4

5.3 Test Environment

AspectChi tiết
EnvironmentStaging (mirror production config)
DatabasePostgreSQL 14+ (verify PERCENTILE_CONT support)
TimezoneServer: UTC. App: Asia/Ho_Chi_Minh (UTC+7)
BrowsersChrome (latest), Safari (latest) — theo standard QA suite
Seed dataD3 seed data + production-like volume cho NFR tests
ToolsPostman (API testing), Hasura Console (GraphQL), DevTools (Network)

5.4 Bug Severity Definition

SeverityMô tảVí dụ trong feature này
BlockerKhông thể tiếp tục test, chặn flow chínhTạo ĐH crash khi có voucher. Migration fail
CriticalFeature chính không hoạt độngChặn thời gian không work. Override tạo nhưng ĐH vẫn bị chặn. Race condition quota
MajorFeature phụ bị lỗi hoặc data saiThống kê tính sai conversion rate. Bucket phân bổ sai
MinorUI/UX nhỏ, không ảnh hưởng logicCopy text sai. Tooltip không hiện. Sort không đúng chiều
TrivialCosmeticSpacing, alignment nhỏ

5.5 Test Execution Order

Phase 1 (P0 — Urgent) ─────────────────────────────────────────────
  Nhóm 1.1-1.2: Config + Chặn         → TC-P1-01 đến TC-P1-06
  Nhóm 1.3: Override                    → TC-P1-07 đến TC-P1-10
  Nhóm 1.4-1.5: Dashboard + Settings   → TC-P1-11 đến TC-P1-19
  Nhóm 1.6: Permission                  → TC-P1-20 đến TC-P1-23
  Nhóm 1.7: Edge Cases                  → TC-P1-24 đến TC-P1-28
  ► EXIT GATE: EX-01 đến EX-09 cho P1

Phase 2 (P1 — High) ───────────────────────────────────────────────
  Prerequisite: PERF-FIX deployed
  Nhóm 2.1-2.5: Functional             → TC-P2-01 đến TC-P2-09
  Nhóm 2.6: Permission                  → TC-P2-10 đến TC-P2-12
  Nhóm 2.7: Edge Cases                  → TC-P2-13 đến TC-P2-16
  ► EXIT GATE: EX-01 đến EX-10 cho P2

Phase 3 (P1 — High) ───────────────────────────────────────────────
  Prerequisite: P2 deployed
  Nhóm 3.1-3.5: Functional             → TC-P3-01 đến TC-P3-09
  Nhóm 3.6: Permission                  → TC-P3-10 đến TC-P3-11
  Nhóm 3.7: Edge Cases                  → TC-P3-12 đến TC-P3-16
  ► EXIT GATE: EX-01 đến EX-08 cho P3

Phase 4 (P2 — Medium) ─────────────────────────────────────────────
  Prerequisite: PERF-FIX deployed
  Nhóm 4.1-4.5: Functional             → TC-P4-01 đến TC-P4-09
  Nhóm 4.6: Permission                  → TC-P4-10 đến TC-P4-11
  Nhóm 4.7: Edge Cases                  → TC-P4-12 đến TC-P4-16
  ► EXIT GATE: EX-01 đến EX-10 cho P4

Traceability Summary

LoạiTổngCoverage
FR → TC22 FRs → 76 TCs100% — mọi FR có ít nhất 1 TC
FORMULA → TC6 formulas → 12 TCs100% — mỗi formula có verify TC + edge case TC
Permission → TC12 role×action combos → 11 TCs100% — mọi permission rule verify
Edge Case (UI-spec) → TC26/30 edge cases covered87% — 4 trivial cases merged vào TC chính
Decision → TCDEC-Q01 → TC-P1-24/25, DEC-Q02 → TC-P3-05100% QA decisions covered

Changelog

VersionNgàyThay đổi
1.02026-03-19Initial — D1-D5, 76 test cases across 4 phases, 6 formula verifications