summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src/routes/stripe/webhook.ts
blob: a9f5759768f400cb2df62d9aae5c245017139263 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { Billing } from "@opencode/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode/console-core/schema/billing.sql.js"
import { Identifier } from "@opencode/console-core/identifier.js"
import { centsToMicroCents } from "@opencode/console-core/util/price.js"
import { Actor } from "@opencode/console-core/actor.js"
import { Resource } from "@opencode/console-resource"

export async function POST(input: APIEvent) {
  const body = await Billing.stripe().webhooks.constructEventAsync(
    await input.request.text(),
    input.request.headers.get("stripe-signature")!,
    Resource.STRIPE_WEBHOOK_SECRET.value,
  )

  console.log(body.type, JSON.stringify(body, null, 2))
  if (body.type === "customer.updated") {
    // check default payment method changed
    const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
    if (!("default_payment_method" in prevInvoiceSettings)) return

    const customerID = body.data.object.id
    const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string

    if (!customerID) throw new Error("Customer ID not found")
    if (!paymentMethodID) throw new Error("Payment method ID not found")

    const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
    await Database.use(async (tx) => {
      await tx
        .update(BillingTable)
        .set({
          paymentMethodID,
          paymentMethodLast4: paymentMethod.card!.last4,
        })
        .where(eq(BillingTable.customerID, customerID))
    })
  }
  if (body.type === "checkout.session.completed") {
    const workspaceID = body.data.object.metadata?.workspaceID
    const customerID = body.data.object.customer as string
    const paymentID = body.data.object.payment_intent as string
    const invoiceID = body.data.object.invoice as string
    const amount = body.data.object.amount_total

    if (!workspaceID) throw new Error("Workspace ID not found")
    if (!customerID) throw new Error("Customer ID not found")
    if (!amount) throw new Error("Amount not found")
    if (!paymentID) throw new Error("Payment ID not found")
    if (!invoiceID) throw new Error("Invoice ID not found")

    await Actor.provide("system", { workspaceID }, async () => {
      const customer = await Billing.get()
      if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")

      // set customer metadata
      if (!customer?.customerID) {
        await Billing.stripe().customers.update(customerID, {
          metadata: {
            workspaceID,
          },
        })
      }

      // get payment method for the payment intent
      const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
        expand: ["payment_method"],
      })
      const paymentMethod = paymentIntent.payment_method
      if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")

      await Database.transaction(async (tx) => {
        await tx
          .update(BillingTable)
          .set({
            balance: sql`${BillingTable.balance} + ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
            customerID,
            paymentMethodID: paymentMethod.id,
            paymentMethodLast4: paymentMethod.card!.last4,
            reload: true,
            reloadError: null,
            timeReloadError: null,
          })
          .where(eq(BillingTable.workspaceID, workspaceID))
        await tx.insert(PaymentTable).values({
          workspaceID,
          id: Identifier.create("payment"),
          amount: centsToMicroCents(Billing.CHARGE_AMOUNT),
          paymentID,
          invoiceID,
          customerID,
        })
      })
    })
  }
  if (body.type === "charge.refunded") {
    const customerID = body.data.object.customer as string
    const paymentIntentID = body.data.object.payment_intent as string
    if (!customerID) throw new Error("Customer ID not found")
    if (!paymentIntentID) throw new Error("Payment ID not found")

    const workspaceID = await Database.use((tx) =>
      tx
        .select({
          workspaceID: BillingTable.workspaceID,
        })
        .from(BillingTable)
        .where(eq(BillingTable.customerID, customerID))
        .then((rows) => rows[0]?.workspaceID),
    )
    if (!workspaceID) throw new Error("Workspace ID not found")

    await Database.transaction(async (tx) => {
      await tx
        .update(PaymentTable)
        .set({
          timeRefunded: new Date(body.created * 1000),
        })
        .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))

      await tx
        .update(BillingTable)
        .set({
          balance: sql`${BillingTable.balance} - ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
        })
        .where(eq(BillingTable.workspaceID, workspaceID))
    })
  }

  console.log("finished handling")

  return Response.json("ok", { status: 200 })
}