From da869f7f905adcb2441f7266ccf73fd6662f97e8 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 3 Apr 2026 13:42:20 +0100 Subject: [PATCH] made checkout lock fail open --- .../handleCheckoutSessionMetadataV2.ts | 10 +++-- .../checkoutSessionLock.ts | 40 +++++++++++++------ server/src/utils/hash/hashJson.ts | 4 ++ .../billingModels/plan/billingResult.ts | 4 +- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts index 5647b2ac1..6846ca909 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts @@ -73,10 +73,12 @@ export const handleCheckoutSessionMetadataV2 = async ({ }); // Clear checkout session lock now that customer_product rows exist - await checkoutSessionLock.clear({ - ctx, - customerId: ctx.fullCustomer?.id ?? "", - }); + const lockCustomerId = + updatedDeferredData.billingContext.fullCustomer.id ?? + updatedDeferredData.billingContext.fullCustomer.internal_id; + if (lockCustomerId) { + await checkoutSessionLock.clear({ ctx, customerId: lockCustomerId }); + } // Queue customer.products.updated webhook (mirrors executeBillingPlan) await billingPlanToSendProductsUpdated({ diff --git a/server/src/internal/billing/v2/actions/locks/checkoutSessionLock/checkoutSessionLock.ts b/server/src/internal/billing/v2/actions/locks/checkoutSessionLock/checkoutSessionLock.ts index aa28a23ef..1c2c6dd11 100644 --- a/server/src/internal/billing/v2/actions/locks/checkoutSessionLock/checkoutSessionLock.ts +++ b/server/src/internal/billing/v2/actions/locks/checkoutSessionLock/checkoutSessionLock.ts @@ -1,9 +1,8 @@ -import { ms } from "@autumn/shared"; import { createStripeCli } from "@/external/connect/createStripeCli"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { CacheManager } from "@/utils/cacheUtils/CacheManager"; -const CHECKOUT_LOCK_TTL_SECONDS = ms.minutes(2); +const CHECKOUT_LOCK_TTL_SECONDS = 2 * 60; interface CheckoutSessionLockData { paramsHash: string; @@ -26,9 +25,14 @@ const get = async ({ ctx: AutumnContext; customerId: string; }): Promise => { - return CacheManager.getJson( - buildKey({ ctx, customerId }), - ); + try { + return await CacheManager.getJson( + buildKey({ ctx, customerId }), + ); + } catch (error) { + ctx.logger.error(`Failed to get checkout session lock: ${error}`); + return null; + } }; const set = async ({ @@ -40,11 +44,15 @@ const set = async ({ customerId: string; data: CheckoutSessionLockData; }): Promise => { - await CacheManager.setJson( - buildKey({ ctx, customerId }), - data, - CHECKOUT_LOCK_TTL_SECONDS, - ); + try { + await CacheManager.setJson( + buildKey({ ctx, customerId }), + data, + CHECKOUT_LOCK_TTL_SECONDS, + ); + } catch (error) { + ctx.logger.error(`Failed to set checkout session lock: ${error}`); + } }; const clear = async ({ @@ -54,7 +62,11 @@ const clear = async ({ ctx: AutumnContext; customerId: string; }): Promise => { - await CacheManager.del(buildKey({ ctx, customerId })); + try { + await CacheManager.del(buildKey({ ctx, customerId })); + } catch (error) { + ctx.logger.error(`Failed to clear checkout session lock: ${error}`); + } }; /** Expire the old Stripe Checkout session then delete the Redis lock. */ @@ -73,8 +85,10 @@ const expireAndClear = async ({ env: ctx.env, }); await stripeCli.checkout.sessions.expire(checkoutSessionId); - } catch { - // Session may already be expired / completed + } catch (error) { + ctx.logger.error( + `Failed to expire checkout session ${checkoutSessionId}: ${error}`, + ); } await clear({ ctx, customerId }); diff --git a/server/src/utils/hash/hashJson.ts b/server/src/utils/hash/hashJson.ts index 1db6d7500..810277519 100644 --- a/server/src/utils/hash/hashJson.ts +++ b/server/src/utils/hash/hashJson.ts @@ -10,6 +10,10 @@ const deterministicStringify = (value: unknown): string => { return `[${value.map(deterministicStringify).join(",")}]`; const obj = value as Record; + + if (typeof obj.toJSON === "function") + return deterministicStringify(obj.toJSON()); + const sortedKeys = Object.keys(obj) .filter((k) => obj[k] !== undefined) .sort(); diff --git a/shared/models/billingModels/plan/billingResult.ts b/shared/models/billingModels/plan/billingResult.ts index 3fe403ff5..39c0315d8 100644 --- a/shared/models/billingModels/plan/billingResult.ts +++ b/shared/models/billingModels/plan/billingResult.ts @@ -5,7 +5,9 @@ export interface StripeBillingPlanResult { deferred?: boolean; stripeInvoice?: Stripe.Invoice; stripeSubscription?: Stripe.Subscription; - stripeCheckoutSession?: Partial; + stripeCheckoutSession?: + | Stripe.Checkout.Session + | (Pick & { url?: string | null }); stripeInvoiceItems?: Stripe.InvoiceItem[]; requiredAction?: { code: PaymentFailureCode;