Subscription upgrades and plan history
Userdocks stores subscription history as plan periods. A plan period describes one continuous billing assignment; it is not a payment ledger. Monthly or yearly renewal payments update the current plan period instead of creating new subscription rows.
New subscriptions snapshot inline unitAmount, currency, quantity,
description, recurringInterval, recurringIntervalCount, and the provider
price ID. Their legacy productPriceUuid is null. Existing legacy identifiers
remain readable and usable.
Atomic Stripe upgrades
When a tenant already has an active Stripe subscription, Userdocks compares the
new and current raw unitAmount values. A higher amount in the same currency is
an upgrade. Prices in different currencies cannot be compared and the request is
rejected.
For an upgrade, Userdocks changes the existing Stripe subscription item. It does
not cancel the subscription or create a replacement subscription. The update
uses Stripe's proration_behavior: "always_invoice" to invoice the price
difference immediately and payment_behavior: "pending_if_incomplete" to make
the price change conditional on payment.
- If payment succeeds, Stripe applies the new item price and Userdocks starts an
upgradedplan period. - If payment requires authentication or a retry,
nextAction.urlpoints to the Stripe hosted invoice. The old price and Userdocks plan period remain active. - If payment fails or the pending update expires, Stripe discards the pending price change and Userdocks writes no upgrade history.
Userdocks leaves the billing anchor unchanged when the old and new price have matching cadences. When the recurring cadence changes, Stripe applies its normal interval-change behavior: it resets the billing date, credits unused time, and bills the new interval immediately.
An upgrade request can provide description for the new/remaining-time line and
descriptionUnused for the old/unused-time line. Userdocks keeps Stripe's
generated prefixes and service-period suffixes and replaces only the product
name between on and after. The active plan stores description, and the
invoice.created webhook applies it to each later draft renewal line.
Custom-description and discounted upgrades use a controlled invoice and therefore require the old and new prices to have the same cadence. The subscription changes only after that invoice is paid. While payment or customer authentication is pending, the checkout response links to Stripe's hosted invoice. Repeating the same request reuses that pending invoice instead of creating another charge. If no custom description is supplied, cadence-change upgrades retain Stripe's standard behavior.
Downgrades retain the existing end-of-period behavior. Discounted Mollie upgrades use a controlled payment followed by an update of the existing subscription. See Named billing discounts for both providers' proration and renewal rules.
Plan-period fields
Each subscription plan period includes:
| Field | Meaning |
|---|---|
status | active for current access or ended for history. |
changeType | started, upgraded, downgraded, replaced, or promotion. |
startedAt / endedAt | Effective boundaries of this price assignment. |
transitionInvoiceId / transitionInvoiceUrl | Immutable invoice that started this plan period. |
transitionAmountPaid / transitionCurrency | Immutable payment information for that transition. |
invoiceId / invoiceUrl | Most recent successfully reconciled billing-cycle invoice. |
paidFrom / paidUntil | Most recent successfully reconciled billing cycle. |
unitAmount / currency | Immutable minor-unit amount and normalized currency snapshot. |
quantity / description | Immutable billed quantity and line description. |
recurringInterval / recurringIntervalCount | Immutable subscription cadence. |
providerPriceId | Provider-side price identifier, when one exists. |
productPriceUuid | Nullable deprecated Userdocks legacy-price identifier. |
The transition fields and startedAt never change on renewal. A successful
renewal updates only the latest invoice and paid-period fields.
Lifecycle and ordering
A row is created only for a distinct plan period:
- first paid provider subscription:
started; - paid upgrade:
upgraded; - downgrade when it becomes active:
downgraded; - same-price provider-subscription replacement:
replaced; - manually granted free subscription:
promotion.
When a paid change becomes effective, Userdocks ends the previous active row and creates one new active row. Cancellation or expiration ends the active row without creating a replacement. Webhook retries and synchronous reconciliation are idempotent, so they do not duplicate a plan period.
Tenant subscription list APIs return the active plan first, followed by ended
plan periods in reverse startedAt order. External user entitlement responses
remain limited to the active plan.
Provider-billed and ended plan periods are audit history and cannot be deleted. Only an active promotional subscription can be permanently deleted.