Bỏ qua nội dung

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

  • 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.
  • 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.
  • 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.
  • 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
    end

Acceptance Criteria

  • AC-1: Đổi gói dùng cùng endpoint POST /subscriptions/me/purchase, authority subscription: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 StateConflictException lý do downgrade.
  • AC-5: Rút ngắn chu kỳ (cycleMonths đích < hiện tại) → 409 lý do downgrade-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_TEAM và 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 qua startedAt thì hoàn full, đã qua paidExpiredAt thì 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ạo UserPackage mới ACTIVE, snapshot giá mới, gửi notification + publish PaidPackageActivatedEvent (nếu VND > 0).
  • AC-14: Trả về EntitlementResponse phả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 ở validatePlanChange với 409.
  • Tier rank lấy theo ordinal() của SubscriptionPackageCode. 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ông ACTIVE (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