open-prompt / src /app /api /stripe /webhook /route.ts
GitHub Action
Automated sync to Hugging Face
bcce530
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { stripe } from '@/lib/stripe'
import prisma from '@/lib/prisma'
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || ''
/**
* POST /api/stripe/webhook
* Handles Stripe webhook events to keep subscription status in sync.
*
* Add this URL to your Stripe Dashboard webhook settings:
* https://your-domain.com/api/stripe/webhook
*
* Required events to enable:
* - checkout.session.completed
* - customer.subscription.updated
* - customer.subscription.deleted
* - invoice.payment_failed
*/
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')
if (!signature) {
return NextResponse.json({ error: 'Missing stripe-signature header' }, { status: 400 })
}
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, WEBHOOK_SECRET)
} catch (err) {
console.error('Webhook signature verification failed:', err)
return NextResponse.json({ error: 'Invalid webhook signature' }, { status: 400 })
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
await handleCheckoutCompleted(session)
break
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription
await handleSubscriptionUpdated(subscription)
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
await handleSubscriptionDeleted(subscription)
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
await handlePaymentFailed(invoice)
break
}
default:
// Unhandled event type — ignore
break
}
return NextResponse.json({ received: true })
} catch (error) {
console.error(`Webhook handler error for ${event.type}:`, error)
return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 })
}
}
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
if (session.mode !== 'subscription') return
const userId = session.metadata?.userId
if (!userId) {
console.error('No userId in checkout session metadata')
return
}
const subscription = await stripe.subscriptions.retrieve(session.subscription as string)
// Stripe 2026 SDK renamed period fields — cast to access them
const sub = subscription as unknown as Record<string, unknown>
const periodStart = sub.current_period_start as number
const periodEnd = sub.current_period_end as number
// Upsert subscription record
await prisma.$transaction([
prisma.subscription.upsert({
where: { userId },
create: {
userId,
stripeCustomerId: session.customer as string,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
plan: 'pro',
status: subscription.status,
currentPeriodStart: new Date(periodStart * 1000),
currentPeriodEnd: new Date(periodEnd * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
update: {
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
status: subscription.status,
currentPeriodStart: new Date(periodStart * 1000),
currentPeriodEnd: new Date(periodEnd * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
}),
// Update user plan
prisma.user.update({
where: { id: userId },
data: {
stripePlan: 'pro',
stripeCustomerId: session.customer as string,
stripeSubscriptionId: subscription.id,
stripeSubscriptionStatus: subscription.status,
},
}),
])
console.log(`✅ Pro subscription activated for user ${userId}`)
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
const userId = subscription.metadata?.userId
if (!userId) return
const isActive = subscription.status === 'active' || subscription.status === 'trialing'
const sub = subscription as unknown as Record<string, unknown>
const periodStart = sub.current_period_start as number
const periodEnd = sub.current_period_end as number
await prisma.$transaction([
prisma.subscription.updateMany({
where: { stripeSubscriptionId: subscription.id },
data: {
status: subscription.status,
currentPeriodStart: new Date(periodStart * 1000),
currentPeriodEnd: new Date(periodEnd * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
canceledAt: subscription.canceled_at ? new Date(subscription.canceled_at * 1000) : null,
},
}),
prisma.user.update({
where: { id: userId },
data: {
stripePlan: isActive ? 'pro' : 'free',
stripeSubscriptionStatus: subscription.status,
},
}),
])
}
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
const userId = subscription.metadata?.userId
if (!userId) return
// Downgrade to free
await prisma.$transaction([
prisma.subscription.updateMany({
where: { stripeSubscriptionId: subscription.id },
data: { status: 'canceled', canceledAt: new Date() },
}),
prisma.user.update({
where: { id: userId },
data: {
stripePlan: 'free',
stripeSubscriptionId: null,
stripeSubscriptionStatus: 'canceled',
},
}),
])
console.log(`Subscription canceled for user ${userId} — downgraded to free`)
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
const customerId = invoice.customer as string
if (!customerId) return
const user = await prisma.user.findUnique({
where: { stripeCustomerId: customerId },
select: { id: true },
})
if (!user) return
await prisma.user.update({
where: { id: user.id },
data: { stripeSubscriptionStatus: 'past_due' },
})
console.warn(`Payment failed for customer ${customerId}`)
}