ledgerone_backend/seed-demo.mjs
2026-03-14 08:51:16 -04:00

408 lines
21 KiB
JavaScript

/**
* LedgerOne Demo Account Seed Script
* Creates a fully-populated demo account for testing all features.
*
* Usage: node seed-demo.mjs
* Creds: demo@ledgerone.app / Demo1234!
*/
import { PrismaClient } from "@prisma/client";
import * as crypto from "crypto";
const prisma = new PrismaClient();
const DEMO_EMAIL = "demo@ledgerone.app";
const DEMO_PASSWORD = "Demo1234!";
function hashPassword(password) {
const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto.pbkdf2Sync(password, salt, 100_000, 64, "sha512").toString("hex");
return `${salt}:${hash}`;
}
function daysAgo(n) {
const d = new Date();
d.setHours(12, 0, 0, 0);
d.setDate(d.getDate() - n);
return d;
}
async function main() {
console.log("🧹 Cleaning up existing demo account...");
const existing = await prisma.user.findUnique({ where: { email: DEMO_EMAIL } });
if (existing) {
const uid = existing.id;
// Delete leaf models first, then parents
await prisma.auditLog.deleteMany({ where: { userId: uid } });
await prisma.refreshToken.deleteMany({ where: { userId: uid } });
await prisma.emailVerificationToken.deleteMany({ where: { userId: uid } });
await prisma.passwordResetToken.deleteMany({ where: { userId: uid } });
await prisma.subscription.deleteMany({ where: { userId: uid } });
await prisma.exportLog.deleteMany({ where: { userId: uid } });
await prisma.googleConnection.deleteMany({ where: { userId: uid } });
const taxReturns = await prisma.taxReturn.findMany({ where: { userId: uid } });
for (const tr of taxReturns) {
await prisma.taxDocument.deleteMany({ where: { taxReturnId: tr.id } });
}
await prisma.taxReturn.deleteMany({ where: { userId: uid } });
const accounts = await prisma.account.findMany({ where: { userId: uid } });
for (const account of accounts) {
const txRaws = await prisma.transactionRaw.findMany({ where: { accountId: account.id } });
for (const tx of txRaws) {
await prisma.ruleExecution.deleteMany({ where: { transactionId: tx.id } });
await prisma.transactionDerived.deleteMany({ where: { rawTransactionId: tx.id } });
}
await prisma.transactionRaw.deleteMany({ where: { accountId: account.id } });
}
await prisma.account.deleteMany({ where: { userId: uid } });
const rules = await prisma.rule.findMany({ where: { userId: uid } });
for (const rule of rules) {
await prisma.ruleExecution.deleteMany({ where: { ruleId: rule.id } });
}
await prisma.rule.deleteMany({ where: { userId: uid } });
await prisma.user.delete({ where: { id: uid } });
console.log(" ✓ Removed previous demo account");
}
// ── User ──────────────────────────────────────────────────────────────────
console.log("\n👤 Creating demo user...");
const user = await prisma.user.create({
data: {
email: DEMO_EMAIL,
passwordHash: hashPassword(DEMO_PASSWORD),
fullName: "Alex Chen",
emailVerified: true, // skip email verification step
companyName: "LedgerOne Demo Corp",
city: "San Francisco",
state: "CA",
country: "US",
},
});
console.log(`${user.email} (id: ${user.id})`);
// ── Subscription ──────────────────────────────────────────────────────────
await prisma.subscription.create({
data: {
userId: user.id,
plan: "pro",
currentPeriodEnd: new Date(Date.now() + 30 * 86_400_000),
cancelAtPeriodEnd: false,
},
});
console.log(" ✓ Pro subscription");
// ── Accounts ──────────────────────────────────────────────────────────────
console.log("\n🏦 Creating accounts...");
const checking = await prisma.account.create({
data: {
userId: user.id,
institutionName: "Chase Bank",
accountType: "checking",
mask: "4521",
currentBalance: 12847.53,
availableBalance: 12347.53,
isoCurrencyCode: "USD",
isActive: true,
},
});
const credit = await prisma.account.create({
data: {
userId: user.id,
institutionName: "American Express",
accountType: "credit",
mask: "2834",
currentBalance: -2341.88, // negative = you owe this
availableBalance: 7658.12,
isoCurrencyCode: "USD",
isActive: true,
},
});
const savings = await prisma.account.create({
data: {
userId: user.id,
institutionName: "Ally Bank",
accountType: "savings",
mask: "9012",
currentBalance: 28500.00,
availableBalance: 28500.00,
isoCurrencyCode: "USD",
isActive: true,
},
});
console.log(" ✓ Chase Checking *4521");
console.log(" ✓ Amex Credit *2834");
console.log(" ✓ Ally Savings *9012");
// ── Transactions ──────────────────────────────────────────────────────────
// Convention: positive = money leaving (expense), negative = money entering (income/deposit)
const txDataset = [
// ── INCOME (checking) ──
{ acct: checking, d: 2, amt: -5800.00, desc: "PAYROLL DIRECT DEPOSIT - ACME CORP", cat: "Income" },
{ acct: checking, d: 32, amt: -5800.00, desc: "PAYROLL DIRECT DEPOSIT - ACME CORP", cat: "Income" },
{ acct: checking, d: 62, amt: -5800.00, desc: "PAYROLL DIRECT DEPOSIT - ACME CORP", cat: "Income" },
{ acct: checking, d: 92, amt: -5800.00, desc: "PAYROLL DIRECT DEPOSIT - ACME CORP", cat: "Income" },
{ acct: checking, d: 122, amt: -5800.00, desc: "PAYROLL DIRECT DEPOSIT - ACME CORP", cat: "Income" },
{ acct: checking, d: 152, amt: -5800.00, desc: "PAYROLL DIRECT DEPOSIT - ACME CORP", cat: "Income" },
{ acct: savings, d: 5, amt: -200.00, desc: "INTEREST EARNED - ALLY BANK", cat: "Income" },
{ acct: savings, d: 35, amt: -194.50, desc: "INTEREST EARNED - ALLY BANK", cat: "Income" },
{ acct: savings, d: 65, amt: -201.20, desc: "INTEREST EARNED - ALLY BANK", cat: "Income" },
// ── RENT (checking) ──
{ acct: checking, d: 5, amt: 2200.00, desc: "ACH - RENT - HARBOR VIEW APARTMENTS", cat: "Rent & Mortgage" },
{ acct: checking, d: 35, amt: 2200.00, desc: "ACH - RENT - HARBOR VIEW APARTMENTS", cat: "Rent & Mortgage" },
{ acct: checking, d: 65, amt: 2200.00, desc: "ACH - RENT - HARBOR VIEW APARTMENTS", cat: "Rent & Mortgage" },
{ acct: checking, d: 95, amt: 2200.00, desc: "ACH - RENT - HARBOR VIEW APARTMENTS", cat: "Rent & Mortgage" },
{ acct: checking, d: 125, amt: 2200.00, desc: "ACH - RENT - HARBOR VIEW APARTMENTS", cat: "Rent & Mortgage" },
{ acct: checking, d: 155, amt: 2200.00, desc: "ACH - RENT - HARBOR VIEW APARTMENTS", cat: "Rent & Mortgage" },
// ── UTILITIES (checking) ──
{ acct: checking, d: 8, amt: 94.50, desc: "PG&E ELECTRIC BILL PAYMENT", cat: "Utilities" },
{ acct: checking, d: 38, amt: 87.20, desc: "PG&E ELECTRIC BILL PAYMENT", cat: "Utilities" },
{ acct: checking, d: 68, amt: 112.80, desc: "PG&E ELECTRIC BILL PAYMENT", cat: "Utilities" },
{ acct: checking, d: 98, amt: 103.40, desc: "PG&E ELECTRIC BILL PAYMENT", cat: "Utilities" },
{ acct: checking, d: 10, amt: 65.00, desc: "COMCAST XFINITY INTERNET", cat: "Utilities" },
{ acct: checking, d: 40, amt: 65.00, desc: "COMCAST XFINITY INTERNET", cat: "Utilities" },
{ acct: checking, d: 70, amt: 65.00, desc: "COMCAST XFINITY INTERNET", cat: "Utilities" },
{ acct: checking, d: 100, amt: 65.00, desc: "COMCAST XFINITY INTERNET", cat: "Utilities" },
// ── GROCERIES (checking) ──
{ acct: checking, d: 3, amt: 127.43, desc: "WHOLE FOODS MARKET #1234 SAN FRANCISCO", cat: "Groceries" },
{ acct: checking, d: 11, amt: 89.22, desc: "TRADER JOE S #456 SF", cat: "Groceries" },
{ acct: checking, d: 18, amt: 145.67, desc: "WHOLE FOODS MARKET #1234 SAN FRANCISCO", cat: "Groceries" },
{ acct: checking, d: 25, amt: 73.11, desc: "SAFEWAY #789", cat: "Groceries" },
{ acct: checking, d: 33, amt: 118.54, desc: "WHOLE FOODS MARKET #1234 SAN FRANCISCO", cat: "Groceries" },
{ acct: checking, d: 45, amt: 92.30, desc: "TRADER JOE S #456 SF", cat: "Groceries" },
{ acct: checking, d: 55, amt: 131.20, desc: "WHOLE FOODS MARKET #1234 SAN FRANCISCO", cat: "Groceries" },
{ acct: checking, d: 70, amt: 85.75, desc: "SAFEWAY #789", cat: "Groceries" },
{ acct: checking, d: 82, amt: 104.88, desc: "WHOLE FOODS MARKET #1234 SAN FRANCISCO", cat: "Groceries" },
{ acct: checking, d: 105, amt: 76.42, desc: "TRADER JOE S #456 SF", cat: "Groceries" },
// ── HEALTHCARE (checking) ──
{ acct: checking, d: 15, amt: 30.00, desc: "CVS PHARMACY #1122", cat: "Healthcare" },
{ acct: checking, d: 60, amt: 250.00, desc: "KAISER PERMANENTE COPAY", cat: "Healthcare" },
{ acct: checking, d: 110, amt: 45.00, desc: "CVS PHARMACY #1122", cat: "Healthcare" },
{ acct: checking, d: 140, amt: 180.00, desc: "UCSF MEDICAL CENTER", cat: "Healthcare" },
// ── DINING (credit) ──
{ acct: credit, d: 1, amt: 68.40, desc: "NOBU RESTAURANT SF", cat: "Dining & Restaurants" },
{ acct: credit, d: 4, amt: 14.50, desc: "STARBUCKS #3421 SAN FRANCISCO CA", cat: "Dining & Restaurants" },
{ acct: credit, d: 7, amt: 42.80, desc: "CHIPOTLE MEXICAN GRILL", cat: "Dining & Restaurants" },
{ acct: credit, d: 13, amt: 89.20, desc: "MOURAD RESTAURANT SF", cat: "Dining & Restaurants" },
{ acct: credit, d: 16, amt: 23.60, desc: "SWEETGREEN #55 SF", cat: "Dining & Restaurants" },
{ acct: credit, d: 22, amt: 112.50, desc: "BENU RESTAURANT SAN FRANCISCO", cat: "Dining & Restaurants" },
{ acct: credit, d: 29, amt: 18.90, desc: "STARBUCKS #3421 SAN FRANCISCO CA", cat: "Dining & Restaurants" },
{ acct: credit, d: 36, amt: 55.30, desc: "IN-N-OUT BURGER #77", cat: "Dining & Restaurants" },
{ acct: credit, d: 48, amt: 78.10, desc: "NOBU RESTAURANT SF", cat: "Dining & Restaurants" },
{ acct: credit, d: 75, amt: 32.40, desc: "CHIPOTLE MEXICAN GRILL", cat: "Dining & Restaurants" },
{ acct: credit, d: 90, amt: 44.20, desc: "THE FRENCH LAUNDRY YOUNTVILLE", cat: "Dining & Restaurants" },
// ── SUBSCRIPTIONS (credit) ──
{ acct: credit, d: 9, amt: 15.99, desc: "NETFLIX.COM SUBSCRIPTION", cat: "Subscriptions" },
{ acct: credit, d: 9, amt: 9.99, desc: "SPOTIFY PREMIUM", cat: "Subscriptions" },
{ acct: credit, d: 9, amt: 14.99, desc: "OPENAI CHATGPT PLUS", cat: "Subscriptions" },
{ acct: credit, d: 9, amt: 19.99, desc: "GITHUB COPILOT SUBSCRIPTION", cat: "Subscriptions" },
{ acct: credit, d: 39, amt: 15.99, desc: "NETFLIX.COM SUBSCRIPTION", cat: "Subscriptions" },
{ acct: credit, d: 39, amt: 9.99, desc: "SPOTIFY PREMIUM", cat: "Subscriptions" },
{ acct: credit, d: 39, amt: 14.99, desc: "OPENAI CHATGPT PLUS", cat: "Subscriptions" },
{ acct: credit, d: 39, amt: 19.99, desc: "GITHUB COPILOT SUBSCRIPTION", cat: "Subscriptions" },
{ acct: credit, d: 69, amt: 15.99, desc: "NETFLIX.COM SUBSCRIPTION", cat: "Subscriptions" },
{ acct: credit, d: 69, amt: 9.99, desc: "SPOTIFY PREMIUM", cat: "Subscriptions" },
// ── ENTERTAINMENT (credit) ──
{ acct: credit, d: 20, amt: 24.99, desc: "AMC THEATRES TICKET SAN FRANCISCO", cat: "Entertainment" },
{ acct: credit, d: 85, amt: 189.00, desc: "GOLDEN STATE WARRIORS - CHASE CENTER", cat: "Entertainment" },
{ acct: credit, d: 130, amt: 95.00, desc: "TICKETMASTER CONCERT TICKETS", cat: "Entertainment" },
// ── TRANSPORTATION (credit) ──
{ acct: credit, d: 2, amt: 18.40, desc: "UBER TRIP SAN FRANCISCO CA", cat: "Transportation" },
{ acct: credit, d: 6, amt: 22.10, desc: "UBER TRIP SAN FRANCISCO CA", cat: "Transportation" },
{ acct: credit, d: 13, amt: 15.80, desc: "LYFT RIDE SAN FRANCISCO", cat: "Transportation" },
{ acct: credit, d: 27, amt: 89.00, desc: "SHELL OIL GAS STATION #4421", cat: "Transportation" },
{ acct: credit, d: 48, amt: 76.50, desc: "SHELL OIL GAS STATION #4421", cat: "Transportation" },
{ acct: credit, d: 58, amt: 21.60, desc: "UBER TRIP SAN FRANCISCO CA", cat: "Transportation" },
{ acct: credit, d: 72, amt: 88.00, desc: "BART CLIPPER CARD RELOAD", cat: "Transportation" },
// ── SHOPPING (credit) ──
{ acct: credit, d: 14, amt: 243.50, desc: "AMAZON.COM PURCHASE", cat: "Shopping" },
{ acct: credit, d: 30, amt: 89.99, desc: "AMAZON.COM PURCHASE", cat: "Shopping" },
{ acct: credit, d: 53, amt: 1450.00, desc: "APPLE.COM/BILL - IPAD PRO", cat: "Shopping" },
{ acct: credit, d: 66, amt: 178.00, desc: "NORDSTROM #0234 SF", cat: "Shopping" },
{ acct: credit, d: 90, amt: 340.00, desc: "BEST BUY #1234 DALY CITY", cat: "Shopping" },
{ acct: credit, d: 115, amt: 67.50, desc: "AMAZON.COM PURCHASE", cat: "Shopping" },
// ── CREDIT CARD PAYMENTS (checking) ──
{ acct: checking, d: 20, amt: 1800.00, desc: "AMEX AUTOPAY - CREDIT CARD PAYMENT", cat: "Transfer" },
{ acct: checking, d: 50, amt: 2100.00, desc: "AMEX AUTOPAY - CREDIT CARD PAYMENT", cat: "Transfer" },
{ acct: checking, d: 80, amt: 1650.00, desc: "AMEX AUTOPAY - CREDIT CARD PAYMENT", cat: "Transfer" },
{ acct: checking, d: 110, amt: 1950.00, desc: "AMEX AUTOPAY - CREDIT CARD PAYMENT", cat: "Transfer" },
// ── SAVINGS TRANSFERS (checking → savings) ──
{ acct: checking, d: 3, amt: 500.00, desc: "TRANSFER TO ALLY SAVINGS *9012", cat: "Transfer" },
{ acct: checking, d: 33, amt: 500.00, desc: "TRANSFER TO ALLY SAVINGS *9012", cat: "Transfer" },
{ acct: checking, d: 63, amt: 500.00, desc: "TRANSFER TO ALLY SAVINGS *9012", cat: "Transfer" },
];
console.log("\n💳 Creating transactions...");
let txCount = 0;
for (const [i, tx] of txDataset.entries()) {
const raw = await prisma.transactionRaw.create({
data: {
accountId: tx.acct.id,
bankTransactionId: `demo-${user.id.slice(0, 8)}-${String(i).padStart(3, "0")}`,
date: daysAgo(tx.d),
amount: tx.amt,
description: tx.desc,
source: "manual",
rawPayload: { demo: true },
},
});
await prisma.transactionDerived.create({
data: {
rawTransactionId: raw.id,
userCategory: tx.cat,
isHidden: false,
modifiedBy: user.id,
},
});
txCount++;
}
console.log(`${txCount} transactions created`);
// ── Categorization Rules ──────────────────────────────────────────────────
console.log("\n📋 Creating categorization rules...");
const rules = [
{
name: "Whole Foods → Groceries",
priority: 10,
conditions: { operator: "AND", filters: [{ field: "description", op: "contains", value: "WHOLE FOODS" }] },
actions: [{ type: "setCategory", value: "Groceries" }],
},
{
name: "Trader Joe's → Groceries",
priority: 10,
conditions: { operator: "AND", filters: [{ field: "description", op: "contains", value: "TRADER JOE" }] },
actions: [{ type: "setCategory", value: "Groceries" }],
},
{
name: "Netflix → Subscriptions",
priority: 10,
conditions: { operator: "AND", filters: [{ field: "description", op: "contains", value: "NETFLIX" }] },
actions: [{ type: "setCategory", value: "Subscriptions" }],
},
{
name: "Uber/Lyft → Transportation",
priority: 10,
conditions: { operator: "OR", filters: [
{ field: "description", op: "contains", value: "UBER" },
{ field: "description", op: "contains", value: "LYFT" },
]},
actions: [{ type: "setCategory", value: "Transportation" }],
},
{
name: "Amazon → Shopping",
priority: 10,
conditions: { operator: "AND", filters: [{ field: "description", op: "contains", value: "AMAZON" }] },
actions: [{ type: "setCategory", value: "Shopping" }],
},
{
name: "Payroll → Income",
priority: 20,
conditions: { operator: "AND", filters: [{ field: "description", op: "contains", value: "PAYROLL" }] },
actions: [{ type: "setCategory", value: "Income" }],
},
{
name: "Starbucks → Dining",
priority: 10,
conditions: { operator: "AND", filters: [{ field: "description", op: "contains", value: "STARBUCKS" }] },
actions: [{ type: "setCategory", value: "Dining & Restaurants" }],
},
{
name: "Large purchase note (>$500)",
priority: 5,
conditions: { operator: "AND", filters: [{ field: "amount", op: "gt", value: 500 }] },
actions: [{ type: "addNote", value: "Large purchase — review for tax deduction" }],
},
];
for (const rule of rules) {
await prisma.rule.create({
data: {
userId: user.id,
name: rule.name,
priority: rule.priority,
conditions: rule.conditions,
actions: rule.actions,
isActive: true,
},
});
}
console.log(`${rules.length} rules created`);
// ── Draft Tax Return ──────────────────────────────────────────────────────
console.log("\n📄 Creating draft tax return...");
await prisma.taxReturn.create({
data: {
userId: user.id,
taxYear: 2025,
filingType: "individual",
jurisdictions: ["federal", "CA"],
status: "draft",
summary: {
totalIncome: 69600,
totalExpenses: 42580,
netIncome: 27020,
categories: {
"Income": 69600.00,
"Rent & Mortgage": 13200.00,
"Groceries": 1044.52,
"Dining & Restaurants": 679.90,
"Transportation": 411.40,
"Subscriptions": 165.93,
"Shopping": 2469.99,
"Healthcare": 505.00,
"Utilities": 755.90,
"Entertainment": 308.99,
"Transfer": 10500.00,
},
},
},
});
console.log(" ✓ Draft 2025 tax return");
// ── Audit Log ─────────────────────────────────────────────────────────────
await prisma.auditLog.create({
data: { userId: user.id, action: "auth.register", metadata: { email: DEMO_EMAIL, source: "seed-script" } },
});
// ── Summary ───────────────────────────────────────────────────────────────
console.log(`
╔══════════════════════════════════════════════╗
║ Demo Account Ready ║
╠══════════════════════════════════════════════╣
║ Email: demo@ledgerone.app ║
║ Password: Demo1234! ║
╠══════════════════════════════════════════════╣
║ Features populated: ║
║ ✓ Email verified (no confirmation needed) ║
║ ✓ Pro subscription (30-day period) ║
║ ✓ 3 accounts (checking/credit/savings) ║
║ ✓ ${String(txCount).padEnd(3)} transactions (6 months of data) ║
║ ✓ ${String(rules.length).padEnd(3)} categorization rules ║
║ ✓ Draft 2025 tax return ║
║ ✓ 2FA disabled (enable via Settings → 2FA) ║
╚══════════════════════════════════════════════╝
`);
}
main()
.catch((e) => {
console.error("❌ Seed failed:", e.message);
process.exit(1);
})
.finally(() => prisma.$disconnect());