fix: checkout race condition

This commit is contained in:
John Yeo
2025-12-04 16:05:20 +00:00
parent 3b80825849
commit 713e7643db
4 changed files with 128 additions and 46 deletions

View File

@@ -2,6 +2,7 @@ import {
type AppEnv,
type Customer,
ErrCode,
hashString,
type Organization,
ProcessorType,
} from "@autumn/shared";
@@ -12,6 +13,7 @@ import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import RecaseError from "@/utils/errorUtils.js";
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
import type { Logger } from "../logtail/logtailUtils";
export const getStripeCus = async ({
stripeCli,
@@ -39,13 +41,14 @@ export const createStripeCusIfNotExists = async ({
org: Organization;
env: AppEnv;
customer: Customer;
logger: any;
logger: Logger;
}) => {
let createNew = false;
const stripeCli = createStripeCli({ org, env });
if (!customer.processor || !customer.processor.id) {
createNew = true;
} else {
const getCurrentStripeCus = async () => {
// 1. If no processor, create new customer
if (!customer.processor?.id) return null;
try {
const stripeCus = await stripeCli.customers.retrieve(
customer.processor.id,
@@ -53,44 +56,106 @@ export const createStripeCusIfNotExists = async ({
expand: ["test_clock", "invoice_settings.default_payment_method"],
},
);
if (!stripeCus.deleted) {
return stripeCus as Stripe.Customer;
} else {
createNew = true;
}
// 2. If customer is deleted, create new customer
if (stripeCus.deleted) return null;
// 3. If customer is not deleted, return customer
return stripeCus as Stripe.Customer;
} catch (_error) {
createNew = true;
// 4. If error, create new customer
return null;
}
}
};
if (createNew) {
logger.info(`Creating new stripe customer for ${customer.id}`);
const stripeCustomer = await createStripeCustomer({
org,
env,
customer,
});
// 1. Get current stripe customer
const stripeCus = await getCurrentStripeCus();
await CusService.update({
db,
idOrInternalId: customer.internal_id,
orgId: org.id,
env,
update: {
processor: {
id: stripeCustomer.id,
type: ProcessorType.Stripe,
},
if (stripeCus) return stripeCus;
// 2. If no current stripe customer, create new customer
logger.info(`Creating new stripe customer for ${customer.id}`);
const idempotencyKey = hashString(
`stripe-create-cus:${customer.id || customer.internal_id}:${org.id}:${env}:${Math.floor(Date.now() / 5000)}`,
);
const stripeCustomer = await createStripeCustomer({
org,
env,
customer,
idempotencyKey,
});
await CusService.update({
db,
idOrInternalId: customer.internal_id,
orgId: org.id,
env,
update: {
processor: {
id: stripeCustomer.id,
type: ProcessorType.Stripe,
},
});
},
});
customer.processor = {
id: stripeCustomer.id,
type: ProcessorType.Stripe,
};
customer.processor = {
id: stripeCustomer.id,
type: ProcessorType.Stripe,
};
return stripeCustomer;
}
return stripeCustomer;
// let createNew = false;
// const stripeCli = createStripeCli({ org, env });
// if (!customer.processor || !customer.processor.id) {
// createNew = true;
// } else {
// try {
// const stripeCus = await stripeCli.customers.retrieve(
// customer.processor.id,
// {
// expand: ["test_clock", "invoice_settings.default_payment_method"],
// },
// );
// if (!stripeCus.deleted) {
// return stripeCus as Stripe.Customer;
// } else {
// createNew = true;
// }
// } catch (_error) {
// createNew = true;
// }
// }
// if (createNew) {
// logger.info(`Creating new stripe customer for ${customer.id}`);
// const stripeCustomer = await createStripeCustomer({
// org,
// env,
// customer,
// });
// await CusService.update({
// db,
// idOrInternalId: customer.internal_id,
// orgId: org.id,
// env,
// update: {
// processor: {
// id: stripeCustomer.id,
// type: ProcessorType.Stripe,
// },
// },
// });
// customer.processor = {
// id: stripeCustomer.id,
// type: ProcessorType.Stripe,
// };
// return stripeCustomer;
// }
};
export const createStripeCustomer = async ({
@@ -99,26 +164,35 @@ export const createStripeCustomer = async ({
customer,
testClockId,
metadata,
idempotencyKey,
}: {
org: Organization;
env: AppEnv;
customer: Customer;
testClockId?: string;
metadata?: Record<string, unknown>;
idempotencyKey?: string;
}) => {
const stripeCli = createStripeCli({ org, env });
try {
const stripeCustomer = await stripeCli.customers.create({
name: customer.name || undefined,
email: customer.email || undefined,
metadata: {
...(metadata || {}),
autumn_id: customer.id || null,
autumn_internal_id: customer.internal_id,
const stripeCustomer = await stripeCli.customers.create(
{
name: customer.name || undefined,
email: customer.email || undefined,
metadata: {
...(metadata || {}),
autumn_id: customer.id || null,
autumn_internal_id: customer.internal_id,
},
test_clock: testClockId,
},
test_clock: testClockId,
});
idempotencyKey
? {
idempotencyKey,
}
: undefined,
);
return stripeCustomer;
} catch (error: any) {

View File

@@ -75,6 +75,7 @@ describe(`${chalk.yellowBright("temp: Testing entity prorated")}`, () => {
prefix: customerId,
});
});
return;
test("should create a subscription with prepaid and prorated", async () => {
await autumn.attach({

View File

@@ -24,3 +24,10 @@ export const keyToTitle = (
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
};
/** Fast hash using Bun's native hasher */
export const hashString = (str: string): string => {
const hasher = new Bun.CryptoHasher("sha256");
hasher.update(str);
return hasher.digest("base64");
};

View File

@@ -43,7 +43,7 @@ export function useAttachPreview(params: AttachPreviewParams = {}) {
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQueryKey(queryKeyDeps);
}, 150);
}, 300);
return () => clearTimeout(timer);
}, [queryKeyDeps]);