zen: disable reload when reload fails

This commit is contained in:
Frank
2026-01-24 08:33:47 -05:00
parent 7c2e59de68
commit d8bbb6df60
3 changed files with 74 additions and 29 deletions

View File

@@ -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",

View File

@@ -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") {

View File

@@ -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) => {