import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { hasRole } from "@/lib/permissions"; import { recalculateAllElo } from "@/lib/elo-utils"; /** * POST /api/admin/players/merge * * Merge multiple players into a single player * Requires: club_admin or site_admin role * * Body: * { * playerIds: number[] // IDs of players to merge (first one becomes canonical) * } * * Notes: * - First player ID in the array becomes the canonical player * - All other players will be merged into the canonical player * - After merging, all Elo ratings are recalculated from scratch */ export async function POST(request: Request) { try { // Check permissions const permission = await hasRole('club_admin'); if (!permission.allowed) { return NextResponse.json( { error: permission.reason || "Not authorized to merge players" }, { status: 403 } ); } const body = await request.json(); const { playerIds } = body; // Validate input if (!playerIds || !Array.isArray(playerIds) || playerIds.length < 2) { return NextResponse.json( { error: "At least 2 player IDs are required for merging" }, { status: 400 } ); } // Fetch all players const players = await prisma.player.findMany({ where: { id: { in: playerIds } }, }); if (players.length !== playerIds.length) { return NextResponse.json( { error: "One or more players not found" }, { status: 404 } ); } // First player becomes the canonical player (target) const [canonicalPlayer, ...duplicatePlayers] = players.sort((a, b) => a.id - b.id); console.log(`Merging ${duplicatePlayers.length} players into ${canonicalPlayer.name}`); // Merge stats from duplicates into canonical player let totalGamesPlayed = canonicalPlayer.gamesPlayed; let totalWins = canonicalPlayer.wins; let totalLosses = canonicalPlayer.losses; for (const dupPlayer of duplicatePlayers) { totalGamesPlayed += dupPlayer.gamesPlayed; totalWins += dupPlayer.wins; totalLosses += dupPlayer.losses; } // Update canonical player with merged stats await prisma.player.update({ where: { id: canonicalPlayer.id }, data: { gamesPlayed: totalGamesPlayed, wins: totalWins, losses: totalLosses, }, }); // Update all foreign key references to point to canonical player const updates = []; // Update matches - team1P1Id updates.push( prisma.match.updateMany({ where: { team1P1Id: { in: duplicatePlayers.map(p => p.id) } }, data: { team1P1Id: canonicalPlayer.id }, }) ); // Update matches - team1P2Id updates.push( prisma.match.updateMany({ where: { team1P2Id: { in: duplicatePlayers.map(p => p.id) } }, data: { team1P2Id: canonicalPlayer.id }, }) ); // Update matches - team2P1Id updates.push( prisma.match.updateMany({ where: { team2P1Id: { in: duplicatePlayers.map(p => p.id) } }, data: { team2P1Id: canonicalPlayer.id }, }) ); // Update matches - team2P2Id updates.push( prisma.match.updateMany({ where: { team2P2Id: { in: duplicatePlayers.map(p => p.id) } }, data: { team2P2Id: canonicalPlayer.id }, }) ); // Update event participants - handle unique constraint for (const dupPlayer of duplicatePlayers) { const dupParticipants = await prisma.eventParticipant.findMany({ where: { playerId: dupPlayer.id }, }); for (const dupParticipant of dupParticipants) { // Check if canonical player already participates in this event const existingParticipant = await prisma.eventParticipant.findFirst({ where: { eventId: dupParticipant.eventId, playerId: canonicalPlayer.id, }, }); if (existingParticipant) { // Delete the duplicate participant await prisma.eventParticipant.delete({ where: { id: dupParticipant.id }, }); } else { // Update the participant to use canonical player await prisma.eventParticipant.update({ where: { id: dupParticipant.id }, data: { playerId: canonicalPlayer.id }, }); } } } // Update partnership games updates.push( prisma.partnershipGame.updateMany({ where: { player1Id: { in: duplicatePlayers.map(p => p.id) } }, data: { player1Id: canonicalPlayer.id }, }) ); updates.push( prisma.partnershipGame.updateMany({ where: { player2Id: { in: duplicatePlayers.map(p => p.id) } }, data: { player2Id: canonicalPlayer.id }, }) ); // Update partnership stats - merge them for (const dupPlayer of duplicatePlayers) { const partnerships = await prisma.partnershipStat.findMany({ where: { OR: [ { player1Id: dupPlayer.id }, { player2Id: dupPlayer.id }, ], }, }); for (const partnership of partnerships) { const otherPlayerId = partnership.player1Id === dupPlayer.id ? partnership.player2Id : partnership.player1Id; // Check if a partnership already exists between canonical and other player const existingPartnership = await prisma.partnershipStat.findFirst({ where: { OR: [ { player1Id: canonicalPlayer.id, player2Id: otherPlayerId }, { player1Id: otherPlayerId, player2Id: canonicalPlayer.id }, ], }, }); if (existingPartnership) { // Merge stats into existing partnership and delete the old one await prisma.partnershipStat.update({ where: { id: existingPartnership.id }, data: { gamesPlayed: { increment: partnership.gamesPlayed }, wins: { increment: partnership.wins }, losses: { increment: partnership.losses }, totalEloChange: { increment: partnership.totalEloChange }, lastPlayed: partnership.lastPlayed && existingPartnership.lastPlayed ? (partnership.lastPlayed > existingPartnership.lastPlayed ? partnership.lastPlayed : existingPartnership.lastPlayed) : partnership.lastPlayed || existingPartnership.lastPlayed, }, }); // Delete the duplicate partnership await prisma.partnershipStat.delete({ where: { id: partnership.id }, }); } else { // Update partnership to use canonical player const isPlayer1 = partnership.player1Id === dupPlayer.id; await prisma.partnershipStat.update({ where: { id: partnership.id }, data: isPlayer1 ? { player1Id: canonicalPlayer.id } : { player2Id: canonicalPlayer.id }, }); } } } // Execute all updates await Promise.all(updates); console.log(`Updated foreign key references`); // Delete elo snapshots for duplicate players (they will be recalculated anyway) for (const dupPlayer of duplicatePlayers) { await prisma.eloSnapshot.deleteMany({ where: { playerId: dupPlayer.id }, }); } console.log(`Deleted elo snapshots for ${duplicatePlayers.length} duplicate players`); // Delete duplicate players for (const dupPlayer of duplicatePlayers) { await prisma.player.delete({ where: { id: dupPlayer.id }, }); } console.log(`Deleted ${duplicatePlayers.length} duplicate players`); // RECALCULATE ALL ELO FROM SCRATCH // This is critical - after merging players, all match data has changed // so we need to recalculate everyone's ELO from the beginning console.log('Recalculating all Elo ratings from scratch...'); const recalcResult = await recalculateAllElo(prisma); console.log(`Elo recalculation complete:`, recalcResult); return NextResponse.json({ success: true, message: `Successfully merged ${duplicatePlayers.length} players into "${canonicalPlayer.name}"`, canonicalPlayer: { id: canonicalPlayer.id, name: canonicalPlayer.name, }, recalculation: recalcResult, }); } catch (error: unknown) { console.error("Error merging players:", error); const message = error instanceof Error ? error.message : "Failed to merge players"; return NextResponse.json( { error: message }, { status: 500 } ); } }