Subscription Auto Renewal

Description

This document outlines the automated process for subscription auto-renewal. Unlike new registrations, this flow is initiated entirely by Stripe when a subscription's billing period is due for renewal. The system relies on a series of webhooks from Stripe to manage the entire lifecycle of the renewal, from payment processing to handling failures.

The primary webhooks involved are invoice.paid for successful renewals and invoice.payment_failed for failures. The system listens for these events to update the user's subscription status, extend their access period, and manage retries in case of payment issues, ensuring a seamless continuation of service for the user without any manual intervention.

Prerequisites:

  • An active user subscription is approaching its deadline_at date.
  • The subscription has auto_renew enabled.
  • Stripe is configured to automatically attempt renewal payments.

Process Flow Diagram

---
config:
  theme: base
  layout: dagre
  flowchart:
    curve: linear
    htmlLabels: true
  themeVariables:
    edgeLabelBackground: "transparent"
---
flowchart TD
    %% == NODES DEFINITION ==
    StripeScheduler((Stripe Scheduler))

    %% Layer components
    subgraph ApiControllerLayer["API Controller Layer"]
        WebhookController[WebhookController]
    end

    subgraph ApiServiceLayer["API Service Layer"]
        SubscriptionService(SubscriptionService)
        InvoiceHandler(SubscriptionInvoiceHandler)
    end

    subgraph DatabaseLayer["Database Layer"]
        SubscriptionDB[(subscriptions)]
        HistoryDB[(subscription_histories)]
        WebhookEventsDB[(stripe_webhook_events)]
    end

    subgraph ExternalServices["External Services"]
        StripeAPI((Stripe API))
    end

    %% == STEPS DEFINITION ==
    StepW1["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W1</span><p style='margin-top: 8px'>Attempt Payment & Send Webhook<br/>(invoice.paid / invoice.payment_failed)</p></div>"]
    StepW2["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W2</span><p style='margin-top: 8px'>Verify & Log Webhook</p></div>"]
    StepW3["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W3</span><p style='margin-top: 8px'>Process Event with Invoice Handler</p></div>"]
    StepW4["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W4</span><p style='margin-top: 8px'>Update Subscription & Create History</p></div>"]

    %% == CONNECTIONS ==
    StripeScheduler --> StripeAPI
    StripeAPI --- StepW1 --> WebhookController
    WebhookController --- StepW2 --> WebhookEventsDB
    WebhookController --- StepW3 --> SubscriptionService
    SubscriptionService --> InvoiceHandler
    InvoiceHandler --- StepW4
    StepW4 --> SubscriptionDB
    StepW4 --> HistoryDB

    %% == STYLING ==
    style ApiControllerLayer fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
    style ApiServiceLayer fill:#f0f8e6,stroke:#339933,stroke-width:2px
    style DatabaseLayer fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style ExternalServices fill:#fcd9d9,stroke:#cc3333,stroke-width:2px
    style StepW1 fill:transparent,stroke:transparent,stroke-width:1px
    style StepW2 fill:transparent,stroke:transparent,stroke-width:1px
    style StepW3 fill:transparent,stroke:transparent,stroke-width:1px
    style StepW4 fill:transparent,stroke:transparent,stroke-width:1px

Use Cases

Case 1: Successful Auto Renewal

Description

When a subscription's recurring payment is processed successfully, Stripe sends an invoice.paid webhook. The system listens for this event to create a new history record for the renewed period and extends the subscription's deadline_at.

Sequence Diagram

sequenceDiagram
    participant StripeAPI as Stripe API
    participant Webhook as WebhookController
    participant SubSvc as SubscriptionService
    participant InvoiceHandler as SubscriptionInvoiceHandler
    participant DB as Database

    Note over StripeAPI,DB: Successful Auto Renewal Flow
    
    StripeAPI->>Webhook: POST /webhook (invoice.paid, billing_reason: subscription_cycle)
    
    rect rgb(255, 255, 200)
    Note right of Webhook: Verify & Log Event
    Webhook->>DB: Check for existing event (idempotency)
    Webhook->>DB: Create stripe_webhook_events record
    end

    Webhook->>SubSvc: handleInvoicePaid(invoice_data)
    SubSvc->>InvoiceHandler: handleInvoicePaid(invoice_data)

    rect rgb(200, 255, 255)
    Note right of InvoiceHandler: Process Renewal
    InvoiceHandler->>DB: Find subscription by provider_id
    
    alt Subscription is active
        InvoiceHandler->>DB: Begin Transaction
        InvoiceHandler->>DB: Update subscriptions (deadline_at = new_period_end)
        InvoiceHandler->>DB: Create new subscription_histories record (type: renewal, payment_status: paid, amount, etc.)
        InvoiceHandler->>DB: Commit Transaction
        InvoiceHandler-->>SubSvc: Success
    else Subscription not active
        Note over InvoiceHandler: Log error, no action taken.
        InvoiceHandler-->>SubSvc: Failure
    end
    end
    
    SubSvc-->>Webhook: Success
    Webhook->>DB: Update stripe_webhook_events (status: completed)
    Webhook-->>StripeAPI: 200 OK

Steps

Step 1: Receive invoice.paid Webhook

  • Stripe automatically attempts payment for the new billing cycle. Upon success, it sends an invoice.paid webhook with billing_reason: 'subscription_cycle'.

Step 2: Verify and Process Event

  • The WebhookController verifies the event and delegates it to the SubscriptionService, which then calls the SubscriptionInvoiceHandler.

Step 3: Update Subscription and Create History

  • The SubscriptionInvoiceHandler identifies the corresponding subscription in the local database.
  • Within a database transaction, it performs two key actions:
    • It updates the main subscription.deadline_at to the new period's end date provided in the webhook data.
    • It creates a new record in subscription_histories to represent the renewal period. This new record has its subscription_histories.type set to renewal and subscription_histories.payment_status set to paid.

Case 2: Failed Auto Renewal

Description

If a recurring payment fails, Stripe sends an invoice.payment_failed webhook. The system logs this failure. After several failed attempts managed by Stripe, Stripe will automatically change the subscription's status (e.g., to past_due or canceled) and send a corresponding customer.subscription.updated or customer.subscription.deleted webhook, at which point the system updates the local subscription status.

Sequence Diagram

sequenceDiagram
    participant StripeAPI as Stripe API
    participant Webhook as WebhookController
    participant SubSvc as SubscriptionService
    participant InvoiceHandler as SubscriptionInvoiceHandler
    participant DB as Database

    Note over StripeAPI,DB: Failed Auto Renewal Flow

    StripeAPI->>Webhook: POST /webhook (invoice.payment_failed)
    
    rect rgb(255, 220, 180)
    Note right of Webhook: Log Initial Failure
    Webhook->>SubSvc: handleInvoicePaymentFailed(invoice_data)
    SubSvc->>InvoiceHandler: handleInvoicePaymentFailed(invoice_data)
    InvoiceHandler->>DB: Find subscription by provider_id
    InvoiceHandler->>DB: Create new subscription_histories record (type: renewal, payment_status: failed, payment_attempt: 1)
    end

    Note over StripeAPI,StripeAPI: Stripe Smart Retries attempt payment again...

    StripeAPI->>Webhook: POST /webhook (invoice.payment_failed)
    
    rect rgb(255, 220, 180)
    Note right of Webhook: Log Retry Failure
    Webhook->>SubSvc: handleInvoicePaymentFailed(invoice_data)
    SubSvc->>InvoiceHandler: handleInvoicePaymentFailed(invoice_data)
    InvoiceHandler->>DB: Find existing failed history record
    InvoiceHandler->>DB: Update subscription_histories (payment_attempt + 1)
    end

    Note over StripeAPI,StripeAPI: After all retries fail...
    
    StripeAPI->>Webhook: POST /webhook (customer.subscription.updated, status: canceled)
    
    rect rgb(255, 200, 200)
    Note right of Webhook: Cancel Subscription
    Webhook->>SubSvc: handleSubscriptionUpdated(subscription_data)
    SubSvc->>DB: Update subscriptions (status: canceled, canceled_at)
    end

Steps

Step 1: Receive invoice.payment_failed Webhook

  • If a recurring payment fails, Stripe sends this webhook.

Step 2: Log Payment Failure

  • The SubscriptionInvoiceHandler finds the relevant subscription.
  • If this is the first failure for the billing period, it creates a new subscription_histories record with subscription_histories.type set to renewal, subscription_histories.payment_status set to failed, and payment_attempt as 1.
  • On subsequent invoice.payment_failed events for the same cycle, the handler finds the existing failed history record and increments its payment_attempt counter.

Step 3: Stripe Manages Retries and Final Status

  • Stripe's Smart Retries feature automatically attempts to charge the customer again.
  • After all retry attempts are exhausted, Stripe updates the subscription's status (e.g., to past_due, or canceled) and sends a customer.subscription.updated or customer.subscription.deleted webhook.

Step 4: Update Final Subscription Status

  • The application listens for these final status-change webhooks. The SubscriptionLifeCycleHandler processes them to update the main subscription.status to canceled (or past_due) and sets the canceled_at timestamp, officially ending the user's access for that period.

Related Database Structure

erDiagram
    users {
        bigint id PK
        string name "User's full name"
        string email "User's email address (unique)"
        string payment_provider_customer_id "Customer ID from Stripe (nullable)"
        timestamp created_at
        timestamp updated_at
    }
    subscriptions {
        bigint id PK
        string slug "Unique identifier for linking with Stripe metadata"
        bigint user_id FK
        bigint group_id FK
        bigint package_id FK
        bigint package_plan_id FK
        string status "unpaid, active, past_due, canceled"
        string payment_provider_subscription_id "Stripe subscription ID"
        boolean auto_renew "Flag for auto-renewal"
        timestamp deadline_at "Current billing period end"
        timestamp canceled_at "Timestamp of cancellation"
        timestamp created_at
        timestamp updated_at
    }
    subscription_histories {
        bigint id PK
        bigint subscription_id FK
        string status "active, inactive"
        string payment_status "pending, paid, failed"
        string type "new_contract, renewal, change"
        string invoice_id "Stripe invoice ID"
        string payment_intent_id "Stripe payment intent ID"
        timestamp started_at "Period start timestamp"
        timestamp expires_at "Period end timestamp"
        timestamp paid_at "Payment success timestamp"
        integer payment_attempt "Number of failed payment attempts for this cycle"
        timestamp created_at
        timestamp updated_at
    }
    stripe_webhook_events {
        bigint id PK
        string stripe_event_id "Stripe event ID to prevent duplicates"
        string event_type
        enum status "pending, processing, completed, failed"
        text error "Error message if processing fails"
        timestamp created_at
        timestamp updated_at
    }
    users ||--o{ subscriptions : has
    subscriptions ||--o{ subscription_histories : has

Related API Endpoints

Method Endpoint Controller Description
POST /api/v1/admin/stripe/webhook WebhookController@handleWebhook Listens for and processes all incoming events from Stripe related to subscriptions.

Error Handling

  • Log:

    • All webhook processing failures are logged to the application log.
    • Webhook processing errors are stored in the error column of the stripe_webhook_events table.
  • Error Detail:

Status Code Error Message Description
400 Invalid webhook signature. The request did not originate from Stripe.
404 Subscription not found for webhook. The subscription ID from the webhook does not match any record in the database.
500 Database error: {message} A database operation failed during webhook processing.

Additional Notes

  • Automation: The entire renewal flow is automated and requires no user interaction from the point of initial subscription.
  • Single Source of Truth: Stripe is the source of truth for the subscription's billing status. The local database is updated to reflect the state changes communicated via webhooks.
  • Idempotency: The system uses the stripe_webhook_events table to track processed event IDs, ensuring that each webhook is only processed once, even if Stripe sends it multiple times.
  • Retry Logic: Payment retry logic is handled by Stripe's "Smart Retries". The application's role is to log these attempts and react to the final subscription status change (e.g., cancellation).