Files
cbd420/app/api/drops/active/route.ts
2025-12-21 08:43:43 +01:00

146 lines
6.1 KiB
TypeScript

import { NextResponse } from 'next/server'
import pool from '@/lib/db'
// GET /api/drops/active - Get the earliest unfilled drop (not sold out) that has started
export async function GET() {
try {
const now = new Date()
// Clean up expired pending orders first to ensure accurate calculations
await pool.execute(
'DELETE FROM pending_orders WHERE expires_at < NOW()',
)
// Get all drops ordered by start_time (or created_at if start_time is null)
const [rows] = await pool.execute(
'SELECT * FROM drops ORDER BY COALESCE(start_time, created_at) ASC'
)
const drops = rows as any[]
// Find the first drop that's not fully sold out and has started
console.log(`Checking ${drops.length} drops for active drop`)
for (const drop of drops) {
// Check if drop has started (start_time is in the past or null)
const startTime = drop.start_time ? new Date(drop.start_time) : new Date(drop.created_at)
console.log(`Checking drop ${drop.id} (${drop.item}): startTime=${startTime.toISOString()}, now=${now.toISOString()}, started=${startTime <= now}`)
if (startTime > now) {
// Drop hasn't started yet - return it with a flag indicating it's upcoming
// Calculate fill (will be 0 for upcoming drops, but include pending for consistency)
const [salesRows] = await pool.execute(
'SELECT COALESCE(SUM(size), 0) as total_fill FROM sales WHERE drop_id = ?',
[drop.id]
)
const salesData = salesRows as any[]
const totalFillInGrams = salesData[0]?.total_fill || 0
// Include non-expired pending orders in fill calculation
const [pendingRows] = await pool.execute(
'SELECT COALESCE(SUM(size), 0) as total_pending FROM pending_orders WHERE drop_id = ? AND expires_at > NOW()',
[drop.id]
)
const pendingData = pendingRows as any[]
const pendingFillInGrams = pendingData[0]?.total_pending || 0
const totalReservedInGrams = totalFillInGrams + pendingFillInGrams
let salesFill = totalFillInGrams
let pendingFill = pendingFillInGrams
if (drop.unit === 'kg') {
salesFill = totalFillInGrams / 1000
pendingFill = pendingFillInGrams / 1000
}
const totalFill = salesFill + pendingFill
console.log(`Returning upcoming drop ${drop.id} (${drop.item}): fill=${totalFill}, size=${drop.size}, starts at ${startTime.toISOString()}`)
return NextResponse.json({
...drop,
fill: totalFill,
sales_fill: salesFill,
pending_fill: pendingFill,
is_upcoming: true,
start_time: drop.start_time || drop.created_at,
})
}
// Calculate fill from sales records and pending orders
// Sales are stored in grams, so we need to convert based on drop unit
const [salesRows] = await pool.execute(
'SELECT COALESCE(SUM(size), 0) as total_fill FROM sales WHERE drop_id = ?',
[drop.id]
)
const salesData = salesRows as any[]
const totalFillInGrams = salesData[0]?.total_fill || 0
// Include non-expired pending orders in fill calculation (shows "on hold" inventory)
const [pendingRows] = await pool.execute(
'SELECT COALESCE(SUM(size), 0) as total_pending FROM pending_orders WHERE drop_id = ? AND expires_at > NOW()',
[drop.id]
)
const pendingData = pendingRows as any[]
// Ensure we get a number, handle null/undefined cases
// When table is empty, SUM returns NULL, COALESCE converts it to 0
let pendingFillInGrams = 0
if (pendingData && pendingData.length > 0 && pendingData[0]) {
const rawValue = pendingData[0].total_pending
// Handle both null (from empty result) and actual 0 values
if (rawValue !== null && rawValue !== undefined) {
pendingFillInGrams = Number(rawValue) || 0
}
}
// Explicitly ensure it's 0 if we got here with no valid data
pendingFillInGrams = Number(pendingFillInGrams) || 0
// Ensure totalFillInGrams and pendingFillInGrams are numbers, not strings
const totalFillNum = Number(totalFillInGrams) || 0
const pendingFillNum = Number(pendingFillInGrams) || 0
const totalReservedInGrams = totalFillNum + pendingFillNum
// Convert to drop's unit for display
let salesFill = totalFillNum
let pendingFill = pendingFillNum
if (drop.unit === 'kg') {
salesFill = totalFillNum / 1000
pendingFill = pendingFillNum / 1000
}
const totalFill = salesFill + pendingFill
// Ensure drop.size is a number for comparison
const dropSize = typeof drop.size === 'string' ? parseFloat(drop.size) : Number(drop.size)
const fillNum = Number(totalFill)
// Check if drop is not fully sold out
// Use a small epsilon for floating point comparison to handle precision issues
// Consider sold out if fill is within epsilon of size (to handle rounding)
const epsilon = drop.unit === 'kg' ? 0.00001 : 0.01
const remaining = dropSize - fillNum
if (remaining > epsilon) {
// Ensure pending_fill is explicitly 0 if no pending orders
const finalPendingFill = Number(pendingFill) || 0
console.log(`Returning active drop ${drop.id} with fill ${fillNum} < size ${dropSize}, pending_fill=${finalPendingFill} (raw: ${pendingFill})`)
return NextResponse.json({
...drop,
fill: fillNum,
sales_fill: Number(salesFill) || 0, // Only confirmed sales
pending_fill: finalPendingFill, // Items on hold (explicitly 0 if no pending orders)
is_upcoming: false,
start_time: drop.start_time || drop.created_at,
})
} else {
console.log(`Drop ${drop.id} is sold out: fill=${fillNum} >= size=${dropSize} (remaining=${remaining})`)
}
}
// No active drops found
return NextResponse.json(null)
} catch (error) {
console.error('Error fetching active drop:', error)
return NextResponse.json(
{ error: 'Failed to fetch active drop' },
{ status: 500 }
)
}
}