PaymentProcessorType contract and handles subscriptions, invoices, checkout sessions, payment methods, and usage-based billing.
What the processor covers
ThePaymentProcessorType contract is satisfied by both the Stripe adapter and the mock. Every operation is available through tools.paymentProcessor:
| Category | Operations |
|---|---|
| Organisations | createOrganization, updateOrganization, findOrganizationBy |
| Products | createProduct, updateProduct, retireProduct, restaureProduct |
| Subscriptions | createSubscription, updateSubscription, cancelSubscription, recordUsage |
| Invoices | previewInvoice, retrieveInvoice |
| Payments | createIntent, confirmPayment, cancelPayment, capturePayment |
| Checkout | setCheckoutSession, getCheckoutSession |
| Payment methods | findPaymentMethodBy, detachPaymentMethod |
| Discounts | validateDiscountCode |
| Downgrades | createDowngradeRequest, cancelDowngradeRequest, syncPendingProductDowngradeScheduledQuantity |
Why
The Stripe adapter gives you access to the full Stripe billing API through a single, testable interface. Every operation is a small pure function that receives the Stripe instance, parameters, and the tools object — making individual operations easy to unit-test in isolation. ThequantityDowngradePolicy setting lets you control whether subscription seat reductions are prorated immediately or deferred to the end of the billing cycle.
Setup
- In
apps/backend/src/configuration/development.ts, settools.paymentProcessor: - In
production.ts, useQuantityDowngradePolicy.END_OF_BILLING_CYCLEunless you have a specific reason for immediate proration. - In your environment, set:
- For local webhook testing, use the Stripe CLI to forward events:
- Run
make test module=backend.
Quantity downgrade policy
ThequantityDowngradePolicy setting controls how Stripe handles subscription seat reductions:
| Policy | Behaviour |
|---|---|
IMMEDIATELY_PRORATED | Stripe issues a prorated credit immediately when seats are reduced. Use in dev/staging. |
END_OF_BILLING_CYCLE | The reduction takes effect at the end of the current billing period. Use in production. |
apps/backend/src/configuration/production.ts:
Testing with stripeMock
In apps/backend/src/configuration/test.ts, the payment processor is configured as stripeMock — a no-op stub that requires no Stripe account, API key, or network traffic:
{ success: false } by default. Two exceptions — retrieveInvoice and cancelSubscription — reject with an error instead. When a test needs a specific response, use jest.spyOn:
PaymentProcessorType contract. When you add a new operation, TypeScript will fail to compile until you add a matching stub to createTestStripePaymentProcessor in apps/backend/src/tools/paymentProcessor/stripe/index.ts — this is intentional.
Adding a new Stripe operation
- Create
apps/backend/src/tools/paymentProcessor/stripe/<operationName>.ts. Export a function that takes(stripe: Stripe, params, tools: ToolsType)and returns{ success: true, result }or{ success: false, reason }. - Add the function to the
StripePaymentProcessorTypeinterface inapps/backend/src/tools/paymentProcessor/stripe/types.ts. - Register it in
buildStripePaymentProcessorinsideapps/backend/src/tools/paymentProcessor/stripe/index.ts. - Add a no-op stub to
createTestStripePaymentProcessorin the same file. - Write a test in
apps/backend/src/tools/paymentProcessor/__tests__/stripe/<operationName>.spec.ts. - Run
make test module=backend.
Webhooks
When Stripe sends a webhook event, it hits thePOST /stripe/webhook endpoint in apps/backend/src/routers/api/stripe.ts. Here’s the full flow:
Step by step:
- Stripe POSTs to
/stripe/webhookwith aStripe-Signatureheader. - The router calls
Stripe.webhooks.constructEventto verify the signature usingwebhookSigningSecret. If verification fails, it returns400immediately. - The router checks whether the event type is in
HandledStripeWebhookEvent(defined inapps/backend/src/utilities/stripe.ts). Unhandled event types get a200response and are silently ignored — this is intentional, since Stripe sends many event types you may not care about. - The router checks
hasWebhookEventBeenProcessedto deduplicate — Stripe can deliver the same event more than once. - The router validates the payload shape using Zod schemas in
webhookValidators.ts. This is a safety net: if Stripe changes their payload format, this fails loudly rather than letting malformed data reach your business logic. - The event is serialised and enqueued to
QueueName.STRIPE_WEBHOOK. - The
stripeWebhookConsumerpicks it up from the queue and dispatches to the right handler based onevent.type.
| Event | Handler file |
|---|---|
customer.subscription.deleted | customerSubscriptionDeleted.ts |
customer.subscription.updated | customerSubscriptionUpdated.ts |
customer.updated | customerUpdated.ts |
payment_intent.amount_capturable_updated | paymentIntentAmountCapturableUpdated.ts |
payment_intent.succeeded | paymentIntentSucceeded.ts |
payment_method.attached | paymentMethodAttached.ts |
payment_method.detached | paymentMethodDetached.ts |
payment_method.updated | paymentMethodUpdated.ts |
Adding a new event handler
-
Add the new event type to
HandledStripeWebhookEventinapps/backend/src/utilities/stripe.ts: -
Add a Zod validator for the payload shape in
apps/backend/src/tools/queue/consumers/stripeWebhookConsumer/webhookValidators.ts. -
Create the handler file
apps/backend/src/tools/queue/consumers/stripeWebhookConsumer/invoicePaymentSucceeded.ts: -
Register the handler in
apps/backend/src/tools/queue/consumers/stripeWebhookConsumer/index.ts. -
Write a test in
apps/backend/src/tools/queue/consumers/stripeWebhookConsumer/__tests__/. -
For local testing, forward events using the Stripe CLI:
-
Verify:
Gotchas
- The Stripe client is configured with
maxNetworkRetries: 3andtimeout: 80000ms. Operations that exceed this timeout will throw — wrap long-running calls withexpRetriesusingisConnectionTimeoutErroras theshouldRetryguard. - Webhook signatures expire quickly. Never replay a webhook event after the tolerance window — Stripe will reject the signature.
- Never log
secretKeyorwebhookSigningSecret— they are credentials. apiVersionmust exactly match a supported Stripe API version string. A mismatch causes a startup error.
What’s next?
- Billing architecture — how Stripe fits into the broader billing system.
- Configuration — environment-level config.