Skip to content
168 changes: 110 additions & 58 deletions packages/console/app/src/routes/stripe/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,78 +109,33 @@ export async function POST(input: APIEvent) {
const type = body.data.object.metadata?.type
if (type === "lite") {
const workspaceID = body.data.object.metadata?.workspaceID
const userID = body.data.object.metadata?.userID
const userEmail = body.data.object.metadata?.userEmail
const coupon = body.data.object.metadata?.coupon
const customerID = body.data.object.customer as string
const invoiceID = body.data.object.latest_invoice as string
const subscriptionID = body.data.object.id as string
const paymentMethodID = body.data.object.default_payment_method as string

if (!workspaceID) throw new Error("Workspace ID not found")
if (!userID) throw new Error("User ID not found")
if (!customerID) throw new Error("Customer ID not found")
if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found")
if (!paymentMethodID) throw new Error("Payment method ID not found")

// get payment method for the payment intent
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
await Actor.provide("system", { workspaceID }, async () => {
// look up current billing
const billing = await Billing.get()
if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`)
if (billing.customerID && billing.customerID !== customerID) throw new Error("Customer ID mismatch")

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

await Database.transaction(async (tx) => {
await tx
await Database.use((tx) =>
tx
.update(BillingTable)
.set({
customerID,
liteSubscriptionID: subscriptionID,
lite: {},
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
})
.where(eq(BillingTable.workspaceID, workspaceID))

await tx.insert(LiteTable).values({
workspaceID,
id: Identifier.create("lite"),
userID: userID,
})

if (userEmail) {
if (coupon === LiteData.firstMonth50Coupon) {
await Billing.redeemCoupon(userEmail, "GO1MONTH50")
} else if (coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
} else if (coupon === LiteData.threeMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO3MONTHS100")
} else if (coupon === LiteData.sixMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO6MONTHS100")
} else if (coupon === LiteData.twelveMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO12MONTHS100")
}
}
})

await Referral.completeFromLiteSubscription({
workspaceID,
userID,
}).catch((error) => {
console.error("Referral sync failed", error)
})
.set({ customerID, liteSubscriptionID: subscriptionID })
.where(eq(BillingTable.workspaceID, workspaceID)),
)
})
}
}
Expand All @@ -207,10 +162,109 @@ export async function POST(input: APIEvent) {
}
}
if (body.type === "invoice.payment_succeeded") {
if (
body.data.object.billing_reason === "subscription_create" ||
body.data.object.billing_reason === "subscription_cycle"
) {
if (body.data.object.billing_reason === "subscription_create") {
const invoiceID = body.data.object.id as string
const amountInCents = body.data.object.amount_paid
const customerID = body.data.object.customer as string
const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
const productID = body.data.object.lines?.data[0].pricing?.price_details?.product as string

if (!customerID) throw new Error("Customer ID not found")
if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found")

const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
expand: ["discounts", "payments"],
})
const paymentID = invoice.payments?.data[0]?.payment.payment_intent as string
const couponID = (invoice.discounts[0] as Stripe.Discount)?.coupon?.id as string
if (!paymentID) {
if (!couponID) throw new Error("Payment ID not found")
}

const subscription = await Billing.stripe().subscriptions.retrieve(subscriptionID)
const workspaceID = subscription.metadata?.workspaceID
const userID = subscription.metadata?.userID
const userEmail = subscription.metadata?.userEmail
const coupon = subscription.metadata?.coupon

if (!workspaceID) throw new Error("Workspace ID not found in subscription metadata")
if (productID === LiteData.productID() && !userID) throw new Error("User ID not found in subscription metadata")

await Actor.provide("system", { workspaceID }, async () => {
const billing = await Billing.get()
if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`)

await Database.use((tx) =>
tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(amountInCents),
paymentID,
invoiceID,
customerID,
enrichment: {
type: productID === LiteData.productID() ? "lite" : "subscription",
currency: body.data.object.currency === "inr" ? "inr" : undefined,
couponID,
},
}),
)

if (productID === LiteData.productID() && !billing.lite) {
const paymentMethod = paymentID
? await Billing.stripe()
.paymentIntents.retrieve(paymentID, { expand: ["payment_method"] })
.then((pi) => (typeof pi.payment_method !== "string" ? pi.payment_method : null))
: null

await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
customerID,
liteSubscriptionID: subscriptionID,
lite: {},
...(paymentMethod
? {
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
}
: {}),
})
.where(eq(BillingTable.workspaceID, workspaceID))

await tx.insert(LiteTable).values({
workspaceID,
id: Identifier.create("lite"),
userID: userID!,
})
})
}

if (productID === LiteData.productID() && userEmail) {
if (coupon === LiteData.firstMonth50Coupon) {
await Billing.redeemCoupon(userEmail, "GO1MONTH50")
} else if (coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
} else if (coupon === LiteData.threeMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO3MONTHS100")
} else if (coupon === LiteData.sixMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO6MONTHS100")
} else if (coupon === LiteData.twelveMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO12MONTHS100")
}

await Referral.completeFromLiteSubscription({
workspaceID,
userID: userID!,
}).catch((error) => {
console.error("Referral sync failed", error)
})
}
})
} else if (body.data.object.billing_reason === "subscription_cycle") {
const invoiceID = body.data.object.id as string
const amountInCents = body.data.object.amount_paid
const customerID = body.data.object.customer as string
Expand All @@ -221,14 +275,12 @@ export async function POST(input: APIEvent) {
if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found")

// get coupon id from subscription
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
expand: ["discounts", "payments"],
})
const paymentID = invoice.payments?.data[0]?.payment.payment_intent as string
const couponID = (invoice.discounts[0] as Stripe.Discount).coupon?.id as string
const couponID = (invoice.discounts[0] as Stripe.Discount)?.coupon?.id as string
if (!paymentID) {
// payment id can be undefined when using coupon
if (!couponID) throw new Error("Payment ID not found")
}

Expand Down
15 changes: 15 additions & 0 deletions packages/console/core/src/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,21 @@ export namespace Billing {
async (input) => {
const { paymentID } = input

const payment = await Database.use((tx) =>
tx
.select()
.from(PaymentTable)
.where(
and(
eq(PaymentTable.workspaceID, Actor.workspace()),
eq(PaymentTable.paymentID, paymentID),
isNull(PaymentTable.timeDeleted),
),
)
.then((rows) => rows[0]),
)
if (!payment) throw new Error("Payment not found")

const intent = await Billing.stripe().paymentIntents.retrieve(paymentID)
if (!intent.latest_charge) throw new Error("No charge found")

Expand Down
Loading