MPP.Methods.Stripe.Subscription (mpp v0.19.0)

Copy Markdown View Source

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

cancel(subscription_id, config)

@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.

challenge_method_details(subscription)

@spec challenge_method_details(MPP.Intents.Subscription.t()) :: map()

Return the public Stripe subscription challenge fields.

inspect_activation(subscription, payment_method_id)

@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.

process_event(event, config)

@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.

process_invoice(event_id, invoice_id, config)

@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.

resolve_activation(subscription, payment_method_id, resolution)

@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.

validate_config!(config)

@spec validate_config!(map()) :: :ok

Validate Stripe subscription configuration at Plug initialization.

verify(payload, subscription)

@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.

void_stale_invoice(subscription_id, invoice_id, as_of, config)

@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.