fix(billing): change reset user stats func to invoice payment succeeded (#1116)

* fix(billing): change reset user stats func to invoice payment succeeded

* remove nonexistent billing reason
This commit is contained in:
Vikhyath Mondreti
2025-08-23 10:50:23 -07:00
committed by GitHub
parent 780870c48e
commit 25b2c45ec0

View File

@@ -18,27 +18,75 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) {
try {
const invoice = event.data.object as Stripe.Invoice
// Check if this is an overage billing invoice
if (invoice.metadata?.type !== 'overage_billing') {
logger.info('Ignoring non-overage billing invoice', { invoiceId: invoice.id })
// Case 1: Overage invoices (metadata.type === 'overage_billing')
if (invoice.metadata?.type === 'overage_billing') {
const customerId = invoice.customer as string
const chargedAmount = invoice.amount_paid / 100
const billingPeriod = invoice.metadata?.billingPeriod || 'unknown'
logger.info('Overage billing invoice payment succeeded', {
invoiceId: invoice.id,
customerId,
chargedAmount,
billingPeriod,
customerEmail: invoice.customer_email,
hostedInvoiceUrl: invoice.hosted_invoice_url,
})
return
}
const customerId = invoice.customer as string
const chargedAmount = invoice.amount_paid / 100 // Convert from cents to dollars
const billingPeriod = invoice.metadata?.billingPeriod || 'unknown'
// Case 2: Subscription renewal invoice paid (primary period rollover)
// Only reset on successful payment to avoid granting a new period while in dunning
if (invoice.subscription) {
// Filter to subscription-cycle renewals; ignore updates/off-cycle charges
const reason = invoice.billing_reason
const isCycle = reason === 'subscription_cycle'
if (!isCycle) {
logger.info('Ignoring non-cycle subscription invoice on payment_succeeded', {
invoiceId: invoice.id,
billingReason: reason,
})
return
}
logger.info('Overage billing invoice payment succeeded', {
invoiceId: invoice.id,
customerId,
chargedAmount,
billingPeriod,
customerEmail: invoice.customer_email,
hostedInvoiceUrl: invoice.hosted_invoice_url,
})
const stripeSubscriptionId = String(invoice.subscription)
const records = await db
.select()
.from(subscriptionTable)
.where(eq(subscriptionTable.stripeSubscriptionId, stripeSubscriptionId))
.limit(1)
// Additional payment success logic can be added here
// For example: update internal billing status, trigger analytics events, etc.
if (records.length === 0) {
logger.warn('No matching internal subscription for paid Stripe invoice', {
invoiceId: invoice.id,
stripeSubscriptionId,
})
return
}
const sub = records[0]
if (sub.plan === 'team' || sub.plan === 'enterprise') {
await resetOrganizationBillingPeriod(sub.referenceId)
logger.info('Reset organization billing period on subscription invoice payment', {
invoiceId: invoice.id,
organizationId: sub.referenceId,
plan: sub.plan,
})
} else {
await resetUserBillingPeriod(sub.referenceId)
logger.info('Reset user billing period on subscription invoice payment', {
invoiceId: invoice.id,
userId: sub.referenceId,
plan: sub.plan,
})
}
return
}
logger.info('Ignoring non-subscription invoice payment', { invoiceId: invoice.id })
} catch (error) {
logger.error('Failed to handle invoice payment succeeded', {
eventId: event.id,
@@ -105,67 +153,20 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
export async function handleInvoiceFinalized(event: Stripe.Event) {
try {
const invoice = event.data.object as Stripe.Invoice
// Case 1: Overage invoices (metadata.type === 'overage_billing')
// Do not reset usage on finalized; wait for payment success to avoid granting new period during dunning
if (invoice.metadata?.type === 'overage_billing') {
const customerId = invoice.customer as string
const invoiceAmount = invoice.amount_due / 100
const billingPeriod = invoice.metadata?.billingPeriod || 'unknown'
logger.info('Overage billing invoice finalized', {
invoiceId: invoice.id,
customerId,
invoiceAmount,
billingPeriod,
customerEmail: invoice.customer_email,
hostedInvoiceUrl: invoice.hosted_invoice_url,
})
return
}
// Case 2: Subscription cycle invoices (primary period rollover)
// When an invoice is finalized for a subscription cycle, align our usage reset to this boundary
if (invoice.subscription) {
const stripeSubscriptionId = String(invoice.subscription)
const records = await db
.select()
.from(subscriptionTable)
.where(eq(subscriptionTable.stripeSubscriptionId, stripeSubscriptionId))
.limit(1)
if (records.length === 0) {
logger.warn('No matching internal subscription for Stripe invoice subscription', {
invoiceId: invoice.id,
stripeSubscriptionId,
})
return
}
const sub = records[0]
// Idempotent reset aligned to the subscriptions new cycle
if (sub.plan === 'team' || sub.plan === 'enterprise') {
await resetOrganizationBillingPeriod(sub.referenceId)
logger.info('Reset organization billing period on subscription invoice finalization', {
invoiceId: invoice.id,
organizationId: sub.referenceId,
plan: sub.plan,
})
} else {
await resetUserBillingPeriod(sub.referenceId)
logger.info('Reset user billing period on subscription invoice finalization', {
invoiceId: invoice.id,
userId: sub.referenceId,
plan: sub.plan,
})
}
return
}
logger.info('Ignoring non-subscription invoice finalization', {
logger.info('Ignoring subscription invoice finalization; will act on payment_succeeded', {
invoiceId: invoice.id,
billingReason: invoice.billing_reason,
})