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/purchasebằng V-Credits, yêu cầu đăng nhập. - Body
PurchasePackageRequest:packageCode(bắt buộc),isRenewal(tuỳ chọn, mặc địnhfalse),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ạoUserPackageactive. - Chọn plan theo
cycleMonths(vd 1 tháng, 12 tháng) từplans[]của gói. - Tạo
UserPackagemới với snapshot giá (purchasePriceCredits),cycleMonths,bonusMonths, mốcpaidExpiredAtvàexpiredAt(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
UserPackagenếuisRenewal = true. - Gửi notification “đã mua gói” và publish
PaidPackageActivatedEvent(cho affiliate commission) khi giá VND > 0. - Trả về
EntitlementResponsesau 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
- Là 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…).
- Là 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í.
- Là user, tôi muốn bật auto-renew ngay khi mua để khỏi lo gói hết hạn.
- Là 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
endAcceptance Criteria
- AC-1:
POST /subscriptions/me/purchaseyêu cầu authoritysubscription:manage:own. - AC-2: Body bắt buộc
packageCode(không blank);cycleMonthsnếu gửi phải dương;isRenewalmặc địnhfalsekhi null;cycleMonthsmặ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→ 409PLAN_NOT_AVAILABLE(BusinessRuleViolationException). - AC-6: Plan có giá
priceCredits <= 0→ 409PACKAGE_NOT_PURCHASABLE. - AC-7: Không cho phép kích hoạt
FREEMIUMnhư gói trả phí → 409INVALID_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
UserPackagemới vớistatus = ACTIVE,purchasePriceCredits= giá plan,cycleMonths,bonusMonthstừ plan. - AC-10:
paidExpiredAt = startedAt + cycleMonths;expiredAt = paidExpiredAt + bonusMonths(nếu có bonus). - AC-11:
UserPackage.renewal= giá trịisRenewaltrong 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
priceCreditscủ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ề
EntitlementResponsephả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_usedchí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. bonusMonthskéo dàiexpiredAtnhư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
- Phụ thuộc: package-view, wallet-topup-create
- Ảnh hưởng: package-change, package-renewal, affiliate, masking-unlock