Appearance
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
| Phase | Priority | Mô tả | Số TC | Risk Level |
|---|---|---|---|---|
| Phase 1 | P0 — Urgent | Kiểm soát thời gian kích hoạt → sử dụng + Override + Settings | 28 | High — ảnh hưởng flow tạo ĐH |
| Phase 2 | P1 — High | Thống kê nhân viên phát voucher + Export + Partner API | 16 | Medium |
| Phase 3 | P1 — High | Đối tác Affiliate — gán, quota, badge | 16 | High — race condition quota |
| Phase 4 | P2 — Medium | Chu kỳ sử dụng — KPI, bucket chart, so sánh | 16 | Medium |
| Tổng | 76 |
1.2 Loại test
| Loại | Mô tả | Phase áp dụng | Tỉ lệ |
|---|---|---|---|
| Functional | Verify FRs + ACs đúng yêu cầu | P1-P4 | 55% |
| Permission / RBAC | Verify role × action × branch scope | P1-P4 | 15% |
| Edge Case | Boundary, concurrent, timezone, NULL | P1-P4 | 15% |
| Formula / Calculation | Verify FORMULA-001 đến FORMULA-006 | P1-P4 | 10% |
| NFR / Performance | Response time, async export, scale | P2, P4 | 5% |
1.3 Ngoài phạm vi
| Nội dung | Lý do |
|---|---|
| UI/UX visual design (color, font, spacing) | QA chỉ verify layout + state, không verify pixel-perfect |
| Load testing > 100k concurrent users | Dùng performance test riêng (staging environment) |
| Cross-browser testing | Covered bởi QA standard regression suite |
| Module Affiliate CRUD | Module 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 ID | Mô tả | Screen | API / Handler | Test Case IDs | Formula |
|---|---|---|---|---|---|
| FR-P1-01 | Config min_activation_hours per campaign | VoucherConfigForm | voucher_campaigns mutation | TC-P1-01, TC-P1-02 | — |
| FR-P1-02 | Chặn tạo ĐH khi voucher chưa đủ thời gian | VoucherTimeBlockDialog | validateOrderVouchers() | TC-P1-03, TC-P1-04, TC-P1-05, TC-P1-06 | FORMULA-001 |
| FR-P1-03 | Manager override | VoucherTimeBlockDialog | approve_voucher_override | TC-P1-07, TC-P1-08, TC-P1-09, TC-P1-10 | — |
| FR-P1-04 | Dashboard giám sát override | VoucherOverrideDashboard | GetVoucherOverrides query | TC-P1-11, TC-P1-12, TC-P1-13 | — |
| FR-P1-05 | Config mặc định thời gian chờ Settings | VoucherSettingsSection | app_setting mutation | TC-P1-14, TC-P1-15 | — |
| FR-P1-06 | Thời hạn voucher manual | — (backend) | activate_offline_voucher | TC-P1-16, TC-P1-17, TC-P1-18 | — |
| FR-P1-07 | Hiển thị hạn sử dụng Partner app/POS | Partner app | user_vouchers query | TC-P1-19 | — |
| — | Permission tests P1 | All P1 screens | — | TC-P1-20, TC-P1-21, TC-P1-22, TC-P1-23 | — |
| — | Edge cases P1 | — | — | TC-P1-24, TC-P1-25, TC-P1-26, TC-P1-27, TC-P1-28 | FORMULA-001 |
Phase 2 — Thống kê nhân viên
| FR ID | Mô tả | Screen | API / Function | Test Case IDs | Formula |
|---|---|---|---|---|---|
| FR-P2-01 | Báo cáo thống kê per NV | VoucherStaffStatisticsTab | get_voucher_staff_statistics | TC-P2-01, TC-P2-02 | FORMULA-002 |
| FR-P2-02 | Drill-down chi tiết per NV | StaffDrillDownDialog | GetStaffVoucherDetail query | TC-P2-03, TC-P2-04 | — |
| FR-P2-03 | Ranking NV | VoucherStaffStatisticsTab | get_voucher_staff_statistics (ORDER BY) | TC-P2-05 | — |
| FR-P2-04 | API thống kê cá nhân Partner app | Partner app | get_voucher_staff_statistics (self) | TC-P2-06, TC-P2-07 | FORMULA-002 |
| FR-P2-05 | Export Excel | VoucherStaffStatisticsTab | export-api | TC-P2-08, TC-P2-09 | — |
| — | Permission tests P2 | — | — | TC-P2-10, TC-P2-11, TC-P2-12 | — |
| — | Edge cases P2 | — | — | TC-P2-13, TC-P2-14, TC-P2-15, TC-P2-16 | FORMULA-002 |
Phase 3 — Đối tác Affiliate
| FR ID | Mô tả | Screen | API / Handler | Test Case IDs | Formula |
|---|---|---|---|---|---|
| FR-P3-01 | Gán đối tác vào campaign | AffiliateAssignStep | voucher_campaign_affiliates mutation | TC-P3-01, TC-P3-02 | — |
| FR-P3-02 | Quota per đối tác | — (backend) | activate_offline_voucher + quota check | TC-P3-03, TC-P3-04, TC-P3-05 | FORMULA-006 |
| FR-P3-03 | Phân biệt NV vs đối tác trong report | VoucherStaffStatisticsTab | get_voucher_staff_statistics | TC-P3-06, TC-P3-07 | — |
| FR-P3-04 | Badge "Voucher do đối tác X phát" | AffiliateSourceBadge | user_vouchers query | TC-P3-08 | — |
| FR-P3-05 | Log gán/gỡ đối tác | — (backend) | affiliate_action_log insert | TC-P3-09 | — |
| — | Permission tests P3 | — | — | TC-P3-10, TC-P3-11 | — |
| — | Edge cases P3 | — | — | TC-P3-12, TC-P3-13, TC-P3-14, TC-P3-15, TC-P3-16 | FORMULA-006 |
Phase 4 — Chu kỳ sử dụng
| FR ID | Mô tả | Screen | API / Function | Test Case IDs | Formula |
|---|---|---|---|---|---|
| FR-P4-01 | KPI tổng quan | VoucherUsageCycleTab | get_voucher_usage_cycle_statistics | TC-P4-01, TC-P4-02 | FORMULA-003, FORMULA-004 |
| FR-P4-02 | Phân bổ theo bucket | CycleBucketChart | get_voucher_usage_cycle_distribution | TC-P4-03, TC-P4-04 | FORMULA-005 |
| FR-P4-03 | So sánh chiến dịch | VoucherUsageCycleTab | get_voucher_usage_cycle_statistics (multi) | TC-P4-05, TC-P4-06 | — |
| FR-P4-04 | Filter + Group by | VoucherUsageCycleTab | function params | TC-P4-07, TC-P4-08 | — |
| FR-P4-05 | Export Excel chu kỳ | VoucherUsageCycleTab | export-api | TC-P4-09 | — |
| — | Permission tests P4 | — | — | TC-P4-10, TC-P4-11 | — |
| — | Edge cases P4 | — | — | TC-P4-12, TC-P4-13, TC-P4-14, TC-P4-15, TC-P4-16 | FORMULA-003, FORMULA-004, FORMULA-005 |
D3. Seed Data
3.1 Campaigns
| ID (alias) | Tên | min_activation_hours | Trạng thái | Vouchers | Affiliates | Phase |
|---|---|---|---|---|---|---|
| CAMP-A | Tết Kim Cương 2026 | 24 | Active | 500 | 3 (KOL Hương 100, PK Đông Y 200, KOL Minh unlimited) | P1-P4 |
| CAMP-B | Sinh nhật Diva 2026 | 48 | Active | 200 | 0 | P1-P2 |
| CAMP-C | Ưu đãi VIP | 0 (no check) | Active | 50 | 0 | P1 edge case |
| CAMP-D | Hè Rực Rỡ (ended) | 24 | Ended | 100 | 0 | P1 edge case |
| CAMP-E | Test Chu Kỳ | 24 | Active | 300 | 2 | P4 so sánh |
3.2 Chi nhánh (Branches)
| ID (alias) | Tên | Ghi chú |
|---|---|---|
| BR-01 | Q.1 HCM | Branch chính |
| BR-02 | Q.3 HCM | Branch phụ |
| BR-03 | Đà Nẵng | Branch khác vùng |
3.3 Users (Staff + Manager + Admin)
| ID (alias) | Tên | Role | Branch | Ghi chú |
|---|---|---|---|---|
| USR-ADMIN | Lê Admin | ITLeader | All | Full access |
| USR-BOD | Trần BOD | BOD | All | Full access, no Settings config |
| USR-MGR1 | Nguyễn Manager Q1 | ManagerCN | BR-01 | Override quyền cho BR-01 |
| USR-MGR2 | Phạm Manager Q3 | ManagerCN | BR-02 | Override quyền cho BR-02 |
| USR-ACC | Vũ Kế Toán | AccLeader | All | Xem report, export, không override |
| USR-NV1 | Nguyễn A | Staff | BR-01 | Phát 120 voucher (45 redeemed, 15 expired) |
| USR-NV2 | Trần B | Staff | BR-01 | Phát 80 voucher (30 redeemed) |
| USR-NV3 | Lê C | Staff | BR-02 | Phát 50 voucher (10 redeemed) |
| USR-NV4 | Hoàng D | Staff (deactivated) | BR-01 | Đã nghỉ việc, có 30 voucher lịch sử |
| USR-NV5 | Đỗ E | Staff | BR-03 | Phát 0 voucher (test empty) |
3.4 Affiliates
| ID (alias) | Tên | Quota trong CAMP-A | distributed_count | Ghi chú |
|---|---|---|---|---|
| AFF-01 | KOL Hương | 100 | 98 | Gần hết quota (test boundary) |
| AFF-02 | PK Đông Y | 200 | 50 | Còn nhiều quota |
| AFF-03 | KOL Minh | NULL (unlimited) | 75 | Unlimited quota |
3.5 Vouchers (user_vouchers) — key records
| ID (alias) | Campaign | Staff | Status | activated_at | Mục đích test |
|---|---|---|---|---|---|
| VC-01 | CAMP-A | USR-NV1 | activated | 2026-03-18 10:00 | Chặn (< 24h) |
| VC-02 | CAMP-A | USR-NV1 | activated | 2026-03-17 08:00 | Đủ thời gian (> 24h) |
| VC-03 | CAMP-B | USR-NV2 | activated | 2026-03-18 14:00 | Chặn (< 48h) |
| VC-04 | CAMP-C | USR-NV1 | activated | 2026-03-19 09:00 | min=0, không chặn |
| VC-05 | CAMP-A | USR-NV1 | redeemed | 2026-03-01 10:00 | Cycle = 8 ngày |
| VC-06 | CAMP-A | USR-NV2 | redeemed | 2026-03-05 10:00 | Cycle = 3 ngày |
| VC-07 | CAMP-A | USR-NV3 | expired | 2026-02-28 10:00 | Expired voucher |
| VC-08 | NULL | USR-NV1 | activated | 2026-03-18 15:00 | Voucher manual (no campaign) |
| VC-09 | CAMP-A | AFF-01 | activated | 2026-03-17 10:00 | Affiliate voucher |
| VC-10 | CAMP-A | AFF-02 | redeemed | 2026-03-10 10:00 | Affiliate, cycle test |
| VC-11 | CAMP-A | USR-NV1 | activated | 2026-03-19 23:30 | Timezone boundary (midnight) |
| VC-12 | CAMP-D | USR-NV1 | activated | 2026-03-18 10:00 | Campaign ended, voucher active |
| VC-13 | CAMP-A | USR-NV1 | redeemed | 2026-03-15 10:00 | Redeemed → restored → redeemed |
| VC-14 | NULL | USR-NV2 | activated | 2026-03-01 10:00 | Manual, sắp hết hạn |
3.6 Override Records
| ID (alias) | Voucher | Approved By | reason_code | hours_remaining | Mục đích test |
|---|---|---|---|---|---|
| OVR-01 | VC-01 | USR-MGR1 | vip_customer | 22.15 | Override chuẩn |
| OVR-02 | VC-03 | USR-MGR2 | special_event | 40.50 | Override khác branch |
| OVR-03 | VC-01 | USR-ADMIN | manager_directive | 18.00 | Double override cùng voucher |
| OVR-04 | VC-12 | USR-MGR1 | system_error | 12.30 | Campaign ended |
| OVR-05 | VC-08 | USR-BOD | other | 20.00 | Manual voucher override |
3.7 Voucher Logs (cho cycle testing)
| Voucher | Action | created_at | Ghi chú |
|---|---|---|---|
| VC-05 | voucher_redeemed | 2026-03-09 14:00 | Cycle = 8.17 ngày |
| VC-06 | voucher_redeemed | 2026-03-08 10:00 | Cycle = 3.0 ngày |
| VC-13 | voucher_redeemed | 2026-03-16 12:00 | Lần redeem đầu |
| VC-13 | voucher_restored | 2026-03-16 14:00 | ĐH hủy → restore |
| VC-13 | voucher_redeemed | 2026-03-18 10:00 | Redeem lại — lấy lần cuối |
| VC-10 | voucher_redeemed | 2026-03-14 10:00 | Affiliate 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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P1-01 |
| Precondition | Login USR-ADMIN. Mở tạo chiến dịch voucher |
| Steps | 1. 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 |
| Expected | Campaign 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" |
| Data | CAMP-B (min_activation_hours=48) |
| Ref | DEC-B01, DEC-B02 |
TC-P1-02: Validation input min_activation_hours
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-01 |
| Precondition | Login USR-ADMIN. Mở tạo chiến dịch voucher, step "Cấu hình" |
| Steps | 1. 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 | — |
| Ref | DEC-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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P1-02 |
| Precondition | Login USR-NV1 (Staff, BR-01). VC-01: activated 2h trước, CAMP-A min=24h |
| Steps | 1. Tạo đơn hàng mới 2. Chọn voucher VC-01 3. Submit tạo ĐH |
| Expected | Hệ 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) |
| Data | VC-01, CAMP-A |
| Formula | FORMULA-001: hours_elapsed = (NOW() - 2026-03-18 10:00) / 3600 ≈ 2.0 < 24 → REJECT. remaining = 24 - 2 = 22h |
| Ref | DEC-T01, DEC-B01 |
TC-P1-04: Cho phép ĐH khi voucher đủ thời gian
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P1-02 |
| Precondition | Login USR-NV1. VC-02: activated > 24h trước, CAMP-A min=24h |
| Steps | 1. 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 |
| Data | VC-02, CAMP-A |
| Formula | FORMULA-001: hours_elapsed = (NOW() - 2026-03-17 08:00) / 3600 > 24 → PASS |
| Ref | DEC-T01 |
TC-P1-05: Voucher min_activation_hours = 0 — bỏ qua check
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P1-02 |
| Precondition | Login USR-NV1. VC-04: activated vừa mới, CAMP-C min=0 |
| Steps | 1. 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 |
| Data | VC-04, CAMP-C |
| Ref | EC-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Formula |
| FR | FR-P1-02 |
| Precondition | VC-03: activated 2026-03-18 14:00, CAMP-B min=48h. Hiện tại: 2026-03-19 10:00 |
| Steps | 1. Tạo ĐH với voucher VC-03 |
| Expected | hours_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" |
| Data | VC-03, CAMP-B |
| Formula | FORMULA-001: EXTRACT(EPOCH FROM ('2026-03-19 10:00' - '2026-03-18 14:00')) / 3600 = 20.0. 20.0 < 48 → REJECT. Remaining = 28.0h |
| Ref | DEC-T01 |
Nhóm 1.3: Manager Override (FR-P1-03)
TC-P1-07: Manager override thành công — happy path
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P1-03 |
| Precondition | Login USR-MGR1 (ManagerCN BR-01). VC-01 bị chặn (< 24h). Voucher thuộc BR-01 |
| Steps | 1. 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" |
| Expected | Override 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 |
| Data | VC-01, USR-MGR1, OVR-01 |
| Ref | DEC-B01, DEC-T02 |
TC-P1-08: Override validation — ghi chú < 10 ký tự
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-03 |
| Precondition | Login USR-MGR1. VC-01 bị chặn. Dialog override mở |
| Steps | 1. Chọn lý do: "Khách VIP" 2. Nhập ghi chú: "OK" (2 ký tự) 3. Click "Xác nhận duyệt" |
| Expected | Validation error: ghi chú phải >= 10 ký tự. Override không tạo |
| Data | VC-01 |
| Ref | dev-spec C5.1 — reason_note min 10 chars |
TC-P1-09: Staff không thể override
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Permission |
| FR | FR-P1-03 |
| Precondition | Login USR-NV1 (Staff). VC-01 bị chặn |
| Steps | 1. Tạo ĐH → chọn VC-01 → bị chặn 2. Verify nút "Yêu cầu Manager duyệt" |
| Expected | Nút "Yêu cầu Manager duyệt" ẩn. Chỉ hiện nút "Đóng". Staff không có quyền override |
| Data | VC-01, USR-NV1 |
| Ref | B5 Permission Matrix — Dialog chặn: Override → Staff ❌ |
TC-P1-10: Manager override khác branch — chặn
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Permission |
| FR | FR-P1-03 |
| Precondition | Login USR-MGR2 (ManagerCN BR-02). VC-01 thuộc BR-01 bị chặn |
| Steps | 1. Gọi API approve_voucher_override với user_voucher_id = VC-01 |
| Expected | Error OVERRIDE_UNAUTHORIZED. Manager BR-02 không thể override voucher BR-01. Branch scope: voucher.branch_id NOT IN ctx.Access.BranchIds |
| Data | VC-01, USR-MGR2 |
| Ref | C8.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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-04 |
| Precondition | Login USR-ADMIN. 5 override records trong seed data |
| Steps | 1. Mở Override Dashboard 2. Verify bảng hiển thị |
| Expected | Bả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 |
| Data | OVR-01 đến OVR-05 |
| Ref | DEC-T02 |
TC-P1-12: Dashboard filter theo chi nhánh + thời gian
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-04 |
| Precondition | Login USR-ADMIN. Dashboard mở, 5 override records |
| Steps | 1. 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ả |
| Expected | Filter 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 |
| Data | OVR-01 đến OVR-05 |
| Ref | FR-P1-04 |
TC-P1-13: Dashboard — Manager chỉ thấy branch mình
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Permission |
| FR | FR-P1-04 |
| Precondition | Login USR-MGR1 (ManagerCN BR-01) |
| Steps | 1. Mở Override Dashboard |
| Expected | Chỉ 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) |
| Data | OVR-01 đến OVR-05 |
| Ref | B5 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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-05 |
| Precondition | Login USR-ADMIN. Settings > Voucher |
| Steps | 1. Sửa "Thời gian chờ mặc định (giờ)" từ 24 → 12 2. Lưu 3. Verify app_setting.VoucherSetting.DefaultMinActivationHours = 12 |
| Expected | Lư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 |
| Data | app_setting |
| Ref | DEC-T08 |
TC-P1-15: ITStaff không được config Settings
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P1-05 |
| Precondition | Login USR (role ITStaff) |
| Steps | 1. Truy cập Settings > Voucher |
| Expected | Redirect + toast "Bạn không có quyền". Chỉ ITLeader, BOD được config |
| Data | — |
| Ref | B5 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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P1-06 |
| Precondition | DefaultManualVoucherExpiryDays = 30. NV kích hoạt voucher manual |
| Steps | 1. NV tặng voucher manual cho khách ngày 01/04/2026 2. Verify user_vouchers.expired_at |
| Expected | expired_at = 01/05/2026 (01/04 + 30 ngày). Voucher có hạn sử dụng 30 ngày kể từ khi tặng |
| Data | VC-08 pattern |
| Ref | DEC-B05 |
TC-P1-17: Cron expired_vouchers đánh hết hạn voucher manual
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-06 |
| Precondition | VC-14: voucher manual, activated_at = 01/03, expired_at = 31/03. Hiện tại > 31/03 |
| Steps | 1. Trigger cron expired_vouchers 2. Verify voucher status |
| Expected | VC-14 chuyển status = 'expired'. Cron check expired_at < NOW() AND status = 'activated' → đánh expired |
| Data | VC-14 |
| Ref | DEC-B05, C6.1 |
TC-P1-18: Notification voucher manual sắp hết hạn (3 ngày trước)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Functional |
| FR | FR-P1-06 |
| Precondition | VC-14: expired_at = 2026-04-01. Hiện tại: 2026-03-29 |
| Steps | 1. Trigger cron expired_vouchers |
| Expected | Notification 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 |
| Data | VC-14 |
| Ref | B4 Notification Spec |
TC-P1-19: Hiển thị hạn sử dụng trên Partner app (FR-P1-07)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P1-07 |
| Precondition | NV vừa tặng voucher manual, expired_at đã set |
| Steps | 1. NV tặng voucher → verify hiển thị 2. Khách mở app xem voucher |
| Expected | Hiể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 |
| Data | VC-08 pattern |
| Ref | DEC-B05 |
Nhóm 1.6: Permission Tests P1
TC-P1-20: AccLeader xem Override Dashboard nhưng không override
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P1-03, FR-P1-04 |
| Precondition | Login USR-ACC (AccLeader) |
| Steps | 1. 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 |
| Expected | Dashboard: xem all branches OK. Dialog chặn: nút "Yêu cầu duyệt" ẩn. Export: OK (AccLeader có quyền export) |
| Data | — |
| Ref | B5 Permission Matrix P1 |
TC-P1-21: Staff không truy cập Override Dashboard
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P1-04 |
| Precondition | Login USR-NV1 (Staff) |
| Steps | 1. Navigate trực tiếp đến URL Override Dashboard |
| Expected | Redirect về /voucher + toast "Bạn không có quyền". Menu Override Dashboard ẩn |
| Data | — |
| Ref | B5 Permission Matrix — Staff ❌ |
TC-P1-22: BOD override không branch scope
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P1-03 |
| Precondition | Login USR-BOD (BOD). VC-03 thuộc BR-02 bị chặn |
| Steps | 1. Tạo ĐH → chọn VC-03 → bị chặn 2. Override với lý do + ghi chú hợp lệ |
| Expected | Override thành công. BOD có quyền override tất cả branches (không branch scope) |
| Data | VC-03, USR-BOD |
| Ref | B5 Permission Matrix — BOD ✅ |
TC-P1-23: Notification override gửi đúng recipients
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Functional |
| FR | FR-P1-03 |
| Precondition | Override tạo thành công (OVR-01) |
| Steps | 1. Verify notification trigger sau override |
| Expected | noti_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 |
| Data | OVR-01 |
| Ref | B4 Notification Spec |
Nhóm 1.7: Edge Cases P1
TC-P1-24: Boundary time — 23h59m vs 24h01m
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Edge Case |
| FR | FR-P1-02 |
| Precondition | VC activated lúc 10:00. min=24h. NOW() = 09:59 ngày hôm sau (23h59m) |
| Steps | 1. 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 |
| Expected | 23h59m < 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 | — |
| Formula | FORMULA-001 boundary |
| Ref | EC-P1-01, DEC-Q01 |
TC-P1-25: Timezone midnight — kích hoạt 23:30, chờ 24h
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Edge Case |
| FR | FR-P1-02 |
| Precondition | VC-11: activated_at = 2026-03-19 23:30 (UTC+7). min=24h |
| Steps | 1. 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 |
| Expected | 22: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 |
| Data | VC-11 |
| Ref | EC-P1-05, DEC-Q01 |
TC-P1-26: Campaign đã kết thúc nhưng voucher chưa expired
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P1-02 |
| Precondition | VC-12: campaign CAMP-D đã ended. Voucher status = 'activated' |
| Steps | 1. Tạo ĐH → chọn VC-12 |
| Expected | Vẫ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 |
| Data | VC-12, CAMP-D |
| Ref | EC-P1-03 |
TC-P1-27: Double override cùng voucher
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P1-03 |
| Precondition | VC-01 đã có override OVR-01 (ĐH đầu bị hủy). Voucher quay lại status 'activated' |
| Steps | 1. 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 |
| Expected | Override 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 |
| Data | VC-01, OVR-01, OVR-03 |
| Ref | EC-P1-04 |
TC-P1-28: Voucher manual — dùng system default khi không có campaign
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Edge Case |
| FR | FR-P1-02, FR-P1-05 |
| Precondition | VC-08: voucher manual (campaign_id = NULL). DefaultMinActivationHours = 24 |
| Steps | 1. Tạo ĐH với VC-08 (activated < 24h trước) |
| Expected | Chặn. Dùng AppSettings.VoucherSetting.DefaultMinActivationHours = 24 thay cho campaign config. Dialog hiện thời gian còn lại đúng |
| Data | VC-08, app_setting |
| Ref | EC-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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P2-01 |
| Precondition | Login USR-ADMIN. Mở campaign CAMP-A → Tab "Thống kê NV" |
| Steps | 1. 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 |
| Expected | Bảng hiện: NV |
| Data | CAMP-A, USR-NV1 đến USR-NV4 |
| Formula | FORMULA-002: 45 / 120 × 100 = 37.50% |
| Ref | DEC-U02 |
TC-P2-02: Filter chi nhánh + thời gian + loại người phát
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P2-01 |
| Precondition | Login USR-ADMIN. Tab Thống kê NV mở |
| Steps | 1. 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 |
| Expected | Filter 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 |
| Data | CAMP-A |
| Ref | FR-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P2-02 |
| Precondition | Login USR-ADMIN. Tab Thống kê NV, CAMP-A |
| Steps | 1. Click hàng USR-NV1 (Nguyễn A) 2. Verify StaffDrillDownDialog |
| Expected | Dialog 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 |
| Data | USR-NV1 vouchers |
| Ref | DEC-U03 |
TC-P2-04: Drill-down — voucher chưa có ĐH
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Edge Case |
| FR | FR-P2-02 |
| Precondition | Voucher status = 'activated' (chưa redeem, chưa có ĐH) |
| Steps | 1. Drill-down NV → xem voucher activated |
| Expected | Cột DT hiện "—" (không hiện 0đ). Trạng thái hiện "Đã kích hoạt" |
| Data | VC-01, VC-02 |
| Ref | B6 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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P2-03 |
| Precondition | Login USR-ADMIN. Tab Thống kê NV, CAMP-A |
| Steps | 1. Click header "Đã phát" → verify sort DESC 2. Click header "Tỉ lệ %" → verify sort DESC 3. Click header "Doanh thu" → verify sort DESC |
| Expected | Sort Đã 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 |
| Data | CAMP-A staff |
| Ref | FR-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P2-04 |
| Precondition | Login USR-NV1 trên Partner app |
| Steps | 1. Mở thống kê voucher cá nhân |
| Expected | Hiệ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) |
| Data | USR-NV1 |
| Formula | FORMULA-002 (self scope) |
| Ref | DEC-B03 |
TC-P2-07: Partner app — NV chưa phát voucher
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Edge Case |
| FR | FR-P2-04 |
| Precondition | Login USR-NV5 (chưa phát voucher nào) trên Partner app |
| Steps | 1. Mở thống kê voucher cá nhân |
| Expected | Empty state: "Bạn chưa phát voucher nào" |
| Data | USR-NV5 |
| Ref | EC-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P2-05 |
| Precondition | Login USR-ADMIN. Tab Thống kê NV, kết quả < 5000 rows |
| Steps | 1. Click "Xuất Excel" |
| Expected | File 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ị |
| Data | CAMP-A staff |
| Ref | FR-P2-05 |
TC-P2-09: Export Excel — async (> 5000 rows)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | NFR |
| FR | FR-P2-05 |
| Precondition | Dataset > 5000 rows (hoặc mock) |
| Steps | 1. Click "Xuất Excel" |
| Expected | Trigger 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 |
| Data | Large dataset mock |
| Ref | C9.1 NFR — export async |
Nhóm 2.6: Permission Tests P2
TC-P2-10: ManagerCN chỉ thấy NV trong branch
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Permission |
| FR | FR-P2-01 |
| Precondition | Login USR-MGR1 (ManagerCN BR-01) |
| Steps | 1. Mở Tab Thống kê NV |
| Expected | Chỉ 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 |
| Data | CAMP-A |
| Ref | B5 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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P2-01 |
| Precondition | Login USR-NV1 (Staff) |
| Steps | 1. Mở campaign CAMP-A detail |
| Expected | Tab "Thống kê NV" ẩn trong tab list. Không hiện cho Staff |
| Data | — |
| Ref | B5 Permission Matrix — Tab Thống kê NV: Staff ❌, B6 — Tab ẩn |
TC-P2-12: AccLeader xem all branches + export
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Permission |
| FR | FR-P2-01, FR-P2-05 |
| Precondition | Login USR-ACC (AccLeader) |
| Steps | 1. Mở Tab Thống kê NV → verify xem all branches 2. Click "Xuất Excel" |
| Expected | Bảng hiện NV tất cả branches. Export OK |
| Data | — |
| Ref | B5 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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P2-01 |
| Precondition | USR-NV4 đã deactivated nhưng có 30 voucher lịch sử |
| Steps | 1. Mở Tab Thống kê NV, CAMP-A |
| Expected | USR-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ử |
| Data | USR-NV4 |
| Ref | EC-P2-01 |
TC-P2-14: NV chuyển branch mid-campaign
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Edge Case |
| FR | FR-P2-01 |
| Precondition | NV phát 50 voucher ở BR-01, sau đó chuyển sang BR-02, phát thêm 20 |
| Steps | 1. Filter BR-01 → verify 50 voucher 2. Filter BR-02 → verify 20 voucher |
| Expected | Report 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 | — |
| Ref | EC-P2-02 |
TC-P2-15: staff_id NULL — voucher online tự claim
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P2-01 |
| Precondition | Một số voucher có staff_id = NULL (voucher online tự claim) |
| Steps | 1. Mở Tab Thống kê NV |
| Expected | Voucher 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 | — |
| Ref | EC-P2-03 |
TC-P2-16: Doanh thu = 0 cho voucher tặng miễn phí
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Edge Case |
| FR | FR-P2-01 |
| Precondition | Voucher tặng dịch vụ miễn phí, DT = 0 |
| Steps | 1. Xem thống kê NV phát voucher miễn phí |
| Expected | Cộ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 | — |
| Formula | FORMULA-002: 0đ vẫn tính vào total_redeemed nếu status='redeemed' |
| Ref | EC-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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P3-01 |
| Precondition | Login USR-ADMIN. Tạo chiến dịch mới, step "Đối tác" |
| Steps | 1. 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 |
| Expected | Campaign 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 |
| Data | AFF-01, AFF-02 |
| Ref | DEC-B04, DEC-T04 |
TC-P3-02: Gán đối tác với quota unlimited
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P3-01 |
| Precondition | Login USR-ADMIN. Wizard step Đối tác |
| Steps | 1. Chọn KOL Minh (AFF-03) 2. Tick "Không giới hạn" (quota = NULL) 3. Submit |
| Expected | voucher_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ờ |
| Data | AFF-03 |
| Ref | FORMULA-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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P3-02 |
| Precondition | AFF-01 trong CAMP-A: quota=100, distributed_count=98 |
| Steps | 1. 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 |
| Expected | Phá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 |
| Data | AFF-01, CAMP-A |
| Formula | FORMULA-006: distributed_count (100) >= quota (100) → REJECT |
| Ref | DEC-T04 |
TC-P3-04: Quota check — unlimited (NULL)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P3-02 |
| Precondition | AFF-03 trong CAMP-A: quota=NULL, distributed_count=75 |
| Steps | 1. AFF-03 phát voucher liên tục (76, 77, ...) |
| Expected | Không bao giờ chặn. quota IS NULL → skip check. distributed_count tăng nhưng không có upper limit |
| Data | AFF-03 |
| Formula | FORMULA-006: quota IS NULL → unlimited |
| Ref | DEC-T04 |
TC-P3-05: Race condition — 2 requests concurrent khi quota còn 1
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Edge Case |
| FR | FR-P3-02 |
| Precondition | AFF-01: quota=100, distributed_count=99. 2 requests phát voucher đồng thời |
| Steps | 1. Gửi 2 request phát voucher song song (concurrent) cho AFF-01 |
| Expected | Chỉ 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 |
| Data | AFF-01 |
| Formula | FORMULA-006 — atomic increment |
| Ref | DEC-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P3-03 |
| Precondition | Tab Thống kê NV, CAMP-A (có cả NV + affiliate) |
| Steps | 1. Mở Tab Thống kê NV 2. Verify cột "Loại" |
| Expected | USR-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" |
| Data | CAMP-A |
| Ref | DEC-T03 |
TC-P3-07: Filter "Chỉ xem đối tác"
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P3-03 |
| Precondition | Tab Thống kê NV, CAMP-A |
| Steps | 1. Filter "Loại người phát" = "Đối tác" |
| Expected | Chỉ hiện AFF-01, AFF-02, AFF-03. Ẩn tất cả NV nội bộ |
| Data | CAMP-A affiliates |
| Ref | FR-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P3-04 |
| Precondition | Login USR-NV1. Tạo ĐH, chọn voucher VC-09 (phát bởi AFF-01 KOL Hương) |
| Steps | 1. Tạo ĐH → chọn voucher VC-09 |
| Expected | Badge hiển thị: "Voucher do KOL Hương phát". AffiliateSourceBadge component render đúng tên đối tác |
| Data | VC-09, AFF-01 |
| Ref | DEC-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P3-05 |
| Precondition | Admin tạo campaign + gán 2 đối tác, sau đó gỡ 1 |
| Steps | 1. Gán AFF-01, AFF-02 → verify log 2. Gỡ AFF-02 (soft delete) → verify log |
| Expected | affiliate_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 |
| Data | AFF-01, AFF-02 |
| Ref | DEC-T04 |
Nhóm 3.6: Permission Tests P3
TC-P3-10: AccLeader không truy cập Wizard Đối tác
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P3-01 |
| Precondition | Login USR-ACC (AccLeader) |
| Steps | 1. Mở tạo/sửa campaign wizard |
| Expected | Step "Đố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 | — |
| Ref | B5 Permission Matrix — Wizard Đối tác: AccLeader ❌ |
TC-P3-11: Affiliate không thấy badge đối tác
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | Permission |
| FR | FR-P3-04 |
| Precondition | Login user role Affiliate (nếu có truy cập tạo ĐH) |
| Steps | 1. Verify badge visibility |
| Expected | Badge "Voucher do đối tác X phát" ẩn cho Affiliate role. Tất cả role khác (ITLeader → Staff) đều thấy |
| Data | — |
| Ref | B5 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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P3-02 |
| Precondition | AFF-01 đang active trong CAMP-A. Admin set is_active=false |
| Steps | 1. AFF-01 cố phát voucher sau khi bị deactivated |
| Expected | Chặ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ử |
| Data | AFF-01 |
| Ref | EC-P3-01 |
TC-P3-13: Admin tăng quota giữa chiến dịch
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P3-02 |
| Precondition | AFF-01: quota=100, distributed_count=100 (hết quota) |
| Steps | 1. Admin sửa quota AFF-01 → 150 2. AFF-01 phát voucher thứ 101 |
| Expected | Phá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ờ |
| Data | AFF-01 |
| Ref | EC-P3-03, DEC-T04 |
TC-P3-14: Soft delete đối tác — voucher đã phát vẫn giữ
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P3-05 |
| Precondition | AFF-02 đã phát 50 voucher trong CAMP-A |
| Steps | 1. Admin gỡ AFF-02 khỏi CAMP-A 2. Verify voucher đã phát + report |
| Expected | voucher_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ỡ) |
| Data | AFF-02 |
| Ref | EC-P3-04 |
TC-P3-15: Campaign không gán đối tác — flow bình thường
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P3-01 |
| Precondition | CAMP-B không gán đối tác nào |
| Steps | 1. NV phát voucher CAMP-B bình thường |
| Expected | Flow 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 |
| Data | CAMP-B |
| Ref | EC-P3-05 |
TC-P3-16: Đối tác phát voucher cho campaign KHÔNG được gán
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Edge Case |
| FR | FR-P3-02 |
| Precondition | AFF-01 được gán CAMP-A nhưng KHÔNG gán CAMP-B |
| Steps | 1. AFF-01 cố phát voucher cho CAMP-B |
| Expected | Từ 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 |
| Data | AFF-01, CAMP-B |
| Ref | EC-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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P4-01 |
| Precondition | Login 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 |
| Steps | 1. Mở Tab Chu kỳ sử dụng 2. Verify 4 KPI cards |
| Expected | TB: 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 |
| Data | CAMP-A voucher_logs |
| Formula | FORMULA-003: cycle_days per voucher. FORMULA-004: PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY cycle_days) |
| Ref | DEC-T05, DEC-T06 |
TC-P4-02: Verify FORMULA-003 — cycle days tính từ voucher_logs
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Formula |
| FR | FR-P4-01 |
| Precondition | VC-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 |
| Steps | 1. Query get_voucher_usage_cycle_statistics cho CAMP-A 2. Verify cycle calculation |
| Expected | VC-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) |
| Data | VC-05, VC-13, voucher_logs |
| Formula | FORMULA-003: DISTINCT ON voucher_id, ORDER BY created_at DESC WHERE action='voucher_redeemed' |
| Ref | DEC-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ính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Functional |
| FR | FR-P4-02 |
| Precondition | CAMP-A: 200 vouchers, phân bổ cycle biết trước |
| Steps | 1. Mở Tab Chu kỳ sử dụng 2. Verify horizontal bar chart |
| Expected | 7 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) |
| Data | CAMP-A |
| Formula | FORMULA-005: percentage = bucket_count / total_count × 100 (1 decimal) |
| Ref | DEC-T07, DEC-U04 |
TC-P4-04: Verify FORMULA-005 — bucket boundary chính xác
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Formula |
| FR | FR-P4-02 |
| Precondition | Vouchers với cycle_days: 0, 3, 3.9, 4, 7, 14, 30, 31, 60, 61 |
| Steps | 1. Verify bucket assignment cho từng voucher |
| Expected | cycle=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. |
| Data | Custom voucher_logs |
| Ref | FORMULA-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P4-03 |
| Precondition | Login USR-ADMIN. Tab Chu kỳ, cả CAMP-A và CAMP-E có data |
| Steps | 1. Chọn CAMP-A + CAMP-E để so sánh 2. Verify chart |
| Expected | Bar 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 |
| Data | CAMP-A, CAMP-E |
| Ref | FR-P4-03 |
TC-P4-06: So sánh chiến dịch — 1 có data, 1 không
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P4-03 |
| Precondition | CAMP-A có data, CAMP-C không có voucher redeemed (chỉ 50 activated) |
| Steps | 1. Chọn CAMP-A + CAMP-C so sánh |
| Expected | CAMP-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% |
| Data | CAMP-A, CAMP-C |
| Ref | EC-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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P4-04 |
| Precondition | Tab Chu kỳ sử dụng |
| Steps | 1. 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 |
| Expected | KPI + 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 |
| Data | CAMP-A, BR-01 |
| Ref | FR-P4-04 |
TC-P4-08: Group by dimension (campaign / branch / staff / overall)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P4-04 |
| Precondition | Tab Chu kỳ, không filter |
| Steps | 1. 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 |
| Expected | Mỗ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 |
| Data | All data |
| Ref | FR-P4-04 |
Nhóm 4.5: Export (FR-P4-05)
TC-P4-09: Export Excel chu kỳ sử dụng
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Functional |
| FR | FR-P4-05 |
| Precondition | Login USR-ADMIN. Tab Chu kỳ với data |
| Steps | 1. Click "Xuất Excel" |
| Expected | File 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 |
| Data | CAMP-A |
| Ref | FR-P4-05 |
Nhóm 4.6: Permission Tests P4
TC-P4-10: ManagerCN xem Tab Chu kỳ — branch scope
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P4-01 |
| Precondition | Login USR-MGR1 (ManagerCN BR-01) |
| Steps | 1. Mở CAMP-A → Tab Chu kỳ sử dụng |
| Expected | KPI + 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) |
| Data | CAMP-A, BR-01 |
| Ref | B5 Permission Matrix — Tab Chu kỳ: ManagerCN ✅(Branch), Export ❌ |
TC-P4-11: Staff không thấy Tab Chu kỳ
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Permission |
| FR | FR-P4-01 |
| Precondition | Login USR-NV1 (Staff) |
| Steps | 1. Mở campaign detail |
| Expected | Tab "Chu kỳ sử dụng" ẩn trong tab list |
| Data | — |
| Ref | B5 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ính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P4-01, FR-P4-02 |
| Precondition | Campaign mới, tất cả voucher status='activated' |
| Steps | 1. Mở Tab Chu kỳ |
| Expected | KPI 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 | — |
| Ref | EC-P4-01, B6 State Matrix — Partial |
TC-P4-13: Voucher redeem → restore → redeem (net cycle)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P0 |
| Type | Edge Case |
| FR | FR-P4-01 |
| Precondition | VC-13: activated 15/03 10:00. Logs: redeem 16/03 12:00, restore 16/03 14:00, redeem 18/03 10:00 |
| Steps | 1. Query cycle stats cho VC-13 |
| Expected | Cycle = 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 |
| Data | VC-13, voucher_logs |
| Formula | FORMULA-003 — net redeem từ voucher_logs |
| Ref | EC-P4-02, DEC-T05 |
TC-P4-14: Cycle = 0 (kích hoạt và dùng cùng ngày)
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P4-01, FR-P4-02 |
| Precondition | Voucher activated 10:00, redeemed 10:30 cùng ngày (campaign min=0) |
| Steps | 1. Verify cycle calculation 2. Verify bucket assignment |
| Expected | Cycle = 0 ngày (hợp lệ). Nằm trong bucket "0-3 ngày". KPI min = 0. Không báo lỗi |
| Data | — |
| Ref | EC-P4-03 |
TC-P4-15: Performance — > 100k vouchers
| Thuộc tính | Giá trị |
|---|---|
| Priority | P2 |
| Type | NFR |
| FR | FR-P4-04 |
| Precondition | Dataset > 100k vouchers (staging environment) |
| Steps | 1. Query get_voucher_usage_cycle_statistics 2. Query get_voucher_usage_cycle_distribution 3. Đo response time |
| Expected | Statistics: < 3s. Distribution: < 3s. FE chỉ nhận kết quả đã aggregate (~10-50 rows), không nhận raw data. DB-level aggregation |
| Data | Large dataset (staging) |
| Ref | EC-P4-04, C9.1 NFR |
TC-P4-16: PERCENTILE_CONT trả NULL khi không có redeem
| Thuộc tính | Giá trị |
|---|---|
| Priority | P1 |
| Type | Edge Case |
| FR | FR-P4-01 |
| Precondition | Filter kết quả: tất cả voucher pending (không có redeem nào) |
| Steps | 1. Query cycle statistics |
| Expected | PERCENTILE_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 | — |
| Formula | FORMULA-004 — empty dataset → NULL |
| Ref | EC-P4-06, DEC-T06 |
D5. Entry / Exit Criteria
5.1 Entry Criteria (bắt đầu test)
| # | Tiêu chí | Kiểm tra | Phase |
|---|---|---|---|
| EN-01 | Migration deploy thành công (không lỗi rollback) | hasura migrate status — no pending | Mỗi phase |
| EN-02 | Hasura metadata apply thành công | hasura metadata apply — no errors | Mỗi phase |
| EN-03 | Go handler compile + unit test pass | go test ./... — 0 failures | P1, P3 |
| EN-04 | Seed data load thành công (D3) | SQL seed script chạy không lỗi | All |
| EN-05 | FE build thành công (no TS errors) | npm run build — exit 0 | Mỗi phase |
| EN-06 | Dev self-test pass (happy path chính) | Dev xác nhận đã test local | Mỗi phase |
| EN-07 | PERF-FIX deploy thành công | Expression index + merge CTEs applied | P2, P4 |
| EN-08 | P2 deploy thành công | Staff statistics function available | P3 (dependency) |
5.2 Exit Criteria (kết thúc test)
| # | Tiêu chí | Ngưỡng | Phase |
|---|---|---|---|
| EX-01 | Tất cả TC P0 PASS | 100% (0 fail) | Mỗi phase |
| EX-02 | Tất cả TC P1 PASS | >= 95% (cho phép 1 known issue với workaround) | Mỗi phase |
| EX-03 | Tất cả TC P2 PASS | >= 90% (known issues documented) | Mỗi phase |
| EX-04 | Không có bug Blocker / Critical mở | 0 Blocker, 0 Critical | Mỗi phase |
| EX-05 | Permission test PASS 100% | Tất cả role × action đều verify | Mỗi phase |
| EX-06 | Formula verification PASS | FORMULA-001 đến FORMULA-006 tất cả ví dụ đúng | All |
| EX-07 | NFR performance đạt target | Time validation < 50ms, Statistics < 2s, Cycle < 3s | P1, P2, P4 |
| EX-08 | Edge case race condition verify | TC-P3-05 (concurrent quota) PASS | P3 |
| EX-09 | Regression test pass | Flow tạo ĐH hiện có không bị ảnh hưởng | P1 |
| EX-10 | Export test pass | Sync + async export hoạt động đúng | P2, P4 |
5.3 Test Environment
| Aspect | Chi tiết |
|---|---|
| Environment | Staging (mirror production config) |
| Database | PostgreSQL 14+ (verify PERCENTILE_CONT support) |
| Timezone | Server: UTC. App: Asia/Ho_Chi_Minh (UTC+7) |
| Browsers | Chrome (latest), Safari (latest) — theo standard QA suite |
| Seed data | D3 seed data + production-like volume cho NFR tests |
| Tools | Postman (API testing), Hasura Console (GraphQL), DevTools (Network) |
5.4 Bug Severity Definition
| Severity | Mô tả | Ví dụ trong feature này |
|---|---|---|
| Blocker | Không thể tiếp tục test, chặn flow chính | Tạo ĐH crash khi có voucher. Migration fail |
| Critical | Feature chính không hoạt động | Chặn thời gian không work. Override tạo nhưng ĐH vẫn bị chặn. Race condition quota |
| Major | Feature phụ bị lỗi hoặc data sai | Thống kê tính sai conversion rate. Bucket phân bổ sai |
| Minor | UI/UX nhỏ, không ảnh hưởng logic | Copy text sai. Tooltip không hiện. Sort không đúng chiều |
| Trivial | Cosmetic | Spacing, 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 P4Traceability Summary
| Loại | Tổng | Coverage |
|---|---|---|
| FR → TC | 22 FRs → 76 TCs | 100% — mọi FR có ít nhất 1 TC |
| FORMULA → TC | 6 formulas → 12 TCs | 100% — mỗi formula có verify TC + edge case TC |
| Permission → TC | 12 role×action combos → 11 TCs | 100% — mọi permission rule verify |
| Edge Case (UI-spec) → TC | 26/30 edge cases covered | 87% — 4 trivial cases merged vào TC chính |
| Decision → TC | DEC-Q01 → TC-P1-24/25, DEC-Q02 → TC-P3-05 | 100% QA decisions covered |
Changelog
| Version | Ngày | Thay đổi |
|---|---|---|
| 1.0 | 2026-03-19 | Initial — D1-D5, 76 test cases across 4 phases, 6 formula verifications |