Skip to content

Deferred Subscription Activation

ADR 001: Deferred Subscription Activation

Date: 2026-04
Status: Accepted
Context: Billing system needs to prevent granting user access before payment confirmation.

Problem

When a user initiates a Primer checkout for a subscription:

  1. We must create a subscription in Chargebee
  2. But we cannot grant access until payment clears
  3. How do we prevent premature access while allowing idempotent retry?

Decision

Create subscriptions with start_date = now + 10 years, keeping them in future status until payment is recorded.

Implementation

// Subscription creation (primer.go)
params := &subscription.CreateWithItemsRequestParams{
StartDate: time.Now().AddDate(10, 0, 0), // Far future
ItemId: itemPriceID,
}
result := subscriptionAction.CreateWithItems(customerID, params).Request()
// → Chargebee status = "future" (not yet started)
// Payment recording (payments/processor.go)
// When Primer webhook confirms payment:
params := &subscription.ChangeTermEndRequestParams{
StartDate: 0, // Chargebee interprets 0 as "now"
}
subscription.ChangeTermEnd(subscriptionID, params).Request()
// → Chargebee status = "active" (immediately started)

Rationale

Why This Approach?

  1. Prevents premature access: Until start_date is updated, subscription remains in future status → user has no rights
  2. Detectable pending state: Cleanup worker identifies expired checkouts as future + start > 1 year
  3. Idempotency-friendly: If payment confirmation is retried, we can verify subscription state before re-activating
  4. Chargebee semantics: The 0 value is Chargebee’s documented way to mean “start immediately”

Alternatives Considered

OptionTradeoffs
Activate then deactivate if payment failsRequires subscription reversal; messy state
Use custom pending_checkout statusRequires custom Chargebee fields; not portable
Use scheduled change at payment timeComplex; harder to retry idempotently

Consequences

Positive

  • Simple state machine: Clear futureactivenon_renewingcancelled transitions
  • Cleanup straightforward: Identify orphaned checkouts in one query
  • Idempotency safe: Locking + state check prevents double-activation

Negative

  • Chargebee term_end quirk: Using 0 as “now” is non-obvious; requires documentation
  • 10-year trick is magical: New developers need explanation; commented WHY, not just HOW
  • Renewal timing edge case: If user’s subscription is created but never activated, renewal engine ignores it (OK by design)

Testing

// Test: Subscription starts in future
sub := createPendingSubscription(itemPriceID)
require.Equal(t, domain.SubscriptionStatusFuture, sub.Status)
require.True(t, sub.StartDate.After(time.Now().AddDate(9, 0, 0)))
// Test: Activation moves to active
err := processor.ActivateSubscription(ctx, sub.ID)
require.NoError(t, err)
activated := getSubscription(sub.ID)
require.Equal(t, domain.SubscriptionStatusActive, activated.Status)
require.True(t, activated.StartDate.Before(time.Now().Add(1*time.Hour)))