Appearance
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 overridePriority rules:
tradeFundStatus = 'granted'→ SUBSCRIBED (overrides everything)tradeFundStatus = 'revoked'→ EXPIRED (overrides everything)- Free-trial / Sponsored: past cycle end or cancelled → EXPIRED, else SUBSCRIBED
- 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_historiesfor 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
| Type | Cycle | Grace Period | Billing Date |
|---|---|---|---|
| Regular | 30 days | +3 days after cycle end | cycleEnd − 1 day |
| Free-trial | 21 days | None | N/A |
| Sponsored | Variable | None | N/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 DBCancellation & 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 staterevoked→ forces EXPIRED regardless of cycle statenot_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