made checkout lock fail open

This commit is contained in:
John Yeo
2026-04-03 13:42:20 +01:00
parent 36a7657835
commit da869f7f90
4 changed files with 40 additions and 18 deletions

View File

@@ -73,10 +73,12 @@ export const handleCheckoutSessionMetadataV2 = async ({
}); });
// Clear checkout session lock now that customer_product rows exist // Clear checkout session lock now that customer_product rows exist
await checkoutSessionLock.clear({ const lockCustomerId =
ctx, updatedDeferredData.billingContext.fullCustomer.id ??
customerId: ctx.fullCustomer?.id ?? "", updatedDeferredData.billingContext.fullCustomer.internal_id;
}); if (lockCustomerId) {
await checkoutSessionLock.clear({ ctx, customerId: lockCustomerId });
}
// Queue customer.products.updated webhook (mirrors executeBillingPlan) // Queue customer.products.updated webhook (mirrors executeBillingPlan)
await billingPlanToSendProductsUpdated({ await billingPlanToSendProductsUpdated({

View File

@@ -1,9 +1,8 @@
import { ms } from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli"; import { createStripeCli } from "@/external/connect/createStripeCli";
import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { AutumnContext } from "@/honoUtils/HonoEnv";
import { CacheManager } from "@/utils/cacheUtils/CacheManager"; import { CacheManager } from "@/utils/cacheUtils/CacheManager";
const CHECKOUT_LOCK_TTL_SECONDS = ms.minutes(2); const CHECKOUT_LOCK_TTL_SECONDS = 2 * 60;
interface CheckoutSessionLockData { interface CheckoutSessionLockData {
paramsHash: string; paramsHash: string;
@@ -26,9 +25,14 @@ const get = async ({
ctx: AutumnContext; ctx: AutumnContext;
customerId: string; customerId: string;
}): Promise<CheckoutSessionLockData | null> => { }): Promise<CheckoutSessionLockData | null> => {
return CacheManager.getJson<CheckoutSessionLockData>( try {
buildKey({ ctx, customerId }), return await CacheManager.getJson<CheckoutSessionLockData>(
); buildKey({ ctx, customerId }),
);
} catch (error) {
ctx.logger.error(`Failed to get checkout session lock: ${error}`);
return null;
}
}; };
const set = async ({ const set = async ({
@@ -40,11 +44,15 @@ const set = async ({
customerId: string; customerId: string;
data: CheckoutSessionLockData; data: CheckoutSessionLockData;
}): Promise<void> => { }): Promise<void> => {
await CacheManager.setJson( try {
buildKey({ ctx, customerId }), await CacheManager.setJson(
data, buildKey({ ctx, customerId }),
CHECKOUT_LOCK_TTL_SECONDS, data,
); CHECKOUT_LOCK_TTL_SECONDS,
);
} catch (error) {
ctx.logger.error(`Failed to set checkout session lock: ${error}`);
}
}; };
const clear = async ({ const clear = async ({
@@ -54,7 +62,11 @@ const clear = async ({
ctx: AutumnContext; ctx: AutumnContext;
customerId: string; customerId: string;
}): Promise<void> => { }): Promise<void> => {
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. */ /** Expire the old Stripe Checkout session then delete the Redis lock. */
@@ -73,8 +85,10 @@ const expireAndClear = async ({
env: ctx.env, env: ctx.env,
}); });
await stripeCli.checkout.sessions.expire(checkoutSessionId); await stripeCli.checkout.sessions.expire(checkoutSessionId);
} catch { } catch (error) {
// Session may already be expired / completed ctx.logger.error(
`Failed to expire checkout session ${checkoutSessionId}: ${error}`,
);
} }
await clear({ ctx, customerId }); await clear({ ctx, customerId });

View File

@@ -10,6 +10,10 @@ const deterministicStringify = (value: unknown): string => {
return `[${value.map(deterministicStringify).join(",")}]`; return `[${value.map(deterministicStringify).join(",")}]`;
const obj = value as Record<string, unknown>; const obj = value as Record<string, unknown>;
if (typeof obj.toJSON === "function")
return deterministicStringify(obj.toJSON());
const sortedKeys = Object.keys(obj) const sortedKeys = Object.keys(obj)
.filter((k) => obj[k] !== undefined) .filter((k) => obj[k] !== undefined)
.sort(); .sort();

View File

@@ -5,7 +5,9 @@ export interface StripeBillingPlanResult {
deferred?: boolean; deferred?: boolean;
stripeInvoice?: Stripe.Invoice; stripeInvoice?: Stripe.Invoice;
stripeSubscription?: Stripe.Subscription; stripeSubscription?: Stripe.Subscription;
stripeCheckoutSession?: Partial<Stripe.Checkout.Session>; stripeCheckoutSession?:
| Stripe.Checkout.Session
| (Pick<Stripe.Checkout.Session, "id"> & { url?: string | null });
stripeInvoiceItems?: Stripe.InvoiceItem[]; stripeInvoiceItems?: Stripe.InvoiceItem[];
requiredAction?: { requiredAction?: {
code: PaymentFailureCode; code: PaymentFailureCode;