Custom Plan (Custom Contract)

Overview

This document describes the custom plan flow (contract-based pricing) using Stripe Checkout with price_data. Custom plans are isolated from base plan mapping to avoid impacting catalog prices and standard plan-change flows.

Core Data Model

subscriptions

  • pricing_type: custom
  • custom_contract_id: contract link
  • payment_provider_subscription_id: Stripe sub_...

custom_contracts

  • Terms: amount, currency, billing_interval, starts_at, ends_at, limits
  • Stripe anchors:
    • provider_checkout_session_id (Stripe cs_...)
    • provider_price_id (Stripe price_...)
    • provider_subscription_item_id (Stripe si_...)

subscription_histories

Per-invoice snapshots:

  • custom_contract_id
  • provider_price_id
  • provider_subscription_item_id
  • invoice_id / payment_intent_id

End-to-End Flow

1) Create Custom Contract (Admin)

API:

  • POST /api/v1/admin/custom-contracts

Logic:

  • Create contract in draft
  • Link existing subscription or create a new one with pricing_type = custom

2) Send Payment Link (Admin)

API:

  • POST /api/v1/admin/custom-contracts/{id}/send-payment-link

Logic:

  • Create Stripe Checkout Session with price_data
  • Set metadata:
    • custom_contract_id
    • subscription_slug
  • Update custom_contracts.provider_checkout_session_id
  • Move status draftoffered

3) Stripe Webhooks (Checkout + Subscription)

customer.subscription.created

  • Map subscription to contract:
    • subscriptions.pricing_type = custom
    • subscriptions.custom_contract_id = <id>
  • Update custom_contracts:
    • provider_price_id
    • provider_subscription_item_id
    • subscription_id

invoice.paid

  • Create/update subscription_histories for the billing cycle
  • Update custom_contracts.provider_price_id / provider_subscription_item_id (fallback from invoice line)
  • On success: custom_contracts.status = active

customer.subscription.updated

  • Sync subscription status
  • Create renewal/change history for the custom contract if needed

customer.subscription.deleted

  • Cancel subscription and update contract:
    • cancelled if terminated early
    • expired if ends_at has passed

Guardrails

  • Custom plans do not use package_plan_to_providers
  • Price or scope changes must go through the custom contract + webhooks
  • Stripe metadata is the key for correct mapping

Notes

  • Webhooks can arrive out of order; the handlers are designed to be idempotent.
  • If amount = 0, still rely on invoice.paid to activate the contract.