// Seeds 10 demo firms, each with one owner user + realistic activity. // Run from monorepo root: npx tsx scripts/seed-demo.ts // // Login: any of the emails below with password `Demo1234!` import path from 'node:path'; import { fileURLToPath } from 'node:url'; import dotenv from 'dotenv'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: path.resolve(__dirname, '../.env') }); import argon2 from 'argon2'; import { getDb, getPool, firms, users, clients, cases, timeEntries, invoices, invoiceItems, } from '@lawdesk/db'; // ─── Demo data pools ─────────────────────────────────────────────────────── const FIRMS = [ { name: 'Hartwell & Associates', plan: 'pro' as const, area: 'Family Law' }, { name: 'Brennan Law Group', plan: 'lifetime' as const, area: 'Personal Injury' }, { name: 'Cohen Legal Solutions', plan: 'starter' as const, area: 'Criminal Defense' }, { name: 'Davenport & Reed PLLC', plan: 'pro' as const, area: 'Real Estate' }, { name: 'Eastman Law Firm', plan: 'starter' as const, area: 'Employment' }, { name: 'Fairmont Legal Partners', plan: 'pro' as const, area: 'Estate Planning' }, { name: 'Gallo & Whitfield LLP', plan: 'lifetime' as const, area: 'Corporate' }, { name: 'Hayes Immigration Law', plan: 'starter' as const, area: 'Immigration' }, { name: 'Iverson & Marsh', plan: 'pro' as const, area: 'Bankruptcy' }, { name: 'Jensen Tax Law', plan: 'pro' as const, area: 'Tax' }, ]; const FIRST_NAMES = ['Emma', 'Liam', 'Olivia', 'Noah', 'Ava', 'Ethan', 'Sophia', 'Mason', 'Isabella', 'James', 'Mia', 'Lucas', 'Charlotte', 'Henry', 'Amelia']; const LAST_NAMES = ['Anderson', 'Brown', 'Carter', 'Davis', 'Evans', 'Foster', 'Garcia', 'Hill', 'Jackson', 'King', 'Lee', 'Morris', 'Nelson', 'Owens', 'Parker', 'Quinn', 'Rivera', 'Stone', 'Taylor', 'Walker']; const CASE_TITLES: Record = { 'Family Law': ['Divorce — assets division', 'Child custody modification', 'Prenuptial agreement', 'Adoption petition', 'Spousal support'], 'Personal Injury': ['Auto accident — rear-end', 'Slip and fall at retail', 'Workplace injury claim', 'Medical malpractice', 'Product liability'], 'Criminal Defense': ['DUI defense', 'Assault charge', 'Drug possession', 'White-collar fraud', 'Theft defense'], 'Real Estate': ['Commercial lease review', 'Title dispute', 'Zoning variance', 'Purchase agreement', 'Easement dispute'], 'Employment': ['Wrongful termination', 'Discrimination claim', 'Wage and hour dispute', 'Non-compete enforcement', 'Severance negotiation'], 'Estate Planning': ['Trust formation', 'Will drafting', 'Probate administration', 'Power of attorney', 'Estate dispute'], 'Corporate': ['M&A advisory', 'Shareholder agreement', 'Series A financing', 'IP licensing', 'Corporate restructuring'], 'Immigration': ['H-1B visa petition', 'Green card application', 'Asylum case', 'Naturalization', 'Family-based visa'], 'Bankruptcy': ['Chapter 7 filing', 'Chapter 13 reorganization', 'Creditor negotiation', 'Asset protection', 'Discharge defense'], 'Tax': ['IRS audit defense', 'Tax debt settlement', 'Estate tax planning', 'Business tax structuring', 'Tax court appeal'], }; const TIME_DESCRIPTIONS = [ 'Initial client consultation', 'Drafted demand letter', 'Reviewed discovery documents', 'Court appearance — motion hearing', 'Phone call with opposing counsel', 'Research case law', 'Prepared deposition outline', 'Client meeting — strategy review', 'Email correspondence', 'Filed motion to compel', 'Mediation session', 'Drafted settlement proposal', 'Reviewed contract terms', 'Prepared trial exhibits', 'Settlement conference', ]; // ─── Helpers ──────────────────────────────────────────────────────────────── function pick(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]!; } function randInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } function daysAgo(days: number): Date { const d = new Date(); d.setDate(d.getDate() - days); return d; } function slug(s: string): string { return s.toLowerCase().replace(/[^a-z]+/g, ''); } // ─── Safety guard ───────────────────────────────────────────────────────────── // This inserts 10 demo firms whose owner logins all use the public password `Demo1234!` // into whatever DATABASE_URL points at — which for this project is the PRODUCTION database. // Require an explicit opt-in so it can never run by accident. if (process.env.ALLOW_SEED !== '1') { const host = (() => { try { return new URL(process.env.DATABASE_URL ?? '').host || 'unknown'; } catch { return 'unknown'; } })(); console.error( `Refusing to seed. This writes 10 demo firms (owner password "Demo1234!") to: ${host}\n` + 'That is the production database for this project. Re-run with ALLOW_SEED=1 only if you are sure:\n' + ' ALLOW_SEED=1 npx tsx scripts/seed-demo.ts', ); process.exit(1); } // ─── Main ─────────────────────────────────────────────────────────────────── async function main() { const db = getDb(); const passwordHash = await argon2.hash('Demo1234!', { type: argon2.argon2id, memoryCost: 64 * 1024, timeCost: 3, parallelism: 1, }); console.log('Seeding 10 firms with activity…\n'); for (let i = 0; i < FIRMS.length; i++) { const def = FIRMS[i]!; const firstName = FIRST_NAMES[i]!; const lastName = LAST_NAMES[i]!; const fullName = `${firstName} ${lastName}`; const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${slug(def.name)}.test`; // Firm const [firm] = await db.insert(firms).values({ name: def.name, plan: def.plan, watermarkEnabled: def.plan === 'starter', }).returning(); // Owner user const [user] = await db.insert(users).values({ firmId: firm.id, email, passwordHash, fullName, role: 'owner', emailVerifiedAt: new Date(), lastSeenAt: daysAgo(randInt(0, 5)), }).returning(); // 3–5 clients const numClients = randInt(3, 5); const clientRows = []; for (let c = 0; c < numClients; c++) { const cName = `${pick(FIRST_NAMES)} ${pick(LAST_NAMES)}`; const [client] = await db.insert(clients).values({ firmId: firm.id, name: cName, email: `${cName.toLowerCase().replace(' ', '.')}@example.com`, phone: `(${randInt(200, 999)}) ${randInt(200, 999)}-${randInt(1000, 9999)}`, }).returning(); clientRows.push(client); } // 5–8 cases const numCases = randInt(5, 8); const caseTitles = CASE_TITLES[def.area]!; const caseRows = []; for (let cc = 0; cc < numCases; cc++) { const status = cc < numCases - 2 ? 'open' : (Math.random() > 0.5 ? 'pending' : 'closed'); const hourlyRate = randInt(150, 450); const openedDaysAgo = randInt(7, 120); const [kase] = await db.insert(cases).values({ firmId: firm.id, clientId: pick(clientRows).id, title: caseTitles[cc % caseTitles.length]!, caseNumber: `${new Date().getFullYear()}-${String(cc + 1).padStart(4, '0')}`, status, practiceArea: def.area, hourlyRate: String(hourlyRate), openedAt: daysAgo(openedDaysAgo), closedAt: status === 'closed' ? daysAgo(randInt(0, openedDaysAgo - 1)) : null, }).returning(); caseRows.push(kase); } // 15–30 time entries spread across cases const numEntries = randInt(15, 30); for (let e = 0; e < numEntries; e++) { const kase = pick(caseRows); const minutes = randInt(15, 240); const startedDaysAgo = randInt(0, 60); const startedAt = daysAgo(startedDaysAgo); const endedAt = new Date(startedAt.getTime() + minutes * 60 * 1000); await db.insert(timeEntries).values({ firmId: firm.id, caseId: kase.id, userId: user.id, description: pick(TIME_DESCRIPTIONS), startedAt, endedAt, minutes, rate: kase.hourlyRate ?? '250', billable: Math.random() > 0.15, }); } // 2–4 invoices const numInvoices = randInt(2, 4); for (let inv = 0; inv < numInvoices; inv++) { const kase = pick(caseRows); const client = clientRows.find((c) => c.id === kase.clientId)!; const numItems = randInt(2, 5); const items = []; let subtotal = 0; for (let it = 0; it < numItems; it++) { const qty = Number((Math.random() * 8 + 0.5).toFixed(2)); const rate = Number(kase.hourlyRate ?? 250); const amount = Number((qty * rate).toFixed(2)); subtotal += amount; items.push({ description: pick(TIME_DESCRIPTIONS), quantity: qty, rate, amount, sortOrder: it }); } const total = Number(subtotal.toFixed(2)); // Status mix: ~40% sent, ~25% paid, ~15% overdue, ~15% draft, ~5% void const r = Math.random(); const status = r < 0.4 ? 'sent' : r < 0.65 ? 'paid' : r < 0.8 ? 'overdue' : r < 0.95 ? 'draft' : 'void'; const issuedDaysAgo = randInt(5, 90); const issuedAt = status === 'draft' ? null : daysAgo(issuedDaysAgo); const dueAt = issuedAt ? new Date(issuedAt.getTime() + 30 * 24 * 60 * 60 * 1000) : null; const paidAt = status === 'paid' ? daysAgo(randInt(1, issuedDaysAgo - 1)) : null; const [invoice] = await db.insert(invoices).values({ firmId: firm.id, clientId: client.id, caseId: kase.id, number: `INV-${new Date().getFullYear()}-${String(inv + 1 + i * 10).padStart(4, '0')}`, status, subtotal: subtotal.toFixed(2), taxRate: '0', total: total.toFixed(2), issuedAt, dueAt, paidAt, }).returning(); for (const it of items) { await db.insert(invoiceItems).values({ invoiceId: invoice.id, description: it.description, quantity: String(it.quantity), rate: String(it.rate), amount: String(it.amount), sortOrder: it.sortOrder, }); } } console.log(` ✓ ${def.name.padEnd(32)} — ${email} (${def.plan})`); } console.log('\nDone. All 10 users use password: Demo1234!'); await getPool().end(); } main().catch((err) => { console.error(err); process.exit(1); });