import { NextRequest, NextResponse } from 'next/server'; import { getSessionUserId } from '@/lib/session'; import { stripe } from '@/lib/stripe'; import { db } from '@/lib/db'; import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; import { scoreUserLifecycle } from '@/lib/revops-server'; export async function POST(request: NextRequest) { try { const userId = getSessionUserId(); // Rate Limiting (user-based) const clientId = userId || getClientIdentifier(request); const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CANCEL); if (!rateLimitResult.success) { return NextResponse.json( { error: 'Too many requests. Please try again later.', retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000) }, { status: 429, headers: { 'X-RateLimit-Limit': rateLimitResult.limit.toString(), 'X-RateLimit-Remaining': rateLimitResult.remaining.toString(), 'X-RateLimit-Reset': rateLimitResult.reset.toString(), } } ); } if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // Get user with subscription info const user = await db.user.findUnique({ where: { id: userId }, select: { stripeSubscriptionId: true, stripeCurrentPeriodEnd: true, plan: true, }, }); if (!user) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } // Already on free plan if (user.plan === 'FREE') { return NextResponse.json({ error: 'Already on free plan' }, { status: 400 }); } // No active subscription if (!user.stripeSubscriptionId) { // Just update plan to FREE if somehow plan is not FREE but no subscription await db.user.update({ where: { id: userId }, data: { plan: 'FREE', stripePriceId: null, stripeCurrentPeriodEnd: null, }, }); await scoreUserLifecycle(userId, 'subscription_deleted'); return NextResponse.json({ success: true }); } // Schedule cancellation at the end of the paid period so paid features stay active. const subscription: any = await stripe.subscriptions.update(user.stripeSubscriptionId, { cancel_at_period_end: true, }); const periodEndTimestamp = subscription.current_period_end || subscription.currentPeriodEnd || subscription.billing_cycle_anchor; const currentPeriodEnd = periodEndTimestamp ? new Date(periodEndTimestamp * 1000) : user.stripeCurrentPeriodEnd; // Keep the paid plan locally until Stripe sends customer.subscription.deleted. await db.user.update({ where: { id: userId }, data: { stripeCurrentPeriodEnd: currentPeriodEnd, }, }); await scoreUserLifecycle(userId, 'subscription_canceled_at_period_end'); return NextResponse.json({ success: true, currentPeriodEnd }); } catch (error) { console.error('Error canceling subscription:', error); return NextResponse.json( { error: 'Failed to cancel subscription' }, { status: 500 } ); } }