← back to Handbag Auth Nextjs

src/app/api/price-history/route.ts

190 lines

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { rateLimit, getClientIp } from '@/lib/rateLimit'
import { addSecurityHeaders } from '@/lib/securityHeaders'
import { validateInput, priceHistoryGetSchema, priceHistoryCreateSchema } from '@/lib/validation'
import { Prisma } from '@prisma/client'

export async function GET(request: NextRequest) {
  try {
    // Rate limiting
    const clientIp = getClientIp(request)
    const rateLimitResult = rateLimit(clientIp, {
      windowMs: 60 * 1000,
      maxRequests: 60, // 60 requests per minute
    })

    if (!rateLimitResult.allowed) {
      const response = NextResponse.json(
        { success: false, error: 'Rate limit exceeded' },
        { status: 429 }
      )
      response.headers.set('Retry-After', String(Math.ceil((rateLimitResult.resetTime - Date.now()) / 1000)))
      addSecurityHeaders(response)
      return response
    }

    // Input validation
    const searchParams = request.nextUrl.searchParams
    const validation = validateInput(priceHistoryGetSchema, {
      externalId: searchParams.get('externalId'),
      listingId: searchParams.get('listingId'),
      limit: searchParams.get('limit'),
    })

    if (!validation.success) {
      const response = NextResponse.json(
        { success: false, error: 'Invalid input parameters' },
        { status: 400 }
      )
      addSecurityHeaders(response)
      return response
    }

    const { externalId, listingId, limit } = validation.data

    if (!externalId && !listingId) {
      const response = NextResponse.json(
        { success: false, error: 'externalId or listingId required' },
        { status: 400 }
      )
      addSecurityHeaders(response)
      return response
    }

    // Type-safe where clause
    const where: Prisma.PriceHistoryWhereInput = {}
    if (externalId) where.externalId = externalId
    if (listingId) where.listingId = listingId

    const history = await prisma.priceHistory.findMany({
      where,
      orderBy: { recordedAt: 'desc' },
      take: limit,
    })

    // Calculate price changes
    const priceChanges = history.map((record, index) => {
      if (index === history.length - 1) {
        return {
          ...record,
          priceChange: null,
          priceChangePercent: null,
        }
      }

      const prevPrice = history[index + 1].priceUsd
      const currentPrice = record.priceUsd

      if (!prevPrice || !currentPrice) {
        return {
          ...record,
          priceChange: null,
          priceChangePercent: null,
        }
      }

      const change = currentPrice - prevPrice
      const changePercent = ((change / prevPrice) * 100).toFixed(2)

      return {
        ...record,
        priceChange: parseFloat(change.toFixed(2)),
        priceChangePercent: parseFloat(changePercent),
      }
    })

    const response = NextResponse.json({
      success: true,
      count: history.length,
      history: priceChanges.reverse(), // Oldest first
    })
    addSecurityHeaders(response)
    return response
  } catch (error) {
    console.error('Price history API error:', error)
    const response = NextResponse.json(
      { success: false, error: 'Failed to fetch price history' },
      { status: 500 }
    )
    addSecurityHeaders(response)
    return response
  }
}

export async function POST(request: NextRequest) {
  try {
    // Rate limiting - stricter for write operations
    const clientIp = getClientIp(request)
    const rateLimitResult = rateLimit(clientIp, {
      windowMs: 60 * 1000,
      maxRequests: 20, // Lower limit for write operations
    })

    if (!rateLimitResult.allowed) {
      const response = NextResponse.json(
        { success: false, error: 'Rate limit exceeded' },
        { status: 429 }
      )
      response.headers.set('Retry-After', String(Math.ceil((rateLimitResult.resetTime - Date.now()) / 1000)))
      addSecurityHeaders(response)
      return response
    }

    // Input validation
    const body = await request.json()
    const validation = validateInput(priceHistoryCreateSchema, body)

    if (!validation.success) {
      const response = NextResponse.json(
        { success: false, error: 'Invalid input data' },
        { status: 400 }
      )
      addSecurityHeaders(response)
      return response
    }

    const {
      listingId,
      externalId,
      source,
      priceJpy,
      priceUsd,
      condition,
      listingType,
      isAvailable,
    } = validation.data

    const now = new Date().toISOString().replace('T', ' ').split('.')[0]

    const priceRecord = await prisma.priceHistory.create({
      data: {
        listingId,
        externalId,
        source,
        priceJpy,
        priceUsd,
        condition,
        listingType,
        recordedAt: now,
        isAvailable,
      },
    })

    const response = NextResponse.json({
      success: true,
      priceRecord,
    })
    addSecurityHeaders(response)
    return response
  } catch (error) {
    console.error('Price history POST error:', error)
    const response = NextResponse.json(
      { success: false, error: 'Failed to record price' },
      { status: 500 }
    )
    addSecurityHeaders(response)
    return response
  }
}