/** * seed.cjs — Populate database with realistic dev/test data. * Usage: node scripts/seed.cjs */ 'use strict'; const fs = require('fs'); const path = require('path'); const { randomUUID } = require('crypto'); const argon2 = require('argon2'); const { Client } = require('pg'); const DEFAULT_STORE_ID = '00000000-0000-0000-0000-000000000001'; // ── Read DATABASE_URL from .env ────────────────────────────────────────────── const envPath = path.join(__dirname, '..', '.env'); if (fs.existsSync(envPath)) { fs.readFileSync(envPath, 'utf8') .split('\n') .filter((l) => l.trim() && !l.startsWith('#')) .forEach((l) => { const eq = l.indexOf('='); if (eq === -1) return; const k = l.slice(0, eq).trim(); const v = l.slice(eq + 1).trim(); if (k && !process.env[k]) process.env[k] = v; }); } const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error('DATABASE_URL not set'); process.exit(1); } // ── Helpers ──────────────────────────────────────────────────────────────── async function sql(client, text, params, label) { try { const result = await client.query(text, params); return result; } catch (err) { console.error(`SQL ERROR [${label}]: ${err.message}`); throw err; } } async function seed(client) { await client.query('BEGIN'); console.log('Seeding database...\n'); // ── Clear existing seed data ────────────────────────────────────────── await sql(client, 'DELETE FROM inventory_movements', [], 'clear-movements'); await sql(client, 'DELETE FROM inventory_stock', [], 'clear-stock'); await sql(client, 'DELETE FROM pricing_variant_prices', [], 'clear-prices'); await sql(client, 'DELETE FROM catalog_product_variants', [], 'clear-variants'); await sql(client, 'DELETE FROM catalog_product_categories', [], 'clear-prod-cats'); await sql(client, 'DELETE FROM catalog_products', [], 'clear-products'); await sql( client, 'DELETE FROM categories_categories WHERE parent_id IS NOT NULL', [], 'clear-children', ); await sql(client, 'DELETE FROM categories_categories', [], 'clear-categories'); await sql(client, 'DELETE FROM brands_brands', [], 'clear-brands'); await sql(client, 'DELETE FROM shipping_methods', [], 'clear-methods'); await sql(client, 'DELETE FROM shipping_zones', [], 'clear-zones'); console.log('Cleared existing data\n'); const catBySlug = {}; const brandBySlug = {}; // ── 1. Categories ───────────────────────────────────────────────────── const catData = [ { name: 'Alimentacion', slug: 'alimentacion', parentSlug: null }, { name: 'Suplementos', slug: 'suplementos', parentSlug: null }, { name: 'Cosmetica Natural', slug: 'cosmetica-natural', parentSlug: null }, { name: 'Limpieza Ecologica', slug: 'limpieza-ecologica', parentSlug: null }, { name: 'Frutos Secos', slug: 'frutos-secos', parentSlug: 'alimentacion' }, { name: 'Aceites', slug: 'aceites', parentSlug: 'alimentacion' }, { name: 'Hierbas e Infusiones', slug: 'hierbas-infusiones', parentSlug: 'alimentacion' }, { name: 'Vitaminas', slug: 'vitaminas', parentSlug: 'suplementos' }, { name: 'Proteinas', slug: 'proteinas', parentSlug: 'suplementos' }, { name: 'Cremas', slug: 'cremas', parentSlug: 'cosmetica-natural' }, { name: 'Jabones', slug: 'jabones', parentSlug: 'cosmetica-natural' }, ]; // Parents first (no FK dep), then children const parents = catData.filter((c) => !c.parentSlug); const children = catData.filter((c) => c.parentSlug); for (const cat of [...parents, ...children]) { const parentId = cat.parentSlug ? catBySlug[cat.parentSlug] : null; await sql( client, 'INSERT INTO categories_categories (id, parent_id, name, slug, seo_title) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (slug) DO UPDATE SET name=EXCLUDED.name, seo_title=EXCLUDED.seo_title', [randomUUID(), parentId, cat.name, cat.slug, cat.name], `cat-${cat.slug}`, ); const r = await client.query('SELECT id FROM categories_categories WHERE slug = $1', [ cat.slug, ]); if (r.rows[0]) catBySlug[cat.slug] = r.rows[0].id; } console.log(`+ ${catData.length} categories`); // ── 2. Brands ──────────────────────────────────────────────────────── const brandData = [ { name: 'EcoVida', slug: 'ecovida' }, { name: 'NaturGreen', slug: 'naturgreen' }, { name: 'BioSana', slug: 'biosana' }, { name: 'VerdePura', slug: 'verdepura' }, { name: 'TierraNatural', slug: 'tierranatural' }, { name: 'SolNatural', slug: 'solnatural' }, ]; for (const brand of brandData) { await sql( client, 'INSERT INTO brands_brands (id, name, slug, seo_title) VALUES ($1,$2,$3,$4) ON CONFLICT (slug) DO UPDATE SET name=EXCLUDED.name, seo_title=EXCLUDED.seo_title', [randomUUID(), brand.name, brand.slug, brand.name], `brand-${brand.slug}`, ); const r = await client.query('SELECT id FROM brands_brands WHERE slug = $1', [brand.slug]); if (r.rows[0]) brandBySlug[brand.slug] = r.rows[0].id; } console.log(`+ ${brandData.length} brands`); // ── 3. Products + variants + prices + stock ─────────────────────────── const productData = [ { name: 'Almendras Crudas Ecologicas', slug: 'almendras-crudas-ecologicas', brandSlug: 'ecovida', catSlug: 'frutos-secos', price: 895, vat: 'general', }, { name: 'Aceite de Oliva Virgen Extra Bio', slug: 'aceite-oliva-virgen-extra-bio', brandSlug: 'naturgreen', catSlug: 'aceites', price: 1290, vat: 'general', }, { name: 'Te Verde Matcha Premium', slug: 'te-verde-matcha-premium', brandSlug: 'biosana', catSlug: 'hierbas-infusiones', price: 1590, vat: 'general', }, { name: 'Vitamina D3 + K2', slug: 'vitamina-d3-k2', brandSlug: 'verdepura', catSlug: 'vitaminas', price: 1890, vat: 'reduced', }, { name: 'Proteina Guisante Ecologica', slug: 'proteina-guisante-ecologica', brandSlug: 'tierranatural', catSlug: 'proteinas', price: 2490, vat: 'reduced', }, { name: 'Complejo B Natural', slug: 'complejo-b-natural', brandSlug: 'ecovida', catSlug: 'vitaminas', price: 1190, vat: 'reduced', }, { name: 'Crema Facial Hidratante Aloe Vera', slug: 'crema-facial-aloe-vera', brandSlug: 'solnatural', catSlug: 'cremas', price: 2190, vat: 'general', }, { name: 'Jabon Negro Africano', slug: 'jabon-negro-africano', brandSlug: 'biosana', catSlug: 'jabones', price: 790, vat: 'general', }, { name: 'Serum Antioxidante Rosa Mosqueta', slug: 'serum-rosa-mosqueta', brandSlug: 'naturgreen', catSlug: 'cremas', price: 2690, vat: 'general', }, { name: 'Detergente Ecologico Ropa', slug: 'detergente-ecologico-ropa', brandSlug: 'verdepura', catSlug: 'limpieza-ecologica', price: 990, vat: 'general', }, { name: 'Limpiador Multiusos Bio', slug: 'limpiador-multiusos-bio', brandSlug: 'tierranatural', catSlug: 'limpieza-ecologica', price: 890, vat: 'general', }, { name: 'Desinfectante Natural Superficies', slug: 'desinfectante-natural-superficies', brandSlug: 'solnatural', catSlug: 'limpieza-ecologica', price: 1190, vat: 'general', }, ]; for (let pi = 0; pi < productData.length; pi++) { const p = productData[pi]; const brandId = brandBySlug[p.brandSlug]; const catId = catBySlug[p.catSlug]; if (!brandId || !catId) { console.warn(` Skipping ${p.slug}: missing brand or cat`); continue; } const productId = randomUUID(); const variantId = randomUUID(); const sku = `SKU-${p.slug.replace(/-/g, '').toUpperCase().slice(0, 8)}-001`; const ean = `590123412345${String(pi + 1).padStart(2, '0')}`; const desc = `${p.name} de primera calidad. Producto 100% natural y ecologico.`; await sql( client, 'INSERT INTO catalog_products (id, brand_id, name, slug, state, seo_title, description) VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (slug) DO UPDATE SET name=EXCLUDED.name, brand_id=EXCLUDED.brand_id, description=EXCLUDED.description', [productId, brandId, p.name, p.slug, 'active', p.name, desc], `product-${p.slug}`, ); await sql( client, 'INSERT INTO catalog_product_categories (product_id, category_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [productId, catId], `prodcat-${p.slug}`, ); await sql( client, 'INSERT INTO catalog_product_variants (id, product_id, sku, ean) VALUES ($1,$2,$3,$4) ON CONFLICT (sku) DO UPDATE SET ean=EXCLUDED.ean', [variantId, productId, sku, ean], `variant-${sku}`, ); await sql( client, 'INSERT INTO pricing_variant_prices (variant_id, vat_rate, net_unit_amount_cents) VALUES ($1,$2,$3) ON CONFLICT (variant_id) DO UPDATE SET net_unit_amount_cents=EXCLUDED.net_unit_amount_cents, vat_rate=EXCLUDED.vat_rate', [variantId, p.vat, p.price], `price-${sku}`, ); await sql( client, 'INSERT INTO inventory_stock (variant_id, store_id, available, reserved) VALUES ($1,$2,$3,$4) ON CONFLICT (variant_id, store_id) DO UPDATE SET available=EXCLUDED.available, reserved=EXCLUDED.reserved', [variantId, DEFAULT_STORE_ID, 25, 0], `stock-${sku}`, ); } console.log(`+ ${productData.length} products (variants, prices, stock)`); // ── 4. Shipping ─────────────────────────────────────────────────── const zoneId = randomUUID(); await sql( client, 'INSERT INTO shipping_zones (id, name, country, postal_code_prefix) VALUES ($1,$2,$3,$4) ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name', [zoneId, 'Espania Peninsular', 'ES', null], 'zone-es', ); await sql( client, 'INSERT INTO shipping_methods (id, zone_id, name, base_cost_cents, free_shipping_threshold_cents) VALUES ($1,$2,$3,$4,$5)', [randomUUID(), zoneId, 'Estandar', 499, 4900], 'method-std', ); await sql( client, 'INSERT INTO shipping_methods (id, zone_id, name, base_cost_cents, free_shipping_threshold_cents) VALUES ($1,$2,$3,$4,$5)', [randomUUID(), zoneId, 'Express 24h', 899, null], 'method-express', ); console.log('+ 1 shipping zone + 2 methods'); // ── 5. Backoffice admin ──────────────────────────────────────────── const adminEmail = process.env.SEED_ADMIN_EMAIL || 'admin@mdv.local'; const adminPassword = process.env.SEED_ADMIN_PASSWORD || 'Admin123456!'; if (process.env.NODE_ENV === 'production' && !process.env.SEED_ADMIN_PASSWORD) { throw new Error('SEED_ADMIN_PASSWORD must be set when NODE_ENV=production'); } const adminHash = await argon2.hash(adminPassword); await sql( client, `INSERT INTO backoffice_users (email, password_hash, role, mfa_enrolled) VALUES ($1, $2, 'admin', false) ON CONFLICT (email) DO UPDATE SET password_hash = EXCLUDED.password_hash, role = 'admin', updated_at = now()`, [adminEmail, adminHash], 'backoffice-admin', ); console.log(`+ backoffice admin (${adminEmail})`); await client.query('COMMIT'); console.log('\nSeed complete!'); } // ── Run ────────────────────────────────────────────────────────────────── async function main() { const client = new Client({ connectionString: DATABASE_URL }); try { await client.connect(); await seed(client); } catch (err) { await client.query('ROLLBACK').catch(() => {}); console.error('Seed failed:', err.message); process.exit(1); } finally { await client.end(); } } main();