Cập nhật gói đang dùng (nâng cấp / hạ gói)
Feature URL
Module
subscription-package
Status
shipped
Priority
P1
Platforms
fe · be
AC progress
14 / 14
Last reviewed
2026-05-30
Mục tiêu
Cho phép user đang có gói active đổi sang gói khác (nâng cấp lên tier cao hơn, hoặc kéo dài chu kỳ ở cùng tier) và được hoàn lại phần credits chưa dùng của gói cũ theo pro-rata. Quy tắc nghiệp vụ hiện tại của backend là upgrade-only: mọi thao tác hạ gói (hạ tier hoặc rút ngắn chu kỳ) và mua lại đúng gói đang dùng đều bị từ chối với 409. Feature này làm rõ ranh giới đó để FE biết khi nào nên cho phép thao tác.
Phạm vi
Trong phạm vi (In scope):
- Đổi gói khi user đang có gói active, qua cùng endpoint
POST /subscriptions/me/purchase. - Cho phép: nâng tier (PREMIUM → PRO → MAX), hoặc giữ nguyên tier nhưng kéo dài chu kỳ (1 tháng → 12 tháng).
- Hoàn pro-rata phần credits chưa dùng của gói cũ (tính trên kỳ trả phí, không tính bonus months) vào ví, rồi trừ giá gói mới.
- Reset daily free unlock về 0 khi là nâng tier thật sự (rank đích > rank hiện tại).
- Expire gói cũ, kích hoạt gói mới active, snapshot giá mới.
- Làm rõ các trường hợp bị từ chối (409): hạ tier, hạ/rút ngắn chu kỳ, mua lại đúng tier + đúng chu kỳ.
Ngoài phạm vi (Out of scope):
- Mua gói lần đầu khi chưa có gói active — xem package-purchase.
- Gia hạn cùng gói khi hết hạn / tự động gia hạn — xem package-renewal.
- Hỗ trợ hạ gói (downgrade) — hiện backend chưa hỗ trợ, chỉ trả 409.
- Hoàn tiền bonus months (không có giá trị hoàn).
- Đổi gói bằng VND qua cổng thanh toán (chỉ qua ví V-Credits).
User Stories
- Là user đang dùng PREMIUM, tôi muốn nâng lên PRO/MAX để có thêm quyền lợi, và được hoàn phần tiền chưa dùng của PREMIUM.
- Là user đang dùng gói 1 tháng, tôi muốn chuyển sang gói 12 tháng (cùng tier) để hưởng giá tốt và bonus months.
- Là user, khi tôi cố hạ gói hoặc mua lại đúng gói đang dùng, tôi muốn nhận thông báo rõ ràng rằng thao tác không được phép thay vì bị trừ tiền nhầm.
- Là hệ thống, tôi muốn từ chối downgrade để tránh phải xử lý hoàn tiền phức tạp và lạm dụng pro-rata.
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 mới + chu kỳ, bấm Đổi gói
App->>BE: POST /subscriptions/me/purchase {packageCode, cycleMonths}
BE->>BE: Lấy gói active hiện tại + gói/plan đích
BE->>BE: validatePlanChange (so rank tier + cycleMonths)
alt Hạ tier
BE-->>App: 409 StateConflict "downgrade"
else Rút ngắn chu kỳ
BE-->>App: 409 StateConflict "downgrade-duration"
else Đúng tier + đúng chu kỳ
BE-->>App: 409 StateConflict "subscribe-same-plan"
else Hợp lệ (nâng tier hoặc kéo dài chu kỳ)
BE->>BE: Tính refund pro-rata credits chưa dùng của gói cũ
BE->>BE: Expire gói cũ, tạo UserPackage mới active
BE->>Wallet: Hoàn refundCredits (nếu > 0) rồi trừ giá gói mới
alt Số dư không đủ sau hoàn
Wallet-->>BE: InsufficientCredits
BE-->>App: 409 (rollback)
else OK
BE->>BE: Reset daily unlock nếu nâng tier, notify + event
BE-->>App: EntitlementResponse mới
end
endAcceptance Criteria
- AC-1: Đổi gói dùng cùng endpoint
POST /subscriptions/me/purchase, authoritysubscription:manage:own. - AC-2: Nâng tier (rank đích > rank hiện tại) ở cùng hoặc dài hơn chu kỳ → cho phép.
- AC-3: Giữ nguyên tier, kéo dài chu kỳ (
cycleMonthsđích > hiện tại) → cho phép. - AC-4: Hạ tier (rank đích < rank hiện tại) → 409
StateConflictExceptionlý dodowngrade. - AC-5: Rút ngắn chu kỳ (
cycleMonthsđích < hiện tại) → 409 lý dodowngrade-duration. - AC-6: Đúng tier + đúng chu kỳ (mua lại gói đang dùng) → 409 lý do
subscribe-same-plan. - AC-7: Tier rank dùng thứ tự
SubscriptionPackageCode: FREEMIUM < PREMIUM < PRO < MAX < TEAM;PREMIUM_TEAMvà code custom có rank không xác định nên bỏ qua check downgrade. - AC-8: Khi đổi gói hợp lệ, hoàn pro-rata phần credits chưa dùng của gói cũ vào ví trước khi trừ giá gói mới.
- AC-9: Pro-rata chỉ tính trên kỳ trả phí (
startedAt → paidExpiredAt), không tính bonus months. - AC-10: Công thức refund:
unused = priceCredits - floor(priceCredits × usedDays / totalDays); chưa quastartedAtthì hoàn full, đã quapaidExpiredAtthì hoàn 0. - AC-11: Reset daily free unlock về 0 chỉ khi là nâng tier thật sự (rank đích > rank hiện tại); kéo dài chu kỳ cùng tier giữ nguyên bộ đếm.
- AC-12: Số dư ví không đủ (sau khi cộng refund) → 409, rollback toàn bộ.
- AC-13: Đổi gói thành công expire gói cũ (
EXPIRED), tạoUserPackagemớiACTIVE, snapshot giá mới, gửi notification + publishPaidPackageActivatedEvent(nếu VND > 0). - AC-14: Trả về
EntitlementResponsephản ánh gói mới.
Quy tắc nghiệp vụ
- Upgrade-only: backend chỉ cho di chuyển lên tier cao hơn hoặc kéo dài chu kỳ ở cùng tier. Mọi downgrade bị chặn ở
validatePlanChangevới 409. - Tier rank lấy theo
ordinal()củaSubscriptionPackageCode. Code không thuộc enum (custom/deprecated,PREMIUM_TEAM) có rank = -1 → không bị check downgrade theo tier (nhưng vẫn bị check theo chu kỳ và same-plan). - Hoàn tiền (
refundCredits) chỉ tính trên kỳ trả phí của gói cũ; bonus months kéo dài quyền lợi nhưng không hoàn. - Refund được cộng vào ví trước khi trừ giá gói mới, trong cùng transaction; thiếu tiền thì rollback.
- Reset daily unlock độc lập với cờ
isRenewal; chỉ phụ thuộc vào việc có phải nâng tier thật sự không. - Đổi gói cũng đi qua cùng logic mua, nên các lỗi
PLAN_NOT_AVAILABLE,PACKAGE_NOT_PURCHASABLE, gói khôngACTIVE(404) vẫn áp dụng. - MVP: mỗi user tối đa một gói active — đổi gói luôn expire gói cũ rồi tạo gói mới, không stack.
Dữ liệu & Trạng thái
Entity nghiệp vụ:
UserPackage: gói cũ bị expire + gói mới được tạo —purchasePriceCredits,cycleMonths,bonusMonths,paidExpiredAt,expiredAt,status.Package: catalog gói đích (định nghĩa tại package-view).PurchasePackageRequest:packageCode,cycleMonths,isRenewal.EntitlementResponse: kết quả sau khi đổi gói.SubscriptionPackageCode: enum định nghĩa thứ tự rank tier.
Trạng thái user-facing:
changing— đang gửi yêu cầu đổi gói.upgraded— đổi gói thành công.blocked-downgrade— bị từ chối vì hạ tier.blocked-shorter-cycle— bị từ chối vì rút ngắn chu kỳ.blocked-same-plan— bị từ chối vì mua lại đúng gói đang dùng.insufficient-credits— thiếu V-Credits (sau khi cộng refund) để hoàn tất.
Liên quan
- Phụ thuộc: package-purchase
- Ảnh hưởng: package-renewal, affiliate, masking-unlock
- Nền tảng: package-view