summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--infra/console.ts2
-rw-r--r--packages/console/app/src/routes/stripe/webhook.ts70
-rw-r--r--packages/console/core/src/billing.ts31
3 files changed, 74 insertions, 29 deletions
diff --git a/infra/console.ts b/infra/console.ts
index 3d482160d..5b08e9cea 100644
--- a/infra/console.ts
+++ b/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",
diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts
index 34f83b446..9e310dc07 100644
--- a/packages/console/app/src/routes/stripe/webhook.ts
+++ b/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") {
diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts
index 7031a384b..44f12db9e 100644
--- a/packages/console/core/src/billing.ts
+++ b/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) => {