460 lines
12 KiB
JavaScript
460 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Seed Demo Data Script
|
|
* Creates realistic bills across common 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');
|
|
// Money columns (expected_amount, current_balance, minimum_payment) are stored as
|
|
// integer cents since migration v1.03 — convert the demo dollars before insert.
|
|
const { toCents } = require('../utils/money');
|
|
|
|
const CATEGORIES = [
|
|
'Utilities',
|
|
'Housing',
|
|
'Insurance',
|
|
'Subscriptions',
|
|
'Transportation',
|
|
'Healthcare',
|
|
'Credit Cards',
|
|
'Loans',
|
|
'Entertainment',
|
|
];
|
|
|
|
// billing_cycle CHECK: 'monthly' | 'quarterly' | 'annually' | 'irregular'
|
|
// cycle_type CHECK: 'monthly' | 'weekly' | 'biweekly' | 'quarterly' | 'annual'
|
|
const BILLS = [
|
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
{
|
|
name: 'Electric Company',
|
|
category: 'Utilities',
|
|
amount: 85,
|
|
dueDay: 15,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'Gas Utility',
|
|
category: 'Utilities',
|
|
amount: 35,
|
|
dueDay: 12,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'City Water Dept',
|
|
category: 'Utilities',
|
|
amount: 45,
|
|
dueDay: 20,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'Internet Provider',
|
|
category: 'Utilities',
|
|
amount: 70,
|
|
dueDay: 18,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'Cell Phone',
|
|
category: 'Utilities',
|
|
amount: 65,
|
|
dueDay: 25,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
currentBalance: 648, // remaining on 24-month 0% carrier installment plan
|
|
minPayment: 27,
|
|
snowballOrder: 0, // smallest balance — first in snowball
|
|
snowballInclude: 1,
|
|
},
|
|
{
|
|
name: 'Trash Service',
|
|
category: 'Utilities',
|
|
amount: 25,
|
|
dueDay: 28,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
|
|
// ── Housing ───────────────────────────────────────────────────────────────
|
|
{
|
|
name: 'Mortgage',
|
|
category: 'Housing',
|
|
amount: 1450, // paying $250/mo above minimum → extra principal paydown
|
|
dueDay: 1,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 3.25,
|
|
currentBalance: 185000,
|
|
minPayment: 1200,
|
|
snowballOrder: null, // not included in snowball — too large, handled separately
|
|
snowballInclude: 0,
|
|
snowballExempt: 1,
|
|
},
|
|
|
|
// ── Insurance ─────────────────────────────────────────────────────────────
|
|
{
|
|
name: 'Car Insurance',
|
|
category: 'Insurance',
|
|
amount: 120,
|
|
dueDay: 5,
|
|
cycle: 'quarterly',
|
|
cycleType: 'quarterly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'Homeowners Insurance',
|
|
category: 'Insurance',
|
|
amount: 300,
|
|
dueDay: 10,
|
|
cycle: 'annually',
|
|
cycleType: 'annual',
|
|
autopay: false,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'Health Insurance',
|
|
category: 'Healthcare',
|
|
amount: 200,
|
|
dueDay: 1,
|
|
cycle: 'quarterly',
|
|
cycleType: 'quarterly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
{
|
|
name: 'Dental Insurance',
|
|
category: 'Healthcare',
|
|
amount: 40,
|
|
dueDay: 15,
|
|
cycle: 'quarterly',
|
|
cycleType: 'quarterly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
},
|
|
|
|
// ── Credit Cards (Snowball — ordered smallest→largest balance) ─────────────
|
|
{
|
|
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 (Snowball — ordered by balance after credit cards) ──────────────
|
|
{
|
|
name: 'Car Payment',
|
|
category: 'Loans',
|
|
amount: 425, // paying $75/mo above contractual minimum to shorten payoff
|
|
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,
|
|
interestRate: 0,
|
|
isSubscription: true,
|
|
subscriptionType: 'streaming',
|
|
website: 'https://netflix.com',
|
|
},
|
|
{
|
|
name: 'Spotify',
|
|
category: 'Subscriptions',
|
|
amount: 9.99,
|
|
dueDay: 14,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
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,
|
|
interestRate: 0,
|
|
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,
|
|
interestRate: 0,
|
|
isSubscription: true,
|
|
subscriptionType: 'shopping',
|
|
website: 'https://amazon.com',
|
|
},
|
|
{
|
|
name: 'Apple iCloud+',
|
|
category: 'Subscriptions',
|
|
amount: 2.99,
|
|
dueDay: 18,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
isSubscription: true,
|
|
subscriptionType: 'cloud',
|
|
website: 'https://icloud.com',
|
|
},
|
|
{
|
|
name: 'Gym Membership',
|
|
category: 'Subscriptions',
|
|
amount: 45,
|
|
dueDay: 10,
|
|
cycle: 'monthly',
|
|
cycleType: 'monthly',
|
|
autopay: true,
|
|
interestRate: 0,
|
|
isSubscription: true,
|
|
subscriptionType: 'fitness',
|
|
},
|
|
|
|
// ── Entertainment ─────────────────────────────────────────────────────────
|
|
{
|
|
name: 'Grocery Delivery',
|
|
category: 'Entertainment',
|
|
amount: 30,
|
|
dueDay: 3,
|
|
cycle: 'irregular',
|
|
cycleType: 'monthly',
|
|
autopay: false,
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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 demo categories if they don't exist
|
|
const categoriesMap = {};
|
|
let categoriesCreated = 0;
|
|
|
|
for (const categoryName of CATEGORIES) {
|
|
const before = db.prepare('SELECT id FROM categories WHERE user_id = ? AND LOWER(name) = LOWER(?)').get(targetUserId, categoryName);
|
|
const category = getCategoryByName(db, targetUserId, categoryName);
|
|
categoriesMap[categoryName] = category.id;
|
|
db.prepare('UPDATE categories SET is_seeded = 1 WHERE id = ?').run(category.id);
|
|
if (!before) categoriesCreated++;
|
|
}
|
|
|
|
// Create bills
|
|
let billsCreated = 0;
|
|
const insertBill = db.prepare(`
|
|
INSERT INTO bills (
|
|
user_id, name, category_id, due_day,
|
|
billing_cycle, cycle_type,
|
|
expected_amount, autopay_enabled, 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)
|
|
`);
|
|
|
|
for (const billData of BILLS) {
|
|
const category = categoriesMap[billData.category];
|
|
|
|
try {
|
|
insertBill.run(
|
|
targetUserId,
|
|
billData.name,
|
|
category,
|
|
billData.dueDay || Math.floor(Math.random() * 28) + 1,
|
|
billData.cycle || 'monthly',
|
|
billData.cycleType || 'monthly',
|
|
toCents(billData.amount),
|
|
billData.autopay !== undefined ? (billData.autopay ? 1 : 0) : 0,
|
|
billData.interestRate ?? 0,
|
|
billData.currentBalance != null ? toCents(billData.currentBalance) : null,
|
|
billData.minPayment != null ? toCents(billData.minPayment) : null,
|
|
billData.snowballOrder ?? null,
|
|
billData.snowballInclude ?? 0,
|
|
billData.snowballExempt ?? 0,
|
|
billData.isSubscription ? 1 : 0,
|
|
billData.subscriptionType ?? null,
|
|
billData.website ?? null,
|
|
billData.has2fa ? 1 : 0,
|
|
);
|
|
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 };
|