#!/usr/bin/env node /** * Seed Tuxedo Corn 2026 tour: locations + stops. * * Uses sql`...` tagged template from @neondatabase/serverless (WebSocket mode) * for INSERTs — this is the only Neon serverless API that actually executes writes. * * Run with: * node scripts/seed-tuxedo-2026.js * * Requires DATABASE_URL in .env.local (or pass explicitly). */ require("dotenv").config({ path: ".env.local" }); const { neon } = require("@neondatabase/serverless"); const ExcelJS = require("exceljs"); const path = require("path"); const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; const YEAR = 2026; const url = process.env.DATABASE_URL; if (!url) { console.error("❌ DATABASE_URL not set in .env.local"); process.exit(1); } const cleanUrl = url.replace(/[?&]channel_binding=require/gi, "").trim(); // Use direct compute endpoint with WebSocket for full DDL + DML support const directUrl = cleanUrl.replace("-pooler.", "."); const sql = neon(directUrl, { webSocket: true }); const MONTH_MAP = { Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06", Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12", }; function parseExcelDate(s) { if (!s) return null; const m = /^([A-Za-z]{3})\s+(\d{1,2})$/.exec(String(s).trim()); if (!m) return null; const mm = MONTH_MAP[m[1]]; if (!mm) return null; return `${YEAR}-${mm}-${parseInt(m[2], 10).toString().padStart(2, "0")}`; } function parseTimeRange(s) { if (!s) return ""; return String(s).replace(/[–—]/g, "-").replace(/\s+/g, " ").trim(); } async function parseXlsx(xlsxPath) { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(xlsxPath); // --- Locations from Stop Directory --- const locSheet = workbook.getWorksheet("Stop Directory"); const locations = []; const seenLoc = new Set(); if (locSheet) { locSheet.eachRow((row, rowNum) => { if (rowNum === 1) return; // Column mapping via getCell(): // col1=Truck, col2=City, col3=State, col4=Host(venue name), col5=Address, col6=Phone, col7=Contact, col12=Notes const rawCity = String(row.getCell(2).value || "").trim(); const rawState = String(row.getCell(3).value || "").trim(); const rawName = String(row.getCell(4).value || "").trim(); const rawAddress = String(row.getCell(5).value || "").trim(); if (!rawCity || !rawName) return; const key = `${rawName}|${rawCity}|${rawState}`; if (seenLoc.has(key)) return; seenLoc.add(key); locations.push({ name: rawName, address: rawAddress || null, city: rawCity, state: rawState, zip: null, phone: String(row.getCell(6).value || "").trim() || null, contact_name: String(row.getCell(7).value || "").trim() || null, contact_email: null, notes: String(row.getCell(12).value || "").trim() || null, active: true, }); }); } console.log(`Parsed ${locations.length} locations`); // --- Stops from Full Schedule --- const schedSheet = workbook.getWorksheet("Full Schedule"); const stops = []; const venueAddressMap = new Map(); if (locSheet) { locSheet.eachRow((row, rowNum) => { if (rowNum === 1) return; const name = String(row.getCell(4).value || "").trim(); const address = String(row.getCell(5).value || "").trim(); if (name && address) venueAddressMap.set(name, address); }); } if (schedSheet) { schedSheet.eachRow((row) => { const rowValues = row.values || []; // Column mapping (1-indexed, col1=null): // col2=Wk, col3=Type, col4=Date, col5=Day, col6=City/Location, col7=Host/Venue, col8=Time, col9=Truck, col10=Status, col11=Notes const firstCell = String(rowValues[1] || "").trim(); // Skip title rows, week headers ("Wk 1"), OFF, Cross-Dock if (!firstCell || /^Wk\s*\d+/i.test(firstCell) || firstCell === "OFF" || firstCell === "Cross-Dock") return; const dateStr = parseExcelDate(rowValues[3]); if (!dateStr) return; const cityStateRaw = String(rowValues[5] || "").trim(); const cityParts = cityStateRaw.split(",").map((s) => s.trim()); const city = cityParts[0] || ""; const state = cityParts[1] || ""; const location = String(rowValues[6] || "").trim(); if (!city || !state || !location) return; const rawTime = rowValues[7]; const time = rawTime ? parseTimeRange(rawTime) : ""; const truck = String(rowValues[8] || "").trim(); const statusText = String(rowValues[9] || "").trim(); const notesText = String(rowValues[10] || "").trim(); const venueAddress = venueAddressMap.get(location) || null; const notes = [truck ? `Truck: ${truck}` : "", statusText, notesText].filter(Boolean).join(" | "); stops.push({ name: `${location} - ${city}, ${state}`, location, address: venueAddress, city, state, zip: null, date: dateStr, time, cutoff_date: null, status: "active", is_public: true, notes: notes || null, }); }); } console.log(`Parsed ${stops.length} stops`); return { locations, stops }; } async function seedLocations(locs) { if (locs.length === 0) return; await sql`DELETE FROM locations WHERE brand_id = ${TUXEDO_BRAND_ID}::uuid`; for (const loc of locs) { await sql`INSERT INTO locations (brand_id, name, address, city, state, zip, phone, contact_name, contact_email, notes, active) VALUES (${TUXEDO_BRAND_ID}::uuid, ${loc.name}, ${loc.address}, ${loc.city}, ${loc.state}, ${loc.zip}, ${loc.phone}, ${loc.contact_name}, ${loc.contact_email}, ${loc.notes}, ${loc.active})`; } console.log(`Seeded ${locs.length} locations`); } async function seedStops(stops) { if (stops.length === 0) return; const minDate = `${YEAR}-07-01`; const maxDate = `${YEAR}-09-30`; await sql`DELETE FROM stops WHERE brand_id = ${TUXEDO_BRAND_ID}::uuid AND date >= ${minDate} AND date <= ${maxDate}`; for (const stop of stops) { await sql`INSERT INTO stops (brand_id, name, location, address, city, state, zip, date, "time", cutoff_date, status, is_public, notes) VALUES (${TUXEDO_BRAND_ID}::uuid, ${stop.name}, ${stop.location}, ${stop.address}, ${stop.city}, ${stop.state}, ${stop.zip}, ${stop.date}, ${stop.time}, ${stop.cutoff_date}, ${stop.status}, ${stop.is_public}, ${stop.notes})`; } console.log(`Seeded ${stops.length} stops`); } async function main() { const xlsxPath = path.join(__dirname, "..", "Tuxedo_Corn_2026_Tour_Schedule-3.xlsx"); console.log("Parsing:", xlsxPath); const { locations, stops } = await parseXlsx(xlsxPath); if (locations.length === 0 && stops.length === 0) { console.error("❌ No data parsed from xlsx"); process.exit(1); } console.log(`Seeding ${locations.length} locations + ${stops.length} stops for Tuxedo Corn...`); await seedLocations(locations); await seedStops(stops); const locCount = await sql.query(`SELECT COUNT(*) FROM locations WHERE brand_id = '${TUXEDO_BRAND_ID}'`); const stopCount = await sql.query(`SELECT COUNT(*) FROM stops WHERE brand_id = '${TUXEDO_BRAND_ID}'`); console.log(`\n✅ Done — ${locCount[0].count} locations, ${stopCount[0].count} stops in DB`); } main().catch((e) => { console.error("❌ Seed failed:", e.message); process.exit(1); });