All files / web/src/app/api/game-results route.ts

0% Statements 0/103
0% Branches 0/1
0% Functions 0/1
0% Lines 0/103

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104                                                                                                                                                                                                               
/**
 * API route for saving game results
 *
 * POST /api/game-results - Save a completed game result
 */

import { NextResponse } from 'next/server'
import { db } from '@/db'
import { gameResults } from '@/db/schema'
import { withAuth } from '@/lib/auth/withAuth'
import { canPerformAction } from '@/lib/classroom'
import { getUserId } from '@/lib/viewer'
import type { GameResultsReport } from '@/lib/arcade/game-sdk/types'
import { metrics } from '@/lib/metrics'
import { getCurrentTraceId, recordError } from '@/lib/tracing'

interface SaveGameResultRequest {
  playerId: string
  userId?: string
  sessionType: 'practice-break' | 'arcade-room' | 'standalone'
  sessionId?: string
  report: GameResultsReport
}

/**
 * POST - Save a completed game result
 *
 * This endpoint is called when a game finishes to persist the result
 * for scoreboard and history features.
 */
export const POST = withAuth(async (request) => {
  try {
    const body: SaveGameResultRequest = await request.json()
    const { playerId, userId, sessionType, sessionId, report } = body

    if (!playerId) {
      return NextResponse.json({ error: 'Player ID required' }, { status: 400 })
    }

    if (!report) {
      return NextResponse.json({ error: 'Game report required' }, { status: 400 })
    }

    // Authorization check - only the player's parent or teacher can save results
    const dbUserId = await getUserId()
    const canSave = await canPerformAction(dbUserId, playerId, 'view')
    if (!canSave) {
      console.warn('[game-results] Auth rejected:', {
        dbUserId,
        playerId,
        gameName: report.gameName,
        sessionType,
        sessionId,
      })
      return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
    }

    // Extract player result (first player for single-player, find by playerId for multiplayer)
    const playerResult =
      report.playerResults.find((p) => p.playerId === playerId) ?? report.playerResults[0]

    const result = await db
      .insert(gameResults)
      .values({
        playerId,
        userId,
        gameName: report.gameName,
        gameDisplayName: report.gameDisplayName,
        gameIcon: report.gameIcon,
        sessionType,
        sessionId,
        normalizedScore: report.leaderboardEntry?.normalizedScore ?? playerResult?.accuracy ?? 0,
        rawScore: playerResult?.score,
        accuracy: playerResult?.accuracy,
        category: report.leaderboardEntry?.category,
        difficulty: report.leaderboardEntry?.difficulty,
        durationMs: report.durationMs,
        playedAt: new Date(report.endedAt),
        fullReport: report,
      })
      .returning()

    // Track arcade metrics
    const gameName = report.gameName || 'unknown'
    const outcome = playerResult?.accuracy && playerResult.accuracy >= 0.8 ? 'win' : 'lose'
    metrics.arcade.gamesCompleted.inc({ game: gameName, outcome })
    if (playerResult?.score !== undefined) {
      metrics.arcade.scoreHistogram.observe({ game: gameName }, playerResult.score)
    }

    return NextResponse.json(result[0])
  } catch (error) {
    console.error('Error saving game result:', error)
    if (error instanceof Error) {
      recordError(error)
    }
    const traceId = getCurrentTraceId()
    return NextResponse.json(
      { error: 'Failed to save game result', ...(traceId && { traceId }) },
      { status: 500 }
    )
  }
})