2026-05-09 13:03:36 -05:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* Seed Demo Data Script
|
|
|
|
|
* Creates 20 realistic bills across 8 categories for demo purposes.
|
|
|
|
|
* Idempotent: can be run multiple times safely.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
// Use DB_PATH from env or default to db/bills.db
|
|
|
|
|
const DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'db', 'bills.db');
|
|
|
|
|
|
|
|
|
|
// Import database helper
|
|
|
|
|
const { getDb, ensureUserDefaultCategories } = require('../db/database');
|
|
|
|
|
|
|
|
|
|
const CATEGORIES = [
|
|
|
|
|
'Utilities',
|
|
|
|
|
'Housing',
|
|
|
|
|
'Insurance',
|
|
|
|
|
'Subscriptions',
|
|
|
|
|
'Transportation',
|
|
|
|
|
'Healthcare',
|
2026-05-14 02:11:54 -05:00
|
|
|
'Credit Cards',
|
|
|
|
|
'Loans',
|
2026-05-09 13:03:36 -05:00
|
|
|
'Entertainment',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Real-world bill names with realistic data
|
|
|
|
|
const BILLS = [
|
|
|
|
|
{ name: 'Electric Company', category: 'Utilities', amount: 85, dueDay: 15, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'City Water Dept', category: 'Utilities', amount: 45, dueDay: 20, cycle: 'monthly', autopay: true, interestRate: 0 },
|
2026-05-14 02:11:54 -05:00
|
|
|
{ name: 'Mortgage', category: 'Housing', amount: 1200, dueDay: 1, cycle: 'monthly', autopay: true, interestRate: 3.25, currentBalance: 185000, minPayment: 1200, snowballOrder: 3, snowballInclude: 0 },
|
2026-05-09 13:03:36 -05:00
|
|
|
{ name: 'Car Insurance', category: 'Insurance', amount: 120, dueDay: 5, cycle: 'quarterly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Netflix', category: 'Subscriptions', amount: 15.99, dueDay: 22, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Gym Membership', category: 'Subscriptions', amount: 45, dueDay: 10, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Internet Provider', category: 'Utilities', amount: 70, dueDay: 18, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Cell Phone', category: 'Utilities', amount: 65, dueDay: 25, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Health Insurance', category: 'Healthcare', amount: 200, dueDay: 1, cycle: 'quarterly', autopay: true, interestRate: 0 },
|
2026-05-14 02:11:54 -05:00
|
|
|
{ name: 'Credit Card', category: 'Credit Cards', amount: 150, dueDay: 28, cycle: 'monthly', autopay: true, interestRate: 19.99, currentBalance: 2800, minPayment: 75, snowballOrder: 0, snowballInclude: 1 },
|
|
|
|
|
{ name: 'Student Loan', category: 'Loans', amount: 250, dueDay: 15, cycle: 'monthly', autopay: true, interestRate: 5.5, currentBalance: 12500, minPayment: 150, snowballOrder: 1, snowballInclude: 1 },
|
2026-05-09 13:03:36 -05:00
|
|
|
{ name: 'Gas Utility', category: 'Utilities', amount: 35, dueDay: 12, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Trash Service', category: 'Utilities', amount: 25, dueDay: 28, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Homeowners Insurance', category: 'Insurance', amount: 300, dueDay: 10, cycle: 'annually', autopay: false, interestRate: 0 },
|
2026-05-14 02:11:54 -05:00
|
|
|
{ name: 'Car Payment', category: 'Loans', amount: 350, dueDay: 22, cycle: 'monthly', autopay: true, interestRate: 4.5, currentBalance: 8400, minPayment: 350, snowballOrder: 2, snowballInclude: 1 },
|
2026-05-09 13:03:36 -05:00
|
|
|
{ name: 'Spotify', category: 'Entertainment', amount: 9.99, dueDay: 14, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Adobe Creative Cloud', category: 'Subscriptions', amount: 54.99, dueDay: 8, cycle: 'monthly', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Amazon Prime', category: 'Subscriptions', amount: 14.99, dueDay: 1, cycle: 'annually', autopay: true, interestRate: 0 },
|
|
|
|
|
{ name: 'Grocery Delivery', category: 'Entertainment', amount: 30, dueDay: 3, cycle: 'irregular', autopay: false, interestRate: 0 },
|
|
|
|
|
{ name: 'Dental Insurance', category: 'Healthcare', amount: 40, dueDay: 15, cycle: 'quarterly', autopay: true, interestRate: 0 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get or create a category by name for a user
|
|
|
|
|
*/
|
|
|
|
|
function getCategoryByName(db, userId, name) {
|
|
|
|
|
let category = db.prepare('SELECT id FROM categories WHERE user_id = ? AND LOWER(name) = LOWER(?)').get(userId, name);
|
|
|
|
|
if (!category) {
|
|
|
|
|
const result = db.prepare('INSERT INTO categories (user_id, name) VALUES (?, ?)').run(userId, name);
|
|
|
|
|
category = { id: result.lastInsertRowid };
|
|
|
|
|
}
|
|
|
|
|
return category;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate realistic random amounts based on type
|
|
|
|
|
*/
|
|
|
|
|
function getRandomAmount(min, max) {
|
|
|
|
|
const range = max - min;
|
|
|
|
|
const randomValue = Math.random() * range + min;
|
|
|
|
|
return Math.round(randomValue * 100) / 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Main seed function
|
|
|
|
|
* @param {number} [userId] - User ID to seed data for. If not provided, uses the first admin user.
|
|
|
|
|
*/
|
|
|
|
|
function seedDemoData(userId = null) {
|
|
|
|
|
const db = getDb();
|
|
|
|
|
|
|
|
|
|
// Check if data already exists for this user (if userId provided) or globally
|
|
|
|
|
let existingCheck;
|
|
|
|
|
if (userId !== null) {
|
|
|
|
|
existingCheck = db.prepare('SELECT COUNT(*) AS count FROM bills WHERE user_id = ?').get(userId);
|
|
|
|
|
} else {
|
|
|
|
|
existingCheck = db.prepare('SELECT COUNT(*) AS count FROM bills').get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existingCheck.count > 0) {
|
|
|
|
|
console.log(`⚠️ Found ${existingCheck.count} existing bills. Skipping seed to prevent duplicates.`);
|
|
|
|
|
console.log(' Run again with --force to overwrite.');
|
|
|
|
|
return { billsCreated: 0, categoriesCreated: 0, message: 'Data already exists' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get user (or admin if userId not provided)
|
|
|
|
|
let targetUser;
|
|
|
|
|
if (userId !== null) {
|
|
|
|
|
targetUser = db.prepare('SELECT id FROM users WHERE id = ?').get(userId);
|
|
|
|
|
} else {
|
|
|
|
|
targetUser = db.prepare('SELECT id FROM users WHERE role = ? ORDER BY id LIMIT 1', 'admin').get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!targetUser) {
|
|
|
|
|
throw new Error('User not found. Please create a user first.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetUserId = targetUser.id;
|
|
|
|
|
console.log(`📝 Seeding demo data for user: ${targetUserId}`);
|
|
|
|
|
|
|
|
|
|
// Ensure default categories exist for this user
|
|
|
|
|
ensureUserDefaultCategories(targetUserId);
|
|
|
|
|
|
|
|
|
|
// Create our 8 demo categories if they don't exist
|
|
|
|
|
const categoriesMap = {};
|
|
|
|
|
let categoriesCreated = 0;
|
|
|
|
|
|
|
|
|
|
for (const categoryName of CATEGORIES) {
|
|
|
|
|
const category = getCategoryByName(db, targetUserId, categoryName);
|
|
|
|
|
categoriesMap[categoryName] = category.id;
|
|
|
|
|
// Tag seeded categories
|
|
|
|
|
db.prepare('UPDATE categories SET is_seeded = 1 WHERE id = ?').run(category.id);
|
|
|
|
|
if (category.id > (db.prepare('SELECT id FROM categories WHERE user_id = ? AND name = ?').get(targetUserId, categoryName)?.id || 0)) {
|
|
|
|
|
categoriesCreated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create bills
|
|
|
|
|
let billsCreated = 0;
|
|
|
|
|
const insertBill = db.prepare(`
|
|
|
|
|
INSERT INTO bills (user_id, name, category_id, due_day, billing_cycle,
|
2026-05-14 02:11:54 -05:00
|
|
|
expected_amount, autopay_enabled, interest_rate,
|
|
|
|
|
current_balance, minimum_payment, snowball_order, snowball_include,
|
|
|
|
|
active, is_seeded)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1)
|
2026-05-09 13:03:36 -05:00
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
for (const billData of BILLS) {
|
|
|
|
|
const category = categoriesMap[billData.category];
|
|
|
|
|
|
|
|
|
|
// Use provided amount or generate random within range
|
|
|
|
|
const amount = billData.amount || getRandomAmount(15, 2500);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
insertBill.run(
|
2026-05-10 15:11:02 -05:00
|
|
|
targetUserId,
|
2026-05-09 13:03:36 -05:00
|
|
|
billData.name,
|
|
|
|
|
category,
|
|
|
|
|
billData.dueDay || Math.floor(Math.random() * 28) + 1,
|
|
|
|
|
billData.cycle || 'monthly',
|
|
|
|
|
amount,
|
|
|
|
|
billData.autopay !== undefined ? (billData.autopay ? 1 : 0) : Math.random() > 0.5 ? 1 : 0,
|
2026-05-14 02:11:54 -05:00
|
|
|
billData.interestRate || (Math.random() > 0.7 ? Math.round(Math.random() * 15 * 100) / 100 : 0),
|
|
|
|
|
billData.currentBalance ?? null,
|
|
|
|
|
billData.minPayment ?? null,
|
|
|
|
|
billData.snowballOrder ?? null,
|
|
|
|
|
billData.snowballInclude ?? 0
|
2026-05-09 13:03:36 -05:00
|
|
|
);
|
|
|
|
|
billsCreated++;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`Failed to create bill "${billData.name}":`, err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`✅ Created ${billsCreated} demo bills`);
|
|
|
|
|
console.log(`✅ Created ${categoriesCreated} demo categories`);
|
|
|
|
|
|
|
|
|
|
return { billsCreated, categoriesCreated };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run seed if called directly
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
try {
|
|
|
|
|
const result = seedDemoData();
|
|
|
|
|
console.log('\nSeed complete:', result);
|
|
|
|
|
process.exit(0);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Seed failed:', err.message);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { seedDemoData };
|