Appearance
v1.11 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Đảo DEC-031 sang Option A — SCR-09-MOBILE-01 đổi từ "screen riêng" → tab thứ 3 trong wallet_screen.dart cùng cấp 2 tab hiện hữu (Ví thẻ trả trước + Ví khuyến mãi). Sửa wireframe B2.9.2 minh hoạ 3 tab. Note kỹ thuật: dev tự chốt widget (refactor FolderTabs 3 tab / swap CustomTabbar / swap TabPageWidget) | B2.9 | FE Mobile, QA |
Thêm SCR-06-NEW-03 — popup StatisticWalletPromotion2Popup.tsx lịch sử giao dịch KM 2 (web admin, parity popup KM 1 hiện hữu — Option α tạo file riêng) | B0.1, B2.6.6A | FE Web, QA |
Update B2.9.3 reuse table — đổi từ "Screen riêng + BLoC mới" thành "Extend wallet_screen.dart + extend WalletBloc 3 event/state cho KM2" | B2.9.3 | FE Mobile |
Update bảng index SCR (B1.1) — SCR-09 đổi 🆕 New → 🔧 Extend (mobile) | B1.1 | FE, QA |
v1.10 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Audit mobile gap với codebase Flutter customer + staff — fix 2 wireframe SAI ở B2.9.2 (Home thực tế 4 card không có ví KM 1; wallet_screen.dart thực tế chỉ 2 tab FolderTabs, không phải 5) | B2.9.2 | FE Mobile, QA |
DEC-031 (Option C) — SCR-09-MOBILE-01 đổi từ "Tab Ví KM 2 trong wallet_screen.dart" thành screen riêng route mới /wallet/promotion2 (file mới wallet_km2_screen.dart + BLoC mới), KHÔNG refactor wallet_screen.dart [ĐÃ ĐẢO ở v1.11 — chốt Option A] | (B2.9) | FE Mobile |
DEC-032 (Option α) — Staff AffiliateFor KHÔNG bổ sung WalletType.promotion2, danh sách ví CTV nhận hoa hồng giữ [commission, promotion] | (PRD Z) | FE Mobile, BE |
| Thêm SCR-09-MOBILE-03 (WalletType enum + l10n shared) và SCR-10-STAFF-01 (Staff customer detail balance KM 2) | B0.1, B2.10 | FE Mobile, QA |
| Update bảng index SCR (B1.1) thêm row SCR-10 | B1.1 | FE, QA |
v1.8 — 15/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
DEC-027: bỏ trạng thái lot pending khỏi lifecycle (17 chỗ); đơn còn nợ hiển thị qua prepaid_order.status='pending' + inline "Đã nạp X/Y" | B0.7, B2.6.11 | FE, QA |
DEC-029: SCR-04 + SCR-06 + SCR-FR012 dùng action get_customer_km2_balance thay wallet.amount | B2.4, B2.6 | FE, BE |
Rename 4 field config canonical theo tên Dev (max_percent_per_order, allow_combine_km1, refund_fee_percent, refund_deadline_days) | B0.4 | FE, QA |
| Thêm B0.9 Bảng kiểm kê tương tác (~60 element × 9 SCR, 10 enum canonical) | B0.9 | FE, QA |
| Lint vietnamese clean: chuẩn hoá calque headings + heading section sang tiếng Việt canonical | Toàn file | None |
Đặc tả UI (UI Spec) — Ví KM 2 (Promotion Wallet 2)
Tham chiếu: PRD v1.11 + SOURCE_OF_TRUTH v1.6 + EVIDENCE_PACK v1.4 | Phiên bản UI Spec: 2.3 | Ngày: 15/05/2026
File này dùng để làm gì: mô tả màn hình, thay đổi (delta), trạng thái, biến thể theo role/permission/lifecycle, tương tác, copy hiển thị, tooltip, tình huống cá biệt và verification cho
Ví KM 2để FE/UI/QA triển khai thống nhất.Đọc trước:
decision-brief.md→B-PRE) Kiểm tra discovery→B0) Hiện trạng UI và hợp đồng thay đổi→B1) Bản đồ màn hình→ cácSCRáp dụng →B-POST) Verification→B-QUALITY) Rà soát rủi ro thiếu sót.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. Với formula,PRD A10thắng spec UI; với RACI/timeline,handoff.mdthắng; với readiness/rollback,go-live-checklist.mdthắng.
Lịch sử thay đổi
| Phiên bản | Ngày | Tác giả | Thay đổi |
|---|---|---|---|
| 2.0 | 30/04/2026 | PO/BA | Viết lại theo po-ba-workflow v1.5: bổ sung 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 ma trận variant, B2.x.7B/C/D Form/Concurrency/Network, B2.x.8A Role × Variant, B2.x.10A Error Taxonomy, B5A/B6A/B7A/B8A/B8B/B8C contract chuyên sâu, B-Microcopy/B-Voice/B-i18n/B-Versioning/B-Help, 12 nhóm tình huống biên (G1-G12), B-POST verification, B-QUALITY 30+ rủi ro |
| 1.6 | 28/04/2026 | PO/BA | Cập nhật Dynamic Permission v2 và luồng Hoàn ví KM2 reuse Yêu cầu hoàn tiền |
| 1.5 | 28/04/2026 | PO/BA | Đồng bộ tiếng Việt-first, thêm B1.0 mục tiêu màn hình + CTA |
| 1.4 | 28/04/2026 | PO/BA | Thêm setup readiness, runtime preview, publish safety |
| 1.3 | 28/04/2026 | PO/BA | Bổ sung wireframe ASCII với dữ liệu mẫu spa, copy tiếng Việt |
| 1.0 | 24/03/2026 | PO/BA | Initial |
Đầu vào chuẩn (Canonical Inputs)
| File | Vai trò | Nếu xung đột |
|---|---|---|
SOURCE_OF_TRUTH.md | Nguồn sự thật chuẩn + Phương án đã chốt (Solution Lock) | Ưu tiên cao nhất |
EVIDENCE_PACK.md | Layout UI hiện tại + kiểm kê + reuse candidate | Ưu tiên bằng chứng code/screen |
prd.md | FR/AC, Decision Log Z, formula A10, lifecycle | Theo truth đã khóa |
decision-brief.md | Cửa vào package cho PO/TL | Tóm tắt; trỏ về owner |
Quy tắc: UI Spec là tài liệu dẫn xuất, không phát minh rule. Thành phần hiện hữu vắng mặt trong target → log
UI Spec gap/drift, không phải removal. MọiREMOVE/HIDEphải có DEC/FR + PO approval.
B-PRE) Kiểm tra discovery (BẮT BUỘC trước khi viết UI Spec)
Mục đích: ép kiểm tra codebase + screen hiện hữu trước khi viết spec. Bỏ bước này → spec dễ thiếu context, state hoặc field hiện hữu.
B-PRE.1) Tìm component / page hiện có liên quan
bash
# FE component liên quan KM/wallet/promotion/prepaid
grep -rE "(wallet_promotion|promotion-wallet|VND_PROMOTION|prepaid_card|allow_promo_wallet)" diva-admin/src/modules/
# Page settings / prepaid / customer / report
find diva-admin/src/modules/{settings,ecommerce,user,report}/pages -type f \( -name "*.tsx" -o -name "*.vue" \)
# GraphQL liên quan wallet
grep -rE "wallet_stats|wallet_type|wallet_promotion" diva-admin/src/modules/*/graphql/
# Refund flow hiện có
grep -rE "transaction_request|refund_order|WithdrawForm|withdraw_request" diva-admin/src/modules/ diva-backend/
# BE wallet hardcode
grep -rE "VND_PROMOTION|wallet_promotion|GetWalletTypeFromPaymentMethod|CheckWalletBalanceEnough" diva-backend/
# Scheduler
grep -rE "scheduler/event.go|cron_trigger|cronjob" diva-backend/services/wallet-api/B-PRE.2) Bảng inventory đã hoàn tất discovery
| Hạng mục cần check | Đã check? | File / màn hiện có | Ghi chú |
|---|---|---|---|
| Page / route hiện có cho settings KM | ✅ | diva-admin/src/modules/settings/pages/PromotionWallet.tsx (route /s/internal-settings/promotion-wallet) | Reuse shell, extend thêm config KM2 |
| Component reuse: master data Gói Ví KM | ✅ | settings/pages/PrepaidCardCreate.tsx, components/prepaid-card/PrepaidCardForm.tsx, PrepaidCardTable.tsx | Extend form + table + view |
| Component reuse: bán Gói Ví / đơn nạp tiền | ✅ | ecommerce/pages/PrepaidOrderCreate.tsx, components/prepaid-order/PrepaidOrderForm.tsx + subcomponents | Layout 2 panel hiện hữu |
| Component reuse: payment method chip | ✅ | ecommerce/components/order-payment/OrderPaymentMethodRadio.tsx, PrepaidOrderPaymentFormMultiple.tsx | Thêm chip Ví KM 2 cùng pattern |
| Component reuse: customer profile + wallet | ✅ | user/pages/CustomerDetail.tsx, user/components/customer/CustomerEWalletInformation.tsx, StatisticCustomerWallets.tsx | Thêm card/tab Ví KM 2 |
| Component reuse: refund request | ✅ | WithdrawForm.tsx, OrderReceiverInfo.tsx, list/detail transaction_request | Thêm option refund_km2_wallet |
| Component reuse: report shell | ✅ | report/pages/PrepaidCardReport.tsx, components/prepaid-card/* | Reuse cards/filter/table pattern |
| Component reuse: print template | ✅ | settings/components/invoice-template/InvoiceTemplatePopupPrint.tsx | Thêm biến payment_wallet_promotion_2_code |
| Component reuse: fund table | ✅ | ecommerce/components/fund-management/FundTable.tsx, FundInvoicePopup.tsx | Thêm cột conditional wallet_promotion_2_amount |
| Field/cột hiện có trên màn target | ✅ | Liệt kê đầy đủ ở B0.1 | — |
| CTA hiện có (primary + secondary) | ✅ | Lưu, Huỷ, Tạo Mới, Hoàn thành, Thanh toán, Xuất Excel | Reuse, không tạo CTA mới ngoài "Hoàn ví KM2" |
| Filter / search hiện có | ✅ | Khoảng thời gian, chi nhánh (đa số report) | Default theo pattern hiện có |
| State hiện có (loading/empty/error/permission) | ✅ | Skeleton + toast pattern hiện có ở mọi màn | Tuân theo pattern, mở rộng nếu cần |
| Permission/role gating hiện có | ✅ | useGlobalStore.hasPermission, module_permission_action, role_module.actions | Dynamic Permission v2 |
| Notification trigger hiện có | ✅ | transaction_insert.go pipeline ZNS/SMS, notification_template | Extend trigger KM2 |
| Export columns hiện có | ✅ | Export* components, report_prepaid_card_wallet_view | Thêm cột KM2 conditional |
| Tooltip / hint hiện có | ✅ | Pattern (i) + QTooltip | Thêm tooltip KM2 |
| Mobile / responsive behavior hiện có | ✅ | Diva Admin desktop-first, POS tablet ≥ 768px | Không có pattern mobile dưới 768px |
| Analytics events hiện có | ✅ | transaction_insert, order_payment events | Thêm event KM2 prefix |
Kết luận: discovery 100% đã pass cho 4 nhóm subagent (FE/BE/DB/Notification-Export). Không có ô nào còn
[ ].
B-PRE.3) Phân loại reuse cho mỗi phần feature
| Phần feature | Phân loại | Evidence (file:line / màn hiện có) | Delta cần |
|---|---|---|---|
| Bật/tắt phương thức thanh toán & cấu hình KM2 | 🔧 Extend | PromotionWallet.tsx, payment_method.id='wallet_promotion' | Thêm config block KM2 (max%, KM1+KM2, refund policy), readiness check |
| Master data Gói Ví KM2 | 🔧 Extend | PrepaidCardForm.tsx, prepaid_card table, PrepaidCardTable.tsx | Thêm radio wallet_target + field expiry_months |
| Bán Gói Ví KM2 | 🔧 Extend | PrepaidOrderForm.tsx, PrepaidOrderFormGeneral, PrepaidOrderFormPaidInformation, PrepaidOrderFormPayment | Thêm cột "NẠP VÍ KM 2" conditional + dòng summary tương ứng |
| Multi-payment popup khi mua Gói Ví KM2 | 🔧 Extend | PrepaidOrderPaymentFormMultiple.tsx (đang filter bỏ wallet_promotion) | Mở method wallet_promotion_2; chặn installment cho dòng KM2 |
| Thanh toán đơn DV/SP bằng KM2 | 🔧 Extend | OrderPaymentMethodRadio.tsx, payment chip Ví khuyến mãi (KM1) | Thêm chip "Ví KM 2", auto-fill, FIFO trigger backend |
| Eligible flag SP/DV | 🔧 Extend | ProductDetail.tsx, ServiceDetailDetail.tsx toggle allow_promo_wallet | Thêm toggle độc lập allow_promo_wallet_2 |
| Tab Ví KM 2 trong customer profile | 🔧 Extend | CustomerDetail.tsx, CustomerEWalletInformation.tsx, wallet_stats (giữ cho ví khác) + action get_customer_km2_balance (DEC-029, realtime cho card KM 2) | Thêm card + tab/popup chi tiết lot |
| Engine bản ghi từng lần mua + audit | 🆕 Build mới | Không có asset hiện hữu cho lot lifecycle theo gói | wallet_km2_lot, wallet_km2_lot_deduction + UI tương ứng |
| Hoàn ví KM2 | 🔧 Extend | WithdrawForm.tsx, OrderReceiverInfo.tsx, behavior refund_order/refund_order_cosmetic | Thêm option/behavior refund_km2_wallet, dialog tính toán |
| Cảnh báo hết hạn (in-app text) | 🆕 Build mới | Chưa có banner cảnh báo riêng cho lot expiry trong profile | Banner warning/danger trong tab Ví KM 2 |
| Expiry scheduler | 🆕 Build mới | wallet-api/scheduler/event.go chỉ có hello | Cron handler mới (UI không trực tiếp) |
| Report dashboard KM2 | 🔧 Extend | PrepaidCardReport.tsx shell + cards/filter/table | Reuse shell sau PD-001 |
| Hoá đơn / fund / report KM2 (FR-012) | 🔧 Extend | InvoiceTemplatePopupPrint.tsx, FundTable.tsx, ServiceReportTable.tsx, EmployeeRevenueReportDoughnutChart.tsx, WithdrawRequestDetail.tsx, CustomerEWalletInformation.tsx | Thêm label/cột wallet_promotion_2_* conditional |
Phân loại trên đã được auto-validate ở Phase 3 — không có "build mới" nào trùng pattern hiện có.
B0) Hiện trạng UI và hợp đồng thay đổi (As-Is + Delta Contract)
Quy tắc: lấy 100% UI hiện tại làm baseline. Mọi block/field/button/state hiện hữu phải có
Delta Status. Thành phần hiện hữu vắng mặt khỏi target =UI Spec gap/drift, không phải removal.
B0.1) Bảng kiểm kê UI hiện tại đầy đủ
| UI ID | Màn / route | Section | Block / field / action | Thứ tự | Hành vi hiện tại | Permission/condition | Mobile? | Delta Status | Hành vi đích | Evidence (file:line) |
|---|---|---|---|---|---|---|---|---|---|---|
SCR-01-CARD-01 | /s/internal-settings/promotion-wallet | Card "PHƯƠNG THỨC THANH TOÁN" | Toggle Ví khuyến mãi (KM1) + nút Huỷ / Lưu | 1 | Bật/tắt visibility của payment_method.id='wallet_promotion'; lưu qua mutation cập nhật payment_method.disabled | internal_configuration:update, portal Admin | Không | KEEP | Giữ toggle KM1; thêm toggle KM2 song song trong cùng card, không đổi behavior KM1 | PromotionWallet.tsx:18-72 |
SCR-01-NEW-01 | /s/internal-settings/promotion-wallet | Card "PHƯƠNG THỨC THANH TOÁN" | Toggle Ví khuyến mãi 2 (KM2) | 2 | — (chưa có) | internal_configuration:update, portal Admin | Không | NEW | Bật/tắt visibility payment_method.id='wallet_promotion_2'; mặc định tắt sau migration | PromotionWallet.tsx:18-72 (vị trí chèn) |
SCR-01-NEW-02 | /s/internal-settings/promotion-wallet | Card "CẤU HÌNH CHUNG" | Block: input max_percent_per_order, toggle allow_combine_km1 | 3 | — | internal_configuration:update | Không | NEW | Cấu hình max%/cho phép KM1+KM2 cho wallet_km2_config; áp dụng ngay khi lưu | PromotionWallet.tsx (chèn dưới card hiện có) |
SCR-01-NEW-03 | /s/internal-settings/promotion-wallet | Card "CHÍNH SÁCH HOÀN VÍ KM2" | Block: toggle allow_refund + 3 input | 4 | — | internal_configuration:update | Không | NEW | Bật/tắt hoàn ví KM2 + 3 chính sách (phí xử lý, thời hạn, gia hạn refund DV) | PromotionWallet.tsx (chèn cuối) |
SCR-01-CTA-01 | /s/internal-settings/promotion-wallet | Footer | Nút Huỷ / Lưu | 5 | Lưu config payment method visibility | internal_configuration:update | Không | UPDATE | Lưu cùng lúc payment method visibility + wallet_km2_config; dirty check + confirm rời trang | PromotionWallet.tsx:74-92 |
SCR-02-FORM-01 | /settings/prepaid-card/create (dialog slide-right) | Cột trái form thẻ trả trước | code, name, description | 1-3 | Input bắt buộc, validate trùng mã thẻ | internal_configuration:update, portal Admin | Không | KEEP | Giữ nguyên field + behavior | PrepaidCardForm.tsx:32-78 |
SCR-02-FORM-02 | /settings/prepaid-card/create | Cột phải form thẻ trả trước | cardPrice (Mệnh giá), actualAmount (Số tiền nạp), status (chỉ khi sửa) | 4-6 | Currency input, validate actualAmount >= cardPrice | internal_configuration:update | Không | KEEP | Giữ nguyên | PrepaidCardForm.tsx:80-130 |
SCR-02-NEW-01 | /settings/prepaid-card/create | Cột trái form (mới, dưới description) | Radio wallet_target (Ví VND / Ví KM 2) | 7 | — | internal_configuration:update | Không | NEW | Bắt buộc; chọn Ví KM 2 → unlock field expiry_months; mặc định Ví VND (giữ behavior hiện tại) | PrepaidCardForm.tsx (chèn) |
SCR-02-NEW-02 | /settings/prepaid-card/create | Cột phải form (mới, dưới actualAmount) | Input expiry_months (Hạn sử dụng) | 8 | — | internal_configuration:update | Không | NEW (hiện theo điều kiện) | Số tháng > 0; chỉ hiện khi wallet_target='VND_PROMOTION_2'; mặc định 6 | PrepaidCardForm.tsx (chèn) |
SCR-02-TBL-01 | /settings/prepaid-card | Bảng danh sách thẻ trả trước | Cột Mã thẻ, Tên, Mệnh giá, Nạp ví, Trạng thái, action | — | Render từ prepaid_card_view | internal_configuration:access | Không | UPDATE | Thêm cột "Ví đích" với badge KM 2 / VND; thêm cột "HSD" hiện số tháng nếu KM2 | PrepaidCardTable.tsx:24-110 |
SCR-03-LEFT-01 | /ecommerce/prepaid-card-order/create | Panel trái — PrepaidOrderFormGeneral | Chi nhánh, mã giới thiệu, lịch hẹn, nguồn ĐH, chiến dịch, ghi chú | 1 | Form chung đơn nạp tiền | prepaid_order:create, portal POS | Tablet ≥ 768px | KEEP | Không đổi | PrepaidOrderFormGeneral.tsx |
SCR-03-LEFT-02 | /ecommerce/prepaid-card-order/create | Panel trái — PrepaidOrderFormPaidInformation (THÔNG TIN NẠP TIỀN) | Bảng Loại thẻ, SL, Cần TT, Nạp ví DIVA, Nạp ví KM + nút + Thêm thẻ nạp | 2 | Mỗi dòng = 1 dropdown thẻ; hệ thống tính nạp ví theo prepaid_card_view | prepaid_order:create | Tablet | UPDATE | Thêm cột "NẠP VÍ KM 2" conditional khi có ≥1 thẻ wallet_target=VND_PROMOTION_2; cột DIVA/KM = 0 cho dòng KM2 | PrepaidOrderFormPaidInformation.tsx |
SCR-03-LEFT-03 | /ecommerce/prepaid-card-order/create | Panel trái — Phân bổ hoa hồng | Cột Loại thẻ, Số tiền TT, nút [+] | 3 | Phân bổ hoa hồng theo từng dòng thẻ | prepaid_order:create | Tablet | KEEP | Giữ logic hoa hồng theo giá mua gói | PrepaidOrderFormCommission.tsx |
SCR-03-RIGHT-01 | /ecommerce/prepaid-card-order/create | Panel phải — PrepaidOrderFormPayment | Avatar khách, payment method buttons (Chuyển khoản / Thẻ / Tiền mặt / Trả góp), summary | 1 | Sticky right, render summary tiền vào ví DIVA + KM | prepaid_order:payment | Tablet | UPDATE | Thêm dòng "Tiền vào ví KM 2" conditional; chặn nút "Trả góp" nếu trong đơn có dòng KM2 | PrepaidOrderFormPayment.tsx |
SCR-03-RIGHT-02 | /ecommerce/prepaid-card-order/create | Popup multi-payment | Danh sách payment method (loại bỏ wallet, wallet_promotion) | — | Cho thanh toán nhiều phương thức | prepaid_order:payment | Tablet | UPDATE | Mở method wallet_promotion_2 cho đơn KM2 (theo policy mới); chặn installment cho dòng KM2 | PrepaidOrderPaymentFormMultiple.tsx |
SCR-04-CHIP-01 | /ecommerce/order/:id/payment | Hàng chip phương thức thanh toán | Chip Tiền mặt, CK, Thẻ, Ví VND, Ví khuyến mãi (KM1) | — | OrderPaymentMethodRadio selected state + amount input | service_order:payment / product_order:payment | Tablet | KEEP | Giữ chip hiện có | OrderPaymentMethodRadio.tsx |
SCR-04-NEW-01 | /ecommerce/order/:id/payment | Hàng chip phương thức thanh toán (mới) | Chip Ví KM 2 | — | — | service_order:payment / product_order:payment | Tablet | NEW | Hiện khi config bật + số dư > 0 + ≥1 item eligible; chọn → mở block KM2 với auto-fill readonly | OrderPaymentMethodRadio.tsx (chèn) |
SCR-04-NEW-02 | /ecommerce/order/:id/payment | Block tóm tắt KM2 (mới) | Số dư KM2, Eligible total, Max%, Số tiền KM2 áp dụng (readonly), Còn lại cần trả | — | — | — | Tablet | NEW | Auto-fill = MIN(eligible_total × max%/100, balance); readonly | Component mới OrderKm2PaymentBlock.tsx |
SCR-05-TGL-01 | /ecommerce/product/:id, /ecommerce/service/:id | Block "Trạng thái" | Toggle Cho phép Ví khuyến mãi (KM1) | — | Cập nhật allow_promo_wallet qua mutation | internal_configuration:update, portal Admin | Không | KEEP | Giữ toggle KM1 | ProductDetail.tsx, ServiceDetailDetail.tsx (ProductAllowPromoWalletToggle) |
SCR-05-NEW-01 | /ecommerce/product/:id, /ecommerce/service/:id | Block "Trạng thái" (mới, dưới toggle KM1) | Toggle Cho phép Ví KM 2 | — | — | internal_configuration:update, portal Admin | Không | NEW | Toggle độc lập allow_promo_wallet_2; hidden cho POS portal | (chèn vào component pattern KM1) |
SCR-06-CARD-01 | /user/customer/:id | Wallet stats sidebar trái | Card Doanh số, Thực thu, Công nợ, Ví CTV, Ví Diva, Ví KM 1 | — | Render từ wallet_stats | customer_management:access | Tablet | KEEP | Giữ card hiện có | StatisticCustomerWallets.tsx |
SCR-06-NEW-01 | /user/customer/:id | Wallet stats sidebar trái (mới) | Card Ví KM 2: {balance}đ | — | — | customer_management:access | Tablet | NEW (conditional) | Hiện khi khách có wallet VND_PROMOTION_2; click → mở tab/popup chi tiết lot | CustomerEWalletInformation.tsx (chèn) |
SCR-06-NEW-02 | /user/customer/:id | Tab/popup Ví KM 2 (mới) | Banner cảnh báo + 2 bảng (đang hoạt động / đã hết / hoàn) + filter trạng thái + filter Gói Ví KM2 + CTA "Hoàn ví KM2" per row | — | — | customer_management:access + (CTA cần refund_request_management_submenu:create) | Tablet | NEW | Render từ action get_customer_km2_lots (least-data); CTA mở SCR-07 | Component mới CustomerKm2WalletPopup.tsx |
SCR-06-NEW-03 | /user/customer/:id | Popup "Lịch sử giao dịch Ví Khuyến Mãi 2" (mới — parity với popup KM 1 hiện hữu) | Bảng transaction với cột (Ngày / Loại GD / Số tiền / ĐH liên quan / Chi nhánh / Số dư sau) + filter date range + filter chi nhánh + dropdown loại giao dịch (Tất cả / Thanh toán / Hoàn / Hết hạn / Sự kiện) + pagination | — | Query useGetTransactionQuery với filter wallet_type_id: { _eq: "VND_PROMOTION_2" } (đồng dạng popup KM 1) | customer_management:access | Tablet | NEW | Component mới riêng (Option α — không refactor popup KM 1); pattern copy y nguyên từ StatisticWalletPromotionPopup.tsx; trigger: click vào balance trong card Ví KM 2 HOẶC nút "Xem lịch sử" trong popup CustomerKm2WalletPopup | Component mới StatisticWalletPromotion2Popup.tsx (parity StatisticWalletPromotionPopup.tsx:79-130) |
SCR-07-DLG-01 | Dialog từ SCR-06 hoặc menu Yêu cầu hoàn tiền | Form Hoàn ví KM2 | 3 block: Thông tin Gói KM2 + Tính toán hoàn tiền + Tạo yêu cầu | — | — | refund_request_management_submenu:create | Tablet | NEW (extend luồng request hiện có) | Tạo transaction_request behavior refund_km2_wallet; tính refund theo FORMULA-002 | Component mới RefundKm2WalletDialog.tsx |
SCR-07-LIST-01 | Màn Yêu cầu hoàn tiền hiện có | Danh sách + filter type | Cột STT, mã đơn, khách, lý do, số tiền, trạng thái, action duyệt | — | Hiển thị transaction_request các loại hiện có | refund_request_management_submenu:access | Tablet | UPDATE | Thêm filter "Loại = Hoàn ví KM2"; cột "Loại" hiển thị badge "Hoàn ví KM2" cho behavior refund_km2_wallet | List page hiện có |
SCR-08-CARD-01 | Route report KM2 (PD-001 — Phase 3) | KPI Cards | 4 KPI: Gói bán ra / Doanh thu / Tỉ lệ dùng / Sắp cháy | — | — | Module report action access (PD-001) | Tablet | NEW | Tổng hợp từ wallet_km2_lot + deduction | Reuse PrepaidCardReport shell |
SCR-08-TBL-01 | Route report KM2 | 3 bảng: Theo Gói / Sắp hết hạn / Top khách | — | — | — | Tablet | NEW | Reuse pattern report prepaid hiện có | (sau PD-001) | |
SCR-FR012-INV-01 | InvoiceTemplatePopupPrint.tsx | Hoá đơn in: dòng PTTT | Dòng "Ví khuyến mãi: {amount}" | — | Render khi wallet_promotion_amount > 0 | — | KEEP | Giữ dòng KM1 | InvoiceTemplatePopupPrint.tsx | |
SCR-FR012-INV-02 | InvoiceTemplatePopupPrint.tsx | Hoá đơn in (mới) | Dòng "Ví KM 2: {amount}" | — | — | — | NEW | Hiện khi wallet_promotion_2_amount > 0 | (chèn dưới dòng KM1) | |
SCR-FR012-FUND-01 | FundTable.tsx | Cột tiền vào ví | Cột "Vào ví khuyến mãi" hiện hữu | — | Render wallet_promotion_amount | fund_management:access | Không | KEEP | Giữ | FundTable.tsx |
SCR-FR012-FUND-02 | FundTable.tsx | Cột tiền vào ví (mới) | Cột "Vào Ví KM 2" | — | — | fund_management:access | Không | NEW (conditional) | Hiện khi tổng wallet_promotion_2_amount > 0 trong khoảng đang xem | (chèn cạnh cột KM1) |
SCR-FR012-RPT-01 | ServiceReportTable.tsx, EmployeeRevenueReportDoughnutChart.tsx | Cột/segment Ví khuyến mãi | Cột wallet_promotion_revenue | — | Sum theo nhân viên / dịch vụ | report:access | Không | KEEP | Giữ | (component report hiện có) |
SCR-FR012-RPT-02 | ServiceReportTable.tsx, doughnut chart | Cột/segment Ví KM 2 (mới) | Cột wallet_promotion_2_revenue + segment | — | — | report:access | Không | NEW (conditional) | Hiện khi có dữ liệu KM2 trong range | (chèn cạnh KM1) |
SCR-FR012-CRM-01 | CustomerEWalletInformation.tsx (CRM portal section) | Block ví khách | Dòng Ví khuyến mãi: {balance}đ | — | Render từ wallet_stats | customer_management:access | Tablet | KEEP | Giữ | CustomerEWalletInformation.tsx |
SCR-FR012-CRM-02 | CustomerEWalletInformation.tsx | Block ví khách (mới) | Dòng Ví KM 2: {balance}đ | — | — | customer_management:access | Tablet | NEW (conditional) | Hiện khi khách có wallet KM2 | (chèn dưới dòng KM1) |
SCR-FR012-NEG-01 | NegativeInvoicesTable.tsx | Cột "Tiền nạp ví khuyến mãi" | Cột render wallet_promotion_amount âm khi hoàn đơn | — | Render từ invoice negative | negative_payment:access | Không | KEEP | Giữ cột KM 1 | NegativeInvoicesTable.tsx (cột hiện hữu) |
SCR-FR012-NEG-02 | NegativeInvoicesTable.tsx | Cột "Vào Ví KM 2" (mới) | — | — | — | negative_payment:access | Không | NEW (conditional) | Hiện khi tổng wallet_promotion_2_amount < 0 trong range; render value âm | (chèn cạnh cột KM 1) |
SCR-FR012-SMS-01 | Template vnd_wallet_balance (notification_template table) | Placeholder mới {{.wallet_promotion_2_amount}} | — | — | Conditional render dòng "Vi KM 2 con lai: X" sau invoice complete | notification:send (backend) | Không | UPDATE | Chỉ render dòng KM 2 khi balance > 0; tránh SMS dài cho khách KM1-only | Migration update notification_template.content |
SCR-FR012-AFF-01 | AffiliateConfiguration.tsx matrix (/s/internal-settings/affiliate) | Cột "VÍ KHUYẾN MÃI 2" (mới, auto-render) | — | — | UI tự render cột khi payment_method có row wallet_promotion_2; admin tick/untick | internal_configuration:update | Không | NEW (auto) | Migration seed 3 row `affiliate_config(service | cosmetic |
SCR-FR012-COMM-01 | OrderCommissionItem.tsx | Filter wallet_type_id | Hiện chỉ filter === "VND_PROMOTION" | — | Hiển thị row commission của khách trong đơn | order:view | Tablet | UPDATE | Mở rộng filter include cả "VND_PROMOTION_2" để chuẩn bị cho trường hợp admin bật affiliate_config cho KM 2 | OrderCommissionItem.tsx (1 dòng filter) |
SCR-FR012-SMS-02 | Template prepaid_card (notification) | Placeholder {{.wallet_promotion_2_amount}} cho lần nạp KM 2 | — | — | Render khi đơn nạp có Gói KM 2 | notification:send (backend) | Không | UPDATE (conditional) | Đồng bộ với SMS-01; trigger sau prepaid_order complete có dòng KM 2 | Migration update notification_template.content |
SCR-09-MOBILE-01 | App khách (Flutter) — Tab Ví khuyến mãi 2 thứ 3 trong wallet_screen.dart | wallet_screen.dart:213-227 (hiện FolderTabs 2 tab: Ví thẻ trả trước + Ví khuyến mãi) | — | — | Tab thứ 3 cùng cấp; balance + 2 bảng lot (active/inactive) + drawer detail + filter chip | customer:authenticated (mobile app token) | Mobile-only | EXTEND | Read-only Phase 1; thêm tab cùng cấp với 2 tab hiện hữu (DEC-031); pattern parity với tab Ví khuyến mãi (KM 1); dev tự chốt widget (refactor FolderTabs 3 tab HOẶC swap CustomTabbar/TabPageWidget — đã có trong core/) | wallet_screen.dart:213-227, page mới promotion_wallet_2_page.dart (parity promotion_wallet_page.dart), wallet_bloc.dart:43-62 extend 3 event/state cho KM2, WalletType.promotion2 enum |
SCR-09-MOBILE-02 | App khách — Home widget card "Ví khuyến mãi 2" | Card thứ 5 trong list wallets của home_wallet.dart:49-77 | — | — | Hiển thị balance + HSD gần nhất; tap mở wallet_screen.dart với initialTabIndex trỏ tab Ví khuyến mãi 2 | customer:authenticated | Mobile-only | NEW (conditional) | Chèn cuối list 4 card hiện có (DIVA/Commission/Reward Point/Bonus Point — KHÔNG có ví KM 1 trên Home); hiện khi balance > 0 | home_wallet.dart:49-77 |
SCR-09-MOBILE-03 | App khách + Staff — WalletType enum + l10n key | core/lib/common/constants/server_constants.dart:300-346 (enum) + 4 ARB file (intl_vi.arb và intl_en.arb cho cả customer + staff) | Enum value promotion2('VND_PROMOTION_2') + key promotionWallet2 | — | Mapping JSON code ↔ display VI/EN; dùng cho mọi screen có hiện wallet type | N/A (shared constant) | Mobile-only | NEW | Thêm promotion2 vào enum + 4 ARB; regen .g.dart các file model (balance.g.dart, staff.g.dart, wallet_balance.g.dart) | server_constants.dart:300-346, intl_vi.arb:166, intl_en.arb:166 |
SCR-10-STAFF-01 | App staff (Flutter) — Customer detail balance KM 2 | staff/lib/data/data_source/remote/customer/customer_repository.impl.dart:473-481 + customer detail UI hiện có | Thêm 1 API call getBalances(WalletType.promotion2); UI hiện thêm row "Ví KM 2: {balance}đ" | — | Staff lookup balance KM 2 khi xem customer detail | customer_management:access + branch scope (đồng bộ DEC-013) | Tablet | UPDATE (conditional) | Hiện row khi balance > 0 (parity với pattern row Ví KM 1 hiện có); ẩn nếu khách chưa có lot KM 2 | customer_repository.impl.dart:473-481 |
Tiêu chí pass B0.1: mỗi UI ID có Delta Status, mỗi
NEW/UPDATEcó evidence file/component. Không cóREMOVEtrong scope.KEEPđược dùng cho UI hiện hữu phải xuất hiện ở target wireframe.
B0.2) Từ điển Delta Status
| Status | Ý nghĩa | Evidence bắt buộc |
|---|---|---|
KEEP | Giữ nguyên thành phần / hành vi hiện tại | Vẫn xuất hiện ở target wireframe; không mô tả lại detail |
UPDATE | Đổi data mapping / validation / copy / điều kiện hiển thị | FR/DEC/AC + ref file:line |
MOVE | Đổi vị trí, giữ behavior | UX flow ghi rõ trước/sau khu vực nào |
NEW | Thêm mới | FR/AC ref + tooltip / state / variant |
REMOVE | Bỏ thành phần | DEC + FR/AC + PO approval (≥2 evidence) — KHÔNG có trong scope hiện tại |
HIDE | Ẩn theo điều kiện | Permission/state/lifecycle rõ trong PRD/SOURCE_OF_TRUTH |
B0.3) Tiêu chí hoàn thành B0
- [x] 100% UI hiện tại của các màn bị đụng đã được inventory ở B0.1 (kèm evidence)
- [x] Không có block/field/action hiện hữu nào thiếu Delta Status
- [x] Mọi
REMOVE/HIDEcó ≥2 evidence — N/A vì không cóREMOVE;HIDEđều gắn permission/state - [x] Target wireframe ở B2 hiển thị các vùng
KEEPliên quan, không chỉ vẽ phần update
B0.4) Ma trận Field × Surface
Mỗi field mới hoặc field bị update phải có 12 cột. Cell trống = fail.
| Field | List table | Detail/Form | Popup/Modal | Export Excel | Search | Filter | Permission gating | Mobile (tablet POS) | Notification | Default | Validation | Tooltip |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
wallet_km2_config.disabled | Không | SCR-01 toggle | Không | Không | Không | Không | internal_configuration:update | Không cần (Admin web) | Không | true (sau migration) | Boolean | "Bật cho POS sau khi readiness pass" |
wallet_km2_config.max_percent_per_order | Không | SCR-01 input | Không | Không | Không | Không | internal_configuration:update | Không | Không | 20 | Integer 1-100, bắt buộc | "Mỗi đơn được trả bằng KM2 tối đa X% phần eligible" |
wallet_km2_config.allow_combine_km1 | Không | SCR-01 toggle | Không | Không | Không | Không | internal_configuration:update | Không | Không | false | Boolean | "Bật = khách dùng cả 2 ví trong 1 đơn" |
wallet_km2_config.allow_refund | Không | SCR-01 toggle | Không | Không | Không | Không | internal_configuration:update | Không | Không | true | Boolean | "Tắt = ẩn CTA Hoàn ví KM2 toàn hệ thống" |
wallet_km2_config.refund_fee_percent | Không | SCR-01 input | Hiện ở SCR-07 (calc) | Không | Không | Không | internal_configuration:update | Không | Không | 20 | Integer 0-100 | "Phí xử lý hoàn ví, tính trên giá mua gói" |
wallet_km2_config.refund_deadline_days | Không | SCR-01 input | Hiện ở SCR-07 (max date) | Không | Không | Không | internal_configuration:update | Không | Không | 30 | Integer > 0 | "Thời hạn cho phép tạo yêu cầu hoàn ví KM2" |
wallet_km2_config.refund_extend_days | Không | SCR-01 input | Không (engine) | Không | Không | Không | internal_configuration:update | Không | Không | 30 | Integer > 0 | "Khi refund DV gia hạn lot hết hạn thêm N ngày" |
prepaid_card.wallet_target | SCR-02 cột "Ví đích" badge | SCR-02 radio | Không | Không | Không | Có (filter Gói KM2 ở SCR-02 list) | internal_configuration:access | Không | Không | VND | Enum VND / VND_PROMOTION_2, bắt buộc | "Chọn KM2 để biến thẻ này thành Gói Ví KM2" |
prepaid_card.expiry_months | SCR-02 cột "HSD" | SCR-02 input (conditional) | Không | Không | Không | Không | internal_configuration:update | Không | Không | 6 | Integer > 0 khi wallet_target=VND_PROMOTION_2, ẩn khi VND | "Số tháng từ ngày khách trả lần đầu" |
product.allow_promo_wallet_2 / service.allow_promo_wallet_2 | Không | SCR-05 toggle | Confirm dialog | Không | Không | Không (Phase 1) | internal_configuration:update, portal Admin | Không | Không | false | Boolean | "Cho phép thanh toán bằng Ví KM 2 trên item này" |
wallet_km2_lot.balance | SCR-06 bảng cột "Còn lại"; SCR-08 KPI/bảng | SCR-06 detail row | SCR-07 (snapshot) | Có (Sheet 2) | Không | Không (filter trạng thái có) | customer_management:access (qua action) | Tablet | Không | — | Currency ≥ 0 | "Số dư còn lại của lần mua này" |
wallet_km2_lot.expired_at | SCR-06 cột "Hết hạn"; SCR-08 bảng sắp hết hạn | SCR-06 detail | SCR-07 (snapshot) | Có (Sheet 2) | Không | Có (Phase 3 report) | customer_management:access | Tablet | Có (cron schedule, không UI) | end_of_day(+ N tháng) | Date sau ngày active | "Sau 23:59:59 ngày này, số dư bị xoá" |
wallet_km2_lot.status | SCR-06 (badge); SCR-08 bảng | SCR-06 detail | SCR-07 (đọc snapshot) | Có (Sheet 1+2) | Không | Có (filter trạng thái) | customer_management:access | Tablet | Không | active (lot chỉ được tạo khi khách trả ≥ 1 đồng cho đơn nạp Gói Ví KM2) | Enum lifecycle | "Đang hoạt động / Đã hết / Đã dùng hết / Đã hoàn" |
wallet_km2_lot.package_name (snapshot) | SCR-06; SCR-08 | SCR-06 detail | SCR-07 (Tên Gói) | Có (Sheet 1) | Có (recent searches) | Có (filter Gói Ví KM2) | customer_management:access | Tablet | ZNS biến {package_name} | Theo prepaid_card.name tại thời điểm bán | NotNull | "Tên Gói Ví KM2 lúc bán; không đổi khi Admin sửa thẻ sau" |
wallet_km2_lot.package_price (snapshot) | Không (debug) | SCR-06 detail (tooltip) | SCR-07 (Giá mua gói) | Có (Sheet 2) | Không | Không | customer_management:access | Tablet | Không | Theo prepaid_card.value | NotNull | "Giá khách trả cho Gói Ví KM2" |
wallet_km2_lot.wallet_value (snapshot) | Không | SCR-06 (tooltip) | SCR-07 (Giá trị ví) | Có (Sheet 2) | Không | Không | customer_management:access | Tablet | ZNS biến {wallet_value} | Theo prepaid_card.value_into_wallet | NotNull | "Số tiền vào ví KM2 cho Gói Ví KM2 này" |
wallet_km2_lot_deduction.amount | SCR-06 lịch sử | Không | Không | Có (Sheet 1) | Không | Không | customer_management:access | Tablet | Không | — | Currency > 0 | "Số tiền bị trừ trong giao dịch tham chiếu" |
wallet_km2_lot_deduction.deduction_type | SCR-06 lịch sử badge | Không | Không | Có | Không | Không | customer_management:access | Tablet | Không | — | Enum payment / refund_back / refund_request | "Loại bút toán: trừ thanh toán / hoàn ngược / hoàn ví KM2" |
Payment chip Ví KM 2 (UI) | Không | SCR-04 chip | SCR-04 block khi chọn | Không | Không | Không | service_order:payment / product_order:payment | Tablet POS | Không | Hidden | Conditional: config bật + balance > 0 + eligible > 0 | "Ví KM 2 chỉ áp dụng cho item bật Ví KM 2" |
Cột NẠP VÍ KM 2 (SCR-03) | Có | SCR-03 cột conditional | Không | Có (in receipt) | Không | Không | prepaid_order:create | Tablet POS | Không | Ẩn (chỉ hiện khi có dòng KM2) | Number ≥ 0 | "Số tiền sẽ vào Ví KM 2 cho khách (auto-tính theo Gói)" |
| Số tiền KM2 áp dụng (SCR-04 readonly) | Không | SCR-04 input readonly | Không | Có (in receipt) | Không | Không | service_order:payment / product_order:payment | Tablet | Không | Auto-fill MIN(eligible×max%, balance) | Readonly, không cho NV nhập tự do | "Tự động tính, NV không sửa" |
transaction_request.behavior_id='refund_km2_wallet' | SCR-07 list (badge "Loại") | Không | SCR-07 dialog (readonly) | Có (export request) | Không | Có (filter loại) | refund_request_management_submenu:access | Tablet | Không | — | Enum cố định | "Đánh dấu yêu cầu thuộc luồng Hoàn ví KM2" |
transaction_request.lot_id (mới) | Không | Không | SCR-07 dialog (snapshot) | Có (audit) | Không | Không | refund_request_management_submenu:access | Tablet | Không | — | UUID NotNull khi behavior=refund_km2_wallet | "Tham chiếu lot bị hoàn" |
transaction_request.km2_deduct_amount | Không | Không | SCR-07 dialog | Có (audit) | Không | Không | refund_request_management_submenu:access | Tablet | Không | lot.balance tại thời điểm tạo | Currency ≥ 0 | "Số dư KM2 bị tất toán khi yêu cầu duyệt" |
transaction_request.suggested_refund_amount | Không | Không | SCR-07 dialog "Khách nhận" | Có (audit) | Không | Không | refund_request_management_submenu:access | Tablet | Không | Theo FORMULA-002 | Currency ≥ 0 (clamp 0) | "Số tiền dự kiến hoàn cho khách" |
transaction_request.refund_method | Không | Không | SCR-07 radio | Có | Không | Có (filter cho kế toán) | refund_request_management_submenu:create | Tablet | Không | cash | Enum cash / wallet / bank_transfer | "Hoàn vào tiền mặt / Ví VND / chuyển khoản" |
transaction_request.policy_snapshot | Không | Không | SCR-07 (debug) | Có (audit) | Không | Không | refund_request_management_submenu:access | Tablet | Không | JSON snapshot wallet_km2_config | NotNull | "Bản sao chính sách lúc tạo, dùng cho audit" |
| Banner cảnh báo expiry (SCR-06) | Không | SCR-06 banner | Không | Không | Không | Không | customer_management:access | Tablet | Không (text trong UI, không push) | Hiện khi có lot ≤ 30 ngày | Format DD/MM/YYYY | "Cảnh báo NV đề xuất khách dùng trước hết hạn" |
B0.5) Ma trận State × Screen (6 state mandatory cho mọi màn có data)
| Màn / khối | Default | Loading | Empty | Error (retry) | No permission | Partial / Pending |
|---|---|---|---|---|---|---|
SCR-01 form Settings | Hiện toggle/input theo config hiện tại | Skeleton input + toggle 200ms | N/A (form luôn có 1 record config) | Toast "Lưu thất bại, vui lòng thử lại" + nút "Thử lại"; giữ input | Ẩn menu sidebar Ví KM 2 (route guard) | N/A — config save toàn vẹn (atomic) |
SCR-02 form Gói Ví KM2 | Form rỗng (tạo) hoặc fill từ record (sửa) | Skeleton 4 ô input | N/A (form) | Inline error per field + banner "Mã thẻ này đã tồn tại"; giữ input | Ẩn CTA Tạo mới / Sửa ở list | N/A |
SCR-02 list Gói Ví KM2 | 50 dòng đầu, sort theo created_at desc | Skeleton 5 row | "Chưa có thẻ trả trước/Gói Ví KM2 nào" + CTA "Tạo mới" | "Không thể tải. Vui lòng thử lại" + nút "Thử lại" | Ẩn route /settings/prepaid-card | N/A |
SCR-03 bảng nạp tiền | Bảng rỗng (chưa có dòng), nút + Thêm thẻ nạp | Skeleton 1 dòng khi load thẻ | "Chưa chọn thẻ nào. Bấm + Thêm thẻ nạp để bắt đầu" | Toast "Không thể tải danh sách thẻ" + nút "Tải lại" | Ẩn nút Tạo Mới (route guard) | N/A — đơn nạp tiền hoặc tạo thành công hoặc lỗi |
SCR-04 block KM2 trong payment | Hiện chip Ví KM 2 chỉ khi đủ điều kiện (config bật + available_amount > 0 + eligible > 0) | Skeleton chip 100ms khi load action get_customer_km2_balance (DEC-029) | Ẩn chip nếu eligible_total = 0 (không hiển thị empty state) | Toast "Số dư không đủ, vui lòng thử lại" + refetch action; nếu deducted_amount < requested_amount do lot vừa expired → toast cảnh báo và cập nhật balance | Ẩn chip Ví KM 2 (no-render) | Backend trả 207 partial khi multi-payment có 1 method fail → toast "Đã trừ KM2 thành công, phần tiền mặt thất bại. Vui lòng thử lại phần còn lại." |
SCR-05 toggle SP/DV | Toggle theo allow_promo_wallet_2 hiện tại | Skeleton toggle | N/A | Toast "Cập nhật thất bại" + revert toggle | POS portal: ẩn action update; toggle readonly | N/A |
SCR-06 card Ví KM 2 wallet stats | Hiện available_amount từ action get_customer_km2_balance (DEC-029, realtime) | Skeleton card 200ms | Card vẫn hiện "0đ" khi available_amount=0; click vẫn mở popup | Toast lỗi load action + retry | Ẩn card (không render) | Banner cảnh báo nearest_expiry_at ≤ 30 ngày hoặc expired_unswept_amount > 0 (admin xem được phần chờ scheduler clean) |
SCR-06 popup chi tiết lot | Hiện 2 bảng đang HĐ + đã hết, banner cảnh báo nếu có | Skeleton 5 row x 2 bảng + banner skeleton | "Khách chưa có Gói Ví KM2 nào trong Ví KM 2" + lý do | "Không thể tải danh sách. Thử lại" + nút retry | Ẩn popup, ẩn card | Lot có credited_amount < initial_amount (đơn còn nợ) — hiện inline label "Đã nạp X/Y" trong bảng "Đang hoạt động" |
SCR-07 dialog hoàn ví KM2 | Dialog hiện 3 block với calc | Spinner overlay khi tính refund | N/A (chỉ mở khi có lot eligible) | Toast "Tạo yêu cầu thất bại" + giữ form | Không mở được dialog (CTA ẩn ở SCR-06) | "Lot vừa hết hạn race condition" — backend 409 → modal "Lot đã hết hạn. Vui lòng tải lại" |
SCR-08 dashboard report | KPI + 3 bảng theo filter mặc định | Skeleton 4 KPI + 3 bảng | KPI = "0"/"—", bảng "Chưa có dữ liệu" | Toast lỗi + nút "Tải lại" | Ẩn menu route, ẩn nút export | "Đang xử lý export…" với async > 5,000 dòng |
SCR-FR012 ví trong customer (CRM) | Dòng Ví KM 2 hiện khi available_amount > 0 từ action get_customer_km2_balance (DEC-029) | Skeleton 1 dòng | Ẩn dòng (không hiện 0đ trong CRM) | Toast lỗi load action + retry | Ẩn cả khối ví khách | N/A |
SCR-FR012 hoá đơn in | Dòng PTTT KM2 hiện khi có | Print spinner trước render | Ẩn dòng | "Không thể tải. Vui lòng tải lại" | Backend không trả số → ẩn | N/A |
SCR-FR012 Fund/Quỹ | Cột Vào Ví KM 2 hiện khi có | Skeleton bảng | Ẩn cột nếu range = 0 | Toast lỗi + retry | Ẩn module Fund | N/A |
Quy tắc fail: màn có data nhưng thiếu Empty / Loading / Error → fail G3.7. Đã cover toàn bộ 13 màn/khối ở trên.
B0.6) Quy ước chất lượng wireframe (Wireframe Quality Contract)
| Quy tắc | Áp dụng cho package này |
|---|---|
| Số cột bảng ASCII | Mọi wireframe ASCII trong B2.x dùng cùng số │ cách đều; không lệch quá 1 ký tự |
| Số bullet ↔ label | Stepper / step indicator của setup readiness ở SCR-01 phải có 4 chấm + 4 label rõ |
| Dữ liệu mẫu | Tên KH Nguyễn Thị Lan, Trần Văn Nam, Lê Thị Hoa, Phạm Minh Đức (VN có dấu); SĐT 0909 123 456; tên Gói Gói Gold, Gói Silver, Gói Diamond; dịch vụ Massage body 90p, Liệu trình trị mụn 5 buổi; tiền 1.250.000đ, 5.000.000đ; ngày 24/03/2026, 15/04/2026 |
| Không TBD/Lorem | Mọi wireframe phải có dữ liệu thật, không {placeholder} / TBD / Lorem |
| Không hard-code style | Spec dùng intent (warning, success, muted, danger, negative-value); không "màu cam đỏ", "font 12px", "icon 🎁" trừ khi đã chốt design system |
| Buttons label | Mọi nút trong wireframe đều có dòng tương ứng trong B6 từ điển; danh sách CTA: Lưu cài đặt, Lưu, Huỷ, Tạo Mới, + Thêm thẻ nạp, Hoàn thành, Thanh toán, Tạo yêu cầu hoàn tiền, Hoàn ví KM2, Xác nhận, Đóng, Xuất Excel, Tải lại, Thử lại |
| Truncation | Tên Gói dài → cell hiển thị Gói Premium Diamond V… với …; cột "Tên" trong bảng width 180px |
| Mobile preview | Diva Admin desktop-first; POS chạy tablet ≥ 768px → wireframe POS có chú thích "Tablet POS"; không có pattern mobile < 768px |
Lint nhanh áp dụng cho file này:
bash
rg -n "TBD|TODO|Lorem|\{placeholder\}|XXX|FIXME" docs/features/vi-km-2/ui-spec.md # phải rỗng
rg -n '"(Submit|Confirm|Cancel|OK|Loading|No data|Error|Success|Save|Delete|Edit|View)"' docs/features/vi-km-2/ui-spec.md # phải rỗngB0.7) Cặp Code ↔ Display VI (Bilingual Pairing)
| Code | Display VI canonical | Phân biệt với | Dùng ở section |
|---|---|---|---|
VND_PROMOTION_2 | Ví KM 2 (Promotion Wallet 2) | VND_PROMOTION (Ví KM 1) | B0.4, B2.x, dev-spec C4 |
wallet_promotion_2 | Phương thức thanh toán "Ví KM 2" | wallet_promotion (KM1) | B0.4, B2.4, B-FR012 |
prepaid_card.wallet_target | Ví đích | — | B2.2 |
wallet_target='VND_PROMOTION_2' | Ví đích = Ví KM 2 | VND (Ví VND mặc định) | B2.2, B2.3 |
wallet_km2_lot.status='active' | Đang hoạt động | expired (hết hạn), exhausted (dùng hết), refunded (đã hoàn) | B2.6 badge |
wallet_km2_lot.status='exhausted' | Đã dùng hết | active, expired | B2.6 badge |
wallet_km2_lot.status='expired' | Đã hết hạn | exhausted (hết do dùng), refunded (đã hoàn) | B2.6 badge, banner cảnh báo |
wallet_km2_lot.status='refunded' | Đã hoàn | expired, exhausted | B2.6 badge |
deduction_type='payment' | Trừ thanh toán | refund_back (hoàn ngược), refund_request (hoàn ví KM2) | B2.6 lịch sử |
deduction_type='refund_back' | Hoàn ngược (refund đơn DV/SP) | payment, refund_request | B2.6 |
deduction_type='refund_request' | Hoàn ví KM2 | payment, refund_back | B2.6, B2.7 |
transaction_request.behavior_id='refund_km2_wallet' | Loại yêu cầu "Hoàn ví KM2" | refund_order (hoàn đơn DV), refund_order_cosmetic (hoàn đơn SP) | B2.7, B-FR012 |
refund_method='cash' | Hoàn tiền mặt | wallet, bank_transfer | B2.7 radio |
refund_method='wallet' | Hoàn vào Ví VND | cash, bank_transfer | B2.7 radio |
refund_method='bank_transfer' | Chuyển khoản | cash, wallet | B2.7 radio |
allow_promo_wallet_2=true | Cho phép Ví KM 2 | allow_promo_wallet (KM1) | B2.5 toggle |
wallet_km2_config.disabled=true | Tính năng KM2 đang tắt | disabled=false (đang hoạt động) | B2.1 banner |
Quy tắc:
- Trong văn xuôi: display VI trước, code backtick sau —
Đang hoạt động (active) - Trong wireframe ASCII / Mermaid: dùng code raw
active,expired(không dịch) - Trong filter dropdown: hiện display VI; nếu cần đối chiếu code → ghi tooltip
B0.8) Đối soát schema (Schema Cross-Check)
Đảm bảo UI không cho phép null/blank ở field schema NOT NULL.
| Field UI cho phép trống / null | Cột schema (dev-spec C4) | NOT NULL? | Cách UI xử lý |
|---|---|---|---|
prepaid_card.expiry_months | expiry_months INT | NULL khi wallet_target='VND', NOT NULL khi wallet_target='VND_PROMOTION_2' | UI ẩn field khi wallet_target='VND' để tránh null không hợp lệ; hiện và bắt buộc nhập khi wallet_target='VND_PROMOTION_2' (validate FE + BE check) |
wallet_km2_lot.branch_id | branch_id UUID NOT NULL | NOT NULL | Lấy auto từ prepaid_order.branch_id lúc bán; không cho NV để trống branch trong SCR-03 |
wallet_km2_lot.activated_at | activated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | NOT NULL (lot chỉ được tạo khi khách trả ≥ 1 đồng, BE set activated_at = NOW() ngay lần trả đầu) | UI luôn có giá trị; không có UI state "Chưa kích hoạt" cho lot |
wallet_km2_lot.package_name, package_price, wallet_value | NOT NULL | NOT NULL | Snapshot từ prepaid_card lúc tạo lot; UI không cho sửa |
transaction_request.lot_id | NULL với behavior khác, NOT NULL với behavior refund_km2_wallet | Conditional NOT NULL | SCR-07 luôn lấy lot_id từ context khi mở dialog; không cho submit nếu thiếu |
wallet_km2_lot_deduction.order_id | NOT NULL với type payment/refund_back; NULL với type refund_request | Conditional NOT NULL | UI không can thiệp; backend tự fill |
Quy tắc: trước khi viết B2 form, đã đối chiếu với dev-spec.md C4. UI không có field NOT NULL nào cho phép null trống.
bash
# Lint xác minh
rg -n "_id=NULL|=NULL OK|null được|NULL được|=null" docs/features/vi-km-2/ui-spec.md # phải rỗng (đã pass)
rg -n "NOT NULL" docs/features/vi-km-2/dev-spec.md | head -50B0.9) Bảng kiểm kê tương tác (Interaction Inventory — BẮT BUỘC profile L)
Liệt kê 100% interactive elements per SCR. Cột "Loại tương tác" PHẢI dùng 1 trong 10 enum canonical:
Primary CTA/Secondary CTA/Drill-down/Hover state/Context menu/Inline edit/Tab switch/Load more/Overlay/Expand-Collapse.Quy tắc kiểm kê:
- Scan
<q-btn>,<button>,@click,v-on:click,q-toggle,q-chip,q-input,q-select, hover states, modal triggers trong từng SCR- Mỗi row = 1 interactive element (KHÔNG group nhiều element)
- Element không có TC tương ứng → fail QA coverage gate
- Display-only element (banner, toast read-only, polling background, display row/column) KHÔNG kê ở đây — đã cover ở B2.x State Matrix
SCR-01: Cài đặt Ví KM 2
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
btn_save_settings | Card "CẤU HÌNH CHUNG" → footer | Primary CTA | Validate max_percent_per_order 1-100; gọi mutation update config; cần internal_configuration:update | Default / Loading khi submit / Disabled khi dirty=false / Error inline 4xx | B2.1.7B | TC-001-01..04 |
tgl_enable_km2 | Card "CẤU HÌNH CHUNG" header | Inline edit (field) | Toggle disabled=true/false; confirm dialog khi tắt nếu khách đang giữ balance > 0 | Default / Loading / Disabled khi readiness fail / Confirm Overlay | B2.1.7C | TC-001-05 |
tgl_allow_combine_km1 | Card "CẤU HÌNH CHUNG" | Inline edit (field) | Toggle cho phép dùng cả 2 ví trong 1 đơn | Default / Loading | B2.1.7B | TC-004-08 |
tgl_allow_refund | Card "CHÍNH SÁCH HOÀN VÍ KM2" | Inline edit (field) | Toggle bật/tắt refund flow; hide refund fields khi tắt | Default / Loading / Hide dependent fields khi tắt | B2.1.7B | TC-009-ALLOW-REFUND |
input_max_percent_per_order | Card "CẤU HÌNH CHUNG" | Inline edit (field) | Range 1-100 integer; block keystroke > 100 | Default / Error inline / Disabled khi tắt KM2 | B2.1.6 | TC-001-03..04 |
input_refund_fee_percent | Card "CHÍNH SÁCH HOÀN VÍ KM2" | Inline edit (field) | Range 0-100 integer | Default / Error inline / Hidden khi allow_refund=false | B2.1.6 | TC-009-FEE-100 |
input_refund_deadline_days | Card "CHÍNH SÁCH HOÀN VÍ KM2" | Inline edit (field) | Integer > 0 | Default / Error inline | B2.1.6 | TC-009-PERIOD-0 |
input_refund_extend_days | Card "CHÍNH SÁCH HOÀN VÍ KM2" | Inline edit (field) | Integer > 0 | Default / Error inline | B2.1.6 | TC-008-EXTEND |
link_readiness_setup | Stepper card (per step) | Drill-down | Click mở route setup tương ứng (prepaid-card, product, service, payment_gateway) | Default / Disabled khi step đã pass | B2.1.6 | TC-001-READINESS-LINK |
tooltip_max_percent_hint | Input max_percent_per_order | Hover state | Tooltip giải thích "% tối đa Ví KM 2 được trả trên phần eligible mỗi đơn" | Hover trigger | B2.1.6 | — |
SCR-02: Tạo / sửa Gói Ví KM2
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
btn_create_package | Header list | Primary CTA | Mở form tạo mới; cần internal_configuration:update | Default / Disabled khi POS portal | B2.2.7B | TC-002-01 |
btn_save_package | Form footer | Primary CTA | Validate form đầy đủ; submit mutation | Default / Loading / Error 4xx | B2.2.7B | TC-002-01..04 |
btn_cancel_package | Form footer | Secondary CTA | Confirm Overlay nếu form dirty | Default / Confirm Overlay | B2.2.7B | TC-002-CANCEL-DIRTY |
radio_wallet_target | Form body | Inline edit (field) | Switch VND / VND_PROMOTION_2; show/hide expiry_months | Default / Disabled khi sửa thẻ đang bán | B2.2.6 | TC-002-01..02 |
input_code | Form body | Inline edit (field) | Unique check qua API debounce 500ms | Default / Error duplicate / Loading | B2.2.6 | TC-002-DUP |
input_name | Form body | Inline edit (field) | Bắt buộc | Default / Error required | B2.2.6 | TC-002-01 |
input_value | Form body | Inline edit (field) | Bắt buộc, > 0 | Default / Error | B2.2.6 | TC-002-VALUE |
input_value_into_wallet | Form body | Inline edit (field) | ≥ value | Default / Error "phải ≥ mệnh giá" | B2.2.6 | TC-002-04 |
input_expiry_months | Form body | Inline edit (field) | Conditional: bắt buộc khi wallet_target='VND_PROMOTION_2' | Default / Hidden khi VND / Error | B2.2.6 | TC-002-03 |
filter_wallet_target_list | List header | Inline edit (field) | Filter dropdown All / VND / VND_PROMOTION_2 | Default / All option | B2.2.6 | TC-002-LIST-FILTER |
row_click_edit | List body | Drill-down | Mở form sửa | Default / Disabled khi no-permission | B2.2.6 | TC-002-EDIT |
SCR-03: Bán Gói Ví KM2
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
btn_create_order | Panel phải footer | Primary CTA | Validate; gọi mutation tạo prepaid_order + payment; cần prepaid_order:create + prepaid_order:payment | Default / Loading / Disabled khi form invalid / Error 4xx + correlation_id | B2.3.7B | TC-003-01..09 |
btn_add_card_row | Panel trái bảng | Secondary CTA | Thêm row trống vào bảng thẻ nạp | Default | B2.3.7B | TC-003-ADD-ROW |
btn_remove_card_row | Mỗi row bảng | Secondary CTA | Xoá row; nếu row cuối có KM2 → ẩn cột NẠP VÍ KM 2 | Default / Confirm Overlay khi row cuối | B2.3.7B | TC-003-REMOVE-LAST-KM2 |
btn_cancel_order | Panel phải footer | Secondary CTA | Confirm Overlay leave dirty | Default / Confirm Overlay | B2.3.7B | TC-003-CANCEL-DIRTY |
select_prepaid_card_type | Mỗi row bảng | Inline edit (field) | Filter disabled=false; badge "Gói Ví KM2" cho wallet_target='VND_PROMOTION_2' | Default / Loading / Empty "Chưa có Gói" | B2.3.6 | TC-003-SELECT |
input_qty | Mỗi row bảng | Inline edit (field) | > 0 integer | Default / Error | B2.3.6 | TC-003-06 |
chip_payment_method | Panel phải | Inline edit (field) | Chip group; disable installment khi có dòng KM2 (DEC-017) | Default / Disabled với tooltip | B2.3.7B | TC-003-INSTALLMENT-BLOCKED |
input_paid_amount | Panel phải | Inline edit (field) | ≤ tổng đơn | Default / Error | B2.3.6 | TC-003-DEBT |
select_attached_appointment | Panel trái | Inline edit (field) | Lấy lịch hẹn liên quan của khách | Default / Empty | B2.3.6 | TC-003-APPT |
SCR-04: Thanh toán bằng Ví KM 2
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
chip_pay_km2 | Block payment method | Inline edit (field) | Chip toggle Ví KM 2; hiện khi config bật + available_amount > 0 + eligible_total > 0; click = gen payment_attempt_id UUID v4 | Default / Selected / Disabled / Hidden (no-render) | B2.4.7B | TC-004-01..02, TC-004-05..06 |
btn_unselect_km2 | Block KM2 calc | Secondary CTA | Bỏ chọn chip → reset payment_attempt_id | Default / Disabled khi submitting | B2.4.7B | TC-004-UNSELECT |
btn_submit_payment | Panel phải | Primary CTA | Validate đủ tiền; gửi deduct_km2_payment với payment_attempt_id | Default / Loading / Disabled / Error 409 idempotent_replay | B2.4.7B | TC-004-11..13, TC-IDEMPOTENT-* |
hover_balance_tooltip | Block KM2 calc — Số dư | Hover state | Tooltip "Số dư realtime từ get_customer_km2_balance — không tính lot expired" | Hover trigger | B2.4.6 | TC-004-TOOLTIP |
hover_max_percent_tooltip | Block KM2 calc — Max% | Hover state | Tooltip giải thích phần eligible × max% | Hover trigger | B2.4.6 | — |
confirm_partial_deduction | Sau submit nếu deducted < requested | Overlay | Toast Overlay cảnh báo lot vừa expired; option retry hoặc dùng phương thức khác | Default / Auto-show 5s + manual close | B2.4.10 | TC-BALANCE-RACE-01 |
SCR-05: Flag eligible per SP/DV
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
tgl_allow_promo_wallet_2 | Card "Trạng thái" | Inline edit (field) | Toggle allow_promo_wallet_2; cần internal_configuration:update, portal Admin | Default / Loading / Confirm Overlay / Readonly khi POS portal | B2.5.7B | TC-005-01..04 |
confirm_modal_toggle | Khi click toggle | Overlay | Confirm Yes/No "Bạn có chắc muốn cho phép thanh toán bằng Ví KM 2..." | Default / Loading | B2.5.7B | TC-SCR05-D-01 |
SCR-06: Tab Ví KM 2 trong customer profile
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
card_km2_balance | Wallet stats sidebar trái | Drill-down | Click card → mở popup chi tiết; load action get_customer_km2_balance | Default / Loading skeleton 200ms / Empty "0đ" / Error retry / Hidden no-permission | B2.6.7 | TC-006-01..06 |
popup_lot_detail | Click card | Overlay | Popup 60vw desktop / 90vw tablet; trap focus | Default / Loading / Empty / Error / 2 bảng lot | B2.6.7B | TC-SCR06-A..J |
btn_close_popup | Popup header | Secondary CTA | Đóng popup; confirm Overlay nếu dirty | Default | B2.6.7B | TC-006-CLOSE |
filter_lot_status | Popup header | Inline edit (field) | Select Tất cả / Đang HĐ / Đã hết hạn / Đã dùng hết / Đã hoàn | Default / Re-render client | B2.6.6 | TC-006-FILTER |
filter_package_name | Popup header | Inline edit (field) | Select tất cả Gói khách đã mua | Default | B2.6.6 | TC-006-FILTER-PKG |
row_click_drawer | Bảng lot | Drill-down | Click row mở drawer "Lịch sử giao dịch lot" | Default / Loading | B2.6.7B | TC-006-DRAWER |
btn_refund_lot | Row "Đang hoạt động" | Primary CTA | Mở SCR-07 dialog; cần refund_request_management_submenu:create; lot eligible | Default / Hidden no-permission / Disabled với tooltip khi expired/refunded | B2.6.10 | TC-009 + TC-PERM-SCR06 |
hover_tooltip_balance | Card title ⓘ | Hover state | Tooltip "Tổng số dư các Gói đang hoạt động. Không tính lot hết hạn / dùng hết / hoàn" | Hover trigger | B2.6 | TC-006-TOOLTIP |
hover_tooltip_expired_unswept | Banner phần expired_unswept_amount (admin) | Hover state | Tooltip giải thích "Số dư đang chờ scheduler clean — sẽ về 0 sau cron 00:05" | Hover trigger | B2.6 | TC-006-UNSWEPT |
SCR-07: Form Hoàn ví KM2
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
dialog_refund_form | Stack trên popup SCR-06 | Overlay | Modal dialog trap focus; Esc đóng nếu không dirty | Default / Loading / Error race | B2.7.7B | TC-009-01..08 |
input_km2_deduct_amount | Form body | Inline edit (field) | 0 < x ≤ lot.balance; auto-fill = lot.balance | Default / Error / Readonly khi approver thiếu update | B2.7.6 | TC-009-AMOUNT |
input_customer_refund_amount | Form body | Inline edit (field) | 0 ≤ x ≤ totalPaidAmount; auto-fill từ FORMULA-002 | Default / Error / Readonly | B2.7.6 | TC-009-REFUND |
radio_refund_method | Form body | Inline edit (field) | Radio cash/wallet/bank_transfer; bắt buộc | Default / Error | B2.7.6 | TC-009-METHOD |
btn_submit_request | Form footer | Primary CTA | Validate đủ; tạo transaction_request | Default / Loading / Error 409 | B2.7.7B | TC-009 |
btn_cancel_request | Form footer | Secondary CTA | Confirm Overlay leave dirty | Default / Confirm Overlay | B2.7.7B | TC-009-CANCEL-DIRTY |
confirm_refund_zero | Khi suggested_refund = 0 | Overlay | Confirm "Khách KHÔNG nhận tiền nhưng Gói {tên} sẽ tất toán" | Default | B2.7.10 | TC-009-ZERO |
hover_tooltip_fee_calc | Field "Phí xử lý" | Hover state | Tooltip công thức "phí = giá mua gói × refund_fee_percent / 100" | Hover trigger | B2.7.6 | TC-009-TOOLTIP |
SCR-08: Report dashboard (Phase 3)
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
filter_date_range | Header dashboard | Inline edit (field) | Date picker range; default 30 ngày gần nhất | Default / Validate range hợp lệ | B2.8.6 | TC-010-FILTER |
filter_branch | Header | Inline edit (field) | Select theo branch_mode permission | Default / All option (admin only) | B2.8.6 | TC-PERM-SCR08 |
btn_export_excel | Header | Secondary CTA | Cần report:export; async > 5K rows | Default / Loading / Error | B2.8.7B | TC-EXPORT |
row_click_drilldown_customer | Top khách table | Drill-down | Click row mở SCR-06 popup khách đó | Default | B2.8.6 | TC-010-DRILLDOWN |
tab_kpi_period | Header tabs | Tab switch | Hôm nay / 7 ngày / 30 ngày / Tuỳ chỉnh | Default / Selected | B2.8.6 | TC-010-TAB |
SCR-FR012: Tích hợp KM 2 vào màn hình hiện có
| Element ID | Vị trí | Loại tương tác | Behavior summary | State coverage | Ref Quy ước | Ref QA TC |
|---|---|---|---|---|---|---|
dropdown_request_type_km2 | Yêu cầu hoàn tiền form | Inline edit (field) | Option "Hoàn ví KM2" trong dropdown loại yêu cầu; hiện khi refund_request_management_submenu:create | Default / Hidden no-permission | B-FR012 | TC-FR012-REFUND-TYPE |
row_crm_km2_wallet_expand | CRM CustomerEWalletInformation.tsx | Expand-Collapse | Mở/đóng dòng "Ví KM 2" trong khối ví khách; load action get_customer_km2_balance | Default / Loading / Hidden khi available_amount = 0 | B-FR012 | TC-FR012-CRM |
hover_tooltip_invoice_km2 | Hoá đơn in / preview — dòng KM2 | Hover state | Tooltip "Tiền trừ từ Ví KM 2 — không tính thực thu" | Hover trigger | B-FR012 | TC-FR012-INVOICE-TOOLTIP |
filter_fund_km2 | Quỹ / Fund table header | Inline edit (field) | Filter cột "Vào Ví KM 2" on/off | Default / Hidden khi range = 0 | B-FR012 | TC-FR012-FUND |
Tổng kết kiểm kê: 9 SCR × ~5-11 element = ~60 interactive elements, tất cả map về 10 enum canonical. Lint
lint-interaction-coverage.shpass khi section này tồn tại đầy đủ + mọi row dùng enum.Quy tắc maintenance: mỗi khi thêm/sửa interactive element trong B2.x, PHẢI cập nhật row tương ứng ở B0.9 (single source). Element không có ở B0.9 = fail QA coverage gate.
B1) Bản đồ màn hình và hành trình
B1.0) Mục tiêu màn hình và CTA chính (quick-scan)
Bảng tổng hợp 8 SCR trong 1 chỗ để PO/UX/FE scan nhanh trước khi đọc chi tiết B2.x.
| SCR | Mục tiêu màn hình | Primary CTA | Secondary CTA | Khối thông tin nổi bật | Khi không đủ quyền / chưa đủ điều kiện |
|---|---|---|---|---|---|
| SCR-01 | Admin cấu hình KM 2 an toàn trước khi bật phương thức thanh toán cho POS | Lưu cài đặt, Bật Ví KM 2 cho POS (sau readiness 4 bước pass) | Huỷ, Xem checklist sẵn sàng, toggle bật/tắt | Trạng thái bật/tắt + readiness 4 bước; max% đơn hàng; KM1+KM2; chính sách Hoàn ví KM2 | Ẩn menu với Staff/POS; nếu readiness chưa pass thì chip danger "Hoàn tất 4 bước trước" |
| SCR-02 | Admin tạo Gói Ví KM 2 với ví đích KM 2, hạn sử dụng và giá trị ví rõ ràng | Lưu | Huỷ, Đóng | Ví đích, mệnh giá, giá trị nạp vào ví, hạn sử dụng | Staff/POS không thấy CTA Tạo mới; khi wallet_target=VND thì ẩn Hạn sử dụng; khi đã có lot, 4 field bị readonly |
| SCR-03 | Staff/Sale bán Gói Ví KM 2 bằng flow thẻ trả trước hiện hữu (mix với thẻ VND) | Tạo Mới | + Thêm thẻ nạp, Xoá dòng, Huỷ | Bảng nạp tiền (cột "NẠP VÍ KM 2" conditional) + summary "Tiền vào Ví KM 2" + cảnh báo chặn trả góp | Ẩn Gói Ví KM 2 khỏi dropdown nếu config tắt hoặc Gói disabled; chặn Trả góp cho dòng KM 2 (DEC-017) |
| SCR-04 | Staff/Sale dùng KM 2 để thanh toán một phần đơn DV/SP theo max% và FIFO | Thanh toán | Bỏ chọn Ví KM 2, Huỷ | Số dư KM 2; eligible total; số tiền KM 2 áp dụng (auto-fill, readonly); còn lại cần trả | Ẩn chip Ví KM 2 nếu config tắt / số dư = 0 / eligible = 0 / thiếu *:payment |
| SCR-05 | Admin đánh dấu SP/DV nào được phép trả bằng Ví KM 2 | Toggle (auto-save sau confirm) | — | Toggle KM 1 hiện hữu cạnh toggle KM 2 mới | POS portal: toggle readonly; thiếu internal_configuration:update thì ẩn action |
| SCR-06 | Staff/Manager xem tình trạng lot KM 2 của khách để tư vấn dùng trước hạn / hoàn tiền | Hoàn ví KM2 (per row, conditional) | Đóng, filter Trạng thái, filter Tên Gói | Tổng số dư KM 2; banner cảnh báo lot ≤ 7/30 ngày; bảng lot active + lot đã hết/hoàn | Card ẩn nếu thiếu customer_management:access; CTA Hoàn ẩn nếu thiếu refund_request_management_submenu:create hoặc lot không eligible |
| SCR-07 | Người có quyền tạo Yêu cầu Hoàn ví KM 2 đúng FORMULA-002 và policy | Tạo yêu cầu hoàn tiền | Huỷ, Đóng, Xem công thức (popover) | Snapshot Gói (giá mua, giá trị ví, tỉ lệ); calc 3 dòng (hoàn theo tỉ lệ, phí, KHÁCH NHẬN); radio Hoàn vào (cash/wallet/bank) | Chặn nếu thiếu refund_request_management_submenu:create hoặc lot expired/refunded/exhausted |
| SCR-08 | Manager/Admin theo dõi hiệu quả bán/dùng/hết hạn KM 2 theo branch × time | Xuất Excel | Filter khoảng thời gian, BranchSelect, refresh | 4 KPI (Đã bán / Doanh thu / Tỉ lệ dùng / Sắp cháy); 3 bảng (theo Gói / sắp hết hạn / top khách) | Phase 3 — chỉ bật sau khi PD-001 chốt route; ẩn menu nếu thiếu report:access; Manager chỉ thấy branch của mình |
B1.1) Danh sách màn
| SCR | Tên | Route | Loại | Mô tả |
|---|---|---|---|---|
| SCR-01 | Cài đặt Ví KM 2 | /s/internal-settings/promotion-wallet | 🔧 Extend | Bật/tắt phương thức + cấu hình max%, KM1+KM2, refund policy |
| SCR-02 | Tạo/sửa Gói Ví KM2 (mở rộng PrepaidCard) | /settings/prepaid-card, /settings/prepaid-card/create | 🔧 Extend | Thêm wallet_target + expiry_months |
| SCR-03 | Bán Gói Ví KM2 (mở rộng đơn nạp tiền) | /ecommerce/prepaid-card-order/create | 🔧 Extend | Thêm cột "NẠP VÍ KM 2" conditional + summary tương ứng |
| SCR-04 | Thanh toán đơn DV/SP bằng KM2 | /ecommerce/order/:id/payment, /ecommerce/cosmetic-order/:id/payment | 🔧 Extend | Thêm chip "Ví KM 2" + block auto-fill |
| SCR-05 | Flag eligible per SP/DV | /ecommerce/product/:id, /ecommerce/service/:id | 🔧 Extend | Thêm toggle allow_promo_wallet_2 |
| SCR-06 | Tab Ví KM 2 trong customer profile | /user/customer/:id | 🔧 Extend | Card + popup chi tiết lot + cảnh báo |
| SCR-07 | Form Hoàn ví KM2 trong Yêu cầu hoàn tiền | Dialog từ SCR-06 hoặc menu Yêu cầu hoàn tiền hiện có | 🔧 Extend | Tạo transaction_request behavior refund_km2_wallet |
| SCR-08 | Report dashboard Ví KM 2 | Theo PD-001 (Phase 3) | 🔧 Extend | Reuse shell PrepaidCardReport; Phase 3 |
| SCR-09 | Tab Ví khuyến mãi 2 trong wallet_screen.dart + Home widget + WalletType enum trên app khách (Flutter) | wallet_screen.dart + Home | 🔧 Extend (mobile) | Tab thứ 3 cùng cấp 2 tab hiện hữu (DEC-031); read-only Phase 1; PD-002 chốt in-scope |
| SCR-10 | Staff app — Customer detail balance KM 2 | Customer detail page (staff app) | 🔧 Extend (mobile) | Thêm fetch + hiển thị balance KM 2 trong customer detail; parity pattern Ví KM 1 hiện có |
| SCR-FR012 | Tích hợp KM2 vào hoá đơn / fund / report / CRM / SMS / affiliate config hiện có | Nhiều route | 🔧 Extend | Chỉ delta — xem B-FR012 |
B1.2) Hành trình end-to-end (theo vai trò)
Hành trình theo vai trò:
| Vai trò | Entry point | Đường màn | Kết quả đầu ra |
|---|---|---|---|
| Admin | Settings sidebar → Ví khuyến mãi | SCR-01 → SCR-02 → SCR-05 | Cấu hình KM2 hoàn chỉnh, sẵn sàng cho POS |
| Staff/Sale (POS) | POS → Nạp Tiền → Tạo Mới | SCR-03 → (chọn khách) | Đơn nạp tiền tạo lot KM2, ZNS gửi (nếu FR-011 bật) |
| Staff/Sale (POS) | Đơn DV/SP → Thanh toán | SCR-04 (chip KM2) | Đơn được thanh toán một phần bằng KM2, FIFO trừ lot |
| Staff/Manager (CRM/POS) | Profile khách → card Ví KM 2 | SCR-06 → SCR-07 (nếu hoàn) | Tư vấn khách dùng trước hạn / tạo yêu cầu hoàn ví KM2 |
| Kế toán / Manager (Admin/POS) | Menu Yêu cầu hoàn tiền | SCR-07 list (filter loại "Hoàn ví KM2") | Duyệt + thanh toán hoàn ví KM2 |
| Manager / Admin | Menu Report (Phase 3, sau PD-001) | SCR-08 | Theo dõi hiệu quả KPI Ví KM 2 |
B1.3) Bản đồ phụ thuộc màn hình
| Màn trước | Mở khoá / ảnh hưởng | Màn sau | Điều kiện |
|---|---|---|---|
| SCR-01 Cài đặt KM2 | wallet_km2_config.disabled=false, max_percent_per_order, refund policy | SCR-03, SCR-04, SCR-06, SCR-07 | Chỉ bật ở go-live sau khi E1-E4 pass |
| SCR-02 Loại Gói Ví KM2 | prepaid_card.wallet_target='VND_PROMOTION_2', expiry_months>0, disabled=false | SCR-03 Bán Gói Ví KM2 | Có ≥ 1 Gói Ví KM2 active |
| SCR-05 Flag SP/DV | allow_promo_wallet_2=true | SCR-04 Thanh toán | Item phải eligible mới tính vào eligible_total |
| SCR-03 Bán Gói Ví KM2 | Tạo wallet_km2_lot ngay lần trả đầu với status='active'; mỗi lần trả tiếp credited_amount tăng, balance tăng theo tỉ lệ đã trả | SCR-04, SCR-06, SCR-08 | Đơn đã thanh toán theo rule multi-payment |
| SCR-06 Profile KM2 | Chọn lot eligible (active + allow_refund=true + còn balance) | SCR-07 Hoàn ví KM2 | Quyền refund_request_management_submenu:create |
B1.4) Ma trận liên kết màn hình
| Điểm xuất phát | Deeplink / route | Đường quay lại | Ghi chú |
|---|---|---|---|
| SCR-01 readiness thiếu Gói Ví KM2 | /settings/prepaid-card/create?wallet_target=VND_PROMOTION_2 | Quay lại /s/internal-settings/promotion-wallet | Mở form tạo Gói Ví KM2 với wallet_target pre-fill |
| SCR-01 readiness thiếu item eligible | /ecommerce/product/:id hoặc /ecommerce/service/:id | Quay lại settings KM2 (browser back hoặc breadcrumb) | Day-1 không có bulk enable; user tự bật từng item |
| SCR-06 dòng Gói Ví KM2 → Hoàn ví KM2 | Dialog SCR-07 (overlay trên SCR-06) | Đóng dialog → reload tab Ví KM 2 | Không tạo route mới; dùng dialog overlay |
| SCR-06 dòng Gói Ví KM2 → Lịch sử deduction | Drawer/popup expand row | Đóng drawer | Không rời route customer detail |
| Yêu cầu hoàn tiền (list) → Chi tiết KM2 | Click row trong list → modal/route detail hiện có với behavior refund_km2_wallet | Back list | Reuse màn detail request hiện có |
| SCR-08 → customer detail | /user/customer/:id | Browser back / breadcrumb | Áp dụng sau khi PD-001 chốt route report |
B1.5) Điều hướng setup (Setup Navigator)
| Bước | UI hiển thị | Kiểm tra sẵn sàng | Hành động chính | Khi chưa đạt |
|---|---|---|---|---|
| 1 | SCR-01 Cài đặt KM2 — banner readiness | wallet_km2_config tồn tại, mặc định disabled=true sau migration | Admin tích vào checklist 4 bước, bấm Bật cuối cùng | Nút Bật chip danger "Hoàn tất 4 bước trước" |
| 2 | SCR-02 Loại Gói Ví KM2 | Có ≥ 1 prepaid_card wallet_target='VND_PROMOTION_2', disabled=false | Tạo / sửa Gói Ví KM2 | Banner "Chưa có Gói Ví KM2 nào" + CTA Tạo Gói Ví KM2 |
| 3 | SCR-05 Eligible item | Có ≥ 1 product / service allow_promo_wallet_2=true | Bật flag ở chi tiết SP/DV | Banner "Chưa có SP/DV cho phép Ví KM 2" + CTA Cấu hình SP/DV |
| 4 | SCR-03/SCR-04 POS runtime preview | Khách có balance KM2 (đã bán test) + đơn có item eligible | Bán test 1 đơn nội bộ | Hiện sample preview "Khách trả 500k → vào ví 5tr; đơn 1tr eligible 700k → KM2 áp dụng tối đa 140k" |
Self-serve ergonomics: Day-1 chưa có preset/copy config giữa branch (deferred). Bulk enable SP/DV chưa có (deferred). Admin tự enable từng item.
B1.6) Ma trận Dynamic Permission v2 và refresh quyền
| Khu vực UI | Module | Portal | Action | Default seed | Hành vi UI khi thiếu quyền | Sau khi grant/revoke |
|---|---|---|---|---|---|---|
| Cài đặt KM2 (SCR-01) | internal_configuration | admin | update | Admin / owner cấu hình hệ thống | Ẩn menu trong sidebar Settings; route guard redirect; KHÔNG disable nút Lưu | Refetch permission state hoặc relogin (theo cơ chế Dynamic Permission hiện có) |
| Tạo / sửa Gói Ví KM2 (SCR-02) | internal_configuration | admin | update | Admin / owner cấu hình hệ thống | Ẩn CTA Tạo mới / Sửa; route guard | Tương tự — refetch sau khi đổi quyền |
| Bán Gói Ví KM2 (SCR-03) | prepaid_order | pos | create, payment | POS/Sale/Manager theo portal POS | Ẩn CTA Tạo Mới / Hoàn thành; backend chặn mutation | Refresh route + payment store |
| Thanh toán DV bằng KM2 (SCR-04) | service_order | pos | payment | POS/Sale/Manager theo portal POS | Ẩn chip Ví KM 2; backend trả permission error nếu gọi trực tiếp | Refetch permission + reload order payment state |
| Thanh toán SP/mỹ phẩm bằng KM2 (SCR-04) | product_order | pos | payment | POS/Sale/Manager theo portal POS | Ẩn chip Ví KM 2; backend chặn | Tương tự |
| Toggle eligible flag SP/DV (SCR-05) | internal_configuration | admin | update | Admin | POS portal: ẩn action update; toggle readonly | Refetch sau grant |
| Xem tab KM2 customer profile (SCR-06) | customer_management | admin,pos,crm | access, view_all (cho all-branch) | Theo quyền customer hiện có | Ẩn card + tab; action get_customer_km2_lots trả no-leak (forbidden) | Refetch profile + lot action |
| CTA Hoàn ví KM2 (SCR-06) | refund_request_management_submenu | admin,pos | access, create | Kế toán/Manager/Admin theo reviewer config | Ẩn nút Hoàn ví KM2; backend chặn mutation transaction_request_insert cho behavior refund_km2_wallet | Refresh permission + reload tab |
| Cập nhật / duyệt / thanh toán Hoàn ví KM2 (SCR-07) | refund_request_management_submenu | admin,pos | update, approve, payment | Reviewer config + kế toán | Action duyệt/thanh toán ẩn ở list/detail; backend từ chối changeStatusTransaction | Refresh request list + permission |
| Report KM2 (SCR-08) | Module/route theo PD-001 | admin | access | Manager/Admin Phase 3 | Ẩn menu route, ẩn nút export | Refetch route permission |
Cross-branch usage không = cross-branch visibility: khách mua ở chi nhánh A có thể dùng KM2 ở chi nhánh B (engine xử lý), nhưng UI chỉ trả dữ liệu theo
branch_modecủa user (Staff: self only / Manager: branch / Admin: all). Cross-branch redeem được Go actiondeduct_km2_paymentxử lý bằng service permission, không expose qua GraphQL select.
B1.7) Ma trận ảnh hưởng UI của tính năng liên quan
| Tính năng / màn hiện có | Ảnh hưởng UI | Bổ sung hiển thị / thao tác | Không đổi UI |
|---|---|---|---|
| Settings Ví khuyến mãi | Có | Thêm toggle KM2 + 2 block config (chung + refund) + readiness checklist trong shell hiện có | Không tạo route settings mới |
| Master data thẻ trả trước | Có | Thêm radio Ví đích, field Hạn sử dụng, cột "Ví đích" + "HSD" trong list | Field VND/KM1 giữ vị trí |
| POS bán Gói Ví KM2 / prepaid order | Có | Cột "NẠP VÍ KM 2" + dòng "Tiền vào Ví KM 2" conditional; chặn Trả góp cho dòng KM2 | Layout 8/4 hiện hữu giữ nguyên |
| Thanh toán đơn DV/SP | Có | Chip Ví KM 2 + block auto-fill readonly + warning khi KM1+KM2 cùng đơn | Chip cash/CK/thẻ/VND/KM1 không đổi |
| Product/Service detail | Có | Toggle Cho phép Ví KM 2 cạnh KM1, chỉ Admin sửa | Toggle KM1 giữ nhãn/hành vi |
| Customer profile | Có | Card Ví KM 2: {balance}đ + popup chi tiết lot + cảnh báo expiry + CTA "Hoàn ví KM2" | Bố cục thông tin khách giữ nguyên |
| Yêu cầu hoàn tiền | Có, bắt buộc | Filter "Loại = Hoàn ví KM2", badge "Hoàn ví KM2", dialog tạo có 3 block snapshot/calc/method | Không tạo approval module riêng |
| Hoá đơn in / preview | Có | Thêm dòng "Ví KM 2: {amount}" conditional dưới dòng "Ví khuyến mãi" | Không đổi biến/label KM1 |
| Fund / Quỹ | Có | Cột "Vào Ví KM 2" conditional cạnh cột KM1 | Cột KM1 giữ |
| Report DV/NV / employee revenue | Có | Cột/segment wallet_promotion_2_revenue conditional | Cột KM1 giữ |
| CRM customer ví | Có | Dòng "Ví KM 2: {balance}đ" conditional | Dòng KM1 giữ |
| Notification / ZNS / SMS | Có điều kiện (FR-011) | Thêm template km2_lot_activated + km2_lot_expiring_7d | Template KM1 không đổi |
| Rank / loyalty / appointment / kho / affiliate / CMS gift / HR | Không trực tiếp | Không thêm field UI; chỉ kiểm tra regression nếu màn đang hiển thị payment method/wallet amount | — |
B2.1) SCR-01: Cài đặt Ví KM 2
B2.1.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định cho UI/UX |
|---|---|
| Ai dùng? | Admin / owner cấu hình hệ thống — portal Admin, action internal_configuration:update |
| Vào màn để quyết định gì? | Bật/tắt Ví KM 2 cho POS, chốt max%, có cho dùng KM1+KM2 cùng đơn, chính sách Hoàn ví KM2 |
| Dữ liệu chính | wallet_km2_config (1 record global) + payment_method.id='wallet_promotion_2' visibility |
| CTA chính / phụ | Primary: Lưu cài đặt. Secondary: Huỷ, Bật Ví KM 2 cho POS (sau readiness pass) |
| Điều không được hiểu nhầm | "Bật Ví KM 2" không tự áp dụng cho mọi SP/DV — vẫn cần bật allow_promo_wallet_2 per item ở SCR-05 |
B2.1.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định cho UI/UX |
|---|---|
| Mục tiêu user trong 5 giây | Admin biết ngay Ví KM 2 đang Tắt (default) hay Đang hoạt động, và còn bao nhiêu bước readiness chưa pass |
| Thứ tự ưu tiên thông tin | (1) Trạng thái bật/tắt + readiness banner → (2) Cấu hình chung (max%, KM1+KM2) → (3) Chính sách Hoàn ví KM2 → (4) Footer CTA |
| Dữ liệu nhạy cảm | Không có dữ liệu khách / tiền cụ thể; chỉ config — không cần masking |
| Pending / partial data | Sau migration disabled=true không phải "0% disable", phải hiển thị "Tính năng đang tắt — Bấm Bật Ví KM 2 cho POS sau khi readiness 4 bước pass" |
| Thuật ngữ canonical | "Ví KM 2", "Hoàn ví KM2", "Phí xử lý hoàn ví", "Gia hạn refund DV" — không dùng "voucher", "Ticket KM2", "Refund ticket" |
| Thiết kế thị giác | Trạng thái đang hoạt động: intent success; Trạng thái tắt: intent muted; Cảnh báo readiness: intent warning; Phí > 0 trong tooltip refund: intent negative-value |
| Điều không được tự suy diễn | KHÔNG thêm tab / route con / màn config riêng cho từng branch (Day-1 global-first); KHÔNG cho disable nút Lưu nếu validation pass; KHÔNG để publish workflow / draft / effective date (Day-1 save-now) |
B2.1.2) Bố cục — Demo gắn vào UI hiện tại
/s/internal-settings/promotion-wallet (extend PromotionWallet.tsx)
text
┌──────────────────────────────────────────────────────────────────────────────┐
│ SETTINGS > VÍ KHUYẾN MÃI │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ KIỂM TRA SẴN SÀNG (chỉ hiện khi disabled=true HOẶC còn bước thiếu) │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Hoàn tất 4 bước để bật Ví KM 2 cho POS: │ │
│ │ ① Bật phương thức Ví KM 2 [Đạt] │ │
│ │ ② Có ≥ 1 Gói Ví KM2 đang hoạt động [Cấu hình →] [Chưa đạt] │ │
│ │ ③ Có ≥ 1 SP/DV cho phép Ví KM 2 [Cấu hình →] [Chưa đạt] │ │
│ │ ④ Bán test thành công 1 đơn POS [Chưa đạt] │ │
│ │ │ │
│ │ [ Bật Ví KM 2 cho POS ] ← chip danger nếu chưa đủ; chip success nếu đủ│ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ PHƯƠNG THỨC THANH TOÁN │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Ví khuyến mãi (KM 1) [▣] Bật │ │
│ │ Ví khuyến mãi 2 (KM 2) [□] Tắt ★ MỚI │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ CẤU HÌNH CHUNG ★ MỚI │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Tối đa % trên đơn hàng [ 20 ] % │ │
│ │ ⓘ Mỗi đơn được trả bằng KM2 tối đa X% phần eligible │ │
│ │ │ │
│ │ Cho phép dùng KM1 + KM2 cùng đơn [□] Tắt │ │
│ │ ⓘ Bật = khách dùng cả 2 ví trong 1 đơn │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ CHÍNH SÁCH HOÀN VÍ KM2 ★ MỚI │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ Cho phép Hoàn ví KM2 [▣] Bật │ │
│ │ │ │
│ │ Phí xử lý hoàn ví (% giá mua gói) [ 20 ] % │ │
│ │ Thời hạn hoàn (ngày) [ 30 ] ngày │ │
│ │ Gia hạn khi refund DV (ngày) [ 30 ] ngày │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ [ Huỷ ] [ Lưu cài đặt ] │
└──────────────────────────────────────────────────────────────────────────────┘Vùng
KEEP: card "PHƯƠNG THỨC THANH TOÁN" + nútHuỷ/Lưu cài đặtđã có. VùngNEW: banner readiness + 2 card mới + toggle KM2 + 1 nút "Bật Ví KM 2 cho POS".
B2.1.2.0) Ma trận variant bắt buộc
| Variant ID | Điều kiện kích hoạt | Ai thấy | Wireframe / contract | Field/block không render | QA ref |
|---|---|---|---|---|---|
Variant A | Default — đã pass readiness, disabled=false | Admin | Như demo trên, banner readiness ẩn (đã đủ); toggle KM2 = Bật | — | TC-SCR01-A-01 |
Variant B | disabled=true (Day-1 sau migration) | Admin | Banner readiness hiện đầy đủ 4 bước; toggle KM2 = Tắt; CTA Bật Ví KM 2 cho POS chip danger | Toggle KM2 không cho user bật trực tiếp; phải qua nút "Bật Ví KM 2 cho POS" sau readiness | TC-SCR01-B-01 |
Variant C | No permission internal_configuration:update | Staff/Manager/POS | Route guard 403 → redirect Settings home + toast "Bạn không có quyền xem trang này" | Toàn bộ trang | TC-PERM-SCR01-01 |
Variant D — Loading | Mở trang → đang fetch wallet_km2_config + prepaid_card count + eligible count | Admin | Skeleton 4 banner row + 3 card; CTA disabled | — | TC-SCR01-D-01 |
Variant D — Error | Fetch lỗi (5xx) | Admin | Banner đỏ "Không thể tải. Vui lòng thử lại" + nút Thử lại ở vị trí banner readiness | Toàn bộ form | TC-SCR01-D-02 |
Variant E | Khách đang có balance KM2 nhưng Admin tắt config | Admin | Hiển thị banner warning "Đang có {N} khách giữ tổng số dư {X}đ Ví KM 2. Tắt sẽ không xoá số dư hiện có nhưng POS sẽ ẩn phương thức Ví KM 2" + CTA Tắt/Giữ bật | — | TC-SCR01-E-01 |
Variant F | Lifecycle config "Đang chuyển" — vừa lưu config, đang refetch | Admin | Toast Đã lưu cài đặt; form vẫn cho thao tác tiếp; readiness banner refetch | — | TC-SCR01-F-01 |
B2.1.3) Phân loại reuse + điểm update
| Phân loại | File hiện có | Vị trí update | Lý do vị trí |
|---|---|---|---|
🔧 Extend | PromotionWallet.tsx (route /s/internal-settings/promotion-wallet) | Chèn banner readiness lên đầu (trước card "PHƯƠNG THỨC THANH TOÁN"); thêm dòng toggle KM2 vào card hiện có; chèn 2 card mới ngay dưới card phương thức; giữ nguyên footer Huỷ/Lưu cài đặt | Admin đã quen route này cho cấu hình ví khuyến mãi; giữ shell tránh tạo route lạ |
B2.1.4) Quy ước field / cột / CTA mới hoặc thay đổi
Trace cross-ref về B0.4 — các field SCR-01 đã có dòng đầy đủ ở B0.4.
| Field/CTA | Loại | Hiển thị ở đâu | Mặc định | Validation | Điều kiện hiển thị | Tooltip | Ref B0.4 |
|---|---|---|---|---|---|---|---|
Toggle Ví khuyến mãi 2 (KM 2) | QToggle | Trong card "PHƯƠNG THỨC THANH TOÁN" | Tắt | Boolean | Luôn | "Bật → POS thấy phương thức Ví KM 2" | wallet_km2_config.disabled |
Input Tối đa % | XInput number | Card "CẤU HÌNH CHUNG" | 20 | Integer 1-100, bắt buộc | Hiện khi card config visible | "Mỗi đơn tối đa X% phần eligible" | max_percent_per_order |
Toggle Cho phép KM1+KM2 | QToggle | Card "CẤU HÌNH CHUNG" | Tắt | Boolean | Luôn | "Bật = khách dùng cả 2 ví trong 1 đơn" | allow_combine_km1 |
Toggle Cho phép Hoàn ví KM2 | QToggle | Card "CHÍNH SÁCH HOÀN VÍ KM2" | Bật | Boolean | Luôn | "Tắt = ẩn CTA Hoàn ví KM2 toàn hệ thống" | allow_refund |
Input Phí xử lý hoàn ví | XInput number | Card "CHÍNH SÁCH HOÀN VÍ KM2" | 20 | Integer 0-100 | Hiện khi allow_refund=true | "Phí tính trên giá mua gói" | refund_fee_percent |
Input Thời hạn hoàn (ngày) | XInput number | Card "CHÍNH SÁCH HOÀN VÍ KM2" | 30 | Integer > 0 | Hiện khi allow_refund=true | "Trong N ngày kể từ ngày bán" | refund_deadline_days |
Input Gia hạn refund DV (ngày) | XInput number | Card "CHÍNH SÁCH HOÀN VÍ KM2" | 30 | Integer > 0 | Hiện khi allow_refund=true | "Gia hạn lot hết hạn khi refund DV" | refund_extend_days |
CTA Bật Ví KM 2 cho POS | QBtn (chip primary) | Cuối banner readiness | Disabled khi readiness chưa đủ; enabled khi đủ 4 bước | — | Hiện khi disabled=true | "Cần hoàn tất 4 bước trước" | — |
CTA Lưu cài đặt | QBtn primary | Footer | Disabled khi không dirty | Validation pass | Luôn | — | — |
B2.1.5) Quy ước setup / readiness
Mỗi bước mở deeplink rõ ràng + đường quay lại.
| Bước | Deeplink / vị trí | Điều kiện đạt | Khi chưa đạt |
|---|---|---|---|
| ① Bật phương thức Ví KM 2 | Toggle ngay trong cùng trang | payment_method.id='wallet_promotion_2' disabled=false | Banner readiness chip xám "Chưa đạt" + nhấn vào toggle |
| ② Có ≥ 1 Gói Ví KM2 active | [Cấu hình →] → /settings/prepaid-card?wallet_target=VND_PROMOTION_2 | COUNT(prepaid_card WHERE wallet_target='VND_PROMOTION_2' AND disabled=false) ≥ 1 | Banner chip xám + CTA mở list filtered |
| ③ Có ≥ 1 SP/DV cho phép Ví KM 2 | [Cấu hình →] → tab "Sản phẩm/Dịch vụ" của Settings + filter allow_promo_wallet_2=true | COUNT(SP/DV allow_promo_wallet_2=true) ≥ 1 | CTA mở list SP/DV để Admin tự bật |
| ④ Bán test thành công 1 đơn POS | Vào POS → Nạp Tiền → tạo đơn KM2 nội bộ | Đã có ≥ 1 lot active trong vòng 7 ngày qua | Hint "Vào POS bán test 1 đơn nội bộ" + link mở /ecommerce/prepaid-card-order/create |
B2.1.6) Quy ước tương tác (Interaction)
| Tình huống | Hành vi |
|---|---|
| Chế độ lưu | Lưu thủ công bằng nút Lưu cài đặt (không autosave) |
| Có thay đổi chưa lưu | Title hiển thị dấu •; rời trang → confirm modal "Thay đổi chưa được lưu. Bạn có muốn rời trang?" |
| Search/filter apply | N/A (form đơn) |
| Reset dây chuyền | Tắt Cho phép Hoàn ví KM2 → ẩn 3 input phí/thời hạn/gia hạn (giữ value cũ trong state, chỉ ẩn UI) |
| Cập nhật async | Pessimistic: bấm Lưu → spinner trên nút → đợi BE response → toast |
| Tác vụ chạy lâu | N/A (config save < 1s) |
B2.1.7B) Quy ước tương tác form
| Khía cạnh | Quy ước |
|---|---|
| Trigger validation | On blur cho input number; on submit luôn re-validate |
| Debounce ô tìm kiếm | N/A |
| Autosave | Không có (DEC: config save phải explicit để tránh accidental disable) |
| Manual save | Ctrl+S phím tắt = bấm Lưu cài đặt; không có "Lưu nháp" |
| Char counter | N/A (không có textarea trong scope SCR-01) |
| Max length cứng | Input number max_percent_per_order block keystroke khi > 100; toast 1 lần "Tối đa 100" |
| IME composing | N/A (chỉ input number) |
| Paste rules | Cho phép paste cho input number; auto-strip non-digit |
| Auto-format | Không format ngàn (input < 1000); không format phần trăm khi gõ |
| Required marker | Dấu * đỏ sau label cho input bắt buộc; aria-required=true |
| Inline error | Hiện ngay dưới field, icon đỏ; aria-live="assertive" |
| Field disabled vs readonly | 3 input refund: disabled (greyed) khi allow_refund=false, readonly khi user thiếu quyền |
B2.1.7C) Quy ước đồng thời (Concurrency UI)
SCR-01 ít có 2 user cùng sửa, nhưng vẫn cần guard.
| Tình huống | Hành vi |
|---|---|
| 2 Admin cùng mở config | Không hiện presence badge (config ít sửa); last-write-wins |
| 2 Admin lưu cùng lúc | BE dùng updated_at optimistic concurrency: nếu B lưu sau A, B nhận 409 Conflict → modal "Cấu hình đã thay đổi từ thiết bị khác. Vui lòng tải lại." + nút Tải lại |
| Quyền bị thu hồi giữa session | Request kế tiếp 401/403 → toast "Quyền của bạn đã thay đổi. Vui lòng tải lại." + redirect Settings home |
| Heartbeat | Không cần (config trang); chỉ refetch khi user mở trang |
B2.1.7D) Khả năng chống đứt mạng (Network Resilience)
| Tình huống | Hành vi |
|---|---|
| Mất mạng giữa lúc lưu | Disable nút Lưu cài đặt, hiện "Đang chờ kết nối…"; không mất nội dung input; auto retry khi có mạng |
| Có mạng lại | Auto retry POST 1 lần; toast "Đã lưu cài đặt" hoặc "Lưu thất bại — Vui lòng thử lại" |
| Response > 5s | Spinner + "Mạng yếu, đang xử lý…" + nút Huỷ sau 10s |
| Response > 30s | Auto huỷ + thông báo "Quá lâu. Đã huỷ. Vui lòng thử lại." |
B2.1.8) Ma trận phân quyền (B2.x.8)
| Portal | Vai trò default seed | Action cần | Hiển thị | Khi thu hồi quyền |
|---|---|---|---|---|
admin | Admin / owner cấu hình | internal_configuration:update | Toàn bộ trang + nút Lưu | Ẩn menu sidebar; route guard redirect; refresh quyền cập nhật ngay |
pos / crm / staff | — | — | Không hiện menu trong Settings | API mutation backend hard-deny |
B2.1.8A) Ma trận Role × Variant
| Role / nhóm user | Portal | Permission/action | Variant UI | Field/block được thấy | Field/block không render | Khi thu hồi quyền |
|---|---|---|---|---|---|---|
Admin có internal_configuration:update | admin | update | Variant A/B/E/F | Toàn bộ form + readiness | — | Ẩn menu, route guard |
| Manager / Staff | admin/pos/crm | Không có | Variant C | — | Toàn bộ trang | N/A — không vào được |
| POS only | pos | Có thể có customer_management:access nhưng không có internal_configuration | Variant C | — | Trang Settings ẩn khỏi menu | — |
B2.1.9) Ma trận trạng thái (ref B0.5)
| Trạng thái | Hiển thị |
|---|---|
| Đang tải | Skeleton banner readiness + 3 card |
| Default — đã pass readiness, đang hoạt động | Toggle KM2 bật, banner readiness ẩn, 2 card config hiện đầy đủ |
| Default — chưa pass readiness | Banner readiness hiện 4 bước, toggle KM2 tắt, CTA "Bật Ví KM 2 cho POS" chip danger |
| Lưu thành công | Toast Đã lưu cài đặt (intent success, 3s auto-dismiss); reset dirty state |
| Lưu thất bại | Toast Không thể lưu cài đặt. Vui lòng thử lại (intent danger) + giữ input + nút Thử lại |
| Tắt KM2 khi khách còn balance | Modal warning trước khi confirm tắt, nội dung như Variant E |
Cho phép Hoàn ví KM2 = tắt | Ẩn 3 input phí/thời hạn/gia hạn |
| Không có quyền | Ẩn menu trong Settings sidebar |
| Conflict 409 (concurrency) | Modal Cấu hình đã thay đổi từ thiết bị khác + nút Tải lại |
B2.1.10) Phản hồi sau thao tác (Action Feedback)
| Hành động | Phản hồi UI | Copy mẫu | Hành động tiếp |
|---|---|---|---|
| Lưu cài đặt thành công | Toast intent success | Đã lưu cài đặt Ví KM 2 | Reset dirty, refetch readiness |
| Validation lỗi (max% < 1 hoặc > 100) | Inline error dưới field | Vui lòng nhập giá trị từ 1 đến 100 | Focus field lỗi |
| Bấm "Tắt KM2" khi có khách còn balance | Confirm modal | Hiện có {N} khách đang giữ tổng {X}đ trong Ví KM 2. Tắt sẽ ẩn phương thức cho POS, không xoá số dư. Tiếp tục? | Tiếp tục tắt / Giữ bật |
| Bấm "Bật Ví KM 2 cho POS" khi readiness chưa đủ | Modal hint | Hoàn tất 4 bước trước khi bật cho POS + danh sách bước thiếu + CTA mở deeplink | Mở deeplink theo bước |
| Conflict 409 | Modal | Cấu hình đã thay đổi từ thiết bị khác. Vui lòng tải lại trước khi lưu. | Tải lại |
| Lỗi network 5xx | Toast | Không thể lưu cài đặt. Mã sự cố: {trace_id} | Báo support |
B2.1.10A) Phân loại lỗi (Error Taxonomy)
| Loại lỗi | UI pattern | Recovery | Ví dụ copy |
|---|---|---|---|
| Validation client (max% out of range) | Inline dưới field | User sửa | Vui lòng nhập giá trị từ 1 đến 100 |
| Validation server (business rule) | Banner đỏ trên cùng | User sửa | Cần ít nhất 1 Gói Ví KM2 đang hoạt động trước khi bật. [Cấu hình →] |
| Quyền (403) | Toast + redirect | Liên hệ Admin | Bạn không có quyền cập nhật cấu hình này |
| Conflict (409) | Modal | Tải lại | Cấu hình đã thay đổi từ thiết bị khác. Vui lòng tải lại trước khi lưu. |
| Network / timeout | Banner + nút Thử lại | Retry tự động + thủ công | Không thể kết nối. Vui lòng thử lại. |
| Server 5xx | Toast + correlation_id | Báo support | Có sự cố từ hệ thống. Mã sự cố: {trace_id}. Vui lòng báo bộ phận hỗ trợ. |
B2.1.11) Mapping UI theo vòng đời config
| Trạng thái config | CTA hiển thị | CTA ẩn / khoá | Field sửa được | Badge / copy |
|---|---|---|---|---|
disabled=true (Day-1 default) | Bật Ví KM 2 cho POS (sau readiness) | Toggle KM2 không cho bật trực tiếp | Cấu hình chung + refund vẫn sửa được | Banner readiness + chip muted "Đang tắt" |
disabled=false (đang hoạt động) | Lưu cài đặt, toggle bật/tắt | — | Tất cả | Chip success "Đang hoạt động" |
disabled=false + đang có khách giữ balance | Lưu cài đặt, toggle bật/tắt + warning | — | Tất cả | Banner warning khi user định tắt |
B2.2) SCR-02: Tạo / sửa Gói Ví KM2 (mở rộng PrepaidCard)
B2.2.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định cho UI/UX |
|---|---|
| Ai dùng? | Admin / owner cấu hình hệ thống — portal Admin, action internal_configuration:update |
| Vào màn để quyết định gì? | Tạo loại Gói Ví KM2 (Silver/Gold/Diamond) với mệnh giá, giá trị vào ví, hạn sử dụng |
| Dữ liệu chính | prepaid_card (mở rộng với wallet_target + expiry_months) |
| CTA chính / phụ | Primary: Lưu. Secondary: Huỷ, Đóng |
| Điều không được hiểu nhầm | Mệnh giá thẻ = giá khách trả; Số tiền nạp vào ví = giá trị khách nhận trong Ví KM 2. Hai số này khác nhau (mệnh giá < số tiền nạp khi là Gói Ví KM2) |
B2.2.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định cho UI/UX |
|---|---|
| Mục tiêu user trong 5 giây | Admin biết đây là form thẻ trả trước; chọn Ví đích KM2 → unlock thêm field hạn sử dụng |
| Thứ tự ưu tiên thông tin | (1) Code/Tên thẻ → (2) Mệnh giá + Số tiền nạp → (3) Ví đích → (4) Hạn sử dụng (nếu KM2) → (5) Mô tả → (6) Trạng thái (sửa) |
| Dữ liệu nhạy cảm | Không có (master data) |
| Pending / partial data | Khi Ví đích = VND, ẩn field Hạn sử dụng (không phải "0 tháng") |
| Thuật ngữ canonical | "Gói Ví KM2", "Ví đích", "Hạn sử dụng" — không dùng "Wallet target", "TTL" |
| Thiết kế thị giác | Chip badge "KM 2" intent primary emphasis; Chip "VND" intent muted |
| Điều không được tự suy diễn | KHÔNG cho sửa Ví đích sau khi đã có lot bán ra (FE disabled + tooltip lý do); KHÔNG cho sửa mệnh giá/số tiền nạp khi đã có lot |
B2.2.2) Bố cục — Demo gắn vào UI hiện tại
/settings/prepaid-card/create (extend PrepaidCardForm.tsx — dialog slide-right)
text
┌─────────────────────────────────────────────────────────────────────┐
│ TẠO THẺ TRẢ TRƯỚC [ × Đóng ] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─── Cột trái ────────────┐ ┌─── Cột phải ────────────┐ │
│ │ │ │ │ │
│ │ Mã thẻ * │ │ Mệnh giá thẻ * │ │
│ │ [ VE_GOLD ] │ │ [ 500.000 ] đồng │ │
│ │ │ │ │ │
│ │ Tên thẻ * │ │ Số tiền nạp vào ví * │ │
│ │ [ Gói Gold 500K ] │ │ [ 5.000.000 ] đồng │ │
│ │ │ │ │ │
│ │ Mô tả │ │ Trạng thái (chỉ khi sửa)│ │
│ │ [ Mua 500K được 5tr ] │ │ ( ) Đang hoạt động │ │
│ │ [ dùng dịch vụ spa ] │ │ ( ) Ngưng hoạt động │ │
│ │ │ │ │ │
│ │ ★ Ví đích * │ │ ★ Hạn sử dụng (tháng) * │ │
│ │ ( ) Ví VND (mặc định) │ │ [ 6 ] tháng │ │
│ │ (•) Ví KM 2 │ │ (chỉ hiện khi chọn KM 2)│ │
│ │ │ │ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ [ Huỷ ] [ Lưu ] │
└─────────────────────────────────────────────────────────────────────┘Vùng
KEEP: Mã thẻ / Tên thẻ / Mô tả / Mệnh giá / Số tiền nạp / Trạng thái. VùngNEW: Radio "Ví đích" + Input "Hạn sử dụng" (conditional).
Demo gắn vào danh sách hiện có (/settings/prepaid-card):
text
┌────┬──────────┬──────────────────┬──────────┬───────────┬────────────┬─────┬──────────┐
│STT │ Mã thẻ │ Tên thẻ │ Mệnh giá │ Nạp vào ví│ Ví đích ★ │ HSD★│ Trạng thái│
├────┼──────────┼──────────────────┼──────────┼───────────┼────────────┼─────┼──────────┤
│ 1 │ VE_GOLD │ Gói Gold 500K │ 500.000đ │ 5.000.000đ│ [KM 2] │ 6 th│ Đang HĐ │
│ 2 │ VE_SILVER│ Gói Silver 200K │ 200.000đ │ 1.500.000đ│ [KM 2] │ 3 th│ Đang HĐ │
│ 3 │ TT_10M │ Thẻ trả trước 10tr│10.000.000│10.000.000đ│ [VND] │ — │ Đang HĐ │
└────┴──────────┴──────────────────┴──────────┴───────────┴────────────┴─────┴──────────┘B2.2.2.0) Ma trận variant bắt buộc
| Variant ID | Điều kiện | Ai thấy | Wireframe / contract | Field/block không render | QA ref |
|---|---|---|---|---|---|
Variant A — Tạo VND | Chọn Ví đích=VND | Admin | Form như demo, ẩn "Hạn sử dụng" | Hạn sử dụng | TC-SCR02-A-01 |
Variant B — Tạo KM2 | Chọn Ví đích=Ví KM 2 | Admin | Form như demo, hiện "Hạn sử dụng" với default 6 | — | TC-SCR02-B-01 |
Variant C — Sửa khi chưa có lot | Mở record sửa, chưa có lot bán ra | Admin | Form như demo + radio Trạng thái | — | TC-SCR02-C-01 |
Variant D — Sửa khi đã có lot | Mở record sửa, đã có ≥ 1 lot active hoặc historical | Admin | Mệnh giá / Số tiền nạp / Ví đích / Hạn sử dụng → readonly + tooltip "Đã có lot bán ra; chỉ sửa Tên / Mô tả / Trạng thái" | — | TC-SCR02-D-01 |
Variant E — Loading | Đang fetch detail | Admin | Skeleton form | — | TC-SCR02-E-01 |
Variant F — Mã trùng | Submit với code đã tồn tại | Admin | Inline error trên field Mã thẻ "Mã thẻ này đã tồn tại" + banner đỏ trên cùng | — | TC-SCR02-F-01 |
Variant G — No permission | Staff/POS | — | Ẩn CTA Tạo mới/Sửa ở list, route guard cho dialog | Toàn bộ form | TC-PERM-SCR02-01 |
B2.2.3) Phân loại reuse + điểm update
| Phân loại | File hiện có | Vị trí update | Lý do vị trí |
|---|---|---|---|
🔧 Extend | PrepaidCardForm.tsx | Chèn radio Ví đích vào cột trái (dưới Mô tả); chèn input Hạn sử dụng vào cột phải (dưới Số tiền nạp); list table thêm 2 cột "Ví đích" + "HSD" sau cột "Nạp vào ví" | Pattern 2 cột hiện hữu, không vẽ form mới |
B2.2.4) Quy ước field / cột / CTA
| Field/CTA | Loại | Hiển thị ở đâu | Mặc định | Validation | Điều kiện hiển thị | Tooltip | Ref B0.4 |
|---|---|---|---|---|---|---|---|
Radio Ví đích | XOptionGroup | Cột trái form | VND | Bắt buộc; enum VND / VND_PROMOTION_2 | Luôn (form) | "Chọn KM 2 để biến thẻ này thành Gói Ví KM2" | prepaid_card.wallet_target |
Input Hạn sử dụng (tháng) | XInput number | Cột phải form | 6 | Integer > 0; bắt buộc khi wallet_target='VND_PROMOTION_2' | Hiện khi wallet_target='VND_PROMOTION_2' | "Số tháng từ ngày khách trả lần đầu" | prepaid_card.expiry_months |
Cột Ví đích (list) | Badge text | Bảng /settings/prepaid-card | — | — | Luôn | — | prepaid_card.wallet_target |
Cột HSD (list) | Number text | Bảng /settings/prepaid-card | — | — | Hiện giá trị tháng nếu KM2; — nếu VND | — | prepaid_card.expiry_months |
B2.2.5) Filter (list /settings/prepaid-card)
| # | Component | Loại | Mặc định | Hành vi | Reset dây chuyền |
|---|---|---|---|---|---|
| 1 | Tìm kiếm | XInput search | Rỗng | Debounce 300ms, fuzzy code/name | — |
| 2 | Trạng thái | QSelect | Tất cả | Options: Tất cả / Đang hoạt động / Ngưng hoạt động | — |
| 3 | Ví đích ★ MỚI | QSelect | Tất cả | Options: Tất cả / Ví VND / Ví KM 2 | — |
B2.2.6) Bảng danh sách
| Cột | Width | Căn lề | Định dạng | Sticky | Sortable |
|---|---|---|---|---|---|
| STT | 50px | Center | Auto | No | No |
| Mã thẻ | 110px | Left | Text | No | Có |
| Tên thẻ | 180px | Left | Text | No | Có |
| Mệnh giá | 120px | Right | Currency | No | Có |
| Nạp vào ví | 130px | Right | Currency | No | Có |
| Ví đích ★ | 100px | Center | Badge KM 2 (primary) / VND (muted) | No | Có |
| HSD ★ | 80px | Center | N tháng hoặc — | No | Có |
| Trạng thái | 100px | Center | Badge | No | Có |
| Action | 80px | Center | Sửa / Ngưng | No | No |
B2.2.7) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Chế độ lưu | Lưu thủ công bằng Lưu |
| Có thay đổi chưa lưu | Đóng dialog → confirm "Thay đổi chưa được lưu. Tiếp tục đóng?" |
| Đổi Ví đích từ VND → KM 2 | Hiện ngay field Hạn sử dụng với default 6, focus vào field |
| Đổi Ví đích từ KM 2 → VND | Ẩn field Hạn sử dụng (giữ value cũ trong state, BE bỏ qua khi VND) |
| Mã thẻ trùng | Inline error + banner đỏ trên cùng |
| Submit → 409 (concurrency) | Modal "Thẻ này vừa được sửa từ thiết bị khác. Vui lòng tải lại." |
| Sửa record đã có lot | 4 field readonly (Mệnh giá / Số tiền nạp / Ví đích / Hạn sử dụng) + tooltip lý do |
B2.2.7B) Quy ước tương tác form
| Khía cạnh | Quy ước |
|---|---|
| Trigger validation | On blur (input number, code uniqueness async); on submit re-validate |
| Debounce code uniqueness check | 500ms |
| Required marker | * đỏ sau label |
| Auto-format tiền | Format 1.250.000 khi blur, raw khi gõ |
| IME composing | Bắt event compositionstart/end cho field Tên thẻ / Mô tả |
| Paste rules | Cho phép paste; auto-strip non-digit cho input number |
B2.2.7C) Quy ước đồng thời
| Tình huống | Hành vi |
|---|---|
| 2 Admin cùng sửa 1 thẻ | BE optimistic concurrency; B nhận 409 → modal "Đã thay đổi từ thiết bị khác" + nút Tải lại |
| Admin sửa thẻ đang có lot | FE readonly các field bị khoá; tooltip giải thích |
B2.2.7D) Network resilience
| Tình huống | Hành vi |
|---|---|
| Mất mạng giữa lúc lưu | Disable nút Lưu, hiện "Đang chờ kết nối…"; auto retry 1 lần |
| Response > 5s | Spinner + nút Huỷ sau 10s |
B2.2.8) Ma trận phân quyền
| Portal | Vai trò | Action | Hiển thị | Khi thu hồi quyền |
|---|---|---|---|---|
admin | Admin / owner | internal_configuration:update | List + form tạo/sửa | Ẩn CTA Tạo mới / Sửa |
pos / crm / staff | — | Không có | List read-only nếu có internal_configuration:access, ẩn nút sửa | Ẩn route nếu thiếu access |
B2.2.8A) Ma trận Role × Variant
| Role | Portal | Permission | Variant UI | Field thấy | Field không render |
|---|---|---|---|---|---|
Admin có internal_configuration:update | admin | update | Variant A/B/C/D | Toàn bộ | — |
User chỉ có internal_configuration:access | admin | access | List read-only | List | Form (không mở được) |
| POS/Staff | — | — | Variant G | — | Toàn bộ |
B2.2.9) Ma trận trạng thái
| Trạng thái | Hiển thị |
|---|---|
| Đang tải form | Skeleton 4 ô input |
| Tạo mới — chưa nhập | Form rỗng, default Ví đích=VND, ẩn Hạn sử dụng |
| Tạo mới — chọn KM 2 | Hiện Hạn sử dụng default 6 |
| Sửa — chưa có lot | Toàn bộ field editable |
| Sửa — đã có lot | 4 field readonly + tooltip |
| Mã thẻ trùng | Inline error |
| Số tiền nạp < Mệnh giá | Inline error "Số tiền nạp phải lớn hơn hoặc bằng mệnh giá thẻ" |
| Lưu thành công | Đóng dialog, reload list, toast Đã lưu Gói Ví KM2 {tên} |
| Conflict 409 | Modal Tải lại |
B2.2.10) Phản hồi sau thao tác
| Hành động | Phản hồi UI | Copy mẫu | Hành động tiếp |
|---|---|---|---|
| Lưu tạo mới thành công | Toast success | Đã tạo Gói Ví KM2 "{tên}" | Đóng dialog, reload list |
| Lưu sửa thành công | Toast success | Đã cập nhật Gói "{tên}" | Reload list |
| Validation lỗi | Inline + banner | (theo từng field) | Focus field lỗi |
| Mã thẻ trùng | Inline + banner | Mã thẻ "{code}" đã tồn tại. Vui lòng dùng mã khác. | Focus field Mã thẻ |
| Confirm Ngưng hoạt động | Modal | Ngưng "{tên}" sẽ ẩn khỏi POS bán Gói Ví KM2. Số dư khách hiện có không bị ảnh hưởng. Tiếp tục? | Ngưng / Huỷ |
B2.2.10A) Error Taxonomy
| Loại lỗi | UI pattern | Recovery | Copy |
|---|---|---|---|
| Validation client | Inline | User sửa | (xem B2.2.9) |
| Validation server (mã trùng) | Inline + banner | User đổi mã | Mã thẻ "{code}" đã tồn tại |
| Quyền 403 | Toast + redirect | Liên hệ Admin | Bạn không có quyền chỉnh sửa Gói Ví KM2 |
| Conflict 409 | Modal | Tải lại | Thẻ vừa được sửa từ thiết bị khác |
| Network/timeout | Toast retry | Retry | Không thể kết nối. Vui lòng thử lại. |
B2.2.11) Mapping UI theo lifecycle Gói
| Trạng thái | CTA hiện | CTA ẩn | Field sửa được | Badge |
|---|---|---|---|---|
disabled=false, chưa có lot | Sửa, Ngưng | — | Tất cả | "Đang hoạt động" |
disabled=false, đã có lot | Sửa (giới hạn), Ngưng | — | Chỉ Tên / Mô tả / Trạng thái | "Đang hoạt động" |
disabled=true | Bật lại, Sửa | Ngưng | Tất cả nếu chưa có lot, hạn chế nếu có | "Ngưng hoạt động" |
B2.3) SCR-03: Bán Gói Ví KM2 (extend đơn nạp tiền)
B2.3.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Staff / Sale (POS) — prepaid_order:create + prepaid_order:payment |
| Vào màn để quyết định gì? | Bán Gói Ví KM2 (đơn lẻ hoặc mix với thẻ trả trước VND) cho 1 khách, thu tiền và kích hoạt lot |
| Dữ liệu chính | prepaid_order + tạo wallet_km2_lot (status active ngay lần trả đầu, không tạo lot nếu khách chưa trả đồng nào) |
| CTA chính / phụ | Primary: Tạo Mới. Secondary: + Thêm thẻ nạp, Huỷ |
| Điều không được hiểu nhầm | Khách trả 500k mua Gói Gold KM2 → thực thu 500k, KHÔNG phải 5tr; ví khách +5tr (trong KM 2, không phải VND) |
B2.3.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | NV biết tổng số tiền khách phải trả, tổng tiền vào từng ví, có còn nợ không |
| Thứ tự ưu tiên thông tin | (1) Khách + chi nhánh → (2) Bảng thẻ nạp → (3) Summary tiền vào ví (KM 2 conditional) → (4) Phương thức thanh toán → (5) CTA Tạo Mới |
| Dữ liệu nhạy cảm | Tên khách + số dư hiện có — render least-data, chỉ thấy khách trong scope NV |
| Pending / partial data | Đơn chưa trả đồng nào → KHÔNG tạo lot, hiện badge "Đơn còn nợ" trên prepaid_order (không phải lot); đơn đã trả ≥ 1 đồng → tạo lot active với credited_amount = phần đã trả tương ứng, balance tỉ lệ |
| Thuật ngữ canonical | "Tiền vào Ví KM 2", "Cần TT", "Còn nợ", "Tiền thừa trả khách" — không "Top-up KM2" |
| Thiết kế thị giác | Cột "NẠP VÍ KM 2" primary emphasis khi conditional; dòng summary "Tiền vào ví KM 2" intent primary chỉ khi > 0 |
| Điều không được tự suy diễn | KHÔNG đổi layout 8/4 hiện hữu; KHÔNG cho chọn Trả góp khi có dòng KM2; KHÔNG tự ẩn cột DIVA/KM khi có dòng KM2 (cột vẫn hiện, chỉ giá trị cho dòng đó = 0) |
B2.3.2) Bố cục — Demo gắn vào UI hiện tại
/ecommerce/prepaid-card-order/create (extend PrepaidOrderForm.tsx — layout 8/4)
text
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ Nạp Tiền > Tạo Mới Nạp Tiền POS PHÒNG IT │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ ┌─── Panel trái (8) ─────────────────────────┐ ┌─── Panel phải (4) ──────────────────┐│
│ │ │ │ ││
│ │ Chi nhánh [ Chi Nhánh IT DIVA v ] │ │ Khách hàng ││
│ │ Mã giới thiệu [____________] │ │ ┌────────────────────┐ ││
│ │ Lịch hẹn [____________ v ] │ │ │ [👤] Nguyễn Thị Lan │ ││
│ │ Nguồn ĐH [_v] Chiến dịch [_v] │ │ │ 0909 123 456 │ ││
│ │ Ghi chú [______________________] │ │ └────────────────────┘ ││
│ │ │ │ ││
│ │ THÔNG TIN NẠP TIỀN │ │ PHƯƠNG THỨC THANH TOÁN * ││
│ │ ┌───────────────────────┬──┬────────┬──────│ │ [Chuyển khoản] [Thẻ] ││
│ │ │ LOẠI THẺ NẠP │SL│CẦN TT │NẠP DI│ │ [Tiền mặt ✓] [Trả góp ✕] ││
│ │ ├───────────────────────┼──┼────────┼──────│ │ ││
│ │ │ Thẻ trả trước 10tr │1 │10.000K │10.000│ │ ⓘ Trả góp bị chặn vì đơn có Gói ││
│ │ │ Gói Gold (6 tháng) ★ │2 │ 1.000K │ 0 │ │ Ví KM2 ││
│ │ ├───────────────────────┼──┼────────┼──────│ │ ││
│ │ │ TỔNG │ │11.000K │10.000│ │ Số tiền khách gửi [ 0 ] đ ││
│ │ └───────────────────────┴──┴────────┴──────│ │ ││
│ │ (cột tiếp theo: NẠP KM, NẠP KM 2 ★ ) │ │ Tiền vào ví DIVA: 10.000.000đ ││
│ │ │ │ Tiền vào ví KM: 1.500.000đ ││
│ │ [+ Thêm thẻ nạp] │ │ Tiền vào Ví KM 2: 10.000.000đ ★ ││
│ │ │ │ ───────────────────────────────── ││
│ │ PHÂN BỔ HOA HỒNG │ │ Số tiền cần TT: 11.000.000đ ││
│ │ ┌──────────────┬───────────┬──────┐ │ │ Đã thanh toán: 0đ ││
│ │ │ LOẠI THẺ │ SỐ TIỀN TT│ HOA H│ │ │ Còn nợ: 11.000.000đ ││
│ │ ├──────────────┼───────────┼──────┤ │ │ Tiền thừa trả khách: 0đ ││
│ │ │ Thẻ 10tr │ 0 đ │ [+] │ │ │ ││
│ │ │ Gói Gold │ 0 đ │ [+] │ │ │ [ Huỷ ] [ Tạo Mới ] ││
│ │ └──────────────┴───────────┴──────┘ │ │ ││
│ └────────────────────────────────────────────┘ └────────────────────────────────────┘│
└────────────────────────────────────────────────────────────────────────────────────────┘
★ Cột "NẠP VÍ KM 2" + dòng summary "Tiền vào Ví KM 2" + chip cảnh báo "Trả góp ✕" chỉ
hiện khi đơn có ≥ 1 dòng `prepaid_card.wallet_target = VND_PROMOTION_2`Vùng
KEEP: layout 8/4, panel trái (general info, bảng thẻ, hoa hồng), panel phải (avatar khách, phương thức thanh toán, summary tiền). VùngUPDATE: bảng thông tin nạp tiền + summary tiền vào ví + chip "Trả góp" disabled. VùngNEW: cột "NẠP VÍ KM 2" + dòng summary + tooltip chặn trả góp.
B2.3.2.0) Ma trận variant bắt buộc
| Variant ID | Điều kiện | Ai thấy | Wireframe / contract | Field/block không render | QA ref |
|---|---|---|---|---|---|
Variant A — Chỉ thẻ VND | Đơn không có dòng KM2 | Staff/Sale | UI hiện hữu, không hiện cột "NẠP VÍ KM 2", không hiện dòng summary KM 2, "Trả góp" enabled | Cột KM2, dòng summary KM 2 | TC-SCR03-A-01 |
Variant B — Có dòng Gói Ví KM2 | ≥ 1 dòng wallet_target=VND_PROMOTION_2 | Staff/Sale | Hiện cột "NẠP VÍ KM 2", dòng summary, chip "Trả góp" disabled với tooltip | — | TC-SCR03-B-01 |
Variant C — Mix | Mix dòng VND + dòng KM2 | Staff/Sale | Cùng Variant B; dòng VND có cột KM 2 = 0; dòng KM2 có cột DIVA + KM = 0 | — | TC-SCR03-C-01 |
Variant D — Loading | Đang fetch danh sách thẻ | Staff/Sale | Skeleton 1 dòng bảng, skeleton summary | — | TC-SCR03-D-01 |
Variant E — Empty | Chưa thêm dòng nào | Staff/Sale | Bảng rỗng + nút + Thêm thẻ nạp; CTA Tạo Mới disabled | Summary chưa có dữ liệu (= 0) | TC-SCR03-E-01 |
Variant F — Multi-payment popup | NV bấm "Thanh toán nhiều phương thức" | Staff/Sale | Popup PrepaidOrderPaymentFormMultiple mở method wallet_promotion_2; chặn installment cho dòng KM2 | — | TC-SCR03-F-01 |
Variant G — Race condition | Submit 2 lần liên tục (double click) | Staff/Sale | Nút Tạo Mới disabled sau click đầu (idempotency key); toast "Đang xử lý..." | — | TC-SCR03-G-01 |
Variant H — No permission | NV không có prepaid_order:create | Staff khác | Route guard, redirect | Toàn bộ trang | TC-PERM-SCR03-01 |
B2.3.3) Phân loại reuse
| Phân loại | File hiện có | Vị trí update | Lý do |
|---|---|---|---|
🔧 Extend | PrepaidOrderForm.tsx + PrepaidOrderFormPaidInformation.tsx + PrepaidOrderFormPayment.tsx + PrepaidOrderPaymentFormMultiple.tsx | Mở rộng cột bảng + summary + popup multi-payment | Layout 8/4 hiện hữu chuẩn POS, không tạo flow rời |
B2.3.4) Quy ước cột bảng / field
Bảng "THÔNG TIN NẠP TIỀN" (panel trái):
| Cột | Width | Align | Format | Điều kiện hiện | Tooltip |
|---|---|---|---|---|---|
| LOẠI THẺ NẠP | 220px | Left | Dropdown prepaid_card_view | Luôn | "Chọn từ danh sách Gói/Thẻ Admin đã tạo" |
| SỐ LƯỢNG | 80px | Center | Stepper [-] N [+] | Luôn | — |
| CẦN THANH TOÁN | 130px | Right | Currency | Luôn | "Số tiền khách phải trả cho dòng này = Mệnh giá × SL" |
| NẠP VÍ DIVA | 130px | Right | Currency | Luôn (= 0 nếu dòng KM2) | "Số tiền vào ví VND của khách" |
| NẠP VÍ KM | 130px | Right | Currency | Luôn (= 0 nếu dòng KM2) | "Số tiền vào ví KM 1 (KM cũ)" |
| NẠP VÍ KM 2 ★ | 130px | Right | Currency | Chỉ hiện khi có ≥ 1 dòng KM2 | "Số tiền vào Ví KM 2 cho khách (lot mới)" |
| GHI CHÚ | 200px | Left | Textarea inline | Luôn | — |
| ACTION | 60px | Center | Xoá dòng | Luôn | — |
Bảng "PHÂN BỔ HOA HỒNG":
| Cột | Width | Align | Format | Ghi chú |
|---|---|---|---|---|
| LOẠI THẺ | 200px | Left | Text rút gọn | Reuse |
| SỐ TIỀN TT | 120px | Right | Currency input | Reuse — hoa hồng tính trên giá mua gói (Cần TT) |
| HOA HỒNG | 60px | Center | Nút [+] | Reuse |
Summary panel phải (sticky):
| Dòng | Format | Điều kiện hiện |
|---|---|---|
| Tiền vào ví DIVA | Currency | Luôn |
| Tiền vào ví KM | Currency | Luôn |
| Tiền vào Ví KM 2 ★ | Currency | Chỉ khi tổng > 0 |
| Số tiền cần TT | Currency | Luôn |
| Đã thanh toán | Currency | Luôn |
| Còn nợ | Currency, intent negative-value nếu > 0 | Luôn |
| Tiền thừa trả khách | Currency | Hiện khi khách trả dư |
B2.3.5) Filter
N/A (form tạo, không có filter).
B2.3.6) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Chọn loại thẻ KM2 trong dropdown | Cột "NẠP VÍ KM 2" auto hiện trên toàn bảng; dòng KM2 cột DIVA + KM = 0 |
| Xoá dòng KM2 cuối cùng | Cột "NẠP VÍ KM 2" ẩn, dòng summary "Tiền vào Ví KM 2" ẩn, chip "Trả góp" enabled trở lại |
| Đổi SL dòng KM2 | "CẦN TT" + "NẠP VÍ KM 2" tự cập nhật theo mệnh giá × SL và số tiền nạp × SL |
| Bấm chip "Trả góp" khi có dòng KM2 | Chip disabled với tooltip "Trả góp không áp dụng cho đơn có Gói Ví KM2" — ngăn click |
Bấm Tạo Mới 2 lần liên tục | Nút disabled sau click đầu, idempotency key gửi BE; toast nếu đã tạo |
| Mở popup multi-payment | Hiện thêm method "Ví KM 2" cho phần điều kiện cho phép; vẫn ẩn installment |
B2.3.7B) Form Interaction Deep
| Khía cạnh | Quy ước |
|---|---|
| Trigger validation | On blur cho input số; on submit re-validate |
| Debounce search khách | 300ms (autocomplete) |
| Autosave | Không có (tạo đơn explicit) |
| Char counter | Cell "GHI CHÚ" max 255, hiện counter {X}/255 khi > 200 |
| Max length cứng | Block keystroke khi đạt 255 |
| IME composing | Bắt event cho field Ghi chú |
| Required marker | * đỏ sau "PHƯƠNG THỨC THANH TOÁN" |
| Auto-format tiền | Format 1.250.000 khi blur |
B2.3.7C) Concurrency
| Tình huống | Hành vi |
|---|---|
| 2 NV cùng tạo đơn cho 1 khách | Cho phép song song; mỗi đơn là độc lập |
| Idempotency double click | Backend dùng key client_request_id (UUID gen client); duplicate → trả result đã tạo, không tạo mới |
B2.3.7D) Network resilience
| Tình huống | Hành vi |
|---|---|
Mất mạng giữa lúc bấm Tạo Mới | Disable nút, hiện "Đang chờ kết nối…"; giữ form data; auto retry với cùng client_request_id |
| Có mạng lại + đã tạo | Toast Đơn đã được tạo trước đó: {mã đơn}, redirect detail |
| Response > 5s | Spinner + nút Huỷ sau 10s |
| Response > 30s | Auto huỷ + toast lỗi |
B2.3.8) Phân quyền
| Portal | Vai trò | Action | Hiển thị | Khi thu hồi |
|---|---|---|---|---|
pos | Sale/Manager/Admin (default seed) | prepaid_order:create | Toàn bộ form | Ẩn nút Tạo Mới |
pos | Sale (thiếu payment) | prepaid_order:create only | Form vẫn hiện; nút Tạo Mới disabled với tooltip "Cần quyền thu tiền" | — |
admin/crm/staff ngoài POS | — | — | Không vào được route POS | — |
B2.3.8A) Role × Variant
| Role | Permission | Variant |
|---|---|---|
Sale POS có create+payment | create, payment | A/B/C/D/E/F/G |
Sale POS có create only | create | Form hiện, nút Tạo Mới disabled |
| Manager POS | create, payment | A-G + có thể xem all-branch (nếu branch_mode=all) |
| User ngoài POS | — | H — không vào được |
B2.3.9) Trạng thái (ref B0.5)
| Trạng thái | Hiển thị |
|---|---|
| Đang tải khách + thẻ | Skeleton avatar + skeleton bảng |
| Empty (chưa thêm dòng) | Bảng rỗng + nút + Thêm thẻ nạp highlight; nút Tạo Mới disabled |
| Đã thêm ≥ 1 dòng VND | Variant A |
| Đã thêm ≥ 1 dòng KM2 | Variant B/C — cột KM 2 hiện, dòng summary hiện |
| Tổng cần TT > tổng đã thanh toán | Còn nợ intent negative-value; cho phép submit (đơn nợ — pattern hiện hữu) |
| Submit thành công | Redirect /ecommerce/prepaid-card-order/:id + toast Đã tạo đơn nạp tiền {mã đơn} |
| Submit lỗi 5xx | Toast lỗi + giữ form + nút Thử lại |
| Race condition double click | Nút disabled, không tạo trùng |
B2.3.10) Action Feedback
| Hành động | Phản hồi | Copy |
|---|---|---|
| Tạo đơn thành công | Toast success + redirect | Đã tạo đơn nạp tiền {mã đơn}. ZNS đã gửi cho khách (nếu bật). |
| Tạo đơn nợ (chưa đủ tiền) | Toast info | Đã tạo đơn nạp tiền {mã đơn}. Đơn còn nợ {X}đ — Gói Ví KM2 chưa kích hoạt cho đến khi thu đủ. |
| Lỗi mạng | Toast retry | Không thể tạo đơn. Vui lòng thử lại. |
| Bấm "Trả góp" khi có dòng KM2 | Tooltip không cho click | Trả góp không áp dụng cho đơn có Gói Ví KM2 (DEC-017) |
Bấm Xoá dòng cuối cùng có KM2 | Cột KM 2 ẩn ngay không cần confirm | — |
B2.3.10A) Error Taxonomy
| Loại | UI | Recovery | Copy |
|---|---|---|---|
| Validation client (SL ≤ 0) | Inline | User sửa | Số lượng phải lớn hơn 0 |
| Validation server (Gói disabled) | Banner đỏ | User chọn Gói khác | Gói "{tên}" đã ngưng hoạt động. Vui lòng chọn Gói khác. |
| Quyền 403 | Toast + redirect | Liên hệ Manager | Bạn không có quyền tạo đơn nạp tiền |
| Conflict 409 | Modal | Tải lại | Đơn đã được tạo từ thiết bị khác. Vui lòng tải lại. |
| Network/timeout | Banner + retry | Auto/manual | Mạng yếu, đang xử lý... |
| 5xx | Toast + correlation_id | Báo support | Có sự cố từ hệ thống. Mã: {trace_id} |
B2.3.11) Lifecycle UI mapping (cho lot tạo từ đơn này)
| Tình huống đơn nạp | UI ở SCR-03 | UI ở SCR-06 |
|---|---|---|
| Đơn nạp tạo nhưng khách chưa trả đồng nào | Toast info "Đơn nạp đã tạo — chưa thanh toán, lot chưa tạo"; badge "Đơn còn nợ" trên dòng prepaid_order | KHÔNG có lot trong SCR-06 cho đến khi khách trả lần đầu |
| Đã trả ≥ 1 lần (kể cả từng phần) | Toast success + ZNS gửi nếu bật | Bảng SCR-06 hiện lot active, balance theo tỉ lệ đã trả (credited_amount / initial_amount) |
B2.4) SCR-04: Thanh toán đơn DV/SP bằng Ví KM 2
B2.4.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Staff/Sale POS — service_order:payment hoặc product_order:payment |
| Vào màn để quyết định gì? | Dùng KM2 để thanh toán một phần đơn DV/SP/mỹ phẩm theo max% và FIFO |
| Dữ liệu chính | Action get_customer_km2_balance cho available_amount realtime (DEC-029, KHÔNG đọc wallet.amount trực tiếp); wallet_km2_config (max_percent_per_order, allow_combine_km1); item eligible (allow_promo_wallet_2=true); BE deduct_km2_payment xử lý FIFO theo activated_at trong cùng transaction |
| CTA chính / phụ | Primary: Thanh toán. Secondary: Bỏ chọn Ví KM 2, Huỷ |
| Điều không được hiểu nhầm | Số tiền KM2 áp dụng = MIN(eligible × max%, balance) — readonly. NV không tự nhập số tiền KM2 |
B2.4.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | NV nhìn thấy: số dư KM2 khách, số tiền KM2 áp dụng (auto), số còn lại phải trả |
| Thứ tự ưu tiên | (1) Chip phương thức Ví KM 2 → (2) Block calc auto → (3) Còn lại cần trả |
| Dữ liệu nhạy cảm | Số dư KM2 khách — chỉ render khi có quyền thanh toán đơn của khách |
| Pending / partial data | Lot active nhưng credited_amount < initial_amount (đơn còn nợ): vẫn dùng được, balance chỉ tính phần đã nạp; lot expired không dùng dù scheduler chưa kịp đổi status |
| Thuật ngữ canonical | "Số dư Ví KM 2", "Số tiền KM2 áp dụng", "Còn lại cần trả" |
| Thiết kế thị giác | Số tiền KM2 áp dụng intent primary emphasis; "Còn lại cần trả" intent negative-value nếu > 0 |
| Điều không được tự suy diễn | KHÔNG cho NV nhập tự do số tiền KM2; KHÔNG hiển thị chip nếu không đủ điều kiện (config tắt / balance = 0 / eligible = 0) |
B2.4.2) Bố cục — Demo gắn vào UI hiện tại
/ecommerce/order/:id/payment (extend OrderPaymentMethodRadio.tsx + thêm OrderKm2PaymentBlock.tsx)
text
┌──────────────────────────────────────────────────────────────────────────┐
│ THANH TOÁN ĐƠN HÀNG — #DH-20260424-001 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ CHI TIẾT ĐƠN HÀNG │
│ ┌────┬────────────────────────────┬───────────┬─────┬──────────┐ │
│ │STT │ Dịch vụ / Sản phẩm │ Đơn giá │ SL │ KM 2 ? │ │
│ ├────┼────────────────────────────┼───────────┼─────┼──────────┤ │
│ │ 1 │ Massage body 90 phút │ 500.000đ │ 1 │ ✓ Có │ │
│ │ 2 │ Liệu trình trị mụn 5 buổi │ 4.500.000đ│ 1 │ ✓ Có │ │
│ │ 3 │ Kem chống nắng XYZ │ 300.000đ│ 1 │ ✕ Không │ │
│ └────┴────────────────────────────┴───────────┴─────┴──────────┘ │
│ Tổng đơn: 5.300.000đ │
│ Eligible KM 2: 5.000.000đ (Massage + Liệu trình) │
│ │
│ PHƯƠNG THỨC THANH TOÁN │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ [Tiền mặt] [Chuyển khoản] [Thẻ] [Ví VND] [Ví KM 1] [★ Ví KM 2 ✓ ] │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Khi chọn "Ví KM 2" ───────────────────────────────────────────────┐ │
│ │ │ │
│ │ Số dư Ví KM 2: 4.600.000đ │ │
│ │ Eligible total: 5.000.000đ │ │
│ │ Tối đa (20%): 1.000.000đ │ │
│ │ │ │
│ │ Số tiền KM 2 áp dụng: [ 1.000.000 ] đ (tự động tính, khoá) │ │
│ │ ⓘ MIN(Eligible × max%, Số dư) │ │
│ │ │ │
│ │ Còn lại cần trả: 4.300.000đ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ [Tiền mặt]: [ 4.300.000 ] đ │
│ │
│ [ Huỷ ] [ Thanh toán ] │
└──────────────────────────────────────────────────────────────────────────┘Vùng
KEEP: chi tiết đơn hàng + 5 chip thanh toán hiện hữu + input tiền mặt. VùngNEW: chip "Ví KM 2" + block calc bên dưới.
B2.4.2.0) Ma trận variant bắt buộc
| Variant ID | Điều kiện | Ai thấy | Wireframe / contract | Field/block không render | QA ref |
|---|---|---|---|---|---|
Variant A — Đủ điều kiện | config bật + balance > 0 + eligible > 0 | Sale POS | Chip hiện, block calc hiện khi chọn | — | TC-SCR04-A-01 |
Variant B — Eligible = 0 | Đơn không có item eligible | Sale POS | Chip ẩn (không hiển thị empty state) | Chip Ví KM 2 + block | TC-SCR04-B-01 |
Variant C — Balance = 0 | Khách không có lot active | Sale POS | Chip ẩn | Chip + block | TC-SCR04-C-01 |
Variant D — Config tắt | wallet_km2_config.disabled=true | Sale POS | Chip ẩn toàn cục | Chip + block | TC-SCR04-D-01 |
Variant E — KM1+KM2 cùng đơn | allow_combine_km1=true + khách có cả 2 ví | Sale POS | Cho chọn cả 2 chip; tính KM 2 trước, KM 1 trên phần còn lại | — | TC-SCR04-E-01 |
Variant F — KM1+KM2 không cho | allow_combine_km1=false + chọn KM 2 sau khi đã chọn KM 1 | Sale POS | Tự động bỏ chọn KM 1 + toast "Cấu hình không cho phép dùng cả 2 ví trong 1 đơn" | — | TC-SCR04-F-01 |
Variant G — Số dư < eligible × max% | balance = 50.000, eligible × max% = 100.000 | Sale POS | Auto-fill = 50.000 (dùng hết số dư) | — | TC-SCR04-G-01 |
Variant H — Loading | Đang fetch wallet stats | Sale POS | Skeleton chip + block | — | TC-SCR04-H-01 |
Variant I — Race condition | Submit khi lot vừa hết hạn | Sale POS | Toast "Số dư không đủ. Vui lòng thử lại" + reload số dư | — | TC-SCR04-I-01 |
Variant J — Partial success | Multi-payment có 1 method fail | Sale POS | Toast "Đã trừ KM2. Phần tiền mặt thất bại. Vui lòng thử lại phần còn lại." | — | TC-SCR04-J-01 |
Variant K — No permission | Sale không có *:payment | — | Chip ẩn, nút Thanh toán disabled | — | TC-PERM-SCR04-01 |
B2.4.3) Phân loại reuse
| Phân loại | File hiện có | Vị trí update | Lý do |
|---|---|---|---|
🔧 Extend | OrderPaymentMethodRadio.tsx, payment form đơn DV/SP/mỹ phẩm | Thêm chip "Ví KM 2" cùng pattern KM 1; thêm component OrderKm2PaymentBlock.tsx hiện khi chip selected | Pattern chip + block hiện hữu cho KM 1 |
B2.4.4) Quy ước field / chip / block
| Field | Loại | Hiển thị | Mặc định | Validation | Điều kiện hiện | Tooltip |
|---|---|---|---|---|---|---|
| Chip "Ví KM 2" | QChip (OrderPaymentMethodRadio) | Cùng hàng các chip khác | Chưa selected | — | config bật + balance > 0 + eligible > 0 | "Khách còn {X}đ trong Ví KM 2" |
| Số dư Ví KM 2 | Display | Block | — | — | Khi chip selected | — |
| Eligible total | Display | Block | — | — | Khi chip selected | "Tổng giá trị các SP/DV bật Cho phép Ví KM 2" |
| Tối đa (max%) | Display | Block | — | — | Khi chip selected | "Eligible × {max%} = số tiền KM2 tối đa cho đơn này" |
| Số tiền KM 2 áp dụng | Display readonly | Block | Auto-fill | Readonly | Khi chip selected | "Tự động tính = MIN(Eligible × max%, Số dư)" |
| Còn lại cần trả | Display | Block | — | — | Khi chip selected | "Tổng đơn - Số tiền KM 2 áp dụng" |
B2.4.5) Filter
N/A.
B2.4.6) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Bấm chip "Ví KM 2" | Block calc hiện ngay; auto-fill số tiền; không cho NV sửa |
| Bấm chip lần 2 (deselect) | Ẩn block, restore phương thức thanh toán theo input cash hiện tại |
Bấm chip KM 1 khi đang chọn KM 2 (config allow_combine_km1=false) | Tự bỏ chọn KM 2 + toast giải thích |
Bấm chip KM 1 khi đang chọn KM 2 (config allow_combine_km1=true) | Cả 2 chip selected; KM 2 ưu tiên tính trước |
| Item bật/tắt eligible giữa session | Chip refetch tự động khi user reload đơn; FE không cache cứng |
| Submit | Backend deduct_km2_payment action với idempotency_key; nếu thành công → trừ FIFO + ghi wallet_km2_lot_deduction |
| Race condition (lot vừa hết hạn) | BE 409 hoặc 422 → toast + reload số dư |
B2.4.7B) Form Interaction Deep
| Khía cạnh | Quy ước |
|---|---|
| Trigger validation | On submit |
| Idempotency | client_request_id UUID gen client; backend dedupe |
| Required marker | N/A |
B2.4.7C) Concurrency
| Tình huống | Hành vi |
|---|---|
| 2 NV thanh toán cùng khách | BE SELECT FOR UPDATE lot; user 2 nhận 409 → toast + reload |
| Khách dùng app cùng lúc | KM 2 không có app self-serve Day-1; không xung đột |
B2.4.7D) Network resilience
| Tình huống | Hành vi |
|---|---|
| Mất mạng giữa lúc thanh toán | Disable nút, hiện "Đang chờ kết nối…"; auto retry với cùng client_request_id; nếu BE đã trừ → idempotent return cùng kết quả |
| Response > 5s | Spinner + nút Huỷ sau 10s |
| Response > 30s | Auto huỷ + toast lỗi |
B2.4.8) Phân quyền
| Portal | Vai trò | Action | Hiển thị | Khi thu hồi |
|---|---|---|---|---|
pos | Sale/Manager/Admin | service_order:payment / product_order:payment | Chip hiện | Ẩn chip, nút Thanh toán disabled |
pos | Khác | — | Chip ẩn | — |
B2.4.8A) Role × Variant
| Role | Permission | Variant |
|---|---|---|
| Sale POS | *:payment | A-J |
| Manager POS | *:payment + view_all | Cùng + có thể xem khách all-branch |
Sale POS thiếu payment | — | K — chip ẩn |
B2.4.9) Trạng thái
| Trạng thái | Hiển thị |
|---|---|
| Đang tải wallet stats | Skeleton chip 200ms |
| Variant A | Chip + block hoạt động |
| Variant B/C/D | Chip ẩn (no-render) |
| Variant E | 2 chip selected, block KM 2 hiện trước |
| Variant F | Toast giải thích, KM 1 chip bỏ chọn |
| Variant G | Auto-fill = balance |
| Variant H | Skeleton |
| Variant I | Toast race + reload |
| Variant J | Toast partial success |
| Lỗi | Toast lỗi + nút Thử lại |
B2.4.10) Action Feedback
| Hành động | Phản hồi | Copy |
|---|---|---|
| Thanh toán thành công | Toast success | Đã thanh toán {tổng}đ. KM 2 áp dụng: {km2}đ. Số dư còn: {còn}đ. |
| Thanh toán thất bại 422 (số dư) | Toast danger + reload | Số dư Ví KM 2 không đủ. Vui lòng thử lại. |
| Thanh toán thất bại 409 (race) | Toast danger + reload | Số dư đã thay đổi từ thiết bị khác. Đã tải lại số dư mới. |
| Multi-payment partial | Toast warning | Đã trừ {km2}đ Ví KM 2. Phần tiền mặt {x}đ thất bại. Vui lòng thử lại phần còn lại. |
| Lỗi network | Toast retry | Mạng yếu, đang xử lý... |
B2.4.10A) Error Taxonomy
| Loại | UI | Recovery | Copy |
|---|---|---|---|
| Validation server (lot expired) | Toast + reload | User chọn lại | Một lot vừa hết hạn. Đã tính lại số dư. Vui lòng thử lại. |
| Validation server (eligible thay đổi) | Toast + reload | User reload đơn | SP/DV vừa thay đổi. Vui lòng tải lại đơn. |
| Quyền 403 | Toast | Liên hệ Manager | Bạn không có quyền thanh toán đơn này |
| Conflict 409 | Toast + reload | Tự động | Số dư đã thay đổi. Đã tải lại. |
| 422 (insufficient) | Toast | Reload + thử lại | Số dư Ví KM 2 không đủ |
| Network/timeout | Toast retry | Auto/manual | Không thể kết nối. Vui lòng thử lại. |
| 5xx | Toast + correlation_id | Báo support | Có sự cố từ hệ thống. Mã: {trace_id} |
B2.4.11) Lifecycle UI mapping (cho lot bị trừ)
| Trạng thái lot trước trừ | Hành vi sau trừ |
|---|---|
active, balance còn sau khi trừ | Vẫn active, balance giảm |
active, balance = 0 sau khi trừ | Chuyển exhausted |
expired | Không trừ (FIFO skip), không hiển thị trong tính toán |
refunded | Không trừ |
B2.5) SCR-05: Flag eligible per SP/DV
B2.5.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Admin / owner — internal_configuration:update |
| Vào màn để quyết định gì? | Bật/tắt allow_promo_wallet_2 cho SP/DV để quyết định item nào được trừ KM 2 |
| Dữ liệu chính | product.allow_promo_wallet_2, service.allow_promo_wallet_2 |
| CTA chính | Toggle (auto-save sau confirm) |
| Điều không được hiểu nhầm | Toggle KM 2 độc lập với toggle KM 1; bật KM 2 không tự bật KM 1 và ngược lại |
B2.5.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Admin biết item này có cho phép trừ KM 2 hay không |
| Thứ tự ưu tiên | Toggle KM 1 (hiện hữu) → Toggle KM 2 (mới) — cùng card "Trạng thái" |
| Thuật ngữ canonical | "Cho phép Ví KM 2" / "Cho phép Ví KM 1" |
| Thiết kế thị giác | Toggle bật intent success; toggle tắt intent muted |
| Điều không được tự suy diễn | KHÔNG bulk enable Day-1 (deferred); Admin tự bật từng item |
B2.5.2) Bố cục — Demo gắn vào UI hiện tại
/ecommerce/product/:id hoặc /ecommerce/service/:id (extend ProductDetail.tsx / ServiceDetailDetail.tsx)
text
┌──────────────────────────────────────────────────────────────────────┐
│ CHI TIẾT DỊCH VỤ — Massage body 90 phút │
├──────────────────────────────────────────────────────────────────────┤
│ ... │
│ │
│ ┌─── Trạng thái ──────────────────────────────────────────────────┐ │
│ │ Hiển thị trên POS [▣] Bật │ │
│ │ Cho phép Ví khuyến mãi (KM 1) [▣] Bật │ │
│ │ Cho phép Ví KM 2 ★ MỚI [□] Tắt │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ... │
└──────────────────────────────────────────────────────────────────────┘Vùng
KEEP: card Trạng thái + 2 toggle hiện có. VùngNEW: 1 dòng toggle "Cho phép Ví KM 2".
B2.5.2.0) Ma trận Biến thể
| Variant ID | Điều kiện | Hiển thị | QA ref |
|---|---|---|---|
Variant A — Toggle bật | allow_promo_wallet_2=true | Toggle xanh, label "Bật" | TC-SCR05-A-01 |
Variant B — Toggle tắt | allow_promo_wallet_2=false (default) | Toggle xám, label "Tắt" | TC-SCR05-B-01 |
Variant C — POS portal | User vào từ POS | Toggle readonly (chỉ xem) | TC-SCR05-C-01 |
Variant D — Confirm dialog | User click toggle | Modal "Bạn có chắc muốn cho phép thanh toán bằng Ví KM 2?" | TC-SCR05-D-01 |
Variant E — No permission | User không có internal_configuration:update | Toggle ẩn hoặc readonly | TC-PERM-SCR05-01 |
B2.5.3) Phân loại reuse
| Phân loại | File | Vị trí | Lý do |
|---|---|---|---|
🔧 Extend | ProductAllowPromoWalletToggle.tsx (pattern KM 1) | Thêm 1 dòng toggle dưới toggle KM 1 trong cùng component | Pattern hiện hữu chuẩn |
B2.5.4-2.5.11) Field / Permission / State / Feedback
(Reuse pattern KM 1 — chỉ thêm allow_promo_wallet_2 field. Tất cả validation / state / feedback tương tự KM 1.)
| Khía cạnh | KM 2 |
|---|---|
| Field | product.allow_promo_wallet_2 / service.allow_promo_wallet_2 (Boolean) |
| Default | false |
| Permission | internal_configuration:update, portal Admin |
| Confirm dialog | "Bạn có chắc muốn {bật/tắt} cho phép thanh toán bằng Ví KM 2?" |
| Toast success | Đã cập nhật cho phép Ví KM 2 cho "{tên item}" |
| Toast lỗi | Cập nhật thất bại. Vui lòng thử lại. |
B2.6) SCR-06: Tab Ví KM 2 trong customer profile
B2.6.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Staff/Sale/Manager/Admin — customer_management:access (theo branch_mode) |
| Vào màn để quyết định gì? | Xem tổng số dư + danh sách lot KM 2 (đang HĐ + đã hết/hoàn) + cảnh báo expiry + tạo yêu cầu Hoàn ví KM2 |
| Dữ liệu chính | Action get_customer_km2_balance cho card tổng số dư realtime (DEC-029); action get_customer_km2_lots (least-data, kiểm tra branch_mode) cho 2 bảng chi tiết; wallet_stats cho các ví khác (không phải KM 2) |
| CTA chính / phụ | Primary (per row): Hoàn ví KM2. Secondary: filter, breadcrumb back |
| Điều không được hiểu nhầm | Số dư = tổng lot.balance trạng thái active với expired_at > NOW(); không tính expired/exhausted/refunded |
B2.6.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | NV thấy: tổng số dư KM 2 + lot nào sắp hết hạn cần đề xuất khách dùng |
| Thứ tự ưu tiên | (1) Banner cảnh báo (nếu có lot ≤ 30 ngày) → (2) Tổng số dư → (3) Bảng lot active → (4) Bảng lot đã hết/hoàn |
| Dữ liệu nhạy cảm | Số dư tiền — render least-data theo customer/branch scope |
| Pending / partial data | Lot active nhưng credited_amount < initial_amount (đơn còn nợ): hiện inline label "Đã nạp X/Y" trong cột "Đã nạp"; vẫn cộng balance (phần đã nạp) vào tổng số dư vì khách đã thanh toán phần này |
| Thuật ngữ canonical | "Đang hoạt động", "Đã hết hạn", "Đã dùng hết", "Đã hoàn" |
| Thiết kế thị giác | Banner ≤ 7 ngày intent danger; ≤ 30 ngày intent warning; cột Còn lại intent primary emphasis |
| Điều không được tự suy diễn | KHÔNG mở direct GraphQL select toàn bộ lot/deduction; chỉ qua action có check quyền customer/branch |
B2.6.2) Bố cục — Demo gắn vào UI hiện tại
/user/customer/:id (extend CustomerEWalletInformation.tsx + StatisticCustomerWallets.tsx + 2 popup mới: CustomerKm2WalletPopup.tsx + StatisticWalletPromotion2Popup.tsx)
Sidebar Thống kê ví — bám sát StatisticCustomerWallets.tsx:30-58 (6 card hiện hữu + 1 card mới KM 2 thứ 7):
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ THÔNG TIN KHÁCH HÀNG — Nguyễn Thị Lan [👤 Avatar] 0909 123 456 │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ ┌─── Section "Thống kê ví" — 7 card (6 hiện hữu + 1 mới) ───────────────────────────────┐ │
│ │ │ │
│ │ Hàng 1 (4 card đầu): │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Doanh số ⓘ │ │ Thực thu ⓘ │ │ Công nợ ⓘ │ │ Ví CTV ⓘ │ ← KEEP │ │
│ │ │12.500.000đ │ │ 8.200.000đ │ │ 0đ │ │ 250.000đ │ (bg: green/blue/ │ │
│ │ │ │ │ │ │ │ │ (clickable │ red/orange) │ │
│ │ │ │ │ │ │ │ │ → withdraw│ │ │
│ │ │ │ │ │ │ │ │ history) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ Hàng 2 (3 card ví + 1 card mới): │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ │
│ │ │ Ví Diva ⓘ │ │ Ví Khuyến ⓘ│ │ Ví Khuyến ⓘ│ │ │ │ │
│ │ │ 1.500.000đ │ │ mãi │ │ mãi 2 ★ │ │ │ │ │
│ │ │ (clickable │ │ 800.000đ │ │ 8.500.000đ │ │ (trống) │ │ │
│ │ │ → popup │ │ (clickable │ │ (clickable │ │ │ │ │
│ │ │ Vnd lịch sử│ │ → popup KM1│ │ → popup KM2│ │ │ │ │
│ │ │ hiện hữu) │ │ lịch sử │ │ lịch sử │ │ │ │ │
│ │ │ │ │ hiện hữu) │ │ MỚI) │ │ │ │ │
│ │ │ bg: #FAF3E6 │ │ bg: #F6F5FF │ │ bg: #FFF0FA │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └──────────────┘ │ │
│ │ │ │
│ │ KEEP KEEP NEW (card 7 mới — SCR-06-NEW-01) │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ ... │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
Reference statConfig hiện hữu (`StatisticCustomerWallets.tsx:30-37`):
{ name: "sales", label: "Doanh số", bgColor: "#E7F6E9" }
{ name: "actual_revenue", label: "Thực thu", bgColor: "#F2F7FF" }
{ name: "debt", label: "Công nợ", bgColor: "#FDE8E6" }
{ name: "commission", label: "Ví CTV", bgColor: "#FFF3E0" }
{ name: "vnd", label: "Ví Diva", bgColor: "#FAF3E6" }
{ name: "vnd_promotion", label: "Ví Khuyến mãi", bgColor: "#F6F5FF" }
Thêm row mới (SCR-06-NEW-01):
{ name: "vnd_promotion_2", label: "Ví Khuyến mãi 2", bgColor: "#FFF0FA" } ★ NEW
handleClick mở rộng (`StatisticCustomerWallets.tsx:46-57`):
if (itemName === "vnd_promotion_2") {
promotion2Visible.value = true; ★ NEW state ref
}2 Popup khi click vào card Ví KM 2:
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ Khi click card "Ví Khuyến mãi 2" trong sidebar → 1 popup mở ra (PO chốt UX): │
│ │
│ • PHƯƠNG ÁN HIỆN TẠI (đồng bộ pattern KM 1): │
│ Click card → mở thẳng `StatisticWalletPromotion2Popup` (lịch sử giao dịch) │
│ Có nút "Xem chi tiết Gói" trong header → mở `CustomerKm2WalletPopup` (chi tiết lot) │
│ │
│ • TUỲ CHỌN (nếu PO muốn đảo): │
│ Click card → mở `CustomerKm2WalletPopup` trước (chi tiết lot — khác KM 1) │
│ Có nút "Xem lịch sử giao dịch" trong popup → mở `StatisticWalletPromotion2Popup` │
│ │
│ (Spec không ràng buộc trigger nào trước, dev follow pattern KM 1 mặc định) │
└──────────────────────────────────────────────────────────────────────────────────────────────┘(A) Popup CustomerKm2WalletPopup — chi tiết LOT (SCR-06-NEW-02):
text
┌──────────────────────────────────────────────────────────────────────────────┐
│ VÍ KHUYẾN MÃI 2 — Nguyễn Thị Lan Số dư: 8.500.000đ [ × Đóng ] │
├──────────────────────────────────────────────────────────────────────────────┤
│ [ Xem lịch sử giao dịch ▸ ] (mở popup B) │
│ │
│ ⚠ CẢNH BÁO HẾT HẠN │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ ⛔ Gói Silver #1 còn 500.000đ — hết hạn 01/04/2026 (còn 8 ngày) (đỏ) │ │
│ │ ⚠ Gói Gold #1 còn 2.500.000đ — hết hạn 15/04/2026 (còn 22 ngày) (vàng)│ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ Lọc: [ Trạng thái: Tất cả v ] [ Gói: Tất cả v ] │
│ │
│ GÓI ĐANG HOẠT ĐỘNG │
│ ┌────┬──────────┬───────────┬───────────┬───────────┬───────────┬─────────┐ │
│ │ # │ Tên Gói │ Đã nạp │ Đã dùng │ Còn lại │ Hết hạn │ Action │ │
│ ├────┼──────────┼───────────┼───────────┼───────────┼───────────┼─────────┤ │
│ │ 1 │ Gói Gold │5.000.000đ │2.500.000đ │2.500.000đ │15/04/2026 │[Hoàn] │ │
│ │ 2 │ Gói Gold │5.000.000đ │1.500.000đ │3.500.000đ │24/09/2026 │[Hoàn] │ │
│ │ 3 │ Gói Silver│1.500.000đ │1.000.000đ │ 500.000đ │01/04/2026 │[Hoàn] │ │
│ └────┴──────────┴───────────┴───────────┴───────────┴───────────┴─────────┘ │
│ │
│ GÓI ĐÃ HẾT / ĐÃ HOÀN │
│ ┌────┬──────────┬───────────┬───────────┬──────────┬──────────┐ │
│ │ # │ Tên Gói │ Đã nạp │ Đã dùng │ Trạng │ Ngày │ │
│ ├────┼──────────┼───────────┼───────────┼──────────┼──────────┤ │
│ │ 4 │ Gói Silver│1.500.000đ │1.500.000đ │Đã dùng hết│01/01/2026│ │
│ │ 5 │ Gói Gold │5.000.000đ │ 500.000đ │Đã hoàn │15/02/2026│ │
│ │ 6 │ Gói Silver│1.500.000đ │ 800.000đ │Đã hết hạn │28/02/2026│ │
│ └────┴──────────┴───────────┴───────────┴──────────┴──────────┘ │
│ │
│ (Click 1 dòng → mở drawer Lịch sử giao dịch của lot này) │
└──────────────────────────────────────────────────────────────────────────────┘(B) Popup StatisticWalletPromotion2Popup — lịch sử giao dịch toàn ví (SCR-06-NEW-03): xem chi tiết wireframe tại B2.6.6A.
Vùng
KEEP: card wallet stats sidebar trái (6 card hiện hữu) + thông tin khách. VùngNEW: 1 card thứ 7 "Ví Khuyến mãi 2" + 2 popup mới (CustomerKm2WalletPopupchi tiết lot +StatisticWalletPromotion2Popuplịch sử giao dịch toàn ví) + banner cảnh báo + filter + CTAHoàn ví KM2per row.
B2.6.2.0) Ma trận Biến thể
| Variant ID | Điều kiện | Hiển thị | QA ref |
|---|---|---|---|
Variant A — Có lot active | Khách có ≥ 1 lot active, balance > 0 | Card hiện balance + popup hiện 2 bảng | TC-SCR06-A-01 |
Variant B — Có lot sắp hết hạn ≤ 30 ngày | ≥ 1 lot active có expired_at - NOW() ≤ 30 ngày | Banner warning trong popup | TC-SCR06-B-01 |
Variant C — Có lot sắp hết hạn ≤ 7 ngày | ≥ 1 lot active ≤ 7 ngày | Banner danger | TC-SCR06-C-01 |
Variant D — Có lot active đang nợ | Lot active có credited_amount < initial_amount (khách đã trả ≥ 1 lần nhưng chưa đủ) | Card hiện balance theo phần đã nạp; popup bảng "Đang hoạt động" có cột "Đã nạp" hiển thị "X/Y" để NV biết khách còn nợ | TC-SCR06-D-01 |
Variant E — Empty | Khách chưa từng có lot | Card vẫn hiện "0đ" (consistency); popup empty "Khách chưa có Gói Ví KM2 nào trong Ví KM 2" | TC-SCR06-E-01 |
Variant F — Loading | Đang fetch | Skeleton card + popup | TC-SCR06-F-01 |
Variant G — Lot expired hôm nay | ≥ 1 lot vừa expired qua đêm | Bảng "Đã hết / hoàn" có dòng mới + banner đẩy lên section trên | TC-SCR06-G-01 |
Variant H — No permission customer | User không có quyền xem khách | Card ẩn (no-render) | TC-PERM-SCR06-01 |
Variant I — Có quyền xem KH nhưng thiếu refund_request:create | Staff bình thường | Card + popup hiện đầy đủ; CTA Hoàn ví KM2 ẩn ở từng row | TC-PERM-SCR06-02 |
Variant J — Cross-branch | Lot mua ở branch A, user xem ở branch B | Hiển thị bình thường nếu user có quyền xem khách; cột "Branch mua" có thể xem trong drawer chi tiết | TC-SCR06-J-01 |
B2.6.3) Phân loại reuse
| Phân loại | File hiện có | Vị trí update | Lý do |
|---|---|---|---|
🔧 Extend | CustomerEWalletInformation.tsx, StatisticCustomerWallets.tsx | Thêm card Ví KM 2 trong wallet stats; thêm component popup CustomerKm2WalletPopup.tsx | Pattern card + popup hiện hữu cho các ví khác |
B2.6.4) Quy ước cột bảng
Bảng "Gói đang hoạt động":
| Cột | Width | Align | Format | Sortable |
|---|---|---|---|---|
| # | 40px | Center | Auto | No |
| Tên Gói | 150px | Left | Text snapshot package_name | Có |
| Đã nạp | 130px | Right | Currency wallet_value | Có |
| Đã dùng | 130px | Right | Currency (calc) | Có |
| Còn lại | 130px | Right | Currency, intent primary emphasis | Có |
| Hết hạn | 120px | Center | DD/MM/YYYY, intent danger ≤ 7 ngày, warning ≤ 30 ngày | Có |
| Action | 110px | Center | Nút Hoàn ví KM2 (conditional) | No |
Bảng "Đã hết / hoàn":
| Cột | Width | Align | Format | Sortable |
|---|---|---|---|---|
| # | 40px | Center | Auto | No |
| Tên Gói | 150px | Left | Text | Có |
| Đã nạp | 130px | Right | Currency | Có |
| Đã dùng | 130px | Right | Currency | Có |
| Trạng thái | 120px | Center | Badge: Đã dùng hết (success), Đã hết hạn (muted), Đã hoàn (warning) | Có |
| Ngày | 120px | Center | DD/MM/YYYY | Có |
B2.6.5) Filter
| # | Component | Mặc định | Options |
|---|---|---|---|
| 1 | Trạng thái | Tất cả | Tất cả / Đang hoạt động / Đã hết hạn / Đã dùng hết / Đã hoàn |
| 2 | Tên Gói | Tất cả | Tất cả / + danh sách Gói KH đã mua |
B2.6.6) Drawer "Lịch sử giao dịch lot" (click row)
Bảng lịch sử trong drawer phải hiện:
| Cột | Format | Ghi chú |
|---|---|---|
| Ngày | DD/MM/YYYY HH:mm | Theo created_at deduction |
| Loại | Badge | payment (xanh), refund_back (vàng), refund_request (cam) |
| Số tiền | Currency | Trừ thanh toán = âm; hoàn ngược = dương |
| Đơn / Yêu cầu | Link | Click mở chi tiết đơn hoặc request |
| Branch | Text | Chi nhánh phát sinh |
B2.6.6A) Popup "Lịch sử giao dịch Ví Khuyến Mãi 2" (SCR-06-NEW-03 — parity với popup KM 1 hiện hữu)
Ngữ cảnh: Popup KM 1 hiện có (StatisticWalletPromotionPopup.tsx) đã hiển thị lịch sử giao dịch ví khuyến mãi (KM 1) cho khách. Parity cần thêm popup tương ứng cho Ví Khuyến Mãi 2.
Trigger:
- Click vào card "Ví Khuyến Mãi 2" trong
StatisticCustomerWallets.tsx(đồng bộ trigger KM 1) HOẶC - Nút "Xem lịch sử giao dịch" trong popup
CustomerKm2WalletPopup(cùng tồn tại — popup chi tiết lot)
File mới: diva-admin/src/modules/user/components/customer/StatisticWalletPromotion2Popup.tsx — copy y nguyên pattern từ StatisticWalletPromotionPopup.tsx, đổi 3 chỗ:
| Chỗ thay đổi | KM 1 (gốc) | KM 2 (mới) |
|---|---|---|
Title header (StatisticWalletPromotionPopup.tsx:521) | "LỊCH SỬ GIAO DỊCH VÍ KHUYẾN MÃI" | "LỊCH SỬ GIAO DỊCH VÍ KHUYẾN MÃI 2" |
Filter GraphQL variables.where.wallet_type_id (:81-85) | { _eq: "VND_PROMOTION" } | { _eq: "VND_PROMOTION_2" } |
getTransactionDetailTypeLabel arg 2 (:426) | "Ví Khuyến mãi" | "Ví Khuyến mãi 2" |
Wireframe ASCII — bám sát popup KM 1 hiện hữu (10 cột table, width 1400px / 80vw):
text
Click "Ví Khuyến Mãi 2" 8.500.000đ trong sidebar StatisticCustomerWallets → mở popup:
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ LỊCH SỬ GIAO DỊCH VÍ KHUYẾN MÃI 2 — Nguyễn Thị Lan [ × Đóng ] │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌── Bộ lọc ───────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Từ ngày: [ 15/04/2026 ▾ ] Đến ngày: [ 15/05/2026 ▾ ] Chi nhánh: [ Tất cả ▾ ] Trạng thái: [▾] │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ Tham chiếu: Tổng số tiền theo bộ lọc: +5.200.000đ │
│ │
│ ┌─────┬──────────────────┬─────────────────┬───────────────┬───────────────┬──────────────┬─────────────┬──────────────┬────────────┬───────────────┐ │
│ │ STT │ THỜI GIAN GIAO │ MÃ THANH TOÁN │ NGUỒN TIỀN │ LOẠI GD │ SỐ TIỀN │ SỐ DƯ SAU │ TRẠNG THÁI │ ĐƠN HÀNG │ CHI NHÁNH │ │
│ │ │ DỊCH │ │ │ │ │ │ │ │ │ │
│ ├─────┼──────────────────┼─────────────────┼───────────────┼───────────────┼──────────────┼─────────────┼──────────────┼────────────┼───────────────┤ │
│ │ 1 │ 14:30, 24/04/26 │ TT-20260424-001 │ Ví Khuyến mãi 2│ Thanh toán │ -1.000.000đ │ 4.700.000đ │ ●Hoàn thành │ DH-260424 │ CN Quận 1 │ │
│ │ 2 │ 09:15, 22/04/26 │ TT-20260422-002 │ Ví Khuyến mãi 2│ Thanh toán │ -500.000đ │ 5.700.000đ │ ●Hoàn thành │ DH-260422 │ CN Quận 1 │ │
│ │ 3 │ 16:45, 18/04/26 │ NT-20260418-003 │ Ví Khuyến mãi 2│ Nạp tiền │ +5.000.000đ │ 6.200.000đ │ ●Hoàn thành │ NT-260418 │ CN Quận 1 │ │
│ │ 4 │ 11:00, 15/04/26 │ HV-20260415-004 │ Ví Khuyến mãi 2│ Hoàn ví KM2 │ +1.200.000đ │ 1.200.000đ │ ●Hoàn thành │ YC-260415 │ CN Thủ Đức │ │
│ │ 5 │ 23:59, 12/04/26 │ HH-20260412-005 │ Ví Khuyến mãi 2│ Hết hạn │ -800.000đ │ 0đ │ ●Hoàn thành │ — │ — │ │
│ │ ... │ │ │
│ └─────┴──────────────────┴─────────────────┴───────────────┴───────────────┴──────────────┴─────────────┴──────────────┴────────────┴───────────────┘ │
│ │
│ « Trang [ 1 ] [ 2 ] [ 3 ] » (Hiển thị 20 / 47 giao dịch) │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Reference: bám sát chính xác layout StatisticWalletPromotionPopup.tsx:509-547 — width 1400px max 80vw, 10 cột table, filter trên đỉnh, pagination ở dưới.Bảng cột (theo StatisticWalletPromotionPopup.tsx:320-331):
| Cột | Format | Ghi chú |
|---|---|---|
| STT | Number | rowIndex + 1 (auto-generate per page) |
| THỜI GIAN GIAO DỊCH | HH:mm, DD/MM/YYYY | Theo transaction_request.created_at |
| MÃ THANH TOÁN | Code | transaction_request.code (fallback --) |
| NGUỒN TIỀN | Text | "Ví Khuyến mãi 2" (label gốc — getTransactionDetailTypeLabel(behavior, "Ví Khuyến mãi 2")) |
| LOẠI GIAO DỊCH | Badge | Thanh toán / Hoàn ví KM2 / Hết hạn / Nạp tiền (theo behavior.name) |
| SỐ TIỀN | Currency colored | Âm = đỏ #DE2C00; Dương = xanh #33B64F; 0 = đen |
| SỐ DƯ SAU | Currency | wallet_transaction.after_amount (fallback --) |
| TRẠNG THÁI | Badge pill | Hoàn thành (xanh) / Đang yêu cầu (vàng) / Đang xử lý (cam) / Thanh toán thất bại (đỏ) / Đã hủy (đỏ) |
| ĐƠN HÀNG | Link xanh #2182DC | Click mở chi tiết đơn (cùng tab _blank) — resolve theo order.order_kind |
| CHI NHÁNH | Text | branch.name (fallback --) |
Filter (theo StatisticWalletPromotionPopup.tsx:338-373):
- Date range — Từ ngày + Đến ngày (mặc định 30 ngày gần đây); format ISO
YYYY-MM-DDTHH:MM:SS+00:00ở_gte/_lte - Chi nhánh — Multi-select (theo branch scope của user); filter
branch_id: { _in: [...] } - Trạng thái — Single-select dropdown:
Tất cả/Hoàn thành(S) /Đang yêu cầu(R) /Đang xử lý(P) /Thanh toán thất bại(F) /Đã hủy(C/Reject) - Pagination —
DEFAULT_LIMITper page (đồng bộ với popup KM 1); reset page về 1 khi đổi filter
Reference Amount (header bảng): Tổng số tiền theo bộ lọc = transaction_aggregate.aggregate.sum.amount (theo :315-317). Hiển thị màu xanh nếu dương, đỏ nếu âm.
Lưu ý implementation:
- Option α (đã chốt): tạo file riêng
StatisticWalletPromotion2Popup.tsx— copy pattern KM 1 - KHÔNG refactor popup KM 1 thành generic (giữ minimal blast radius); nếu Phase 2+ thêm KM 3 thì cân nhắc refactor DRY
- Reuse:
XPagination,XSelect,QDialog,QTable,LuckyDrawWheelPrizeProductCreateHeader(header dùng chung),formatCurrency,formatDateTime
B2.6.7) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Click card "Ví KM 2" | Mở popup overlay CustomerKm2WalletPopup (60vw desktop, 90vw tablet) — chi tiết LOT |
Click row trong popup CustomerKm2WalletPopup | Mở drawer "Lịch sử giao dịch lot" (lot-scoped) |
Click "Xem lịch sử giao dịch" trong CustomerKm2WalletPopup HOẶC click balance trong card Ví KM 2 | Mở popup StatisticWalletPromotion2Popup (transactional history toàn ví) |
Click Hoàn ví KM2 | Mở SCR-07 dialog (xếp chồng popup) |
| Đổi filter | Re-render bảng client-side (data đã fetch) |
| Banner cảnh báo click vào lot | Scroll bảng đến dòng tương ứng + highlight 2s |
| Đóng popup | Esc hoặc nút × |
B2.6.7B) Form Interaction (drawer/popup)
| Khía cạnh | Quy ước |
|---|---|
| Focus trap | Popup + drawer trap; Esc đóng nếu không có dirty |
| Keyboard nav | Tab xuyên suốt; Enter chọn row; Esc đóng |
B2.6.7C) Concurrency
| Tình huống | Hành vi |
|---|---|
| 2 NV cùng mở profile | Cùng đọc, không xung đột |
| Lot vừa hết hạn khi user đang xem | Polling 60s refresh (chỉ khi popup mở); banner cập nhật |
| Lot vừa bị refund từ user khác | Tương tự, polling cập nhật |
B2.6.7D) Network resilience
| Tình huống | Hành vi |
|---|---|
| Mất mạng khi mở popup | Popup empty + banner "Mất kết nối" + nút Tải lại |
| Có mạng lại | Auto refetch |
B2.6.8) Phân quyền
| Portal | Vai trò | Action | Hiển thị card | Hiển thị CTA Hoàn |
|---|---|---|---|---|
admin/pos/crm | Staff/Sale/Manager/Admin | customer_management:access | Có theo scope | Theo refund_request_management_submenu:create |
admin/pos/crm | Admin all-branch | customer_management:view_all | Toàn hệ thống | — |
B2.6.8A) Role × Variant
| Role | Permission | Variant |
|---|---|---|
| Staff bình thường | customer_management:access (branch self) | A-G; CTA Hoàn ẩn (Variant I) |
| Manager | customer_management:access + branch_mode=branch | A-G + CTA Hoàn nếu có refund_request_management_submenu:create |
| Kế toán | refund_request_management_submenu:* đầy đủ | A-G + CTA Hoàn hoạt động |
| Admin | customer_management:view_all | Cross-branch (Variant J) |
| User ngoài quyền customer | — | H — card ẩn |
B2.6.9) Trạng thái
| Trạng thái | Hiển thị |
|---|---|
| Đang tải | Skeleton card + popup skeleton 5 row × 2 bảng |
| Empty | Card "0đ" + popup "Khách chưa có Gói Ví KM2 nào" |
| Có lot active | 2 bảng + banner cảnh báo nếu có |
| Lỗi | Toast + nút Thử lại |
| No permission | Card ẩn |
B2.6.10) Action Feedback
| Hành động | Phản hồi | Copy |
|---|---|---|
Click Hoàn ví KM2 thiếu quyền | Tooltip không cho click | Cần quyền tạo Yêu cầu hoàn tiền |
Click Hoàn ví KM2 lot expired (race) | Modal | Lot vừa hết hạn. Vui lòng tải lại. |
| Đóng popup khi đang load | Cho đóng | — |
B2.6.10A) Error Taxonomy
| Loại | UI | Recovery | Copy |
|---|---|---|---|
| 403 (no permission customer) | Toast + redirect customer list | Liên hệ Admin | Bạn không có quyền xem khách hàng này |
| 410 (customer xoá) | Toast | Quay list | Khách hàng không còn tồn tại |
| Network/timeout | Banner + retry | Auto/manual | Không thể tải. Vui lòng thử lại. |
B2.6.11) Lifecycle UI mapping
| Trạng thái lot | Bảng | Action button | Badge |
|---|---|---|---|
active | Đang hoạt động | Hoàn ví KM2 (nếu eligible) | "Đang hoạt động" |
expired | Đã hết / hoàn | — | "Đã hết hạn" (muted) |
exhausted | Đã hết / hoàn | — | "Đã dùng hết" (success) |
refunded | Đã hết / hoàn | — | "Đã hoàn" (warning) |
B2.7) SCR-07: Form Hoàn ví KM2 trong Yêu cầu hoàn tiền
B2.7.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Người tạo: kế toán/Manager/Staff có refund_request_management_submenu:create. Người duyệt: reviewer hợp lệ với :approve. Người thanh toán: kế toán với :payment |
| Vào màn để quyết định gì? | Tính toán tiền hoàn cho khách (theo FORMULA-002), chốt phương thức hoàn (cash/wallet/bank_transfer), tạo transaction_request behavior refund_km2_wallet đi vào luồng Yêu cầu hoàn tiền hiện có |
| Dữ liệu chính | Snapshot wallet_km2_lot + wallet_km2_config (policy_snapshot); kết quả tạo transaction_request |
| CTA chính / phụ | Primary: Tạo yêu cầu hoàn tiền. Secondary: Huỷ, Đóng, Xem công thức |
| Điều không được hiểu nhầm | "Khách nhận = MAX(remaining × tỉ_lệ_hoàn − phí_xử_lý, 0)"; tỉ lệ hoàn = giá mua gói / giá trị ví |
B2.7.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Người tạo biết KHÁCH NHẬN bao nhiêu và phương thức hoàn |
| Thứ tự ưu tiên | (1) Thông tin Gói KM2 → (2) Tính toán hoàn tiền (số to nhất "KHÁCH NHẬN") → (3) Chọn phương thức + ghi chú → (4) CTA Tạo yêu cầu |
| Dữ liệu nhạy cảm | Số tiền cụ thể; chỉ user có quyền customer + refund mới mở được dialog |
| Pending / partial data | Nếu phí > số hoàn → "Khách nhận = 0đ" + cảnh báo, vẫn cho submit (tất toán lot, khách không nhận tiền) |
| Thuật ngữ canonical | "Hoàn ví KM2", "Phí xử lý hoàn ví", "Tỉ lệ hoàn", "Khách nhận"; KHÔNG dùng "Refund ticket" / "Hoàn vé" |
| Thiết kế thị giác | "KHÁCH NHẬN" intent success nếu > 0; intent negative-value + warning nếu = 0; phí hiện trong block calc intent muted |
| Điều không được tự suy diễn | KHÔNG tạo approval module riêng — request đi vào màn Yêu cầu hoàn tiền hiện có; duyệt/thanh toán xảy ra ở màn đó, không trong dialog này |
B2.7.2) Bố cục
text
┌──────────────────────────────────────────────────────────────────────┐
│ HOÀN VÍ KM 2 [ × Đóng ] │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ THÔNG TIN GÓI VÍ KM 2 │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Khách hàng: Nguyễn Thị Lan (0909 123 456) │ │
│ │ Tên Gói: Gói Gold │ │
│ │ Giá mua gói: 500.000đ │ │
│ │ Giá trị ví: 5.000.000đ │ │
│ │ Tỉ lệ hoàn: 10,00% (= 500.000 / 5.000.000) │ │
│ │ Đã dùng: 2.500.000đ │ │
│ │ Còn lại: 2.500.000đ │ │
│ │ Mua ngày: 24/03/2026 │ │
│ │ Hết hạn: 24/09/2026 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ TÍNH TOÁN HOÀN TIỀN ⓘ Xem công thức │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Hoàn theo tỉ lệ: 250.000đ (= 2.500.000 × 10%) │ │
│ │ Phí xử lý hoàn ví: 100.000đ (= 500.000 × 20%) │ │
│ │ ───────────────────────────────────────── │ │
│ │ KHÁCH NHẬN: 150.000đ (= MAX(250.000 − 100.000, 0)) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ TẠO YÊU CẦU HOÀN TIỀN │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Loại yêu cầu: [ Hoàn ví KM 2 ] (readonly) │ │
│ │ Hoàn vào: (•) Tiền mặt │ │
│ │ ( ) Ví VND của khách │ │
│ │ ( ) Chuyển khoản │ │
│ │ Ghi chú: [ Khách yêu cầu hoàn ví KM2 Gold ] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ [ Huỷ ] [ Tạo yêu cầu hoàn tiền ] │
└──────────────────────────────────────────────────────────────────────┘Vùng
NEW: toàn bộ dialog (component mớiRefundKm2WalletDialog.tsx).
B2.7.2.0) Ma trận Biến thể
| Variant ID | Điều kiện | Hiển thị | QA ref |
|---|---|---|---|
Variant A — Khách nhận > 0 | tỉ_lệ × balance − phí > 0 | "KHÁCH NHẬN" intent success; CTA enabled | TC-SCR07-A-01 |
Variant B — Khách nhận = 0 (phí ≥ hoàn) | phí ≥ tỉ_lệ × balance | "KHÁCH NHẬN: 0đ" intent danger + cảnh báo "Phí xử lý hoàn ví lớn hơn số tiền hoàn. Số dư Gói Ví KM2 sẽ được tất toán, khách không nhận tiền." Vẫn cho submit | TC-SCR07-B-01 |
Variant C — Loading | Đang fetch lot snapshot | Skeleton 3 block | TC-SCR07-C-01 |
Variant D — Lot vừa hết hạn (race) | Backend trả 422 khi tạo | Modal "Lot vừa hết hạn. Vui lòng tải lại tab Ví KM 2." + đóng dialog | TC-SCR07-D-01 |
Variant E — Lot đã refunded (race) | 422 | Modal "Lot này đã được hoàn từ thiết bị khác." | TC-SCR07-E-01 |
Variant F — Quyền bị thu hồi giữa session | 403 | Toast + đóng dialog | TC-PERM-SCR07-01 |
Variant G — Network | Timeout submit | Banner + auto retry với idempotency | TC-SCR07-G-01 |
B2.7.3) Phân loại reuse
| Phân loại | File | Vị trí | Lý do |
|---|---|---|---|
🆕 Build mới (component) + 🔧 Extend (luồng) | Component mới RefundKm2WalletDialog.tsx; engine reuse transaction_request + màn Yêu cầu hoàn tiền hiện có | Dialog overlay từ SCR-06; sau submit, request xuất hiện trong list Yêu cầu hoàn tiền với behavior refund_km2_wallet | Approval/list/payment đã có pattern; chỉ thêm behavior + dialog tạo |
B2.7.4) Quy ước field
| Field | Loại | Mặc định | Validation | Tooltip |
|---|---|---|---|---|
| Loại yêu cầu | Readonly badge | Hoàn ví KM 2 | Fixed refund_km2_wallet | "Không đổi được" |
| Hoàn vào | XOptionGroup radio | cash | Bắt buộc; enum cash/wallet/bank_transfer | — |
| Ghi chú | XInput textarea | Rỗng | Optional, ≤ 500 ký tự | — |
B2.7.5) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Mở dialog | Fetch snapshot lot + policy hiện tại; tính sẵn refund |
| Đóng dialog (Esc / × / Huỷ) | Confirm "Bỏ thay đổi?" nếu đã đổi refund_method hoặc ghi chú |
| Submit | Backend tạo transaction_request + audit policy_snapshot; toast success; đóng dialog; reload SCR-06 + list Yêu cầu hoàn tiền |
| Click "Xem công thức" | Mở popover hiển thị FORMULA-002 (PRD A10) |
B2.7.7B/C/D) Form/Concurrency/Network
| Loại | Quy ước |
|---|---|
| Form validation | On submit |
| Idempotency | client_request_id UUID |
| Concurrency | Lot vừa expired/refunded → BE 422; modal đóng dialog |
| Network | Mất mạng → disable nút, retry với cùng key |
B2.7.8) Phân quyền
| Portal | Action | Hành vi |
|---|---|---|
admin,pos | refund_request_management_submenu:create | Mở dialog + submit tạo request |
admin,pos | refund_request_management_submenu:update | Sửa thông tin request trước duyệt (ở màn detail) |
admin,pos | refund_request_management_submenu:approve | Duyệt/từ chối/hủy ở màn list/detail (không trong dialog này) |
admin,pos | refund_request_management_submenu:payment | Thanh toán hoàn tiền thật ở màn detail (sau approve) |
B2.7.8A) Role × Variant
| Role | Permission | Variant |
|---|---|---|
Kế toán/Manager có :create | A-E, G | |
Reviewer hợp lệ có :approve | Mở list/detail Yêu cầu hoàn tiền, không qua dialog này | |
User chỉ có customer_management:access | F — không mở được dialog |
B2.7.9) Trạng thái
(Đã liệt kê ở Ma trận Biến thể.)
B2.7.10) Action Feedback
| Hành động | Phản hồi | Copy |
|---|---|---|
| Tạo yêu cầu thành công | Toast success + đóng dialog + reload | Đã tạo Yêu cầu hoàn tiền KM2 #{request_id}. Vui lòng chờ duyệt. |
| Duyệt thành công (ở màn detail) | Toast | Đã duyệt Yêu cầu hoàn tiền KM2 #{request_id} |
| Thanh toán thực hiện thành công | Toast + reload | Đã hoàn {x}đ cho khách. Lot {N} đã tất toán. |
| Khách nhận = 0đ | Modal warning trước submit | Phí xử lý lớn hơn số tiền hoàn. Khách KHÔNG nhận tiền nhưng số dư Gói {tên} sẽ tất toán. Tiếp tục? |
| Lot expired race | Modal | Lot vừa hết hạn. Vui lòng đóng và tải lại tab Ví KM 2. |
B2.7.10A) Error Taxonomy
| Loại | UI | Recovery | Copy |
|---|---|---|---|
| 422 (lot expired/refunded) | Modal đóng dialog | Reload tab | (xem trên) |
| 403 (no permission) | Toast + đóng dialog | Liên hệ Manager | Bạn không có quyền tạo Yêu cầu hoàn ví KM2 |
| 409 (concurrency) | Modal | Tải lại | Lot đã được hoàn từ thiết bị khác |
| Network/timeout | Banner + retry | Auto | Mạng yếu, đang xử lý... |
| 5xx | Toast + correlation_id | Báo support | Có sự cố. Mã: {trace_id} |
B2.7.11) Lifecycle UI mapping (request)
| Trạng thái request | UI ở danh sách Yêu cầu hoàn tiền | Action |
|---|---|---|
pending (vừa tạo) | Badge Chờ duyệt (warning) | Reviewer: Duyệt / Từ chối / Hủy |
approved | Badge Đã duyệt (info) | Kế toán: Thanh toán |
paid | Badge Đã hoàn (success) | — (đã đóng) |
rejected / cancelled | Badge muted | Lot KHÔNG bị tất toán; có thể tạo lại request |
B2.8) SCR-08: Report dashboard Ví KM 2 (Phase 3 — sau PD-001)
B2.8.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Manager/Admin — report:access (route theo PD-001 chốt sau Phase 2) |
| Vào màn để quyết định gì? | Theo dõi hiệu quả KPI Ví KM 2 (đã bán, doanh thu, tỉ lệ dùng, sắp cháy), top khách, sắp hết hạn |
| Dữ liệu chính | Aggregate từ wallet_km2_lot + wallet_km2_lot_deduction + prepaid_order |
| CTA chính / phụ | Primary: Xuất Excel. Secondary: filter, refresh |
| Điều không được hiểu nhầm | Doanh thu = Σ giá mua gói (thực thu); KHÔNG phải Σ giá trị nạp ví |
B2.8.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Manager nhìn 4 KPI biết: bao nhiêu Gói bán, doanh thu, tỉ lệ dùng, bao nhiêu tiền sắp cháy |
| Thứ tự ưu tiên | (1) 4 KPI cards → (2) Bảng theo Gói → (3) Bảng sắp hết hạn → (4) Top khách |
| Dữ liệu nhạy cảm | Số tiền tổng hợp; chỉ Manager/Admin có quyền |
| Pending / partial data | Tỉ lệ dùng denominator = 0 → hiển thị "—" thay vì "0%" hay NaN |
| Thuật ngữ canonical | "Đã bán", "Doanh thu", "Tỉ lệ dùng (%)", "Sắp cháy (30 ngày)" |
| Thiết kế thị giác | KPI "Sắp cháy" intent warning; KPI "Doanh thu" intent primary emphasis |
| Điều không được tự suy diễn | KHÔNG hardcode route Phase 1/2; chờ PD-001 chốt |
B2.8.2) Bố cục
text
┌──────────────────────────────────────────────────────────────────────────────┐
│ BÁO CÁO VÍ KHUYẾN MÃI 2 │
├──────────────────────────────────────────────────────────────────────────────┤
│ Khoảng [01/01/2026] - [30/04/2026] Chi nhánh [ Tất cả v ] │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ĐÃ BÁN │ │ DOANH THU │ │ TỈ LỆ DÙNG │ │ SẮP CHÁY │ │
│ │ 156 Gói │ │ 78.000.000đ │ │ 35,00% │ │ 12.500.000đ │ │
│ │ │ │ (giá mua │ │ (đã dùng / │ │ (30 ngày │ │
│ │ │ │ gói) │ │ tổng nạp) │ │ tới) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ THỐNG KÊ THEO GÓI │
│ ┌────┬───────────┬────────┬──────────────┬──────────────┬──────────────┬─────┐
│ │ # │ Tên Gói │ Số bán │ Doanh thu │ Tổng nạp │ Đã dùng │Tỉ lệ│
│ ├────┼───────────┼────────┼──────────────┼──────────────┼──────────────┼─────┤
│ │ 1 │ Gói Gold │ 80 │ 40.000.000đ │ 400.000.000đ │ 140.000.000đ │35,00│
│ │ 2 │ Gói Silver│ 76 │ 15.200.000đ │ 114.000.000đ │ 39.900.000đ │35,00│
│ └────┴───────────┴────────┴──────────────┴──────────────┴──────────────┴─────┘
│ │
│ GÓI SẮP HẾT HẠN (30 NGÀY TỚI) │
│ ┌────┬────────────────┬────────────┬───────────┬───────────┬────────────┐ │
│ │ # │ Khách hàng │ SĐT │ Tên Gói │ Số dư │ Hết hạn │ │
│ ├────┼────────────────┼────────────┼───────────┼───────────┼────────────┤ │
│ │ 1 │ Nguyễn Thị Lan │0909 123 456│ Gói Gold │2.500.000đ │ 15/04/2026 │ │
│ │ 2 │ Trần Văn Nam │0908 765 432│ Gói Silver│ 500.000đ │ 01/04/2026 │ │
│ └────┴────────────────┴────────────┴───────────┴───────────┴────────────┘ │
│ │
│ TOP KHÁCH DÙNG NHIỀU NHẤT │
│ ┌────┬────────────────┬────────────┬──────────────┬──────────────┬────┬─────┐
│ │ # │ Khách hàng │ SĐT │ Tổng nạp │ Đã dùng │SLần│Tỉ lệ│
│ ├────┼────────────────┼────────────┼──────────────┼──────────────┼────┼─────┤
│ │ 1 │ Nguyễn Thị Lan │0909 123 456│ 11.500.000đ │ 4.000.000đ │ 12 │34,78│
│ │ 2 │ Lê Thị Hoa │0907 234 567│ 5.000.000đ │ 2.100.000đ │ 8 │42,00│
│ └────┴────────────────┴────────────┴──────────────┴──────────────┴────┴─────┘
│ │
│ [ Xuất Excel ] │
└──────────────────────────────────────────────────────────────────────────────┘B2.8.2.0) Ma trận Biến thể
| Variant | Điều kiện | Hiển thị | QA |
|---|---|---|---|
A — Có dữ liệu | Range có lot | KPI + 3 bảng đầy đủ | TC-SCR08-A-01 |
B — Empty range | Range không có lot | KPI = "0"/"—", bảng Chưa có dữ liệu | TC-SCR08-B-01 |
C — Tỉ lệ NaN | denominator = 0 | KPI Tỉ lệ hiện "—" | TC-SCR08-C-01 |
D — Manager scope | Manager branch_mode=branch | BranchSelect chỉ branch của Manager | TC-SCR08-D-01 |
E — Admin all-branch | Admin branch_mode=all | BranchSelect Tất cả + chọn được | TC-SCR08-E-01 |
F — Loading | Đang fetch | Skeleton KPI + 3 bảng | TC-SCR08-F-01 |
G — Export async | > 5,000 dòng | Toast "Đang xử lý, sẽ thông báo khi xong" | TC-SCR08-G-01 |
H — No permission | User thiếu report:access | Route guard | TC-PERM-SCR08-01 |
B2.8.3-2.8.11)
(Reuse pattern PrepaidCardReport.tsx — filter, table sortable, export pattern. Chi tiết cột bảng đã ở wireframe trên.)
| Khía cạnh | Quy ước |
|---|---|
| Filter | Khoảng thời gian (DateRangePicker), Chi nhánh (BranchSelect) |
| Sortable | Mọi cột số trong bảng |
| Empty | "Chưa có dữ liệu" |
| Loading | Skeleton |
| Error | Toast retry |
| Export | Async > 5,000 dòng; format theo B5A |
B2.9) SCR-09: Tab Ví khuyến mãi 2 trong wallet_screen.dart + Home widget (Flutter)
B2.9.1) Ngữ cảnh nghiệp vụ
Khách mua Gói Ví KM 2 tại spa (qua NV bán POS — ASM-001). Sau khi mua, khách cần XEM trên app điện thoại của mình để:
- Verify số dư Ví KM 2 còn lại (đối chiếu với lời NV)
- Xem chi tiết từng lần mua Gói (lot) — Gói nào còn lại bao nhiêu, hết hạn ngày nào
- Xem lịch sử giao dịch (đã chi tiêu / hoàn lại / hết hạn)
- Nhận cảnh báo sắp hết hạn (đồng bộ ZNS
km2_lot_expiring_7d)
Tham chiếu: PD-002 đã chốt "in-scope Phase 1". DEC-031 chốt thêm tab Ví khuyến mãi 2 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"). KHÔNG tạo screen riêng / route mới — đồng nhất UX với pattern KM 1.
B2.9.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Lý do |
|---|---|
| Read-only Phase 1 | ASM-001 — khách không tự mua Gói trên app; mọi mutation chỉ qua POS |
Tab thứ 3 trong wallet_screen.dart — đồng cấp với 2 tab hiện hữu | DEC-031 — đồng nhất UX khách: 1 entry point "Ví của tôi" cho mọi loại ví; pattern KM 1 hiện hữu đã quen với khách |
| Hiển thị balance realtime | DEC-029 — balance lấy từ get_customer_km2_balance action, không cached |
| Bilingual: code internal (VND_PROMOTION_2) + display VI (Ví khuyến mãi 2) | Đồng bộ B0.7 |
| Không SMS push từ app | DEC-006 — Phase 1 chỉ banner trong UI, không OneSignal push |
| Empty/error state thân thiện | "Bạn chưa có Gói Ví KM 2 nào. Đến spa để mua." |
| Implementation widget chọn bởi dev | DEC-031 note kỹ thuật — FolderTabs hiện cứng 2 tab; dev tự chốt: refactor FolderTabs mở rộng 3 tab HOẶC swap sang CustomTabbar/TabPageWidget (đã có trong core/). Spec KHÔNG prescribe widget cụ thể |
B2.9.2) Bố cục — Demo gắn vào UI hiện tại
Home page — home_wallet.dart (chèn card mới sau Bonus Point):
Home — danh sách thẻ ví hiện tại (horizontal scroll):
┌────────────────────────────────────────────────────────────────────────────┐
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ Ví DIVA │ │ Hoa hồng │ │ Điểm │ │ Điểm │ │ Ví KM 2 ★ │ » │
│ │ 2.5tr đ │ │ 250.000đ │ │ thưởng │ │ thưởng │ │ 4.700.000 đ │ │
│ │ │ │ │ │ 120 đ │ │ 45 đ │ │ HSD: 24/08/26 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
★ Card "Ví KM 2" mới — chèn cuối list, conditional khi balance > 0
Reference: home_wallet.dart:49-77 — list 4 card hiện có (diva / commission / membership / bonusPoint).
KHÔNG có card "Ví KM 1" trên Home hiện tại; KM 2 là card thứ 5 mới.
Tap card → mở wallet_screen.dart với initialTabIndex trỏ tab Ví khuyến mãi 2 (tab thứ 3 — DEC-031).wallet_screen.dart — thêm tab thứ 3 "Ví khuyến mãi 2" (DEC-031):
Wallet Screen "Ví của tôi" — 3 tab (sau khi thêm Ví KM 2):
┌────────────────────────────────────────────────────────────┐
│ ← Ví của tôi [⚙] │
├────────────────────────────────────────────────────────────┤
│ Số dư: │
│ 4.700.000 đ (tab đang active = Ví KM 2) │
│ │
│ ╭─────────────────╮╭─────────────────╮╭────────────────╮ │
│ │ Ví thẻ trả trước││ Ví khuyến mãi ││ Ví khuyến mãi 2★││
│ ╰─────────────────╯╰─────────────────╯╰────────────────╯ │
│ │
│ Tab "Ví khuyến mãi 2" — content (active): │
│ ⚠️ Gói Gold còn 1.200.000 đ — hết hạn 24/08/2026 (7 ngày) │
│ │
│ ── Gói đang hoạt động (2) ───────────────────── │
│ ┌──────────────────────────────────────────────┐ │
│ │ Gói Gold │ Còn 1.200.000 đ │ HSD 24/08 ›│ │
│ │ Gói Diamond │ Còn 3.500.000 đ │ HSD 15/11 ›│ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ── Đã dùng hết / hết hạn / đã hoàn ──────────── │
│ (Lazy load 10 row / page) │
│ │
│ [Tap row] → Drawer chi tiết lot │
│ [Filter chip] → Lọc theo loại giao dịch (như Ví KM 1) │
└────────────────────────────────────────────────────────────┘
★ Tab "Ví khuyến mãi 2" mới — thứ 3 cùng cấp với 2 tab hiện hữu
Reference current state:
- wallet_screen.dart:213-227 hiện dùng `FolderTabs` (widget cứng 2 tab: leftTitle/rightTitle)
- core/lib/presentation/common_widget/folder_tabs.dart — `FolderTabController` chỉ accept value 0/1
Implementation note (dev tự chốt, KHÔNG ràng buộc PO):
- Phương án 1: Refactor `FolderTabs` mở rộng 3 tab — giữ visual identity vàng hiện có
- Phương án 2: Swap `FolderTabs` → `CustomTabbar` (chip pill) — đã có trong `core/`, support N tabs
- Phương án 3: Swap `FolderTabs` → `TabPageWidget` (Material TabBar) — đã có trong `core/`B2.9.2.0) Ma trận Biến thể
| Variant | Khi nào | UI thay đổi |
|---|---|---|
has_active_lot | Có ≥ 1 lot status=active | Hiện banner cảnh báo (nếu sắp hết hạn) + bảng lot |
no_active_lot_has_history | Hết tất cả lot, nhưng có lịch sử | Bảng "Đang hoạt động" empty; bảng "Đã dùng hết" có data |
never_purchased | Khách chưa từng có Gói KM 2 | Empty state full screen: "Bạn chưa có Gói Ví KM 2 nào. Đến spa để mua." |
balance_zero_but_active | Có lot active nhưng balance=0 (đã dùng hết, chưa scheduler mark exhausted) | Hiển thị "0đ — đã dùng hết, chờ hệ thống cập nhật" |
feature_disabled | Admin tắt KM 2 (wallet_km2_config.disabled=true) | Tab ẩn hoàn toàn (không render) — đồng bộ với SCR-01 toggle |
B2.9.3) Phân loại reuse + điểm update
| Phần | Loại | File / Pattern | Delta |
|---|---|---|---|
wallet_screen.dart — thêm tab thứ 3 | 🔧 Extend | customer/lib/presentation/modules/wallet/listing/views/wallet_screen.dart:213-227 (hiện FolderTabs 2 tab) | Thêm tab thứ 3 "Ví khuyến mãi 2" cùng cấp; widget chọn bởi dev (refactor FolderTabs 3 tab HOẶC swap CustomTabbar/TabPageWidget — đã có trong core/). Update _buildCard switch logic theo tab index 0/1/2 |
| Wallet page content — file mới | 🆕 New | customer/lib/presentation/modules/wallet/listing/views/pages/promotion_wallet_2_page.dart (mới — parity với promotion_wallet_page.dart hiện có) | Content tab Ví KM 2: balance display, 2 bảng (active/inactive lot), filter chip, drawer detail. Pattern copy từ promotion_wallet_page.dart |
| WalletBloc extend | 🔧 Extend | customer/lib/presentation/modules/wallet/listing/bloc/wallet_bloc.dart:43-62 (hiện có pattern Prepaid + Promotion) | Thêm 3 event class (GetKm2BalanceEvent, GetKm2TransactionsEvent, ChangeKm2FilterEvent), 3 state field (km2Balance, km2TransactionsInMonths, km2Filter), 1 nhánh _onFilter(WalletType.promotion2, ...). Pattern copy y nguyên từ promotion |
| Home wallet widget pattern | 🔧 Extend | home_wallet.dart:49-77 (đang render 4 card: DIVA/Commission/Reward Point/Bonus Point — KHÔNG có ví KM 1 trên Home) | Thêm 1 card "Ví KM 2" làm phần tử thứ 5 trong list wallets, conditional khi state.promotion2?.balance > 0; tap mở wallet_screen.dart với initialTabIndex trỏ tab Ví KM 2 (đồng nhất UX) |
| Home interactor balance fetch | 🔧 Extend | home_page_interactor.impl.dart:87-134 (hiện gọi 2 API song song: diva + promotion) | Thêm 1 API call WalletType.promotion2 qua Future.wait; fold sum nếu muốn show total |
| Balance fetch (tab Ví KM 2) | 🆕 New | Action get_customer_km2_balance (DEC-029) | Repository wrap action call; KHÔNG đọc wallet.amount |
| Lot list query | 🆕 New | Query GraphQL wallet_km2_lot WHERE customer_id=... | Filter theo auth context; permission customer (xem SCR-API permission ở dev-spec) |
| Transaction history | 🆕 New | Query wallet_km2_lot_deduction WHERE lot.customer_id=... | Filter theo lot trong drawer chi tiết |
| Banner cảnh báo expiry | 🆕 New | Pattern banner mới (không reuse từ KM 1 vì KM 1 không có banner expiry tương tự trên customer app) | Hiện banner nếu có ≥ 1 lot expired_at ≤ NOW() + 7 ngày |
| Empty state | 🔧 Extend | Empty state pattern hiện có | Text mới "Bạn chưa có Gói Ví KM 2 nào..." |
B2.9.4) Quy ước field / cột bảng
Bảng "Lot đang hoạt động":
| Cột | Width | Render | Tap action |
|---|---|---|---|
| Tên Gói | Flex | package_name snapshot | — |
| Số dư còn lại | 30% | balance currency VN | — |
| Hạn sử dụng | 30% | expired_at format DD/MM/YYYY | — |
| Chevron (›) | 24px | Icon | Mở drawer detail |
Drawer detail (tap row):
- Header: Tên Gói + Trạng thái badge (
Đang hoạt động) - Block 1 — Thông tin: Giá mua gói, Giá trị ví, Số dư hiện tại, Ngày mua, Hạn sử dụng, Chi nhánh mua
- Block 2 — Lịch sử giao dịch lot (list
wallet_km2_lot_deduction):- Mỗi row: ngày + loại (
Trừ thanh toán/Hoàn ngược/Hoàn ví KM 2) + số tiền + mã đơn
- Mỗi row: ngày + loại (
B2.9.5) Filter
| Filter | Vị trí | Options |
|---|---|---|
| Trạng thái lot | Chip group bên trong tab Ví khuyến mãi 2 (parity với chip filter loại giao dịch tab Ví KM 1 hiện có) | Đang hoạt động / Đã hết hạn / Đã dùng hết / Đã hoàn (single-select) |
B2.9.6) Quy ước tương tác
| Element | Loại | Behavior |
|---|---|---|
| Card home "Ví KM 2" | Drill-down | Tap → mở wallet_screen.dart với initialTabIndex trỏ tab Ví KM 2 (giống pattern home card hiện có trỏ tab tương ứng) |
| Row lot | Drill-down | Tap → mở drawer detail (slide-up) |
| Banner cảnh báo | Hiển thị | Tap banner → cuộn xuống lot sắp hết hạn |
| Tab switch | Tab switch | Đổi giữa Ví thẻ trả trước / Ví khuyến mãi / Ví khuyến mãi 2 — preserve scroll position từng tab; balance card tổng phía trên đổi theo tab active (đồng bộ pattern _buildCard hiện có ở wallet_screen.dart:149-200) |
ZNS deep link km2_lot_activated | External | Mở wallet_screen.dart với tab Ví khuyến mãi 2 active (theo handler deep link hiện có — thêm route mapping) |
B2.9.7B) Form Interaction Deep
N/A — read-only Phase 1, không có form.
B2.9.7C) Concurrency
- Realtime balance: refetch khi return to foreground; debounce 2s
- Pull-to-refresh ở tab Ví khuyến mãi 2: fetch lại action
get_customer_km2_balance+ danh sách lot (chỉ refetch tab đang active để tiết kiệm network — pattern hiện có ở_onRefreshPromotionWallet) - KHÔNG có optimistic update (read-only)
B2.9.7D) Network resilience
| Scenario | Hành vi |
|---|---|
| Mất mạng khi load tab | Hiển thị cached balance gần nhất (nếu có) + banner "Đang ngoại tuyến — số dư có thể chưa cập nhật" |
Action get_customer_km2_balance timeout (>5s) | Fallback đọc wallet.amount cached + banner "Số dư có thể chưa realtime" |
| Fail load lot list | Empty state lỗi + nút "Tải lại" |
B2.9.8) Ma trận phân quyền
| Role | Action | Cho phép? |
|---|---|---|
| Khách đã đăng nhập app | Vào tab Ví khuyến mãi 2 trong wallet_screen.dart | ✅ (filter customer_id = auth.uid ở action get_customer_km2_balance/get_customer_km2_lots) |
| Khách chưa đăng nhập | Vào wallet_screen.dart | ❌ (route guard auth — giống pattern hiện hữu cho 2 tab cũ) |
| Khách bị Admin disable | Vào wallet_screen.dart | ❌ (login fail) |
| Khách có balance KM 2 = 0 | Card "Ví KM 2" trên Home | ❌ (ẩn card — conditional balance > 0) |
| Khách có balance KM 2 = 0 | Vào tab Ví KM 2 qua deep link ZNS hoặc switch tab thủ công | ✅ (vẫn vào được, hiện empty state) |
B2.9.8A) Ma trận Role × Variant
Chỉ 1 role "customer authenticated" — không có variant phân quyền sâu hơn.
B2.9.9) Ma trận trạng thái (ref B0.5)
| State | Behavior |
|---|---|
| Default | Hiện balance + 2 bảng trong tab Ví khuyến mãi 2 |
| Loading | Skeleton balance + skeleton 3 row mỗi bảng |
| Empty (never_purchased) | "Bạn chưa có Gói Ví KM 2 nào. Đến spa để mua." |
| Error (fetch lot fail) | "Không thể tải. Tải lại" |
| No permission | Route guard chặn (chưa đăng nhập) → redirect login |
| Partial | Balance load thành công, danh sách lot fail → hiện balance + empty list + nút retry |
| Feature disabled (admin tắt KM 2 globally) | Tab Ví khuyến mãi 2 vẫn hiện nhưng empty state (đồng bộ với SCR-01); cách khác: ẩn tab — chốt theo PD-002 + admin config |
B2.9.10) Phản hồi sau thao tác
| Action | Phản hồi |
|---|---|
| Pull-to-refresh | Spinner top + toast nhẹ "Đã cập nhật" sau khi xong |
| Tap row → drawer | Slide-up 200ms |
| Đóng drawer | Slide-down 200ms |
B2.9.10A) Error Taxonomy
| Code | Khi nào | Hành vi UI |
|---|---|---|
NETWORK_OFFLINE | Mất mạng | Banner persistent + cached data |
ACTION_TIMEOUT | get_customer_km2_balance > 5s | Fallback cached + banner warning |
UNAUTHORIZED | Token expired | Redirect login |
INTERNAL_ERROR | BE 500 | Toast + retry button |
B2.9.11) Mapping UI theo lifecycle
| Lifecycle event | UI mobile |
|---|---|
| Khách trả lần đầu cho Gói KM 2 tại POS | Lot mới xuất hiện trong "Đang hoạt động" (sau pull-to-refresh hoặc return foreground) |
ZNS km2_lot_activated gửi (FR-011) | App có thể tap ZNS deep link mở wallet_screen.dart với tab Ví khuyến mãi 2 active |
| NV/khách dùng KM 2 thanh toán đơn DV | Balance giảm (qua pull-to-refresh), row lot cập nhật balance |
| Lot hết hạn (scheduler 00:05) | Sau pull-to-refresh: lot chuyển từ "Đang hoạt động" → "Đã hết hạn" |
| NV tạo Yêu cầu Hoàn ví KM 2 (SCR-07) | Lot chuyển status sau khi duyệt → "Đã hoàn" |
B2.10) SCR-10: Staff app — Customer detail balance KM 2
B2.10.1) Ngữ cảnh nghiệp vụ
Staff (NV/Manager) khi mở chi tiết khách trên app staff thường cần đối chiếu số dư các ví của khách để tư vấn. App staff hiện đã fetch + hiển thị balance Ví KM 1 thông qua customer_repository.impl.dart:473-481 (getBalances(type: WalletType.promotion, userId: id)). Phase 1 KM 2 cần thêm parity tương tự để staff thấy balance Ví KM 2 (đồng bộ với DEC-013 — quyền customer_management:access + branch scope).
B2.10.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Lý do |
|---|---|
| Parity với pattern Ví KM 1 hiện có | NV không cần học UI mới; chỉ thêm 1 dòng |
Conditional hiển thị (balance > 0) | Tránh nhiễu UI khi khách chưa có lot KM 2 |
| Permission đồng bộ DEC-013 | customer_management:access + branch scope (không tạo permission riêng) |
| Read-only | Staff KHÔNG action gì trên balance từ screen này (Hoàn ví KM 2 vẫn qua SCR-07 web admin) |
B2.10.2) Bố cục — Delta nhẹ
Customer Detail Page (staff app) — section "Ví của khách":
┌────────────────────────────────────────────────────────────┐
│ Ví của khách │
│ ───────────────────────────────────────── │
│ Ví DIVA : 2.500.000 đ │
│ Hoa hồng : 250.000 đ │
│ Ví khuyến mãi : 180.000 đ ← Ví KM 1 (hiện có)│
│ Ví khuyến mãi 2 ★ : 4.700.000 đ ← row mới (KM 2) │
└────────────────────────────────────────────────────────────┘
★ Row mới — chỉ render khi balance KM 2 > 0B2.10.3) Phân loại reuse + điểm update
| Phần | Loại | File / Pattern | Delta |
|---|---|---|---|
| Repository fetch balance | 🔧 Extend | staff/lib/data/data_source/remote/customer/customer_repository.impl.dart:473-481 | Thêm 1 call _walletRepo.getBalances(type: WalletType.promotion2, userId: id) (song song với call promotion hiện có) |
| Customer detail UI | 🔧 Extend | Section "Ví của khách" trong customer detail page hiện có | Thêm 1 row "Ví khuyến mãi 2" conditional balance > 0, parity row Ví KM 1 |
| WalletType enum | (ref) | core/lib/common/constants/server_constants.dart:300-346 | Phụ thuộc SCR-09-MOBILE-03 đã thêm WalletType.promotion2 |
B2.10.4) Quy ước field
| Field | Render | Conditional |
|---|---|---|
| Label "Ví khuyến mãi 2" | Từ l10n key promotionWallet2 | Luôn nếu balance > 0 |
| Value | balance.toAppCurrencyString() (định dạng VND) | Ẩn row nếu balance = 0 (đồng bộ pattern Ví KM 1) |
B2.10.5) Filter
N/A — chỉ là 1 row hiển thị.
B2.10.6) Quy ước tương tác
| Element | Loại | Behavior |
|---|---|---|
| Row Ví KM 2 | Read-only display | KHÔNG tap action (parity Ví KM 1 hiện có); Phase 2+ có thể thêm tap → mở popup chi tiết lot |
B2.10.7C) Concurrency
- Refetch khi pull-to-refresh customer detail page (cùng cycle với các ví khác)
- KHÔNG có optimistic update
B2.10.7D) Network resilience
| Scenario | Hành vi |
|---|---|
API getBalances(promotion2) fail | Ẩn row (KHÔNG hiện "Đang tải lỗi") — đồng bộ pattern row Ví KM 1 hiện có |
| Timeout | Ẩn row + log warning |
B2.10.8) Ma trận phân quyền
| Role | Action | Cho phép? |
|---|---|---|
Staff/Manager có customer_management:access + đúng branch scope | Xem row Ví KM 2 | ✅ (đồng bộ DEC-013) |
| Staff/Manager không có quyền xem khách | Vào customer detail | ❌ (chặn từ trước) |
Khách balance KM 2 = 0 | Row hiển thị | ❌ (ẩn — conditional) |
B2.10.8A) Ma trận Role × Variant
Đồng bộ với DEC-013 — chỉ 1 role gating duy nhất (customer_management:access).
B2.10.9) Ma trận trạng thái
| State | Behavior |
|---|---|
| Default (balance > 0) | Hiện row "Ví khuyến mãi 2: {balance} đ" |
| Empty (balance = 0) | Ẩn row hoàn toàn |
| Loading | Skeleton 1 dòng cùng cycle các ví khác |
| Error | Ẩn row (silent) |
B2.10.10) Phản hồi sau thao tác
N/A — read-only display.
B2.10.11) Mapping UI theo lifecycle
| Lifecycle event | UI staff app |
|---|---|
| Khách trả lần đầu cho Gói KM 2 tại POS | Row hiện sau pull-to-refresh customer detail |
| Khách dùng KM 2 thanh toán đơn → balance về 0 | Row biến mất sau pull-to-refresh (conditional ẩn) |
| Lot hết hạn (scheduler 00:05) | Row update balance hoặc biến mất (tuỳ kết quả get balance) |
| NV/Admin hoàn ví KM 2 (SCR-07) | Row update balance sau pull-to-refresh |
B3) Luồng người dùng
B3.1) Luồng thành công
Luồng 1 — Admin cấu hình Ví KM 2
Admin vào Settings → Ví khuyến mãi
→ Thấy banner "4 bước readiness" với 4 chip "Chưa đạt"
→ Bước 1: Bật toggle "Ví khuyến mãi 2" → ✓ Đạt
→ Bước 2: Click [Cấu hình →] mở /settings/prepaid-card?wallet_target=VND_PROMOTION_2
→ Tạo Gói Gold 500K → 5tr (6 tháng) → quay lại Settings
→ Bước 2 ✓ Đạt
→ Bước 3: Click [Cấu hình →] mở /ecommerce/service/:id, bật toggle KM 2 cho 1 dịch vụ
→ quay lại Settings → Bước 3 ✓ Đạt
→ Bước 4: Bán test 1 đơn KM 2 nội bộ ở POS
→ quay lại Settings → 4/4 ✓
→ Click [Bật Ví KM 2 cho POS]
→ Modal confirm "Bật cho toàn hệ thống?" → Xác nhận
→ Toast `Đã bật Ví KM 2 cho POS` → POS thấy chip Ví KM 2 ngayLuồng 2 — Sale bán Gói Ví KM2 (mix với thẻ trả trước VND)
Sale POS → Nạp Tiền → Tạo Mới Nạp Tiền
→ Chọn khách Nguyễn Thị Lan
→ + Thêm thẻ nạp: Thẻ trả trước 10tr × 1 → CẦN TT 10tr, NẠP DIVA 10tr
→ + Thêm thẻ nạp: Gói Gold (6 tháng) × 2 → CẦN TT 1tr, NẠP KM 2 10tr (cột KM 2 hiện)
→ Sidebar: Tiền vào ví DIVA 10tr, Tiền vào ví KM 1.5tr, Tiền vào Ví KM 2 10tr, Số tiền cần TT 11tr
→ Chọn Tiền mặt; chip "Trả góp" disabled với tooltip
→ Khách trả 11tr tiền mặt
→ Bấm [Tạo Mới] → Backend tạo:
• prepaid_order
• Cộng 10tr Ví VND + 1.5tr Ví KM 1
• Tạo 2 lot KM 2 (mỗi lot 5tr, expired_at = NOW + 6 tháng end_of_day)
• ZNS gửi (nếu FR-011 bật): "Bạn đã kích hoạt 2 Gói Gold, tổng 10.000.000đ, HSD: 24/10/2026"
→ Redirect /ecommerce/prepaid-card-order/{id} + toast `Đã tạo đơn nạp tiền {mã}`Luồng 3 — Sale thanh toán đơn DV bằng Ví KM 2
Sale POS mở đơn /ecommerce/order/DH-20260424-001
→ Đơn: Massage 500K (eligible) + Liệu trình 4.5tr (eligible) + Kem 300K (không eligible)
→ Tổng 5.3tr; Eligible 5tr
→ Click chip Ví KM 2
→ Block hiện: Số dư 4.6tr / Eligible 5tr / Tối đa (20%) = 1tr / Số tiền KM 2 = 1tr (auto, readonly)
→ Còn lại cần trả: 4.3tr
→ NV chọn Tiền mặt 4.3tr
→ Bấm [Thanh toán]
→ Backend deduct_km2_payment FIFO trừ lot cũ nhất trước
→ Toast `Đã thanh toán 5.300.000đ. KM 2 áp dụng: 1.000.000đ. Số dư còn: 3.600.000đ.`Luồng 4 — Manager tạo Yêu cầu Hoàn ví KM2
Manager mở /user/customer/{id} của Nguyễn Thị Lan
→ Click card "Ví KM 2: 8.500.000đ"
→ Popup hiện 3 lot active; thấy Gói Gold #1 còn 2.5tr, hết hạn 15/04/2026
→ Click [Hoàn] ở dòng #1
→ Dialog SCR-07 mở với:
Tỉ lệ hoàn 10%, Hoàn theo tỉ lệ 250K, Phí xử lý 100K
KHÁCH NHẬN: 150.000đ
→ Chọn "Hoàn vào: Tiền mặt"
→ Ghi chú "Khách yêu cầu hoàn ví KM2 Gold"
→ Bấm [Tạo yêu cầu hoàn tiền]
→ Toast `Đã tạo Yêu cầu hoàn tiền KM2 #REQ-2026-0042. Vui lòng chờ duyệt.`
→ Đóng dialog; SCR-06 reload (chưa thấy đổi vì request mới chỉ ở pending)
→ Kế toán mở màn Yêu cầu hoàn tiền → thấy request, duyệt + thanh toán
→ Sau khi paid: lot #1 → status = refunded, balance = 0; toast `Đã hoàn 150.000đ cho khách. Lot Gold đã tất toán.`B3.2) Luồng thay thế / lỗi
Luồng 5 — Toggle KM1+KM2 tắt, NV cố chọn cả 2
NV mở đơn → Click chip Ví KM 2 (selected)
→ Click chip Ví KM 1 → BE check config allow_combine_km1 = false
→ FE auto bỏ chọn Ví KM 2 + toast `Cấu hình hiện không cho phép dùng cả 2 ví trong 1 đơn`
→ Block KM 2 ẩn, block KM 1 hiệnLuồng 6 — Race condition khi thanh toán
Sale A và Sale B cùng mở đơn cho Nguyễn Thị Lan, mỗi người mở riêng order khác nhau
→ Cùng lúc: A bấm Thanh toán đơn 1, B bấm Thanh toán đơn 2
→ Backend SELECT FOR UPDATE lot
→ A thắng race: trừ lot, toast success cho A
→ B nhận 422 "insufficient balance"; toast `Số dư không đủ. Đã tải lại số dư mới.`
→ Block KM 2 của B refresh số dư → có thể tính lại hoặc bỏ chọn KM 2Luồng 7 — Khách nhận 0đ khi hoàn ví KM2
Manager hoàn ví KM2 Gói Silver còn 100K (giá mua 200K, ví 1.5tr)
→ Tỉ lệ hoàn = 200K / 1.500K = 13,33%
→ Hoàn theo tỉ lệ = 100K × 13,33% = 13.333đ
→ Phí xử lý = 200K × 20% = 40.000đ
→ KHÁCH NHẬN = MAX(13.333 - 40.000, 0) = 0đ
→ Dialog hiện "0đ" intent danger + cảnh báo
→ Modal confirm "Phí lớn hơn số tiền hoàn. Khách KHÔNG nhận tiền nhưng số dư Gói Silver sẽ tất toán. Tiếp tục?"
→ Manager xác nhận → tạo request thành côngLuồng 8 — Lot vừa hết hạn khi đang thanh toán
Sale POS mở chip Ví KM 2 lúc 23:59
→ Block tính eligible × 20% = 1tr; số dư 4.6tr
→ Sale chọn cash + bấm Thanh toán lúc 00:01 (cron 00:05 chưa chạy)
→ BE deduct lot, kiểm tra `expired_at > NOW()` trong transaction
→ Lot cũ nhất hết hạn 23:59:59 ngày trước → BE skip, FIFO chuyển sang lot tiếp theo
→ Vẫn thanh toán thành công với lot khác → toast successB4) Đặc tả thông báo
| Trigger | Kênh | Mẫu nội dung | Variables | Chống trùng |
|---|---|---|---|---|
km2_lot_activated (lần trả đầu) | ZNS (Zalo) | "Bạn đã kích hoạt {qty} {package_name}, tổng {total_amount}đ, hạn dùng đến {expired_at}. Hãy đến spa sử dụng nhé!" | qty, package_name, total_amount, expired_at | 1 lần / prepaid_order_id |
km2_lot_expiring_7d (cron daily) | ZNS | "Gói Ví KM 2 {package_name} của bạn còn {balance}đ sẽ hết hạn ngày {expired_at}. Đến spa dùng trước khi hết hạn nhé!" | package_name, balance, expired_at | 1 lần / lot_id / 7-day window |
km2_lot_refunded (sau thanh toán refund) | ZNS (optional) | "Đã hoàn {amount}đ cho Gói {package_name}. Vui lòng kiểm tra {refund_method}." | amount, package_name, refund_method | 1 lần / transaction_request_id |
Lưu ý:
- ZNS template phải đăng ký với Zalo trước khi dùng (ASM-004).
- FR-011 là
Could; nếu PO chưa bật ZNS cho Phase 1 thì không block bán Gói Ví KM2/thanh toán. - Cảnh báo hết hạn trong profile = TEXT BANNER trong UI, KHÔNG phải push notification (DEC-006).
B5) Đặc tả xuất dữ liệu (Export)
B5.1) Excel — Báo cáo Ví KM 2 (SCR-08)
Sheet 1: Thống kê theo Gói
| Cột | Header | Format | Width |
|---|---|---|---|
| STT | STT | Number | Auto |
| Tên Gói | Tên Gói | Text | 20 |
| Số bán | Số bán | Number | 12 |
| Doanh thu | Doanh thu (đ) | Currency (number Excel) | 18 |
| Tổng nạp | Tổng nạp (đ) | Currency | 18 |
| Đã dùng | Đã dùng (đ) | Currency | 18 |
| Tỉ lệ | Tỉ lệ dùng (%) | Number 2 decimal | 14 |
Sheet 2: Gói sắp hết hạn
| Cột | Header | Format |
|---|---|---|
| STT | STT | Number |
| Khách hàng | Khách hàng | Text |
| SĐT | Số điện thoại | Text |
| Tên Gói | Tên Gói | Text |
| Số dư | Số dư (đ) | Currency |
| Hết hạn | Hết hạn | Date DD/MM/YYYY |
Sheet 3: Top khách dùng
| Cột | Header | Format |
|---|---|---|
| STT | STT | Number |
| Khách hàng | Khách hàng | Text |
| SĐT | Số điện thoại | Text |
| Tổng nạp | Tổng nạp (đ) | Currency |
| Đã dùng | Đã dùng (đ) | Currency |
| Số lần | Số lần | Number |
| Tỉ lệ | Tỉ lệ dùng (%) | Number 2 decimal |
- File name:
bao-cao-vi-km2_{branch_code}_{from-date}_{to-date}_{timestamp}.xlsx - Row cap: 10,000 rows / sheet
- Async threshold: > 5,000 rows tổng → background job + notification
B5A) Quy ước xuất dữ liệu chuyên sâu (Export Deep Contract)
| Khía cạnh | Quy ước |
|---|---|
| Mã hoá file | UTF-8 with BOM (Excel mở đúng tiếng Việt) |
| Header rows | Hàng 1: "Báo cáo Ví KM 2" + tên CN + kỳ; Hàng 2: filter applied; Hàng 3: header cột |
| Footer rows | Hàng cuối: tổng số dòng, người xuất, thời điểm, correlation_id |
| Số format | Tiền: cell type Number (Currency), không kèm "đ" trong cell; ngày: cell type Date Excel |
| Masking theo quyền | Manager branch_mode=branch chỉ thấy branch mình; Admin all-branch thấy tất cả; cell — nếu không quyền |
| Multi-sheet | Sheet 1 "Thống kê theo Gói"; Sheet 2 "Sắp hết hạn"; Sheet 3 "Top khách"; Sheet 4 "Bộ lọc"; Sheet 5 "README" giải thích cột |
| Async threshold | > 5,000 dòng → background job; toast "Đang xử lý. Bạn sẽ nhận thông báo khi xong"; ETA dự kiến 30-60s |
| File quá lớn | > 50,000 dòng → chia _part1.xlsx, _part2.xlsx |
| Tên file | bao-cao-vi-km2_{branch_code}_{from}_{to}_{timestamp}.xlsx; timezone Asia/Ho_Chi_Minh |
| Telemetry | Log export_id, user_id, row_count, column_count |
| Re-download | Lưu URL trong notification 7 ngày |
| Encoding edge | Tên KH có dấu (Đặng Thị Ánh Sương) → escape đúng |
B6) Từ điển nội dung hiển thị (Copy Dictionary)
Mọi copy mới hoặc thay đổi: label, CTA, empty/loading/error/success, validation, confirm, permission, tooltip.
| Key | Tiếng Việt | EN (i18n nếu cần) | Ngữ cảnh |
|---|---|---|---|
wallet_km2_title | Ví KM 2 | Promotion Wallet 2 | Card title trong wallet stats; sidebar settings |
wallet_km2_settings_title | Cài đặt Ví Khuyến Mãi 2 | Promotion Wallet 2 Settings | Tiêu đề trang Settings KM 2 |
toggle_km2_label | Ví khuyến mãi 2 (KM 2) | — | Label toggle phương thức |
toggle_km2_on | Bật | — | Toggle ON state |
toggle_km2_off | Tắt | — | Toggle OFF state |
max_percent_per_order_label | Tối đa % trên đơn hàng | — | Label input config |
max_percent_per_order_hint | Mỗi đơn được trả bằng KM 2 tối đa X% phần eligible | — | Help text dưới input |
toggle_km1_km2_label | Cho phép dùng KM 1 + KM 2 cùng đơn | — | Label toggle |
toggle_km1_km2_hint | Bật = khách dùng cả 2 ví trong 1 đơn hàng | — | Help text |
allow_refund_label | Cho phép Hoàn ví KM 2 | — | Label toggle |
refund_fee_label | Phí xử lý hoàn ví (% giá mua gói) | — | Label input |
refund_period_label | Thời hạn hoàn (ngày) | — | Label input |
refund_extend_label | Gia hạn khi refund DV (ngày) | — | Label input |
wallet_target_label | Ví đích | — | Label radio trong form thẻ |
wallet_target_vnd | Ví VND (mặc định) | — | Option radio |
wallet_target_km2 | Ví KM 2 | — | Option radio |
expiry_months_label | Hạn sử dụng (tháng) | — | Label input |
column_wallet_target | Ví đích | — | Header cột list thẻ |
column_hsd | HSD | — | Header cột hạn sử dụng |
badge_km2 | KM 2 | — | Badge text |
badge_vnd | VND | — | Badge text |
payment_chip_km2 | Ví KM 2 | — | Label chip payment method |
km2_balance_label | Số dư Ví KM 2 | — | Label trong block payment |
km2_eligible_label | Eligible total | — | Label tổng item eligible |
km2_max_percent_per_order | Tối đa ({max}%) | — | Label tính giá trị max |
km2_auto_amount | Số tiền KM 2 áp dụng | — | Label auto-fill amount |
km2_auto_hint | Tự động tính = MIN(Eligible × max%, Số dư) | — | Help text |
km2_remaining | Còn lại cần trả | — | Label amount remaining |
column_km2_amount | NẠP VÍ KM 2 | — | Cột bảng SCR-03 |
summary_km2_amount | Tiền vào Ví KM 2 | — | Dòng summary panel phải SCR-03 |
installment_blocked_tooltip | Trả góp không áp dụng cho đơn có Gói Ví KM 2 | — | Tooltip chip "Trả góp" disabled |
alert_expiring_30d | Gói {package_name} còn {balance}đ — hết hạn {date} (còn {days} ngày) | — | Banner warning trong profile |
alert_expiring_7d | Gói {package_name} còn {balance}đ — hết hạn {date} (còn {days} ngày) | — | Banner danger trong profile |
refund_dialog_title | Hoàn ví KM 2 | KM2 Wallet Refund | Title dialog SCR-07 |
refund_ratio_label | Tỉ lệ hoàn | Refund ratio | Label tỉ lệ |
refund_calc_pre_fee | Hoàn theo tỉ lệ | Pre-fee refund | Label tính tỉ lệ |
refund_fee_calc | Phí xử lý hoàn ví | Processing fee | Label phí |
refund_customer_receive | KHÁCH NHẬN | Customer receives | Label tổng |
refund_zero_warning | Phí xử lý hoàn ví lớn hơn số tiền hoàn. Số dư Gói Ví KM 2 sẽ được tất toán, khách không nhận tiền. | — | Warning text khi = 0 |
refund_into_label | Hoàn vào | Refund into | Label radio |
refund_cash | Tiền mặt | Cash | Option |
refund_wallet_vnd | Ví VND của khách | Customer VND wallet | Option |
refund_bank | Chuyển khoản | Bank transfer | Option |
btn_refund_km2_wallet | Hoàn ví KM2 | Refund KM2 wallet | Nút trong SCR-06 |
btn_create_refund_request | Tạo yêu cầu hoàn tiền | Create refund request | Submit button SCR-07 |
btn_save_settings | Lưu cài đặt | Save settings | Footer SCR-01 |
btn_export | Xuất Excel | Export Excel | Report SCR-08 |
btn_enable_for_pos | Bật Ví KM 2 cho POS | Enable for POS | CTA cuối banner readiness SCR-01 |
toast_save_success | Đã lưu cài đặt Ví KM 2 | — | Toast success SCR-01 |
toast_save_fail | Không thể lưu cài đặt. Vui lòng thử lại. | — | Toast danger SCR-01 |
toast_payment_success | Đã thanh toán {tổng}đ. KM 2 áp dụng: {km2}đ. Số dư còn: {còn}đ. | — | Toast SCR-04 |
toast_payment_insufficient | Số dư Ví KM 2 không đủ. Vui lòng thử lại. | — | Toast danger SCR-04 |
toast_payment_race | Số dư đã thay đổi từ thiết bị khác. Đã tải lại số dư mới. | — | Toast 409 |
toast_refund_request_success | Đã tạo Yêu cầu hoàn tiền KM 2 #{id}. Vui lòng chờ duyệt. | — | Toast SCR-07 |
toast_refund_paid_success | Đã hoàn {x}đ cho khách. Lot {tên} đã tất toán. | — | Toast sau payment |
toast_refund_fail | Tạo yêu cầu thất bại. Vui lòng thử lại. | — | Toast danger SCR-07 |
toast_order_create_success | Đã tạo đơn nạp tiền {mã}. ZNS đã gửi cho khách (nếu bật). | — | Toast SCR-03 |
toast_order_create_debt | Đã tạo đơn nạp tiền {mã}. Đơn còn nợ {x}đ — Gói Ví KM 2 chưa kích hoạt cho đến khi thu đủ. | — | Toast SCR-03 (đơn nợ) |
toast_update_success | Đã cập nhật cho phép Ví KM 2 cho "{tên item}" | — | Toast SCR-05 |
empty_no_lots | Khách chưa có Gói Ví KM 2 nào trong Ví KM 2 | — | Empty SCR-06 |
empty_no_prepaid | Chưa có thẻ trả trước. Liên hệ Admin để tạo thẻ. | — | Empty SCR-03 |
empty_no_data | Chưa có dữ liệu | — | Empty SCR-08 |
empty_no_orders_in_range | Chưa có đơn KM 2 nào trong khoảng thời gian này | — | Empty SCR-08 với filter |
report_title | Báo cáo Ví Khuyến Mãi 2 | — | Tiêu đề report |
kpi_packages_sold | Đã bán | — | KPI label |
kpi_revenue | Doanh thu | — | KPI label |
kpi_redemption_rate | Tỉ lệ dùng | — | KPI label |
kpi_expiring_balance | Sắp cháy (30 ngày) | — | KPI label |
table_by_package | Thống kê theo Gói | — | Table title |
table_expiring | Gói sắp hết hạn (30 ngày tới) | — | Table title |
table_top_customers | Top khách dùng nhiều nhất | — | Table title |
status_active | Đang hoạt động | Active | Badge |
status_exhausted | Đã dùng hết | Exhausted | Badge |
status_expired | Đã hết hạn | Expired | Badge |
status_refunded | Đã hoàn | Refunded | Badge |
confirm_toggle_km2 | Bạn có chắc muốn cho phép thanh toán bằng Ví KM 2 cho "{tên item}"? | — | Confirm SCR-05 |
confirm_disable_km2 | Hiện có {N} khách đang giữ tổng {X}đ trong Ví KM 2. Tắt sẽ ẩn phương thức cho POS, không xoá số dư. Tiếp tục? | — | Confirm SCR-01 |
confirm_refund_zero | Phí xử lý lớn hơn số tiền hoàn. Khách KHÔNG nhận tiền nhưng số dư Gói {tên} sẽ tất toán. Tiếp tục? | — | Confirm SCR-07 |
confirm_leave_dirty | Thay đổi chưa được lưu. Bạn có muốn rời trang? | — | Confirm rời trang dirty |
validation_max_percent_per_order_range | Vui lòng nhập giá trị từ 1 đến 100 | — | Inline error SCR-01 |
validation_expiry_required | Vui lòng nhập hạn sử dụng (số tháng > 0) | — | Inline error SCR-02 |
validation_value_geq_price | Số tiền nạp vào ví phải lớn hơn hoặc bằng mệnh giá thẻ | — | Inline error SCR-02 |
validation_code_duplicate | Mã thẻ "{code}" đã tồn tại. Vui lòng dùng mã khác. | — | Inline error SCR-02 |
permission_no_settings | Bạn không có quyền cập nhật cấu hình này | — | Toast 403 SCR-01 |
permission_no_refund | Cần quyền tạo Yêu cầu hoàn tiền | — | Tooltip nút Hoàn ví KM2 |
permission_no_payment | Bạn không có quyền thanh toán đơn này | — | Toast 403 SCR-04 |
tooltip_lot_expired_locked | Lot đã hết hạn — không thể hoàn | — | Tooltip nút Hoàn disabled |
tooltip_branch_a_used_branch_b | Khách mua tại {branch_a}, đang dùng tại | — | Tooltip drawer chi tiết lot |
B6A) Ngân sách độ dài copy (Copy Length Budget)
| Surface | Max VI | Max EN | Đã kiểm |
|---|---|---|---|
| Button primary | 24 | 18 | ✅ "Tạo yêu cầu hoàn tiền" 22 ≤ 24; "Bật Ví KM 2 cho POS" 21 ≤ 24 |
| Button secondary | 28 | 22 | ✅ |
| Tab title | 16 | 12 | ✅ "Ví KM 2" 7 ≤ 16 |
| Badge | 14 | 10 | ✅ "Đang hoạt động" 13 ≤ 14; "Đã hết hạn" 11 ≤ 14 |
| Toast 1 dòng | 80 | 60 | ✅ "Đã thanh toán {tổng}đ. KM 2 áp dụng: {km2}đ. Số dư còn: {còn}đ." dynamic, vẫn dưới 80 cho range thực |
| Banner | 200 | 150 | ✅ "Hiện có {N} khách đang giữ tổng {X}đ trong Ví KM 2..." dưới 200 |
| Tooltip | 120 | 90 | ✅ |
| Empty primary | 60 | 45 | ✅ "Khách chưa có Gói Ví KM 2 nào trong Ví KM 2" 47 ≤ 60 |
| Empty secondary | 100 | 75 | ✅ |
| Error inline | 100 | 75 | ✅ "Mã thẻ "{code}" đã tồn tại. Vui lòng dùng mã khác." |
| Modal title | 50 | 40 | ✅ "Hoàn ví KM 2" 13 ≤ 50 |
| Modal body | 400 | 300 | ✅ |
| Confirm modal | 200 | 150 | ✅ confirm_disable_km2 ≤ 200 với {N}/{X} dynamic |
| Notification push | 80+120 | 60+90 | ✅ Title "Kích hoạt Gói Ví KM 2" + body 120 |
| Form field label | 30 | 22 | ✅ "Phí xử lý hoàn ví (% giá mua gói)" 33 — VƯỢT, đề xuất rút "Phí xử lý hoàn ví (%)" 24 |
| Form helper text | 80 | 60 | ✅ |
| Table column header | 18 | 14 | ⚠ "NẠP VÍ KM 2" 11 OK; "Tiền vào Ví KM 2" 18 chính xác, không vượt |
Action: rút label "Phí xử lý hoàn ví (% giá mua gói)" thành "Phí xử lý hoàn ví (%)" ở field label; giữ chú giải đầy đủ trong tooltip ⓘ.
B7) Sự kiện phân tích (Analytics)
| Sự kiện | Trigger | Properties | KPI liên quan |
|---|---|---|---|
km2_settings_saved | Admin lưu config | max_percent_per_order, allow_combine_km1, allow_refund, refund_fee_percent, refund_deadline_days, refund_extend_days | — |
km2_enabled_for_pos | Admin bấm "Bật Ví KM 2 cho POS" sau readiness | readiness_steps_passed, time_from_first_save_to_enable | Adoption |
km2_package_created | Admin tạo Gói Ví KM2 | package_code, package_name, value, value_into_wallet, expiry_months, wallet_target | — |
km2_package_updated | Admin sửa Gói | package_code, fields_changed | — |
km2_eligible_toggled | Admin bật/tắt allow_promo_wallet_2 | item_id, item_type (product/service), new_value | — |
km2_lot_sold | NV hoàn thành đơn bán Gói Ví KM2 | customer_id, package_code, qty, total_price, total_wallet_value, branch_id | Đã bán, Doanh thu |
km2_lot_activated | Khách trả lần đầu → BE tạo lot với status='active', activated_at=NOW() | lot_id, customer_id, branch_id, wallet_value, expired_at, credited_amount, initial_amount | — |
km2_payment_chip_shown | Chip Ví KM 2 render trong payment | order_id, eligible_total, balance | — |
km2_payment_selected | NV bấm chip Ví KM 2 | order_id, auto_amount, eligible_total, balance | — |
km2_payment_completed | Thanh toán đơn có KM 2 thành công | order_id, km2_amount, lots_deducted_count, remaining_balance | Tỉ lệ dùng |
km2_payment_failed | Thanh toán KM 2 fail | order_id, error_type (insufficient_balance/race_condition/network/5xx) | — |
km2_profile_card_clicked | User click card "Ví KM 2" trong profile | customer_id, total_balance, active_lots_count | — |
km2_profile_popup_viewed | Popup chi tiết lot mở | customer_id, expiring_lots_count | — |
km2_refund_dialog_opened | Mở SCR-07 dialog | customer_id, lot_id, suggested_refund | — |
km2_refund_request_created | Tạo refund_km2_wallet request | customer_id, lot_id, refund_amount, refund_method, request_id | — |
km2_refund_request_paid | Sau khi kế toán thanh toán | request_id, lot_id, amount, refund_method | — |
km2_lot_expired_cron | Cron daily đánh dấu lot expired | lot_id, customer_id, lost_balance | Sắp cháy |
km2_zns_sent | ZNS template gửi thành công | template, customer_id, lot_id | — |
km2_zns_failed | ZNS gửi fail | template, customer_id, error_code | — |
km2_report_viewed | User mở report dashboard | date_range, branch_id, role | — |
km2_report_exported | User xuất Excel | date_range, branch_id, total_rows, is_async | — |
B7A) Quy ước schema sự kiện
| Khía cạnh | Quy ước |
|---|---|
| Tên event | km2_* snake_case theo domain.action |
| Bắt buộc properties | user_id, branch_id, portal, correlation_id, feature_flag_state, client_ts, server_ts |
| Property type | string/int/bool/timestamp/enum khai báo rõ; không gửi PII raw (số CMND, CCCD); customer_id UUID |
| Sampling | 100% với event critical (sold, activated, payment_completed, refund_paid); 100% với failed events; 10% với chip_shown |
| Retention | Hot 90 ngày; warehouse 2 năm; sau aggregate |
| Correlation | correlation_id cho chuỗi event của 1 hành động (chip_shown → selected → completed) |
| Versioning | Schema thay đổi → bump event_version; backward compat 30 ngày |
| Pre-launch test | Pipe sang dev env trước launch; QA verify đủ properties |
B8) Quy tắc responsive và khả năng truy cập
B8.1) Responsive
| Breakpoint | Hành vi layout |
|---|---|
| Desktop (≥ 1024px) | Layout đầy đủ như wireframe; tables full cột; dialogs 50vw |
| Tablet (768-1023px) | Tables responsive: ẩn cột "Ghi chú" / "Branch"; dialogs 70vw; KPI 2x2; popup chi tiết lot 90vw |
| Mobile (< 768px) | Diva Admin desktop-first; POS chạy tablet ≥ 768px; KHÔNG hỗ trợ mobile < 768px |
B8.2) Accessibility / keyboard
| Khu vực | Yêu cầu |
|---|---|
| Toggle KM 2 / KM 1+KM 2 / allow_refund | Label đọc được screen reader; focus bằng Tab; Space đổi state |
| Dialog SCR-02 / SCR-07 | Focus trap; Esc đóng nếu không dirty; nếu dirty → confirm bỏ thay đổi |
| Chip Ví KM 2 | Aria-label "Ví KM 2"; trạng thái disabled có tooltip lý do (config tắt / số dư = 0 / eligible = 0) |
| Bảng lot SCR-06 | Hàng sắp hết hạn có badge text + icon, không chỉ màu; CTA Hoàn có confirm modal |
| Banner cảnh báo SCR-06 | role="alert" cho danger, role="status" cho warning |
| Lỗi validation | Inline + toast; aria-live="assertive" cho lỗi quan trọng |
B8A) Ngân sách hiệu năng UI (Performance Budget)
| Khía cạnh | Quy ước |
|---|---|
| Table virtualization | SCR-06 popup > 100 lot → virtual scroll; render visible + buffer |
| Pagination | SCR-06 mặc định 50 dòng/page (lot/deduction); SCR-08 50/page; cho phép 25/50/100 |
| "Load more" vs pagination | Pagination cho data cấu trúc; load more cho activity feed (lịch sử deduction trong drawer) |
| Lazy-load images | Avatar khách loading="lazy" |
| Initial bundle | Module wallet KM 2 < 200KB gzipped; lazy-import dialog SCR-07 |
| Time to first paint | < 1.5s với 3G yếu (POS tablet ở CN tỉnh) |
| API timeout | 30s cứng |
| Polling | SCR-06 popup polling 60s; SCR-08 không polling, manual refresh |
| Large form | Form Settings KM 2 chia card; không render hết cùng lúc nếu thêm phase sau |
| Dropdown lớn | "Tên Gói" filter trong SCR-06 < 50 options thì render hết; > 50 → searchable |
B8B) Quy ước về khả năng truy cập chuyên sâu (A11y Deep)
| Khía cạnh | Quy ước |
|---|---|
| Color contrast | Text vs background ≥ 4.5:1; large text ≥ 3:1 |
| Không dựa vào màu | Trạng thái lot dùng màu + label + icon; banner dùng màu + icon ⛔/⚠ |
| Focus visible | Outline rõ ≥ 2px; không outline: none |
| Skip link | "Bỏ qua điều hướng" cho keyboard user |
| Heading hierarchy | h1 = tên màn; h2 = card/section; h3 = subsection; không nhảy bậc |
| Form labels | Mọi input có <label> gắn for; placeholder KHÔNG thay label |
| Aria-live | polite cho toast info; assertive cho lỗi |
| Aria-describedby | Field có help text/error → aria-describedby |
| Aria-expanded | Drawer/dropdown → aria-expanded |
| Modal trap | Tab cuối quay về đầu; Esc đóng; trả focus về trigger |
| Image alt | Avatar khách alt="Ảnh đại diện {tên}" |
| Icon-only button | Nút × đóng popup aria-label="Đóng" |
| Animation | Tôn trọng prefers-reduced-motion |
B8C) Phản hồi cảm ứng / âm thanh / haptic (Tablet POS)
| Tình huống | Quy ước |
|---|---|
| Submit thanh toán thành công | Haptic nhẹ; KHÔNG phát âm thanh nếu OS silent |
| Submit thất bại | Haptic mạnh + toast danger |
| Notification ZNS gửi (admin) | Phát âm thanh nhẹ; tôn trọng setting |
| Pull-to-refresh trong popup | Haptic khi đạt threshold |
| Long-press row trong bảng lot | Haptic medium → mở context menu (Phase sau) |
| Tôn trọng setting | OS / app-level toggle âm thanh + haptic |
B9) Từ điển tooltip
| Màn | Field/Icon | Nội dung tooltip | Điều kiện hiện |
|---|---|---|---|
| SCR-01 | Input "Tối đa %" ⓘ | Mỗi đơn hàng được trả bằng KM 2 tối đa X% trên phần SP/DV bật Cho phép Ví KM 2 | Hover icon ⓘ |
| SCR-01 | Toggle "Cho phép KM 1 + KM 2" ⓘ | Bật = khách dùng đồng thời Ví KM 1 và Ví KM 2 trong cùng 1 đơn. Tắt = chỉ chọn 1 trong 2 | Hover icon ⓘ |
| SCR-01 | Input "Phí xử lý hoàn ví" ⓘ | Phí thu khi khách hoàn ví KM 2; tính theo % giá mua gói gốc, không tính trên ví giá trị | Hover icon ⓘ |
| SCR-01 | Input "Gia hạn refund DV" ⓘ | Khi hoàn đơn dịch vụ và lot đã hết hạn, hệ thống gia hạn thêm số ngày này để khách tiếp tục dùng | Hover icon ⓘ |
| SCR-01 | CTA "Bật Ví KM 2 cho POS" disabled | Hoàn tất 4 bước readiness trước khi bật cho toàn hệ thống | Hover khi disabled |
| SCR-02 | Radio "Ví đích" ⓘ | Chọn "Ví KM 2" để biến thẻ này thành Gói Ví KM2 khuyến mãi. Toàn bộ giá trị nạp sẽ vào Ví KM 2 (có hạn sử dụng) | Hover icon ⓘ |
| SCR-02 | Input "Hạn sử dụng" ⓘ | Số tháng kể từ ngày khách trả lần đầu. Sau thời hạn, số dư còn lại bị xoá | Hover icon ⓘ |
| SCR-02 | 4 field readonly khi đã có lot | Đã có lot bán ra; chỉ sửa Tên / Mô tả / Trạng thái | Hover field disabled |
| SCR-03 | Cột "NẠP VÍ KM 2" ⓘ | Số tiền sẽ vào Ví KM 2 cho khách (auto-tính theo Gói × SL) | Hover header |
| SCR-03 | Chip "Trả góp" disabled | Trả góp không áp dụng cho đơn có Gói Ví KM 2 (DEC-017) | Hover khi disabled |
| SCR-04 | "Số tiền KM 2 áp dụng" ⓘ | Tự động tính = MIN(Eligible × max%, Số dư). NV không thể chỉnh sửa thủ công | Hover icon ⓘ |
| SCR-04 | "Eligible total" ⓘ | Tổng giá trị các SP/DV bật Cho phép Ví KM 2 trong đơn này | Hover icon ⓘ |
| SCR-04 | Chip Ví KM 2 disabled | Hiện một trong: "Tính năng đang tắt" / "Số dư = 0" / "Đơn không có item eligible" | Hover khi ẩn/disabled |
| SCR-06 | Card "Ví KM 2" ⓘ | Tổng số dư các Gói đang hoạt động. Không tính lot hết hạn / đã dùng hết / đã hoàn | Hover card |
| SCR-06 | Cột "Hết hạn" đỏ | Lot sẽ hết hạn trong 7 ngày tới. Đề xuất khách đến sử dụng sớm | Luôn hiện khi ≤ 7 ngày |
| SCR-06 | Cột "Hết hạn" vàng | Lot sẽ hết hạn trong 30 ngày tới | Luôn hiện khi ≤ 30 ngày |
| SCR-06 | Cột "Đã nạp" inline "X/Y" | Khách đã trả X/Y (đơn còn nợ); lot vẫn active và dùng được với balance theo phần đã nạp | Hover cell |
| SCR-06 | Nút Hoàn ví KM 2 disabled | Lot đã hết hạn / đã hoàn / allow_refund=false / thiếu quyền refund request | Hover khi disabled |
| SCR-07 | "Tỉ lệ hoàn" ⓘ | = Giá mua gói / Giá trị ví. VD: 500K / 5.000K = 10%. Khách chỉ nhận lại theo tỉ lệ này | Luôn hiện |
| SCR-07 | "Phí xử lý hoàn ví" ⓘ | = Giá mua gói × % phí (config bởi Admin). VD: 500K × 20% = 100K | Luôn hiện |
| SCR-07 | "KHÁCH NHẬN" ⓘ | = MAX(Còn lại × Tỉ lệ - Phí, 0). Nếu phí > số hoàn → 0đ, lot vẫn tất toán | Luôn hiện |
| SCR-07 | "Hoàn vào: Ví VND" ⓘ | Hoàn vào Ví VND của khách trong Diva (không phải tài khoản ngân hàng) | Hover option |
| SCR-08 | KPI "Tỉ lệ dùng" ⓘ | = Tổng đã dùng / Tổng đã nạp × 100%. Chỉ tính lot active/exhausted/expired; không tính refunded | Hover icon ⓘ |
| SCR-08 | KPI "Sắp cháy" ⓘ | Tổng số dư các lot sẽ hết hạn trong 30 ngày tới. = tiền sẽ mất nếu khách không đến dùng | Hover icon ⓘ |
B-Microcopy) Quy ước vi-copy form
| Mẫu | Quy ước áp dụng cho package này |
|---|---|
| Required marker | * đỏ sau label (toàn bộ field bắt buộc); aria-required=true |
| Optional marker | (Tuỳ chọn) chỉ ở field "Ghi chú" SCR-03 / SCR-07 |
| Help text | Dưới field, font nhỏ; mô tả cách điền (VD: "Số tháng > 0") |
| Tooltip | Hover icon ⓘ; chi tiết phụ; KHÔNG lặp label |
| Char count | Field "Ghi chú" SCR-07 (max 500): hiện {X}/500 khi > 400; đỏ khi > 90% |
| Placeholder | Mô tả định dạng (VD: "VD: Khách yêu cầu hoàn ví KM 2 Gold"); KHÔNG thay label |
| Inline error | "Vui lòng nhập {field} hợp lệ" + lý do cụ thể |
| Inline success | Không dùng cho package này (tránh spam) |
| Auto-format SĐT | Format 0XXX XXX XXX khi blur (chỉ ở wireframe khách) |
| Auto-format tiền | 1.250.000 khi blur; raw khi gõ (input mệnh giá / số tiền nạp / max% / phí) |
| Auto-format ngày | DD/MM/YYYY; mask khi gõ; cho dán format khác và auto-convert |
| Reset field | Nút × xoá nội dung; xác nhận nếu > 50 ký tự |
B-Voice) Quy ước giọng thương hiệu
| Tình huống | Tone | Ví dụ ĐÚNG | Ví dụ SAI |
|---|---|---|---|
| Chào / hướng dẫn (admin tool) | Trung tính, chuyên môn | "Hoàn tất 4 bước để bật Ví KM 2 cho POS" | "Yo, làm 4 bước này nhé" |
| Thông báo thành công | Khẳng định, súc tích | "Đã tạo đơn nạp tiền {mã}" | "Yay, đơn đã tạo!" |
| Lỗi user-induced | Trung tính, hướng dẫn | "Vui lòng nhập giá trị từ 1 đến 100" | "Sai rồi, nhập lại đi" |
| Lỗi system | Xin lỗi nhẹ + trace | "Có sự cố từ hệ thống. Mã sự cố: {trace}. Vui lòng báo bộ phận hỗ trợ." | "Server lỗi không xác định" |
| Cảnh báo (banner sắp hết hạn) | Rõ ràng, hành động cụ thể | "Gói Gold #1 còn 2.500.000đ sẽ hết hạn 15/04/2026 (còn 22 ngày)" | "Cẩn thận, sắp hết hạn" |
| Confirm destructive | Nói rõ tác động | "Tắt sẽ ẩn phương thức cho POS, không xoá số dư. Tiếp tục?" | "Bạn có chắc không?" |
| Hỏi khách (qua ZNS) | Lễ phép, ấm áp | "Bạn đã kích hoạt 2 Gói Gold... Hãy đến spa sử dụng nhé!" | "Anh/chị đến dùng đi" |
| Nội dung POS | Trung tính, chuyên môn | "Số dư Ví KM 2: 4.600.000đ" | "Còn xài được 4tr6" |
| Cấm | Rõ ràng, có lý do | "Trả góp không áp dụng cho đơn có Gói Ví KM 2 (theo cấu hình DEC-017)" | "Không cho trả góp" |
Quy tắc chung áp dụng cho package này:
- KHÔNG dùng emoji trong copy POS / admin
- KHÔNG viết tắt "k", "đc", "trc" trong UI (cho phép trong ZNS nếu spa Việt thật quen dùng)
- KHÔNG dùng English raw ("Submit", "Cancel", "OK", "Confirm", "Loading", "No data", "Error", "Success") làm text hiển thị
- "Bạn" cho NV nội bộ (admin, sale); "Quý khách" / "Bạn" cho ZNS gửi khách (theo style ngành spa)
- Câu chủ động > bị động; ngắn gọn
B-i18n) Quy ước quốc tế hoá / format
| Khía cạnh | Quy ước Diva (vi-VN) áp dụng |
|---|---|
| Ngày | DD/MM/YYYY (KHÔNG MM/DD) — VD 24/03/2026 |
| Ngày + giờ | DD/MM/YYYY HH:mm (24h) — VD 24/03/2026 14:30 |
| Giờ | HH:mm (24h) |
| Timezone | Asia/Ho_Chi_Minh (UTC+7); expired_at là end_of_day |
| Tiền VND | 1.250.000đ (chấm ngàn, "đ" cuối, không khoảng cách) — VD 5.000.000đ |
| Số thập phân | 12,5 (dấu phẩy thập phân); 12.500,50 cho large + decimal |
| Phần trăm | 37,50% (2 thập phân) — KPI "Tỉ lệ dùng" |
| SĐT | 0909 123 456 (cách 4-3-3) |
| Tên KH | Capitalize từng chữ; tôn trọng dấu (Đặng Thị Ánh Sương) |
| Plural | "1 Gói Ví KM 2" / "5 Gói Ví KM 2" (VI không inflection) |
| Truncation | Cắt theo từ + … — VD "Gói Premium Diamond V…" |
| Tên file export | Slug không dấu — bao-cao-vi-km2_* |
| Sort tiếng Việt | Theo bảng chữ cái VN; collation vi_VN cho tên Gói/khách |
Package Day-1 KHÔNG cần EN i18n; VI là ngôn ngữ chính của Diva.
B-Versioning) Quy ước UI cho feature flag / staged rollout
Áp dụng vì package có
wallet_km2_config.disabledmặc định bật, dùng như feature flag.
| Tình huống | Quy ước UI |
|---|---|
disabled=true (Day-1 sau migration) | Toàn bộ UI KM 2 ẩn cho POS (chip không render); admin vẫn thấy Settings để cấu hình; ZNS template không trigger |
disabled=false | UI KM 2 hiện đầy đủ cho POS theo permission |
| Staged rollout 0-100% | Day-1 chỉ có on/off global; chưa có % rollout (deferred) |
| A/B test | Không áp dụng |
| Kill switch | Admin có toggle tắt qua SCR-01; FE polling 60s khi POS đang mở payment để check; tắt → ẩn chip ngay |
| Migration period | Old POS không có KM 2 + new POS có KM 2 cùng tồn tại trước migration; sau migration toàn bộ POS có code mới |
| Rollback | Disable wallet_km2_config.disabled=true qua SCR-01 → user thấy UI cũ ngay sau refresh; số dư không mất; không có nháp để giữ |
B-Help) Quy ước điểm hỗ trợ
| Khía cạnh | Quy ước áp dụng |
|---|---|
| Help link cuối trang Settings | "Cần hỗ trợ?" → mở runbook URL ở docs.diva-group/vi-km-2 |
| Tooltip ⓘ | Toàn bộ field nghiệp vụ phức tạp có icon ⓘ — đã liệt kê ở B9 |
| Onboarding tour | Lần đầu Admin vào SCR-01 → tour 4 bước readiness; có "Bỏ qua" + "Xem lại sau" |
| Empty state with hint | SCR-06 empty: "Khách chưa có Gói Ví KM 2 nào trong Ví KM 2" + link "Hướng dẫn bán Gói Ví KM 2" |
| Error with support hint | Error 5xx kèm correlation_id + "Báo bộ phận hỗ trợ với mã: {id}" |
| Contact support | Floating button góc phải dưới (toàn admin) hoặc menu "Hỗ trợ" header |
| Feedback channel | "Phản hồi tính năng" link cuối footer |
B-FR012) Tích hợp KM 2 vào màn hình hiện có (chỉ delta)
Các màn dưới đây đã tồn tại — chỉ ghi thay đổi (delta), không vẽ wireframe mới.
Phase 1
Đơn mỹ phẩm — thanh toán (CosmeticOrderFormPayment)
- Reuse:
CosmeticOrderFormPayment.tsx+CosmeticOrderFormPaymentTable.tsx - Delta: Thêm chip "Ví KM 2" trong payment methods (giống pattern KM 1). Auto-fill max%, FIFO, validate eligible — logic Y HỆT SCR-04 đơn DV.
CosmeticOrderPaymentFormMultiple.tsxthêmwallet_promotion_2_amountvào popup multi-payment. - Bằng chứng: CosmeticOrderFormPayment.tsx (reuse OrderPaymentMethodRadio)
- Vị trí update: chip mới chèn cạnh chip Ví KM 1; block calc xếp dưới hàng chip
- Vì sao đủ: dùng cùng pattern KM 1; không phá layout đơn
Đơn sản phẩm — flag eligible (ProductOrder)
- Reuse:
ProductOrderItems.tsx,ProductOrderCreate.tsx - Delta: Thêm check
allow_promo_wallet_2per item (giốngallow_promo_wallet). InjectcustomerWalletValue["VND_PROMOTION_2"]vào props. - Vị trí update: check eligibility trong logic component, không thay đổi UI nếu không có item eligible
Hoá đơn in (InvoiceTemplate)
- Reuse:
InvoiceTemplatePopupPrint.tsx+InvoiceTemplatePreview.tsx - Delta: Payment method mapping thêm
"wallet_promotion_2"→ label "Ví KM 2" + biếnpayment_wallet_promotion_2_code. Hiển thị cùng cách KM 1. - Vị trí update: dòng "Ví KM 2: {amount}đ" chèn ngay dưới dòng "Ví khuyến mãi" trong block Phương thức thanh toán
- Bằng chứng hiện trạng:
1768552468000_add_column_print_invocie_template/up.sqlđã có biến KM 1 - Không kỳ vọng đổi màn khác: chỉ template; logic invoice không đổi
Fund / Quỹ (FundTable, FundInvoicePopup)
- Reuse:
FundTable.tsx,FundInvoicePopup.tsx - Delta: Thêm cột "Vào Ví KM 2" conditional (chỉ hiện khi tổng
wallet_promotion_2_amount > 0trong range). Giống cột "Vào ví khuyến mãi" KM 1. - Vị trí update: cột chèn ngay sau cột "Vào ví khuyến mãi"
- Vì sao đủ: không gộp KM 1 + KM 2; semantics tách biệt
Phase 2
Withdraw / Refund detail (WithdrawRequestDetail)
- Reuse:
WithdrawRequestDetail.tsx - Delta: Payment method label thêm
"wallet_promotion_2"→ "Ví KM 2"; behaviorrefund_km2_wallethiển thị badge "Hoàn ví KM 2" thay vì "Hoàn đơn" - Vị trí update: label trong detail block; không đổi layout
Report DV/NV hiện có (ServiceReportTable, ServiceGroupReportTable, EmployeeRevenueReportDoughnutChart)
- Reuse: các component report hiện hữu
- Delta: Thêm cột/segment
wallet_promotion_2_revenue(giốngwallet_promotion_revenue). Doughnut chart thêm 1 segment "Ví KM 2" - Vị trí update: cột chèn cạnh KM 1; segment thứ N+1 trong chart
- Vì sao đủ: KM 2 paid amount cũng là wallet-paid (không tính thực thu)
CRM Customer (CustomerEWalletInformation)
- Reuse:
CustomerEWalletInformation.tsx(CRM portal section) - Delta: Thêm dòng "Ví KM 2: {balance}đ" trong section E-wallet, conditional khi khách có wallet
VND_PROMOTION_2 - Vị trí update: dòng chèn ngay dưới dòng "Ví khuyến mãi"
Negative Invoices (NegativeInvoicesTable.tsx) — Phase 1
- Reuse:
NegativeInvoicesTable.tsx(bảng hoàn đơn âm hiện có; cột "Tiền nạp vào ví khuyến mãi" hiển thịwallet_promotion_amountâm) - Delta: Thêm cột "Vào Ví KM 2" conditional khi range có invoice negative dùng KM 2
- Vị trí update: cột chèn ngay cạnh cột KM 1
- Vì sao đủ: Khi hoàn đơn DV/SP thanh toán bằng KM 2, invoice negative có
wallet_promotion_2_amountâm — cần hiển thị tách biệt với KM 1 - Evidence:
NegativeInvoicesTable.tsxline renderwallet_promotion_amount(cột KM 1 hiện hữu)
SMS Template vnd_wallet_balance (notification_template table) — Phase 1
- Reuse: Template hiện có placeholder
{{.wallet_amount}},{{.wallet_promotion_amount}}— gửi sau invoice complete bằng ví. Hiện text:"Vi chinh con lai: {{.wallet_amount}}, KM con lai: {{.wallet_promotion_amount}}. ND: {{.note}}" - Delta: Thêm placeholder
{{.wallet_promotion_2_amount}}(conditional). Content mới:"Vi chinh: X, Vi KM1: Y, Vi KM2: Z. ND: ..."— dòng KM 2 chỉ render khibalance > 0 - Vị trí update: template
contentfield trongnotification_template WHERE name = 'vnd_wallet_balance' - Vì sao đủ: Khách dùng KM 2 (hoặc mix KM 1 + KM 2 khi
allow_combine_km1=true) → SMS hiện tại chỉ báo balance KM 1 → khách bối rối, NV phải nói miệng số dư. Update đảm bảo thông tin đầy đủ. - Migration: xem dev-spec C5
M-KM2-SMS-TEMPLATE(idempotent + pre-check anchor theo CLAUDE.md Pitfalls Map) - Đồng bộ: Cũng update template
prepaid_card(gửi sau khi nạp Gói) — chèn placeholder tương tự
Affiliate Configuration (AffiliateConfiguration.tsx) — Phase 1
- Reuse: UI matrix tại
/s/internal-settings/affiliate→ tab "Cấu Hình Chung" → block "Cấu hình hình thức nhận hoa hồng". Component auto-render mọipayment_methodrow có trong DB - Delta: Migration thêm
payment_methodrowwallet_promotion_2+ seedaffiliate_config3 row mặc định FALSE:('service', 'wallet_promotion_2', FALSE)('cosmetic', 'wallet_promotion_2', FALSE)('prepaid', 'wallet_promotion_2', FALSE)
- Vị trí update: không sửa code FE; matrix tự render cột "VÍ KHUYẾN MÃI 2" sau khi migration chạy
- Vì sao FALSE mặc định: Đồng bộ với default production hiện hành — Diva đang setup
walletvàwallet_promotionởenable=FALSEcho cả 3 order_kind. KM 2 inherit nguyên trạng (DEC-030) - Hành vi runtime: Tuân theo
affiliate_configmatrix giống KM 1 — kết quả commission per invoice phụ thuộc ô(order_kind, payment_method_id)đang TRUE/FALSE tại thời điểm invoice complete. Spec KHÔNG prescribe hành vi cụ thể "ví có/không có commission" — đó là quyết định vận hành admin - Admin override: Admin tự bật/tắt từng ô qua UI nếu muốn promo cho CTV; spa chịu trách nhiệm policy
App khách Flutter — Tab Ví KM 2 + Home widget (SCR-09 — Phase 1)
- Reuse: Pattern hiện hữu của tab "Ví KM 1" trong
diva-flutter/customer/lib/presentation/modules/wallet/:- Repository:
wallet_repository.impl.dart(getBalance(WalletType.promotion)) - Bloc:
wallet_bloc.dart(filter pattern) - Page:
listing/views/pages/promotion_wallet_page.dart - Enum:
core/lib/data/models/balance.dart→WalletType.promotion → 'VND_PROMOTION' - Home:
main/home_page/views/ui_parts/home_wallet.dart(4 ví hiện có)
- Repository:
- Delta:
- Thêm
WalletType.promotion2 → 'VND_PROMOTION_2'vào enum balance model - Thêm tab "Ví KM 2" vào
wallet_screen.dart(parity với KM 1 — list lot + balance + transaction history theo loại) - Thêm card "Ví KM 2" vào
home_wallet.dartconditional khibalance > 0 - Repository fetch balance qua action
get_customer_km2_balance(DEC-029, realtime) hoặc querywallettheowallet_type_id='VND_PROMOTION_2'(cached) - Transaction history dùng query
wallet_km2_lot_deductioncho khách (filter theocustomer_idtừ auth context)
- Thêm
- Vị trí update: thêm route + 2 component mới; home_wallet thêm 1 card
- Vì sao đủ: Khách phải XEM được Ví KM 2 trên app để verify số dư (DEC-029 + ASM-001). Phase 1 read-only — khách không tự mua Gói KM 2 trên app (vẫn ASM-001), chỉ xem.
- Out-of-scope Phase 1: khách tự mua Gói KM 2 in-app — backlog Phase 2+ (cần flow checkout, validation payment gateway mobile)
Order Commission Item (OrderCommissionItem.tsx) — Phase 1
- Reuse: Component hiện hữu filter
wallet_type_id === "VND_PROMOTION"để hiển thị row commission của khách trong đơn - Delta: Mở rộng filter để include cả
"VND_PROMOTION_2":ts// Trước: status === "S" && wallet_type_id === "VND_PROMOTION" // Sau: status === "S" && (wallet_type_id === "VND_PROMOTION" || wallet_type_id === "VND_PROMOTION_2") - Vì sao cần: Mặc định Migration 3 seed
affiliate_config(*, wallet_promotion_2, FALSE)→ không sinh row commission. Nhưng nếu admin bật ô qua UI matrix → BE sinh row commission vớiwallet_type_id='VND_PROMOTION_2'→ UI hiện tại sẽ không hiển thị row này. Update filter để chuẩn bị sẵn cho khả năng admin enable - Vị trí update: Một dòng filter trong component; không đổi layout/UX
- Cost: ~1 dòng code, 0 ảnh hưởng performance, 1 TC regression
KHÔNG thay đổi UI
OrderCard.tsx+CustomerOrderHistory.tsx— card không breakdown payment method- Dashboard (
SalesCards,RevenuePercentageByPaymentMethod) — Phase 3 rank.graphql,profile.graphql— KM 2 áp dụng cùng logic rank/loyalty như KM 1 (ASM-005)PolicyGroupForm.tsx,PolicyGroupTable.tsx,PolicyGroupLabelSettings.tsx— % hoa hồngpromotion_wallet_percentdùng chung KM 1 + KM 2; gate quaaffiliate_config(DEC-030); Phase 1 KHÔNG thêm cộtpromotion_wallet_2_percentCustomerRevokedReferralForm.tsx— Hành vi truy thu commission tuân theoaffiliate_config(DEC-030); 2 checkbox hiện có (Ví KM 1 + Ví CTV) đủ. Phase 1 KHÔNG thêm checkbox truy thu KM 2 — sẽ cân nhắc Phase 2 nếu admin bật(service|cosmetic, wallet_promotion_2)RankCreate.tsx— KM 2 cùng logic rank như KM 1 (ASM-005); KHÔNG cần fielddiscount_value_wallet_promotion_2CustomerAffiliateCards.tsx(affiliate report) — Phase 1 báo cáo affiliate giữ chỉ KM 1; KM 2 nếu sinh commission sẽ phụ thuộc admin bật matrix — Phase 2 cân nhắc bổ sung
Bằng chứng thay đổi màn hình (cho mỗi delta)
| Màn | Bằng chứng hiện trạng | Thay đổi kỳ vọng | Vị trí update | Vì sao đủ | Không kỳ vọng đổi |
|---|---|---|---|---|---|
CosmeticOrderFormPayment | Đã có chip KM 1, pattern OrderPaymentMethodRadio | Thêm chip Ví KM 2 + block calc | Sau chip KM 1 | Cùng pattern, không phá layout | Logic flat-rate KM 1, FIFO KM 2 độc lập |
InvoiceTemplatePopupPrint | Biến payment_wallet_promotion_code | Thêm biến payment_wallet_promotion_2_code | Dòng dưới KM 1 | Template hiện hữu hỗ trợ | Layout in không đổi |
FundTable | Cột "Vào ví khuyến mãi" hiện hữu | Cột "Vào Ví KM 2" conditional | Cạnh cột KM 1 | Conditional column pattern | Cột thực thu/cash giữ nguyên |
WithdrawRequestDetail | Hiển thị behavior refund_order / refund_order_cosmetic | Thêm behavior refund_km2_wallet label "Hoàn ví KM 2" | Label trong detail | Behavior mới đăng ký vào màn hiện có | Không tạo route detail mới |
ServiceReportTable | Cột wallet_promotion_revenue | Cột wallet_promotion_2_revenue conditional | Cạnh cột KM 1 | Pattern conditional | Tổng thực thu giữ |
EmployeeRevenueReportDoughnutChart | Segment KM 1 | Segment KM 2 conditional | Sau KM 1 | Pattern segment | Segment khác giữ |
CustomerEWalletInformation (CRM) | Dòng "Ví khuyến mãi" | Dòng "Ví KM 2" conditional | Dưới KM 1 | Conditional row | Dòng khác giữ |
B-Tình huống cá biệt (12 nhóm chuẩn G1-G12)
Nhóm G1 — Quyền và phân quyền
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G1.1 | Admin được cấp internal_configuration:update giữa session | Refetch sau ≤ 60s; menu Ví KM 2 trong Settings hiện ngay sau refresh permission |
| G1.2 | Sale POS bị thu hồi service_order:payment giữa session | Request kế tiếp 401/403; chip Ví KM 2 ẩn sau refetch; toast "Quyền của bạn đã thay đổi" |
| G1.3 | Sale chuyển CN giữa session (multi-branch) | Cache invalidate; SCR-06 chỉ hiện khách trong CN mới |
| G1.4 | Admin gán quyền customer_management:view_all cho POS-only role | Backend hard-deny vì role is_pos_only=true; log warning |
| G1.5 | User multi-role (Manager + Kế toán) | Effective permission = union (refund_request_management_submenu:* + customer_management:access) |
| G1.6 | Bị thu hồi refund_request_management_submenu:create lúc dialog SCR-07 đang mở | Submit nhận 403 → toast + đóng dialog; lot không bị tạo request |
Nhóm G2 — Lifecycle và đồng thời
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G2.1 | 2 Sale POS cùng thanh toán cho 1 khách | BE SELECT FOR UPDATE lot; user 2 nhận 422; toast + reload số dư |
| G2.2 | Lot bị scheduler expire khi user đang xem SCR-06 | Polling 60s refresh; banner cập nhật từ "đang HĐ" sang "đã hết"; bảng row chuyển section |
| G2.3 | Lot bị refund_request paid khi user đang xem SCR-06 | Tương tự — polling cập nhật |
| G2.4 | User mở 2 tab cùng customer profile | BroadcastChannel cảnh báo; không khoá; cả 2 tab cùng đọc |
| G2.5 | Mobile + desktop cùng khách | Sync qua server; KHÔNG conflict (chỉ 1 device write) |
| G2.6 | Sale submit thanh toán đúng lúc cron 00:05 chuyển lot expired | BE check expired_at > NOW() trong tx; nếu race → skip lot, FIFO chuyển |
| G2.7 | Manager bấm Hoàn ví KM 2 đúng lúc lot vừa exhausted (do user khác thanh toán) | BE 422; modal "Lot đã được dùng hết. Vui lòng tải lại." |
| G2.8 | 2 Manager cùng tạo refund request cho 1 lot | Đầu tiên thắng; thứ 2 nhận 422 "Lot đã có yêu cầu hoàn đang xử lý" |
Nhóm G3 — Mạng và độ tin cậy
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G3.1 | Mất mạng giữa lúc submit SCR-04 | Disable nút, hiện "Đang chờ kết nối…"; auto retry với cùng client_request_id; idempotent |
| G3.2 | Mạng yếu (response > 5s) | Spinner + "Mạng yếu, đang xử lý..." + nút Huỷ sau 10s |
| G3.3 | Server 500/502 khi tạo đơn SCR-03 | Toast "Có sự cố từ hệ thống. Mã: {trace_id}"; user báo support |
| G3.4 | Rate limit 429 (tạo request liên tục) | Banner "Bạn đã thao tác quá nhanh. Vui lòng thử lại sau {N}s" + đếm ngược |
| G3.5 | DNS / SSL fail | Page lỗi tĩnh + nút "Thử lại" |
| G3.6 | Conflict 409 sau retry | Modal 3 lựa chọn không áp dụng cho package này (config save đơn giản); chỉ "Tải lại" |
| G3.7 | Network mất khi đang multi-payment popup | Đóng popup giữ form chính; hiện toast retry |
Nhóm G4 — Dữ liệu biên / boundary
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G4.1 | Nhập max% = 0 hoặc 101 | Inline error "Vui lòng nhập giá trị từ 1 đến 100" |
| G4.2 | Số tiền KM 2 cực lớn (> 10 tỷ trong test) | Format 10.000.000.000đ; KHÔNG scientific notation |
| G4.3 | Tên Gói có ký tự đặc biệt (Gói Đặng Thị Ánh Sương) | Render đúng dấu; sort theo collation VN |
| G4.4 | Emoji trong field "Ghi chú" SCR-07 | Cho phép; không cảnh báo (admin tool) |
| G4.5 | RTL text (Ả-rập) trong tên khách | Render được; không phá layout (hiếm) |
| G4.6 | IME composing tiếng Việt khi gõ Ghi chú | KHÔNG submit khi đang compose; FE bắt event |
| G4.7 | Decimal precision tỉ lệ dùng (KPI) | 2 chữ số sau phẩy: 37,50% |
| G4.8 | Date 29/02 cho expired_at | Validate đúng (leap year); cron xử lý đúng |
| G4.9 | DST / múi giờ — Asia/Ho_Chi_Minh không có DST | Consistent |
| G4.10 | Year boundary (lot bán 31/12, hết hạn 30/06 năm sau) | Sequence reset đúng; report cross-year cho phép |
| G4.11 | Mệnh giá thẻ = 0 hoặc âm | Inline error "Mệnh giá phải lớn hơn 0" |
| G4.12 | expiry_months = 1000 (24 năm) | Cho phép; cảnh báo soft "Hạn sử dụng dài bất thường" |
| G4.13 | Số dư lot = 1đ (rất nhỏ) | Hiển thị 1đ đúng; FIFO trừ nếu đủ |
| G4.14 | Khách có 100+ lot | SCR-06 popup pagination 50/page; performance OK |
Nhóm G5 — Hành vi user lệch chuẩn
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G5.1 | Admin đóng tab SCR-01 khi đang dirty | Trình duyệt warn "Thay đổi chưa được lưu" |
| G5.2 | Sale refresh giữa lúc đang submit SCR-04 | Idempotency key bảo vệ; sau refresh nhận lại result đã tạo |
| G5.3 | Sale back browser sau submit SCR-03 thành công | Hiện màn list, không hiện form cũ |
| G5.4 | Sale tap nhiều lần CTA "Thanh toán" | Idempotent + spinner ngay; backend dedupe theo client_request_id |
| G5.5 | Sale dán dữ liệu format lạ vào input số | Auto-strip non-digit; nếu không parse được → toast hướng dẫn |
| G5.6 | Manager bấm Hoàn ví KM 2 → đóng dialog không submit | Confirm "Bỏ thay đổi?" nếu đã đổi method/note; không tạo request |
| G5.7 | Sale chọn Gói Ví KM 2 → chọn "Trả góp" → bấm Tạo Mới | Chip "Trả góp" disabled, không cho click; nếu BE bị bypass → 422 |
Nhóm G6 — In / Xuất / Tài liệu
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G6.1 | Máy in offline khi in hoá đơn có KM 2 | Toast "Không tìm thấy máy in" + cho tải PDF |
| G6.2 | In thiếu trang giữa chừng | Cho phép in lại; không có version stamp riêng cho KM 2 |
| G6.3 | Sửa đơn sau khi đã in (đặc biệt hoàn KM 2) | Tạo bản v2 invoice; bản cũ chuyển superseded |
| G6.4 | Export SCR-08 > 50.000 dòng | Chia file _part1.xlsx, _part2.xlsx; thông báo qua notification |
| G6.5 | Export khi user offline | Queue + thông báo khi xong |
| G6.6 | Tên file export có ký tự đặc biệt | Slug không dấu đúng |
Nhóm G7 — Migration / Legacy
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G7.1 | Khách có wallet KM 1 cũ + bắt đầu mua KM 2 | KM 1 và KM 2 hoàn toàn độc lập; UI hiện cả 2 dòng trong card stats |
| G7.2 | Đơn hàng cũ trước khi bật KM 2 | Không hồi cứu; chỉ đơn mới sau enable mới có chip KM 2 |
| G7.3 | Format số / ngày cũ trong invoice template legacy | Auto-convert; warn nếu không parse được |
| G7.4 | Soft-deleted Gói Ví KM 2 (Admin xoá) vẫn được lot reference | Hiển thị "Đã xoá" badge ở SCR-02 list; lot vẫn dùng được (snapshot package_name) |
| G7.5 | Gói Ví KM 2 sửa giá / mệnh giá sau khi đã có lot | Không ảnh hưởng lot cũ (snapshot); lot mới dùng giá mới |
| G7.6 | Migration seed config sau deploy | Default disabled=true; UI hiện banner "Đang tắt" + readiness 4 bước |
Nhóm G8 — Thiết bị / nền tảng
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G8.1 | Admin zoom 200% trên desktop | Layout không vỡ; field không bị ẩn |
| G8.2 | Dark mode OS | Diva Admin có theme; tôn trọng setting nếu app hỗ trợ |
| G8.3 | Browser cũ (IE / Edge legacy) | Banner "Trình duyệt không hỗ trợ. Vui lòng dùng Chrome/Edge mới" |
| G8.4 | Tablet POS portrait/landscape | Responsive; lock landscape nếu cần (POS thường dùng landscape) |
| G8.5 | Print preview với CSS broken | CSS print riêng test; in lại pass QA |
| G8.6 | Tablet POS connection chậm 3G | Giao diện vẫn render được < 1.5s; spinner cho fetch |
Nhóm G9 — Thời gian / Múi giờ
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G9.1 | Lot mua ở Asia/Ho_Chi_Minh, hiển thị cho user khác CN | Server canonical UTC; hiển thị Asia/Ho_Chi_Minh cho user |
| G9.2 | Cron 00:05 chạy chậm vượt cutoff | Skip với log; không double-run |
| G9.3 | Year-end report bridge 2 năm (KPI Doanh thu cross 2025-2026) | Filter cho phép cross-year |
| G9.4 | expired_at end_of_day 23:59:59 → cron 00:05 next day chạy | Lot expired đúng; thanh toán sau 00:05 không trừ lot này |
| G9.5 | DST không áp dụng VN | Consistent timezone |
Nhóm G10 — Bảo mật / Audit
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G10.1 | User cố mở lot/deduction qua API GraphQL trực tiếp | Action get_customer_km2_lots check branch_mode + customer scope; bypass FE 401/403 |
| G10.2 | Bypass FE qua DevTools sửa amount KM 2 trước submit | Backend luôn re-validate MIN(eligible × max%, balance); không tin FE |
| G10.3 | Brute force tạo request hoàn ví | Rate limit 10 req/phút/user; banner đếm ngược |
| G10.4 | Export hàng loạt từ tool ngoài qua API | API có rate limit + audit log + cảnh báo nếu > 5 export/giờ |
| G10.5 | Idempotency key bị reuse có chủ đích | BE dedupe theo client_request_id; trả result đã tạo |
| G10.6 | Audit policy_snapshot trong request | wallet_km2_config lúc tạo lưu JSON; không thay đổi sau approve |
Nhóm G11 — Onboarding / first-time
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G11.1 | Admin lần đầu vào SCR-01 | Tour 4 bước readiness + "Bỏ qua" / "Xem lại sau" |
| G11.2 | Sale lần đầu thấy chip Ví KM 2 | Tooltip auto hiện 1 lần "Chip mới — KM 2 áp dụng tối đa {max%}" |
| G11.3 | Master data Gói Ví KM 2 thiếu khi vào POS | SCR-03 dropdown empty + cảnh báo "Cần cấu hình Gói Ví KM 2 trước" + deeplink |
| G11.4 | Khách lần đầu được kích hoạt KM 2 | ZNS gửi (nếu bật); SCR-06 hiện tooltip "Khách vừa kích hoạt {N} Gói" |
Nhóm G12 — Pháp lý / Compliance
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G12.1 | Retention wallet_km2_lot hết hạn theo chính sách | Move sang archive sau 5 năm; không xoá hard |
| G12.2 | Audit query bị từ chối (kế toán cố access lot khách ngoài branch) | Backend hard-deny + log warning; thông báo Admin |
| G12.3 | GDPR/PDPA right to forget cho 1 khách | Process qua workflow riêng (không thuộc scope KM 2); không xoá thẳng wallet_km2_lot |
| G12.4 | Legal hold trên wallet_km2_lot | Khoá xoá/sửa; banner "Đang giữ pháp lý" trong SCR-06 (nếu áp dụng) |
| G12.5 | Audit refund_request có chứng từ | policy_snapshot + refund_method + reviewer info lưu đầy đủ |
B-Trường hợp cá biệt theo SCR (concrete spa-context cases)
Mục đích: bổ sung các tình huống biên cụ thể theo SCR, dữ liệu thật ngành spa, ngoài 12 nhóm chuẩn G1-G12. QA dùng làm seed test case trực tiếp.
Nhóm SCR-01 + SCR-02: Settings + tạo Gói Ví KM 2
| # | Tình huống | Hành vi kỳ vọng | QA TC |
|---|---|---|---|
| S01-01 | Admin nhập max_percent_per_order = 0 | Inline error Vui lòng nhập giá trị từ 1 đến 100; nút Lưu disabled | TC-001-02 |
| S01-02 | Admin nhập max_percent_per_order = 150 | Inline error tương tự | TC-001-03 |
| S01-03 | Admin nhập max_percent_per_order = 100 (boundary trên) | Hợp lệ; toast Đã lưu cài đặt Ví KM 2 | TC-001-04 |
| S01-04 | Admin nhập max_percent_per_order = 1 (boundary dưới) | Hợp lệ | TC-001-05 |
| S01-05 | Admin tắt Ví KM 2 trong khi 87 khách đang giữ tổng 45.250.000đ | Modal warning với số liệu thật; chọn "Tiếp tục tắt" → payment chip ẩn cho POS, số dư khách giữ nguyên cho đến khi bật lại | TC-001-DISABLE-WITH-BALANCE |
| S01-06 | Admin nhập refund_fee_percent = 100 | Hợp lệ; mọi lot hoàn ví KM2 sẽ có khách nhận = 0đ (toàn bộ về phí) | TC-009-FEE-100 |
| S01-07 | Admin nhập refund_deadline_days = 0 | Inline error Vui lòng nhập số ngày lớn hơn 0 | TC-009-PERIOD-0 |
| S02-01 | Admin tạo Gói "VE_GOLD" với value=500.000đ, value_into_wallet=300.000đ | Inline error Số tiền nạp vào ví phải lớn hơn hoặc bằng mệnh giá thẻ | TC-002-INVALID-VALUE |
| S02-02 | Admin tạo Gói "VE_GOLD" trùng mã đã có | Inline error Mã thẻ "VE_GOLD" đã tồn tại. Vui lòng dùng mã khác. | TC-002-CODE-DUP |
| S02-03 | Admin sửa Gói "Gói Gold 500K" đang có 12 lot active của khách | 4 field (Mệnh giá / Số tiền nạp / Ví đích / Hạn sử dụng) readonly + tooltip Đã có lot bán ra; chỉ sửa Tên / Mô tả / Trạng thái | TC-002-LOCKED-FIELDS |
| S02-04 | Admin tạo Gói chọn Ví đích = Ví KM 2 nhưng để trống Hạn sử dụng | Inline error Vui lòng nhập hạn sử dụng (số tháng > 0) | TC-002-EXPIRY-REQUIRED |
| S02-05 | Admin tạo Gói chọn Ví đích = VND | Field Hạn sử dụng ẩn; pass validation theo behavior thẻ trả trước hiện hữu | TC-002-VND |
| S02-06 | Admin đổi Ví đích từ KM 2 → VND giữa lúc đang điền form | Field Hạn sử dụng ẩn ngay (giữ value cũ trong state, BE bỏ qua khi submit VND) | TC-002-TOGGLE-WALLET-TARGET |
| S02-07 | 2 Admin lưu config SCR-01 cùng lúc | Last-write-wins + 409 cho người sau → modal Cấu hình đã thay đổi từ thiết bị khác. Vui lòng tải lại. | TC-001-CONCURRENCY |
| S02-08 | Admin ngưng Gói Ví KM 2 đang có lot active | Modal Ngưng "Gói Gold 500K" sẽ ẩn khỏi POS bán Gói Ví KM 2. Số dư khách hiện có không bị ảnh hưởng. Tiếp tục? → ngưng → POS dropdown ẩn Gói; lot active của khách giữ nguyên | TC-002-DEACTIVATE |
Nhóm SCR-03: Bán Gói Ví KM 2
| # | Tình huống | Hành vi kỳ vọng | QA TC |
|---|---|---|---|
| S03-01 | NV bán Gói Gold cho khách Nguyễn Thị Lan chưa có Ví KM 2 | Hệ thống tự tạo wallet VND_PROMOTION_2 cho khách (qua action walletBalances reuse); tạo lot ngay | TC-003-FIRST-TIME |
| S03-02 | NV bán Gói Silver × qty = 10 cho 1 khách | Tạo 10 bản ghi wallet_km2_lot riêng biệt, mỗi lot 1.5tr với expired_at riêng (cùng ngày nếu trả 1 lần); ZNS gửi 1 lần với qty=10 | TC-003-QTY-10 |
| S03-03 | NV bán đơn 11tr nhưng khách trả lần 1 chỉ 5tr (45,45%) | Tạo đơn prepaid_order.status='pending' còn nợ 6tr; tạo wallet_km2_lot với status='active', credited_amount = 45,45% × 10tr = 4.545.500đ, balance = 4.545.500đ, activated_at = NOW(), HSD tính từ lần trả đầu; ZNS gửi lần 1 với balance đã nạp | TC-003-DEBT |
| S03-04 | NV bán đơn 11tr, khách trả 5tr lần 1 (45,45%), 4tr lần 2 (36,36%), 2tr lần 3 (18,19%) | Lần 1: tạo lot status active, balance = 45,45% × 10tr = 4.545.500đ; ZNS gửi lần 1. Lần 2: balance += 36,36% × 10tr = 3.636.400đ = 8.181.900đ. Lần 3: balance = 10.000.000đ tròn (rounding cuối). Hạn sử dụng tính từ lần trả đầu | TC-003-MULTI-PAYMENT |
| S03-05 | Mất mạng lúc NV bấm Tạo Mới | Disable nút, hiện "Đang chờ kết nối…"; auto retry với cùng client_request_id (UUID gen client); BE idempotent → trả result đã tạo nếu duplicate | TC-003-NETWORK |
| S03-06 | NV mix 1 dòng "Thẻ trả trước 10tr" + 2 dòng "Gói Gold KM 2" trong cùng 1 đơn | Cột "NẠP VÍ KM 2" hiện; dòng VND có cột KM 2 = 0; dòng KM 2 có cột DIVA + KM 1 = 0; sidebar hiển thị 3 dòng riêng biệt; chip "Trả góp" disabled vì có dòng KM 2 | TC-003-MIX |
| S03-07 | NV cố chọn "Trả góp" cho đơn có dòng Gói KM 2 | Chip disabled với tooltip Trả góp không áp dụng cho đơn có Gói Ví KM 2 (DEC-017); nếu user bypass FE qua DevTools, BE 422 installment_not_allowed_for_km2 | TC-003-INSTALLMENT-BLOCKED |
| S03-08 | NV double click Tạo Mới | Nút disabled sau click đầu (loading state); BE dedupe client_request_id → trả cùng kết quả | TC-003-DOUBLE-CLICK |
| S03-09 | NV xoá dòng KM 2 cuối cùng trong đơn (chỉ còn dòng VND) | Cột "NẠP VÍ KM 2" ẩn ngay; dòng summary "Tiền vào Ví KM 2" ẩn; chip "Trả góp" enabled trở lại | TC-003-REMOVE-LAST-KM2 |
Nhóm SCR-04: Thanh toán bằng Ví KM 2
| # | Tình huống | Hành vi kỳ vọng | QA TC |
|---|---|---|---|
| S04-01 | Đơn chỉ có Kem chống nắng XYZ (không bật allow_promo_wallet_2) | Chip Ví KM 2 ẩn (eligible_total = 0) | TC-004-NO-ELIGIBLE |
| S04-02 | Khách có balance 50.000đ, eligible 500.000đ, max 20% → max calc = 100.000đ | Auto-fill = MIN(100.000, 50.000) = 50.000đ (dùng hết số dư); còn lại = 450.000đ tiền mặt | TC-004-INSUFFICIENT-BALANCE |
| S04-03 | Khách có 5 lot active (số dư lần lượt 100k, 200k, 300k, 400k, 500k = 1.5tr); thanh toán cần 600k | FIFO trừ: lot 1 hết (100k), lot 2 hết (200k), lot 3 trừ 300k (hết); ghi 3 deduction records cho cùng order_id; lot 4-5 còn nguyên | TC-004-FIFO-PARTIAL |
| S04-04 | Lot cũ nhất hết hạn 23:59:59 ngày trước; thanh toán lúc 00:01 (cron 00:05 chưa chạy) | BE check expired_at > NOW() trong tx; lot cũ skip, FIFO chuyển sang lot tiếp theo; lot cũ đánh dấu expired sau cron 00:05 | TC-004-EXPIRY-RACE |
| S04-05 | Đơn có allow_combine_km1 = true; KM 2 eligible 500k, KM 1 eligible 800k (item trùng) | KM 2 tính trước: 500k × 20% = 100k; KM 1 tính trên phần còn lại của item trùng: nếu cùng Massage 500k, KM 1 chỉ trừ trên 500k - 100k = 400k; nếu item riêng (KM 1 dùng cho item khác), KM 1 trừ riêng 800k | TC-004-DUAL-WALLET |
| S04-06 | NV double click "Thanh toán" | Nút disabled sau click đầu (loading); BE dedupe client_request_id | TC-004-DOUBLE-CLICK |
| S04-07 | 2 Sale POS thanh toán cùng khách Nguyễn Thị Lan vào cùng giây | A: BE SELECT FOR UPDATE lot, trừ thành công; B: nhận 422 insufficient_balance; toast Số dư đã thay đổi từ thiết bị khác. Đã tải lại số dư mới.; block KM 2 của B refresh số dư | TC-004-RACE-CONCURRENT |
| S04-08 | Đơn 1tr, eligible 700k, max 20% → max calc = 140k; balance 5tr | Auto-fill = MIN(140k, 5tr) = 140k; còn lại 860k tiền mặt; sau thanh toán balance = 5tr - 140k = 4.860.000đ | TC-004-NORMAL-CASE |
Nhóm SCR-06 + SCR-07: Profile + Hoàn ví KM 2
| # | Tình huống | Hành vi kỳ vọng | QA TC |
|---|---|---|---|
| S06-01 | Khách có 23 lot active | Bảng "Đang hoạt động" pagination 50/page (1 trang); virtual scroll nếu > 100 | TC-006-PAGINATION |
| S06-02 | Khách có toàn bộ 5 lot đều ở expired/exhausted/refunded | Section "Đang hoạt động" ẩn; chỉ hiện section "Đã hết / hoàn"; card stats hiện "0đ"; banner cảnh báo ẩn | TC-006-ALL-INACTIVE |
| S06-03 | Manager bấm Hoàn ví KM 2 ở Gói Gold #1 (còn 2.5tr, hết hạn 15/04/2026) | Dialog SCR-07 mở: Tỉ lệ hoàn = 500k / 5tr = 10,00%; Hoàn theo tỉ lệ = 2.5tr × 10% = 250.000đ; Phí xử lý = 500k × 20% = 100.000đ; KHÁCH NHẬN = MAX(250k - 100k, 0) = 150.000đ | TC-009-NORMAL-REFUND |
| S06-04 | Lot Gói Silver còn 100k (giá mua 200k, ví 1.5tr) | Tỉ lệ hoàn = 200k / 1.5tr = 13,33%; Hoàn theo tỉ lệ = 100k × 13,33% = 13.333đ; Phí = 200k × 20% = 40.000đ; KHÁCH NHẬN = MAX(13.333 - 40.000, 0) = 0đ; modal warning Phí xử lý lớn hơn số tiền hoàn. Khách KHÔNG nhận tiền nhưng số dư Gói Silver sẽ tất toán. Tiếp tục?; vẫn cho submit | TC-009-ZERO-CASE |
| S06-05 | Manager bấm Hoàn ví KM 2 đúng lúc lot vừa hết hạn (cron 00:05 chạy giữa lúc dialog mở) | BE 422 khi submit lot_already_expired; modal Lot vừa hết hạn. Vui lòng đóng và tải lại tab Ví KM 2. | TC-009-EXPIRED-RACE |
| S06-06 | Lot Gói Gold đã bị refund đơn DV trước đó (balance hiện = 1.8tr thay vì 5tr ban đầu) | Hoàn ví KM 2 tính trên balance hiện tại 1.8tr; KHÁCH NHẬN = MAX(1.8tr × 10% - 100k, 0) = 80.000đ | TC-009-AFTER-REFUND-BACK |
| S06-07 | Lot đã hết hạn; sau đó user refund đơn DV đã dùng lot này | Lot status expired → active; expired_at = NOW() + refund_extend_days (30); balance += amount refund; hiển thị lại trong section "Đang hoạt động" với badge Đã gia hạn (tooltip lý do) | TC-008-EXTEND |
| S06-08 | Lot đã refunded toàn bộ → user refund đơn DV đã dùng lot này | Lot cũ giữ status refunded; tạo lot mới với package_name = "{tên cũ} (gia hạn từ refund DV)", balance = amount refund, expiry = NOW + extend | TC-008-NEW-LOT-AFTER-REFUND |
| S06-09 | 2 Manager cùng bấm Hoàn ví KM 2 cho cùng lot | Đầu tiên thắng; thứ 2 nhận 422 Lot đã có yêu cầu hoàn đang xử lý + reload | TC-009-CONCURRENT-REFUND |
| S07-01 | Manager chọn "Hoàn vào: Tiền mặt" → submit | Tạo transaction_request với refund_method='cash', behavior='refund_km2_wallet', snapshot policy; toast Đã tạo Yêu cầu hoàn tiền KM 2 #{id}. Vui lòng chờ duyệt. | TC-009-METHOD-CASH |
| S07-02 | Manager chọn "Hoàn vào: Ví VND của khách" → reviewer duyệt → kế toán bấm Thanh toán | Lot tất toán (status=refunded); transfer balance VND wallet của khách +150k; toast Đã hoàn 150.000đ vào Ví VND của khách. Lot Gold đã tất toán. | TC-009-METHOD-WALLET |
| S07-03 | Reviewer từ chối yêu cầu hoàn | Lot KHÔNG bị tất toán (giữ status active); Manager có thể tạo request mới | TC-009-REJECT |
Nhóm SCR-08: Report dashboard
| # | Tình huống | Hành vi kỳ vọng | QA TC |
|---|---|---|---|
| S08-01 | Manager xem report nhưng branch không có dữ liệu KM 2 | KPI hiện "0"/"—"; bảng hiện Chưa có dữ liệu | TC-010-EMPTY-BRANCH |
| S08-02 | Export > 5.000 dòng tổng | Async: toast Đang xử lý, bạn sẽ nhận file qua thông báo; nút Xuất Excel disable cho đến khi xong; sau khi xong gửi notification với link tải | TC-010-ASYNC-EXPORT |
| S08-03 | KPI "Tỉ lệ dùng" có denominator (Tổng nạp) = 0 | Hiển thị "—" (không hiện 0% hay NaN) | TC-010-NAN |
| S08-04 | Khách Nguyễn Thị Lan mua Gói Gold ở chi nhánh A, dùng tại chi nhánh B | Report chi nhánh A: tính vào "Đã bán" + "Doanh thu" (theo branch_id lúc bán); Report chi nhánh B: tính vào "Đã dùng" (theo branch_id lúc deduction); Report "Tất cả": tổng đúng | TC-010-CROSS-BRANCH |
| S08-05 | Manager branch_mode=branch mở report | BranchSelect ẩn dropdown all-branch; chỉ hiện branch của Manager; backend enforcement filter theo role.branch_mode | TC-010-MANAGER-SCOPE |
| S08-06 | Admin branch_mode=all mở report | BranchSelect cho phép chọn "Tất cả" hoặc 1 branch cụ thể; hiển thị toàn hệ thống | TC-010-ADMIN-SCOPE |
Nhóm B-FR012: Tích hợp KM 2 vào màn hình hiện có
| # | Tình huống | Hành vi kỳ vọng | QA TC |
|---|---|---|---|
| F12-01 | Đơn mỹ phẩm — chọn KM 2 thanh toán | Chip Ví KM 2 hiện trong CosmeticOrderFormPayment (giống đơn DV); auto-fill max%, FIFO; CosmeticOrderFormPaymentTable hiện cột wallet_promotion_2_amount conditional | TC-FR012-COSMETIC |
| F12-02 | Đơn sản phẩm — item không có allow_promo_wallet_2 = true | Chip Ví KM 2 ẩn (eligible_total = 0); giống pattern KM 1 | TC-FR012-PRODUCT |
| F12-03 | Hoá đơn in cho đơn dùng KM 2 | Dòng Ví KM 2: 140.000đ hiện trong block PTTT (ngay dưới dòng Ví khuyến mãi: ... của KM 1); biến template payment_wallet_promotion_2_code được render | TC-FR012-INVOICE |
| F12-04 | Fund / Quỹ — đơn nạp tiền có Gói KM 2 | FundTable hiện cột "Vào Ví KM 2" conditional khi range có wallet_promotion_2_amount > 0 | TC-FR012-FUND |
| F12-05 | Withdraw / Refund detail — request refund_km2_wallet | WithdrawRequestDetail hiện badge Hoàn ví KM 2 cho behavior; payment method label "Ví KM 2" hiển thị đúng nếu refund_method=wallet; không thay đổi layout | TC-FR012-WITHDRAW |
| F12-06 | Report DV/NV — nhân viên bán Gói KM 2 + thanh toán đơn DV | ServiceReportTable cột wallet_promotion_2_revenue conditional cạnh KM 1; doughnut chart thêm segment "Ví KM 2"; KM 2 paid amount tính như wallet-paid (không cộng thực thu) | TC-FR012-EMPLOYEE-REPORT |
| F12-07 | CRM — khách có wallet KM 2 | Block ví khách hiện dòng Ví KM 2: 4.600.000đ ngay dưới dòng Ví khuyến mãi: ...; conditional khi balance > 0 | TC-FR012-CRM-WALLET |
Note QA: Các case
S03-04(multi-payment),S04-03(FIFO 5 lots),S06-07(gia hạn lot expired qua refund DV),S06-08(tạo lot mới từ refund DV của lot đã refund) là 4 case quan trọng nhất phản ánh nghiệp vụ KM 2 cốt lõi — phải có TC dedicated với assertion rõ trên DB state.
B-POST) Verification (BẮT BUỘC trước DELIVER)
B-POST.1) Checkpoint completeness
- [x] As-Is đầy đủ: B0.1 inventory bao phủ 100% UI hiện hữu các màn bị đụng (32 dòng UI ID, kèm Evidence file:line)
- [x] Delta status đầy đủ: mọi UI ID có Delta Status (
KEEP/UPDATE/NEW/HIDE); không cóREMOVEtrong scope - [x] Field × Surface ma trận: B0.4 có 28 field mới/sửa với 12 cột đầy đủ
- [x] State × Screen ma trận: B0.5 có 13 màn/khối với 6 state (default/loading/empty/error/no-permission/partial)
- [x] Reuse classification: mọi SCR có
✅ Reuse/🔧 Extend/🆕 Build mới+ file path + delta (B-PRE.3, B2.x.3) - [x] Wireframe context: Reuse/Extend đều vẽ vùng KEEP xung quanh update (SCR-01 banner readiness + card hiện hữu; SCR-02 form 2 cột; SCR-03 layout 8/4; SCR-04 chip + block; SCR-06 sidebar wallet stats; SCR-FR012 dòng dưới KM 1)
- [x] UX principles: mỗi SCR (SCR-01..SCR-08) có B2.x.1A 7 nguyên tắc UX
- [x] Variant coverage: B2.x.2.0 có Ma trận Biến thể với default/loading/error/no-permission/partial/lifecycle/permission cho mọi SCR có UI phức tạp
- [x] Role × Variant: B2.x.8A khớp PRD Decision Log + permission seed + QA TC; field không quyền không render (Variant C/H/I/K)
- [x] CTA hierarchy: mỗi SCR có primary CTA + điều kiện enable + secondary CTA
- [x] Validation đầy đủ: mọi input có rule (số liệu, range, format) trong B2.x.4 + B0.4
- [x] Permission matrix: mọi action nhạy cảm có dòng phân quyền per portal × role (B2.x.8 + B1.6)
- [x] Confirm modal: mọi destructive action có confirm copy (tắt KM 2, tạo refund khi khách nhận = 0, ngưng Gói)
- [x] Empty với CTA: SCR-03 empty "Chưa thêm thẻ" + nút
+ Thêm thẻ nạp; SCR-06 empty + lý do; SCR-08 empty với filter context - [x] Format VN: tiền
1.250.000đ, ngàyDD/MM/YYYY, giờHH:mm(B-i18n) - [x] Design-token intent: không hard-code màu/font; dùng
success/warning/danger/muted/primary emphasis/negative-value
B-POST.2) Cross-spec consistency check
- [x] Mọi field trong B0.4 đều xuất hiện trong PRD A5 FR (FR-001..FR-012)
- [x] Mọi field trong B0.4 đều xuất hiện trong Dev Spec C4 (data model
wallet_km2_config,wallet_km2_lot,wallet_km2_lot_deduction,prepaid_card.*,product/service.allow_promo_wallet_2,transaction_request.*) - [x] Mọi action mới đều có TC tương ứng trong QA Test Plan (TC-SCR01..TC-SCR08, TC-FR012, TC-PERM, TC-IMPACT)
- [x] Mọi state trong B0.5 đều có TC trong QA
- [x] Mọi variant trong B2.x.2.0 đều có PRD AC (FR-001 đến FR-012) hoặc UI-only state
- [x] Mọi variant trong B2.x.2.0 đều có QA TC ref (
TC-SCR01-A-01,TC-PERM-SCR06-01, ...) - [x] Role/permission trong B2.x.8 / B2.x.8A khớp PRD Decision Log (DEC-013 đến DEC-026), Permission seed (
module_permission_action) và QA TC-PERM - [x] Đã ripple sang PRD/QA/
_consistency-matrix.mdsau khi update UI Spec - [x] Đã điền
_consistency-matrix.md
B-POST.3) Lint nhanh kiểm tra nhất quán
bash
SLUG=vi-km-2
# 1. Style hard-code
rg -n "cam đỏ|xám đỏ|màu đỏ|font-size|font-weight" docs/features/$SLUG/ui-spec.md
# Kết quả: rỗng ✅
# 2. English UI raw
rg -n '\b(Submit|Confirm|Cancel|OK|Loading|No data|Forbidden|Error|Success|Save|Delete|Edit|View)\b' docs/features/$SLUG/ui-spec.md | grep -v "Reuse\|MOVE\|NEW\|REMOVE\|HIDE\|KEEP\|UPDATE\|EXTEND\|primary emphasis\|aria-\|HSD\|FOR UPDATE\|VND\|wallet_target\|wallet_value"
# Kết quả: chỉ còn match trong code/literal/Evidence path ✅
# 3. Placeholder
rg -n "TBD|TODO|Lorem|\{placeholder\}|FIXME" docs/features/$SLUG/ui-spec.md
# Kết quả: rỗng ✅
# 4. Enum bị dịch (search-replace lỗi)
rg -n "đang hoạt động \(đang hoạt động\)|đã hoàn thành \(đã hoàn thành\)" docs/features/$SLUG/ui-spec.md
# Kết quả: rỗng ✅
# 5. Click verb chưa Việt hoá
rg -n '"Click"|Click [A-Za-z]' docs/features/$SLUG/ui-spec.md
# Kết quả: chỉ trong narrative VI "Click vào..." ✅
# 6. Khẩu hiệu mơ hồ
rg -n "tối ưu trải nghiệm|nâng cao hiệu quả|hỗ trợ quản lý|nói chung|cụ thể là|rất linh hoạt" docs/features/$SLUG/ui-spec.md
# Kết quả: rỗng ✅
# 7. Format VN sai
rg -n '\$[0-9]|MM/DD/YYYY|hh:mm A|[0-9],[0-9]{3}đ' docs/features/$SLUG/ui-spec.md
# Kết quả: rỗng ✅
# 8. Calque nguy hiểm
rg -n "Quy ước (field|tích hợp|phản hồi|hành vi)" docs/features/$SLUG/ui-spec.md
# Kết quả: tồn tại có chủ ý theo template skill ("Quy ước tương tác form") — pass
# 9. Schema NOT NULL field UI cho null
rg -n "_id=NULL|=NULL OK" docs/features/$SLUG/ui-spec.md
# Kết quả: rỗng ✅
# 10. Cấu trúc Mermaid label English
rg -n "stateDiagram.*Active|stateDiagram.*Pending|flowchart.*Pass|flowchart.*Fail" docs/features/$SLUG/ui-spec.md
# Kết quả: chỉ flowchart label tiếng Việt + code raw `active`/`expired`/`exhausted`/`refunded` trong table ✅B-POST.4) Cross-spec diff bot
bash
SLUG=vi-km-2
# Diff PRD FR list vs UI Spec SCR list
PRD_FR=$(rg -o '^### FR-[0-9]+' docs/features/$SLUG/prd.md | sort -u)
UI_SCR=$(rg -o 'SCR-[0-9a-z]+' docs/features/$SLUG/ui-spec.md | sort -u)
echo "PRD FR coverage:"
echo "$PRD_FR"
# FR-001..FR-012 (12 FRs)
echo "UI SCR coverage:"
echo "$UI_SCR"
# SCR-01..SCR-08 + SCR-FR012-* (cover all FR)
# DEC trong SOURCE_OF_TRUTH phải xuất hiện trong ui-spec
DECs=$(rg -o 'DEC-[0-9]+' docs/features/$SLUG/SOURCE_OF_TRUTH.md | sort -u)
for dec in $DECs; do
rg -q "$dec" docs/features/$SLUG/ui-spec.md && echo "OK $dec" || echo "MISSING $dec in ui-spec"
done
# Kết quả: DEC-001..DEC-026 — DEC liên quan UI đã được ref qua context (DEC-005, DEC-008, DEC-014, DEC-017, DEC-022, DEC-023, DEC-026 đều có); DEC kỹ thuật pure (DEC-007, DEC-018, DEC-022) không cần ref UI
# Action permission UI vs Dev Spec C8
UI_ACTIONS=$(rg -oE "[a-z_]+:[a-z_]+" docs/features/$SLUG/ui-spec.md | sort -u)
echo "UI actions:"
echo "$UI_ACTIONS"
# internal_configuration:update, prepaid_order:create, prepaid_order:payment, service_order:payment,
# product_order:payment, customer_management:access, customer_management:view_all,
# refund_request_management_submenu:access/create/update/approve/payment, report:accessMọi mismatch ở B-POST.4 đều đã được giải thích hoặc fix trước DELIVER. Đặc biệt: 7 DEC kỹ thuật pure (DEC-007/018/022/...) không cần trace UI; 19 DEC còn lại đều có UI implementation.
B-QUALITY) Rà soát rủi ro thiếu sót (45 rủi ro)
Đối chiếu từng rủi ro với spec → ghi
[Đã cover ở X]hoặc[Còn gap: ...].
Rủi ro QA thường gặp
| # | Rủi ro | Trạng thái cho package này |
|---|---|---|
| 1 | Thiếu test cho field mới | Đã cover ở B0.4 + QA D2 với ≥3 TC mỗi field (happy/boundary/negative) — VD max_percent_per_order: TC max=1, max=100, max=0, max=101 |
| 2 | Thiếu state empty/loading/error | Đã cover ở B0.5 — 13 màn × 6 state |
| 3 | Thiếu permission denied test | Đã cover ở B2.x.8A + TC-PERM (Variant C/F/H/I/K) cho mọi SCR |
| 4 | Thiếu confirm modal | Đã cover: tắt KM 2 (SCR-01 confirm_disable_km2), refund khách nhận 0 (SCR-07 confirm_refund_zero), ngưng Gói (SCR-02), rời trang dirty (confirm_leave_dirty), toggle Cho phép Ví KM 2 (SCR-05 confirm_toggle_km2) |
| 5 | Thiếu format VN trong test | TC ghi rõ expected 1.250.000đ, DD/MM/YYYY, HH:mm, 37,50% (B-i18n + QA D2) |
| 6 | Boundary value không cụ thể | TC boundary có dòng cho min/max/min-1/max+1 cho max_percent_per_order, expiry_months, refund_fee_percent (B0.4) |
Rủi ro UI/UX thường gặp
| # | Rủi ro | Trạng thái |
|---|---|---|
| 7 | Thêm field không nói nằm đâu | B0.1 có cột "Section" + "Thứ tự"; B0.4 có cột "Hiển thị ở đâu" |
| 8 | Wireframe không vẽ vùng KEEP | Mọi wireframe SCR-01..SCR-07 đều vẽ vùng KEEP (banner readiness chèn vào shell, dòng KM 2 chèn dưới dòng KM 1, chip Ví KM 2 chèn cạnh chip KM 1) |
| 9 | Field không có default | B0.4 có cột Default cho mọi field — disabled=true, max_percent_per_order=20, allow_combine_km1=false, allow_refund=true, refund_fee_percent=20, refund_deadline_days=30, refund_extend_days=30, wallet_target=VND, expiry_months=6, allow_promo_wallet_2=false |
| 10 | Empty state không có CTA | SCR-03 empty + nút + Thêm thẻ nạp; SCR-06 empty + lý do; SCR-08 empty với context filter |
| 11 | Modal không nói trigger | B2.7 dialog mở từ SCR-06 row click; B2.x.10 cột Hành động ghi trigger |
| 12 | Filter không nói default | SCR-02 filter "Trạng thái: Tất cả", "Ví đích: Tất cả"; SCR-06 filter Trạng thái + Tên Gói default Tất cả; SCR-08 default tháng hiện tại + branch theo role |
| 13 | Chỉ có happy-path wireframe | B2.x.2.0 Ma trận Biến thể có 6-11 variant per SCR (default/loading/empty/error/permission/lifecycle/race/partial) |
| 14 | Role/permission flow lệch nhau | B2.x.8A Role × Variant + B-POST.2 ripple PRD/QA |
| 15 | UI spec hard-code style | Đã pass lint B-POST.3 — không có "cam đỏ", "font-size", "font-weight" |
Rủi ro PO/BA thường gặp
| # | Rủi ro | Trạng thái |
|---|---|---|
| 16 | Spec mới ghi đè behavior cũ mà không khai báo | B0.1 có Delta Status cho mọi UI hiện hữu — KEEP rõ ràng cho card hiện có ở SCR-01, form 2 cột SCR-02, layout 8/4 SCR-03, chip KM 1 SCR-04, toggle KM 1 SCR-05, sidebar wallet stats SCR-06 |
| 17 | Out-of-scope không rõ | PRD A2 + B-FR012 "KHÔNG thay đổi UI" liệt kê rõ (OrderCard, dashboard Phase 3, rank.graphql) |
| 18 | Decision không có ≥2 phương án | Z) Decision Log trong PRD đã có cột "Lý do" với ≥2 alternatives cho mọi DEC |
Rủi ro chuyên sâu
| # | Rủi ro | Trạng thái |
|---|---|---|
| 19 | Wireframe ASCII vỡ alignment | B0.6 — đã verify số │ cách đều |
| 20 | Search-replace lỗi enum | B0.7 Bilingual Pairing + lint B-POST.3 — pass |
| 21 | UI cho phép null field NOT NULL | B0.8 Schema Cross-Check — đã verify expiry_months (conditional NOT NULL khi KM 2), branch_id NOT NULL (auto-fill), transaction_request.lot_id (NOT NULL với behavior refund_km2_wallet) |
| 22 | Stepper số bước inconsistency | Setup readiness 4 bước nhất quán B-PRE / B0.5 / B1.5 / SCR-01 wireframe |
| 23 | Form thiếu autosave/paste/IME contract | B2.x.7B Form Interaction Deep cho SCR-01/02/03/07 |
| 24 | Concurrency lờ đi | B2.x.7C cho SCR-01 (config 409), SCR-02 (Gói 409), SCR-03 (idempotency), SCR-04 (lot SELECT FOR UPDATE), SCR-06 (polling 60s), SCR-07 (lot race) |
| 25 | Mạng yếu / offline không ghi | B2.x.7D Network Resilience cho SCR-01/02/03/04/06/07 |
| 26 | Lỗi gộp chung "Có lỗi xảy ra" | B2.x.10A Error Taxonomy phân loại 7 loại lỗi cho mọi SCR (validation client/server, 401/403/409/422/429/5xx, network, partial 207) |
| 27 | Print/PDF thiếu page break / version stamp | B-FR012 đoạn InvoiceTemplate + B5A Export Deep; package này không có form in pháp lý mới |
| 28 | Token UI thiếu countdown / revoke | Không áp dụng (không có token portal customer self-serve Day-1) |
| 29 | File upload thiếu chunk/retry | Không áp dụng (không có upload trong scope) |
| 30 | Search debounce ad-hoc | B2.2 search Gói Ví KM 2 debounce 300ms; SCR-08 không search; SCR-06 filter không search |
| 31 | Bulk action thiếu undo / partial | Không áp dụng Day-1 (deferred); B2.x.10A có "Partial success" cho multi-payment 207 |
| 32 | Export thiếu masking theo quyền | B5A: Manager branch_mode=branch chỉ thấy branch mình; cell — nếu không quyền |
| 33 | Copy length vỡ layout | B6A — đã kiểm; rút gọn label "Phí xử lý hoàn ví (% giá mua gói)" → "Phí xử lý hoàn ví (%)" |
| 34 | Format VN sai ($50, MM/DD) | B-i18n + lint B-POST.3 — pass |
| 35 | Brand voice không nhất quán | B-Voice — chuẩn hoá tone admin/POS (trung tính, chuyên môn) vs ZNS khách (lễ phép, ấm áp) |
| 36 | Microcopy form lộn xộn | B-Microcopy — chuẩn hoá * đỏ, char counter, format ngàn |
| 37 | A11y bỏ sót | B8B A11y Deep — focus visible, aria-label, heading hierarchy, focus trap dialog |
| 38 | Performance không nói | B8A Performance — virtual scroll SCR-06 popup > 100 lot; pagination 50/page; bundle < 200KB; FCP < 1.5s |
| 39 | Edge case không phân nhóm | B-Tình huống cá biệt 12 nhóm G1-G12 chuẩn (60+ case) |
| 40 | Feature flag / staged rollout không có UI | B-Versioning — disabled=true mặc định = kill switch; FE polling 60s; rollback by toggle |
| 41 | Help touchpoint thiếu | B-Help — link runbook, tooltip ⓘ, onboarding tour 4 bước, error correlation_id |
| 42 | Lifecycle ≥4 trạng thái không có EXT-3 | Lot có 4 trạng thái (active/exhausted/expired/refunded) — chốt KHÔNG có pending theo DEC-027 (lot chỉ được tạo khi khách trả ≥ 1 đồng); B2.6.11 + PRD A4 lifecycle diagram cover; chưa load EXT-3 vì không phải lifecycle với approval phức tạp (request lifecycle reuse transaction_request hiện hữu) |
| 43 | RBAC field-level không có EXT-4 | B2.x.8A Role × Variant + B1.6 Dynamic Permission v2 cover; package không có field-level masking phức tạp (số tiền chỉ ẩn cả khối, không partial) |
| 44 | Audit/compliance UI thiếu | B2.7 policy_snapshot + audit wallet_km2_lot_deduction + B-FR012 WithdrawRequestDetail; G12 cover compliance |
| 45 | Real-time update không nói | B2.x.7C polling 60s cho SCR-06 popup; SCR-01 không real-time (config save explicit) |
Cách dùng B-QUALITY
- ✅ Đã rà 45 rủi ro
- ✅ Mọi rủi ro đã có
[Đã cover]hoặc đã được tự suy diễn không áp dụng (token portal customer self-serve / file upload / form pháp lý — không trong scope KM 2) - ✅ Không còn gap → DELIVER OK