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, matchSlug: match.slug, 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, slug: match.slug, 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/:slug/messages - Get messages for a match router.get('/:slug/messages', authenticate, async (req, res, next) => { try { const { slug } = req.params; const userId = req.user.id; // Find match const match = await prisma.match.findUnique({ where: { slug }, select: { id: true, user1Id: true, user2Id: true, roomId: true, status: 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 messages for this match', }); } // Check if match is accepted if (match.status !== 'accepted' || !match.roomId) { return res.status(400).json({ success: false, error: 'Match must be accepted before viewing messages', }); } // Get messages const messages = await prisma.message.findMany({ where: { roomId: match.roomId }, include: { user: { select: { id: true, username: true, avatar: true, firstName: true, lastName: true, }, }, }, orderBy: { createdAt: 'asc' }, take: 100, // Last 100 messages }); res.json({ success: true, count: messages.length, data: messages.map(msg => ({ id: msg.id, roomId: msg.roomId, userId: msg.user.id, username: msg.user.username, avatar: msg.user.avatar, firstName: msg.user.firstName, lastName: msg.user.lastName, content: msg.content, type: msg.type, createdAt: msg.createdAt, })), }); } catch (error) { next(error); } }); // GET /api/matches/:slug - Get specific match router.get('/:slug', authenticate, async (req, res, next) => { try { const { slug } = req.params; const userId = req.user.id; const match = await prisma.match.findUnique({ where: { slug }, 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; // Check if current user has already rated this match const userRating = await prisma.rating.findUnique({ where: { matchId_raterId_ratedId: { matchId: match.id, raterId: userId, ratedId: partner.id, }, }, }); res.json({ success: true, data: { id: match.id, slug: match.slug, 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, hasRated: !!userRating, createdAt: match.createdAt, }, }); } catch (error) { next(error); } }); // PUT /api/matches/:slug/accept - Accept a pending match router.put('/:slug/accept', authenticate, async (req, res, next) => { try { const { slug } = req.params; const userId = req.user.id; // Find match const match = await prisma.match.findUnique({ where: { slug }, 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: { slug }, 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, matchSlug: updatedMatch.slug, 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, slug: updatedMatch.slug, 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/:slug - Reject or cancel a match router.delete('/:slug', authenticate, async (req, res, next) => { try { const { slug } = req.params; const userId = req.user.id; // Find match const match = await prisma.match.findUnique({ where: { slug }, 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: { slug }, }); // 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, matchSlug: match.slug, 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); } }); // POST /api/matches/:slug/ratings - Rate a partner after match router.post('/:slug/ratings', authenticate, async (req, res, next) => { try { const { slug } = req.params; const userId = req.user.id; const { score, comment, wouldCollaborateAgain } = req.body; // Validation if (!score || score < 1 || score > 5) { return res.status(400).json({ success: false, error: 'Score must be between 1 and 5', }); } // Find match const match = await prisma.match.findUnique({ where: { slug }, select: { id: true, user1Id: true, user2Id: true, status: true, }, }); if (!match) { return res.status(404).json({ success: false, error: 'Match not found', }); } // Check authorization - user must be part of this match if (match.user1Id !== userId && match.user2Id !== userId) { return res.status(403).json({ success: false, error: 'You are not authorized to rate this match', }); } // Check if match is accepted if (match.status !== 'accepted' && match.status !== 'completed') { return res.status(400).json({ success: false, error: 'Match must be accepted before rating', }); } // Determine who is being rated (the other user in the match) const ratedUserId = match.user1Id === userId ? match.user2Id : match.user1Id; // Check if user already rated this match const existingRating = await prisma.rating.findUnique({ where: { matchId_raterId_ratedId: { matchId: match.id, raterId: userId, ratedId: ratedUserId, }, }, }); if (existingRating) { return res.status(400).json({ success: false, error: 'You have already rated this match', }); } // Create rating const rating = await prisma.rating.create({ data: { matchId: match.id, raterId: userId, ratedId: ratedUserId, score, comment: comment || null, wouldCollaborateAgain: wouldCollaborateAgain || false, }, include: { rater: { select: { id: true, username: true, firstName: true, lastName: true, }, }, rated: { select: { id: true, username: true, firstName: true, lastName: true, }, }, }, }); // Check if both users have rated - if so, mark match as completed const otherUserRating = await prisma.rating.findUnique({ where: { matchId_raterId_ratedId: { matchId: match.id, raterId: ratedUserId, ratedId: userId, }, }, }); if (otherUserRating) { // Both users have rated - mark match as completed await prisma.match.update({ where: { id: match.id }, data: { status: 'completed' }, }); } res.status(201).json({ success: true, data: rating, }); } catch (error) { // Handle unique constraint violation if (error.code === 'P2002') { return res.status(400).json({ success: false, error: 'You have already rated this match', }); } next(error); } }); module.exports = router;