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
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({

View File

@@ -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<CheckoutSessionLockData | null> => {
return CacheManager.getJson<CheckoutSessionLockData>(
buildKey({ ctx, customerId }),
);
try {
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 ({
@@ -40,11 +44,15 @@ const set = async ({
customerId: string;
data: CheckoutSessionLockData;
}): Promise<void> => {
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<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. */
@@ -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 });

View File

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

View File

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