diff --git a/apps/next-frontend/app/api/debug/invite-codes/route.ts b/apps/next-frontend/app/api/debug/invite-codes/route.ts new file mode 100644 index 00000000..2d394c65 --- /dev/null +++ b/apps/next-frontend/app/api/debug/invite-codes/route.ts @@ -0,0 +1,34 @@ +import { db } from '@ticketeate/db'; + +export const dynamic = 'force-dynamic'; + +export async function GET(request: Request) { + try { + // Verificar que la tabla invite_codes existe + const inviteCodesCount = await db.invite_codes.count(); + const colaboradoresCount = await db.colaborador_eventos.count(); + + // Obtener los primeros 5 códigos de invitación + const inviteCodes = await db.invite_codes.findMany({ + take: 5, + include: { + eventos: { + select: { + eventoid: true, + nombre: true, + }, + }, + }, + }); + + return Response.json({ + status: 'ok', + inviteCodesCount, + colaboradoresCount, + recentInviteCodes: inviteCodes, + }); + } catch (error) { + console.error('Error in debug endpoint:', error); + return Response.json({ error: 'Error in debug endpoint' }, { status: 500 }); + } +} diff --git a/apps/next-frontend/app/api/invite-codes/use/route.ts b/apps/next-frontend/app/api/invite-codes/use/route.ts new file mode 100644 index 00000000..8bc1c290 --- /dev/null +++ b/apps/next-frontend/app/api/invite-codes/use/route.ts @@ -0,0 +1,99 @@ +import { auth } from '@/lib/auth'; +import { db } from '@ticketeate/db'; + +export const dynamic = 'force-dynamic'; + +interface UseInviteCodeResponse { + success: boolean; + message: string; + eventoid?: string; + colaborador_evento_id?: string; + error?: string; +} + +export async function POST(request: Request): Promise { + try { + // Obtener la sesión del usuario + const session = await auth.api.getSession({ + headers: request.headers, + }); + + if (!session || !session.user) { + return Response.json({ error: 'Usuario no autenticado' }, { status: 401 }); + } + + const { codigo } = await request.json(); + + if (!codigo || typeof codigo !== 'string') { + return Response.json({ error: 'Código de invitación requerido' }, { status: 400 }); + } + + const codigoTrimmed = codigo.trim().toUpperCase(); + + // Buscar el código en la base de datos + const inviteCode = await db.invite_codes.findFirst({ + where: { + codigo: codigoTrimmed, + estado: 'ACTIVO', + fecha_expiracion: { + gt: new Date(), + }, + }, + }); + + if (!inviteCode) { + return Response.json({ error: 'Código de invitación no válido o expirado' }, { status: 401 }); + } + + // Verificar que no ha excedido el máximo de usos + if (inviteCode.usos_totales >= inviteCode.usos_max) { + return Response.json( + { error: 'Código de invitación ya ha alcanzado el máximo de usos' }, + { status: 401 }, + ); + } + + // Verificar si el usuario ya es colaborador de este evento + const existingColaborador = await db.colaborador_eventos.findFirst({ + where: { + eventoid: inviteCode.eventoid, + usuarioid: session.user.id, + }, + }); + + if (existingColaborador) { + return Response.json({ error: 'Ya eres colaborador de este evento' }, { status: 400 }); + } + + // Crear el registro de colaborador_eventos + const colaborador = await db.colaborador_eventos.create({ + data: { + eventoid: inviteCode.eventoid, + usuarioid: session.user.id, + invite_code_used: inviteCode.codigoid, + }, + }); + + // Incrementar el contador de usos del código + await db.invite_codes.update({ + where: { + codigoid: inviteCode.codigoid, + }, + data: { + usos_totales: { + increment: 1, + }, + }, + }); + + return Response.json({ + success: true, + message: 'Te has registrado como colaborador correctamente', + eventoid: inviteCode.eventoid, + colaborador_evento_id: colaborador.colaborador_evento_id, + } as UseInviteCodeResponse); + } catch (error) { + console.error('Error using invite code:', error); + return Response.json({ error: 'Error al usar código de invitación' }, { status: 500 }); + } +} diff --git a/apps/next-frontend/app/api/invite-codes/validate/route.ts b/apps/next-frontend/app/api/invite-codes/validate/route.ts new file mode 100644 index 00000000..a96ff2fb --- /dev/null +++ b/apps/next-frontend/app/api/invite-codes/validate/route.ts @@ -0,0 +1,68 @@ +import { auth } from '@/lib/auth'; +import { db } from '@ticketeate/db'; + +export const dynamic = 'force-dynamic'; + +interface ValidateInviteCodeResponse { + valid: boolean; + eventoid?: string; + evento?: { + eventoid: string; + nombre: string; + descripcion: string; + fecha_inicio: string; + }; + error?: string; +} + +export async function POST(request: Request): Promise { + try { + const { codigo } = await request.json(); + + if (!codigo || typeof codigo !== 'string') { + return Response.json({ error: 'Código de invitación requerido' }, { status: 400 }); + } + + // Buscar el código en la base de datos + const inviteCode = await db.invite_codes.findFirst({ + where: { + codigo: codigo.trim().toUpperCase(), + estado: 'ACTIVO', + fecha_expiracion: { + gt: new Date(), // Mayor que la fecha actual + }, + }, + include: { + eventos: { + select: { + eventoid: true, + nombre: true, + descripcion: true, + fecha_inicio: true, + }, + }, + }, + }); + + if (!inviteCode) { + return Response.json({ error: 'Código de invitación no válido o expirado' }, { status: 401 }); + } + + // Verificar que no ha excedido el máximo de usos + if (inviteCode.usos_totales >= inviteCode.usos_max) { + return Response.json( + { error: 'Código de invitación ya ha alcanzado el máximo de usos' }, + { status: 401 }, + ); + } + + return Response.json({ + valid: true, + eventoid: inviteCode.eventoid, + evento: inviteCode.eventos, + } as ValidateInviteCodeResponse); + } catch (error) { + console.error('Error validating invite code:', error); + return Response.json({ error: 'Error validando código de invitación' }, { status: 500 }); + } +} diff --git a/apps/next-frontend/app/api/mercadopago/create-preference/route.ts b/apps/next-frontend/app/api/mercadopago/create-preference/route.ts index 8d074ba8..a0977cae 100644 --- a/apps/next-frontend/app/api/mercadopago/create-preference/route.ts +++ b/apps/next-frontend/app/api/mercadopago/create-preference/route.ts @@ -128,7 +128,19 @@ export async function POST(request: NextRequest) { }, auto_return: 'approved', metadata: { - ...metadata, + // Información del evento + event_id: metadata?.event_id, + event_titulo: metadata?.event_titulo, + // Información del comprador + buyer_id: metadata?.buyer_id, + buyer_email: metadata?.buyer_email, + buyer_nombre: metadata?.buyer_nombre, + // Información del pedido + cantidad: metadata?.cantidad, + sector: metadata?.sector, + precio_unitario: metadata?.precio_unitario, + transaction_id: metadata?.transaction_id, + // Información del vendedor/organizador seller_id: seller.id, seller_mp_user_id: seller.mercado_pago_user_id, marketplace_fee: marketplaceFee, @@ -190,10 +202,17 @@ export async function POST(request: NextRequest) { marketplace_fee: marketplaceFee, status: 'pending', metadata: { - ...metadata, + event_id: metadata?.event_id, + event_titulo: metadata?.event_titulo, + buyer_id: metadata?.buyer_id, + buyer_email: metadata?.buyer_email, + buyer_nombre: metadata?.buyer_nombre, + cantidad: metadata?.cantidad, + sector: metadata?.sector, + precio_unitario: metadata?.precio_unitario, + transaction_id: metadata?.transaction_id, seller_id: seller.id, - buyer_id: session.user.id, - event_id: eventId, + seller_mp_user_id: seller.mercado_pago_user_id, marketplace_fee: marketplaceFee, }, }, diff --git a/apps/next-frontend/app/api/mercadopago/webhook/route.ts b/apps/next-frontend/app/api/mercadopago/webhook/route.ts index 2a4b10bb..55595a9a 100644 --- a/apps/next-frontend/app/api/mercadopago/webhook/route.ts +++ b/apps/next-frontend/app/api/mercadopago/webhook/route.ts @@ -38,41 +38,78 @@ function generateQRCode(): string { /** * Crea las entradas (reservas y códigos QR) cuando un pago es aprobado */ -async function createOrderTickets(order: any, preferenceDetails: any, buyerEmail?: string) { +async function createOrderTickets( + metadata: Record, + preferenceDetails: any, + sellerId: string, +) { if (!preferenceDetails?.items || preferenceDetails.items.length === 0) { console.warn('[MP Webhook] Preference has no items'); return; } - // Obtener al comprador (por email si es disponible, o buscar si existe) + // Obtener al comprador desde la metadata + const buyerEmail = metadata.buyer_email; + const eventId = metadata.event_id; + const cantidad = metadata.cantidad; + const sector = metadata.sector; + + if (!eventId) { + console.error('[MP Webhook] No eventId in metadata'); + return; + } + + console.log('[MP Webhook] Creating tickets with metadata:', { + eventId, + buyerEmail, + cantidad, + sector, + }); + + // Obtener al comprador por email let buyer = null; if (buyerEmail) { buyer = await prisma.user.findUnique({ where: { email: buyerEmail }, + select: { id: true, name: true, email: true }, + }); + console.log('[MP Webhook] Buyer lookup:', { + email: buyerEmail, + found: !!buyer, + buyerId: buyer?.id, }); - } - - // Buscar al evento desde los metadatos - const eventId = order.metadata?.eventId; - if (!eventId) { - console.error('[MP Webhook] No eventId in order metadata'); - return; } // Para cada item en la preferencia, crear las reservas y entradas for (const item of preferenceDetails.items) { try { - // Obtener el stock de entradas para esta categoría - const stockEntry = await prisma.stock_entrada.findFirst({ + // Obtener el stock de entradas - buscar por nombre del item + let stockEntry = await prisma.stock_entrada.findFirst({ where: { eventoid: eventId, - nombre: item.title, // Simplificado - en prod deberías pasar el stock ID + // El nombre debería coincidir con el título del item + // En este caso: "Nombre del evento - sector" }, + orderBy: { stockid: 'asc' }, }); if (!stockEntry) { - console.warn('[MP Webhook] Stock entry not found for item:', item.title); - continue; + console.warn('[MP Webhook] Stock entry not found for event:', { + eventId, + itemTitle: item.title, + }); + // Intentar obtener cualquier stock disponible para este evento + const fallbackStock = await prisma.stock_entrada.findFirst({ + where: { eventoid: eventId }, + orderBy: { stockid: 'asc' }, + }); + + if (!fallbackStock) { + console.error('[MP Webhook] No stock entries found for event:', eventId); + continue; + } + + stockEntry = fallbackStock; } // Crear la reserva @@ -88,8 +125,8 @@ async function createOrderTickets(order: any, preferenceDetails: any, buyerEmail continue; } - // Si no hay buyer, crear un guest user o usar anonymous - const usuarioId = buyer?.id || order.seller_id; // Fallback al vendedor si no hay buyer + // Usuario de la reserva: el comprador si existe, sino crear un registro temporal + const usuarioId = buyer?.id || metadata.buyer_id; await prisma.reservas.create({ data: { @@ -105,6 +142,13 @@ async function createOrderTickets(order: any, preferenceDetails: any, buyerEmail }, }); + console.log('[MP Webhook] Reserva created:', { + reservaId, + usuarioId, + eventId, + cantidad: item.quantity, + }); + // Crear las entradas individuales con códigos QR const entradas = []; for (let i = 0; i < item.quantity; i++) { @@ -118,7 +162,7 @@ async function createOrderTickets(order: any, preferenceDetails: any, buyerEmail estado: 'VALIDA', version: 1, is_active: true, - updated_by: order.seller_id, + updated_by: sellerId, }); } @@ -129,16 +173,17 @@ async function createOrderTickets(order: any, preferenceDetails: any, buyerEmail skipDuplicates: true, }); - console.log('[MP Webhook] Created tickets:', { + console.log('[MP Webhook] ✅ Created tickets:', { reservaId, entradaCount: entradas.length, eventId, + buyerEmail, }); } } catch (itemError) { console.error('[MP Webhook] Error creating ticket for item:', { item: item.title, - error: itemError, + error: itemError instanceof Error ? itemError.message : String(itemError), }); // Continuar con el siguiente item } @@ -156,87 +201,118 @@ export async function POST(request: NextRequest) { try { const body = await request.json().catch(() => ({})); - console.log('[MP Webhook] Notification received:', { - timestamp: new Date().toISOString(), - body: JSON.stringify(body, null, 2), - }); + console.log('[MP Webhook] ===== NOTIFICATION RECEIVED ====='); + console.log('[MP Webhook] Body:', JSON.stringify(body, null, 2)); const { type, action, data } = body; - // MercadoPago envía diferentes tipos de notificaciones - // Nos interesan: "payment" y "merchant_order" - if (type === 'payment' || action === 'payment.created' || action === 'payment.updated') { - const paymentId = data?.id; + console.log('[MP Webhook] Processing:', { + type, + action, + paymentId: data?.id, + timestamp: new Date().toISOString(), + }); - if (!paymentId) { - console.error('[MP Webhook] Missing payment ID'); - return NextResponse.json({ error: 'Missing payment ID' }, { status: 400 }); - } + // MercadoPago envía diferentes tipos de notificaciones + // Nos interesan: type=payment (cuando el estado cambia) + // action puede ser: payment.created (pending), payment.updated (cambios de estado) + if (type !== 'payment') { + console.log('[MP Webhook] ⚠️ Ignoring notification - type:', type); + return NextResponse.json({ received: true }, { status: 200 }); + } - // Obtener detalles del pago desde la API de MercadoPago - const platformAccessToken = process.env.MP_PLATFORM_ACCESS_TOKEN; - if (!platformAccessToken) { - console.error('[MP Webhook] MP_PLATFORM_ACCESS_TOKEN not configured'); - return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); - } + const paymentId = data?.id; - const paymentResponse = await fetch(`https://api.mercadopago.com/v1/payments/${paymentId}`, { - headers: { - Authorization: `Bearer ${platformAccessToken}`, - }, - }); + if (!paymentId) { + console.error('[MP Webhook] ❌ Missing payment ID'); + return NextResponse.json({ error: 'Missing payment ID' }, { status: 400 }); + } - if (!paymentResponse.ok) { - const errorText = await paymentResponse.text(); - console.error('[MP Webhook] Error fetching payment:', { - status: paymentResponse.status, - body: errorText, - }); - return NextResponse.json({ error: 'Error fetching payment' }, { status: 500 }); - } + // Obtener detalles del pago desde la API de MercadoPago + const platformAccessToken = process.env.MP_PLATFORM_ACCESS_TOKEN; + if (!platformAccessToken) { + console.error('[MP Webhook] ❌ MP_PLATFORM_ACCESS_TOKEN not configured'); + return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); + } - const payment = await paymentResponse.json(); + console.log('[MP Webhook] Fetching payment details from MP API...'); + const paymentResponse = await fetch(`https://api.mercadopago.com/v1/payments/${paymentId}`, { + headers: { + Authorization: `Bearer ${platformAccessToken}`, + }, + }); - console.log('[MP Webhook] Payment details:', { - id: payment.id, - status: payment.status, - external_reference: payment.external_reference, - transaction_amount: payment.transaction_amount, - marketplace_fee: payment.fee_details?.find((f: any) => f.type === 'marketplace_fee') - ?.amount, + if (!paymentResponse.ok) { + const errorText = await paymentResponse.text(); + console.error('[MP Webhook] ❌ Error fetching payment:', { + status: paymentResponse.status, + body: errorText, }); + return NextResponse.json({ error: 'Error fetching payment' }, { status: 500 }); + } - // Buscar la orden en nuestra base de datos - const order = await prisma.mercadopago_orders.findFirst({ - where: { - external_reference: payment.external_reference, - }, - }); + const payment = await paymentResponse.json(); - if (!order) { - console.warn('[MP Webhook] Order not found for external_reference:', { - external_reference: payment.external_reference, - }); - // No fallar - puede ser una notificación válida para una orden que no guardamos - return NextResponse.json({ received: true }, { status: 200 }); - } + console.log('[MP Webhook] Payment details fetched:', { + id: payment.id, + status: payment.status, + external_reference: payment.external_reference, + transaction_amount: payment.transaction_amount, + payer_email: payment.payer?.email, + metadata: payment.metadata, + }); - // Actualizar estado de la orden según el estado del pago - const statusMap: Record = { - approved: 'approved', - pending: 'pending', - in_process: 'processing', - rejected: 'rejected', - cancelled: 'cancelled', - refunded: 'refunded', - charged_back: 'charged_back', - }; + // Extraer metadata directamente del pago + const metadata = payment.metadata || {}; + const eventId = metadata.event_id; + const buyerEmail = metadata.buyer_email || payment.payer?.email; + const buyerNombre = metadata.buyer_nombre || payment.payer?.first_name || ''; + const sellerId = metadata.seller_id; + const sellerMpUserId = metadata.seller_mp_user_id; + + if (!eventId) { + console.error('[MP Webhook] ❌ No eventId in payment metadata'); + return NextResponse.json({ error: 'Missing eventId' }, { status: 400 }); + } - const newStatus = statusMap[payment.status] || 'unknown'; - const isPaid = payment.status === 'approved'; + if (!sellerId) { + console.error('[MP Webhook] ❌ No sellerId in payment metadata'); + return NextResponse.json({ error: 'Missing sellerId' }, { status: 400 }); + } + + console.log('[MP Webhook] Metadata extracted:', { + eventId, + buyerEmail, + buyerNombre, + sellerId, + cantidad: metadata.cantidad, + sector: metadata.sector, + }); + // Actualizar estado de la orden según el estado del pago + const statusMap: Record = { + approved: 'approved', + pending: 'pending', + in_process: 'processing', + rejected: 'rejected', + cancelled: 'cancelled', + refunded: 'refunded', + charged_back: 'charged_back', + }; + + const newStatus = statusMap[payment.status] || 'unknown'; + const isPaid = payment.status === 'approved'; + + // Actualizar la orden en la BD si existe + const existingOrder = await prisma.mercadopago_orders.findFirst({ + where: { + external_reference: payment.external_reference, + }, + }); + + if (existingOrder) { await prisma.mercadopago_orders.update({ - where: { id: order.id }, + where: { id: existingOrder.id }, data: { status: newStatus, payment_id: payment.id.toString(), @@ -246,59 +322,93 @@ export async function POST(request: NextRequest) { }, }); - console.log('[MP Webhook] Order updated:', { - orderId: order.id, - preferenceId: order.preference_id, + console.log('[MP Webhook] Order status updated:', { + orderId: existingOrder.id, + preferenceId: existingOrder.preference_id, + oldStatus: existingOrder.status, newStatus, isPaid, - paymentId: payment.id, }); + } else { + console.warn('[MP Webhook] ⚠️ Order not found in database for external_reference:', { + external_reference: payment.external_reference, + }); + } - // Lógica cuando el pago es aprobado - if (isPaid) { - console.log('[MP Webhook] Payment approved - Processing order:', { - sellerId: order.seller_id, - amount: order.amount, - marketplaceFee: order.marketplace_fee, - netAmount: Number(order.amount) - Number(order.marketplace_fee), - }); + // Lógica cuando el pago es aprobado + if (isPaid) { + console.log('[MP Webhook] 🎉 Payment APPROVED - Creating tickets:', { + eventId, + sellerId, + amount: payment.transaction_amount, + buyerEmail, + }); - try { - // Obtener los detalles de la preferencia para las entradas - const preferenceDetails = await getPreferenceDetails(order.preference_id); - - if (preferenceDetails) { - // Crear las entradas/reservas para el comprador - await createOrderTickets(order, preferenceDetails, payment.payer?.email); - - console.log('[MP Webhook] Tickets created successfully for order:', { - orderId: order.id, - externalReference: order.external_reference, - }); - } - } catch (createTicketsError) { - console.error('[MP Webhook] Error creating tickets:', createTicketsError); - // No fallar la request - el pago ya se procesó correctamente + try { + // Obtener los detalles de la preferencia para las entradas + const preferenceId = existingOrder?.preference_id; + if (!preferenceId) { + console.error('[MP Webhook] ❌ No preference_id found'); + return NextResponse.json( + { + received: true, + orderId: existingOrder?.id, + status: newStatus, + isPaid, + error: 'No preference_id', + }, + { status: 200 }, + ); } - } - return NextResponse.json( - { - received: true, - orderId: order.id, - status: newStatus, - }, - { status: 200 }, - ); + console.log('[MP Webhook] Fetching preference details:', preferenceId); + const preferenceDetails = await getPreferenceDetails(preferenceId); + + if (preferenceDetails) { + console.log('[MP Webhook] Preference details received, creating tickets...'); + // Crear las entradas/reservas para el comprador + await createOrderTickets(metadata, preferenceDetails, sellerId); + + console.log('[MP Webhook] ✅ TICKETS CREATED SUCCESSFULLY:', { + orderId: existingOrder?.id, + externalReference: payment.external_reference, + eventId, + }); + } else { + console.warn('[MP Webhook] ⚠️ Could not get preference details'); + } + } catch (createTicketsError) { + console.error('[MP Webhook] ❌ Error creating tickets:', { + error: + createTicketsError instanceof Error + ? createTicketsError.message + : String(createTicketsError), + stack: createTicketsError instanceof Error ? createTicketsError.stack : undefined, + }); + // No fallar la request - el pago ya se procesó correctamente + } + } else { + console.log('[MP Webhook] ⏳ Payment not approved yet:', { + paymentStatus: payment.status, + externalReference: payment.external_reference, + }); } - // Para otros tipos de notificaciones, simplemente confirmar recepción - console.log('[MP Webhook] Unhandled notification type:', { type, action }); - return NextResponse.json({ received: true }, { status: 200 }); + return NextResponse.json( + { + received: true, + status: newStatus, + isPaid, + eventId, + }, + { status: 200 }, + ); } catch (error) { - console.error('[MP Webhook] Unexpected error:', error); + console.error('[MP Webhook] ❌ Unexpected error:', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); // Importante: Siempre retornar 200 para evitar que MercadoPago reintente indefinidamente - // si el error no es recuperable return NextResponse.json( { received: true, diff --git a/apps/next-frontend/app/colaborador/scanner/page.tsx b/apps/next-frontend/app/colaborador/scanner/page.tsx index f2ad9d75..071af487 100644 --- a/apps/next-frontend/app/colaborador/scanner/page.tsx +++ b/apps/next-frontend/app/colaborador/scanner/page.tsx @@ -58,7 +58,7 @@ export default function ScannerPage() { setIsUsingCode(true); try { - const response = await fetch('/api/administrador/invitados/usar-codigo', { + const response = await fetch('/api/invite-codes/use', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -71,7 +71,7 @@ export default function ScannerPage() { const data = await response.json(); if (!response.ok) { - toast.error(data.message || 'Error al validar código'); + toast.error(data.error || 'Error al validar código'); return; } diff --git a/apps/next-frontend/app/compra-exitosa/content.tsx b/apps/next-frontend/app/compra-exitosa/content.tsx index cdebbed7..80f46336 100644 --- a/apps/next-frontend/app/compra-exitosa/content.tsx +++ b/apps/next-frontend/app/compra-exitosa/content.tsx @@ -24,10 +24,12 @@ export default function CompraExitosaContent() {
{loading ? ( -
-
- +
+
+
+
+

Procesando tu compra...

) : ( <> @@ -40,11 +42,11 @@ export default function CompraExitosaContent() { {paymentId && (

ID de Pago:

-

{paymentId}

+

{paymentId}

{externalReference && ( <>

Referencia:

-

{externalReference}

+

{externalReference}

)}
diff --git a/apps/next-frontend/app/compra-fallida/content.tsx b/apps/next-frontend/app/compra-fallida/content.tsx index f10f5d08..43351c22 100644 --- a/apps/next-frontend/app/compra-fallida/content.tsx +++ b/apps/next-frontend/app/compra-fallida/content.tsx @@ -26,13 +26,6 @@ export default function CompraFallidaContent() {

Error en la Compra

{getErrorMessage()}

- {paymentId && ( -
-

ID de Pago:

-

{paymentId}

-
- )} -