Skip to content

SubscriptionManager

Manages core subscription lifecycle: creation, cancellation, resumption, renewal, and trade fund status.

High-Level Design

SubscriptionManager is the central authority for subscription state within a UserPaperTradePortfolio DO. It coordinates with several peer managers and cross-DO RPCs to keep subscription state consistent.

Read-Time Status Calculation

Status is never stored — it's derived on every read from four stored fields:

billingCycleStartAt + billingCycleInDays → currentCycleEndAt
currentCycleEndAt + 3 days              → gracePeriodEnd
cancelledAt                             → wind-down signal
tradeFundStatus                         → hard override

Priority rules:

  1. tradeFundStatus = 'granted'SUBSCRIBED (overrides everything)
  2. tradeFundStatus = 'revoked'EXPIRED (overrides everything)
  3. Free-trial / Sponsored: past cycle end or cancelled → EXPIRED, else SUBSCRIBED
  4. Regular: within cycle → SUBSCRIBED or WIND_DOWN (if cancelled); within grace period (and not cancelled) → GRACE_PERIOD; else EXPIRED

This eliminates state synchronization issues — no background job needed to expire subscriptions.

Subscription Creation Flow

Creation is a multi-step atomic operation with cascading rollback:

1. Insert subscription row
2. Validate & record promo code (PromoCodeRegistry RPC)
   └─ fail → delete subscription, return error
3. Record payment (if regular + txHash)
   └─ fail → delete payment + subscription, return error
4. Start challenge (ChallengeManager)
   └─ fail → delete payment + subscription + promo, return error
5. Record referral to referrer's DO (best-effort, never fails creation)

Each step checks the previous succeeded; on failure, all prior side effects are unwound in reverse order.

Idempotency

Three layers prevent duplicate operations:

  • txHash lookup: Regular subscriptions and renewals check payment_histories for existing txHash before proceeding
  • Trial uniqueness: Only one free-trial per user lifetime; if exists, return it
  • Sponsored gating: Active subscription of any type blocks new sponsored creation

Billing Cycle Mechanics

TypeCycleGrace PeriodBilling Date
Regular30 days+3 days after cycle endcycleEnd − 1 day
Free-trial21 daysNoneN/A
SponsoredVariableNoneN/A

Renewal extends billingCycleStartAt to the current cycle end, starting a fresh 30-day cycle. If a scheduledPlan exists (downgrade), renewal applies it — closing all positions, resetting balance, and starting a new challenge on the lower plan.

Lazy Pending Credit Conversion

Pending reset credits (granted by promo codes or admin) have an effectiveAfter timestamp. Rather than a background job, credits are converted to active on every getSubscription() call:

getSubscription() → resetCreditsManager.convertPendingCredits()
  for each pending where effectiveAfter <= now:
    move to active credits, update DB

Cancellation & Grace Period

  • Cancel during active cycle: Sets cancelledAt, status becomes WIND_DOWN. User keeps trading until cycle end.
  • Resume during active cycle: Clears cancelledAt, returns to SUBSCRIBED.
  • Cancel during grace period: Deactivates challenge immediately (closes alerts), status becomes EXPIRED.
  • Grace period without cancellation: User retains GRACE_PERIOD status for 3 days, allowing renewal without data loss.

Trade Fund Override

Trade fund status provides admin-level control that bypasses all billing logic:

  • granted → forces SUBSCRIBED regardless of cycle state
  • revoked → forces EXPIRED regardless of cycle state
  • not_granted → normal billing rules apply

In-Memory Cache

The manager caches the subscription object in memory. Because DOs are single-threaded, this is safe without invalidation complexity. Every mutation updates the cache; peer managers access it via Internal-suffixed methods (getLatestSubscriptionInternal, refreshCacheInternal, invalidateCacheInternal).

Edge Cases

  • Free-trial is one-per-user; any past subscription blocks trial creation
  • Promo code validation failure rolls back the subscription insert
  • Challenge creation failure rolls back subscription, payment, and promo records
  • Referral recording is best-effort (errors logged, not propagated)
  • Grace period cancellation deactivates challenge immediately
  • Renewal with scheduled downgrade closes all positions and resets balance

See Also