Просмотр исходного кода

zen: disable reload when reload fails

Frank 3 недель назад
Родитель
Сommit
d8bbb6df60

+ 2 - 0
infra/console.ts

@@ -77,6 +77,8 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
     "checkout.session.expired",
     "charge.refunded",
     "invoice.payment_succeeded",
+    "invoice.payment_failed",
+    "invoice.payment_action_required",
     "customer.created",
     "customer.deleted",
     "customer.updated",

+ 66 - 4
packages/console/app/src/routes/stripe/webhook.ts

@@ -141,8 +141,6 @@ export async function POST(input: APIEvent) {
         return couponID
       })()
 
-      // get user
-
       await Actor.provide("system", { workspaceID }, async () => {
         // look up current billing
         const billing = await Billing.get()
@@ -422,8 +420,8 @@ export async function POST(input: APIEvent) {
     }
     if (body.type === "invoice.payment_succeeded") {
       if (
-        body.data.object.billing_reason === "subscription_cycle" ||
-        body.data.object.billing_reason === "subscription_create"
+        body.data.object.billing_reason === "subscription_create" ||
+        body.data.object.billing_reason === "subscription_cycle"
       ) {
         const invoiceID = body.data.object.id as string
         const amountInCents = body.data.object.amount_paid
@@ -476,6 +474,70 @@ export async function POST(input: APIEvent) {
             },
           }),
         )
+      } else if (body.data.object.billing_reason === "manual") {
+        const workspaceID = body.data.object.metadata?.workspaceID
+        const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
+        const invoiceID = body.data.object.id as string
+        const customerID = body.data.object.customer as string
+
+        if (!workspaceID) throw new Error("Workspace ID not found")
+        if (!customerID) throw new Error("Customer ID not found")
+        if (!amountInCents) throw new Error("Amount not found")
+        if (!invoiceID) throw new Error("Invoice ID not found")
+
+        await Actor.provide("system", { workspaceID }, async () => {
+          // get payment id from invoice
+          const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
+            expand: ["payments"],
+          })
+          await Database.transaction(async (tx) => {
+            await tx
+              .update(BillingTable)
+              .set({
+                balance: sql`${BillingTable.balance} + ${centsToMicroCents(amountInCents)}`,
+                reloadError: null,
+                timeReloadError: null,
+              })
+              .where(eq(BillingTable.workspaceID, Actor.workspace()))
+            await tx.insert(PaymentTable).values({
+              workspaceID: Actor.workspace(),
+              id: Identifier.create("payment"),
+              amount: centsToMicroCents(amountInCents),
+              invoiceID,
+              paymentID: invoice.payments?.data[0].payment.payment_intent as string,
+              customerID,
+            })
+          })
+        })
+      }
+    }
+    if (body.type === "invoice.payment_failed" || body.type === "invoice.payment_action_required") {
+      if (body.data.object.billing_reason === "manual") {
+        const workspaceID = body.data.object.metadata?.workspaceID
+        const invoiceID = body.data.object.id
+
+        if (!workspaceID) throw new Error("Workspace ID not found")
+        if (!invoiceID) throw new Error("Invoice ID not found")
+
+        const paymentIntent = await Billing.stripe().paymentIntents.retrieve(invoiceID);
+        console.log(JSON.stringify(paymentIntent))
+        const errorMessage =
+          typeof paymentIntent === "object" && paymentIntent !== null
+            ? paymentIntent.last_payment_error?.message
+            : undefined
+
+        await Actor.provide("system", { workspaceID }, async () => {
+          await Database.use((tx) =>
+            tx
+              .update(BillingTable)
+              .set({
+                reload: false,
+                reloadError: errorMessage ?? "Payment failed.",
+                timeReloadError: sql`now()`,
+              })
+              .where(eq(BillingTable.workspaceID, Actor.workspace())),
+          )
+        })
       }
     }
     if (body.type === "charge.refunded") {

+ 6 - 25
packages/console/core/src/billing.ts

@@ -78,8 +78,6 @@ export namespace Billing {
     const customerID = billing.customerID
     const paymentMethodID = billing.paymentMethodID
     const amountInCents = (billing.reloadAmount ?? Billing.RELOAD_AMOUNT) * 100
-    const paymentID = Identifier.create("payment")
-    let invoice
     try {
       const draft = await Billing.stripe().invoices.create({
         customer: customerID!,
@@ -87,6 +85,10 @@ export namespace Billing {
         default_payment_method: paymentMethodID!,
         collection_method: "charge_automatically",
         currency: "usd",
+        metadata: {
+          workspaceID: Actor.workspace(),
+          amount: amountInCents.toString(),
+        },
       })
       await Billing.stripe().invoiceItems.create({
         amount: amountInCents,
@@ -103,19 +105,17 @@ export namespace Billing {
         description: ITEM_FEE_NAME,
       })
       await Billing.stripe().invoices.finalizeInvoice(draft.id!)
-      invoice = await Billing.stripe().invoices.pay(draft.id!, {
+      await Billing.stripe().invoices.pay(draft.id!, {
         off_session: true,
         payment_method: paymentMethodID!,
-        expand: ["payments"],
       })
-      if (invoice.status !== "paid" || invoice.payments?.data.length !== 1)
-        throw new Error(invoice.last_finalization_error?.message)
     } catch (e: any) {
       console.error(e)
       await Database.use((tx) =>
         tx
           .update(BillingTable)
           .set({
+            reload: false,
             reloadError: e.message ?? "Payment failed.",
             timeReloadError: sql`now()`,
           })
@@ -123,25 +123,6 @@ export namespace Billing {
       )
       return
     }
-
-    await Database.transaction(async (tx) => {
-      await tx
-        .update(BillingTable)
-        .set({
-          balance: sql`${BillingTable.balance} + ${centsToMicroCents(amountInCents)}`,
-          reloadError: null,
-          timeReloadError: null,
-        })
-        .where(eq(BillingTable.workspaceID, Actor.workspace()))
-      await tx.insert(PaymentTable).values({
-        workspaceID: Actor.workspace(),
-        id: paymentID,
-        amount: centsToMicroCents(amountInCents),
-        invoiceID: invoice.id!,
-        paymentID: invoice.payments?.data[0].payment.payment_intent as string,
-        customerID,
-      })
-    })
   }
 
   export const grantCredit = async (workspaceID: string, dollarAmount: number) => {