const express = require('express'); const { prisma } = require('../utils/db'); const { authenticate } = require('../middleware/auth'); const { getIO } = require('../socket'); const router = express.Router(); // POST /api/matches - Create a match request router.post('/', authenticate, async (req, res, next) => { try { const { targetUserId, eventSlug } = req.body; const requesterId = req.user.id; // Validation if (!targetUserId || !eventSlug) { return res.status(400).json({ success: false, error: 'targetUserId and eventSlug are required', }); } if (requesterId === targetUserId) { return res.status(400).json({ success: false, error: 'Cannot create match with yourself', }); } // Find event by slug const event = await prisma.event.findUnique({ where: { slug: eventSlug }, select: { id: true, name: true }, }); if (!event) { return res.status(404).json({ success: false, error: 'Event not found', }); } // Check if both users are participants const [requesterParticipant, targetParticipant] = await Promise.all([ prisma.eventParticipant.findUnique({ where: { userId_eventId: { userId: requesterId, eventId: event.id, }, }, }), prisma.eventParticipant.findUnique({ where: { userId_eventId: { userId: targetUserId, eventId: event.id, }, }, }), ]); if (!requesterParticipant) { return res.status(403).json({ success: false, error: 'You must be a participant of this event', }); } if (!targetParticipant) { return res.status(400).json({ success: false, error: 'Target user is not a participant of this event', }); } // Check if match already exists (in either direction) const existingMatch = await prisma.match.findFirst({ where: { eventId: event.id, OR: [ { user1Id: requesterId, user2Id: targetUserId }, { user1Id: targetUserId, user2Id: requesterId }, ], }, }); if (existingMatch) { return res.status(400).json({ success: false, error: 'Match already exists with this user', match: existingMatch, }); } // Create match const match = await prisma.match.create({ data: { user1Id: requesterId, user2Id: targetUserId, eventId: event.id, status: 'pending', }, include: { user1: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, user2: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, event: { select: { id: true, slug: true, name: true, }, }, }, }); // Emit socket event to target user try { const io = getIO(); const targetSocketRoom = `user_${targetUserId}`; io.to(targetSocketRoom).emit('match_request_received', { matchId: match.id, from: { id: match.user1.id, username: match.user1.username, avatar: match.user1.avatar, firstName: match.user1.firstName, lastName: match.user1.lastName, }, event: { slug: match.event.slug, name: match.event.name, }, createdAt: match.createdAt, }); } catch (socketError) { console.error('Failed to emit match request notification:', socketError); } res.status(201).json({ success: true, data: match, }); } catch (error) { // Handle unique constraint violation if (error.code === 'P2002') { return res.status(400).json({ success: false, error: 'Match already exists with this user for this event', }); } next(error); } }); // GET /api/matches - List matches for current user (optionally filtered by event) router.get('/', authenticate, async (req, res, next) => { try { const userId = req.user.id; const { eventSlug, status } = req.query; // Build where clause const where = { OR: [ { user1Id: userId }, { user2Id: userId }, ], }; // Filter by event if provided if (eventSlug) { const event = await prisma.event.findUnique({ where: { slug: eventSlug }, select: { id: true }, }); if (!event) { return res.status(404).json({ success: false, error: 'Event not found', }); } where.eventId = event.id; } // Filter by status if provided if (status) { where.status = status; } // Fetch matches const matches = await prisma.match.findMany({ where, include: { user1: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, user2: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, event: { select: { id: true, slug: true, name: true, location: true, startDate: true, endDate: true, }, }, room: { select: { id: true, }, }, }, orderBy: { createdAt: 'desc', }, }); // Transform matches to include partner info const transformedMatches = matches.map(match => { const isUser1 = match.user1Id === userId; const partner = isUser1 ? match.user2 : match.user1; const isInitiator = match.user1Id === userId; return { id: match.id, partner: { id: partner.id, username: partner.username, avatar: partner.avatar, firstName: partner.firstName, lastName: partner.lastName, }, event: match.event, status: match.status, roomId: match.roomId, isInitiator, createdAt: match.createdAt, }; }); res.json({ success: true, count: transformedMatches.length, data: transformedMatches, }); } catch (error) { next(error); } }); // GET /api/matches/:id - Get specific match router.get('/:id', authenticate, async (req, res, next) => { try { const { id } = req.params; const userId = req.user.id; const match = await prisma.match.findUnique({ where: { id: parseInt(id) }, include: { user1: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, user2: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, event: { select: { id: true, slug: true, name: true, location: true, startDate: true, endDate: true, }, }, room: { select: { id: true, }, }, }, }); if (!match) { return res.status(404).json({ success: false, error: 'Match not found', }); } // Check authorization if (match.user1Id !== userId && match.user2Id !== userId) { return res.status(403).json({ success: false, error: 'You are not authorized to view this match', }); } // Transform match data const isUser1 = match.user1Id === userId; const partner = isUser1 ? match.user2 : match.user1; const isInitiator = match.user1Id === userId; res.json({ success: true, data: { id: match.id, partner: { id: partner.id, username: partner.username, avatar: partner.avatar, firstName: partner.firstName, lastName: partner.lastName, }, event: match.event, status: match.status, roomId: match.roomId, isInitiator, createdAt: match.createdAt, }, }); } catch (error) { next(error); } }); // PUT /api/matches/:id/accept - Accept a pending match router.put('/:id/accept', authenticate, async (req, res, next) => { try { const { id } = req.params; const userId = req.user.id; // Find match const match = await prisma.match.findUnique({ where: { id: parseInt(id) }, include: { user1: { select: { id: true, username: true, avatar: true, }, }, user2: { select: { id: true, username: true, avatar: true, }, }, event: { select: { id: true, slug: true, name: true, }, }, }, }); if (!match) { return res.status(404).json({ success: false, error: 'Match not found', }); } // Check authorization - only user2 can accept if (match.user2Id !== userId) { return res.status(403).json({ success: false, error: 'Only the match recipient can accept', }); } // Check status if (match.status !== 'pending') { return res.status(400).json({ success: false, error: `Match is already ${match.status}`, }); } // Create private chat room and update match in transaction const updatedMatch = await prisma.$transaction(async (tx) => { // Create private chat room const chatRoom = await tx.chatRoom.create({ data: { type: 'private', eventId: match.eventId, }, }); // Update match status and link to chat room const updated = await tx.match.update({ where: { id: parseInt(id) }, data: { status: 'accepted', roomId: chatRoom.id, }, include: { user1: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, user2: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, event: { select: { id: true, slug: true, name: true, }, }, room: { select: { id: true, }, }, }, }); return updated; }); // Emit socket event to both users try { const io = getIO(); const user1SocketRoom = `user_${match.user1Id}`; const user2SocketRoom = `user_${match.user2Id}`; const notification = { matchId: updatedMatch.id, roomId: updatedMatch.roomId, event: { slug: updatedMatch.event.slug, name: updatedMatch.event.name, }, }; io.to(user1SocketRoom).emit('match_accepted', { ...notification, partner: { id: updatedMatch.user2.id, username: updatedMatch.user2.username, avatar: updatedMatch.user2.avatar, }, }); io.to(user2SocketRoom).emit('match_accepted', { ...notification, partner: { id: updatedMatch.user1.id, username: updatedMatch.user1.username, avatar: updatedMatch.user1.avatar, }, }); } catch (socketError) { console.error('Failed to emit match accepted notification:', socketError); } // Transform response const isUser1 = updatedMatch.user1Id === userId; const partner = isUser1 ? updatedMatch.user2 : updatedMatch.user1; res.json({ success: true, data: { id: updatedMatch.id, partner: { id: partner.id, username: partner.username, avatar: partner.avatar, firstName: partner.firstName, lastName: partner.lastName, }, event: updatedMatch.event, status: updatedMatch.status, roomId: updatedMatch.roomId, isInitiator: isUser1, createdAt: updatedMatch.createdAt, }, }); } catch (error) { next(error); } }); // DELETE /api/matches/:id - Reject or cancel a match router.delete('/:id', authenticate, async (req, res, next) => { try { const { id } = req.params; const userId = req.user.id; // Find match const match = await prisma.match.findUnique({ where: { id: parseInt(id) }, include: { event: { select: { slug: true, name: true, }, }, }, }); if (!match) { return res.status(404).json({ success: false, error: 'Match not found', }); } // Check authorization - both users can delete if (match.user1Id !== userId && match.user2Id !== userId) { return res.status(403).json({ success: false, error: 'You are not authorized to delete this match', }); } // Cannot delete completed matches if (match.status === 'completed') { return res.status(400).json({ success: false, error: 'Cannot delete completed matches', }); } // Delete match (will cascade delete chat room if exists) await prisma.match.delete({ where: { id: parseInt(id) }, }); // Emit socket event to the other user try { const io = getIO(); const otherUserId = match.user1Id === userId ? match.user2Id : match.user1Id; const otherUserSocketRoom = `user_${otherUserId}`; io.to(otherUserSocketRoom).emit('match_cancelled', { matchId: match.id, event: { slug: match.event.slug, name: match.event.name, }, }); } catch (socketError) { console.error('Failed to emit match cancelled notification:', socketError); } res.json({ success: true, message: 'Match deleted successfully', }); } catch (error) { next(error); } }); module.exports = router;