Explorar el Código

fix(console): reuse workspace Stripe customers

Jack Jiang hace 6 días
padre
commit
c4219eb9a7

+ 1 - 18
packages/console/app/src/routes/black/subscribe/[plan].tsx

@@ -87,24 +87,7 @@ const createSetupIntent = async (input: { plan: string; workspaceID: string }) =
       return { error: formError.alreadySubscribed }
     }
 
-    let customerID = customer?.customerID
-    if (!customerID) {
-      const customer = await Billing.stripe().customers.create({
-        email,
-        metadata: {
-          workspaceID,
-        },
-      })
-      customerID = customer.id
-      await Database.use((tx) =>
-        tx
-          .update(BillingTable)
-          .set({
-            customerID,
-          })
-          .where(eq(BillingTable.workspaceID, workspaceID)),
-      )
-    }
+    const customerID = await Billing.ensureCustomer(email)
 
     const intent = await Billing.stripe().setupIntents.create({
       customer: customerID,

+ 49 - 27
packages/console/core/src/billing.ts

@@ -42,6 +42,39 @@ export namespace Billing {
     )
   }
 
+  export const ensureCustomer = async (email?: string) => {
+    const billing = await get()
+    if (billing?.customerID) return billing.customerID
+
+    const workspaceID = Actor.workspace()
+    const stripe = Billing.stripe()
+    const created = await stripe.customers.create(
+      {
+        metadata: {
+          workspaceID,
+        },
+      },
+      {
+        idempotencyKey: `opencode-workspace-customer:${workspaceID}`,
+      },
+    )
+    await Database.use((tx) =>
+      tx
+        .update(BillingTable)
+        .set({
+          customerID: created.id,
+        })
+        .where(and(eq(BillingTable.workspaceID, workspaceID), isNull(BillingTable.customerID))),
+    )
+
+    const customerID = (await get())?.customerID
+    if (!customerID) throw new Error(`Workspace with ID ${workspaceID} not found`)
+    if (customerID === created.id && email) {
+      await stripe.customers.update(customerID, { email })
+    }
+    return customerID
+  }
+
   export const payments = async () => {
     return await Database.use((tx) =>
       tx
@@ -231,8 +264,9 @@ export namespace Billing {
       }
 
       const email = await User.getAuthEmail(user.properties.userID)
-      const customer = await Billing.get()
-      const amountInCents = (amount ?? customer.reloadAmount ?? Billing.RELOAD_AMOUNT) * 100
+      const billing = await Billing.get()
+      const customerID = await Billing.ensureCustomer(email ?? undefined)
+      const amountInCents = (amount ?? billing.reloadAmount ?? Billing.RELOAD_AMOUNT) * 100
       const session = await Billing.stripe().checkout.sessions.create({
         mode: "payment",
         billing_address_collection: "required",
@@ -254,18 +288,11 @@ export namespace Billing {
             quantity: 1,
           },
         ],
-        ...(customer.customerID
-          ? {
-              customer: customer.customerID,
-              customer_update: {
-                name: "auto",
-                address: "auto",
-              },
-            }
-          : {
-              customer_email: email!,
-              customer_creation: "always",
-            }),
+        customer: customerID,
+        customer_update: {
+          name: "auto",
+          address: "auto",
+        },
         currency: "usd",
         invoice_creation: {
           enabled: true,
@@ -311,6 +338,7 @@ export namespace Billing {
 
       if (billing.subscriptionID) throw new Error("Already subscribed to Black")
       if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite")
+      const customerID = await Billing.ensureCustomer(email)
 
       const coupons = await Database.use((tx) =>
         tx
@@ -335,17 +363,11 @@ export namespace Billing {
         Billing.stripe().checkout.sessions.create({
           mode: "subscription",
           discounts: coupon ? [{ coupon }] : undefined,
-          ...(billing.customerID
-            ? {
-                customer: billing.customerID,
-                customer_update: {
-                  name: "auto",
-                  address: "auto",
-                },
-              }
-            : {
-                customer_email: email,
-              }),
+          customer: customerID,
+          customer_update: {
+            name: "auto",
+            address: "auto",
+          },
           ...(() => {
             if (method === "alipay") {
               return {
@@ -411,14 +433,14 @@ export namespace Billing {
 
         // get pending payment intent
         const intents = await Billing.stripe().paymentIntents.search({
-          query: `-status:'canceled' AND -status:'processing' AND -status:'succeeded' AND customer:'${billing.customerID}'`,
+          query: `-status:'canceled' AND -status:'processing' AND -status:'succeeded' AND customer:'${customerID}'`,
         })
         if (intents.data.length === 0) throw e
 
         for (const intent of intents.data) {
           // get checkout session
           const sessions = await Billing.stripe().checkout.sessions.list({
-            customer: billing.customerID!,
+            customer: customerID,
             payment_intent: intent.id,
           })