diff --git a/apps/next-frontend/app/api/invite-codes/use/route.ts b/apps/next-frontend/app/api/invite-codes/use/route.ts index c3e6ba1..2d34026 100644 --- a/apps/next-frontend/app/api/invite-codes/use/route.ts +++ b/apps/next-frontend/app/api/invite-codes/use/route.ts @@ -21,10 +21,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Código de invitación requerido' }, { status: 400 }); } + const normalizedCode = codigo.trim().toUpperCase(); + // Buscar el código de invitación const inviteCode = await prisma.invite_codes.findFirst({ where: { - codigo: codigo.trim().toUpperCase(), + codigo: normalizedCode, estado: 'ACTIVO', fecha_expiracion: { gt: new Date(), @@ -48,12 +50,10 @@ export async function POST(request: NextRequest) { } // Verificar si el usuario ya es colaborador de este evento - const existingColaborador = await prisma.colaborador_eventos.findUnique({ + const existingColaborador = await prisma.colaborador_eventos.findFirst({ where: { - eventoid_usuarioid: { - eventoid: inviteCode.eventoid, - usuarioid: session.user.id, - }, + eventoid: inviteCode.eventoid, + usuarioid: session.user.id, }, }); @@ -61,31 +61,43 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Ya eres colaborador de este evento' }, { status: 400 }); } - // Crear el registro de colaborador - const newColaborador = await prisma.colaborador_eventos.create({ - data: { - eventoid: inviteCode.eventoid, - usuarioid: session.user.id, - invite_code_used: codigo.trim().toUpperCase(), - }, - }); - - // Incrementar el contador de usos del código de invitación - await prisma.invite_codes.update({ - where: { codigoid: inviteCode.codigoid }, - data: { - usos_totales: { - increment: 1, + const [newColaborador, updatedInviteCode, evento] = await prisma.$transaction([ + prisma.colaborador_eventos.create({ + data: { + eventoid: inviteCode.eventoid, + usuarioid: session.user.id, + invite_code_used: normalizedCode, }, - }, - }); + }), + prisma.invite_codes.update({ + where: { codigoid: inviteCode.codigoid }, + data: { + usos_totales: { + increment: 1, + }, + }, + }), + prisma.eventos.findUnique({ + where: { eventoid: inviteCode.eventoid }, + select: { + eventoid: true, + titulo: true, + }, + }), + ]); - return NextResponse.json({ - success: true, - message: '¡Código validado! Ahora eres colaborador del evento', - eventoid: inviteCode.eventoid, - colaborador_evento_id: newColaborador.colaborador_evento_id, - }); + return NextResponse.json( + { + success: true, + message: '¡Código validado! Ahora eres colaborador del evento', + eventoid: inviteCode.eventoid, + evento: evento ?? null, + colaborador_evento_id: newColaborador.colaborador_evento_id, + usos_totales: updatedInviteCode.usos_totales, + usos_max: updatedInviteCode.usos_max, + }, + { status: 200 }, + ); } catch (error) { console.error('Error using invite code:', error); return NextResponse.json({ error: 'Error al validar 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 index 6239c07..6abf234 100644 --- a/apps/next-frontend/app/api/invite-codes/validate/route.ts +++ b/apps/next-frontend/app/api/invite-codes/validate/route.ts @@ -11,10 +11,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Código de invitación requerido' }, { status: 400 }); } + const normalizedCode = codigo.trim().toUpperCase(); + // Buscar el código en la base de datos const inviteCode = await prisma.invite_codes.findFirst({ where: { - codigo: codigo.trim().toUpperCase(), + codigo: normalizedCode, estado: 'ACTIVO', fecha_expiracion: { gt: new Date(), @@ -47,11 +49,17 @@ export async function POST(request: NextRequest) { }, }); - return NextResponse.json({ - valid: true, - eventoid: inviteCode.eventoid, - titulo: evento?.titulo, - }); + return NextResponse.json( + { + valid: true, + eventoid: inviteCode.eventoid, + titulo: evento?.titulo ?? null, + descripcion: evento?.descripcion ?? null, + usos_totales: inviteCode.usos_totales, + usos_max: inviteCode.usos_max, + }, + { status: 200 }, + ); } catch (error) { console.error('Error validating invite code:', error); return NextResponse.json({ error: 'Error validando código de invitación' }, { status: 500 }); diff --git a/apps/next-frontend/app/api/mercadopago/webhook/route.ts b/apps/next-frontend/app/api/mercadopago/webhook/route.ts index 55595a9..20de7d1 100644 --- a/apps/next-frontend/app/api/mercadopago/webhook/route.ts +++ b/apps/next-frontend/app/api/mercadopago/webhook/route.ts @@ -268,7 +268,6 @@ export async function POST(request: NextRequest) { 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'); @@ -301,7 +300,16 @@ export async function POST(request: NextRequest) { }; const newStatus = statusMap[payment.status] || 'unknown'; - const isPaid = payment.status === 'approved'; + const isApproved = payment.status === 'approved'; + const isPending = payment.status === 'pending'; + const acceptPending = process.env.MP_WEBHOOK_ACCEPT_PENDING === 'true'; + const shouldProcessPayment = isApproved || (acceptPending && isPending); + + if (acceptPending && isPending) { + console.log('[MP Webhook] ⚠️ Pending payment accepted for ticket creation (testing mode).'); + } + + const effectiveStatus = shouldProcessPayment ? 'approved' : newStatus; // Actualizar la orden en la BD si existe const existingOrder = await prisma.mercadopago_orders.findFirst({ @@ -314,10 +322,10 @@ export async function POST(request: NextRequest) { await prisma.mercadopago_orders.update({ where: { id: existingOrder.id }, data: { - status: newStatus, + status: effectiveStatus, payment_id: payment.id.toString(), merchant_order_id: payment.order?.id?.toString() || null, - paid_at: isPaid ? new Date() : null, + paid_at: shouldProcessPayment ? new Date() : null, updated_at: new Date(), }, }); @@ -326,8 +334,9 @@ export async function POST(request: NextRequest) { orderId: existingOrder.id, preferenceId: existingOrder.preference_id, oldStatus: existingOrder.status, - newStatus, - isPaid, + newStatus: effectiveStatus, + shouldProcessPayment, + paymentStatus: payment.status, }); } else { console.warn('[MP Webhook] ⚠️ Order not found in database for external_reference:', { @@ -335,13 +344,15 @@ export async function POST(request: NextRequest) { }); } - // Lógica cuando el pago es aprobado - if (isPaid) { - console.log('[MP Webhook] 🎉 Payment APPROVED - Creating tickets:', { + // Lógica cuando el pago está listo para generar tickets + if (shouldProcessPayment) { + console.log('[MP Webhook] 🎉 Payment ready for ticket generation:', { eventId, sellerId, amount: payment.transaction_amount, buyerEmail, + paymentStatus: payment.status, + acceptedPending: acceptPending && isPending, }); try { @@ -353,8 +364,8 @@ export async function POST(request: NextRequest) { { received: true, orderId: existingOrder?.id, - status: newStatus, - isPaid, + status: effectiveStatus, + isPaid: shouldProcessPayment, error: 'No preference_id', }, { status: 200 }, @@ -385,10 +396,10 @@ export async function POST(request: NextRequest) { : String(createTicketsError), stack: createTicketsError instanceof Error ? createTicketsError.stack : undefined, }); - // No fallar la request - el pago ya se procesó correctamente + // No fallar la request - el pago ya se procesó correctamente o se aceptó por testing } } else { - console.log('[MP Webhook] ⏳ Payment not approved yet:', { + console.log('[MP Webhook] ⏳ Payment not ready for tickets yet:', { paymentStatus: payment.status, externalReference: payment.external_reference, }); @@ -397,8 +408,9 @@ export async function POST(request: NextRequest) { return NextResponse.json( { received: true, - status: newStatus, - isPaid, + status: effectiveStatus, + isPaid: shouldProcessPayment, + acceptedPending: acceptPending && isPending, eventId, }, { status: 200 }, diff --git a/apps/next-frontend/app/colaborador/scanner/page.tsx b/apps/next-frontend/app/colaborador/scanner/page.tsx index 071af48..3a9f616 100644 --- a/apps/next-frontend/app/colaborador/scanner/page.tsx +++ b/apps/next-frontend/app/colaborador/scanner/page.tsx @@ -64,7 +64,7 @@ export default function ScannerPage() { 'Content-Type': 'application/json', }, body: JSON.stringify({ - codigo: inviteCodeInput.trim().toUpperCase(), + codigo: inviteCodeInput.trim(), }), }); @@ -75,14 +75,12 @@ export default function ScannerPage() { return; } - toast.success('¡Código validado! Uniendo al evento...'); + toast.success(data.message || '¡Código validado! Uniendo al evento...'); setInviteCodeInput(''); setShowCodeModal(false); // Recargar eventos después de usar el código - setTimeout(() => { - loadEventos(); - }, 1500); + await loadEventos(); } catch (error) { toast.error(error instanceof Error ? error.message : 'Error al validar código'); } finally { diff --git a/apps/next-frontend/components/qr-scanner-free-event.tsx b/apps/next-frontend/components/qr-scanner-free-event.tsx index 87ed1c5..4ab4184 100644 --- a/apps/next-frontend/components/qr-scanner-free-event.tsx +++ b/apps/next-frontend/components/qr-scanner-free-event.tsx @@ -212,10 +212,12 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { }; const handleScan = async (codigo: string) => { - if (!codigo.trim()) return; + const normalizedCode = codigo.trim().toUpperCase(); + if (!normalizedCode) return; setManualCode(''); setScanning(true); + setShowCameraModal(false); try { const response = await fetch('/api/validar-qr', { @@ -223,7 +225,7 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ eventId: eventoid, - codigo: codigo.trim(), + codigo: normalizedCode, }), }); @@ -243,8 +245,6 @@ export function QRScannerFreeEvent({ eventoid }: ScannerFreeEventProps) { } else { toast.success(`✓ ${data.data.inscripcion.nombre} - Entrada validada`); } - // Cerrar modal de cámara después de escaneo exitoso - setShowCameraModal(false); } // Recargar la lista (sin mostrar errores si falla) try {