Appearance
v1.11 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Đảo DEC-031 — từ Option C (screen riêng /wallet/promotion2) sang Option A (thêm tab Ví khuyến mãi 2 thứ 3 vào wallet_screen.dart cùng cấp 2 tab hiện hữu). Lý do: đồng nhất UX khách — 1 entry point "Ví của tôi"; pattern parity với KM 1 hiện có. Note kỹ thuật: dev tự chốt widget (refactor FolderTabs 3 tab / swap CustomTabbar / swap TabPageWidget — cả 3 đã có trong core/) | Z) Nhật ký quyết định | FE Mobile, QA |
Thêm SCR-06-NEW-03 (web admin) — popup mới StatisticWalletPromotion2Popup.tsx lịch sử giao dịch Ví KM 2 (parity với popup KM 1 hiện có StatisticWalletPromotionPopup.tsx). Option α: tạo file riêng, không refactor popup KM 1 thành generic | (ui-spec B0.1, B2.6.6A) | FE Web, QA |
| Update dev-spec C11: chia P2-MOBILE-04 thành 4 subtask (P2-MOBILE-04A/B/C/D) — refactor tab + page mới + BLoC extend + balance card switch; thêm P2-01B/C (popup web admin) | (dev-spec C11) | Mobile, FE, PM |
| Update QA: 15 TC-SCR09 (thêm TC-SCR09-13 regression 2 tab cũ + TC-SCR09-15 regression FolderTabs các màn khác); 5 TC-006 mở rộng cho popup KM 2; tổng 137 TC | (qa-test-plan D2, D4) | QA |
v1.10 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Chốt DEC-031 — Customer app Ví KM 2 dùng screen riêng route mới /wallet/promotion2 (Option C), KHÔNG refactor wallet_screen.dart hiện hữu (2 tab FolderTabs) [ĐÃ ĐẢO ở v1.11 — chốt Option A] | Z) Nhật ký quyết định | FE Mobile, QA |
Chốt DEC-032 — Staff app AffiliateFor KHÔNG bổ sung WalletType.promotion2 (Option α) — danh sách ví CTV chọn nhận hoa hồng giữ nguyên [commission, promotion] | Z) Nhật ký quyết định | FE Mobile, BE |
Sửa wireframe SAI (Home + Wallet) — Home hiện có 4 card (DIVA/Commission/Reward Point/Bonus Point, KHÔNG có ví KM 1); wallet_screen.dart hiện chỉ 2 tab FolderTabs | (ui-spec B2.9.2) | FE Mobile |
Thêm 2 SCR mới: SCR-09-MOBILE-03 (WalletType enum + l10n shared); SCR-10-STAFF-01 (Staff customer detail balance KM 2) | (ui-spec B0.1, B2.10) | FE Mobile, QA |
| Thêm 9 task mobile vào dev-spec C11 Phase 2 (P2-MOBILE-01..09) — enum + l10n + screen mới + BLoC + home card + staff detail | (dev-spec C11) | Mobile, PM |
| Thêm 26 TC mobile vào QA (TC-SCR09-13/14 regression + TC-SCR09-ENUM-01..05 + TC-SCR10-01..07) — tổng 131 TC | (qa-test-plan D2, D4) | QA |
v1.9 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Chốt DEC-030 — KM 2 inherit cơ chế affiliate_config của KM 1; migration seed 3 row (*, wallet_promotion_2, FALSE) đồng bộ default production hiện hành (không prescribe nghiệp vụ commission, để admin quản lý qua UI matrix) | Z) Nhật ký quyết định | BE, QA, Ops |
| Cập nhật ASM-005 — KM 2 áp dụng cùng logic rank/loyalty như KM 1 (không thêm ASM riêng cho commission) | A6) Giả định | All |
| Cập nhật ASM-001 — khách XEM được KM2 trên app (không tự mua) | A6) Giả định | FE Mobile, QA |
| Mở PD-002 — KM2 in-scope Flutter customer app Phase 1: tab Ví KM 2 + home widget parity với KM 1 (SCR-09) | Quyết định còn mở | FE Mobile |
Thêm 4 surface UI vào B-FR012: SMS template vnd_wallet_balance, NegativeInvoicesTable, AffiliateConfiguration matrix, SCR-09 mobile | (ui-spec) | FE, BE |
v1.8 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Chốt DEC-027 — lot KHÔNG có trạng thái pending | Z) Nhật ký quyết định | All |
Chốt DEC-028 — schema wallet_km2_payment_attempt merge audit fields + lock primitives | Z) Nhật ký quyết định | BE, QA |
Chốt DEC-029 — balance realtime qua action get_customer_km2_balance | Z) Nhật ký quyết định | BE, FE |
Update FORMULA-001 dùng available_amount realtime (KHÔNG dùng wallet.amount) | A10) Công thức nghiệp vụ | BE, FE |
Rename 4 field config canonical (max_percent_per_order, allow_combine_km1, refund_fee_percent, refund_deadline_days) | A5) Yêu cầu chức năng (FR/AC) | FE, QA |
Lint vietnamese clean: chuẩn hoá calque headings + định dạng số tiền VN locale (chi tiết xem lint-vietnamese.sh output) | Toàn file | None |
Ví Khuyến Mãi 2 (Promotion Wallet 2)
Phiên bản: 1.11 Ngày: 15/05/2026 Tác giả: PO/BA Loại: Tính năng mới Độ phức tạp: L Module: wallet, ecommerce, settings, user
File này dùng để làm gì: chốt bức tranh business, scope, decision, FR và formulas ở mức PO/BA/TL/QA. Nên đọc trước:
decision-brief.md->EVIDENCE_PACK.md->SOURCE_OF_TRUTH.md->Tóm tắt điều hành->Z) Nhật ký quyết định->A5) Yêu cầu chức năng.Quy tắc ưu tiên: nếu nội dung file này xung đột với
SOURCE_OF_TRUTH.md, ưu tiênSOURCE_OF_TRUTH.md.
Lịch sử thay đổi
| Phiên bản | Ngày | Tác giả | Thay đổi |
|---|---|---|---|
| 1.11 | 15/05/2026 | PO/BA | Đảo DEC-031: từ Option C (screen riêng /wallet/promotion2) → Option A (thêm tab Ví khuyến mãi 2 thứ 3 vào wallet_screen.dart cùng cấp 2 tab hiện hữu — đồng nhất UX khách). Note kỹ thuật: dev tự chốt widget (refactor FolderTabs 3 tab / swap CustomTabbar / swap TabPageWidget). Thêm SCR-06-NEW-03 (web admin) — popup mới StatisticWalletPromotion2Popup.tsx lịch sử giao dịch Ví KM 2 parity với popup KM 1 (Option α — file riêng). Cập nhật dev-spec C11: chia P2-MOBILE-04 thành 4 subtask + thêm P2-01B/C (popup web admin); QA thêm TC-SCR09-13/15 regression + 5 TC-006 mở rộng (tổng 137 TC). |
| 1.10 | 15/05/2026 | PO/BA | Audit mobile gap (Flutter customer + staff app): phát hiện 2 wireframe SAI ở SCR-09 (Home thực tế 4 card không có ví KM 1; wallet_screen.dart thực tế chỉ 2 tab FolderTabs); chốt DEC-031 (Option C — screen riêng /wallet/promotion2, KHÔNG refactor wallet_screen.dart) [ĐÃ ĐẢO ở v1.11]; chốt DEC-032 (Option α — AffiliateFor KHÔNG bổ sung promotion2, danh sách CTV nhận hoa hồng giữ [commission, promotion]). Thêm 2 SCR mới: SCR-09-MOBILE-03 (enum + l10n shared) và SCR-10-STAFF-01 (Staff customer detail balance KM 2). Cập nhật dev-spec C11 thêm 9 task mobile (P2-MOBILE-01..09); QA thêm 26 TC mobile (tổng 131 TC). |
| 1.9 | 15/05/2026 | PO/BA | Audit gap so với ví KM 1 (87 file FE + 114 file BE + Flutter customer): chốt DEC-030 (KM 2 inherit cơ chế affiliate_config của KM 1 — seed 3 row FALSE đồng bộ production hiện hành); cập nhật ASM-001 (khách XEM được KM2 trên app), ASM-005 (KM 2 cùng logic rank như KM 1); mở PD-002 (SCR-09 mobile Phase 1); bổ sung 4 surface UI vào B-FR012: SMS template vnd_wallet_balance, NegativeInvoicesTable, AffiliateConfiguration matrix, SCR-09 mobile customer app. Spec không prescribe nghiệp vụ commission cho ví — đó là quyết định vận hành admin. Xem discussion-commission-affiliate.md. |
| 1.8 | 15/05/2026 | PO/BA | Phase 5 review fix-up: chốt DEC-027 (lot không có pending), DEC-028 (schema wallet_km2_payment_attempt merge audit + lock primitives), DEC-029 (balance realtime qua action get_customer_km2_balance); rename 4 field config canonical theo tên Dev; sửa lint vietnamese (calque + format ngàn + heading); thêm B0.9 Bảng kiểm kê tương tác; bổ sung TC-IDEMPOTENT-01..09 + TC-BALANCE-RACE-01..05 vào QA. PRD content business không đổi. |
| 1.7 | 30/04/2026 | PO/BA | Tham chiếu UI Spec v2.0 mới (B-PRE Discovery + B0.4 Field × Surface + B0.5 State × Screen + B0.6-0.8 wireframe/bilingual/schema + B2.x.1A UX nguyên tắc + B2.x.2.0 Variant + B2.x.7B/C/D Form/Concurrency/Network + B2.x.8A Role × Variant + B2.x.10A Error Taxonomy + B5A/B6A/B7A/B8A/B/C contract chuyên sâu + B-Microcopy/B-Voice/B-i18n/B-Versioning/B-Help + 12 nhóm trường hợp cá biệt + B-POST verification + B-QUALITY 45 rủi ro). PRD content business không đổi. |
| 1.6 | 28/04/2026 | PO/BA | Cập nhật Dynamic Permission v2; chốt behavior refund_km2_wallet và luồng Hoàn ví KM2 reuse Yêu cầu hoàn tiền hiện có |
| 1.5 | 28/04/2026 | PO/BA | Fix sau review đa góc nhìn: đồng bộ Decision Log với SOURCE_OF_TRUTH.md v1.4 và giữ DEC-013..DEC-024 là registry chuẩn cho traceability |
| 1.4 | 28/04/2026 | PO/BA | Đồng bộ PRD với template mới nhất: heading tiếng Việt-first, bỏ RACI/sign-off chi tiết khỏi PRD, giữ RACI/timeline canonical ở handoff.md |
| 1.3 | 28/04/2026 | PO/BA | Fix sau review đa góc nhìn: chốt seed mặc định tắt trước go-live, sửa permission visibility/RLS, polish tiếng Việt-first |
| 1.2 | 28/04/2026 | PO/BA | Fix theo po-ba-workflow mới: thêm Decision Brief/Go-Live, nâng profile L, đồng bộ route/default config/idempotency/traceability |
| 1.1 | 21/04/2026 | PO/BA | Canonicalized package: bổ sung EVIDENCE_PACK + SOURCE_OF_TRUTH, review lại theo codebase hiện tại |
| 1.0 | 24/03/2026 | PO/BA | Initial |
Đầu vào chuẩn (Canonical Inputs)
| File | Vai trò |
|---|---|
EVIDENCE_PACK.md | Ghi lại bằng chứng code/screen/config thật, tránh tự suy diễn |
SOURCE_OF_TRUTH.md | Khóa nguồn sự thật chuẩn và khóa giải pháp cho toàn package |
decision-brief.md | Cửa vào package cho PO/TL/Sếp; chỉ tóm tắt và trỏ sang file chịu trách nhiệm |
Hướng dẫn đọc (người đọc / trách nhiệm)
| Người đọc | Đọc phần nào | Trách nhiệm |
|---|---|---|
| PO/BA | PRD: A0 (tổng quan) → Z → A5 (FR) → A10 (công thức). Đặc tả UI: B0-B1 (kiểm kê + bản đồ màn hình) | Duyệt yêu cầu nghiệp vụ và luồng UX |
| UI/UX Designer | PRD: A0 (kiến trúc) → A5 FR-012 (màn hình ảnh hưởng). Đặc tả UI: B0-B9 + B-FR012 (delta màn hình hiện có) | Thiết kế màn mới (SCR-01,06,07,08) và rà delta (FR-012) |
| Tech Lead | PRD: A0 → Z (quyết định). Đặc tả kỹ thuật: C1-C4 (phạm vi, ảnh hưởng, mô hình dữ liệu) | Duyệt kiến trúc và rủi ro triển khai |
| FE Dev | Đặc tả UI: B2.1-B9 + B-FR012. Đặc tả kỹ thuật: C5-C6 (API, components) | Triển khai UI cho màn mới và delta |
| BE Dev | Đặc tả kỹ thuật: C1-C12. PRD: A10 (công thức) | Triển khai backend |
| QA | PRD: A5 (FR + AC). Kế hoạch kiểm thử: D1-D5 | Viết và chạy ca kiểm thử |
Tóm tắt điều hành
Ví KM 2 là cơ chế khuyến mãi theo gói nạp ưu đãi: khách mua Gói Ví KM2 với giá nhỏ → được cộng số tiền lớn vào ví → mỗi lần dùng dịch vụ chỉ được trả tối đa X% đơn hàng. Tái sử dụng flow thẻ trả trước hiện có, thêm wallet type mới VND_PROMOTION_2 với theo dõi theo từng lần mua, FIFO deduction và auto-expiry.
Tóm tắt bàn giao
Timeline chi tiết và RACI chuẩn nằm ở
handoff.md; cổng phát hành và rollback chuẩn nằm ởgo-live-checklist.md.
| Mốc | Mục tiêu | Phụ trách | Điều kiện |
|---|---|---|---|
| Phase 1 — Core (bán Gói Ví KM2 + thanh toán) | T+15 ngày | BE + FE Dev | — |
| Phase 2 — Customer-facing (profile + refund) | T+25 ngày | BE + FE Dev | Sau Phase 1 deploy |
| Phase 3 — Report dashboard | T+35 ngày | BE + FE Dev | Sau Phase 2 deploy |
Quyết định còn mở
| PD | Câu hỏi | Lựa chọn / giả định | Phụ trách | Hạn chót | Trạng thái | Kết luận |
|---|---|---|---|---|---|---|
| PD-001 | Vị trí Report dashboard trong menu | A) Module Report B) Settings → Ví KM 2 C) Module Wallet | PO | Trước Phase 3 | Mở | — |
| PD-002 | App khách Flutter hiển thị KM 2 | A) In-scope Phase 1 — full parity với KM 1 (tab Ví KM 2 + home widget); B) Defer Phase 2 | PO | Trước Phase 1 freeze | Đã chốt | A — In-scope Phase 1; spec ở SCR-09; backlog bỏ row "khách tự mua app" vì khách vẫn không tự mua (chỉ xem) |
Backlog giai đoạn sau (ngoài phạm vi hiện tại)
| # | Tính năng | Lý do defer |
|---|---|---|
| 1 | % max per Gói Ví KM2 (thay vì chung) | Hiện chỉ 1 config chung. Nâng cấp khi có nhiều Gói Ví KM2 với chiến lược khác nhau |
| 2 | Push notification nhắc hết hạn | Phase 1-2 chỉ hiển thị text trong profile. Push notification cần tích hợp OneSignal |
| 3 | Khách tự mua Gói Ví KM2 trên app | Hiện chỉ NV bán tại POS (ASM-001). Phase 1 app khách chỉ XEM (SCR-09); flow checkout in-app cần Phase 2+ |
| 5 | Cấu hình promotion_wallet_2_percent per tier | DEC-030 dùng cùng % với KM 1; nếu Diva muốn % riêng cho Gói KM 2 → Phase 2 thêm cột policy_tag.promotion_wallet_2_percent |
| 6 | UI mới commission_enabled per Gói (toggle ở SCR-02) | Hiện dùng affiliate_config matrix admin-level đủ kiểm soát; nếu cần granular per Gói → Phase 2 |
| 4 | Route/menu report dashboard | PD-001 chốt trước Phase 3, không block Phase 1/2 |
Z) Nhật ký quyết định
| ID | Nhóm | Quyết định | Lý do | Ngày | Trạng thái |
|---|---|---|---|---|---|
| DEC-001 | Kỹ thuật | Tạo loại ví riêng VND_PROMOTION_2, không mở rộng VND_PROMOTION | (A) Mở rộng VND_PROMOTION: trộn 2 nghiệp vụ, mọi query phải thêm điều kiện phân loại, rủi ro hồi quy KM1. (B) Loại ví riêng: tách biệt hoàn toàn, không ảnh hưởng production. Chọn B. | 24/03/2026 | Đã khóa |
| DEC-002 | Nghiệp vụ | Nhiều Gói Ví KM2, khác giá bán + giá trị nạp + hạn sử dụng | (A) 1 Gói Ví KM2 cố định: đơn giản nhưng không linh hoạt. (B) Nhiều loại: spa tạo Silver/Gold/Diamond với ưu đãi khác nhau. Chọn B — linh hoạt hơn, admin quản lý. | 24/03/2026 | Đã khóa |
| DEC-003 | Nghiệp vụ | % max chung cho cả Ví KM 2, không cấu hình riêng theo từng Gói Ví KM2 | (A) Theo từng Gói Ví KM2: Silver 20%, Gold 40%. Phức tạp khi khách có nhiều lần mua với % khác nhau. (B) Chung: 1 config admin. Chọn B — đơn giản, nhất quán. Nâng cấp sau nếu cần. | 24/03/2026 | Đã khóa |
| DEC-004 | Kỹ thuật | Mỗi lần mua Gói Ví KM2 tạo 1 bản ghi wallet_km2_lot riêng, FIFO, có hạn sử dụng. Qty=N → N bản ghi | (A) Cộng dồn 1 ví: đơn giản nhưng không theo dõi được từng lần mua, khó gắn hạn sử dụng. (B) Có bản ghi theo từng lần mua: rõ hạn sử dụng, rõ số dư, hỗ trợ FIFO. Chọn B — UI vẫn hiển thị như 1 ví tổng; engine bên trong dùng lot. | 24/03/2026 | Đã khóa |
| DEC-005 | Nghiệp vụ | Hết hạn = mất số dư | (A) Mất luôn: tạo urgency. (B) Chuyển sang KM1: khách vẫn dùng, nhưng thay đổi bản chất tiền. (C) Gia hạn: phức tạp. Chọn A — phổ biến trong ngành, khuyến khích khách quay lại sớm. | 24/03/2026 | Đã khóa |
| DEC-006 | UX | Cảnh báo hết hạn = nội dung cảnh báo trong tab Ví KM 2, không gửi thông báo đẩy | (A) Thông báo đẩy: cần tích hợp OneSignal, phức tạp. (B) Nội dung cảnh báo trong hồ sơ khách: đơn giản, NV thấy khi mở khách. Chọn B — Phase 1-2 đủ dùng. Thông báo đẩy đưa vào backlog. | 24/03/2026 | Đã khóa |
| DEC-007 | Kỹ thuật | Tái sử dụng luồng thẻ trả trước, thêm wallet_target để rẽ nhánh | (A) Tái sử dụng: 80% code có sẵn, NV không cần học lại. (B) Luồng mới: tách biệt nhưng dev gấp đôi. (C) Module riêng: quá mức cần thiết, trùng logic. Chọn A. | 24/03/2026 | Đã khóa |
| DEC-008 | Nghiệp vụ | Toggle KM1+KM2: tắt = chỉ dùng 1 trong 2, bật = cả 2 cùng đơn | (A) Luôn cho phép cả 2: rủi ro double discount. (B) Toggle: admin kiểm soát. Chọn B — mặc định tắt, bật khi spa muốn ưu đãi mạnh. | 24/03/2026 | Đã khóa |
| DEC-009 | Nghiệp vụ | Flag allow_promo_wallet_2 theo từng sản phẩm/dịch vụ | (A) Không giới hạn: mọi sản phẩm đều dùng KM2. (B) Flag theo từng sản phẩm: giống KM1, dễ kiểm soát. Chọn B — nhất quán với pattern KM1. | 24/03/2026 | Đã khóa |
| DEC-010 | Nghiệp vụ | Hoàn tiền đơn dịch vụ: hoàn vào bản ghi mua cũ, bản ghi hết hạn thì gia hạn, bản ghi đã hoàn thì tạo bản ghi mới | (A) Hoàn KM1 như phương án dự phòng: thay đổi bản chất tiền. (B) Hoàn vào bản ghi mua cũ + gia hạn: nhất quán và công bằng. (C) Tạo bản ghi mới khi bản ghi gốc đã hoàn. Chọn B+C — dùng audit trail (deduction records) để hoàn chính xác. | 24/03/2026 | Đã khóa |
| DEC-011 | Nghiệp vụ | Hoàn ví KM2: tỉ lệ hoàn (giá mua gói/giá trị ví) + phí xử lý hoàn ví có thể cấu hình | (A) Hoàn toàn bộ bằng tiền mặt: spa lỗ (300k mua → 3tr còn dư → hoàn 3tr?). (B) Tỉ lệ hoàn + phí xử lý hoàn ví: công bằng cho cả spa và khách. (C) Không cho hoàn: dễ gây phản ứng. Chọn B. | 24/03/2026 | Đã khóa |
| DEC-012 | Kỹ thuật | Audit trail wallet_km2_lot_deduction cho mọi thao tác trừ/hoàn | (A) Không có audit: khi hoàn phải suy ngược FIFO, dễ sai. (B) Có audit: truy đúng bản ghi đã trừ và số tiền đã trừ. Chọn B — truy vết đầy đủ. | 24/03/2026 | Đã khóa |
| DEC-013 | Nghiệp vụ | Nhân sự có quyền xem thông tin khách được xem Ví KM2 khi truy cập profile khách | (A) Chỉ nhóm quyền cao: an toàn nhưng NV không tư vấn được. (B) Theo quyền customer_management:access + branch scope: nhất quán với quyền xem thông tin khách hiện tại. Chọn B. | 24/03/2026 | Đã khóa |
| DEC-014 | Nghiệp vụ | Khách mua Gói Ví KM2 ở chi nhánh A, dùng được ở bất kỳ chi nhánh nào | (A) Chỉ dùng ở chi nhánh mua: hạn chế. (B) Dùng khác chi nhánh: linh hoạt cho khách. Chọn B — branch_id trên bản ghi mua chỉ phục vụ báo cáo. | 24/03/2026 | Đã khóa |
| DEC-015 | Kỹ thuật | expired_at lưu dạng end_of_day (23:59:59) ngày cuối cùng | (A) Timestamp chính xác: cron có thể miss. (B) End-of-day: cron 00:05 luôn bắt đúng. Chọn B. | 24/03/2026 | Đã khóa |
| DEC-016 | Kỹ thuật | Snapshot giá mua gói vào wallet_km2_lot (trùng prepaid_card info) | (A) Join trực tiếp prepaid_card: đơn giản nhưng vướng cross-DB wallet↔ecommerce, Hasura không hỗ trợ cross-DB join. Nếu Admin sửa giá sau khi bán → dữ liệu cũ sai. (B) Snapshot vào wallet_km2_lot: thêm cột package_name, package_price, wallet_value. Giải quyết cả cross-DB lẫn tính toàn vẹn dữ liệu. Chọn B. | 24/03/2026 | Đã khóa |
| DEC-017 | Nghiệp vụ | KHÔNG cho mua Gói Ví KM2 bằng trả góp (installment) | (A) Cho phép: khách trả góp 500k/3 tháng nhưng ví đã có 5tr ngay → rủi ro khách dùng hết rồi ngưng trả góp. (B) Không cho phép: ẩn lựa chọn trả góp khi đơn có Gói Ví KM2. Chọn B — rủi ro tài chính quá lớn. | 24/03/2026 | Đã khóa |
| DEC-018 | Kỹ thuật | FIFO deduction dùng Go action handler với direct PostgreSQL connection (không qua Hasura GraphQL) | (A) Hasura mutations: không atomic, partial deduction gây inconsistency. (B) Raw SQL transaction trong Go handler: BEGIN; SELECT FOR UPDATE; UPDATE lots; INSERT deductions; UPDATE wallet; COMMIT. Chọn B — đảm bảo atomicity. | 24/03/2026 | Đã khóa |
| DEC-019 | Nghiệp vụ | Thanh toán nhiều lần cho Gói Ví KM2: tiền vào ví theo tỉ lệ đã trả, bản ghi mua tạo từ lần trả đầu tiên | (A) Chỉ tạo bản ghi khi trả đủ 100%: đơn giản nhưng khách không thấy giá trị ví khi trả một phần. (B) Tạo bản ghi ngay lần trả đầu, số dư tăng dần: giống luồng thẻ trả trước hiện tại, hạn sử dụng bắt đầu từ lần trả đầu. Chọn B. | 24/03/2026 | Đã khóa |
| DEC-020 | Bàn giao | Package vận hành theo profile L vì chạm wallet/payment, scheduler, report/fund/print và rollback | (A) Giữ M: thiếu cổng go-live cho tiền/ví. (B) Nâng L: thêm checklist phát hành, rollback, monitoring. Chọn B. | 28/04/2026 | Đã khóa |
| DEC-021 | UX/Cấu hình | Settings KM2 dùng lại route hiện có /s/internal-settings/promotion-wallet; report route là PD-001 cho Phase 3 | (A) Tạo route settings/report mới ngay: nhanh ghi spec nhưng lệch code/menu hiện có. (B) Dùng lại màn settings hiện có và để report route cho PO chốt. Chọn B. | 28/04/2026 | Đã khóa |
| DEC-022 | Kỹ thuật | FIFO deduction phải có idempotency key và không trừ lot đã hết hạn dù scheduler miss | (A) Chỉ dùng DB lock: chống race nhưng chưa chống retry/double submit và expired miss. (B) Thêm idempotency + expiry guard trong transaction. Chọn B. | 28/04/2026 | Đã khóa |
| DEC-023 | Vận hành/Cấu hình | Migration seed cấu hình KM2 ở trạng thái tắt; PO/Admin chỉ bật ở go-live sau khi kiểm tra sẵn sàng đạt | (A) Seed bật ngay: nhanh nhưng rủi ro phương thức thanh toán xuất hiện trước khi seed/test đủ. (B) Seed disabled=true, bật ở E4.7: an toàn hơn cho tính năng ví/tiền. Chọn B. | 28/04/2026 | Đã khóa |
| DEC-024 | Bảo mật | Cross-branch usage không đồng nghĩa với cross-branch visibility qua GraphQL | (A) Role user đọc mọi lot/deduction: dễ dev nhưng lộ dữ liệu. (B) UI/GraphQL giữ Staff/Manager/Admin scope; Go action dùng service permission để redeem cross-branch. Chọn B. | 28/04/2026 | Đã khóa |
| DEC-025 | Bảo mật/Permission | KM2 áp dụng Dynamic Permission v2, không hard-code role name | Quyền thực tế phải lấy từ role_module.actions + portal + branch_mode + backend enforcement. Role chỉ là default seed; grant/revoke qua Dynamic Permission UI phải ảnh hưởng menu/button/API sau refresh/relogin. | 28/04/2026 | Đã khóa |
| DEC-026 | Nghiệp vụ/Hoàn tiền | Dùng refund_km2_wallet, nhãn "Hoàn ví KM2", dùng lại luồng Yêu cầu hoàn tiền hiện có | Tránh lẫn với CRM case. KM2 chỉ thêm loại xử lý mới vào engine Yêu cầu hoàn tiền hiện có; không dựng module duyệt riêng. | 28/04/2026 | Đã khóa |
| DEC-027 | Kỹ thuật/Lifecycle | wallet_km2_lot KHÔNG có trạng thái pending. ENUM chỉ gồm active/exhausted/expired/refunded. Lot chỉ được tạo khi khách trả ≥ 1 đồng cho đơn nạp Gói Ví KM2 | (A) Có pending: tạo lot ngay khi tạo prepaid_order để track sớm, nhưng phải xử lý lot rác khi đơn huỷ, schema activated_at phải NULL → break dev-spec, thêm state cần test/migrate. (B) Không pending: lot chỉ tồn tại khi có thanh toán thật, đơn còn nợ thì badge ở prepaid_order. Chọn B — đơn giản state machine, không lot dangling, đồng bộ với DEC-019. Đơn còn nợ hiển thị qua prepaid_order.status='pending' ở SCR-03 và inline "Đã nạp X/Y" trong cột "Đã nạp" của SCR-06. | 15/05/2026 | Đã khóa |
| DEC-028 | Kỹ thuật/Schema | Schema wallet_km2_payment_attempt chốt merge: PK id UUID riêng + UNIQUE payment_attempt_id UUID; có đủ lock_holder, lock_expires_at, requested_at, error, request_hash; status enum processing/completed/failed/expired; cross-check verify (order_id, customer_id, user_id) chống bypass FE | (A) Schema cũ (composite (order_id, payment_attempt_id) UNIQUE, status in_progress/succeeded/failed): không cover lock TTL → fail TG-003 test scenarios. (B) Schema mới chỉ có lock primitives nhưng thiếu Diva audit fields: vi phạm pattern. (C) Merge: lấy điểm mạnh cả hai — Diva pattern (id, deleted_at, created_by/updated_by) + lock primitives (TG-003) + cross-check verify (chống bypass). Chọn C — không thay nghiệp vụ, khoá TG-003 gate, không phải refactor schema sau Phase 1. | 15/05/2026 | Đã khóa |
| DEC-029 | Kỹ thuật/Nguồn balance | Balance Ví KM 2 cho UI auto-fill + chip eligibility check lấy realtime từ action get_customer_km2_balance (SUM(lot.balance) WHERE status='active' AND expired_at > NOW()), KHÔNG đọc wallet.amount trực tiếp. wallet.amount chỉ là cached value cho report/KPI/dashboard | (A) Dùng wallet.amount: đơn giản nhưng có race window giữa lot expired và scheduler 00:05 → UI hiển thị 5tr, BE deduct chỉ được 3tr → sai tiền + UX bug. (B) Sync wallet.amount realtime qua trigger: phức tạp, ảnh hưởng pattern wallet hiện có. (C) Action realtime: đóng race window vĩnh viễn, reuse pattern Diva (get_customer_km2_lots đã có), không phải refactor wallet.amount. Chọn C — match DEC-005 "hết hạn = mất số dư" enforce realtime, không phụ thuộc scheduler. | 15/05/2026 | Đã khóa |
| DEC-030 | Kỹ thuật/Affiliate | KM 2 áp dụng cùng cơ chế affiliate_config matrix như KM 1 (gate ở InsertInvoiceAffiliate — services/ecommerce-api/event/invoice_insert_update.go:1287-1298). Migration KM 2 seed 3 row `(service | cosmetic | prepaid, wallet_promotion_2, FALSE)— giá trịFALSEđồng bộ với default production hiện hành của Diva chowalletvàwallet_promotion(cả 6 ô đang FALSE — xem screenshot UIAffiliateConfiguration.tsx). Admin có thể bật/tắt từng ô qua UI /s/internal-settings/affiliate` → tab "Cấu Hình Chung" → block "Cấu hình hình thức nhận hoa hồng". | (A) Tạo cơ chế skip mới riêng cho KM 2 (hard-code trong code Go): tăng phức tạp, drift khỏi pattern hiện hữu, dead config UI. (B) Inherit cơ chế KM 1 — seed config FALSE đồng bộ với KM 1: không touch code Go, admin có quyền điều chỉnh, dùng UI matrix sẵn có. Chọn B. Spec KHÔNG prescribe nghiệp vụ "ví có/không có commission" — đó là quyết định vận hành của admin. |
| DEC-031 | UX/Mobile | Customer Flutter app Ví KM 2 thêm tab thứ 3 vào wallet_screen.dart hiện có (cùng cấp với 2 tab hiện hữu: "Ví thẻ trả trước" + "Ví khuyến mãi"). Card Home "Ví khuyến mãi 2" (home_wallet.dart:49-77 — card thứ 5 trong list 4 card hiện có) tap → mở wallet_screen.dart với initialTabIndex trỏ tab Ví KM 2. KHÔNG tạo screen riêng / route mới. Note kỹ thuật cho dev (KHÔNG phải decision nghiệp vụ): wallet_screen.dart:213-227 đang dùng widget FolderTabs (2 tab cứng — core/lib/presentation/common_widget/folder_tabs.dart); để thêm tab thứ 3 dev tự chốt 1 trong 3 phương án implementation: refactor FolderTabs mở rộng 3 tab, hoặc swap sang CustomTabbar (chip pill) / TabPageWidget (Material TabBar) — cả 3 đã có trong codebase core/. Decision này thuộc dev/tech-lead, KHÔNG ràng buộc PO. | (A) Thêm tab thứ 3 vào wallet_screen.dart: nhất quán UX khách — khách quen mở 1 màn "Ví của tôi" và switch giữa các ví; tap card home → mở wallet_screen ở đúng tab; phù hợp PD-002 "parity với pattern KM 1 hiện có". (B) Dropdown filter trong tab Promotion hiện có: phải tách logic balance theo lựa chọn dropdown; semantics rối (1 tab 2 loại ví khác nhau). (C) Screen riêng route mới: tách biệt nhưng KHÔNG nhất quán UX hiện hữu — khách phải nhớ thêm 1 entry point ngoài "Ví của tôi". Chọn A — đồng nhất UX, đồng bộ pattern KM 1. | 15/05/2026 | Đã khóa |
| DEC-032 | Nghiệp vụ/Affiliate | App staff (affiliate_for.dart:103-116) KHÔNG bổ sung WalletType.promotion2 vào danh sách ví CTV chọn nhận hoa hồng. Danh sách giữ nguyên [commission, promotion]. Khi admin bật affiliate_config(*, wallet_promotion_2, TRUE) qua UI matrix, BE vẫn ghi nhận hoa hồng vào ví commission mặc định của CTV (theo user.walletReceiveCommission đã chọn) — KHÔNG cho CTV chọn nhận hoa hồng VÀO ví KM 2. | (α) Giữ nguyên [commission, promotion]: KM 2 chỉ là benefit khách (parity nghiệp vụ KM 1 — KM 1 cũng có wallet_promotion trong list nhưng business intent là "ví KM của CTV", không phải "ví KM khách"); Ví KM 2 là dạng "Gói trả trước" thuộc khách, không phù hợp làm ví commission của CTV. (β) Mở rộng thành [commission, promotion, promotion2]: cho CTV chọn nhận hoa hồng vào ví KM 2 — drift semantics (ví khách thành ví CTV), risk schema (chưa rõ lot ownership). Chọn α — semantics rõ ràng, không vỡ pattern hiện có; nếu Phase 2+ admin muốn CTV nhận vào KM 2 thì cân nhắc decision riêng. | 15/05/2026 | Đã khóa |
A) PRD
A0) Tổng quan tính năng
Đọc section này trước — nắm toàn bộ feature trong 2 phút. Chi tiết xem các section A1-A10 bên dưới.
Ý tưởng cốt lõi
Khách mua Gói Ví KM2 (VD: 500k) → được 5 triệu vào Ví KM 2 → mỗi lần dùng dịch vụ hệ thống chỉ cho trả tối đa X% đơn hàng (VD: max 20%) → khách phải quay lại nhiều lần để dùng hết → tăng giữ chân khách và tạo cảm giác "đã có tiền trong ví" thay vì chỉ được giảm giá một lần.
Kiến trúc tổng thể
+-----------------------------------+
| Cấu hình Admin |
| - Tạo Gói Ví KM2 (prepaid card) |
| - Cấu hình Ví KM2 (max%, toggle) |
| - Chính sách Hoàn ví KM2 |
+-----------------+-----------------+
|
+---------------------------------+--------------------------------+
| | |
v v v
+---------+ +--------------+ +--------------+
| Bán Gói Ví KM2 | | Thanh toán | | Hoàn ví KM2 |
| (POS) | | đơn hàng | | hoàn ví KM2 |
| | | | | |
| Dùng lại| | Logic max% | | Yêu cầu hoàn |
| đơn nạp | | FIFO lot | | + loại KM2 |
| tiền | | Toggle KM1+2 | | |
+----+----+ +------+-------+ +------+-------+
| | |
v v v
+---------------------------------------------------------------------+
| Wallet Engine |
| wallet_type: VND_PROMOTION_2 |
| wallet_km2_lot: theo dõi từng lần mua Gói Ví KM2, hạn dùng, FIFO |
| wallet_km2_lot_deduction: audit trail từng lần trừ/hoàn |
| transaction / transaction_request: ghi log như các ví khác |
| Expiry Scheduler: Hasura cron trigger đánh dấu bản ghi hết hạn |
+---------------------------------------------------------------------+Mapping sang tài liệu chi tiết:
| Khối | PRD | Đặc tả UI | Đặc tả kỹ thuật |
|---|---|---|---|
| Cấu hình Admin | FR-001, FR-002 | SCR-01, SCR-02 | C4, C5, C8 |
| Bán Gói Ví KM2 (POS) | FR-003 | SCR-03 | C5, C6 |
| Thanh toán | FR-004, FR-005 | SCR-04, SCR-05 | C3 FORMULA-001, C5 |
| Hoàn tiền / Hoàn ví KM2 | FR-008, FR-009 | SCR-07 | C3 FORMULA-002, C5 |
| Wallet Engine | FR-007 | SCR-06 | C4, C7, C9 |
Quyết định chính (tóm tắt theo nhóm)
| Nhóm | Quyết định | Ref |
|---|---|---|
| Kiến trúc | Loại ví riêng VND_PROMOTION_2 (tách biệt KM1). Tái sử dụng luồng thẻ trả trước + rẽ nhánh wallet_target. Snapshot giá mua gói vào bản ghi mua (tránh cross-DB + sai dữ liệu lịch sử). FIFO dùng Go action handler + direct PostgreSQL tx, idempotency và expiry guard | DEC-001, DEC-007, DEC-016, DEC-018, DEC-022 |
| Gói Ví KM2 | Nhiều gói bán cho khách (Silver/Gold/Diamond), khác giá + giá trị ví + hạn sử dụng. % max chung cho cả ví, không cấu hình riêng theo từng gói. KHÔNG cho mua trả góp | DEC-002, DEC-003, DEC-017 |
Lần mua Ví KM2 (lot kỹ thuật) | Mỗi lần khách mua 1 Gói Ví KM2 tạo 1 bản ghi wallet_km2_lot; qty=N → N bản ghi. FIFO trừ bản ghi cũ trước. Hết hạn = mất số dư. Audit trail cho mọi thao tác. Thanh toán nhiều lần: bản ghi tạo từ lần trả đầu, balance tăng dần theo tỉ lệ | DEC-004, DEC-005, DEC-012, DEC-019 |
| Thanh toán | Max X% trên eligible items. Toggle KM1+KM2 (tắt = chỉ 1 trong 2). Cross-branch OK | DEC-003, DEC-008, DEC-009, DEC-014 |
| Hoàn tiền | Đơn DV: hoàn vào bản ghi mua cũ (query audit trail), hết hạn → gia hạn, đã hoàn → tạo bản ghi mới. Hoàn ví KM2: tỉ lệ (giá mua gói / giá trị ví) + phí xử lý hoàn ví có thể cấu hình | DEC-010, DEC-011 |
| Bàn giao/Go-live | Vì liên quan tiền/ví, package phải có Go-Live checklist, seed cấu hình mặc định tắt, rollback bằng cách tắt cấu hình/phương thức thanh toán và monitoring ngày đầu | DEC-020, DEC-023 |
| Quyền dữ liệu | Cross-branch usage chỉ xử lý trong Go action; UI/GraphQL vẫn giữ Staff/Manager/Admin scope theo quyền truy cập khách/branch | DEC-014, DEC-024 |
Logic kế toán quan trọng
Khi mua Gói Ví KM2 — ghi nhận giống thẻ trả trước hiện tại:
Khách trả 500k mua Gói Gold → Thực thu = 500k (giá mua gói)
→ Ví KM 2 = +5.000.000đ (toàn bộ vào KM2)
→ Ví VND = 0đ, Ví KM 1 = 0đ
(Khác KM1: KM1 chia base_value→VND + bonus→KM1. KM2: toàn bộ vào KM2)Khi dùng KM2 thanh toán dịch vụ — giống wallet/wallet_promotion:
Đơn 1tr, eligible 700k, max 20%
→ KM2 trả 140k (không tính thực thu, không tính customer points)
→ Tiền mặt 860k (tính thực thu bình thường)
→ Doanh số = 1.000.000đ | Thực thu = 860,000đBảng FR tóm tắt
| FR | Mô tả | Mức ưu tiên | Giai đoạn |
|---|---|---|---|
| FR-001 | Config Ví KM 2 (max%, toggle, refund policy) | Must | 1 |
| FR-002 | Tạo/sửa Gói Ví KM2 (thêm wallet_target + expiry) | Must | 1 |
| FR-003 | Bán Gói Ví KM2 → tạo bản ghi theo từng lần mua (reuse flow nạp tiền, conditional cột KM2) | Must | 1 |
| FR-004 | Thanh toán bằng KM2 (auto-fill max%, FIFO, deduction records) | Must | 1 |
| FR-005 | Flag allow_promo_wallet_2 per sản phẩm/dịch vụ | Must | 1 |
| FR-006 | Tab Ví KM 2 trong profile khách (danh sách Gói Ví KM2 đã mua, cảnh báo hết hạn) | Must | 2 |
| FR-007 | Scheduler tự động hết hạn lot (cron daily) | Must | 1 |
| FR-008 | Refund đơn DV → hoàn vào lot (query deduction records) | Should | 2 |
| FR-009 | Hoàn ví KM2 qua Yêu cầu hoàn tiền | Should | 2 |
| FR-010 | Report dashboard (KPI, top khách, Gói Ví KM2 sắp hết hạn) | Could | 3 |
| FR-011 | ZNS thông báo (kích hoạt Gói Ví KM2, nhắc hết hạn) | Could | 2+ |
| FR-012 | Hiển thị Ví KM 2 trong đơn hàng hiện có (summary, bảng, hoá đơn in) | Must | 1 |
Mô hình dữ liệu tóm tắt
| Bảng | Loại | Mục đích |
|---|---|---|
wallet_km2_lot | MỚI | Bản ghi theo từng lần mua Gói Ví KM2: balance, expiry, status, branch_id |
wallet_km2_lot_deduction | MỚI | Audit trail mỗi lần trừ/hoàn: lot_id, order_id, amount, type |
wallet_km2_config | MỚI | Config chung: max%, toggle KM1+KM2, refund policy |
prepaid_card | SỬA | +wallet_target, +expiry_months |
product / service | SỬA | +allow_promo_wallet_2 |
wallet_type + payment_gateway | SEED | INSERT VND_PROMOTION_2 + wallet_promotion_2 |
Ảnh hưởng code hiện tại
- 2 hàm Go High-risk:
CheckWalletBalanceEnough+GetWalletTypeFromPaymentMethod(hardcode wallet types) - 10+ files hardcode
"wallet_promotion"cần grep + review (xem dev-spec C2) - Invoice struct thiếu field
WalletPromotionAmount— bug tiềm ẩn, fix luôn Phase 1
Ma trận ảnh hưởng tính năng liên quan
Nguồn chuẩn chi tiết nằm ở SOURCE_OF_TRUTH.md mục 5.2-5.3. PRD chỉ chốt hành vi PO/BA cần duyệt để team không mở rộng scope sai.
| Tính năng hiện có | Ảnh hưởng? | Bổ sung bắt buộc cho KM2 | Không ảnh hưởng / không đổi |
|---|---|---|---|
| Ví khuyến mãi hiện tại (KM1) | Có, vì shared wallet/payment/report code | Thêm wallet/payment method KM2 riêng, label/report riêng | Không đổi số dư, công thức, dữ liệu KM1 |
| Settings Ví khuyến mãi | Có | Mở rộng page hiện có với config KM2, readiness và default disabled | Không tạo route settings mới Day-1 |
| Thẻ trả trước / đơn nạp tiền | Có | Thêm loại Gói Ví KM2, wallet_target, expiry_months, summary "Tiền vào Ví KM2", cấm trả góp | Không đổi thẻ VND/KM1 |
| Thanh toán đơn DV/mỹ phẩm | Có | Thêm payment method KM2, max%, eligibility, FIFO deduction, idempotency | Không đổi cash/card/bank/wallet/KM1 |
| Product/Service detail | Có | Thêm allow_promo_wallet_2 riêng | Không overload allow_promo_wallet của KM1 |
| Customer profile | Có | Thêm tab/card/danh sách Gói Ví KM2 đã mua, expiry warning, CTA "Hoàn ví KM2" theo quyền | Không mở quyền xem khách ngoài scope |
| Yêu cầu hoàn tiền | Có, bắt buộc | Thêm option "Hoàn ví KM2", behavior refund_km2_wallet, field lot_id, km2_deduct_amount, suggested_refund_amount, refund_method, policy_snapshot; approve/payment đi qua flow hiện có | Không tạo approval module riêng; không dùng tên legacy cũ |
| Refund đơn đã dùng KM2 | Có | Hoàn lại đúng lot dựa trên wallet_km2_lot_deduction; hết hạn thì gia hạn, lot refunded thì tạo lot mới | Không hoàn về ví chính/KM1 |
| Hoá đơn, quỹ, actual revenue | Có | Thêm amount/label KM2; KM2 paid amount không tính thực thu/fund, bán Gói Ví KM2 tính thực thu theo giá mua gói | Không cộng value_into_wallet vào doanh thu |
| Báo cáo/export | Có | Thêm cột/field KM2 riêng cho prepaid, DV/NV, actual revenue, ERP/export | Không đổi nghĩa wallet_promotion_amount của KM1 |
| Notification/ZNS/SMS | Có điều kiện | Thêm biến/template KM2 cho nạp Gói Ví KM2, thanh toán, expiry nếu bật thông báo | Không reuse biến KM1 cho KM2 |
| Rank/loyalty, appointment, kho, affiliate/referral, CMS gift/event, HR cơ bản | Không trực tiếp | Chỉ kiểm tra skip list doanh thu nếu module đọc payment method | Không thêm rule nghiệp vụ mới cho các module này |
Tóm tắt giai đoạn
Timeline chi tiết, RACI và blocker chuẩn nằm trong handoff.md; readiness/deploy/rollback chuẩn nằm trong go-live-checklist.md.
| Phase | Nội dung | Điều kiện chuyển phase |
|---|---|---|
| Phase 1 — Core | DB + bán Gói Ví KM2 + thanh toán + scheduler + config + FR-012 Phase 1 | Go-live E1/E3 pass, payment smoke test pass |
| Phase 2 — Customer-facing | Profile khách + refund + hoàn ví KM2 + FR-012 Phase 2 | Phase 1 QA pass, không còn bug critical/major |
| Phase 3 — Report | Dashboard báo cáo | PO chốt PD-001 route/menu report |
A1) Bản thiết kế tóm tắt
| Trường | Giá trị |
|---|---|
| Feature | Ví Khuyến Mãi 2 (Promotion Wallet 2) |
| Loại | New Feature |
| Platform | Web Admin (diva-admin) — POS + Admin |
| Module ảnh hưởng | FE: settings, ecommerce, user / BE: wallet-api, ecommerce-api / DB: wallet, ecommerce |
A2) Bối cảnh
Hiện trạng
- Ví KM 1 (
VND_PROMOTION): nạp bao nhiêu dùng bấy nhiêu, trừ thẳng 100% vào đơn hàng - Thẻ trả trước: mua thẻ → tiền chia vào Ví VND (base) + Ví KM 1 (bonus)
- Không có cơ chế giới hạn % sử dụng per đơn, không có lot tracking, không có auto-expiry
Kỳ vọng
- Ví KM 2 (
VND_PROMOTION_2): mua Gói Ví KM2 → toàn bộ tiền vào Ví KM 2 (theo lần mua Ví KM2, có hạn sử dụng) - Mỗi lần thanh toán chỉ trừ tối đa X% đơn hàng eligible → khách quay lại nhiều lần
- FIFO deduction across lots, auto-expiry, audit trail cho refund chính xác
Kiến trúc tổng thể, quyết định chính, logic kế toán, bảng FR, data model, timeline → xem A0) Tổng quan tính năng phía trên.
A3) Mục tiêu và chỉ số thành công
| Mục tiêu | Chỉ số | Mục tiêu đo |
|---|---|---|
| Tăng tần suất quay lại | Số lần visit/khách/tháng (khách có Gói Ví KM2) | +30% so với khách không có Ví KM2 |
| Thu trước tiền | Doanh thu bán Gói Ví KM2/tháng | > 0 (baseline Phase 1) |
| Tăng retention | Tỉ lệ sử dụng Ví KM2 (% đã dùng/tổng nạp) | > 50% sau 3 tháng |
| Kiểm soát discount | % discount thực tế per đơn | Không vượt max% config |
A4) Chân dung người dùng
| Chân dung | Vai trò | Việc cần làm (JTBD) | Tần suất |
|---|---|---|---|
| Khách hàng | Người dùng Ví KM2 | Muốn được ưu đãi khi dùng dịch vụ, cảm giác "xài tiền mình" | Mỗi lần đến spa |
| NV bán hàng (Sale/Staff) | Bán Gói Ví KM2 + thanh toán | Tư vấn khách mua Gói Ví KM2, thanh toán đơn hàng bằng Ví KM 2 | Hàng ngày |
| Manager | Duyệt refund + xem báo cáo | Kiểm soát hoạt động bán Gói Ví KM2, duyệt hoàn tiền/hoàn ví KM2 | Hàng tuần |
| Admin | Cấu hình hệ thống | Tạo Gói Ví KM2, cấu hình max%, toggle, refund policy | Khi cần thay đổi |
A5) Yêu cầu chức năng
FR-001: Cấu hình Ví KM 2 (Ref: DEC-003, DEC-008)
Mức ưu tiên: Must | SCR: SCR-01
AC:
- [ ] Admin vào Settings → Ví KM 2, bật/tắt Ví KM 2 (disabled toggle)
- [ ] Admin nhập tối đa % trên đơn hàng (1-100, số nguyên, mặc định 20)
- [ ] Admin bật/tắt toggle "Cho phép dùng KM1 + KM2 cùng đơn" (mặc định: tắt)
- [ ] Default sau migration: config KM2 được seed ở trạng thái tắt (
disabled=true). PO/Admin chỉ bật sangdisabled=falsetrong go-live khi readiness pass - [ ] Default rule sau khi bật: max% = 20, không cho dùng KM1+KM2 cùng đơn, cho phép hoàn ví KM2, phí xử lý hoàn ví = 20%, thời hạn hoàn = 30 ngày, gia hạn refund DV = 30 ngày
- [ ] Admin bật/tắt "Cho phép Hoàn ví KM2", nhập phí xử lý hoàn ví % (0-100), thời hạn hoàn (ngày), gia hạn lot khi refund DV (ngày)
- [ ] Lưu config thành công → toast "Lưu cài đặt thành công"
- [ ] Config áp dụng ngay lập tức cho mọi đơn hàng mới
FR-002: Tạo/sửa Gói Ví KM2 (Ref: DEC-002, DEC-007)
Mức ưu tiên: Must | SCR: SCR-02
AC:
- [ ] Admin vào Settings → Thẻ trả trước → Tạo mới, thấy thêm radio "Ví đích": Ví VND (mặc định) / Ví KM 2
- [ ] Khi chọn "Ví KM 2", hiện thêm field "Hạn sử dụng" (số tháng, bắt buộc, > 0)
- [ ] Giá bán (value) và Giá trị vào ví (value_into_wallet) nhập độc lập, value_into_wallet >= value
- [ ] Lưu thành công → thẻ xuất hiện trong danh sách với cột "Ví đích" = KM 2
FR-003: Bán Gói Ví KM2 qua flow thẻ trả trước (Ref: DEC-004, DEC-007)
Mức ưu tiên: Must | SCR: SCR-03
Luồng (theo layout UI thực tế — form "Tạo Mới Nạp Tiền"):
NV mở POS → Nạp Tiền → Tạo Mới Nạp Tiền
│
▼
Chọn chi nhánh, nguồn đơn hàng, chiến dịch tiếp thị (giữ nguyên)
│
▼
THÔNG TIN NẠP TIỀN — "+ Thêm thẻ nạp" (nhiều dòng, mỗi dòng 1 loại thẻ)
│
│ LOẠI THẺ NẠP │ SL │ CẦN TT │ NẠP VÍ DIVA │ NẠP VÍ KM │ NẠP VÍ KM 2
│ Thẻ trả trước 10tr │ 1 │ 10,000k │ 10,000k │ 1,500k │ 0
│ Gói Gold (6 tháng) │ 2 │ 1,000k │ 0 │ 0 │ 10,000k
│ TỔNG │ │ 11,000k │ 10,000k │ 1,500k │ 10,000k
│
│ ★ Cột "NẠP VÍ KM 2" chỉ hiện khi có ≥1 thẻ wallet_target = KM2
│
▼
PHƯƠNG THỨC THANH TOÁN (sidebar phải):
│ [Chuyển khoản] [Thanh toán thẻ] [Tiền mặt] [Trả góp]
│ Tiền vào ví DIVA: 10.000.000đ
│ Tiền vào ví khuyến mãi: 1,500.000đ
│ Tiền vào ví KM 2: 10.000.000đ ← chỉ hiện khi > 0
│ Số tiền cần thanh toán: 11.000.000đ
│
▼
Bấm hoàn thành → Backend xử lý theo từng dòng thẻ:
│
├─ Dòng 1 (wallet_target='VND') → flow cũ (chia VND + KM1)
│
└─ Dòng 2 (wallet_target='VND_PROMOTION_2') → flow mới:
├─ Cộng 10.000.000đ vào wallet VND_PROMOTION_2
├─ Tạo 2 bản ghi mua riêng biệt (mỗi bản ghi 5tr, HSD +6 tháng)
├─ Gửi ZNS: "Kích hoạt 2 Gói Gold, tổng 10tr, HSD: 24/09/2026"
└─ Commission NV → giữ nguyên flow hiện tạiAC:
- [ ] NV tạo đơn nạp tiền, chọn Gói Ví KM2 từ dropdown (VD: Gói Gold 500k → 5.000.000đ)
- [ ] NV chọn qty (VD: 2), hệ thống tính: CẦN TT = 1,000k, NẠP VÍ KM 2 = 10,000k
- [ ] Cột "NẠP VÍ KM 2" chỉ hiện khi đơn có ≥1 thẻ có wallet_target = VND_PROMOTION_2
- [ ] Sidebar: dòng "Tiền vào ví KM 2" chỉ hiện khi tổng > 0
- [ ] Cho phép mix thẻ VND + Gói Ví KM2 trong cùng 1 đơn — mỗi dòng xử lý theo wallet_target riêng
- [ ] NV thanh toán lần đầu → hệ thống tạo bản ghi mua ngay (mỗi Gói Ví KM2 1 bản ghi
wallet_km2_lot, hạn sử dụng tính từ lần trả đầu tiên) - [ ] Thanh toán nhiều lần: tiền vào Ví KM 2 theo tỉ lệ đã trả (giống flow thẻ trả trước hiện tại)
- [ ] VD: Gói Gold 500k → 5tr, qty=2, tổng 1,000k. Trả lần 1: 600k (60%) → ví nhận 6tr. Trả lần 2: 400k → ví nhận 4tr. Tổng 10tr ✓
- [ ] Công thức per lần trả:
km2_credit = (customer_paid_amount / total_order_amount) × total_value_into_wallet - [ ] Số dư bản ghi mua tăng dần theo mỗi lần trả. Trạng thái
wallet_km2_lot.status = activetừ lần trả đầu tiên - [ ] Gửi ZNS: "Bạn đã kích hoạt 2 Gói Gold, tổng 10.000.000đ, HSD: 24/09/2026" (gửi lần trả đầu tiên)
- [ ] Commission NV tính bình thường trên giá mua gói (giống flow thẻ trả trước hiện tại)
- [ ] Khi Gói Ví KM2 được chọn, cột NẠP VÍ DIVA và NẠP VÍ KM hiển thị 0 cho dòng đó
Logic kế toán (QUAN TRỌNG — phân biệt thực thu vs giá trị ví):
Ghi nhận giống module thẻ trả trước hiện tại. Khác biệt duy nhất: toàn bộ bonus vào Ví KM 2 thay vì chia VND + KM1.
| Khoản mục | Giá trị | Giải thích |
|---|---|---|
| Thực thu (actual_revenue) | = giá mua gói (VD: 500.000đ) | Số tiền khách thực trả — KHÔNG phải 5.000.000đ |
| Doanh số (sales) | = giá mua gói (VD: 500.000đ) | Ghi nhận doanh số theo giá mua gói |
| Fund/Quỹ | = giá mua gói | Tạo fund entry theo giá mua gói (ProcessInvoicePayment) |
| Commission NV | Tính trên giá mua gói | VD: 500k × 5% = 25k (KHÔNG tính trên 5tr) |
| Tiền vào Ví VND | = 0đ | Khác KM1: KM1 nạp base_value vào VND |
| Tiền vào Ví KM 1 | = 0đ | Khác KM1: KM1 nạp bonus vào KM1 |
| Tiền vào Ví KM 2 | = value_into_wallet (VD: 5.000.000đ) | Toàn bộ giá trị ví vào KM2 |
So sánh nhanh với thẻ trả trước hiện tại:
Thẻ trả trước KM1: mua 10tr → VND: 10tr + KM1: 1.5tr → thực thu = 10tr
Gói Ví KM2: mua 500k → VND: 0 + KM2: 5tr → thực thu = 500k
↑ toàn bộ vào KM2, không chiaFR-004: Thanh toán đơn hàng bằng Ví KM 2 (Ref: DEC-003, DEC-008, DEC-009, DEC-012, DEC-014)
Mức ưu tiên: Must | SCR: SCR-04
Luồng:
NV tạo đơn dịch vụ → chọn phương thức thanh toán
│
▼
Hiển thị "Ví KM 2" nếu: cấu hình bật + số dư > 0 + có sản phẩm/dịch vụ hợp lệ
│
▼
NV chọn Ví KM 2 → hệ thống tự điền:
│ Đơn hàng: 1.000.000đ
│ Tổng hợp lệ: 700,000đ (chỉ item có allow_km2 = true)
│ Ví KM 2 (max 20%): 140,000đ [tự điền, chỉ đọc]
│ Số dư KM 2: 4,600,000đ
│ Còn lại: 860,000đ [NV chọn tiền mặt/CK/...]
│
▼
Bấm thanh toán → FIFO Deduction (atomic):
│
│ Cần trừ: 140,000đ
│ Lot Silver (balance 300k): trừ 140k → balance = 160k ✓
│ (Nếu lot chỉ còn 100k: trừ 100k → exhausted,
│ sang lot tiếp: trừ 40k)
│
├─ Ghi wallet_km2_lot_deduction cho MỖI lot bị trừ
├─ Cập nhật wallet.amount -= 140,000
└─ Tạo invoice (payment_method_id = 'wallet_promotion_2')AC:
- [ ] Ví KM 2 hiển thị trong danh sách phương thức thanh toán khi: cấu hình bật + khách có số dư > 0 + ít nhất 1 item hợp lệ
- [ ] Hệ thống tự điền:
MIN(eligible_total × max% / 100, total_balance). VD: đơn hợp lệ 700k, max 20% → tự điền 140k - [ ] NV không tự nhập số tiền KM2 — chỉ có số tiền hệ thống tự điền hoặc bỏ chọn KM2
- [ ] Toggle KM1+KM2 tắt: chọn KM2 → ẩn KM1 (và ngược lại)
- [ ] Bấm thanh toán → FIFO deduction: trừ bản ghi mua cũ nhất trước, ghi
wallet_km2_lot_deductioncho mỗi bản ghi bị trừ - [ ] Bản ghi mua dùng hết (balance = 0) → tự động chuyển status
exhausted - [ ] Khách mua Gói Ví KM2 chi nhánh A, dùng được chi nhánh B (cross-branch) qua Go action; UI/GraphQL không mở quyền đọc toàn bộ lot/deduction cho Staff/Sale
Logic kế toán khi thanh toán bằng Ví KM 2 (giống Ví KM 1 hiện tại):
| Khoản mục | Giá trị | Giải thích |
|---|---|---|
| Phần KM2 trả | VD: 140,000đ | Trừ từ ví, KHÔNG tính thực thu (giống wallet/wallet_promotion hiện tại) |
| Phần tiền mặt/CK trả | VD: 860,000đ | Tính thực thu bình thường |
| Doanh số (sales) | = tổng đơn (1.000.000đ) | Ghi nhận toàn bộ giá trị đơn |
| Thực thu (actual_revenue) | = phần tiền thật (860,000đ) | Chỉ tính tiền mặt/CK, KHÔNG tính KM2 |
| Customer points | Skip cho phần KM2 | Giống logic ProcessInvoicePayment hiện tại |
Tóm lại: Ví KM 2 thanh toán xử lý giống Ví KM 1 — tiền ví không tính thực thu, không tính customer points, không ảnh hưởng hạng thành viên (hạng tính dựa trên thực thu).
ProcessInvoicePaymentthêm"wallet_promotion_2"vào skip list. Không cần sửa rank/loyalty module.
FR-005: Flag cho phép Ví KM 2 per sản phẩm/dịch vụ (Ref: DEC-009)
Mức ưu tiên: Must | SCR: SCR-05
AC:
- [ ] Form sản phẩm/dịch vụ có thêm checkbox "Cho phép Ví KM 2" (mặc định: tắt)
- [ ] Sản phẩm không bật flag → không tính vào eligible_total khi thanh toán KM2
- [ ] Đơn hàng mix: chỉ item eligible mới được tính max%. VD: Gội đầu 200k (eligible) + Kem 300k (không eligible) → max% chỉ tính trên 200k
FR-006: Tab Ví KM 2 trong profile khách (Ref: DEC-006, DEC-013)
Mức ưu tiên: Must | SCR: SCR-06
AC:
- [ ] Profile khách hiển thị card "Ví KM 2" với tổng số dư (tổng balance các bản ghi lần mua đang hoạt động)
- [ ] Click vào → mở tab chi tiết: bảng Gói Ví KM2 đã mua (tên gói, nạp, đã dùng, còn lại, hạn), phân đang hoạt động vs đã hết
- [ ] Gói Ví KM2 đã mua sắp hết hạn (≤ 30 ngày) + còn số dư → hiển thị text cảnh báo phía trên bảng
- [ ] Lot hết hạn ≤ 7 ngày → text cảnh báo màu đỏ
- [ ] Staff/Sale truy cập profile khách được phân quyền → thấy tab Ví KM 2 của khách đó; không được đọc lot/deduction của khách ngoài phạm vi quyền
FR-007: Tác vụ tự động xử lý bản ghi mua hết hạn (Ref: DEC-005, DEC-015)
Mức ưu tiên: Must | SCR: —
Luồng:
Cron 00:05 hàng ngày (Asia/Ho_Chi_Minh)
│
▼
Query: `wallet_km2_lot.status = 'active' AND expired_at <= NOW()`
│
▼
Cho mỗi bản ghi mua hết hạn:
├─ UPDATE status = 'expired'
├─ Trừ wallet.amount -= lot.balance
├─ Ghi transaction log: "Gói {tên} hết hạn, {balance}đ bị xóa"
└─ Ghi deduction record (type = 'expiry_cancel')AC:
- [ ] Cron chạy mỗi ngày 00:05 (Asia/Ho_Chi_Minh)
- [ ] Tìm
wallet_km2_lot.status = activevàexpired_at <= NOW() - [ ] Đánh dấu status =
expired, trừ wallet.amount -= lot.balance - [ ] Ghi transaction log với note "Gói {tên} hết hạn, số dư {balance}đ bị xóa"
- [ ] Ghi
wallet_km2_lot_deduction(type =expiry_cancel)
FR-008: Hoàn tiền đơn dịch vụ — phần Ví KM 2 (Ref: DEC-010, DEC-012)
Mức ưu tiên: Should | SCR: SCR-04 (reuse payment screen)
Luồng:
Manager duyệt hoàn tiền đơn dịch vụ đã dùng Ví KM 2
│
▼
Query deduction records → biết chính xác bản ghi mua nào đã bị trừ bao nhiêu
│ VD: Lot Silver trừ 150k + Lot Gold trừ 50k = 200k
│
▼
Hoàn vào từng bản ghi mua:
├─ Bản ghi còn hạn → cộng lại balance
├─ Bản ghi hết hạn → gia hạn +{refund_extend_days} ngày, status = 'active'
└─ Bản ghi đã hoàn → tạo bản ghi MỚI (amount = hoàn, hạn = +N ngày)
│
▼
Bút toán âm hoa hồng NV (nếu có) → luồng hiện tạiAC:
- [ ] Khi hoàn tiền đơn đã dùng Ví KM 2, hệ thống query
wallet_km2_lot_deductionđể biết chính xác bản ghi mua nào bị trừ bao nhiêu - [ ] Hoàn vào đúng bản ghi gốc, cộng lại balance
- [ ] Nếu bản ghi đã hết hạn → gia hạn thêm
refund_extend_daysngày (config), status =activelại - [ ] Nếu bản ghi đã
refunded→ tạo bản ghi MỚI với amount = số tiền hoàn, hạn = NOW() + refund_extend_days - [ ] Bút toán âm hoa hồng NV: giữ nguyên luồng hiện tại
FR-009: Hoàn ví KM2 qua Yêu cầu hoàn tiền (Ref: DEC-011, DEC-019, DEC-026)
Mức ưu tiên: Should | SCR: SCR-07
Luồng (dùng lại chức năng Yêu cầu hoàn tiền hiện có, thêm loại xử lý mới):
User có quyền mở Gói Ví KM2 đã mua còn hiệu lực (balance > 0) → bấm "Hoàn ví KM2"
│
▼
Form tạo Yêu cầu hoàn tiền (dùng lại pattern `WithdrawForm`, thêm loại xử lý KM2):
│
│ ── Thông tin Gói Ví KM2 (chỉ đọc) ───────────────────────────────
│ Gói Gold | Giá mua gói: 500k | Đã trả: 300k/500k
│ Ví đã nhận: 3,000,000đ | Đã dùng: 1.000.000đ | Còn: 2,000,000đ
│
│ ── Người tạo yêu cầu nhập ────────────────────────────
│ Loại yêu cầu: Hoàn ví KM2
│ Số tiền KM2 trừ: [2,000,000] đ ← tự điền = balance, sửa được (≤ balance)
│ Số tiền hoàn khách: [ 100,000] đ ← tự điền theo FORMULA-002, sửa được (≤ đã trả)
│ PTTT hoàn: [Tiền mặt] [Ví VND] [Chuyển khoản]
│ Ghi chú: [_______________]
│
▼
Tạo `transaction_request` behavior = `refund_km2_wallet`, label UI = "Hoàn ví KM2"
│
▼
Người duyệt xử lý trên màn Yêu cầu hoàn tiền hiện có
│
▼
Backend xử lý sau khi duyệt:
├─ lot.balance -= số tiền KM2 trừ
│ (balance = 0 → status = 'refunded', > 0 → vẫn active — partial refund)
├─ wallet.amount -= số tiền KM2 trừ
├─ Hoàn tiền thật cho khách (cash/VND/CK)
├─ Bút toán âm hoa hồng NV nếu chính sách yêu cầu truy thu
└─ Nếu đơn mua Gói Ví KM2 còn nợ → xoá nợ tương ứng với phần hoànAC:
- [ ] User có
refund_request_management_submenu:access/create+ quyền xem customer mở Gói Ví KM2 đã mua → nút "Hoàn ví KM2" (chỉ hiện khi status =active, balance > 0 và config allow_refund = true) - [ ] Form hiển thị chỉ đọc: tên Gói Ví KM2, giá mua gói, đã trả, ví đã nhận, đã dùng, còn lại
- [ ] Bấm tạo yêu cầu tạo
transaction_requestvới behavior/action kỹ thuậtrefund_km2_wallet, label nghiệp vụ "Hoàn ví KM2"; không dùng tên legacy cũ - [ ] Số tiền KM2 trừ: hệ thống tự điền = lot.balance, Manager sửa được (0 < value ≤ balance)
- [ ] Số tiền hoàn khách: hệ thống tự điền theo FORMULA-002 (tỉ lệ × KM2 trừ - phí), Manager sửa được (0 ≤ value ≤ tổng đã trả)
- [ ] FORMULA-002 chỉ là số tiền gợi ý — Manager có quyền sửa tuỳ tình huống (VD: khách VIP hoàn nhiều hơn)
- [ ] PTTT hoàn: tiền mặt / Ví VND / chuyển khoản
- [ ] Yêu cầu xuất hiện trong danh sách Yêu cầu hoàn tiền hiện có, lọc/tìm kiếm theo loại "Hoàn ví KM2"
- [ ] Người duyệt dùng cấu hình duyệt và quyền
refund_request_management_submenu:approve/paymenthiện có để duyệt/từ chối/hủy và thực hiện hoàn tiền; quyềnupdatechỉ dùng cho chỉnh thông tin/chứng từ yêu cầu trước khi duyệt - [ ] Truy thu hoa hồng: nếu áp dụng, dùng lại form/bảng truy thu của luồng hoàn tiền đơn dịch vụ hiện tại (
CommissionRefundConfirmForm) - [ ] Duyệt → hệ thống trừ KM2, hoàn tiền khách, ghi bút toán âm hoa hồng nếu có
- [ ] Hoàn một phần: nếu KM2 trừ < balance → bản ghi mua vẫn
active(balance giảm, chưarefunded) - [ ] Hoàn toàn bộ: nếu KM2 trừ = balance →
wallet_km2_lot.status = refunded - [ ] Nếu đơn mua Gói Ví KM2 còn nợ → xoá nợ tương ứng
- [ ] Nút "Hoàn ví KM2" ẩn khi: Gói Ví KM2 đã hết hạn (expired), đã dùng hết (exhausted), đã hoàn hết (refunded), hoặc user bị thu hồi quyền qua Dynamic Permission
FR-010: Báo cáo tổng hợp Ví KM 2 (Ref: DEC-003)
Mức ưu tiên: Could | SCR: SCR-08
AC:
- [ ] Dashboard hiển thị 4 KPI cards: Gói Ví KM2 bán ra, Doanh thu, Tỉ lệ sử dụng Ví KM2, Số dư sắp cháy (30 ngày)
- [ ] Bảng chi tiết theo Gói Ví KM2: số bán, doanh thu, tổng nạp, đã dùng, tỉ lệ sử dụng
- [ ] Bảng Gói Ví KM2 sắp hết hạn (30 ngày tới): khách, Gói Ví KM2, số dư, hết hạn
- [ ] Bảng top khách dùng nhiều nhất: tổng nạp, đã dùng, số lần, rate
- [ ] Filter: khoảng thời gian, chi nhánh (Manager: branch, Admin: all)
FR-011: ZNS thông báo (Ref: DEC-006)
Mức ưu tiên: Could | SCR: —
AC:
- [ ] Mua Gói Ví KM2 thành công → ZNS trigger
on_km2_wallet_activated: "Bạn đã kích hoạt {N} Gói {tên}, tổng {amount}đ, HSD: {expired_at}" - [ ] 7 ngày trước hết hạn (nếu balance > 0) → ZNS trigger
on_km2_wallet_expiring_7d: "Gói {tên} còn {balance}đ sẽ hết hạn ngày {expired_at}"
FR-012: Hiển thị + tích hợp Ví KM 2 trong các module hiện có (Ref: DEC-001)
Mức ưu tiên: Must (Phase 1) + Should (Phase 2) | SCR: SCR-03, SCR-04 + các màn hiện có
Nguyên tắc: dựa trên UI hiện tại, thêm KM2 theo đúng pattern có sẵn. KHÔNG thay đổi layout.
AC — Phase 1: Chi tiết đơn nạp tiền (PrepaidOrderPayments):
- [ ] Summary header thêm dòng "Nạp vào ví KM 2: {amount}đ" — conditional, chỉ hiện khi > 0
- [ ] Bảng thanh toán (
PrepaidOrderPaymentTable) thêm cột "Ví KM 2" — conditional khi đơn có Gói Ví KM2 - [ ] Phần "Số tiền nạp vào ví KM 2" riêng biệt với "Nạp vào ví khuyến mãi" (KM1)
AC — Phase 1: Đơn mỹ phẩm (CosmeticOrderFormPayment):
- [ ] Thêm payment method
wallet_promotion_2vào flow thanh toán mỹ phẩm (giống cáchwallet_promotionđã có) - [ ]
CosmeticOrderFormPaymentTable.tsx: thêm hiển thịwallet_promotion_2_amount - [ ]
CosmeticOrderPaymentFormMultiple.tsx: thêm KM2 vào thanh toán nhiều lần
AC — Phase 1: Đơn sản phẩm (ProductOrder):
- [ ]
ProductOrderItems.tsx: thêm checkallow_promo_wallet_2per sản phẩm (giốngallow_promo_wallet) - [ ]
ProductOrderCreate.tsx: injectcustomerWalletValue["VND_PROMOTION_2"]
AC — Phase 1: Hoá đơn in:
- [ ]
print_invoice_popup.go+print_invoice_preview.go: thêm case"wallet_promotion_2"→ label "Ví KM 2" - [ ]
InvoiceTemplatePopupPrint.tsx+InvoiceTemplatePreview.tsx: thêm KM2 vào template
AC — Phase 1: Fund/Quỹ:
- [ ]
FundTable.tsx+FundInvoicePopup.tsx: thêm hiển thịwallet_promotion_2_amount(giốngwallet_promotion_amount)
AC — Phase 2: Withdraw/Refund detail:
- [ ]
WithdrawRequestDetail.tsx: hiện label "Ví KM 2" khi payment_method =wallet_promotion_2
AC — Phase 2: Report DV/NV hiện có:
- [ ]
ServiceReportTable.tsx+ServiceGroupReportTable.tsx: thêm cộtwallet_promotion_2_revenue - [ ]
EmployeeRevenueReportDoughnutChart.tsx: thêm KM2 vào biểu đồ - [ ]
report_service.graphql+report_employee_revenue.graphql: thêm fields KM2
AC — Phase 2: CRM Customer:
- [ ]
CustomerEWalletInformation.tsx: thêm hiển thị số dư Ví KM 2 - [ ]
CustomerVisitSumcard.tsx: tooltip thêm mention Ví KM 2 nếu có
AC — KHÔNG thay đổi (giữ nguyên):
- [ ]
OrderCard.tsx+CustomerOrderHistory.tsx: card không breakdown — giữ nguyên - [ ] Filter "Đơn hàng nạp tiền": đã cover Gói Ví KM2 — không thêm filter
- [ ] Dashboard (
SalesCards,RevenuePercentageByPaymentMethod): Phase 3 report KM2 handle - [ ] Rank/Loyalty: KM2 thanh toán không tính thực thu → không ảnh hưởng hạng (giống KM1). Không cần sửa
rank.graphql,profile.graphql
A6) Giả định
| ID | Giả định | Người xác nhận |
|---|---|---|
| ASM-001 | Khách chỉ mua Gói Ví KM2 tại POS (NV bán), không tự mua trên app. App khách có hiển thị tab Ví KM 2 (read-only, parity với KM 1) — xem SCR-09 và PD-002 | PO |
| ASM-002 | % max là số nguyên (20%, 40%), không cần thập phân | PO |
| ASM-003 | Hệ thống wallet hiện tại (transaction_request → transaction → event trigger) đủ handle thêm 1 wallet type | Tech Lead |
| ASM-004 | ZNS template mới cần đăng ký với provider (Zalo) trước khi dùng | Ops |
| ASM-005 | KM 2 áp dụng cùng logic rank/loyalty như KM 1 — không thay đổi rank.graphql / profile.graphql; RankCreate.tsx KHÔNG cần field mới | PO |
A7) Rủi ro
| ID | Rủi ro | Ảnh hưởng | Xác suất | Cách giảm thiểu |
|---|---|---|---|---|
| RSK-001 | Tranh chấp FIFO deduction khi 2 NV thanh toán cùng lúc cho 1 khách | Cao | Thấp | SELECT FOR UPDATE lock bản ghi mua trong transaction |
| RSK-002 | 10+ file Go hard-code wallet_promotion → dev bỏ sót file | Cao | Trung bình | Grep toàn bộ "wallet_promotion" trước khi deploy, dùng checklist code review |
| RSK-003 | Scheduler bỏ sót bản ghi mua hết hạn | Trung bình | Thấp | expired_at lưu end_of_day (23:59:59), cron chạy 00:05, retry 3 lần |
| RSK-004 | Invoice struct Go thiếu field WalletPromotionAmount (bug tiềm ẩn hiện tại) | Trung bình | Cao | Fix luôn trong Phase 1 |
A8) Chỉ số sau phát hành
| Chỉ số | Cách đo | Mục tiêu | Khi nào đo |
|---|---|---|---|
| Số Gói Ví KM2 bán ra | COUNT(wallet_km2_lot) theo tháng | Baseline tháng 1, tăng 20% tháng 2 | Hàng tháng |
| Doanh thu bán Gói Ví KM2 | SUM(order.total) WHERE wallet_target = KM2 | > 0 | Hàng tháng |
| Tỉ lệ sử dụng Ví KM2 | SUM(used_amount) / SUM(initial_amount) × 100% | > 50% sau 3 tháng | Hàng tháng |
| Tần suất khách quay lại (khách có Ví KM2) | COUNT(orders) / COUNT(DISTINCT customer) WHERE used KM2 | +30% so với baseline | Hàng quý |
| Số dư cháy (expired) | SUM(balance) WHERE status = expired | < 30% tổng nạp | Hàng tháng |
A9) Bảng thuật ngữ
| Thuật ngữ (VI) | Thuật ngữ (EN) | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Ví KM 2 | Promotion Wallet 2 | Ví khuyến mãi dùng theo giới hạn max X% trên mỗi đơn | ≠ Ví KM 1 (trả 100% trực tiếp) |
| Gói Ví KM2 | KM2 wallet package | Gói nạp ưu đãi bán cho khách. VD: khách trả 500k, nhận 5.000.000đ vào Ví KM 2, có hạn sử dụng 6 tháng | ≠ Thẻ trả trước thường (ví đích = VND) |
Lần mua Ví KM2 (lot) | KM2 wallet lot | Bản ghi kỹ thuật được tạo sau mỗi lần khách mua Gói Ví KM2; lưu số đã nạp, đã dùng, còn lại, hạn sử dụng và trạng thái | ≠ Gói Ví KM2 là cấu hình/loại gói bán |
| FIFO | First In First Out | Khi thanh toán, hệ thống trừ bản ghi lần mua cũ nhất (activated_at ASC) trước | — |
| Tổng hợp lệ để dùng KM2 | Eligible total | Tổng giá trị sản phẩm/dịch vụ trong đơn có allow_promo_wallet_2 = true | ≠ Tổng đơn hàng |
| Tỉ lệ hoàn | Refund ratio | Giá mua gói / giá trị ví. VD: 500k/5tr = 10% | ≠ Hoàn 100% |
| Phí xử lý hoàn ví | Refund processing fee | % giá mua gói bị trừ khi hoàn ví KM2. VD: 20% × 500k = 100k | — |
| Hoàn ví KM2 | KM2 wallet refund | Yêu cầu hoàn tiền behavior refund_km2_wallet, dùng để tất toán/hoàn phần số dư KM2 còn lại cho khách | ≠ Refund đơn DV hoàn KM2 về bản ghi lần mua gốc |
| Yêu cầu hoàn tiền | Refund request | Luồng transaction_request hiện có để tạo, duyệt, từ chối, hủy hoàn tiền | ≠ Module approval riêng cho KM2 |
| Tỉ lệ sử dụng Ví KM2 | — | % tiền đã dùng so với tổng nạp | — |
| Số dư cháy | Burned balance | Số dư bị xóa do bản ghi lần mua Ví KM2 hết hạn | — |
A10) Công thức nghiệp vụ
FORMULA-001: Số tiền Ví KM 2 thanh toán trên mỗi đơn hàng
- Mô tả: Tính số tiền tối đa Ví KM 2 được trả cho 1 đơn hàng
- Công thức:
km2_pay = MIN(eligible_total × max_percent_per_order / 100, available_amount) - Biến số:
eligible_total: tổng giá trị sản phẩm/dịch vụ cóallow_promo_wallet_2 = true— nguồn:SUM(order_item.price × quantity)WHEREproduct.allow_promo_wallet_2 = truemax_percent_per_order: % tối đa mỗi đơn — nguồn:wallet_km2_config.max_percent_per_orderavailable_amount: tổng số dư Ví KM 2 khả dụng realtime — nguồn: Actionget_customer_km2_balance.available_amount=SUM(wallet_km2_lot.balance)WHEREstatus='active' AND expired_at > NOW()(DEC-029: KHÔNG dùngwallet.amounttrực tiếp, để đóng race window scheduler miss)
- Đơn vị: VND (số nguyên, không làm tròn)
- Ví dụ: Tổng hợp lệ của đơn là 700k, max_percent_per_order = 20%,
available_amount= 4.6tr → MIN(700k × 20%, 4.6tr) = MIN(140k, 4.6tr) = 140.000đ - Trường hợp cá biệt:
eligible_total = 0(không có sản phẩm/dịch vụ hợp lệ) →km2_pay = 0, ẩn lựa chọn KM2available_amount < km2_max→ dùng hết số dư khả dụngavailable_amount = 0→ ẩn option KM2- Bản ghi mua đã hết hạn (
expired_at <= NOW()) → KHÔNG tính vàoavailable_amountdù scheduler chưa kịp đổistatus(DEC-029 enforce realtime)
FORMULA-002: Gợi ý số tiền hoàn ví KM2 (auto-fill, approver sửa được)
- Mô tả: Tính số tiền gợi ý hoàn cho khách trong yêu cầu
refund_km2_wallet. Chỉ là số tiền tự điền ban đầu — người duyệt có quyền sửa tuỳ tình huống nếu có quyềnrefund_request_management_submenu:update; bước duyệt dùngapprove, bước chi tiền dùngpayment. - Công thức:
suggested_refund = MAX(km2_deduct × refund_ratio - refund_fee, 0) - Biến số:
km2_deduct: số tiền KM2 trừ (Manager nhập, auto-fill = lot.balance) — nguồn: form inputrefund_ratio: giá mua gói / giá trị ví — nguồn: wallet_km2_lot.package_price / wallet_km2_lot.wallet_value (DEC-016 snapshot)refund_fee: phí xử lý hoàn ví = giá mua gói × phí xử lý hoàn ví % — nguồn: wallet_km2_lot.package_price × wallet_km2_config.refund_fee_percent / 100
- Đơn vị: VND (số nguyên)
- Ví dụ: KM2 trừ = 2tr, ratio = 500k/5tr = 10%, phí = 500k × 20% = 100k → MAX(2tr × 10% - 100k, 0) = 100,000đ (gợi ý)
- Người duyệt sửa số tiền: người duyệt nhập số khác (VD: 150k cho khách VIP). Kiểm tra hợp lệ: 0 ≤ value ≤ tổng đã trả
- Trường hợp cá biệt:
- lot.balance = 0 → ẩn nút "Hoàn ví KM2"
- suggested_refund = 0 (phí > hoàn) → tự điền 0đ, Manager vẫn sửa được
- refund_fee_percent = 0 → không trừ phí
- Partial refund: KM2 trừ < balance → lot vẫn active, formula tính trên phần trừ
- Đơn mua Gói Ví KM2 còn nợ → form hiển thị "Đã trả: X/Y", Manager cân nhắc khi nhập
FORMULA-003: Tỉ lệ sử dụng Ví KM2
- Mô tả: Tỉ lệ tiền đã sử dụng so với tổng nạp
- Công thức:
rate = total_used / total_deposited × 100 - Biến số:
total_used: tổng đã dùng — nguồn: SUM(wallet_km2_lot.used_amount) WHERE status IN ('active', 'exhausted', 'expired')total_deposited: tổng đã nạp — nguồn: SUM(wallet_km2_lot.initial_amount) WHERE status IN ('active', 'exhausted', 'expired')
- Đơn vị: % (2 decimal)
- Ví dụ: Tổng nạp 10tr, đã dùng 3.5tr → 3.5/10 × 100 = 35.00%
- Trường hợp cá biệt:
- total_deposited = 0 → hiển thị "—"
- Exclude status = 'refunded' (Gói Ví KM2 đã hoàn, không tính)
FORMULA-004: Tiền vào Ví KM 2 per lần thanh toán (multi-payment)
- Mô tả: Khi khách trả nhiều lần cho đơn mua Gói Ví KM2, mỗi lần trả → ví nhận theo tỉ lệ (giống thẻ trả trước hiện tại)
- Công thức:
km2_credit = (customer_paid_amount / total_order_amount) × total_value_into_wallet - Biến số:
customer_paid_amount: số tiền khách trả lần này — nguồn: invoice.customer_paid_amounttotal_order_amount: tổng tiền đơn (giá mua gói × qty) — nguồn: order.amounttotal_value_into_wallet: tổng giá trị ví (value_into_wallet × qty) — nguồn: SUM(order_item.prepaid_value_into_wallet)
- Đơn vị: VND (số nguyên)
- Ví dụ: Gói Gold 500k→5tr, qty=2, tổng đơn 1,000k. Trả lần 1: 600k → ví nhận: (600k / 1,000k) × 10tr = 6.000.000đ
- Trường hợp cá biệt:
- Trả đủ 100% 1 lần → credit = total_value_into_wallet (case phổ biến nhất)
- Trả lần cuối → credit = total_value_into_wallet - SUM(credit các lần trước) (tránh sai lệch do làm tròn)
- Lot balance tăng dần: lần 1 tạo lot (balance = proportional), lần 2+ cộng thêm vào lot