Stripe Billing activation, renewal accounting, and cancellation for the shared subscription intent.
Implements the constrained fixed-price profile from
draft-stripe-subscription-00: one Customer, one recurring Price, one
quantity-one Subscription, and synchronously paid invoices mapped onto
locally persisted canonical periods.
A PaymentMethod holds at most one live activation per plan (amount,
currency, cadence, description, externalId, metadata, and network).
Activating the same plan again under a different challenge is rejected
until the earlier subscription's cancellation has taken effect; use
distinct externalId values for plans that may legitimately coexist.
Every Subscription created during activation carries mpp_activation_claim
and mpp_activation_generation metadata. These two keys count against
Stripe's metadata limit alongside mpp_challenge_id and mpp_external_id.
An activation owns its claim for a 900-second lease and re-checks the claim
before every Stripe write, starting a write only while at least 300 seconds
of the lease remain. Write requests run without retries under bounded
timeouts, so a claim is taken over only after every write of its previous
owner has finished. Operator req_options cannot widen those write bounds.
When an activation's Stripe outcome is unknown (lost response, store failure,
interrupted worker), the payment method and plan stay blocked. A later
activation lists the Customer's live Subscriptions and adopts the tagged one
only when it is the single candidate and passes full validation, including
its paid first invoice. Every other outcome (a Stripe error, several
candidates, a candidate that does not validate) leaves the claim in
:needs_reconciliation; reconciliation never cancels anything on Stripe.
Resolve such a claim with inspect_activation/2 and resolve_activation/3.
An activation cancels only the Subscription it created itself, and only when
it lost its claim or that Subscription definitively failed validation.
Before that cancel it records the Subscription on the claim as cancelling
in the same compare-and-set store write that adoption uses, so a cancel and
an adoption of one Subscription never both happen: adoption refuses a
Subscription marked cancelling, and the cancel is skipped once the claim
names it as adopted. A cancel that cannot record its mark leaves the
Subscription to the operator instead.
Verifying the activating challenge again returns the existing receipt only for the exact credential payload that activated it (compared by a SHA-256 digest of its canonical JSON); any other payload under that challenge is rejected. The receipt path exists so a client whose activation request failed before the transport recorded the credential as used (a lost response, a store error, an activation later adopted by reconciliation) can retry that same credential and receive its receipt.
Summary
Functions
Schedule cancellation at the end of the last durably paid billing period.
Return the public Stripe subscription challenge fields.
Report the activation claim for a payment method and plan, and the live Stripe Subscriptions tagged with it.
Process a Stripe subscription event after its webhook signature has been verified.
Validate and durably record a paid Stripe renewal invoice.
Resolve an activation claim that is waiting for reconciliation.
Validate Stripe subscription configuration at Plug initialization.
Activate a Stripe subscription and verify its synchronously paid first invoice.
Stop Stripe collection for an unpaid invoice after its canonical period closes.
Functions
@spec cancel(String.t(), map()) :: {:ok, MPP.Subscription.Record.t()} | {:error, MPP.Errors.t()}
Schedule cancellation at the end of the last durably paid billing period.
@spec challenge_method_details(MPP.Intents.Subscription.t()) :: map()
Return the public Stripe subscription challenge fields.
@spec inspect_activation(MPP.Intents.Subscription.t(), String.t()) :: {:ok, map()} | {:error, MPP.Errors.t()}
Report the activation claim for a payment method and plan, and the live Stripe Subscriptions tagged with it.
Pass the same MPP.Intents.Subscription (including method_details) the
route verifies with, and the Stripe PaymentMethod id (pm_...) the payer
activated with. A :needs_reconciliation status means automatic
reconciliation could not prove a single valid subscription; resolve it with
resolve_activation/3.
@spec process_event(map(), map()) :: {:ok, MPP.Subscription.Record.t()} | {:error, MPP.Errors.t()}
Process a Stripe subscription event after its webhook signature has been verified.
@spec process_invoice(String.t(), String.t(), map()) :: {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
Validate and durably record a paid Stripe renewal invoice.
@spec resolve_activation( MPP.Intents.Subscription.t(), String.t(), {:adopt, String.t()} | :release ) :: {:ok, MPP.Subscription.Record.t()} | {:error, MPP.Errors.t()}
Resolve an activation claim that is waiting for reconciliation.
{:adopt, stripe_subscription_id} records that live, claim-tagged Stripe
Subscription as the activation after the same validation an activation runs,
including its paid first invoice. :release reopens the claim, but only
after its lease has expired and only while no live Subscription tagged with
the claim remains; cancel unwanted ones in Stripe first. Nothing here cancels
anything on Stripe.
@spec validate_config!(map()) :: :ok
Validate Stripe subscription configuration at Plug initialization.
@spec verify(map(), MPP.Intents.Subscription.t()) :: {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
Activate a Stripe subscription and verify its synchronously paid first invoice.
@spec void_stale_invoice(String.t(), String.t(), DateTime.t(), map()) :: {:ok, MPP.Subscription.Record.t()} | {:error, MPP.Errors.t()}
Stop Stripe collection for an unpaid invoice after its canonical period closes.