Files
mercadodevida/project/scripts/seed.cjs
2026-08-21 22:14:29 +02:00

338 lines
12 KiB
JavaScript

/**
* 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();