#!/usr/bin/env node /** * Seed Demo Data — a full "take it for a ride" dataset. * * Populates every user-facing surface (bills, categories + groups, payment * history with drift + debt paydown, monthly overrides/skips/snoozes, planning * income/starting amounts, spending budgets/rules/transactions, a demo bank * source + accounts + matched/unmatched/subscription transactions, an active * snowball plan, merchant rules, a calendar feed) so a new user can explore the * whole app. * * Removal contract: everything created here is either marked `is_seeded = 1` on a * user-scoped table, or is a bill-child that cascade-deletes with its seeded bill. * `POST /user/clear-demo-data` removes exactly these rows and nothing else — see * routes/user.cts. Keep that in sync when adding a new seeded surface. * * Idempotent: refuses to run when the account already has seeded bills. */ const path = require('path'); // DB_PATH is read at require-time by db/database — keep the default in sync. process.env.DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'db', 'bills.db'); const { getDb, ensureUserDefaultCategories } = require('../db/database.cts'); // Money columns are integer cents (migration v1.03) — convert demo dollars. const { toCents } = require('../utils/money.mts'); // ── Static demo content ───────────────────────────────────────────────────── const BILL_CATEGORIES = [ 'Utilities', 'Housing', 'Insurance', 'Subscriptions', 'Transportation', 'Healthcare', 'Credit Cards', 'Loans', 'Entertainment', ]; // Spending categories are distinct from bill categories and from the app's // DEFAULT_CATEGORIES, so they are always seed-created (and thus removable). const SPENDING_CATEGORIES = ['Groceries', 'Dining Out', 'Gas & Fuel', 'Shopping', 'Coffee']; const CATEGORY_GROUPS = [ { name: 'Living Expenses', members: ['Groceries', 'Gas & Fuel'] }, { name: 'Discretionary', members: ['Dining Out', 'Shopping', 'Coffee'] }, ]; // billing_cycle: 'monthly' | 'quarterly' | 'annually' | 'irregular' // cycle_type: 'monthly' | 'weekly' | 'biweekly' | 'quarterly' | 'annual' const BILLS = [ // ── Utilities ── { name: 'Electric Company', category: 'Utilities', amount: 85, dueDay: 15, cycle: 'monthly', cycleType: 'monthly', autopay: true, autopayVerified: true, }, { name: 'Gas Utility', category: 'Utilities', amount: 35, dueDay: 12, cycle: 'monthly', cycleType: 'monthly', autopay: true, }, { name: 'City Water Dept', category: 'Utilities', amount: 45, dueDay: 20, cycle: 'monthly', cycleType: 'monthly', autopay: true, }, { name: 'Internet Provider', category: 'Utilities', amount: 70, dueDay: 18, cycle: 'monthly', cycleType: 'monthly', autopay: true, autopayVerified: true, }, { name: 'Cell Phone', category: 'Utilities', amount: 65, dueDay: 25, cycle: 'monthly', cycleType: 'monthly', autopay: true, currentBalance: 648, minPayment: 27, interestRate: 0, snowballOrder: 0, snowballInclude: 1, }, { name: 'Trash Service', category: 'Utilities', amount: 25, dueDay: 28, cycle: 'monthly', cycleType: 'monthly', autopay: false, }, // ── Housing ── { name: 'Mortgage', category: 'Housing', amount: 1450, dueDay: 1, cycle: 'monthly', cycleType: 'monthly', autopay: true, autopayVerified: true, interestRate: 3.25, currentBalance: 185000, minPayment: 1200, snowballInclude: 0, snowballExempt: 1, }, // ── Insurance / Healthcare ── { name: 'Car Insurance', category: 'Insurance', amount: 120, dueDay: 5, cycle: 'quarterly', cycleType: 'quarterly', autopay: true, }, { name: 'Homeowners Insurance', category: 'Insurance', amount: 300, dueDay: 10, cycle: 'annually', cycleType: 'annual', autopay: false, }, { name: 'Health Insurance', category: 'Healthcare', amount: 200, dueDay: 1, cycle: 'quarterly', cycleType: 'quarterly', autopay: true, }, { name: 'Dental Insurance', category: 'Healthcare', amount: 40, dueDay: 15, cycle: 'quarterly', cycleType: 'quarterly', autopay: true, }, // ── Credit Cards (snowball, smallest→largest) ── { name: 'Discover It Card', category: 'Credit Cards', amount: 65, dueDay: 26, cycle: 'monthly', cycleType: 'monthly', autopay: true, interestRate: 22.99, currentBalance: 920, minPayment: 35, snowballOrder: 1, snowballInclude: 1, website: 'https://discover.com', has2fa: true, }, { name: 'Capital One Quicksilver', category: 'Credit Cards', amount: 95, dueDay: 28, cycle: 'monthly', cycleType: 'monthly', autopay: true, interestRate: 24.49, currentBalance: 1850, minPayment: 55, snowballOrder: 2, snowballInclude: 1, website: 'https://capitalone.com', has2fa: true, }, { name: 'Chase Freedom', category: 'Credit Cards', amount: 140, dueDay: 12, cycle: 'monthly', cycleType: 'monthly', autopay: true, interestRate: 21.49, currentBalance: 3200, minPayment: 90, snowballOrder: 3, snowballInclude: 1, website: 'https://chase.com', has2fa: true, }, // ── Loans ── { name: 'Car Payment', category: 'Loans', amount: 425, dueDay: 22, cycle: 'monthly', cycleType: 'monthly', autopay: true, interestRate: 4.5, currentBalance: 8400, minPayment: 350, snowballOrder: 4, snowballInclude: 1, }, { name: 'Student Loan', category: 'Loans', amount: 250, dueDay: 15, cycle: 'monthly', cycleType: 'monthly', autopay: true, interestRate: 5.5, currentBalance: 12500, minPayment: 150, snowballOrder: 5, snowballInclude: 1, }, // ── Subscriptions ── { name: 'Netflix', category: 'Subscriptions', amount: 15.99, dueDay: 22, cycle: 'monthly', cycleType: 'monthly', autopay: true, isSubscription: true, subscriptionType: 'streaming', website: 'https://netflix.com', }, { name: 'Spotify', category: 'Subscriptions', amount: 9.99, dueDay: 14, cycle: 'monthly', cycleType: 'monthly', autopay: true, isSubscription: true, subscriptionType: 'music', website: 'https://spotify.com', }, { name: 'Adobe Creative Cloud', category: 'Subscriptions', amount: 54.99, dueDay: 8, cycle: 'monthly', cycleType: 'monthly', autopay: true, isSubscription: true, subscriptionType: 'software', website: 'https://adobe.com', has2fa: true, }, { name: 'Amazon Prime', category: 'Subscriptions', amount: 139, dueDay: 1, cycle: 'annually', cycleType: 'annual', autopay: true, isSubscription: true, subscriptionType: 'shopping', website: 'https://amazon.com', }, { name: 'Apple iCloud+', category: 'Subscriptions', amount: 2.99, dueDay: 18, cycle: 'monthly', cycleType: 'monthly', autopay: true, isSubscription: true, subscriptionType: 'cloud', website: 'https://icloud.com', }, { name: 'Gym Membership', category: 'Subscriptions', amount: 45, dueDay: 10, cycle: 'monthly', cycleType: 'monthly', autopay: true, isSubscription: true, subscriptionType: 'fitness', }, // ── Entertainment (a genuine recurring entertainment bill, monthly) ── { name: 'AMC A-List', category: 'Entertainment', amount: 24.99, dueDay: 3, cycle: 'monthly', cycleType: 'monthly', autopay: true, isSubscription: true, subscriptionType: 'entertainment', website: 'https://amctheatres.com', }, ]; // Bills deliberately left OVERDUE this month (due date passed, no payment). const OVERDUE_THIS_MONTH = new Set(['Trash Service', 'Dental Insurance']); // Bills whose most-recent payments also appear as matched bank transactions. const BANK_MATCHED = new Set([ 'Electric Company', 'Internet Provider', 'Netflix', 'Spotify', 'Car Payment', ]); // One skipped month (bill leaves that month unpaid, flagged skipped not overdue). const SKIP = { bill: 'Gym Membership', monthsAgo: 1 }; // One amount override (actual differs from expected) and one note. const OVERRIDE = { bill: 'Electric Company', monthsAgo: 1, actual: 132.4 }; const NOTE = { bill: 'City Water Dept', text: 'Called about the leak — credit expected next cycle.', }; // Un-tracked subscription charges (in the catalog, no matching bill) → drive the // Subscriptions "recommendations" surface. const UNTRACKED_SUBS = [ { payee: 'HULU', amount: 17.99, day: 6 }, { payee: 'HBO MAX', amount: 15.99, day: 11 }, { payee: 'YOUTUBE PREMIUM', amount: 13.99, day: 19 }, ]; // Everyday spending (not bills) → populate the Spending page + budgets/rules. const SPENDING_MERCHANTS = [ { payee: 'WHOLE FOODS MARKET', category: 'Groceries', amounts: [82.14, 46.03, 118.77] }, { payee: "TRADER JOE'S", category: 'Groceries', amounts: [54.2, 61.88] }, { payee: 'CHIPOTLE', category: 'Dining Out', amounts: [14.85, 22.4] }, { payee: 'OLIVE GARDEN', category: 'Dining Out', amounts: [63.12] }, { payee: 'SHELL OIL', category: 'Gas & Fuel', amounts: [48.9, 51.35] }, { payee: 'AMAZON MARKETPLACE', category: 'Shopping', amounts: [39.99, 27.49, 112.0] }, { payee: 'STARBUCKS', category: 'Coffee', amounts: [6.45, 5.75, 6.45] }, ]; const SPENDING_RULES = [ { merchant: 'WHOLE FOODS', category: 'Groceries' }, { merchant: 'SHELL', category: 'Gas & Fuel' }, { merchant: 'STARBUCKS', category: 'Coffee' }, { merchant: 'AMAZON', category: 'Shopping' }, ]; const SPENDING_BUDGETS = { Groceries: 500, 'Dining Out': 200, 'Gas & Fuel': 160, Shopping: 250, Coffee: 40, }; const HISTORY_MONTHS = 6; // months of payment history to generate (incl. current) // ── Date helpers ──────────────────────────────────────────────────────────── function ymd(d) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function clampedDate(year, monthIdx0, day) { const lastDay = new Date(year, monthIdx0 + 1, 0).getDate(); return new Date(year, monthIdx0, Math.min(day, lastDay)); } // Month offsets (0 = current, negative = past) a bill occurs in, within the window. function cycleOffsets(bill) { switch (bill.cycle) { case 'annually': return [-1]; case 'quarterly': return [0, -3]; case 'irregular': return [0, -1, -2]; default: return Array.from({ length: HISTORY_MONTHS }, (_, i) => -(HISTORY_MONTHS - 1) + i); // -5..0 } } // Small deterministic-ish drift on variable bills so amounts differ from expected. function driftedAmount(bill, offset) { const variable = ['Utilities'].includes(bill.category) && !bill.currentBalance; if (!variable) return bill.amount; const pct = ((Math.abs(bill.name.length * 7 + offset * 13) % 21) - 10) / 100; // ±10% return Math.round(bill.amount * (1 + pct) * 100) / 100; } // ── Main ──────────────────────────────────────────────────────────────────── function seedDemoData(userId = null) { const db = getDb(); const now = new Date(); const todayStr = ymd(now); const targetUser = userId !== null ? db.prepare('SELECT id FROM users WHERE id = ?').get(userId) : db.prepare("SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1").get(); if (!targetUser) throw new Error('User not found. Please create a user first.'); const uid = targetUser.id; const alreadySeeded = db .prepare('SELECT COUNT(*) AS c FROM bills WHERE user_id = ? AND is_seeded = 1') .get(uid).c; if (alreadySeeded > 0) { const err = new Error( 'Demo data is already seeded for this account. Clear it first to re-seed.', ); err.status = 409; err.code = 'ALREADY_SEEDED'; throw err; } const counts = { bills: 0, categories: 0, groups: 0, payments: 0, transactions: 0, monthlyState: 0, planning: 0, budgets: 0, spendingRules: 0, merchantRules: 0, snowballPlans: 0, calendarTokens: 0, }; db.transaction(() => { ensureUserDefaultCategories(uid); // ── Categories (bill + spending), flagging ONLY ones this seed creates ── const catId = {}; const getOrCreateCategory = (name, { spending = false } = {}) => { const existing = db .prepare('SELECT id FROM categories WHERE user_id = ? AND name = ? COLLATE NOCASE') .get(uid, name); if (existing) { catId[name] = existing.id; return; } const r = db .prepare( 'INSERT INTO categories (user_id, name, spending_enabled, is_seeded) VALUES (?, ?, ?, 1)', ) .run(uid, name, spending ? 1 : 0); catId[name] = r.lastInsertRowid; counts.categories++; }; for (const name of BILL_CATEGORIES) getOrCreateCategory(name); for (const name of SPENDING_CATEGORIES) getOrCreateCategory(name, { spending: true }); // ── Category groups (organise spending categories) ── for (const [i, grp] of CATEGORY_GROUPS.entries()) { const r = db .prepare( 'INSERT INTO category_groups (user_id, name, sort_order, is_seeded) VALUES (?, ?, ?, 1)', ) .run(uid, grp.name, i); counts.groups++; for (const member of grp.members) { if (catId[member]) db.prepare('UPDATE categories SET group_id = ? WHERE id = ?').run( r.lastInsertRowid, catId[member], ); } } // ── Bills ── const insertBill = db.prepare(` INSERT INTO bills ( user_id, name, category_id, due_day, billing_cycle, cycle_type, expected_amount, autopay_enabled, autodraft_status, autopay_verified_at, interest_rate, current_balance, minimum_payment, snowball_order, snowball_include, snowball_exempt, is_subscription, subscription_type, website, has_2fa, active, is_seeded ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1) `); const billId = {}; for (const b of BILLS) { const verifiedAt = b.autopayVerified ? ymd(clampedDate(now.getFullYear(), now.getMonth(), Math.min(b.dueDay, 28))) : null; const r = insertBill.run( uid, b.name, catId[b.category] ?? null, b.dueDay, b.cycle, b.cycleType, toCents(b.amount), b.autopay ? 1 : 0, b.autopayVerified ? 'confirmed' : 'none', verifiedAt, b.interestRate ?? 0, b.currentBalance != null ? toCents(b.currentBalance) : null, b.minPayment != null ? toCents(b.minPayment) : null, b.snowballOrder ?? null, b.snowballInclude ?? 0, b.snowballExempt ?? 0, b.isSubscription ? 1 : 0, b.subscriptionType ?? null, b.website ?? null, b.has2fa ? 1 : 0, ); billId[b.name] = r.lastInsertRowid; counts.bills++; } // ── Demo bank source + account (for matched/unmatched transactions) ── const source = db .prepare( "INSERT INTO data_sources (user_id, type, provider, name, status, is_seeded) VALUES (?, 'manual', 'demo', 'Demo Bank', 'active', 1)", ) .run(uid); const sourceId = source.lastInsertRowid; const account = db .prepare( ` INSERT INTO financial_accounts (user_id, data_source_id, provider_account_id, name, org_name, account_type, currency, balance, available_balance, monitored, is_seeded) VALUES (?, ?, 'demo-checking', 'Everyday Checking', 'Demo Bank', 'checking', 'USD', ?, ?, 1, 1) `, ) .run(uid, sourceId, toCents(4210.55), toCents(4210.55)); const accountId = account.lastInsertRowid; let txnSeq = 0; const insertTxn = db.prepare(` INSERT INTO transactions (user_id, data_source_id, account_id, provider_transaction_id, source_type, transaction_type, posted_date, transacted_at, amount, currency, description, payee, category, matched_bill_id, match_status, spending_category_id, is_seeded) VALUES (?, ?, ?, ?, 'manual', ?, ?, ?, ?, 'USD', ?, ?, ?, ?, ?, ?, 1) `); const addTxn = ({ type = 'debit', date, amountDollars, description, payee, category = null, matchedBillId = null, matchStatus = 'unmatched', spendingCategoryId = null, }) => { const cents = type === 'credit' ? toCents(Math.abs(amountDollars)) : -toCents(Math.abs(amountDollars)); const r = insertTxn.run( uid, sourceId, accountId, `demo-${++txnSeq}`, type, date, date, cents, description, payee, category, matchedBillId, matchStatus, spendingCategoryId, ); counts.transactions++; return r.lastInsertRowid; }; // ── Payment history + matched bank transactions + monthly overrides ── const insertPayment = db.prepare(` INSERT INTO payments (bill_id, amount, paid_date, method, notes, balance_delta, payment_source, transaction_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); for (const b of BILLS) { const bid = billId[b.name]; for (const offset of cycleOffsets(b)) { const y = now.getFullYear(), m = now.getMonth() + offset; const payDate = clampedDate(y, m, b.dueDay); if (payDate > now) continue; // future occurrence → naturally "upcoming", no payment // Deliberate skipped month → recorded as a monthly_bill_state skip, no payment. if (b.name === SKIP.bill && offset === -SKIP.monthsAgo) { db.prepare( 'INSERT INTO monthly_bill_state (bill_id, year, month, is_skipped) VALUES (?, ?, ?, 1)', ).run(bid, payDate.getFullYear(), payDate.getMonth() + 1); counts.monthlyState++; continue; } // Current month: leave the designated overdue bills unpaid (past due, no payment). if (offset === 0 && OVERDUE_THIS_MONTH.has(b.name)) continue; let amount = driftedAmount(b, offset); if (b.name === OVERRIDE.bill && offset === -OVERRIDE.monthsAgo) amount = OVERRIDE.actual; // Debt bills: split into interest + principal so the balance pays down. let balanceDelta = null; if (b.currentBalance) { const interest = Math.round( (toCents(b.currentBalance) * (b.interestRate ?? 0)) / 100 / 12, ); balanceDelta = -Math.max(0, toCents(amount) - interest); } // A subset of recent payments are auto-matched to a seeded bank transaction. let txnId = null, source_ = 'manual'; if (BANK_MATCHED.has(b.name) && offset >= -1) { txnId = addTxn({ date: ymd(payDate), amountDollars: amount, description: `${b.name.toUpperCase()} AUTOPAY`, payee: b.name, matchedBillId: bid, matchStatus: 'matched', }); source_ = 'transaction_match'; } insertPayment.run( bid, toCents(amount), ymd(payDate), 'demo', null, balanceDelta, source_, txnId, ); counts.payments++; } } // Amount-override state (actual recorded above; flag the month's actual_amount) + a note. { const od = clampedDate(now.getFullYear(), now.getMonth() - OVERRIDE.monthsAgo, 1); db.prepare( 'INSERT INTO monthly_bill_state (bill_id, year, month, actual_amount) VALUES (?, ?, ?, ?)', ).run(billId[OVERRIDE.bill], od.getFullYear(), od.getMonth() + 1, toCents(OVERRIDE.actual)); counts.monthlyState++; db.prepare( 'INSERT INTO monthly_bill_state (bill_id, year, month, notes) VALUES (?, ?, ?, ?)', ).run(billId[NOTE.bill], now.getFullYear(), now.getMonth() + 1, NOTE.text); counts.monthlyState++; // Snooze one overdue bill's alert (Overdue Command Center demo). const snoozeUntil = ymd(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 5)); db.prepare( 'INSERT INTO monthly_bill_state (bill_id, year, month, snoozed_until) VALUES (?, ?, ?, ?)', ).run(billId['Trash Service'], now.getFullYear(), now.getMonth() + 1, snoozeUntil); counts.monthlyState++; } // ── Unmatched bank transactions that look like bills → Matches suggestions ── for (const name of ['Gas Utility', 'City Water Dept']) { const b = BILLS.find((x) => x.name === name); const d = clampedDate(now.getFullYear(), now.getMonth(), b.dueDay); if (d <= now) addTxn({ date: ymd(d), amountDollars: b.amount, description: `${name.toUpperCase()} PMT`, payee: name, }); } // ── Un-tracked subscription charges → Subscriptions recommendations ── for (const s of UNTRACKED_SUBS) { const d = clampedDate(now.getFullYear(), now.getMonth(), s.day); if (d <= now) addTxn({ date: ymd(d), amountDollars: s.amount, description: `${s.payee} SUBSCRIPTION`, payee: s.payee, }); } // ── Everyday spending transactions (categorised) → Spending page ── for (const merchant of SPENDING_MERCHANTS) { const scid = catId[merchant.category] ?? null; merchant.amounts.forEach((amt, i) => { const d = clampedDate(now.getFullYear(), now.getMonth() - (i % 2), 2 + ((i * 5) % 26)); if (d <= now) addTxn({ date: ymd(d), amountDollars: amt, description: merchant.payee, payee: merchant.payee, spendingCategoryId: scid, }); }); } // ── Planning: monthly income + starting amounts (recent months) ── for (let off = -3; off <= 0; off++) { const d = new Date(now.getFullYear(), now.getMonth() + off, 1); const y = d.getFullYear(), mo = d.getMonth() + 1; const hasIncome = db .prepare('SELECT 1 FROM monthly_income WHERE user_id = ? AND year = ? AND month = ?') .get(uid, y, mo); if (!hasIncome) { db.prepare( 'INSERT INTO monthly_income (user_id, year, month, label, amount, is_seeded) VALUES (?, ?, ?, ?, ?, 1)', ).run(uid, y, mo, 'Paycheck', toCents(5200)); counts.planning++; } const hasStart = db .prepare( 'SELECT 1 FROM monthly_starting_amounts WHERE user_id = ? AND year = ? AND month = ?', ) .get(uid, y, mo); if (!hasStart) { db.prepare( 'INSERT INTO monthly_starting_amounts (user_id, year, month, first_amount, fifteenth_amount, is_seeded) VALUES (?, ?, ?, ?, ?, 1)', ).run(uid, y, mo, toCents(3200), toCents(2800)); counts.planning++; } } // ── Spending budgets + auto-categorise rules ── for (const [cat, amount] of Object.entries(SPENDING_BUDGETS)) { if (!catId[cat]) continue; db.prepare( 'INSERT INTO spending_budgets (user_id, category_id, year, month, amount, is_seeded) VALUES (?, ?, ?, ?, ?, 1)', ).run(uid, catId[cat], now.getFullYear(), now.getMonth() + 1, toCents(amount)); counts.budgets++; } for (const rule of SPENDING_RULES) { if (!catId[rule.category]) continue; db.prepare( 'INSERT INTO spending_category_rules (user_id, category_id, merchant, is_seeded) VALUES (?, ?, ?, 1)', ).run(uid, catId[rule.category], rule.merchant); counts.spendingRules++; } // ── Snowball: an active plan + the user's extra-payment setting ── const debtBills = BILLS.filter((b) => b.snowballInclude).sort( (a, b) => (a.snowballOrder ?? 99) - (b.snowballOrder ?? 99), ); const snapshot = JSON.stringify({ method: 'snowball', extra_payment_cents: toCents(200), order: debtBills.map((b) => ({ bill_id: billId[b.name], name: b.name, balance_cents: toCents(b.currentBalance), })), }); db.prepare( `INSERT INTO snowball_plans (user_id, name, method, status, extra_payment, plan_snapshot, is_seeded) VALUES (?, 'My Debt Snowball', 'snowball', 'active', ?, ?, 1)`, ).run(uid, toCents(200), snapshot); counts.snowballPlans++; // Only set the extra-payment if the user hasn't chosen one (don't clobber real data). db.prepare( 'UPDATE users SET snowball_extra_payment = ? WHERE id = ? AND COALESCE(snowball_extra_payment, 0) = 0', ).run(toCents(200), uid); // ── Merchant → bill auto-match rules (cascade with their bill) ── for (const name of ['Netflix', 'Spotify']) { db.prepare( 'INSERT INTO bill_merchant_rules (user_id, bill_id, merchant) VALUES (?, ?, ?)', ).run(uid, billId[name], name.toUpperCase()); counts.merchantRules++; } // ── Calendar feed subscription token ── const crypto = require('crypto'); db.prepare( 'INSERT INTO calendar_tokens (user_id, token, label, active, is_seeded) VALUES (?, ?, ?, 1, 1)', ).run(uid, crypto.randomBytes(24).toString('hex'), 'Demo Calendar Feed'); counts.calendarTokens++; })(); return { billsCreated: counts.bills, categoriesCreated: counts.categories, paymentsCreated: counts.payments, transactionsCreated: counts.transactions, counts, }; } // CLI entry if (require.main === module) { try { const result = seedDemoData(); console.log('\n✅ Seed complete:', JSON.stringify(result.counts, null, 2)); process.exit(0); } catch (err) { console.error('Seed failed:', err.message); process.exit(err.code === 'ALREADY_SEEDED' ? 0 : 1); } } module.exports = { seedDemoData };