Appearance
PRD — Nâng cấp Voucher Management: Kiểm soát, Thống kê & Đối tác
Version: 1.0 Ngày: 2026-03-16 Tác giả: PO/BA Trạng thái: Draft — Chờ review
Z) Decision Log
Z1 — Business Decisions
| ID | Quyết định | Lý do | Ngày |
|---|---|---|---|
| DEC-B01 | Chặn cứng thời gian kích hoạt → sử dụng voucher (Option D: chặn + cấu hình + override) | Ngăn gian lận tạo ĐH rồi kích hoạt voucher ngay. Cần linh hoạt cho case VIP/đặc biệt | 2026-03-16 |
| DEC-B02 | Mặc định min_activation_hours = 24h, cấu hình per campaign | Phù hợp ngành spa — voucher mục đích kéo khách quay lại, không phải giảm giá tức thì | 2026-03-16 |
| DEC-B03 | Không xây commission mới — chỉ thống kê doanh thu phát sinh | Hoa hồng đã có cơ chế trong ĐH (nhân viên gán nhãn tỉ lệ). Voucher chỉ cần track ai phát → phát sinh bao nhiêu | 2026-03-16 |
| DEC-B04 | Kết nối Affiliate module hiện có (không tạo partner system mới) | Affiliate đã có: affiliate_user, order_affiliate, invoice_affiliate. Tái sử dụng thay vì duplicate | 2026-03-16 |
| DEC-B05 | Thời hạn voucher manual dùng field expired_at có sẵn | user_vouchers.expired_at đã tồn tại + cron expired_vouchers đã check. Không cần cột mới | 2026-03-16 |
| DEC-B06 | Triển khai 4 phase theo priority P0 → P2 | P0 (chống gian lận) cấp bách nhất, P2 (chu kỳ) có thể chờ | 2026-03-16 |
Z2 — UX Decisions
| ID | Quyết định | Lý do | Ngày |
|---|---|---|---|
| DEC-U01 | Dialog chặn + nút "Yêu cầu duyệt" khi voucher chưa đủ thời gian | UX rõ ràng: nhân viên hiểu lý do bị chặn + có flow escalate cho Manager | 2026-03-16 |
| DEC-U02 | Tab "Thống kê nhân viên" trong voucher management (không tách module report riêng) | Giữ context — thống kê liên quan trực tiếp đến voucher, nằm chung cho dễ truy cập | 2026-03-16 |
| DEC-U03 | Drill-down: click nhân viên → danh sách voucher đã phát | Cho manager thấy chi tiết per nhân viên mà không cần navigate ra ngoài | 2026-03-16 |
| DEC-U04 | Horizontal bar chart cho phân bổ chu kỳ sử dụng | Trực quan, dễ so sánh giữa các bucket. Chart.js đã có sẵn | 2026-03-16 |
Z3 — Technical Decisions
| ID | Quyết định | Lý do | Ngày |
|---|---|---|---|
| DEC-T01 | Check thời gian trong validateOrderVouchers() của create_order.go | Đây là điểm validation duy nhất khi tạo ĐH với voucher — centralized, không bỏ sót | 2026-03-16 |
| DEC-T02 | Override table voucher_activation_overrides riêng | Tách biệt audit trail, không mix vào voucher_logs. Dễ query dashboard giám sát | 2026-03-16 |
| DEC-T03 | Thêm distributor_type vào user_vouchers (không dùng JOIN runtime) | Performance tốt hơn khi report — tránh JOIN phức tạp với affiliate_user mỗi lần query | 2026-03-16 |
| DEC-T04 | Upsert pattern cho voucher_campaign_affiliates (không delete-then-insert) | Junction table có distributed_count — delete-then-insert sẽ mất counter | 2026-03-16 |
| DEC-T05 | Dùng voucher_logs để tính cycle time (không dùng redeemed_at trực tiếp) | Xử lý đúng case voucher bị restore khi ĐH hủy (Gross vs Net pattern đã proven) | 2026-03-16 |
| DEC-T06 | Dùng PERCENTILE_CONT(0.5) PostgreSQL built-in cho median | PostgreSQL 14 hỗ trợ sẵn, không cần extension hay function custom | 2026-03-16 |
| DEC-T07 | Bucket aggregation tại DB level (không tính ở frontend) | Scale tốt với 100k+ vouchers. Pattern đã proven trong 5 analytics function hiện có | 2026-03-16 |
| DEC-T08 | Config mặc định voucher trong app_setting (JSONB pattern) | Theo pattern AppSettings struct đã có (nested JSONB). Cho voucher manual không thuộc campaign | 2026-03-16 |
Z4 — QA Decisions
| ID | Quyết định | Lý do | Ngày |
|---|---|---|---|
| DEC-Q01 | Test chặn thời gian phải cover: chặn đúng, override đúng, edge case midnight | Logic thời gian dễ sai ở timezone + boundary. Vietnam timezone (UTC+7) cần test kỹ | 2026-03-16 |
| DEC-Q02 | Test affiliate quota phải cover race condition (concurrent activation) | Pattern atomic increment đã có cho offline_used_count nhưng cần verify cho affiliate quota | 2026-03-16 |
A) PRD
A1. Blueprint
| Thuộc tính | Giá trị |
|---|---|
| Tên tính năng | Nâng cấp Voucher Management: Kiểm soát, Thống kê & Đối tác |
| Loại | Enhancement (nâng cấp module hiện có) |
| Complexity | Large (4 phases, cross-module) |
| Modules ảnh hưởng | cms (voucher), ecommerce, affiliate, report, settings |
| Services ảnh hưởng | ecommerce-api, controller (Hasura) |
| Platform | Admin web + Diva Partner app (API only) |
A2. Context
Hiện trạng:
- Module voucher management đã production-grade: 17 bảng, 11 DB functions, 3 kênh phát (online/offline/print), lifecycle đầy đủ, analytics cơ bản (funnel, branch, product, date)
- Voucher tặng trực tiếp tại cửa hàng (offline) là loại chính đang sử dụng
- Không có thời hạn sử dụng cho voucher manual (tặng qua Partner app/POS)
- Không có cơ chế kiểm soát thời gian giữa kích hoạt và sử dụng
- Không có thống kê theo nhân viên phát voucher
- Không có kết nối với module Affiliate cho đối tác bên ngoài
- Chi phí và doanh thu trên báo cáo hiện tại đều hiển thị 0đ
Vấn đề:
- Gian lận: nhân viên có thể tạo ĐH rồi kích hoạt voucher ngay để giảm giá trái phép
- Không kiểm soát được ai phát bao nhiêu voucher, hiệu quả ra sao
- Đối tác bên ngoài không được quản lý trong hệ thống voucher
- Không đo được hiệu quả voucher theo chu kỳ khách quay lại
A3. Goals & Success Metrics
| Goal | Metric | Target |
|---|---|---|
| Ngăn gian lận voucher | Số case kích hoạt + tạo ĐH trong < 24h | Giảm > 95% so với hiện tại |
| Kiểm soát nhân viên | Thống kê lượt phát per nhân viên/chi nhánh | 100% voucher offline được track |
| Mở rộng đối tác | Số đối tác affiliate tham gia chiến dịch voucher | >= 5 đối tác trong 3 tháng đầu |
| Đo hiệu quả | Thời gian TB từ phát → sử dụng voucher | Có data để so sánh giữa chiến dịch |
A4. Personas
| Persona | Vai trò | Nhu cầu chính |
|---|---|---|
| Admin/BOD | Quản lý toàn hệ thống | Xem báo cáo cross-branch, kiểm soát gian lận, cấu hình policy |
| Manager chi nhánh | Quản lý 1 chi nhánh | Xem thống kê nhân viên tại chi nhánh, approve override |
| Nhân viên (Staff) | Phát voucher tại cửa hàng/POS | Tặng voucher cho khách, xem thống kê cá nhân trên Partner app |
| Đối tác (Affiliate) | Phát voucher cho khách hàng riêng | Phát voucher theo quota, xem doanh thu phát sinh |
| Kế toán | Đối soát | Xem báo cáo doanh thu từ voucher, export Excel |
A5. Functional Requirements
Phase 1 (P0) — Kiểm soát thời gian + Thời hạn voucher manual
| FR ID | Mô tả | Acceptance Criteria | Ref |
|---|---|---|---|
| FR-P1-01 | Cấu hình thời gian chờ tối thiểu (giờ) per campaign | Admin tạo chiến dịch, set "Thời gian chờ tối thiểu = 48 giờ". Voucher kích hoạt lúc 10:00 01/04 → không thể dùng trong ĐH trước 10:00 03/04 | DEC-B01, DEC-B02 |
| FR-P1-02 | Chặn tạo ĐH khi voucher chưa đủ thời gian | NV tạo ĐH với voucher kích hoạt 2h trước, campaign set 24h → hệ thống chặn, hiển thị "Voucher cần chờ thêm 22 giờ" | DEC-T01 |
| FR-P1-03 | Manager override khi bị chặn | Manager nhập lý do (chọn dropdown + ghi chú) → approve → ĐH được tạo. Log: manager_id, reason, timestamp | DEC-B01, DEC-T02 |
| FR-P1-04 | Dashboard giám sát override | Admin/BOD xem danh sách override: ai approve, cho voucher nào, lý do gì, lúc nào. Filter theo chi nhánh, thời gian | DEC-T02 |
| FR-P1-05 | Cấu hình mặc định thời gian chờ trong Settings | Admin vào Settings → Voucher → set "Thời gian chờ mặc định = 24 giờ". Áp dụng cho voucher manual không thuộc campaign | DEC-T08 |
| FR-P1-06 | Thời hạn sử dụng cho voucher manual | Admin cấu hình "Hạn sử dụng voucher manual = 30 ngày". NV tặng voucher ngày 01/04 → voucher hết hạn 01/05. Cron tự đánh expired | DEC-B05 |
| FR-P1-07 | Hiển thị hạn sử dụng trên Partner app/POS | Khi NV tặng voucher → hiển thị "Hết hạn: 01/05/2026". Khách xem voucher trong app cũng thấy hạn | DEC-B05 |
Phase 2 (P1) — Thống kê nhân viên
| FR ID | Mô tả | Acceptance Criteria | Ref |
|---|---|---|---|
| FR-P2-01 | Báo cáo thống kê phát voucher theo nhân viên | Admin xem bảng: NV Nguyễn A (Q.1 HCM) đã phát 120 voucher, 45 đã dùng (37.5%), doanh thu 22.5 triệu. Filter theo chi nhánh, chiến dịch, thời gian | DEC-U02 |
| FR-P2-02 | Drill-down chi tiết per nhân viên | Click NV Nguyễn A → danh sách: mã voucher CDLF42L4S, tặng cho Trần Thị Mai ngày 15/02, trạng thái "Đã dùng", DT 500.000đ | DEC-U03 |
| FR-P2-03 | Ranking nhân viên | Sắp xếp theo: số phát, tỉ lệ conversion, doanh thu. Top 10 nhân viên phát voucher hiệu quả nhất | DEC-U02 |
| FR-P2-04 | API thống kê cá nhân cho Partner app | NV đăng nhập Partner app → xem: "Tháng này bạn phát 25 voucher, 8 đã dùng (32%), DT 4 triệu". Chỉ thấy data của mình | DEC-B03 |
| FR-P2-05 | Export Excel báo cáo nhân viên | Admin click "Xuất Excel" → file chứa: danh sách NV, số liệu, chi tiết voucher. Async nếu > 5000 rows | DEC-U02 |
Phase 3 (P1) — Đối tác Affiliate
| FR ID | Mô tả | Acceptance Criteria | Ref |
|---|---|---|---|
| FR-P3-01 | Gán đối tác vào chiến dịch voucher (Hybrid Distribution) | Admin tạo chiến dịch → bước "Đối tác" → chọn KOL Hương (quota 200, kênh: Both), PK Đông Y (quota 500, kênh: Batch). Mỗi đối tác chọn kênh phát: App (real-time qua Partner App), Batch (generate mã offline QR/tờ rơi), hoặc Both. Đối tác lấy từ module Affiliate hiện có | DEC-B04 |
| FR-P3-01a | Generate batch mã cho đối tác offline | Admin vào chi tiết campaign → tab "Đối tác" → chọn PK Đông Y (kênh: Batch) → bấm "Generate 500 mã" → hệ thống tạo 500 mã pre-assign cho đối tác → quota trừ ngay → tải PDF/Excel | DEC-B04, DEC-T04 |
| FR-P3-02 | Quota per đối tác per chiến dịch | Quota = tổng cam kết (app + batch). KOL Hương phát app: 12, generate batch: 100 → distributed_count = 112/200. Khách kích hoạt batch → KHÔNG trừ thêm quota (đã trừ khi generate). Hết quota → chặn cả app lẫn generate thêm | DEC-T04 |
| FR-P3-03 | Phân biệt nhân viên nội bộ vs đối tác trong report | Báo cáo thống kê hiển thị cột "Loại": Nhân viên / Đối tác. Filter: "Chỉ xem đối tác" | DEC-T03 |
| FR-P3-04 | Hiển thị "Voucher do đối tác X phát" khi tạo ĐH | NV tạo ĐH, chọn voucher → hệ thống hiển thị "Voucher này do KOL Hương phát" → gợi ý gán affiliate | DEC-B03 |
| FR-P3-05 | Log gán/gỡ đối tác vào affiliate_action_log | Admin gán KOL Hương vào chiến dịch → log action campaign_affiliate_assigned với metadata chiến dịch | DEC-T04 |
Phase 4 (P2) — Chu kỳ sử dụng
| FR ID | Mô tả | Acceptance Criteria | Ref |
|---|---|---|---|
| FR-P4-01 | KPI tổng quan chu kỳ | Admin xem: TB 12 ngày, nhanh nhất 1 ngày, trung vị 9 ngày, tỉ lệ dùng 35.2% | DEC-T06 |
| FR-P4-02 | Phân bổ theo bucket | Biểu đồ ngang: 0-3 ngày (28%), 4-7 ngày (20%), 8-14 ngày (17%), 15-30 ngày (14%), 31-60 ngày (10%), 60+ ngày (6%), chưa dùng (5%) | DEC-T07, DEC-U04 |
| FR-P4-03 | So sánh giữa chiến dịch | Chọn 2+ chiến dịch → biểu đồ chồng so sánh phân bổ chu kỳ. VD: "Tết Kim Cương" TB 8.5 ngày vs "Sinh nhật" TB 14.2 ngày | DEC-T05 |
| FR-P4-04 | Filter theo dimension | Filter: chiến dịch, chi nhánh, nhân viên, thời gian. Group by: chiến dịch / chi nhánh / nhân viên / tổng quan | DEC-T07 |
| FR-P4-05 | Export Excel chu kỳ | Export bảng chi tiết + summary. Async nếu data lớn | DEC-U02 |
A6. Assumptions
| ID | Assumption | Impact nếu sai |
|---|---|---|
| ASM-01 | staff_id trong user_vouchers đã được set đúng cho tất cả voucher offline hiện tại | Phase 2 report sẽ thiếu data lịch sử → cần migration backfill |
| ASM-02 | Partner app gọi API qua Hasura GraphQL (cùng endpoint với admin) | Nếu Partner app dùng REST riêng → cần tạo REST endpoint thay vì chỉ GraphQL |
| ASM-03 | Đối tác trong module Affiliate đã có data (không phải module trống) | Nếu trống → Phase 3 cần thêm flow tạo đối tác mới |
| ASM-04 | PostgreSQL version >= 14 (hỗ trợ PERCENTILE_CONT) | Nếu version cũ → cần implement median function thủ công |
A7. Risks
| ID | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| RSK-01 | staff_id type mismatch (Go: *string, DB: uuid) gây lỗi khi JOIN report | Medium | High | Verify + fix trong Phase 2 migration |
| RSK-02 | Override flow bị lạm dụng (Manager approve tràn lan) | Low | Medium | Dashboard giám sát + alert khi override > X lần/tháng |
| RSK-03 | Race condition khi 2 đối tác phát voucher cùng lúc (quota) | Low | Medium | Atomic increment pattern (đã proven cho offline_used_count) |
| RSK-04 | Performance report chậm khi > 100k vouchers | Low | Medium | DB-level aggregation + materialized view nếu cần |
A8. Metrics & Monitoring
| Metric | Cách đo | Alert threshold |
|---|---|---|
| Số lần bị chặn thời gian / ngày | Count từ voucher_activation_overrides attempts | > 20 lần/ngày → review |
| Số lần override / tháng | Count từ voucher_activation_overrides approved | > 50 lần/tháng → escalate BOD |
| Thời gian TB phát → sử dụng | get_voucher_usage_cycle_statistics | < 1 ngày → nghi ngờ gian lận |
| Conversion rate voucher | redeemed / activated | < 10% → review chiến dịch |
B) Impact Map
Modules ảnh hưởng
| Module | Thay đổi | Phase |
|---|---|---|
| cms/voucher | Thêm field config, tab report mới, wizard step đối tác | P1, P2, P3, P4 |
| ecommerce (order) | Thêm validation thời gian trong tạo ĐH | P1 |
| settings | Thêm config mặc định voucher | P1 |
| affiliate | Kết nối vào voucher campaign | P3 |
Services ảnh hưởng
| Service | Thay đổi | Phase |
|---|---|---|
| ecommerce-api | Sửa create_order.go, activate_offline_voucher.go. Thêm override action | P1, P3 |
| controller (Hasura) | Thêm tables, functions, permissions YAML | P1, P2, P3, P4 |
| export-api | Thêm export template cho staff statistics + cycle | P2, P4 |
Database changes
| Thay đổi | Bảng/Function | Phase |
|---|---|---|
| ALTER TABLE | voucher_campaigns + min_activation_hours | P1 |
| CREATE TABLE | voucher_activation_overrides | P1 |
| ALTER TABLE | user_vouchers + distributor_type | P2 |
| CREATE FUNCTION | get_voucher_staff_statistics | P2 |
| CREATE TABLE | voucher_campaign_affiliates | P3 |
| CREATE FUNCTION | get_voucher_usage_cycle_statistics | P4 |
| CREATE FUNCTION | get_voucher_usage_cycle_distribution | P4 |
ALTER app_setting | Thêm VoucherSetting vào AppSettings JSONB | P1 |
C) Technical Notes
C1. Phase 1 — Kiểm soát thời gian
Điểm chặn: validateOrderVouchers() trong create_order.go (line 707-843).
Thêm check sau validation status + expiration hiện có:
IF campaign.min_activation_hours > 0:
hours_elapsed = (NOW() AT TIME ZONE 'Asia/Ho_Chi_Minh' - voucher.activated_at).hours
IF hours_elapsed < campaign.min_activation_hours:
IF NOT exists override for this voucher:
remaining = campaign.min_activation_hours - hours_elapsed
→ REJECT "Voucher cần chờ thêm {remaining} giờ nữa mới được sử dụng"Voucher manual (không thuộc campaign):
- Đọc
AppSettings.VoucherSetting.DefaultMinActivationHourstừapp_setting - Ưu tiên: campaign config > system default
Override action handler mới: POST /actions/approve_voucher_override
- Input:
voucher_id,reason_code(enum),reason_note(text) reason_codeenum values:vip_customer(Khách VIP),special_event(Sự kiện đặc biệt),manager_directive(Chỉ đạo cấp trên),system_error(Lỗi hệ thống),other(Khác)- Validate: caller phải có role Manager/Admin
- Insert vào
voucher_activation_overrides
Thời hạn voucher manual:
- Trong
activate_offline_voucher.go→updateVoucherToActivated():- Nếu voucher không thuộc campaign → set
expired_at = activated_at + default_expiry_days default_expiry_daystừAppSettings.VoucherSetting.DefaultManualVoucherExpiryDays
- Nếu voucher không thuộc campaign → set
- Cron
expired_vouchers.gođã checkexpired_at < NOW()→ tự động handle
C2. Data Models
Table: voucher_activation_overrides (Phase 1)
sql
CREATE TABLE voucher_activation_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_voucher_id UUID NOT NULL REFERENCES user_vouchers(id),
order_id UUID, -- ĐH được tạo sau override (nullable, set sau khi tạo ĐH)
approved_by UUID NOT NULL, -- Manager/Admin user ID
branch_id UUID, -- Chi nhánh nơi override xảy ra
reason_code TEXT NOT NULL CHECK (reason_code IN (
'vip_customer', 'special_event', 'manager_directive', 'system_error', 'other'
)),
reason_note TEXT NOT NULL, -- Ghi chú bắt buộc
hours_remaining NUMERIC(8,2), -- Số giờ còn lại tại thời điểm override
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL
);
CREATE INDEX idx_voucher_overrides_approved_by ON voucher_activation_overrides(approved_by);
CREATE INDEX idx_voucher_overrides_branch_id ON voucher_activation_overrides(branch_id);
CREATE INDEX idx_voucher_overrides_created_at ON voucher_activation_overrides(created_at);Table: voucher_campaign_affiliates (Phase 3)
sql
CREATE TABLE voucher_campaign_affiliates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES voucher_campaigns(id),
affiliate_user_id UUID NOT NULL REFERENCES affiliate_user(id),
quota INTEGER, -- NULL = không giới hạn
distributed_count INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by UUID,
deleted_at TIMESTAMPTZ, -- Soft delete
UNIQUE(campaign_id, affiliate_user_id)
);
CREATE INDEX idx_vca_campaign_id ON voucher_campaign_affiliates(campaign_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_vca_affiliate_user_id ON voucher_campaign_affiliates(affiliate_user_id) WHERE deleted_at IS NULL;
-- Auto-update updated_at
CREATE TRIGGER update_voucher_campaign_affiliates_timestamp
BEFORE UPDATE ON voucher_campaign_affiliates
FOR EACH ROW EXECUTE FUNCTION update_timestamp();ALTER: voucher_campaigns (Phase 1)
sql
ALTER TABLE voucher_campaigns
ADD COLUMN min_activation_hours INTEGER NOT NULL DEFAULT 24;ALTER: user_vouchers (Phase 2)
sql
ALTER TABLE user_vouchers
ADD COLUMN distributor_type TEXT CHECK (distributor_type IN ('internal_staff', 'affiliate'));C3. Phase 2 — Thống kê nhân viên
DB Function pattern: Copy từ get_voucher_branch_statistics (migration 1766800000000):
- Thay
branch_id→staff_id - JOIN
user_vouchers.staff_idvới user table để lấy tên - Thêm aggregation:
total_distributed,total_redeemed,conversion_rate,total_revenue,avg_days_to_redeem
Hasura permission cho Partner app:
- Function result filter:
staff_id = X-Hasura-User-Id - Role: customer hoặc partner role (tùy Partner app setup)
C4. Phase 3 — Đối tác Affiliate
Upsert pattern (không delete-then-insert):
- INSERT ON CONFLICT (campaign_id, affiliate_user_id) DO UPDATE SET quota, is_active, updated_at, updated_by
Quota check trong activate flow:
IF staff is affiliate:
find voucher_campaign_affiliates record WHERE deleted_at IS NULL
IF quota IS NOT NULL AND distributed_count >= quota:
→ REJECT "Đối tác đã hết quota"
atomic: distributed_count = distributed_count + 1C5. Phase 4 — Chu kỳ sử dụng
Data source: voucher_logs (không dùng user_vouchers.redeemed_at trực tiếp)
- Xử lý đúng case restore (ĐH hủy → voucher restored → redeem lại)
- Pattern:
DISTINCT ON (voucher_id) WHERE action = 'voucher_redeemed' ORDER BY created_at DESC
Bucket aggregation tại DB:
sql
CASE
WHEN cycle_days BETWEEN 0 AND 3 THEN '0-3'
WHEN cycle_days BETWEEN 4 AND 7 THEN '4-7'
WHEN cycle_days BETWEEN 8 AND 14 THEN '8-14'
WHEN cycle_days BETWEEN 15 AND 30 THEN '15-30'
WHEN cycle_days BETWEEN 31 AND 60 THEN '31-60'
WHEN cycle_days > 60 THEN '60+'
ELSE 'pending'
END as cycle_bucketMedian: PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY cycle_days)
C6. Security
- Override action: chỉ role Manager/Admin (check
ctx.Access.Roletrong Go handler) - Override dashboard: chỉ role có Report Access (ITLeader, BOD, AccountantLeader)
- Affiliate quota increment: atomic operation, race condition safe (pattern từ
offline_used_count) - Partner app API: Hasura row-level security filter
staff_id = X-Hasura-User-Id - Timezone: tất cả time comparison dùng
Asia/Ho_Chi_Minh(theo DB pattern)
D) Deployment Plan
Phase Roadmap
| Phase | Priority | Phụ thuộc | Scope |
|---|---|---|---|
| Phase 1 | P0 — Urgent | Độc lập | min_activation_hours + override + expired_at manual + settings |
| Phase 2 | P1 — High | Độc lập | Staff statistics function + admin report + Partner API |
| Phase 3 | P1 — High | Phase 2 (report) | voucher_campaign_affiliates + affiliate quota + extend report |
| Phase 4 | P2 — Medium | Nên sau Phase 2 | Usage cycle function + chart + tab mới |
RACI Matrix
| Phase | Backend Dev | Frontend Dev | QA | PO/BA | Tech Lead | BOD |
|---|---|---|---|---|---|---|
| Phase 1 | R | R | R | A | C | I |
| Phase 2 | R | R | R | A | C | I |
| Phase 3 | R | R | R | A | C | I |
| Phase 4 | R | R | R | A | C | I |
| Partner app UI | Partner Dev (R) | — | R | A | C | I |
R = Responsible, A = Accountable, C = Consulted, I = Informed
Files cần thay đổi (tổng hợp)
Backend — Go:
| File | Thay đổi | Phase |
|---|---|---|
services/ecommerce-api/action/create_order.go | Thêm time check trong validateOrderVouchers() | P1 |
services/ecommerce-api/action/activate_offline_voucher.go | Set expired_at cho manual + affiliate quota check | P1, P3 |
services/ecommerce-api/action/activate_voucher.go | Set distributor_type trong updateVoucherToActivated() | P2 |
services/ecommerce-api/action/approve_voucher_override.go | Mới — override action handler | P1 |
services/ecommerce-api/action/create_draft_voucher_campaign.go | Thêm affiliate insert | P3 |
services/ecommerce-api/action/update_voucher_campaign.go | Thêm affiliate upsert | P3 |
pkg/store/app_setting.go | Thêm VoucherSetting struct | P1 |
pkg/store/user_vouchers.go | Thêm DistributorType field | P2 |
pkg/store/voucher_campaigns.go | Thêm MinActivationHours field | P1 |
Backend — Hasura:
| File | Thay đổi | Phase |
|---|---|---|
controller/migrations/ecommerce/[new]/up.sql | Migration: alter tables + create tables + create functions | P1-P4 |
controller/metadata/databases/ecommerce/tables/public_voucher_activation_overrides.yaml | Mới | P1 |
controller/metadata/databases/ecommerce/tables/public_voucher_campaign_affiliates.yaml | Mới | P3 |
controller/metadata/databases/ecommerce/functions/public_get_voucher_staff_statistics.yaml | Mới | P2 |
controller/metadata/databases/ecommerce/functions/public_get_voucher_usage_cycle_statistics.yaml | Mới | P4 |
controller/metadata/databases/ecommerce/functions/public_get_voucher_usage_cycle_distribution.yaml | Mới | P4 |
Frontend — Admin:
| File/Component | Thay đổi | Phase |
|---|---|---|
VoucherConfigForm.tsx | Thêm field "Thời gian chờ tối thiểu (giờ)" | P1 |
| Màn hình tạo ĐH | Dialog chặn + nút "Yêu cầu duyệt" | P1 |
| Settings page | Config mặc định voucher (hạn sử dụng, thời gian chờ) | P1 |
VoucherTab.tsx | Thêm tab "Thống kê nhân viên" + "Chu kỳ sử dụng" | P2, P4 |
VoucherStaffStatisticsTab.tsx | Mới — bảng + drill-down + export | P2 |
VoucherUsageCycleTab.tsx | Mới — KPI cards + bar chart + bảng | P4 |
VoucherCreate.tsx (wizard) | Thêm bước "Đối tác phát voucher" | P3 |
voucher.graphql | Thêm queries mới | P1-P4 |
| Override dashboard | Mới — danh sách override | P1 |
Hướng dẫn đọc
| Audience | Đọc sections | Mục đích |
|---|---|---|
| PO/BA | Z, A | Xác nhận requirements, decisions, scope |
| Tech Lead | Z3, B, C | Review kiến trúc, impact, data model |
| Backend Dev | C (Technical Notes), D (Files) | Implement Go handlers, migrations, Hasura metadata |
| Frontend Dev | A5 (FRs + wireframes trong conversation), D (Files) | Implement UI components |
| QA | A5 (Acceptance Criteria), Z4 | Viết test cases dựa trên AC |
| BOD | A2, A3, D (Phase Roadmap) | Overview, metrics, timeline |
Companion Files
| File | Nội dung | Trạng thái |
|---|---|---|
| UI Spec | Permission Matrix, State Matrix, ASCII Wireframes, Edge Cases, Copy Text, Notification Spec, Export Spec | ✅ Done |
| Dev Spec | Full SQL migrations, Hasura YAML, API contracts, NFR, Traceability Matrix | ✅ Done |
| QA Test Plan | 76 test cases, Seed data, Entry/Exit criteria | ✅ Done |
| Go-Live Checklist | Per-phase deploy steps, Rollback plan, Day-0/Day-1 monitoring | ✅ Done |
| Perf-fix Campaign Detail | Prerequisite — 3 tasks tối ưu hiệu năng trang chi tiết chiến dịch | ✅ Done |
⚠️ Prerequisite: Perf-fix Campaign Detail PHẢI deploy trước Phase 2 và Phase 4. Trang chi tiết chiến dịch hiện bị chậm (2× function execution mỗi lần đổi trang) — perf-fix giảm 6-8x, đảm bảo 2 tab mới (Thống kê NV + Chu kỳ) hoạt động mượt.
Changelog
| Version | Ngày | Thay đổi |
|---|---|---|
| 1.0 | 2026-03-16 | Initial draft — 4 phases, reviewed by Tech Lead |
| 1.1 | 2026-03-16 | Fix review: thêm schema voucher_activation_overrides, audit fields cho voucher_campaign_affiliates, RACI, reason_code enum, timezone spec, security section, companion files plan |
| 1.2 | 19/03/2026 | Bổ sung Dev Spec, QA Test Plan, Go-Live Checklist, Handoff. Thêm perf-fix prerequisite reference |