import type { VercelRequest, VercelResponse } from '@vercel/node'; import { createClient } from '@supabase/supabase-js'; import Papa from 'papaparse'; const supabase = createClient( process.env.SUPABASE_URL || '', process.env.SUPABASE_SERVICE_KEY || '' ); function parseEUNumber(val: string | undefined | null): number | null { if (!val || val.trim() === '') return null; const cleaned = val.replace(/\./g, '').replace(',', '.'); const num = parseFloat(cleaned); return isNaN(num) ? null : num; } function parseIntSafe(val: string | undefined | null): number | null { if (!val || val.trim() === '') return null; const cleaned = val.replace(/\./g, '').replace(',', '.'); const num = parseInt(cleaned, 10); return isNaN(num) ? null : num; } interface CSVRow { [key: string]: string; } export default async function handler(req: VercelRequest, res: VercelResponse) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') return res.status(200).end(); if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' }); try { const csvText = typeof req.body === 'string' ? req.body : req.body?.toString() || ''; if (!csvText.trim()) { return res.status(400).json({ error: 'Empty CSV body' }); } const parsed = Papa.parse(csvText, { header: true, skipEmptyLines: true, }); if (parsed.errors.length > 0) { console.error('[upload-vendor-data] Parse errors:', parsed.errors.slice(0, 5)); } const rows = parsed.data .filter(row => row['Date'] && row['Market'] && row['ASIN']) .map(row => ({ date: row['Date'], market: row['Market'], asin: row['ASIN'], product_title: row['Product Title'] || null, tags: row['Tags'] || null, bsr_top_rank: parseIntSafe(row['Top Level Category (Rank)']), bsr_top_category: row['Top Level Category (Name)'] || null, bsr_detail_rank: parseIntSafe(row['Detail Level Category (Rank)']), bsr_detail_category: row['Detail Level Category (Name)'] || null, avg_rating: parseEUNumber(row['Average Rating']), num_reviews: parseIntSafe(row['Number of Reviews']), buybox_owner: row['Buybox Seller Name'] || null, buybox_price: parseEUNumber(row['Buybox Price']), amazon_has_buybox: row['Amazon Has Buybox'] === '1', glance_views: parseIntSafe(row['Glance Views']), })); if (rows.length === 0) { return res.status(400).json({ error: 'No valid rows found in CSV' }); } // Upsert in batches of 500 const BATCH_SIZE = 500; let totalUpserted = 0; for (let i = 0; i < rows.length; i += BATCH_SIZE) { const batch = rows.slice(i, i + BATCH_SIZE); const { error } = await supabase .from('vendor_daily_data') .upsert(batch, { onConflict: 'date,market,asin' }); if (error) { console.error('[upload-vendor-data] Upsert error at batch', i, error); throw error; } totalUpserted += batch.length; } console.log(`[upload-vendor-data] Successfully upserted ${totalUpserted} rows`); res.status(200).json({ success: true, rowsParsed: parsed.data.length, rowsUpserted: totalUpserted, }); } catch (error: any) { console.error('[upload-vendor-data] Error:', error); res.status(500).json({ error: error.message }); } }