SYSTEM OVERVIEW
Payment & Collection
Payment is how a finalized invoice becomes collected revenue. Four paths (on/off-session × create/update), two processors (Stripe and Braintree), and one safety property: confirm payment before committing state.
| Path | Session | Trigger | CVV / 3DS? |
|---|---|---|---|
| On-session create | On-session | User purchases in browser | Yes — customer present, SCA required |
| On-session update | On-session | User upgrades/downgrades in browser | Yes — customer present, SCA required |
| Off-session create | Off-session | API / catalog / queue-initiated | No — references prior on-session auth |
| Off-session update | Off-session | Renewal, scheduled change, admin action | No — references prior on-session auth |
The on/off-session distinction is a card-network concept, not just a UX one. Off-session charges look indistinguishable from fraud unless a prior on-session authorization exists for the same merchant and card. Stripe references that first successful on-session transaction to prove legitimacy. A naive off-session attempt without prior setup will be declined by the issuer.
Every payment — regardless of path — flows through six steps. The first four steps write nothing to our database. Only step five crosses the commitment boundary.
Steps 1–4 are the Green Zone: no state has been committed to our database. If payment fails at any point before step 5, nothing needs to be rolled back. The PaymentIntent is simply cancelled or abandoned. This is the key safety property that PDE establishes on all five payment paths.
| PM Type | Processor | Collection Method | Notes |
|---|---|---|---|
| Credit Card | Stripe | charge_automatically | pm_XXXXXX token. CVV optional on-session. Vaultable. |
| Apple Pay | Stripe | charge_automatically | Wallet token pm_XXXXXX. On-session only for initial auth. |
| Google Pay | Stripe | charge_automatically | Wallet token pm_XXXXXX. On-session only for initial auth. |
| ACH / SEPA | Stripe | charge_automatically | Bank debit. Mandate required. Supports on-session. |
| PayPal | Braintree | send_invoice | Always off-session. Braintree Billing Agreement. The outlier. |
PayPal is the outlier in every dimension. Stripe manages the subscription lifecycle, but Braintree executes the actual charge via a GraphQL API call to ChargePayPalAccount. The result is reconciled back to the Stripe invoice via MarkInvoicePaidOutOfBand(). This dual-processor burden — Stripe for subscription state, Braintree for money movement — is the root of PayPal's unique risks: double-charge window (30s Braintree dedup), tax timing gaps (5s/10s Avalara wait), and no automatic retry on failure. All other payment methods use a single processor.
PDE (Payment Determinism Engine) is the migration from commit-then-rollback to confirm-before-commit. The old model committed entitlements and state to our database first, then attempted payment, then rolled back on failure. Rollbacks were incomplete — ~200/day — leaving ~35 uncollected invoices daily. The new model holds all state in Stripe (as a pending PaymentIntent) until payment is confirmed, then commits atomically.
| Parameter | Used For | Effect |
|---|---|---|
default_incomplete | On-session creates | Subscription created in incomplete state; activates on payment |
pending_if_incomplete | On-session + off-session updates | Update held pending; applied on payment (not on failure) |
pay_immediately | All paths (must flip to true) | Eliminates separate collection step; payment inline with change |
setup_future_usage: off_session | All PaymentIntents | Stripe auto-vaults PM on customer after payment succeeds |
When Stripe returns requires_action on a PaymentIntent, the customer must complete a 3DS challenge. On-session, this is handled by the frontend: the API response includes a client_secret, which the Stripe.js unified checkout component uses to render the 3DS UI inline. No page navigation required.
Off-session 3DS (rare, but possible for SCA-regulated regions) is handled server-side via a Temporal workflow. The workflow polls the PaymentIntent status with a 24-hour timeout. Status mapping: succeeded → commit and provision; requires_action → return client secret to caller; requires_payment_method → mark failed, trigger dunning. The Temporal workflow resolves the concern about incomplete webhook handling for off-session paths.
Three out-of-band paths exist for payments outside Stripe automatic collection: wire transfers (enterprise, manually reconciled via NinjaPanel), PayPal renewals (Braintree charge followed by MarkInvoicePaidOutOfBand), and check deposits (support-only, manual). All converge on the PaidOutOfBand API.
PDE transforms payment from hope-and-rollback to confirm-then-commit. The Green Zone is the safety property: nothing committed until payment is confirmed. When PDE is universal, ~200 daily rollbacks and ~35 bad invoices go to zero. When payment fails on any path, the invoice enters the dunning cycle — see Dunning & Recovery.