All files / web/src/app/api/worksheets/upload route.ts

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

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 105 106 107 108 109 110 111                                                                                                                                                                                                                             
import { randomUUID } from 'crypto'
import { writeFile } from 'fs/promises'
import { type NextRequest, NextResponse } from 'next/server'
import { join } from 'path'
import { db } from '@/db'
import { worksheetAttempts } from '@/db/schema'
import { processWorksheetAttempt } from '@/lib/grading/processAttempt'
import { getUserId } from '@/lib/viewer'
import { withAuth } from '@/lib/auth/withAuth'

/**
 * Trigger processing in background without blocking response
 * In production, this would be a proper job queue
 */
function processAttemptInBackground(attemptId: string, userId?: string) {
  // Fire and forget - don't await
  processWorksheetAttempt(attemptId, userId).catch((error) => {
    console.error(`Background processing failed for ${attemptId}:`, error)
  })
}

/**
 * Upload API endpoint for worksheet images
 *
 * Teachers upload photos of completed paper worksheets.
 * This endpoint stores the image and creates a database record
 * for background AI grading.
 *
 * Optional: sessionId can be provided to group multiple uploads together
 * (used for QR code batch upload workflow)
 */
export const POST = withAuth(async (request) => {
  try {
    // Get viewer ID from session (works for both authenticated users and guests)
    const userId = await getUserId()

    const formData = await request.formData()
    const file = formData.get('image') as File | null

    if (!file) {
      return NextResponse.json({ error: 'No file provided' }, { status: 400 })
    }

    // Validate file type
    if (!file.type.startsWith('image/')) {
      return NextResponse.json({ error: 'File must be an image' }, { status: 400 })
    }

    // Generate unique ID for this attempt
    const attemptId = randomUUID()
    const extension = file.name.split('.').pop() || 'jpg'
    const filename = `${attemptId}.${extension}`

    // Save to local storage (MVP) - will move to Cloudflare R2 later
    const uploadDir = join(process.cwd(), 'data', 'uploads')
    const filepath = join(uploadDir, filename)

    const bytes = await file.arrayBuffer()
    await writeFile(filepath, Buffer.from(bytes))

    // Get optional metadata from form data
    const worksheetId = formData.get('worksheetId') as string | null
    const sessionId = formData.get('sessionId') as string | null // For batch QR uploads
    const operatorInput = formData.get('operator') as string | null
    const digitCountInput = formData.get('digitCount') as string | null
    const problemCountInput = formData.get('problemCount') as string | null

    // Parse metadata with defaults
    const operator = operatorInput || 'addition'
    const digitCount = digitCountInput ? parseInt(digitCountInput, 10) : 2
    const problemCount = problemCountInput ? parseInt(problemCountInput, 10) : 20

    // Create database record
    const now = new Date()
    await db.insert(worksheetAttempts).values({
      id: attemptId,
      userId,
      uploadedImageUrl: `/uploads/${filename}`,
      worksheetId,
      sessionId, // Optional: groups uploads from same QR code session
      operator: operator as 'addition' | 'subtraction' | 'mixed',
      digitCount,
      problemCount,
      gradingStatus: 'pending',
      createdAt: now,
      updatedAt: now,
    })

    // For MVP: Process immediately (not truly async)
    // In production, this should be queued to a background job
    // For now, we'll trigger processing but not wait for it
    processAttemptInBackground(attemptId, userId)

    return NextResponse.json({
      attemptId,
      sessionId, // Echo back for client tracking
      status: 'pending',
      message: 'Upload successful. Grading will begin shortly.',
    })
  } catch (error) {
    console.error('Upload error:', error)
    return NextResponse.json(
      {
        error: 'Upload failed',
        details: error instanceof Error ? error.message : 'Unknown error',
      },
      { status: 500 }
    )
  }
})