diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts index 50e1e74bf..0aa30f4ed 100644 --- a/server/src/external/stripe/stripeCusUtils.ts +++ b/server/src/external/stripe/stripeCusUtils.ts @@ -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; + 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) { diff --git a/server/tests/_temp/temp1.test.ts b/server/tests/_temp/temp1.test.ts index 59b61f438..ad89492af 100644 --- a/server/tests/_temp/temp1.test.ts +++ b/server/tests/_temp/temp1.test.ts @@ -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({ diff --git a/shared/utils/utils.ts b/shared/utils/utils.ts index 8766a822b..62da38497 100644 --- a/shared/utils/utils.ts +++ b/shared/utils/utils.ts @@ -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"); +}; diff --git a/vite/src/components/forms/attach-product/use-attach-preview.ts b/vite/src/components/forms/attach-product/use-attach-preview.ts index 5021cdd80..3efae1746 100644 --- a/vite/src/components/forms/attach-product/use-attach-preview.ts +++ b/vite/src/components/forms/attach-product/use-attach-preview.ts @@ -43,7 +43,7 @@ export function useAttachPreview(params: AttachPreviewParams = {}) { useEffect(() => { const timer = setTimeout(() => { setDebouncedQueryKey(queryKeyDeps); - }, 150); + }, 300); return () => clearTimeout(timer); }, [queryKeyDeps]);