916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
178 lines
6.9 KiB
TypeScript
178 lines
6.9 KiB
TypeScript
/**
|
|
* 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 (<admin_user_id>, <brand_id>, '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:
|
|
"<p>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.</p>",
|
|
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:
|
|
"<p>Direct-from-grove wholesale produce. Family farms, fair pricing, fast delivery.</p>",
|
|
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', '<h1>Fresh this week</h1><p>Hi {{name}}, our harvest is in. Reply to reserve.</p>'
|
|
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);
|
|
});
|