Files
CrazeAnalytix/api/upload-vendor-data.ts
T
Christian Vidal WolfandClaude Opus 4.6 2bce0a839b feat: add Supabase vendor data integration (BSR, ratings, buy box)
- Create vendor_daily_data table schema and Supabase client service
- Add upload API route for Vendor Central CSV parsing and upsert
- Add VendorDataView with BSR trend, ratings, and buy box charts
- Integrate new Vendor tab into app navigation (desktop + mobile)
- Add vendor CSV upload card to FileUpload modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:19:25 +01:00

105 lines
3.5 KiB
TypeScript

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<CSVRow>(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 });
}
}