From a3c80f8d2cd778a9a3fde58fa6e9874187f5614c Mon Sep 17 00:00:00 2001
From: theld <lars@adornis.de>
Date: Tue, 29 Apr 2025 13:37:42 +0000
Subject: [PATCH 1/3] feat!: allow for multiple stripe accounts

---
 modules/payments/README.md                |  33 +++++
 modules/payments/db/payments.ts           |  17 ++-
 modules/payments/server/checkout.ts       |  36 ++++--
 modules/payments/server/customer.ts       |   7 +-
 modules/payments/server/payment.ts        |  13 +-
 modules/payments/server/products.ts       |  21 ++-
 modules/payments/server/refund.ts         |   3 +-
 modules/payments/server/router-helper.ts  |   2 +-
 modules/payments/server/router.ts         | 148 ++-------------------
 modules/payments/server/stripe-client.ts  |  16 ++-
 modules/payments/server/stripeRouter.ts   | 149 ++++++++++++++++++++++
 modules/payments/server/types.ts          |  13 ++
 modules/payments/server/webhookHandler.ts |  89 +++++++------
 13 files changed, 334 insertions(+), 213 deletions(-)
 create mode 100644 modules/payments/server/stripeRouter.ts
 create mode 100644 modules/payments/server/types.ts

diff --git a/modules/payments/README.md b/modules/payments/README.md
index 8dac3c721a..d8bd8ff1e6 100644
--- a/modules/payments/README.md
+++ b/modules/payments/README.md
@@ -38,6 +38,8 @@ If customer doesn't want to share account access, we need:
 
 ### Env Vars
 
+The default convenience function `paymentsRouter` uses the following env vars:
+
 - STRIPE_PUBLISHABLE_KEY -> stripe credentials
 - STRIPE_PRIVATE_KEY -> stripe credentials
 - STRIPE_WEBHOOK_SECRET -> webhook secret to verify requests to the webhook endpoint. You can get this at the webhook page in stripe
@@ -80,6 +82,36 @@ You can also pass two more paramters:
 - SubscriptionClass, which also has to extend `Payment`, to be used for subscription payments.
 - InvoiceClass, has to extend `Invoice` and is used to generate Invoices. This has a `createFromStripeInvoice` static method, which can be overwritten to modify what info is copied from stripes invoices. Stripe automatically generates invoices for subscriptions, if you also want to generate invoices for one-time payments, pass `createInvoice: true` to the `payment.pay` method.
 
+#### Multiple stripe accounts
+
+Some usecases require the use of a different stripe account depending on the product bought, or the user buying the product (or a random number for what i care...). The `paymentsRouter` function used above is a convenience function that creates a stripe router for a single stripe account via the credentials provided through env vars.
+If you need to use multiple stripe accounts, you can create a stripe router for each account and use them in your express server. You will have to provide the stripe credentials for each router yourself if you use this approach.
+
+```ts
+expressServer.use(
+  stripeRouter({
+    PaymentsClass: YourPaymentExtension,
+    scope: 'scopeA',
+    stripePrivateKey: 'stripePrivateKey',
+    stripePublishableKey: 'stripePublishableKey',
+    stripeWebhookSecret: 'stripeWebhookSecret',
+  }),
+);
+expressServer.use(
+  stripeRouter({
+    PaymentsClass: YourPaymentExtension,
+    scope: 'scopeB',
+    stripePrivateKey: 'stripePrivateKey',
+    stripePublishableKey: 'stripePublishableKey',
+    stripeWebhookSecret: 'stripeWebhookSecret',
+  }),
+);
+```
+
+The `scope` parameter is used to create a unique path for the stripe router. Default scope if not provided is `'default'`. You cannot use the same scope for multiple stripe routers.
+
+The scope can also be provided to the Payment.pay function described below to target a specific router/stripe account.
+
 ### Client
 
 ```ts
@@ -115,6 +147,7 @@ await new Payment({}).pay({
   mode: 'payment', // payment for one-time, subscription for subscriptions.
   createInvoice: false, // can be set to trigger invoice creation.
   savePaymentMethod: true, // saves the payment method on the customer
+  scope: 'scopeA', // optionally provide a scope to target a specific stripe router. If no scope is provided, the default scope is used.
 });
 ```
 
diff --git a/modules/payments/db/payments.ts b/modules/payments/db/payments.ts
index 215ab23d71..c429205b82 100644
--- a/modules/payments/db/payments.ts
+++ b/modules/payments/db/payments.ts
@@ -1,4 +1,5 @@
 import { A } from '@adornis/base/env-info.js';
+import type { Maybe } from '@adornis/base/utilTypes.js';
 import { Float, ID, Int } from '@adornis/baseql/baseqlTypes.js';
 import { Arg, Entity, Field, Mutation, Query, Subscription } from '@adornis/baseql/decorators.js';
 import { AdornisEntity } from '@adornis/baseql/entities/adornisEntity.js';
@@ -9,14 +10,14 @@ import { context, publishFilter } from '@adornis/baseql/server/context.js';
 import { runInServerContext } from '@adornis/baseql/server/server.js';
 import { selectionSet, type BaseQLSelectionSet } from '@adornis/baseql/utils/queryGeneration.js';
 import { goTo } from '@adornis/router/client/open-href.js';
-import { EntityPatch, Versioned } from '@adornis/versioned-entity/db/versioned-mongo-entity.js';
+import { Versioned, type EntityPatch } from '@adornis/versioned-entity/db/versioned-mongo-entity.js';
 import { DateTime } from 'luxon';
 import { from } from 'rxjs';
 import { switchMap } from 'rxjs/operators';
 import type Stripe from 'stripe';
 import { getSessionUrlForNewPaymentInstance } from '../server/checkout.js';
 import { cancelPayment, refundPayment } from '../server/payment.js';
-import { type BasicProduct } from './basic-product.js';
+import type { BasicProduct } from './basic-product.js';
 import { ChosenProduct } from './chosenProduct.js';
 import { Invoice } from './invoice.js';
 import { PaymentError } from './paymentError.js';
@@ -32,6 +33,7 @@ export interface CreatePaymentOptions {
     product: BasicProduct;
     priceID: string;
   }>;
+  scope?: string;
   mode?: Stripe.Checkout.Session.Mode;
   paymentMethods?: Array<Stripe.Checkout.SessionCreateParams.PaymentMethodType> | null;
   successURL?: string;
@@ -53,6 +55,7 @@ export class PayOptions extends AdornisEntity {
   }
   @Field(type => String) successURL!: string;
   @Field(type => String) cancelURL!: string;
+  @Field(type => String) scope?: Maybe<string>;
   @Field(type => [String]) paymentMethods!: Array<Stripe.Checkout.SessionCreateParams.PaymentMethodType> | null;
   @Field(type => Boolean) createInvoice?: boolean;
   @Field(type => Boolean) savePaymentMethod?: boolean;
@@ -121,6 +124,9 @@ export class Payment extends MongoEntity {
   @Field(type => String)
   paymentType!: Stripe.Checkout.SessionCreateParams.PaymentMethodType;
 
+  @Field(type => String)
+  scope?: Maybe<string>;
+
   @Field(type => String)
   cancelReason?: string;
 
@@ -185,6 +191,7 @@ export class Payment extends MongoEntity {
   async createPaymentLink({
     products,
     mode = 'payment',
+    scope,
     paymentMethods = ['card', 'paypal'],
     successURL = '/product/success',
     cancelURL = '/product/cancel',
@@ -220,6 +227,7 @@ export class Payment extends MongoEntity {
       new PayOptions({
         successURL,
         cancelURL,
+        scope,
         paymentMethods,
         sellerID,
         createInvoice,
@@ -265,8 +273,9 @@ export class Payment extends MongoEntity {
     return async (gqlFields: void) => {
       if (context.serverContext) throw new Error('the server cannot buy anything');
       const userID = context.userID;
-      return runInServerContext(async () =>
+      return runInServerContext(() =>
         getSessionUrlForNewPaymentInstance({
+          scope: payOptions.scope ?? 'default',
           paymentInstance,
           products,
           userID,
@@ -293,6 +302,7 @@ export class Payment extends MongoEntity {
       const payment = await this.getByID<Payment>(paymentId)({
         _id: 1,
         _class: 1,
+        scope: 1,
         processorInfo: ProcessorInfo.allFields,
       });
       if (!payment) throw new Error('Payment not found');
@@ -311,6 +321,7 @@ export class Payment extends MongoEntity {
       const payment = await this.getByID<Payment>(paymentId)({
         _id: 1,
         _class: 1,
+        scope: 1,
         processorInfo: ProcessorInfo.allFields,
         status: 1,
       });
diff --git a/modules/payments/server/checkout.ts b/modules/payments/server/checkout.ts
index 508b5fc39b..9b154b0a9a 100644
--- a/modules/payments/server/checkout.ts
+++ b/modules/payments/server/checkout.ts
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/no-dynamic-delete */
+/* eslint-disable @typescript-eslint/only-throw-error */
 import { A } from '@adornis/base/env-info.js';
 import { logger } from '@adornis/base/logging.js';
 import type { EntityData } from '@adornis/baseql/entities/types.js';
@@ -13,10 +15,12 @@ import { PaymentError } from '../db/paymentError.js';
 import { StatusUpdate, type Payment } from '../db/payments.js';
 import { createPayment } from './payment.js';
 import { ensureStripeProduct } from './products.js';
-import { STRIPE_CHECKOUT_REDIRECT_PATH } from './router.js';
-import { stripe } from './stripe-client.js';
+import { getStripeClient } from './stripe-client.js';
+import { STRIPE_CHECKOUT_REDIRECT_PATH } from './stripeRouter.js';
 
 export const createPaymentCheckoutSession = async ({
+  stripe,
+  scope,
   paymentInstance,
   paymentMethods,
   successURL,
@@ -27,6 +31,8 @@ export const createPaymentCheckoutSession = async ({
   collectTaxID,
   automaticTax,
 }: {
+  stripe: Stripe;
+  scope: string;
   paymentInstance: Payment;
   paymentMethods?: Array<Stripe.Checkout.SessionCreateParams.PaymentMethodType>;
   successURL: string;
@@ -38,9 +44,9 @@ export const createPaymentCheckoutSession = async ({
   automaticTax?: boolean;
 }) => {
   for (const product of paymentInstance.products) {
-    await ensureStripeProduct(product.product);
+    await ensureStripeProduct(stripe, product.product);
   }
-  const line_items = paymentInstance.products.map(product => {
+  const lineItems = paymentInstance.products.map(product => {
     const data = {
       price: product.price.stripeID,
       // price_data: {
@@ -61,7 +67,7 @@ export const createPaymentCheckoutSession = async ({
     payment_method_types: paymentMethods,
     success_url:
       A.absoluteUrl(
-        STRIPE_CHECKOUT_REDIRECT_PATH,
+        STRIPE_CHECKOUT_REDIRECT_PATH(scope),
         new URLSearchParams({
           redirect: successURL,
           paymentsID: paymentInstance._id,
@@ -71,7 +77,7 @@ export const createPaymentCheckoutSession = async ({
       ) + '&token={CHECKOUT_SESSION_ID}',
     cancel_url:
       A.absoluteUrl(
-        STRIPE_CHECKOUT_REDIRECT_PATH,
+        STRIPE_CHECKOUT_REDIRECT_PATH(scope),
         new URLSearchParams({
           redirect: cancelURL,
           paymentsID: paymentInstance._id,
@@ -80,7 +86,7 @@ export const createPaymentCheckoutSession = async ({
         }),
       ) + '&token={CHECKOUT_SESSION_ID}',
     mode: paymentInstance.mode,
-    line_items,
+    line_items: lineItems,
     client_reference_id: paymentInstance._id,
   };
   if (paymentInstance.mode === 'subscription') {
@@ -126,6 +132,7 @@ export const createPaymentCheckoutSession = async ({
 };
 
 export const getSessionUrlForNewPaymentInstance = async ({
+  scope,
   paymentInstance,
   products,
   userID,
@@ -138,6 +145,7 @@ export const getSessionUrlForNewPaymentInstance = async ({
   collectTaxID,
   requireBillingAddress,
 }: {
+  scope: string;
   paymentInstance: Payment;
   products: ChosenProduct[];
   userID: any;
@@ -168,10 +176,11 @@ export const getSessionUrlForNewPaymentInstance = async ({
     );
   };
 
+  const stripe = getStripeClient(scope);
+
   // TODO this needs more thought, as the only info really required to buy a product is: product._id, product._class and the priceID of the selected price
   // TODO maybe change api of ChosenProduct to reflect that
-  for (let i = 0; i < products.length; i++) {
-    const product = products[i]!;
+  for (const product of products) {
     const productCollection = await getRawCollection<any>(
       (product.product.constructor as typeof BasicProduct)._collectionName,
     );
@@ -184,6 +193,7 @@ export const getSessionUrlForNewPaymentInstance = async ({
   }
 
   const { err: instanceCreateError, id: paymentInstanceID } = await createPayment({
+    scope,
     paymentInstance,
     products,
     userID,
@@ -197,6 +207,8 @@ export const getSessionUrlForNewPaymentInstance = async ({
   let sessionCreateError: Error | null = null;
   try {
     stripeSession = await createPaymentCheckoutSession({
+      stripe,
+      scope,
       paymentInstance,
       paymentMethods: paymentMethods ?? undefined,
       successURL,
@@ -234,15 +246,18 @@ export const getSessionUrlForNewPaymentInstance = async ({
 const completionLocks: Record<string, string> = {};
 
 export const successfullyCompleteSession = async ({
+  stripe,
   stripeSessionID,
   PaymentClass,
   paymentID,
 }: {
+  stripe: Stripe;
   stripeSessionID: string;
   PaymentClass: typeof Payment;
   paymentID: string;
 }): Promise<string> => {
   logger.debug({ stripeSessionID, lock: completionLocks[stripeSessionID] }, 'confirming stripe payment');
+
   // this payment is already in the process of being confirmed
   // lock is required since the session is completed by both the redirected user via the paymentsRouter and potentially a webhook sent by stripe.
   let taskID = completionLocks[stripeSessionID];
@@ -284,7 +299,7 @@ export const successfullyCompleteSession = async ({
           expand: ['payment_intent', 'subscription'],
         });
         if (session.mode === 'payment') await completePaymentSession(session, paymentInstance);
-        if (session.mode === 'subscription') await completeSubscriptionSession(session, paymentInstance);
+        if (session.mode === 'subscription') await completeSubscriptionSession(stripe, session, paymentInstance);
         await paymentInstance.save();
         logger.info(
           { orderID: paymentInstance._id, status: session.status },
@@ -332,6 +347,7 @@ const completePaymentSession = async (
 };
 
 const completeSubscriptionSession = async (
+  stripe: Stripe,
   session: Stripe.Response<Stripe.Checkout.Session>,
   paymentInstance: NarrowedEntity<
     Payment,
diff --git a/modules/payments/server/customer.ts b/modules/payments/server/customer.ts
index bea4cc7a0a..6a27e5e531 100644
--- a/modules/payments/server/customer.ts
+++ b/modules/payments/server/customer.ts
@@ -1,10 +1,11 @@
 import type Stripe from 'stripe';
-import { stripe } from './stripe-client.js';
+import { getStripeClient } from './stripe-client.js';
 
 /**
  * simple wrapper function to stripes create customer call.
  * See the JSDoc of the option parameter or https://stripe.com/docs/api/customers/create for more details.
  */
-export const createStripeCustomer = async (customerOptions: Stripe.CustomerCreateParams) => {
-  return stripe.customers.create(customerOptions);
+export const createStripeCustomer = async (customerOptions: Stripe.CustomerCreateParams, scope?: string) => {
+  const stripeClient = getStripeClient(scope);
+  return stripeClient.customers.create(customerOptions);
 };
diff --git a/modules/payments/server/payment.ts b/modules/payments/server/payment.ts
index 1ac498cdf7..9582a32caa 100644
--- a/modules/payments/server/payment.ts
+++ b/modules/payments/server/payment.ts
@@ -5,20 +5,22 @@ import { DateTime } from 'luxon';
 import type Stripe from 'stripe';
 import type { ChosenProduct } from '../db/chosenProduct.js';
 import { PaymentError } from '../db/paymentError.js';
-import { Payment } from '../db/payments.js';
+import type { Payment } from '../db/payments.js';
 import { UserReference } from '../db/userReference.js';
 import { isStripeRefundReason, refundStripePayment } from './refund.js';
-import { stripe } from './stripe-client.js';
+import { getStripeClient } from './stripe-client.js';
 
 export interface CreatePaymentOptions {
+  scope: string;
   paymentInstance: Payment;
   products: ChosenProduct[];
   userID?: string;
 }
 
-export const createPayment = async ({ paymentInstance, products, userID }: CreatePaymentOptions) => {
+export const createPayment = async ({ scope, paymentInstance, products, userID }: CreatePaymentOptions) => {
   paymentInstance._id = A.getGloballyUniqueID();
   try {
+    paymentInstance.scope = scope;
     paymentInstance.status = 'created';
     paymentInstance.products = products;
     // if userID is not given here we assume a guest user and fill in email and name when capturing the order
@@ -35,6 +37,7 @@ export const createPayment = async ({ paymentInstance, products, userID }: Creat
 };
 
 export const cancelPayment = async ({ paymentInstance, reason }: { paymentInstance: Payment; reason?: string }) => {
+  const stripe = getStripeClient(paymentInstance.scope);
   if (paymentInstance.status !== 'issued')
     throw new Error(`Cannot cancel payment with status "${paymentInstance.status}"`);
   if (!paymentInstance.processorInfo.stripeSessionID) throw new Error('stripe payment without stripeSessionID?');
@@ -53,13 +56,14 @@ export const refundPayment = async ({
   reason?: Stripe.RefundCreateParams.Reason | string;
   refund?: number;
 }) => {
+  const stripe = getStripeClient(paymentInstance.scope);
   if (paymentInstance.status !== 'paid')
     throw new Error(`Cannot refund payment with status "${paymentInstance.status}"`);
   if (!isStripeRefundReason(reason))
     throw new Error(
       `Stripe only accepts "duplicate" | "fraudulent" | "requested_by_customer" as a refund reason, got ${reason}`,
     );
-  const response = await refundStripePayment({ paymentInstance, reason, refund });
+  const response = await refundStripePayment({ stripe, paymentInstance, reason, refund });
   paymentInstance.processorInfo.stripeRefundID = response.id;
   paymentInstance.status = 'refund';
   await paymentInstance.save();
@@ -77,7 +81,6 @@ export const migratePaymentsRedundantIDs = async (klass: typeof Payment) => {
       if (!productPricing.product.reference) continue;
       productPricing.product._id = productPricing.product.reference + '_copied_' + A.getGloballyUniqueID();
     }
-    console.log('.');
     bulkWrite.find({ _id: issue._id }).update({ $set: { products: issue.products } });
   }
   await bulkWrite.execute({});
diff --git a/modules/payments/server/products.ts b/modules/payments/server/products.ts
index 0a4a72e4d6..0616c41e0d 100644
--- a/modules/payments/server/products.ts
+++ b/modules/payments/server/products.ts
@@ -3,12 +3,11 @@ import { getRawCollection } from '@adornis/baseql/server/collections.js';
 import type Stripe from 'stripe';
 import type { BasicProduct } from '../db/basic-product.js';
 import type { Price } from '../db/prices.js';
-import { stripe } from './stripe-client.js';
 
 const productDiffers = (stripeProduct: Stripe.Response<Stripe.Product>, product: BasicProduct) =>
   stripeProduct.name !== product.name || (!!product.description && stripeProduct.description !== product.description);
 
-export const ensureStripeProduct = async (product: BasicProduct) => {
+export const ensureStripeProduct = async (stripe: Stripe, product: BasicProduct) => {
   if (!product.reference) throw new Error('Cannot ensure stripe product without reference');
   try {
     const stripeProduct = await stripe.products.retrieve(product.reference);
@@ -17,20 +16,20 @@ export const ensureStripeProduct = async (product: BasicProduct) => {
       if (product.description) productUpdate.description = product.description;
       await stripe.products.update(product.reference, productUpdate);
     }
-    await ensurePrices(product);
+    await ensurePrices(stripe, product);
   } catch (err: any) {
     if (err.statusCode === 404) {
       const productCreateData: Stripe.ProductCreateParams = { name: product.name, id: product.reference };
       if (product.description) productCreateData.description = product.description;
       await stripe.products.create(productCreateData);
-      await ensurePrices(product);
+      await ensurePrices(stripe, product);
       return;
     }
     throw err;
   }
 };
 
-const createPrice = async (productID: string, price: Price) => {
+const createPrice = async (stripe: Stripe, productID: string, price: Price) => {
   const priceCreateData: Stripe.PriceCreateParams = {
     currency: price.currency,
     product: productID,
@@ -41,9 +40,9 @@ const createPrice = async (productID: string, price: Price) => {
   return stripe.prices.create(priceCreateData);
 };
 
-const ensurePrice = async (productID: string, price: Price) => {
+const ensurePrice = async (stripe: Stripe, productID: string, price: Price) => {
   if (!price.stripeID) {
-    const stripePrice = await createPrice(productID, price);
+    const stripePrice = await createPrice(stripe, productID, price);
     price.stripeID = stripePrice.id;
     return true;
   }
@@ -51,14 +50,14 @@ const ensurePrice = async (productID: string, price: Price) => {
     const stripePrice = await stripe.prices.retrieve(price.stripeID);
     if (priceDiffers(stripePrice, price)) {
       await stripe.prices.update(price.stripeID, { active: false });
-      const newStripePrice = await createPrice(productID, price);
+      const newStripePrice = await createPrice(stripe, productID, price);
       price.stripeID = newStripePrice.id;
       return true;
     }
     return false;
   } catch (err: any) {
     if (err.statusCode === 404) {
-      const stripePrice = await createPrice(productID, price);
+      const stripePrice = await createPrice(stripe, productID, price);
       price.stripeID = stripePrice.id;
       return true;
     }
@@ -66,10 +65,10 @@ const ensurePrice = async (productID: string, price: Price) => {
   }
 };
 
-const ensurePrices = async (product: BasicProduct) => {
+const ensurePrices = async (stripe: Stripe, product: BasicProduct) => {
   let modified = false;
   for (const price of product.prices) {
-    const check = await ensurePrice(product.reference!, price);
+    const check = await ensurePrice(stripe, product.reference!, price);
     modified ||= check;
   }
   if (modified) {
diff --git a/modules/payments/server/refund.ts b/modules/payments/server/refund.ts
index dbeb7a6708..90fa0d1abc 100644
--- a/modules/payments/server/refund.ts
+++ b/modules/payments/server/refund.ts
@@ -1,16 +1,17 @@
 import type Stripe from 'stripe';
 import { type Payment } from '../db/payments.js';
-import { stripe } from './stripe-client.js';
 
 export const isStripeRefundReason = (reason?: string): reason is Stripe.RefundCreateParams.Reason => {
   return !reason || ['duplicate', 'fraudulent', 'requested_by_customer'].includes(reason);
 };
 
 export const refundStripePayment = async ({
+  stripe,
   paymentInstance,
   reason,
   refund,
 }: {
+  stripe: Stripe;
   paymentInstance: Payment;
   reason?: Stripe.RefundCreateParams.Reason;
   refund?: number;
diff --git a/modules/payments/server/router-helper.ts b/modules/payments/server/router-helper.ts
index 54c3012481..463b919090 100644
--- a/modules/payments/server/router-helper.ts
+++ b/modules/payments/server/router-helper.ts
@@ -1,5 +1,5 @@
 import { baseConfig } from '@adornis/config/baseConfig.js';
-import { type Response } from 'express';
+import type { Response } from 'express';
 
 export const badRequest = (response: Response, reason: string) => {
   response.status(400);
diff --git a/modules/payments/server/router.ts b/modules/payments/server/router.ts
index bb4479995c..47e03633a9 100644
--- a/modules/payments/server/router.ts
+++ b/modules/payments/server/router.ts
@@ -1,139 +1,13 @@
-import { A } from '@adornis/base/env-info.js';
-import { logger } from '@adornis/base/logging.js';
-import { getTaskByID } from '@adornis/tasks/server/tasks.js';
-import express, { Router, type Request } from 'express';
-import { URL } from 'url';
-import { Invoice } from '../db/invoice.js';
-import { type Payment } from '../db/payments.js';
-import { ProcessorInfo } from '../db/processorInfo.js';
-import { UserReference } from '../db/userReference.js';
 import { paymentConfig } from './config.js';
-import { migrate } from './migrations.js';
-import { cancelPayment } from './payment.js';
-import { badRequest, notFound } from './router-helper.js';
-import { stripe } from './stripe-client.js';
-import { handleStripeWebhookEvents } from './webhookHandler.js';
-import { successfullyCompleteSession } from './checkout.js';
-import { getByID } from '@adornis/baseql/operations/mongo.js';
-import { lastValueFrom } from 'rxjs';
-
-export const STRIPE_CHECKOUT_REDIRECT_PATH = '/api/payments/stripe/checkout';
-export const STRIPE_CHECKOUT_WEBHOOK_PATH = '/api/payments/stripe/webhooks';
-
-const verifyStripeWebhookRequest = (request: Request) => {
-  const signature = request.headers['stripe-signature'];
-  if (!signature) {
-    logger.warn('stripe webhook endpoint got request without stripe-signature header');
-    return;
-  }
-  try {
-    return stripe.webhooks.constructEvent(request.body, signature, paymentConfig.get('stripeWebhookSecret'));
-  } catch (err: any) {
-    logger.warn({ err }, `Stripe Webhook signature verification failed.`);
-    return;
-  }
-};
-
-export const paymentsRouter = ({
-  completionPendingRedirect,
-  PaymentClass,
-  SubscriptionClass,
-  InvoiceClass = Invoice,
-}: {
-  completionPendingRedirect?: string;
-  PaymentClass: typeof Payment;
-  SubscriptionClass?: typeof Payment;
-  InvoiceClass?: typeof Invoice;
-}) => {
-  migrate(PaymentClass).catch(err => logger.error({ err }, 'Migration of payments failed'));
-  const getPaymentClass = (mode: string) =>
-    mode === 'subscription' ? SubscriptionClass ?? PaymentClass : PaymentClass;
-
-  const router = Router();
-  router.use(STRIPE_CHECKOUT_REDIRECT_PATH, async (request, response) => {
-    const redirectStatus = request.query.redirect_status as string;
-    const paymentID = request.query.paymentsID as string;
-    const stripeSessionID = request.query.token as string;
-    const mode = request.query.mode as string;
-    const redirect = request.query.redirect as string; // could use some checking on where we redirect here, like allowedDomains
-    if (!redirectStatus) return badRequest(response, 'queryParam redirect_status not set.');
-    if (!paymentID) return badRequest(response, 'queryParam paymentsID not set.');
-    if (!stripeSessionID) return badRequest(response, 'queryParam token not set. This is normally set by stripe.');
-    if (!mode) return badRequest(response, 'queryParam mode not set.');
-    if (!redirect) return badRequest(response, 'queryParam redirect not set.');
-
-    const redirectUrl = new URL(A.absoluteUrl(redirect));
-    redirectUrl.searchParams.append('token', stripeSessionID);
-    redirectUrl.searchParams.append('id', paymentID);
-    if (redirectStatus === 'canceled') {
-      logger.info({ orderID: paymentID }, 'Payments: cancelling order');
-      try {
-        const paymentInstance: any = await getByID(
-          getPaymentClass(mode),
-          paymentID,
-        )({
-          _id: 1,
-          _class: 1,
-          status: 1,
-          paymentType: 1,
-          userReference: UserReference.allFields,
-          processorInfo: ProcessorInfo.allFields,
-        });
-        if (!paymentInstance) {
-          return notFound(response, 'Payment not found. Is the paymentsID query param correctly set?');
-        }
-        await cancelPayment({ paymentInstance, reason: 'User cancelled payment' });
-      } catch (error: any) {
-        logger.error({ err: error }, 'error while cancelling stripe payment after user cancelled it');
-      }
-      response.redirect(redirectUrl.toString());
-      return;
-    }
-    logger.info({ orderID: paymentID }, 'Payments: successful stripe checkout session');
-
-    const completionTaskID = await successfullyCompleteSession({
-      stripeSessionID,
-      PaymentClass: getPaymentClass(mode),
-      paymentID,
-    });
-    redirectUrl.searchParams.append('task', completionTaskID);
-    if (completionPendingRedirect) {
-      const pendingUrl = new URL(A.absoluteUrl(completionPendingRedirect));
-      pendingUrl.searchParams.append('token', stripeSessionID);
-      pendingUrl.searchParams.append('id', paymentID);
-      pendingUrl.searchParams.append('task', completionTaskID);
-      response.redirect(pendingUrl.toString());
-      return;
-    }
-    try {
-      const task = getTaskByID(completionTaskID);
-      if (!task) {
-        response.status(500).end('Error while creating payment completion task');
-        return;
-      }
-      await lastValueFrom(task?.observable);
-      redirectUrl.searchParams.append('captured', 'true');
-      response.redirect(redirectUrl.toString());
-    } catch (error: any) {
-      if (error.reason === 'NOT_FOUND')
-        return notFound(response, 'Payment not found. Is the paymentsID query param correctly set?');
-      logger.warn(
-        { paymentID, stripeSessionID, err: error },
-        'Error while updating payment with stripe customer details',
-      );
-      redirectUrl.searchParams.append('captured', 'false');
-      response.redirect(redirectUrl.toString());
-    }
-  });
-  router.use(STRIPE_CHECKOUT_WEBHOOK_PATH, express.raw({ type: 'application/json' }), async (request, response) => {
-    const event = verifyStripeWebhookRequest(request);
-    if (!event) return response.sendStatus(400);
-    try {
-      await handleStripeWebhookEvents(event, response, getPaymentClass, InvoiceClass);
-    } catch (err: any) {
-      logger.error({ err, event }, 'Error while handling stripe webhook event');
-      return response.sendStatus(500);
-    }
+import { stripeRouter } from './stripeRouter.js';
+import type { StripeRouterOptions } from './types.js';
+
+export const paymentsRouter = (
+  options: Omit<StripeRouterOptions, 'stripePrivateKey' | 'stripePublishableKey' | 'stripeWebhookSecret'>,
+) =>
+  stripeRouter({
+    ...options,
+    stripePrivateKey: paymentConfig.get('stripePrivateKey'),
+    stripePublishableKey: paymentConfig.get('stripePublishableKey'),
+    stripeWebhookSecret: paymentConfig.get('stripeWebhookSecret'),
   });
-  return router;
-};
diff --git a/modules/payments/server/stripe-client.ts b/modules/payments/server/stripe-client.ts
index aee614996e..027522810c 100644
--- a/modules/payments/server/stripe-client.ts
+++ b/modules/payments/server/stripe-client.ts
@@ -1,4 +1,16 @@
+import type { Maybe } from '@adornis/base/utilTypes.js';
 import Stripe from 'stripe';
-import { paymentConfig } from './config.js';
 
-export const stripe = new Stripe(paymentConfig.get('stripePrivateKey'), { apiVersion: '2023-08-16' });
+const stripeClients: Record<string, Stripe> = {};
+
+export const getStripeClient = (scope?: Maybe<string>) => {
+  const realScope = scope ?? 'default';
+  if (!stripeClients[realScope]) throw new Error(`No stripe client found for scope ${realScope}`);
+  return stripeClients[realScope];
+};
+
+export const createStripeClient = (scope: string, apiKey: string) => {
+  if (stripeClients[scope]) throw new Error(`Stripe client already exists for scope ${scope}`);
+  stripeClients[scope] = new Stripe(apiKey, { apiVersion: '2023-08-16' });
+  return stripeClients[scope];
+};
diff --git a/modules/payments/server/stripeRouter.ts b/modules/payments/server/stripeRouter.ts
new file mode 100644
index 0000000000..eccd8b7eca
--- /dev/null
+++ b/modules/payments/server/stripeRouter.ts
@@ -0,0 +1,149 @@
+import { A } from '@adornis/base/env-info.js';
+import { logger } from '@adornis/base/logging.js';
+import { getByID } from '@adornis/baseql/operations/mongo.js';
+import { getTaskByID } from '@adornis/tasks/server/tasks.js';
+import express, { Router, type Request } from 'express';
+import { lastValueFrom } from 'rxjs';
+import { URL } from 'url';
+import { Invoice } from '../db/invoice.js';
+import { ProcessorInfo } from '../db/processorInfo.js';
+import { UserReference } from '../db/userReference.js';
+import { successfullyCompleteSession } from './checkout.js';
+import { migrate } from './migrations.js';
+import { cancelPayment } from './payment.js';
+import { badRequest, notFound } from './router-helper.js';
+import { createStripeClient, getStripeClient } from './stripe-client.js';
+import type { StripeRouterOptions } from './types.js';
+import { handleStripeWebhookEvents } from './webhookHandler.js';
+
+export const STRIPE_CHECKOUT_REDIRECT_PATH = (scope = 'default') =>
+  `/api/payments/${scope !== 'default' ? `${scope}/` : ''}stripe/checkout`;
+export const STRIPE_CHECKOUT_WEBHOOK_PATH = (scope = 'default') =>
+  `/api/payments/${scope !== 'default' ? `${scope}/` : ''}stripe/webhooks`;
+
+const verifyStripeWebhookRequest = (request: Request, scope: string, stripeWebhookSecret: string) => {
+  const signature = request.headers['stripe-signature'];
+  if (!signature) {
+    logger.warn('stripe webhook endpoint got request without stripe-signature header');
+    return;
+  }
+  try {
+    return getStripeClient(scope).webhooks.constructEvent(request.body, signature, stripeWebhookSecret);
+  } catch (err: any) {
+    logger.warn({ err }, `Stripe Webhook signature verification failed.`);
+  }
+};
+
+export const stripeRouter = ({
+  completionPendingRedirect,
+  scope = 'default',
+  stripePrivateKey,
+  stripePublishableKey,
+  stripeWebhookSecret,
+  PaymentClass,
+  SubscriptionClass,
+  InvoiceClass = Invoice,
+}: StripeRouterOptions) => {
+  if (scope === 'default') migrate(PaymentClass).catch(err => logger.error({ err }, 'Migration of payments failed'));
+  const stripe = createStripeClient(scope, stripePrivateKey);
+
+  const getPaymentClass = (mode: string) =>
+    mode === 'subscription' ? SubscriptionClass ?? PaymentClass : PaymentClass;
+
+  const router = Router();
+  router.use(STRIPE_CHECKOUT_REDIRECT_PATH(scope), async (request, response) => {
+    const redirectStatus = request.query.redirect_status as string;
+    const paymentID = request.query.paymentsID as string;
+    const stripeSessionID = request.query.token as string;
+    const mode = request.query.mode as string;
+    const redirect = request.query.redirect as string; // could use some checking on where we redirect here, like allowedDomains
+    if (!redirectStatus) return badRequest(response, 'queryParam redirect_status not set.');
+    if (!paymentID) return badRequest(response, 'queryParam paymentsID not set.');
+    if (!stripeSessionID) return badRequest(response, 'queryParam token not set. This is normally set by stripe.');
+    if (!mode) return badRequest(response, 'queryParam mode not set.');
+    if (!redirect) return badRequest(response, 'queryParam redirect not set.');
+
+    const redirectUrl = new URL(A.absoluteUrl(redirect));
+    redirectUrl.searchParams.append('token', stripeSessionID);
+    redirectUrl.searchParams.append('id', paymentID);
+    if (redirectStatus === 'canceled') {
+      logger.info({ orderID: paymentID }, 'Payments: cancelling order');
+      try {
+        const paymentInstance: any = await getByID(
+          getPaymentClass(mode),
+          paymentID,
+        )({
+          _id: 1,
+          _class: 1,
+          status: 1,
+          scope: 1,
+          paymentType: 1,
+          userReference: UserReference.allFields,
+          processorInfo: ProcessorInfo.allFields,
+        });
+        if (!paymentInstance) {
+          return notFound(response, 'Payment not found. Is the paymentsID query param correctly set?');
+        }
+        if ((paymentInstance.scope ?? 'default') !== scope) {
+          return notFound(response, 'Payment not found. Is the paymentsID query param correctly set? (scope mismatch)');
+        }
+        await cancelPayment({ stripe, paymentInstance, reason: 'User cancelled payment' });
+      } catch (error: any) {
+        logger.error({ err: error }, 'error while cancelling stripe payment after user cancelled it');
+      }
+      response.redirect(redirectUrl.toString());
+      return;
+    }
+    logger.info({ orderID: paymentID }, 'Payments: successful stripe checkout session');
+
+    const completionTaskID = await successfullyCompleteSession({
+      stripe,
+      stripeSessionID,
+      PaymentClass: getPaymentClass(mode),
+      paymentID,
+    });
+    redirectUrl.searchParams.append('task', completionTaskID);
+    if (completionPendingRedirect) {
+      const pendingUrl = new URL(A.absoluteUrl(completionPendingRedirect));
+      pendingUrl.searchParams.append('token', stripeSessionID);
+      pendingUrl.searchParams.append('id', paymentID);
+      pendingUrl.searchParams.append('task', completionTaskID);
+      response.redirect(pendingUrl.toString());
+      return;
+    }
+    try {
+      const task = getTaskByID(completionTaskID);
+      if (!task) {
+        response.status(500).end('Error while creating payment completion task');
+        return;
+      }
+      await lastValueFrom(task.observable);
+      redirectUrl.searchParams.append('captured', 'true');
+      response.redirect(redirectUrl.toString());
+    } catch (error: any) {
+      if (error.reason === 'NOT_FOUND')
+        return notFound(response, 'Payment not found. Is the paymentsID query param correctly set?');
+      logger.warn(
+        { paymentID, stripeSessionID, err: error },
+        'Error while updating payment with stripe customer details',
+      );
+      redirectUrl.searchParams.append('captured', 'false');
+      response.redirect(redirectUrl.toString());
+    }
+  });
+  router.use(
+    STRIPE_CHECKOUT_WEBHOOK_PATH(scope),
+    express.raw({ type: 'application/json' }),
+    async (request, response) => {
+      const event = verifyStripeWebhookRequest(request, scope, stripeWebhookSecret);
+      if (!event) return response.sendStatus(400);
+      try {
+        await handleStripeWebhookEvents({ stripe, event, response, getPaymentClass, InvoiceClass });
+      } catch (err: any) {
+        logger.error({ err, event }, 'Error while handling stripe webhook event');
+        return response.sendStatus(500);
+      }
+    },
+  );
+  return router;
+};
diff --git a/modules/payments/server/types.ts b/modules/payments/server/types.ts
new file mode 100644
index 0000000000..d6b56a9d65
--- /dev/null
+++ b/modules/payments/server/types.ts
@@ -0,0 +1,13 @@
+import type { Invoice } from '../db/invoice.js';
+import type { Payment } from '../db/payments.js';
+
+export interface StripeRouterOptions {
+  completionPendingRedirect?: string;
+  scope?: string;
+  stripePrivateKey: string;
+  stripePublishableKey: string;
+  stripeWebhookSecret: string;
+  PaymentClass: typeof Payment;
+  SubscriptionClass?: typeof Payment;
+  InvoiceClass?: typeof Invoice;
+}
diff --git a/modules/payments/server/webhookHandler.ts b/modules/payments/server/webhookHandler.ts
index ac1de63e8b..a06e749bcd 100644
--- a/modules/payments/server/webhookHandler.ts
+++ b/modules/payments/server/webhookHandler.ts
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/require-await */
+/* eslint-disable @typescript-eslint/only-throw-error */
 import { logger } from '@adornis/base/logging.js';
 import { constructValue } from '@adornis/baseql/entities/construct.js';
 import type { EntityData } from '@adornis/baseql/entities/types.js';
@@ -7,15 +9,15 @@ import type Stripe from 'stripe';
 import { Invoice } from '../db/invoice.js';
 import { PaymentError } from '../db/paymentError.js';
 import type { Payment } from '../db/payments.js';
-import { stripe } from './stripe-client.js';
 import { successfullyCompleteSession } from './checkout.js';
 
-type StripeEventHandler = (
-  event: Stripe.Event,
-  response: Response,
-  getPaymentClass: (mode: string) => typeof Payment,
-  InvoiceClass: typeof Invoice,
-) => Promise<void>;
+type StripeEventHandler = (options: {
+  event: Stripe.Event;
+  response: Response;
+  stripe: Stripe;
+  getPaymentClass: (mode: string) => typeof Payment;
+  InvoiceClass: typeof Invoice;
+}) => Promise<void>;
 
 const safeSave = async (payment: Payment) => {
   try {
@@ -27,6 +29,7 @@ const safeSave = async (payment: Payment) => {
 };
 
 const getPaymentInstanceFromInvoice = async (
+  stripe: Stripe,
   getPaymentClass: (mode: string) => typeof Payment,
   invoice: Stripe.Invoice,
 ) => {
@@ -44,23 +47,27 @@ const getPaymentInstanceFromInvoice = async (
   return { collection, rawPaymentInstance, PaymentClass };
 };
 
-const handleInvoicePaymentFailed: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleInvoicePaymentFailed: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const invoice = event.data.object as Stripe.Invoice;
-  console.log('invloice payment failed', invoice.id, invoice.status);
+  logger.warn({ invoice }, 'invoice payment failed');
 
   response.sendStatus(204);
 };
 
-const handleInvoicePaymentActionRequired: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleInvoicePaymentActionRequired: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   // const invoice = event.data.object as Stripe.Invoice;
   response.sendStatus(204);
 };
 
-const handleInvoicePaid: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleInvoicePaid: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const invoice = event.data.object as Stripe.Invoice;
-  console.log('invoice payment paid', invoice.id, invoice.subscription);
+  logger.info({ invoice }, 'invoice payment paid');
   try {
-    const { rawPaymentInstance, PaymentClass } = await getPaymentInstanceFromInvoice(getPaymentClass, invoice).catch(
+    const { rawPaymentInstance, PaymentClass } = await getPaymentInstanceFromInvoice(
+      stripe,
+      getPaymentClass,
+      invoice,
+    ).catch(
       // ? this is ugly but works, can i do error handling in a more readable way?
       err => {
         throw { err, step: 'get' };
@@ -107,20 +114,19 @@ const handleInvoicePaid: StripeEventHandler = async (event, response, getPayment
   response.sendStatus(204);
 };
 
-const handleInvoiceFinalizationFailed: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleInvoiceFinalizationFailed: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   // const invoice = event.data.object as Stripe.Invoice;
   response.sendStatus(204);
 };
 
-const handleInvoiceCreated: StripeEventHandler = async (event, response, getPaymentClass, InvoiceClass) => {
+const handleInvoiceCreated: StripeEventHandler = async ({ event, response, getPaymentClass, InvoiceClass, stripe }) => {
   const invoice = event.data.object as Stripe.Invoice;
   try {
     const localInvoice = await InvoiceClass.createFromStripeInvoice(invoice);
-    const { collection, rawPaymentInstance } = await getPaymentInstanceFromInvoice(getPaymentClass, invoice);
+    const { collection, rawPaymentInstance } = await getPaymentInstanceFromInvoice(stripe, getPaymentClass, invoice);
     const updateResult = await collection.updateOne(
       { _id: rawPaymentInstance._id },
       {
-        // @ts-expect-error strange typing in pnpm run lint(-rough)
         $push: { invoices: localInvoice.toJSON() },
       },
     );
@@ -133,7 +139,7 @@ const handleInvoiceCreated: StripeEventHandler = async (event, response, getPaym
   }
 };
 
-const handleSubsctiptionUpdated: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleSubsctiptionUpdated: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const subscription = event.data.object as Stripe.Subscription;
   if (event.data.previous_attributes && 'status' in event.data.previous_attributes) {
     const collection = await getRawCollection<EntityData<Payment>>(getPaymentClass('subscription')._collectionName);
@@ -145,14 +151,14 @@ const handleSubsctiptionUpdated: StripeEventHandler = async (event, response, ge
   response.sendStatus(204);
 };
 
-const handleSubsctiptionDeleted: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleSubsctiptionDeleted: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const subscription = event.data.object as Stripe.Subscription;
   const collection = await getRawCollection<EntityData<Payment>>(getPaymentClass('subscription')._collectionName);
   await collection.updateOne({ _id: subscription.metadata.paymentReference }, { $set: { status: 'canceled' } });
   response.sendStatus(204);
 };
 
-const handleSessionExpired: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleSessionExpired: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const session = event.data.object as Stripe.Checkout.Session;
   const paymentID = session.client_reference_id;
   if (!paymentID) {
@@ -173,7 +179,7 @@ const handleSessionExpired: StripeEventHandler = async (event, response, getPaym
   // construct value here so all hooks are executed when saving the instance
   const payment: Payment = constructValue(rawPaymentInstance);
   // if we expired the session because the user clicked "abort and return to store" we dont want to overwrite that.
-  if (payment && payment.status !== 'canceled') {
+  if (payment.status !== 'canceled') {
     payment.status = 'expired';
     const err = await safeSave(payment);
     if (err) {
@@ -184,7 +190,7 @@ const handleSessionExpired: StripeEventHandler = async (event, response, getPaym
   response.sendStatus(204);
 };
 
-const handleSessionCompleted: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleSessionCompleted: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const session = event.data.object as Stripe.Checkout.Session;
   try {
     if (!session.client_reference_id) {
@@ -192,6 +198,7 @@ const handleSessionCompleted: StripeEventHandler = async (event, response, getPa
       return;
     }
     await successfullyCompleteSession({
+      stripe,
       stripeSessionID: session.id,
       PaymentClass: getPaymentClass(session.mode),
       paymentID: session.client_reference_id,
@@ -206,7 +213,7 @@ const handleSessionCompleted: StripeEventHandler = async (event, response, getPa
   }
 };
 
-const handleChargeFailed: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleChargeFailed: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   const charge = event.data.object as Stripe.Charge;
   const sessionList = await stripe.checkout.sessions.list({ payment_intent: charge.payment_intent as string });
   const session = sessionList.data[0];
@@ -224,7 +231,9 @@ const handleChargeFailed: StripeEventHandler = async (event, response, getPaymen
     response.sendStatus(404);
     return;
   }
-  const collection = await getRawCollection(getPaymentClass(session.mode)._collectionName);
+  const collection = await getRawCollection<{ _id: string; errors?: any[] }>(
+    getPaymentClass(session.mode)._collectionName,
+  );
   const payment = await collection.findOne({ 'processorInfo.stripeSessionID': session.id });
   if (!payment) {
     logger.warn(
@@ -242,7 +251,6 @@ const handleChargeFailed: StripeEventHandler = async (event, response, getPaymen
   }).toJSON();
   const update = payment.errors ? { $push: { errors: error } } : { $set: { errors: [error] } };
   try {
-    // @ts-expect-error
     await collection.updateOne({ _id: payment._id }, update);
     response.sendStatus(204);
   } catch (err: any) {
@@ -251,41 +259,42 @@ const handleChargeFailed: StripeEventHandler = async (event, response, getPaymen
   }
 };
 
-const handleChargeSucceeded: StripeEventHandler = async (event, response, getPaymentClass) => {
+const handleChargeSucceeded: StripeEventHandler = async ({ event, response, getPaymentClass, stripe }) => {
   // TODO do we need to listen for this?
   // const charge = event.data.object as Stripe.Charge;
   response.sendStatus(204);
 };
 
-export const handleStripeWebhookEvents: StripeEventHandler = async (
+export const handleStripeWebhookEvents: StripeEventHandler = async ({
+  stripe,
   event,
   response,
   getPaymentClass,
-  InvoiceClass: typeof Invoice = Invoice,
-) => {
+  InvoiceClass = Invoice,
+}) => {
   switch (event.type) {
     case 'invoice.payment_failed':
-      return handleInvoicePaymentFailed(event, response, getPaymentClass, InvoiceClass);
+      return handleInvoicePaymentFailed({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'invoice.payment_action_required':
-      return handleInvoicePaymentActionRequired(event, response, getPaymentClass, InvoiceClass);
+      return handleInvoicePaymentActionRequired({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'invoice.paid':
-      return handleInvoicePaid(event, response, getPaymentClass, InvoiceClass);
+      return handleInvoicePaid({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'invoice.finalization_failed':
-      return handleInvoiceFinalizationFailed(event, response, getPaymentClass, InvoiceClass);
+      return handleInvoiceFinalizationFailed({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'invoice.created':
-      return handleInvoiceCreated(event, response, getPaymentClass, InvoiceClass);
+      return handleInvoiceCreated({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'customer.subscription.updated':
-      return handleSubsctiptionUpdated(event, response, getPaymentClass, InvoiceClass);
+      return handleSubsctiptionUpdated({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'customer.subscription.deleted':
-      return handleSubsctiptionDeleted(event, response, getPaymentClass, InvoiceClass);
+      return handleSubsctiptionDeleted({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'checkout.session.expired':
-      return handleSessionExpired(event, response, getPaymentClass, InvoiceClass);
+      return handleSessionExpired({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'checkout.session.completed':
-      return handleSessionCompleted(event, response, getPaymentClass, InvoiceClass);
+      return handleSessionCompleted({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'charge.failed':
-      return handleChargeFailed(event, response, getPaymentClass, InvoiceClass);
+      return handleChargeFailed({ event, response, getPaymentClass, InvoiceClass, stripe });
     case 'charge.succeeded':
-      return handleChargeSucceeded(event, response, getPaymentClass, InvoiceClass);
+      return handleChargeSucceeded({ event, response, getPaymentClass, InvoiceClass, stripe });
     default:
       logger.info({ event }, 'Unhandled Stripe Webhook Event!');
       response.sendStatus(404);
-- 
GitLab


From 4f0fbd31b30ef0981b582e485dc6333c1e4bd073 Mon Sep 17 00:00:00 2001
From: theld <lars@adornis.de>
Date: Tue, 29 Apr 2025 13:42:09 +0000
Subject: [PATCH 2/3] chore: fix missed param

---
 modules/payments/server/stripeRouter.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/payments/server/stripeRouter.ts b/modules/payments/server/stripeRouter.ts
index eccd8b7eca..0e00155d80 100644
--- a/modules/payments/server/stripeRouter.ts
+++ b/modules/payments/server/stripeRouter.ts
@@ -87,7 +87,7 @@ export const stripeRouter = ({
         if ((paymentInstance.scope ?? 'default') !== scope) {
           return notFound(response, 'Payment not found. Is the paymentsID query param correctly set? (scope mismatch)');
         }
-        await cancelPayment({ stripe, paymentInstance, reason: 'User cancelled payment' });
+        await cancelPayment({ paymentInstance, reason: 'User cancelled payment' });
       } catch (error: any) {
         logger.error({ err: error }, 'error while cancelling stripe payment after user cancelled it');
       }
-- 
GitLab


From 63bffbd5d8ad1ea0c0ea5d540560537aa2a12c12 Mon Sep 17 00:00:00 2001
From: theld <lars@adornis.de>
Date: Tue, 29 Apr 2025 13:44:01 +0000
Subject: [PATCH 3/3] chore: fix more broken typing

---
 modules/payments/server/webhookHandler.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/payments/server/webhookHandler.ts b/modules/payments/server/webhookHandler.ts
index a06e749bcd..b39e5b2324 100644
--- a/modules/payments/server/webhookHandler.ts
+++ b/modules/payments/server/webhookHandler.ts
@@ -127,7 +127,7 @@ const handleInvoiceCreated: StripeEventHandler = async ({ event, response, getPa
     const updateResult = await collection.updateOne(
       { _id: rawPaymentInstance._id },
       {
-        $push: { invoices: localInvoice.toJSON() },
+        $push: { invoices: localInvoice.toJSON() as any },
       },
     );
     if (!updateResult.modifiedCount)
-- 
GitLab