Skip to content

v1.11 — 15/05/2026

Thay đổiSectionẢnh hưởng
Đảo DEC-031 sang Option A — đổi tên section TC-SCR-09 lại về "Tab Ví khuyến mãi 2 trong wallet_screen.dart"; sửa toàn bộ 14 TC cũ reference "route /wallet/promotion2" → "tab thứ 3"; thêm TC-SCR09-13 regression (3 tab cùng work), TC-SCR09-15 regression FolderTabs ở KPI/Tet/RevenueKpi (nếu dev refactor widget gốc)TC-SCR-09QA, FE Mobile
Thêm 5 TC-006 mở rộng cho SCR-06-NEW-03 — popup StatisticWalletPromotion2Popup.tsx lịch sử giao dịch KM 2 (filter, date range, regression popup KM 1, trigger từ CustomerKm2WalletPopup)TC-FR-006QA, FE Web
Update D4 traceability matrix: 131 → 137 ca kiểm thửD4QA

v1.10 — 15/05/2026

Thay đổiSectionẢnh hưởng
Đổi tên section TC-SCR-09 từ "Tab Ví KM 2" → "Screen Ví KM 2" (DEC-031); update 12 TC cũ (TC-SCR09-01..12) để reference route mới /wallet/promotion2 thay vì tab [ĐÃ ĐẢO ở v1.11](TC-SCR-09)QA, FE Mobile
Thêm 2 TC regression mới TC-SCR09-13/14 — wallet_screen 2 tab hiện hữu KHÔNG bị ảnh hưởng (DEC-031); AffiliateFor không có option KM 2 (DEC-032)(TC-SCR-09)QA, FE Mobile
Thêm TC-SCR09-ENUM-01..05 (5 TC mới) — WalletType.promotion2 enum + l10n shared (SCR-09-MOBILE-03)(TC-SCR09-ENUM)QA, FE Mobile
Thêm TC-SCR-10 — Staff app customer detail balance KM 2 (7 TC, SCR-10-STAFF-01)(TC-SCR-10)QA, FE Mobile
Update D4 traceability matrix: 117 → 131 ca kiểm thửD4QA

v1.8 — 15/05/2026

Thay đổiSectionẢnh hưởng
Thêm TC-IDEMPOTENT-01..09 cho TG-003 idempotency gate (DEC-028) — 9 test scenariosD2) Ca kiểm thửQA, BE
Thêm TC-BALANCE-RACE-01..05 cho action get_customer_km2_balance (DEC-029) — 5 test scenariosD2) Ca kiểm thửQA, BE
Update D4 traceability matrix: 80 → 94 ca kiểm thửD4QA
Lint vietnamese clean: chuẩn hoá thuật ngữ "Trường hợp cá biệt" thay cho calque cũToàn fileNone

Kế hoạch kiểm thử (QA Test Plan) — Ví KM 2 (Promotion Wallet 2)

Tham chiếu: PRD v1.11 + SOURCE_OF_TRUTH v1.6 | Ngày: 15/05/2026


File này dùng để làm gì: chốt coverage test, seed data và traceability cho Ví KM 2. Quy tắc ưu tiên: nếu có khác biệt với SOURCE_OF_TRUTH.md, ưu tiên SOURCE_OF_TRUTH.md.

Đầu vào chuẩn (Canonical Inputs)

FileVai trò
SOURCE_OF_TRUTH.mdNguồn sự thật chuẩn + khóa giải pháp
EVIDENCE_PACK.mdBằng chứng code/screen/db/config thật
prd.mdFR/AC và công thức nghiệp vụ cần kiểm thử
dev-spec.mdData/API/security contract để đối chiếu test tích hợp
ui-spec.mdState, permission, copy và interaction contract

D1) Phạm vi kiểm thử

FRMô tảMức ưu tiên
FR-001Cấu hình Ví KM 2 (max%, toggle, refund policy)Must
FR-002Tạo/sửa Gói Ví KM2 (wallet_target, expiry_months)Must
FR-003Bán Gói Ví KM2 → tạo lot(s) trong Ví KM 2Must
FR-004Thanh toán đơn hàng bằng Ví KM 2 (FIFO, max%, deduction)Must
FR-005Flag allow_promo_wallet_2 per sản phẩm/dịch vụMust
FR-006Tab Ví KM 2 trong profile khách (danh sách lần mua Ví KM2, cảnh báo)Must
FR-007Scheduler tự động hết hạn lotMust
FR-008Refund đơn DV — phần Ví KM 2Should
FR-009Hoàn ví KM2 qua Yêu cầu hoàn tiền (refund_km2_wallet)Should
FR-010Report dashboardCould
FR-011ZNS thông báoCould
FR-012Hiển thị Ví KM 2 trong đơn hàng hiện có (summary, bảng, hoá đơn in, fund/report impact)Must

D2) Ca kiểm thử

TC-FR-001: Cấu hình Ví KM 2

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-001-00Sau migration, KM2 chưa tự bậtMigration seed config xong, chưa bấm enablewallet_km2_config.disabled=true, payment method KM2 chưa hiện ở POSP0
TC-001-01Bật Ví KM 2, lưu config mặc địnhBật toggle, max% = 20, KM1+KM2 = tắtToast "Lưu cài đặt thành công", config lưu DBP0
TC-001-02Sửa max% thành 40%, lưumax% = 40Config cập nhật, đơn mới áp dụng max 40%P0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-001-03Biênmax% = 0Nhập 0Lỗi kiểm tra hợp lệ: "Tối thiểu 1%"P1
TC-001-04Biênmax% = 101Nhập 101Lỗi kiểm tra hợp lệ: "Tối đa 100%"P1
TC-001-05LỗiTắt Ví KM 2 khi khách đang có số dưTắt toggleẨn payment method KM2, số dư không mất, bật lại → hiện lạiP1
TC-001-06QuyềnStaff truy cập configĐăng nhập Staff, vào URL configẨn menu, redirectP1

TC-FR-002: Tạo/sửa loại Gói Ví KM2

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-002-01Tạo Gói Gold target KM2value=500k, value_into_wallet=5tr, expiry_months=6Lưu thành công, danh sách hiện "Ví đích = KM2", data có wallet_target=VND_PROMOTION_2P0
TC-002-02Tạo thẻ VND cũwallet_target=VNDTrường expiry_months ẩn, flow cũ không đổiP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-002-03BiênKM2 nhưng expiry_months rỗngwallet_target=KM2, expiry emptyLỗi kiểm tra hợp lệ, không lưuP1
TC-002-04Biênvalue_into_wallet < valuevalue=500k, value_into_wallet=300kLỗi kiểm tra hợp lệ "Số tiền nạp phải >= mệnh giá thẻ"P1
TC-002-05Hồi quySửa thẻ VND hiện cóKhông đổi wallet_targetKhông phát sinh expiry bắt buộc, data cũ không bị đổi sang KM2P1

TC-FR-003: Bán Gói Ví KM2 → tạo lot(s)

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-003-01Bán 1 Gói Gold (500k → 5tr, 6 tháng)qty=1, payment=cash 500k1 lot: initial=5tr, balance=5tr, expired_at=+6m, status=activeP0
TC-003-02Bán 3 Gói Silver (300k → 2tr, 3 tháng)qty=3, payment=cash 900k3 lots riêng biệt, mỗi lot initial=2tr, balance=2tr, expired_at=+3mP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-003-03Kết hợpMix thẻ VND + Gói Ví KM2 trong cùng 1 đơnDòng 1: Thẻ 10tr (VND), Dòng 2: Gói Gold (KM2)Cột "NẠP VÍ KM 2" xuất hiện, dòng 1 KM2=0, dòng 2 KM2=5tr. Sidebar hiện "Tiền vào ví KM 2". Submit: VND cộng ví thường, KM2 tạo lotP0
TC-003-04Kết hợpChỉ thẻ VND (không có KM2)2 dòng thẻ VNDCột "NẠP VÍ KM 2" KHÔNG hiện. Sidebar KHÔNG có dòng "Tiền vào ví KM 2". Flow cũ 100%P0
TC-003-05LỗiBán Gói Ví KM2 cho khách chưa có wallet VND_PROMOTION_2Khách mớiHệ thống auto-create wallet, tạo lot thành côngP1
TC-003-06Biênqty = 0 hoặc âmqty=0Lỗi kiểm tra hợp lệ, không tạo đơnP1
TC-003-07Kết hợpXoá dòng KM2 cuối cùng khỏi đơnCó 1 dòng KM2, bấm xoáCột "NẠP VÍ KM 2" tự ẩn, sidebar ẩn dòng KM2P1

TC-FR-004: Thanh toán đơn hàng bằng Ví KM 2

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-004-01Đơn 1tr, max 20%, ví còn 5trChọn KM2Auto-fill 200k, khách trả 800kP0
TC-004-02Đơn eligible 700k (mix eligible + non-eligible), max 20%2 items, 1 eligibleKM2 = 700k × 20% = 140k, khách trả 860kP0
TC-004-03FIFO: 2 lots, lot cũ 300k, lot mới 4.7tr, cần trừ 400kĐơn 2tr, max 20%Lot cũ: trừ 300k (exhausted), lot mới: trừ 100k. 2 deduction recordsP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-004-04BiênVí chỉ còn 50k, max% cho phép 200kĐơn 1tr, max 20%KM2 = MIN(200k, 50k) = 50k, khách trả 950kP1
TC-004-05BiênVí = 0Đơn 1trẨn payment method KM2P1
TC-004-06LỗiTất cả items không eligibleĐơn 1tr, 0 item eligibleẨn payment method KM2P1
TC-004-07Kết hợpToggle KM1+KM2 tắt, đã chọn KM1Chọn KM2KM1 bị ẩn, chỉ thấy KM2P1
TC-004-08Kết hợpToggle KM1+KM2 bật, chọn cả 2KM1 300k + KM2 200k + cash 500kCả 2 ví bị trừ đúng, invoice ghi đúngP1
TC-004-09Race condition2 NV thanh toán cùng lúc cho 1 khách2 đơn submit song song1 thành công, 1 lỗi "Số dư không đủ" (SELECT FOR UPDATE)P0
TC-004-10Cross-branchKhách mua Gói Ví KM2 chi nhánh A, thanh toán chi nhánh BBranch khácThanh toán thành công, lot bị trừP1
TC-004-11IdempotencyRetry cùng payment_attempt_id sau timeoutSubmit lại cùng order/payment_attempt_idKhông tạo deduction mới, wallet không bị trừ lần 2P0
TC-004-12Idempotency + multi-lotCùng một payment_attempt_id cần trừ qua 2 lotLot A balance 100k, Lot B balance 300k, amount KM2 = 140k; submit rồi retry cùng keyLần đầu tạo 1 wallet_km2_payment_attempt + 2 deduction rows; retry trả kết quả cũ, không tạo thêm row và không trừ ví lần 2P0
TC-004-13Expiry guardLot active nhưng expired_at <= NOW() do cron missThanh toán bằng KM2Lot đó bị skip, nếu không còn lot hợp lệ thì báo số dư không đủP0

TC-IDEMPOTENT: Bộ test bắt buộc cho TG-003 (Idempotency Contract — DEC-028)

Bộ test này port từ dev-spec C5 (section E "Test scenarios bắt buộc"). TL ký xác nhận trước khi merge deduct_km2_payment lên staging.

TCScenarioDữ liệu nhậpKỳ vọngMức ưu tiên
TC-IDEMPOTENT-01Bấm Thanh toán 1 lần, BE 2001 lần submit thành côngLot trừ 1 lần; wallet_km2_payment_attempt.status='completed'; idempotent_replay=falseP0
TC-IDEMPOTENT-02Bấm 2 lần liên tục < 1s (double-click)2 click submit cùng payment_attempt_idLot trừ 1 lần; lần 2 nhận idempotent_replay=true (FE đợi response lần đầu thì tự bỏ) hoặc 409 attempt_in_progressP0
TC-IDEMPOTENT-03Bấm 1 lần, mạng timeout, retry sau 5sSubmit lần đầu timeout, retry cùng payment_attempt_idLot trừ 1 lần; lần retry nhận idempotent_replay=true với result cachedP0
TC-IDEMPOTENT-04Crash giữa lúc lock; sau 90s NV retryWorker crash khi đang processing; sau 90s lock expireLock expired → cho retry mới; lot trừ 1 lần (vì transaction lần đầu đã rollback do crash)P0
TC-IDEMPOTENT-05NV A submit; NV B submit cùng attempt_id (bypass FE)2 NV khác nhau dùng cùng payment_attempt_idA thắng; B nhận 409 attempt_mismatch (cross-check user_id JWT không khớp record)P0
TC-IDEMPOTENT-06Submit khi 1 lot vừa hết hạn (race với cron 00:05)Lot expired ở 23:59:59, NV submit 23:59:59.500Tx check expired_at > NOW() → skip lot expired, FIFO chuyển lot tiếp; idempotent_replay=false cho retry sau; deducted_amount có thể < requested_amountP0
TC-IDEMPOTENT-07Submit thành công; sau 8 ngày retry cùng attempt_idRecord TTL > 7 ngày, đã được cron cleanupRecord đã expired/cleanup → coi như attempt mới; trừ lần 2 (vì user thực sự muốn payment thứ 2)P1
TC-IDEMPOTENT-08Bypass scenario: đổi order_id nhưng giữ attempt_id2 order khác nhau dùng cùng payment_attempt_idLần 2 reject attempt_mismatch (cross-check order_id không khớp)P0
TC-IDEMPOTENT-09Bypass scenario: đổi customer_id nhưng giữ attempt_idCùng payment_attempt_id nhưng customer_id khácReject attempt_mismatch (cross-check customer_id không khớp)P0

Acceptance: 9 test pass + BE unit test cho 9 scenario này. Đây là gate TL-001 → TG-003 pass.

TC-BALANCE-RACE: Action get_customer_km2_balance realtime (DEC-029)

Đảm bảo balance hiển thị cho UI luôn match với BE deduct, đóng race window scheduler miss giữa wallet.amount cached và lot expired.

TCScenarioDữ liệu nhậpKỳ vọngMức ưu tiên
TC-BALANCE-RACE-01Lot vừa hết hạn race với cron 00:05Lot Gold balance 2tr, expired_at='2026-05-15 23:59:59'; tại 00:00:30 NV mở SCR-04 cho kháchAction trả available_amount=0 (lot expired bị filter expired_at > NOW()), expired_unswept_amount=2tr (admin có thể xem); chip KM 2 ẩn vì balance=0P0
TC-BALANCE-RACE-02Wallet.amount cached lệch với SUM(lot.balance)wallet.amount=5tr (cached cũ), thực tế 1 lot 2tr đã expired; FE call action mớiAction trả available_amount=3tr (realtime SUM, KHÔNG dùng wallet.amount); UI hiển thị 3tr, BE deduct tối đa 3tr — không lệchP0
TC-BALANCE-RACE-03Scheduler đang chạy lúc NV mở SCR-04Cron 00:05 đang update 1 lot expired; cùng lúc NV mở SCR-04Action chỉ đọc lot status='active' AND expired_at > NOW(), race không gây sai số (idempotent với scheduler); response < 100msP1
TC-BALANCE-RACE-04Action có nearest_expiry_at cho banner cảnh báoKhách có 3 lot: lot A expired_at +5 ngày, lot B +20 ngày, lot C +60 ngàynearest_expiry_at = +5 ngày; UI render banner danger (≤ 7 ngày)P1
TC-BALANCE-RACE-05Permission no-leak qua actionStaff không có quyền xem customer X gọi action với customer_id=XAction trả 403 no_permission_customer, không lộ balanceP0

TC-FR-005: Flag allow_promo_wallet_2 per sản phẩm/dịch vụ

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-005-01Admin bật KM2 cho dịch vụService detail -> bật toggle KM2Lưu thành công, order eligible tính dịch vụ nàyP0
TC-005-02Dynamic Permission revoke updateUser không có internal_configuration:update, vào product/service detailToggle KM2 ẩn hoặc disabled theo permission, không mutation; gọi mutation trực tiếp bị chặnP1
TC-005-03Tắt KM2 cho item đang bánallow_promo_wallet_2=falseĐơn mới không tính item này vào eligible_totalP1
TC-005-04KM1 không bị đổiBật/tắt KM2Trường allow_promo_wallet của KM1 không đổiP1

TC-FR-006: Tab Ví KM 2 trong profile khách

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-006-01Xem ví KM 2 có 2 Gói Ví KM2 đang hoạt độngProfile khách có 2 Gói Ví KM2 đã muaTổng số dư = tổng 2 bản ghi mua, bảng hiện 2 dòng đang hoạt độngP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-006-02Trạng tháiKhách không có lot nàoProfile khách mớiTổng = 0, bảng trạng thái rỗng "Chưa có Gói Ví KM2 nào"P1
TC-006-03BiênLot hết hạn trong 7 ngàyexpired_at = +5 ngàyCảnh báo màu đỏP1
TC-006-04BiênLot hết hạn trong 25 ngàyexpired_at = +25 ngàyCảnh báo bình thường (không đỏ)P2
TC-006-05QuyềnStaff xem profile khách được phân quyềnĐăng nhập Staff, mở profile khách thuộc phạm vi được phépTab Ví KM 2 hiển thị, action get_customer_km2_lots trả đúng lot của khách đóP1
TC-006-06Bảo mậtStaff gọi action xem khách ngoài phạm viĐăng nhập Staff, gọi get_customer_km2_lots với customer ngoài quyềnBị chặn 403/permission error, không trả lot/deductionP0
TC-006-07SCR-06-NEW-03 — Popup lịch sử giao dịch KM 2 hiển thị đúngKhách có 5 giao dịch KM 2 (3 payment + 1 refund + 1 expired); mở popup StatisticWalletPromotion2PopupBảng hiển thị đủ 5 row với cột (Ngày / Loại / Số tiền / ĐH / Chi nhánh / Số dư sau); query filter wallet_type_id = 'VND_PROMOTION_2' (KHÔNG lẫn với KM 1)P0
TC-006-08SCR-06-NEW-03 — Filter dropdown loại giao dịchChọn dropdown "Thanh toán"Bảng chỉ hiện row payment (3 row), filter client-side hoặc refetchP1
TC-006-09SCR-06-NEW-03 — Filter date rangeChọn date range 7 ngày gần nhấtBảng hiện chỉ row trong range; pagination reset về page 1P1
TC-006-10SCR-06-NEW-03 — Backwards compat popup KM 1Mở popup KM 1 (StatisticWalletPromotionPopup) cho khách có cả KM 1 + KM 2Popup KM 1 hiện chỉ giao dịch KM 1 (filter VND_PROMOTION), KHÔNG lẫn KM 2 — DEC-031 Option α (popup riêng, không refactor)P0
TC-006-11SCR-06-NEW-03 — Trigger từ CustomerKm2WalletPopupMở CustomerKm2WalletPopup (chi tiết lot), click nút "Xem lịch sử giao dịch"StatisticWalletPromotion2Popup mở overlay xếp chồng đúng z-indexP2

TC-FR-007: Scheduler tự động hết hạn lot

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-007-01Bản ghi mua hết hạn hôm nay1 bản ghi đang hoạt động, expired_at = hôm quaSau cron: status=expired, wallet.amount giảm, deduction record type=expiry_cancelP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-007-02BiênLot hết hạn nhưng balance = 0Lot exhausted (balance=0)Không bị expire (đã exhausted rồi), không trừ walletP1
TC-007-03LỗiCron chạy nhưng không có bản ghi mua hết hạnTất cả bản ghi đang hoạt động, expired_at > NOW()Không có thay đổi, log "0 lots expired"P2

TC-FR-008: Refund đơn DV — phần Ví KM 2

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-008-01Refund đơn dùng KM2 200k (1 lot)Manager duyệt refundLot cộng lại 200k, wallet.amount tăng 200k, deduction type=refundP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-008-02Kết hợpRefund đơn trừ 2 lots (FIFO: lot1=150k, lot2=50k)Refund toàn bộ 200kLot1 +150k, lot2 +50k (query deduction records)P1
TC-008-03BiênRefund vào lot đã hết hạnLot expired + refundLot gia hạn +30 ngày (config), status=active, cộng balanceP1
TC-008-04BiênRefund vào lot đã refundedLot refunded + refund đơn DVTạo lot MỚI, initial_amount = refund amount, expired_at = +30dP1

TC-FR-009: Hoàn ví KM2 qua Yêu cầu hoàn tiền

Luồng đúng:

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-009-01Hoàn ví KM2 Gold: 500k→5tr, dùng 2tr, còn 3tr, phí 20%User có quyền tạo request bấm "Hoàn ví KM2", approver duyệt request refund_km2_walletRequest xuất hiện trong Yêu cầu hoàn tiền; khách nhận: 3tr × 10% - 100k = 200k. Lot=refunded, wallet -= 3trP0

Trường hợp cá biệt / lỗi:

TCLoạiMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-009-02BiênPhí xử lý hoàn ví > tiền hoàn (phí 100k > hoàn 50k)Gói Ví KM2 500k→5tr, còn 500k. Hoàn=50k-100k=-50kHiển thị "Khách nhận: 0đ", cho tạo request; khi duyệt, lot bị trừ nhưng không hoàn tiền thậtP1
TC-009-03LỗiHoàn ví KM2 đã hết hạnLot expiredNút "Hoàn ví KM2" ẩn; gọi API trực tiếp bị chặn KM2_LOT_INVALID_STATUSP1
TC-009-04LỗiHoàn ví KM2 đã dùng hếtLot exhausted (balance=0)Nút "Hoàn ví KM2" ẩnP1
TC-009-05Quyền revokeUser không có refund_request_management_submenu:access/createRefresh/relogin, mở profile kháchNút "Hoàn ví KM2" ẩn; gọi tạo request trực tiếp bị chặn, không leak lotP0
TC-009-06Quyền approve/paymentUser có access/create nhưng không có approve/paymentTạo request thành công, vào màn Yêu cầu hoàn tiềnThấy request nếu thuộc scope nhưng không thấy nút duyệt/thanh toán; API approve/payment bị chặnP0
TC-009-07Portal splitGrant refund_request_management_submenu:approve ở Admin portal, revoke ở POS portalTest cùng account ở Admin/POSQuyền không bleed giữa portal; POS không duyệt nếu bị revokeP1
TC-009-08Branch scopeApprover branch A duyệt request customer/lot branch BCó request refund_km2_wallet ngoài scopeBị chặn bởi branch_mode; không đổi lot/walletP0

TC-PERM-001: Dynamic Permission v2 regression

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-PERM-01Grant/revoke internal_configuration:updateGrant rồi revoke user test, refresh/reloginMenu/settings/flag KM2 hiện rồi ẩn đúng; mutation trực tiếp bị chặn sau revokeP0
TC-PERM-02Grant/revoke service_order:paymentGrant rồi revoke trên POS portalChip Ví KM2 hiện rồi ẩn trong payment DV; deduct_km2_payment no-leak khi revokeP0
TC-PERM-03Grant/revoke product_order:paymentGrant rồi revoke trên POS portalChip Ví KM2 hiện rồi ẩn trong payment mỹ phẩm/SP; API no-leak khi revokeP1
TC-PERM-04customer_management:access + branch scopeStaff có quyền xem customer A, không xem customer BAction get_customer_km2_lots trả A, chặn BP0
TC-PERM-05view_all behaviorRole có/không có customer_management:view_allview_all xem all-branch nếu branch_mode cho phép; không có thì branch scopedP1

TC-FR-010: Report dashboard

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-010-01Report summary theo thángCó bản ghi mua active/exhausted/expiredKPI số Gói Ví KM2 bán, tổng nạp, đã dùng, tỉ lệ sử dụng Ví KM2 đúngP1
TC-010-02Manager branch scopeĐăng nhập Manager branch AChỉ thấy lot bán tại branch A trong reportP1
TC-010-03Denominator = 0Không có bản ghi mua trong kỳTỉ lệ hiển thị "—", không NaN/0% sai nghĩaP2

TC-FR-011: ZNS thông báo

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-011-01FR-011 tắt ở Phase 1PO chưa bật ZNSKhông block bán Gói Ví KM2/thanh toán, không gửi ZNSP1
TC-011-02FR-011 bật và template đã đăng kýBán Gói GoldGửi đúng 1 ZNS activation với biến qty/package/amount/expired_atP2

TC-FR-012: Hiển thị Ví KM 2 trong màn hiện có

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-012-01Prepaid order summary có KM2Mix thẻ VND + Gói Ví KM2Cột/dòng KM2 hiện conditional, flow VND cũ không đổiP0
TC-012-02Invoice print labelĐơn thanh toán wallet_promotion_2Hóa đơn/preview hiện label "Ví KM 2", không hiện nhầm "Ví khuyến mãi" KM1P0
TC-012-03Fund không tạo incoming cho phần KM2 thanh toánĐơn có cash + KM2Fund chỉ ghi phần cash/non-wallet; KM2 không tính actual revenueP0
TC-012-04Cosmetic order paymentĐơn mỹ phẩm có item eligible KM2Bảng payment hiện wallet_promotion_2_amount, total đúngP1
TC-012-05Product order eligibilityProduct không eligible KM2Không tính vào eligible_totalP1
TC-012-06Report DV/NV tách KM2Có invoice KM1 + KM2Report không gộp nhầm KM1/KM2, cột/segment KM2 đúngP1
TC-012-COMM-KM2-OFFRULE-COMM-001 — Default config: bán Gói KM 2 có CTV thanh toán bằng KM 2Migration 3 đã seed affiliate_config(prepaid, wallet_promotion_2, FALSE). Khách có CTV wallet_receive_commission='promotion', mua Gói Gold 500k thanh toán bằng wallet_promotion_2SELECT * FROM invoice_affiliate WHERE invoice_id = ...0 row. Log BE có dòng "Affiliate not enable for payment method: wallet_promotion_2"P0
TC-012-COMM-KM2-CASHRULE-COMM-001 — Default config: bán Gói KM 2 có CTV thanh toán tiền mặtKhách có CTV, mua Gói Gold 500k thanh toán cash. affiliate_config(prepaid, cash)=TRUE (mặc định Diva)SELECT * FROM invoice_affiliate WHERE invoice_id = ...1 row, commission_amount = 500.000 × policy_tag.promotion_wallet_percent / 100 (hành vi giống KM 1)P0
TC-012-COMM-KM2-MIXRULE-COMM-001 — Default config: MIX paymentKhách có CTV, mua Gói Gold 500k thanh toán 60% cash + 40% wallet_promotion_22 sub-invoice tạo ra với payment_method_id khác nhau. Gate InsertInvoiceAffiliate chạy per sub-invoice; kết quả phụ thuộc ô matrix: sub-invoice cash → 1 row commission; sub-invoice ví → 0 row commission. Commission chỉ tính trên 300k cashP0
TC-012-COMM-KM2-ADMIN-OVERRIDERULE-COMM-001 — Admin bật overrideAdmin vào /s/internal-settings/affiliate, bật ô (service, wallet_promotion_2) → khách dùng KM 2 thanh toán đơn DV có CTVinvoice_affiliate có row commission với wallet_type_id='VND_PROMOTION_2'. Gate affiliate_config thật sự respect admin override (không có hard-code skip nào)P1
TC-012-COMM-DISPLAYSCR-FR012-COMM-01 — OrderCommissionItem hiển thị row KM 2Tiếp tục từ TC-012-COMM-KM2-ADMIN-OVERRIDE: vào trang chi tiết đơn DV đó trong Admin/POSComponent OrderCommissionItem hiển thị row commission của VND_PROMOTION_2 (filter mở rộng); không lọc bỏ vì filter mới include cả 2 wallet typeP1
TC-012-SMS-KM2-BALANCESCR-FR012-SMS-01 — SMS sau invoice complete bao gồm KM 2Khách có balance KM 1 = 100k + balance KM 2 = 5tr, thanh toán đơn DV 200k bằng KM 2SMS gửi đi có nội dung "Vi chinh: ..., Vi KM1: 100.000, Vi KM2: 4.800.000. ND: ..." (3 dòng balance)P0
TC-012-SMS-KM2-ONLYSCR-FR012-SMS-01 — Khách chỉ có KM 2Khách balance KM 1 = 0, KM 2 = 3trSMS render: "Vi chinh: ..., Vi KM2: 2.800.000. ND: ..." (skip dòng KM 1 vì balance = 0)P1
TC-012-SMS-KM1-ONLYSCR-FR012-SMS-01 — Backwards compat khách không có KM 2Khách balance KM 1 = 100k, không có KM 2SMS render giống cũ: "Vi chinh: ..., Vi KM1: 100.000. ND: ..." (skip dòng KM 2)P1
TC-012-NEG-INVOICE-KM2SCR-FR012-NEG-02 — Hoàn đơn DV thanh toán KM 2Tạo đơn DV 500k thanh toán KM 2, sau đó hoàn đơnNegativeInvoicesTable hiện row với cột "Vào Ví KM 2" giá trị âm (-500.000); cột "Vào ví KM 1" trong row đó = 0 hoặc emptyP0
TC-012-NEG-INVOICE-MIXSCR-FR012-NEG-02 — Mix refund KM 1 + KM 2Đơn DV 1tr thanh toán 60% KM 1 + 40% KM 2, hoàn toàn bộNegativeInvoicesTable có 2 row hoặc 1 row với cả 2 cột: "Vào ví KM 1" = -600k, "Vào Ví KM 2" = -400kP1
TC-012-AFF-MATRIX-RENDERSCR-FR012-AFF-01 — UI Admin Affiliate Configuration tự render cột mớiSau Migration 3, vào /s/internal-settings/affiliate tab "Cấu Hình Chung"Matrix có cột mới "VÍ KHUYẾN MÃI 2" giữa các cột hiện có; checkbox cho 3 row (DV/Mỹ phẩm/Nạp tiền) đều OFF mặc địnhP0
TC-012-AFF-MATRIX-TOGGLESCR-FR012-AFF-01 — Admin toggle ô KM 2Admin tick (service, wallet_promotion_2) → Lưu → khách dùng KM 2 thanh toán đơn DV có CTVSave thành công; sau đó invoice_affiliate có row commission (kết hợp TC-012-COMM-KM2-ADMIN-OVERRIDE)P1

TC-SCR-09: App khách Flutter — Tab Ví khuyến mãi 2 trong wallet_screen.dart + Home widget (PD-002, DEC-031)

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-SCR09-01Khách có lot active vào tab Ví khuyến mãi 2Khách có 2 lot active (Gold 1.2tr, Diamond 3.5tr); mở wallet_screen.dart, switch sang tab "Ví khuyến mãi 2" (tab thứ 3)Tab Ví khuyến mãi 2 hiển thị balance = 4.7tr (realtime từ get_customer_km2_balance), 2 row lot trong bảng "Đang hoạt động"; balance card phía trên cập nhật giá trị 4.7trP0
TC-SCR09-02Empty state — khách chưa có lot nàoKhách chưa mua Gói KM 2 bao giờ; switch tab Ví khuyến mãi 2Tab hiện empty state: "Bạn chưa có Gói Ví KM 2 nào. Đến spa để mua."P0
TC-SCR09-03Banner cảnh báo sắp hết hạnKhách có lot Gold, expired_at còn 7 ngày; switch tab Ví khuyến mãi 2Banner ⚠️ hiện trên cùng tab; tap banner cuộn xuống row lot GoldP0
TC-SCR09-04Home widget conditional ẩnKhách có balance KM 2 = 0 (đã dùng hết hoặc chưa có)Home (home_wallet.dart) chỉ hiện 4 card hiện có (DIVA/Commission/Reward Point/Bonus Point); KHÔNG hiện card thứ 5 "Ví KM 2"P1
TC-SCR09-05Home widget hiện khi có balanceKhách có balance > 0Home hiện thêm card thứ 5 "Ví khuyến mãi 2: 4.7tr" + HSD gần nhất; tap → mở wallet_screen.dart với tab Ví khuyến mãi 2 active sẵn (initialTabIndex đúng)P0
TC-SCR09-06Drawer detail lotTap row Gold trong bảng tab Ví khuyến mãi 2Slide-up drawer hiện 2 block: Thông tin Gói + Lịch sử giao dịch (load wallet_km2_lot_deduction của lot đó)P0
TC-SCR09-07Pull-to-refresh tabSau khi NV tại POS bán thêm 1 Gói cho khách, khách pull-to-refresh trên tab Ví khuyến mãi 2Sau refresh balance + danh sách lot cập nhật giá trị mới (realtime qua action get_customer_km2_balance); 2 tab kia KHÔNG bị refetchP0
TC-SCR09-08Security — khách query lot khách khácKhách A gọi action get_customer_km2_lots với input.customer_id = <khach_B_id>Backend reject 403 (UNAUTHORIZED); UI hiện "Không thể tải. Tải lại"P0
TC-SCR09-09Feature flag tắtAdmin tắt KM 2 (wallet_km2_config.disabled=true)Card home ẩn (conditional balance); switch tab Ví khuyến mãi 2 → hiển thị empty stateP1
TC-SCR09-10Offline modeKhách mở tab Ví khuyến mãi 2 offlineHiển thị cached balance gần nhất + banner "Đang ngoại tuyến — số dư có thể chưa cập nhật"P2
TC-SCR09-11Lot vừa hết hạn (cron miss)Lot có expired_at < NOW() nhưng scheduler chưa chạy; expired_unswept_amount > 0Action trả available_amount không bao gồm số này (realtime), UI hiển thị balance đúng; bảng "Đã hết hạn" hiện lot này sau scheduler chạyP1
TC-SCR09-12ZNS deep link mở tabKhách tap ZNS km2_lot_activated (FR-011)App mở wallet_screen.dart với tab Ví khuyến mãi 2 active sẵn (không phải 2 tab cũ)P2
TC-SCR09-13Regression — 2 tab hiện hữu KHÔNG bị vỡMở wallet_screen.dart, switch giữa tab "Ví thẻ trả trước" + "Ví khuyến mãi" + "Ví khuyến mãi 2"Cả 3 tab work bình thường, balance card switch đúng giá trị từng tab; KHÔNG có visual regression (folder shape / chip / TabBar tuỳ implementation chọn)P0
TC-SCR09-14DEC-032 — Affiliate radio button không có KM 2NV mở form tạo service order ở staff app, gắn 1 CTV (affiliate_user)Component AffiliateFor chỉ hiện 2 radio option [Hoa hồng, Ví khuyến mãi], KHÔNG có option "Ví khuyến mãi 2" (DEC-032 Option α)P0
TC-SCR09-15Regression FolderTabs — KPI/Tet screens không bị vỡ (nếu dev chọn refactor FolderTabs)Mở kpi_management_screen.dart, revenue_kpi_screen.dart, tet_holiday_gifting_screen.dart2 tab hiển thị đúng pattern hiện có; folder shape (nếu giữ widget gốc) hoặc widget mới (nếu swap) consistentP0

TC-SCR09-ENUM: WalletType enum + l10n shared (SCR-09-MOBILE-03)

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-SCR09-ENUM-01Enum serializationGọi WalletType.promotion2.type (Dart enum)Trả về string 'VND_PROMOTION_2' (đồng bộ với BE wallet_type.id)P0
TC-SCR09-ENUM-02Localization VIMở app với locale viWalletType.promotion2.walletLocalized(context) trả về "Ví khuyến mãi 2"P0
TC-SCR09-ENUM-03Localization ENMở app với locale enWalletType.promotion2.walletLocalized(context) trả về tương đương EN (đồng bộ với ARB)P1
TC-SCR09-ENUM-04JSON deserializationResponse API có {"wallet_type_id": "VND_PROMOTION_2"}Parse thành công thành WalletType.promotion2 (qua _$WalletTypeEnumMap)P0
TC-SCR09-ENUM-05Backwards compatResponse API có {"wallet_type_id": "VND_PROMOTION"}Vẫn parse thành WalletType.promotion cũ (không bị ảnh hưởng)P0

TC-SCR-10: Staff app Flutter — Customer detail balance KM 2 (SCR-10-STAFF-01)

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-SCR10-01Khách có balance KM 2Staff/Manager mở customer detail của khách có lot active (balance 4.7tr)Section "Ví của khách" hiện đủ 4 dòng: Ví DIVA, Hoa hồng, Ví khuyến mãi (KM 1), Ví khuyến mãi 2 (mới — 4.700.000 đ)P0
TC-SCR10-02Khách KHÔNG có balance KM 2 (conditional ẩn)Khách chưa mua KM 2; balance = 0Row "Ví khuyến mãi 2" KHÔNG hiện (conditional balance > 0); các row khác hiện như cũP0
TC-SCR10-03API call song song (performance)Network inspector log customer_repository.impl.dart:473-481Có 2+ call getBalances song song qua Future.wait (promotion + promotion2), KHÔNG tạo serial latencyP1
TC-SCR10-04API fail silentlyMock getBalances(promotion2) trả errorRow KM 2 ẩn (KHÔNG hiện "Đang tải lỗi"); các row khác vẫn hiện bình thườngP1
TC-SCR10-05Permission gatingStaff không có customer_management:accessToàn bộ customer detail bị chặn từ trước (không vào được), nên row Ví KM 2 không reachableP0
TC-SCR10-06Branch scopeManager branch A xem khách branch BBị chặn theo DEC-013 (đồng bộ với pattern Ví KM 1)P1
TC-SCR10-07Pull-to-refresh sau khi POS bán Gói mớiNV bán Gói Gold cho khách → Staff pull-to-refresh customer detailRow "Ví khuyến mãi 2" hiện balance mới (hoặc xuất hiện nếu trước đó balance = 0)P0

TC-IMPACT-001: Regression biên ảnh hưởng / không ảnh hưởng

TCMô tảDữ liệu nhậpKỳ vọngMức ưu tiên
TC-IMPACT-01KM1 không bị đổi nghĩaCustomer có VND_PROMOTION, không có KM2Số dư KM1, thanh toán KM1, report KM1 giữ nguyên; không bị cộng/gộp với KM2P0
TC-IMPACT-02Yêu cầu hoàn tiền có thêm loại Hoàn ví KM2Tạo request refund_km2_walletList/detail/filter hiện "Hoàn ví KM2", đủ field lot/KM2/refund/policy; approve/payment đi qua màn hiện cóP0
TC-IMPACT-03Hoàn đơn đã dùng KM2 khác Hoàn ví KM2Đơn DV đã dùng KM2 bị refundHệ thống hoàn về đúng lot theo deduction; không tạo request refund_km2_wallet nếu đây là refund đơnP0
TC-IMPACT-04Rank/loyalty không đổiĐơn thanh toán một phần bằng KM2Phần KM2 không tính actual revenue/customer points; rank formula không thêm rule mớiP1
TC-IMPACT-05Appointment/kho/CRM assignment không mở scopeCustomer có appointment/order/KM2Lịch hẹn, stock movement, owner/pipeline không đổi; chỉ order payment/profile hiển thị KM2P1
TC-IMPACT-06Notification không dùng nhầm biến KM1Template có wallet_promotion_amount, đơn KM2KM2 không ghi vào biến KM1; nếu bật thông báo KM2 thì dùng biến/template KM2 riêngP1
TC-IMPACT-07Export/report không gộp KM1/KM2Dataset có cash, wallet, KM1, KM2Export/report có label/field KM2 riêng; wallet_promotion_amount vẫn chỉ là KM1P1

D3) Dữ liệu seed

Dataset: DS-001 — Dữ liệu test Ví KM 2

Cách tạo: SQL Script

sql
-- 1. Config Ví KM 2: seed mặc định phải tắt. TC-001-01 sẽ bật disabled=false cho các test runtime.
INSERT INTO wallet_km2_config (id, max_percent_per_order, allow_combine_km1, allow_refund, refund_fee_percent, refund_deadline_days, refund_extend_days, disabled)
VALUES (gen_random_uuid(), 20, false, true, 20, 30, 30, true);

-- Sau khi verify TC-001-00, bật config cho các test payment/runtime:
UPDATE wallet_km2_config
SET disabled = false
WHERE branch_id IS NULL AND deleted_at IS NULL;

-- 2. Wallet type + Payment gateway (nếu chưa có)
-- Xem dev-spec C4 seed data

-- 3. Prepaid cards (Gói Ví KM2)
INSERT INTO prepaid_card (id, code, name, value, value_into_wallet, wallet_target, expiry_months, disabled)
VALUES
  (gen_random_uuid(), '#TC-SILVER', 'Gói Silver', 300000, 2000000, 'VND_PROMOTION_2', 3, false),
  (gen_random_uuid(), '#TC-GOLD', 'Gói Gold', 500000, 5000000, 'VND_PROMOTION_2', 6, false);

-- 4. Products với allow_promo_wallet_2
UPDATE product SET allow_promo_wallet_2 = true WHERE name ILIKE '%massage%' OR name ILIKE '%gội%';
UPDATE product SET allow_promo_wallet_2 = false WHERE name ILIKE '%kem%' OR name ILIKE '%serum%';

-- 5. Test customer với 2 lots
-- (Tạo qua flow bán Gói Ví KM2, hoặc INSERT trực tiếp cho test)

-- Verify: Kiểm tra dữ liệu
SELECT wt.id, wt.name FROM wallet_type wt WHERE id = 'VND_PROMOTION_2';
SELECT COUNT(*) FROM wallet_km2_config WHERE deleted_at IS NULL;
SELECT COUNT(*) FROM prepaid_card WHERE wallet_target = 'VND_PROMOTION_2' AND disabled = false;

D4) Truy vết

FRTC-IDCoverageTrạng thái
FR-001TC-001-*7 ca kiểm thử (3 happy + 4 edge)
FR-002TC-002-*5 ca kiểm thử (2 happy + 3 edge/regression)
FR-003TC-003-*7 ca kiểm thử (2 happy + 5 edge)
FR-004TC-004-* + TC-IDEMPOTENT-* + TC-BALANCE-RACE-*13 ca payment + 9 ca idempotency (TG-003) + 5 ca balance race (DEC-029)
FR-005TC-005-*4 ca kiểm thử (permission + regression KM1)
FR-006TC-006-*11 ca kiểm thử (1 happy + 5 edge/security + 5 popup lịch sử KM 2 SCR-06-NEW-03)
FR-007TC-007-*3 ca kiểm thử (1 happy + 2 edge)
FR-008TC-008-*4 ca kiểm thử (1 happy + 3 edge)
FR-009TC-009-*8 ca kiểm thử (1 happy + 7 edge/permission)
FR-010TC-010-*Phase 3, 3 ca kiểm thử
FR-011TC-011-*Tuỳ phase, 2 ca kiểm thử
FR-012TC-012-*17 ca: 6 hiện hữu + 5 commission (RULE-COMM-001, gồm TC-COMM-DISPLAY) + 3 SMS (SCR-FR012-SMS-01) + 2 negative invoice (SCR-FR012-NEG-02) + 2 affiliate matrix (SCR-FR012-AFF-01)
SCR-09 (mobile customer)TC-SCR09-*15 ca app khách Flutter (PD-002, DEC-031, DEC-032): tab Ví khuyến mãi 2 trong wallet_screen.dart (tab thứ 3), home widget, drawer, security, offline, ZNS deep link, regression 2 tab cũ, regression 3 màn dùng FolderTabs khác, affiliate radio button
SCR-09-MOBILE-03 (enum + l10n)TC-SCR09-ENUM-*5 ca WalletType.promotion2 enum + l10n key
SCR-10 (mobile staff)TC-SCR10-*7 ca staff app Flutter (SCR-10-STAFF-01): customer detail balance KM 2 conditional, API parallel, permission, branch scope
Permission v2TC-PERM-*5 ca kiểm thử grant/revoke/portal/branch/API no-leak
Impact boundaryTC-IMPACT-*7 ca regression biên ảnh hưởng / không ảnh hưởng
Tổng137 ca kiểm thử (94 + 11 TC-012 mở rộng + 15 TC-SCR09 + 5 TC-SCR09-ENUM + 7 TC-SCR10 + 5 TC-006 mở rộng SCR-06-NEW-03)

D5) Điều kiện vào/ra kiểm thử

Điều kiện vào (bắt đầu test)

  • [ ] BE deploy xong (migrations wallet + ecommerce + Hasura metadata)
  • [ ] FE deploy xong trên test env
  • [ ] Seed data DS-001 có sẵn
  • [ ] Test accounts: 1 Admin, 1 Manager, 1 Staff, 1 Customer (có wallet VND_PROMOTION_2)
  • [ ] Đã verify TC-001-00: sau migration config KM2 ở trạng thái tắt
  • [ ] Sau đó Admin bật config Ví KM 2 (max 20%) để chạy các test runtime/payment
  • [ ] Ít nhất 2 Gói Ví KM2 tạo sẵn (Silver + Gold)
  • [ ] Ít nhất 2 sản phẩm eligible + 2 non-eligible

Điều kiện ra (kết thúc test)

  • [ ] Tất cả P0 ca kiểm thử trong D2 PASS
  • [ ] Tất cả P1 ca kiểm thử PASS hoặc có waiver từ PO/TL
  • [ ] P2 ca kiểm thử: document known issues nếu FAIL
  • [ ] Gate kỹ thuật TG-001..TG-007 trong handoff.md có bằng chứng pass hoặc waiver rõ owner/hạn xử lý
  • [ ] Performance: FIFO deduction < 500ms cho 10 lots
  • [ ] Không còn bug critical/major đang mở