import { NextRequest, NextResponse } from 'next/server' import { cookies } from 'next/headers' import pool from '@/lib/db' import { getNowPaymentsConfig } from '@/lib/nowpayments' // POST /api/payments/create-invoice - Create a NOWPayments payment // Note: Endpoint name kept as "create-invoice" for backward compatibility // but it now uses the /v1/payment endpoint instead of /v1/invoice export async function POST(request: NextRequest) { try { // Get buyer_id from session cookie const cookieStore = await cookies() const buyerIdCookie = cookieStore.get('buyer_id')?.value if (!buyerIdCookie) { return NextResponse.json( { error: 'Authentication required' }, { status: 401 } ) } const buyer_id = parseInt(buyerIdCookie, 10) const body = await request.json() const { drop_id, size, pay_currency, buyer_data_id } = body // Validate required fields if (!drop_id || !size || !buyer_data_id) { return NextResponse.json( { error: 'Missing required fields: drop_id, size, and buyer_data_id' }, { status: 400 } ) } // Verify buyer_data_id exists and belongs to the buyer const [buyerDataRows] = await pool.execute( 'SELECT id FROM buyer_data WHERE id = ? AND buyer_id = ?', [buyer_data_id, buyer_id] ) const buyerData = buyerDataRows as any[] if (buyerData.length === 0) { return NextResponse.json( { error: 'Invalid buyer_data_id or buyer_data does not belong to user' }, { status: 400 } ) } // Get drop details const [dropRows] = await pool.execute( 'SELECT * FROM drops WHERE id = ?', [drop_id] ) const drops = dropRows as any[] if (drops.length === 0) { return NextResponse.json( { error: 'Drop not found' }, { status: 404 } ) } const drop = drops[0] // Get IPN callback URL from environment variable (IPN is handled by external service) // This URL is still required for NOWPayments to know where to send payment notifications const ipnCallbackUrl = process.env.IPN_CALLBACK_URL if (!ipnCallbackUrl) { return NextResponse.json( { error: 'IPN_CALLBACK_URL environment variable is required. This should point to your external IPN handler service.' }, { status: 500 } ) } // Use transaction to atomically check and reserve inventory const connection = await pool.getConnection() await connection.beginTransaction() try { // Check inventory availability including non-expired pending orders // First, clean up expired pending orders await connection.execute( 'DELETE FROM pending_orders WHERE expires_at < NOW()', ) // Calculate current fill from sales const [salesRows] = await connection.execute( 'SELECT COALESCE(SUM(size), 0) as total_fill FROM sales WHERE drop_id = ?', [drop_id] ) const salesData = salesRows as any[] const currentFill = salesData[0]?.total_fill || 0 // Calculate pending orders (non-expired) that are holding inventory const [pendingRows] = await connection.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 pendingFill = pendingData[0]?.total_pending || 0 console.log(`total fill : ${currentFill} + ${pendingFill} = ${Number(currentFill) + Number(pendingFill)}`) // Total reserved = sales + pending orders, ensure both are numbers const totalReserved = Number(currentFill) + Number(pendingFill) // Convert fill to the drop's unit for comparison let totalReservedInDropUnit = totalReserved let sizeInDropUnit = size if (drop.unit === 'kg') { totalReservedInDropUnit = totalReserved / 1000 sizeInDropUnit = size / 1000 } // Check if there's enough remaining inventory const remaining = drop.size - totalReservedInDropUnit if (sizeInDropUnit > remaining) { await connection.rollback() connection.release() return NextResponse.json( { error: 'Not enough inventory remaining. Item may be on hold by another buyer.' }, { status: 400 } ) } // Calculate price // ppu is stored as integer where 1000 = $1.00, so divide by 1000 to get actual price const pricePerUnit = drop.ppu / 1000 let priceAmount = 0 if (drop.unit === 'kg') { priceAmount = (size / 1000) * pricePerUnit } else { priceAmount = size * pricePerUnit } // Round to 2 decimal places priceAmount = Math.round(priceAmount * 100) / 100 // Generate order ID const orderId = `SALE-${Date.now()}-${drop_id}-${buyer_id}` // Get base URL for success/cancel redirects const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.headers.get('origin') || 'http://localhost:3420' // Get NOWPayments config (testnet or production) const nowPaymentsConfig = getNowPaymentsConfig() // Calculate expiration time (10 minutes from now) const expiresAt = new Date() expiresAt.setMinutes(expiresAt.getMinutes() + 10) // Create NOWPayments payment // Note: Payment API requires pay_currency (crypto currency) // Use currency from request, or fall back to env/default const payCurrency = pay_currency || process.env.NOWPAYMENTS_PAY_CURRENCY || 'btc' // Optional: Use fixed rate for 20 minutes (prevents rate changes during checkout) // If is_fixed_rate is true, payment expires after 20 minutes if not paid const isFixedRate = process.env.NOWPAYMENTS_FIXED_RATE === 'true' || false const nowPaymentsResponse = await fetch(`${nowPaymentsConfig.baseUrl}/v1/payment`, { method: 'POST', headers: { 'x-api-key': nowPaymentsConfig.apiKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ price_amount: priceAmount, price_currency: nowPaymentsConfig.currency, pay_currency: payCurrency, // Required: crypto currency (btc, eth, etc) order_id: orderId, order_description: `${drop.item} - ${size}g`, ipn_callback_url: ipnCallbackUrl, is_fixed_rate: isFixedRate, // Optional: freeze exchange rate for 20 minutes }), }) if (!nowPaymentsResponse.ok) { const error = await nowPaymentsResponse.json() console.error('NOWPayments error:', error) await connection.rollback() connection.release() return NextResponse.json( { error: 'Failed to create payment', details: error }, { status: 500 } ) } const payment = await nowPaymentsResponse.json() // Store pending order with expiration time (atomically reserves inventory) // payment.payment_id is the NOWPayments payment ID await connection.execute( 'INSERT INTO pending_orders (payment_id, order_id, drop_id, buyer_id, buyer_data_id, size, price_amount, price_currency, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [payment.payment_id, orderId, drop_id, buyer_id, buyer_data_id, size, priceAmount, nowPaymentsConfig.currency, expiresAt] ) // Commit transaction - inventory is now reserved await connection.commit() connection.release() // Return payment details - sale will be created by external IPN handler when payment is confirmed return NextResponse.json({ payment_id: payment.payment_id, payment_status: payment.payment_status, pay_address: payment.pay_address, // Address where customer sends payment pay_amount: payment.pay_amount, // Amount in crypto to pay pay_currency: payment.pay_currency, // Crypto currency price_amount: payment.price_amount, // Price in fiat price_currency: payment.price_currency, // Fiat currency order_id: orderId, payin_extra_id: payment.payin_extra_id, // Memo/tag for certain currencies (XRP, XLM, etc) expiration_estimate_date: payment.expiration_estimate_date, // When payment expires }, { status: 201 }) } catch (error) { await connection.rollback() connection.release() throw error } } catch (error) { console.error('Error creating payment:', error) return NextResponse.json( { error: 'Failed to create payment' }, { status: 500 } ) } }