Spaces:
Build error
Build error
| import { NextRequest, NextResponse } from "next/server"; | |
| import { db } from "@/lib/db"; | |
| import { stripe, parseWebhookEvent } from "@/lib/stripe"; | |
| // Webhook de Stripe mejorado con validaci贸n de firma | |
| export async function POST(request: NextRequest) { | |
| try { | |
| const signature = request.headers.get("stripe-signature"); | |
| if (!signature) return NextResponse.json({ success: false, error: "Firma faltante" }, { status: 400 }); | |
| const body = await request.text(); | |
| // Parsear y validar webhook | |
| let event; | |
| try { | |
| event = parseWebhookEvent(body, signature); | |
| } catch (err) { | |
| console.error("Firma de webhook inv谩lida:", err); | |
| return NextResponse.json({ success: false, error: "Firma de webhook inv谩lida" }, { status: 403 }); | |
| } | |
| // Manejar eventos de Stripe | |
| switch (event.type) { | |
| case "checkout.session.completed": | |
| return await handleCheckoutCompleted(event.data.object); | |
| case "customer.subscription.updated": | |
| return await handleSubscriptionUpdated(event.data.object); | |
| case "customer.subscription.deleted": | |
| return await handleSubscriptionDeleted(event.data.object); | |
| case "invoice.payment_succeeded": | |
| return await handleInvoicePaymentSucceeded(event.data.object); | |
| case "invoice.payment_failed": | |
| return await handleInvoicePaymentFailed(event.data.object); | |
| default: | |
| console.log(`Evento no manejado: ${event.type}`); | |
| return NextResponse.json({ success: true, received: true }); | |
| } | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "Error desconocido"; | |
| console.error("Webhook error:", message); | |
| return NextResponse.json({ success: false, error: message }, { status: 500 }); | |
| } | |
| } | |
| // Sesi贸n de checkout completada: crear suscripci贸n | |
| async function handleCheckoutCompleted(session: any) { | |
| try { | |
| const userId = session.metadata?.userId; | |
| const influencerId = session.metadata?.influencerId; | |
| const tier = session.metadata?.tier; | |
| const stripeCustomerId = session.customer; | |
| const stripeSubscriptionId = session.subscription; | |
| if (!userId || !influencerId || !tier) { | |
| console.error("Metadata incompleta en checkout"); | |
| return NextResponse.json({ success: false, error: "Metadata incompleta" }, { status: 400 }); | |
| } | |
| // Obtener detalles de la suscripci贸n en Stripe | |
| const stripeSub = await stripe.subscriptions.retrieve(stripeSubscriptionId); | |
| const firstItem = stripeSub.items.data[0]; | |
| const price = firstItem?.price?.unit_amount ? firstItem.price.unit_amount / 100 : 0; | |
| const nextRenewalDate = firstItem ? new Date(firstItem.current_period_end * 1000) : new Date(); | |
| // Crear suscripci贸n en BD | |
| const subscription = await db.influencerSubscription.create({ | |
| data: { | |
| influencerId, | |
| userId, | |
| tier, | |
| price, | |
| currency: "USD", | |
| stripeSubscriptionId, | |
| stripeCustomerId, | |
| autoRenew: true, | |
| nextRenewalDate, | |
| }, | |
| }); | |
| // TODO: Crear Earning cuando exista un registro real en MonetizationPlatform | |
| // El platformId debe ser un cuid v谩lido de la tabla MonetizationPlatform | |
| console.log(`Suscripci贸n creada: ${subscription.id}`); | |
| return NextResponse.json({ success: true, subscription }); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "Error"; | |
| console.error("Error handleCheckoutCompleted:", message); | |
| return NextResponse.json({ success: false, error: message }, { status: 500 }); | |
| } | |
| } | |
| // Renovaci贸n autom谩tica: invoice pagado | |
| async function handleInvoicePaymentSucceeded(invoice: any) { | |
| try { | |
| const stripeSubscriptionId = invoice.subscription; | |
| if (!stripeSubscriptionId) return NextResponse.json({ success: true }); | |
| const subscription = await db.influencerSubscription.findFirst({ | |
| where: { stripeSubscriptionId }, | |
| }); | |
| if (!subscription) { | |
| console.error(`Suscripci贸n no encontrada: ${stripeSubscriptionId}`); | |
| return NextResponse.json({ success: true }); | |
| } | |
| // Obtener detalles de la suscripci贸n actualizada | |
| const stripeSub = await stripe.subscriptions.retrieve(stripeSubscriptionId); | |
| const firstItem = stripeSub.items.data[0]; | |
| const nextRenewalDate = firstItem ? new Date(firstItem.current_period_end * 1000) : new Date(); | |
| const amountPaid = invoice.amount_paid / 100; | |
| // Actualizar pr贸xima renovaci贸n | |
| await db.influencerSubscription.update({ | |
| where: { id: subscription.id }, | |
| data: { nextRenewalDate }, | |
| }); | |
| // TODO: Crear Earning cuando exista un registro real en MonetizationPlatform | |
| // El platformId debe ser un cuid v谩lido de la tabla MonetizationPlatform | |
| console.log(`Renovaci贸n procesada: suscripci贸n ${subscription.id}, pr贸xima: ${nextRenewalDate}`); | |
| return NextResponse.json({ success: true }); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "Error"; | |
| console.error("Error handleInvoicePaymentSucceeded:", message); | |
| return NextResponse.json({ success: false, error: message }, { status: 500 }); | |
| } | |
| } | |
| // Pago de renovaci贸n fallido: marcar suscripci贸n para aviso | |
| async function handleInvoicePaymentFailed(invoice: any) { | |
| try { | |
| const stripeSubscriptionId = invoice.subscription; | |
| if (!stripeSubscriptionId) return NextResponse.json({ success: true }); | |
| const subscription = await db.influencerSubscription.findFirst({ | |
| where: { stripeSubscriptionId }, | |
| }); | |
| if (!subscription) return NextResponse.json({ success: true }); | |
| // Registrar intento fallido | |
| console.error(`Pago fallido para suscripci贸n: ${subscription.id}`); | |
| // TODO: notificar al usuario | |
| return NextResponse.json({ success: true }); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "Error"; | |
| console.error("Error handleInvoicePaymentFailed:", message); | |
| return NextResponse.json({ success: false, error: message }, { status: 500 }); | |
| } | |
| } | |
| // Suscripci贸n actualizada: cambios de plan, estado, etc. | |
| async function handleSubscriptionUpdated(stripeSub: any) { | |
| try { | |
| const subscription = await db.influencerSubscription.findFirst({ | |
| where: { stripeSubscriptionId: stripeSub.id }, | |
| }); | |
| if (!subscription) return NextResponse.json({ success: true }); | |
| const nextRenewalDate = stripeSub.cancel_at ? new Date(stripeSub.cancel_at * 1000) : new Date(stripeSub.current_period_end * 1000); | |
| await db.influencerSubscription.update({ | |
| where: { id: subscription.id }, | |
| data: { nextRenewalDate }, | |
| }); | |
| console.log(`Suscripci贸n actualizada: ${subscription.id}`); | |
| return NextResponse.json({ success: true }); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "Error"; | |
| console.error("Error handleSubscriptionUpdated:", message); | |
| return NextResponse.json({ success: false, error: message }, { status: 500 }); | |
| } | |
| } | |
| // Suscripci贸n eliminada/cancelada | |
| async function handleSubscriptionDeleted(stripeSub: any) { | |
| try { | |
| const subscription = await db.influencerSubscription.findFirst({ | |
| where: { stripeSubscriptionId: stripeSub.id }, | |
| }); | |
| if (!subscription) return NextResponse.json({ success: true }); | |
| // Marcar como cancelada | |
| await db.influencerSubscription.update({ | |
| where: { id: subscription.id }, | |
| data: { status: "cancelled", autoRenew: false }, | |
| }); | |
| console.log(`Suscripci贸n cancelada: ${subscription.id}`); | |
| return NextResponse.json({ success: true }); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "Error"; | |
| console.error("Error handleSubscriptionDeleted:", message); | |
| return NextResponse.json({ success: false, error: message }, { status: 500 }); | |
| } | |
| } | |