Bỏ qua nội dung

Mua gói package bằng V-Credits

Feature URL
Module
subscription-package
Status
shipped
Priority
P0
Platforms
fe · be
AC progress
16 / 16
Last reviewed
2026-05-30

Mục tiêu

Cho phép user mua một gói thuê bao trả phí bằng ví V-Credits của mình. Đây là flow kích hoạt gói lần đầu (từ trạng thái chưa có gói trả phí), trừ tiền ví và bật quyền lợi của gói ngay trong cùng một transaction để không bao giờ xảy ra trạng thái “kích hoạt gói nhưng chưa trừ tiền” hoặc ngược lại.

Phạm vi

Trong phạm vi (In scope):

  • API mua gói POST /subscriptions/me/purchase bằng V-Credits, yêu cầu đăng nhập.
  • Body PurchasePackageRequest: packageCode (bắt buộc), isRenewal (tuỳ chọn, mặc định false), cycleMonths (tuỳ chọn, mặc định 1).
  • Trừ ví V-Credits đúng bằng giá plan theo cycleMonths, atomic với việc tạo UserPackage active.
  • Chọn plan theo cycleMonths (vd 1 tháng, 12 tháng) từ plans[] của gói.
  • Tạo UserPackage mới với snapshot giá (purchasePriceCredits), cycleMonths, bonusMonths, mốc paidExpiredAtexpiredAt (cộng bonus months).
  • Khi mua lần đầu (chưa có gói active) → reset bộ đếm daily free unlock về 0, set quota theo gói mới.
  • Bật cờ auto-renew trên UserPackage nếu isRenewal = true.
  • Gửi notification “đã mua gói” và publish PaidPackageActivatedEvent (cho affiliate commission) khi giá VND > 0.
  • Trả về EntitlementResponse sau khi kích hoạt.

Ngoài phạm vi (Out of scope):

  • Nâng cấp / hạ gói khi đang có gói active — xem package-change.
  • Gia hạn & tự động gia hạn — xem package-renewal.
  • Nạp tiền vào ví V-Credits — xem wallet-topup-create.
  • Mua gói FREEMIUM (không phải gói trả phí, không kích hoạt qua flow này).
  • Thanh toán trực tiếp bằng VND qua cổng thanh toán (flow này chỉ trừ ví V-Credits).

User Stories

  • user, tôi muốn mua gói trả phí bằng số dư V-Credits để mở khoá quyền lợi (xem clean data, AI tools, unlock nhiều hơn…).
  • user, tôi muốn chọn chu kỳ thanh toán (1 tháng hoặc 12 tháng + bonus months) khi mua để tối ưu chi phí.
  • user, tôi muốn bật auto-renew ngay khi mua để khỏi lo gói hết hạn.
  • hệ thống, tôi muốn việc trừ ví và kích hoạt gói là atomic để thiếu tiền thì không kích hoạt, kích hoạt rồi thì chắc chắn đã trừ tiền.

Luồng chức năng

sequenceDiagram
    actor User
    participant App as FE/Mobile
    participant BE
    participant Wallet as Ví V-Credits

    User->>App: Chọn gói + chu kỳ, bấm Mua
    App->>BE: POST /subscriptions/me/purchase {packageCode, isRenewal, cycleMonths}
    BE->>BE: Tìm gói ACTIVE theo code (404 nếu không có / không ACTIVE)
    BE->>BE: Lấy plan theo cycleMonths (PLAN_NOT_AVAILABLE nếu không có)
    alt Giá plan <= 0
        BE-->>App: 409 PACKAGE_NOT_PURCHASABLE
    else Giá plan > 0
        BE->>BE: Lock user row, expire gói active cũ (nếu có)
        BE->>BE: Tạo UserPackage active (snapshot giá, cycle, bonus)
        BE->>Wallet: Trừ priceCredits
        alt Số dư không đủ
            Wallet-->>BE: InsufficientCredits
            BE-->>App: 409 (rollback toàn bộ)
        else Đủ số dư
            Wallet-->>BE: OK
            BE->>BE: Reset daily unlock (lần mua đầu)
            BE->>BE: Notify "đã mua gói" + publish PaidPackageActivatedEvent (nếu VND > 0)
            BE-->>App: EntitlementResponse mới
        end
    end

Acceptance Criteria

  • AC-1: POST /subscriptions/me/purchase yêu cầu authority subscription:manage:own.
  • AC-2: Body bắt buộc packageCode (không blank); cycleMonths nếu gửi phải dương; isRenewal mặc định false khi null; cycleMonths mặc định 1 khi null.
  • AC-3: packageCode được normalize uppercase trước khi tra cứu.
  • AC-4: Gói không tồn tại hoặc không ở trạng thái ACTIVE → 404 (PackageNotFoundException).
  • AC-5: Không có plan khớp cycleMonths → 409 PLAN_NOT_AVAILABLE (BusinessRuleViolationException).
  • AC-6: Plan có giá priceCredits <= 0 → 409 PACKAGE_NOT_PURCHASABLE.
  • AC-7: Không cho phép kích hoạt FREEMIUM như gói trả phí → 409 INVALID_PLAN_CODE.
  • AC-8: Số dư ví V-Credits không đủ → 409, toàn bộ thay đổi (tạo UserPackage, expire gói cũ) bị rollback.
  • AC-9: Mua thành công tạo UserPackage mới với status = ACTIVE, purchasePriceCredits = giá plan, cycleMonths, bonusMonths từ plan.
  • AC-10: paidExpiredAt = startedAt + cycleMonths; expiredAt = paidExpiredAt + bonusMonths (nếu có bonus).
  • AC-11: UserPackage.renewal = giá trị isRenewal trong request (mặc định false).
  • AC-12: Lần mua đầu (chưa có gói active) → reset daily free unlock về 0 và set quota theo gói mới.
  • AC-13: Trừ ví đúng bằng priceCredits của plan, atomic với kích hoạt gói.
  • AC-14: Mua thành công gửi notification “đã mua gói” cho user.
  • AC-15: Khi giá VND của plan > 0, publish PaidPackageActivatedEvent (cho affiliate commission).
  • AC-16: Trả về EntitlementResponse phản ánh gói vừa kích hoạt.

Quy tắc nghiệp vụ

  • Mua gói chỉ trừ ví V-Credits; VND chỉ dùng để reconciliation và phát event affiliate, không trừ trực tiếp ở flow này.
  • Atomicity: tạo UserPackage → trừ ví → notify → publish event nằm trong cùng một transaction. Lỗi trừ ví (thiếu credits) rollback việc kích hoạt gói để không có “free upgrade”.
  • User row bị pessimistic lock trong suốt giao dịch mua để snapshot unlock_used chính xác, tránh race với decrement free-unlock đồng thời.
  • Khi mua, mọi gói active cũ của user bị set EXPIRED (MVP: mỗi user tối đa một gói active tại một thời điểm).
  • Giá mua được snapshot vào UserPackage.purchasePriceCredits — thay đổi catalog sau đó không ảnh hưởng gói đã mua.
  • bonusMonths kéo dài expiredAt nhưng không nằm trong kỳ trả phí (paidExpiredAt) và không có giá trị hoàn tiền khi nâng cấp sau này.
  • Reset daily unlock chỉ áp dụng cho lần mua đầu / nâng cấp tier thật sự — độc lập với cờ isRenewal.
  • Nếu user đang có gói active và mua một gói khác, flow đi qua nhánh đổi gói (upgrade-only) — chi tiết tại package-change.

Dữ liệu & Trạng thái

Entity nghiệp vụ:

  • Package: catalog gói (định nghĩa tại package-view).
  • UserPackage: bản ghi gói user vừa mua — status, startedAt, paidExpiredAt, expiredAt, renewal, purchasePriceCredits, cycleMonths, bonusMonths.
  • PurchasePackageRequest: body mua gói — packageCode, isRenewal, cycleMonths.
  • EntitlementResponse: kết quả trả về sau khi mua.
  • PaidPackageActivatedEvent: event cho affiliate commission (userId, packageId, userPackageId, priceVnd, activatedAt).
  • UserWallet: ví V-Credits bị trừ khi mua.

Trạng thái user-facing:

  • selecting — user đang chọn gói và chu kỳ.
  • purchasing — đang gửi request mua.
  • purchased — mua thành công, gói active.
  • insufficient-credits — thiếu V-Credits, cần nạp thêm.
  • package-unavailable — gói không tồn tại / không bán / plan không khả dụng / không mua được bằng ví.

Liên quan