/** * Seed script. Idempotent — safe to re-run. Uses `ON CONFLICT (col) DO UPDATE` * (with a target) for tables that have a unique constraint; uses * `WHERE NOT EXISTS` for tables that don't. * * npm run db:seed * * Populates: * - 2 brands (Tuxedo, Indian River Direct) * - brand_settings per brand * - sample products, stops, customers per brand * - sample communication templates + a draft campaign * * NOTE: Admin users are managed by Neon Auth. To create an admin user: * 1. Sign up / sign in via the app (creates a neon_auth.user row) * 2. Manually insert into admin_users + admin_user_brands: * INSERT INTO admin_users (email, name, role) VALUES ('you@example.com', 'Your Name', 'brand_admin'); * INSERT INTO admin_user_brands (admin_user_id, brand_id, role) VALUES (, , 'brand_admin'); */ import "dotenv/config"; import { Pool } from "pg"; const pool = new Pool({ connectionString: process.env.DATABASE_ADMIN_URL ?? process.env.DATABASE_URL ?? "postgres://postgres:postgres@localhost:5432/route_commerce", }); async function main() { const client = await pool.connect(); try { await client.query("BEGIN"); // ── Brands ────────────────────────────────────────────────────────────── const brandsData = [ { slug: "tuxedo", name: "Tuxedo Citrus", brandName: "Tuxedo Citrus Co.", tagline: "Sun-ripened citrus, delivered.", aboutHtml: "

Family-run citrus grove in the Indian River region. We grow, pack, and ship premium grapefruit, oranges, and specialty varieties to wholesale buyers across the country.

", primaryColor: "#F59E0B", contactEmail: "hello@tuxedocitrus.example", contactPhone: "(555) 010-2200", }, { slug: "indian-river-direct", name: "Indian River Direct", brandName: "Indian River Direct", tagline: "From our groves to your store.", aboutHtml: "

Direct-from-grove wholesale produce. Family farms, fair pricing, fast delivery.

", primaryColor: "#0F766E", contactEmail: "orders@indianriverdirect.example", contactPhone: "(555) 010-3300", }, ]; for (const b of brandsData) { // Upsert brand const brandRes = await client.query<{ id: string }>( `INSERT INTO brands (name, slug, plan_tier) VALUES ($1, $2, 'starter') ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name RETURNING id`, [b.name, b.slug], ); const brandId = brandRes.rows[0].id; // Brand settings (PK is brand_id) await client.query( `INSERT INTO brand_settings (brand_id, brand_name, tagline, about_html, primary_color, contact_email, contact_phone) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (brand_id) DO UPDATE SET brand_name = EXCLUDED.brand_name, tagline = EXCLUDED.tagline, about_html = EXCLUDED.about_html, primary_color = EXCLUDED.primary_color, contact_email = EXCLUDED.contact_email, contact_phone = EXCLUDED.contact_phone`, [brandId, b.brandName, b.tagline, b.aboutHtml, b.primaryColor, b.contactEmail, b.contactPhone], ); // Sample products const products = [ { name: "Ruby Red Grapefruit", price: 2400, unit: "40 lb case", desc: "Sweet, juicy, seedless." }, { name: "Navel Oranges", price: 2200, unit: "40 lb case", desc: "Classic eating orange, easy-peel." }, { name: "Honeybells", price: 3200, unit: "20 lb case", desc: "Limited season — bell-shaped, super sweet." }, { name: "Tangelos", price: 2800, unit: "40 lb case", desc: "Tangerine-grapefruit hybrid." }, ]; for (const p of products) { await client.query( `INSERT INTO products (brand_id, name, description, price_cents, inventory, unit, active) SELECT $1, $2, $3, $4, 100, $5, true WHERE NOT EXISTS ( SELECT 1 FROM products WHERE brand_id = $1 AND name = $2 )`, [brandId, p.name, p.desc, p.price, p.unit], ); } // Sample stops const stops = [ { name: "Downtown Farmers Market", address: "100 Main St", schedule: [{ day: "Saturday", time: "08:00" }] }, { name: "Eastside Pickup Hub", address: "555 Oak Ave", schedule: [{ day: "Wednesday", time: "16:00" }] }, ]; for (const s of stops) { await client.query( `INSERT INTO stops (brand_id, name, address, schedule, status) SELECT $1, $2, $3, $4::jsonb, 'active' WHERE NOT EXISTS ( SELECT 1 FROM stops WHERE brand_id = $1 AND name = $2 )`, [brandId, s.name, s.address, JSON.stringify(s.schedule)], ); } // Sample customers const customers = [ { name: "Green Grocer Co.", email: "buyer@greengrocer.example", phone: "555-010-0010" }, { name: "Sunset Cafe", email: "orders@sunsetcafe.example", phone: "555-010-0020" }, { name: "Northside Co-op", email: "produce@northsidecoop.example", phone: "555-010-0030" }, ]; for (const c of customers) { await client.query( `INSERT INTO customers (brand_id, name, email, phone, sms_opt_in, email_opt_in) SELECT $1, $2, $3, $4, true, true WHERE NOT EXISTS ( SELECT 1 FROM customers WHERE brand_id = $1 AND email = $3 )`, [brandId, c.name, c.email, c.phone], ); } // Sample communication template const tmplRes = await client.query<{ id: string }>( `INSERT INTO communication_templates (brand_id, name, subject, body_html) SELECT $1, 'Weekly Availability', 'This week at the grove', '

Fresh this week

Hi {{name}}, our harvest is in. Reply to reserve.

' WHERE NOT EXISTS ( SELECT 1 FROM communication_templates WHERE brand_id = $1 AND name = 'Weekly Availability' ) RETURNING id`, [brandId], ); if (tmplRes.rows[0]) { await client.query( `INSERT INTO communication_campaigns (brand_id, template_id, name, status) SELECT $1, $2, 'Welcome series — week 1', 'draft' WHERE NOT EXISTS ( SELECT 1 FROM communication_campaigns WHERE brand_id = $1 AND name = 'Welcome series — week 1' )`, [brandId, tmplRes.rows[0].id], ); } } console.log(`Seeded ${brandsData.length} brands with sample data`); await client.query("COMMIT"); console.log("✅ Seed complete"); } catch (err) { await client.query("ROLLBACK"); throw err; } finally { client.release(); } } main() .then(() => pool.end()) .catch((err) => { console.error("❌ Seed failed:", err); pool.end(); process.exit(1); });