Paid Activation Flow
Current paid subscription lifecycle in the service layer, including pending purchase creation, activation, extension, and failure cleanup.
Paid Activation Flow
Audience: backend, QA Scope: current paid lifecycle in
SubscriptionService
Current State
The subscription domain still contains a paid purchase lifecycle, but the current SubscriptionMobileController does not expose purchase or payment-verification routes.
Today, paid subscription activation is a service-level workflow built around:
createPendingPurchase(userId, planId, planPriceId)activateSubscription(pendingSubscriptionId)handlePaymentFailure(pendingSubscriptionId)
Any order or payment integration must call these methods explicitly.
Step 1: Create Pending Purchase
createPendingPurchase():
- loads the target plan and price
- derives the module through
plan -> tier -> module - checks for an existing live subscription for the same user and module
- checks for an existing pending subscription for the same user and module
- either:
- updates the existing pending row, or
- creates a new
subscriptionrow withstatus = "pending_payment"
Notes:
startDateandendDateare initialized tonowin the current implementationpriceSnapshotNpris copied from the chosen plan price- a
subscription_historyrow withcreatedis inserted only when a new pending row is created
Step 2: Activate Pending Purchase
activateSubscription() handles three cases.
Case A: Trial Conversion
If a live trial exists for the same user and module:
- the existing trial subscription is updated to
active planId,planPriceId,priceSnapshotNpr,startDate, andendDateare updated- the
user_trialrow getsconvertedAt - a
trial_convertedhistory row is written - the pending row is deleted
user_accessis updated withgrantType = "subscription"
Case B: Extension / Repurchase
If a live active subscription exists for the same user and module:
- the existing active subscription is extended
- the base date is
max(existing.endDate, now) newEndDate = baseDate + durationDays- the pending row is deleted
user_accessexpiry is extended- a subscription history row with
extendedis written
Case C: First-Time Activation
If no live active/trial subscription exists:
- the pending row becomes the active subscription
status = "active"startDate = nowendDate = now + durationDaysuser_accessis created or updated- a subscription history row with
activatedis written
After all three cases, user access caches are invalidated.
Step 3: Failure Cleanup
handlePaymentFailure() only acts on pending_payment subscriptions.
If the pending row still exists:
- it is deleted
- an activity record is written
No access row is granted during pending state, so failed payment cleanup does not need entitlement rollback.
Access Side Effects
Paid activation always flows through upsertUserAccess().
That method guarantees:
- current entitlement is stored in
user_access - every change gets a
user_access_historyrow - repeated grants for the same
userId + moduleIdupdate the active row rather than creating a new current row
Operational Notes
pending_paymentexists in the schema and analytics, but there is no current mobile controller endpoint to create it- the current implementation supports paid lifecycle orchestration from service code
- documentation should not assume the old payment routes still exist under the subscription controller