// Made-up statements for nine months of 2026, shaped like the real exports // (Discover, Chase and Bank of America checking). Nothing here is real data. // They carry the patterns milestone 2 has to find: subscriptions with a price // change, a cancelled service that charges again, one billed on two cards, a // yearly membership, variable bills, and an unusually large purchase. function random(seed: number) { return () => { seed |= 0; seed = (seed + 0x6d2b79f5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const pad = (n: number) => String(n).padStart(2, '0'); const us = (iso: string) => `${iso.slice(5, 7)}/${iso.slice(8, 10)}/${iso.slice(0, 4)}`; const day = (month: number, d: number) => `2026-${pad(month)}-${pad(d)}`; const money = (cents: number) => (cents / 100).toFixed(2); const quoted = (cents: number) => `"${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}"`; interface Row { date: string; description: string; /** Money out negative. */ cents: number; category?: string; } function everyDay(months: number[], fn: (date: string, weekday: number) => void) { for (const m of months) { const last = new Date(2026, m, 0).getDate(); for (let d = 1; d <= last; d++) fn(day(m, d), new Date(2026, m - 1, d).getDay()); } } const MONTHS = [1, 2, 3, 4, 5, 6, 7, 8, 9]; export function discoverRows(): Row[] { const r = random(7); const rows: Row[] = []; for (const m of MONTHS) { rows.push({ date: day(m, 12), description: 'NETFLIX.COM 866-579-7172 CA', cents: m <= 6 ? -1549 : -1799, category: 'Services' }); rows.push({ date: day(m, 3), description: 'SPOTIFY USA 877-778-1161 NY', cents: -1199, category: 'Services' }); rows.push({ date: day(m, 20), description: 'HULU 877-8244858 CA', cents: -799, category: 'Services' }); rows.push({ date: day(m, 25), description: 'MICRO CENTER #045 CHARLOTTE NC', cents: -Math.round(4000 + r() * 4000), category: 'Merchandise' }); rows.push({ date: day(m, 8), description: 'JOES DINER #12 CHARLOTTE NC', cents: -Math.round(2500 + r() * 3500), category: 'Restaurants' }); rows.push({ date: day(m, 22), description: 'JOES DINER #12 CHARLOTTE NC', cents: -Math.round(2500 + r() * 3500), category: 'Restaurants' }); rows.push({ date: day(m, 28), description: 'INTERNET PAYMENT - THANK YOU', cents: 60000, category: 'Payments and Credits' }); } everyDay(MONTHS, (date, weekday) => { if (weekday === 6) rows.push({ date, description: 'KROGER #123 CHARLOTTE NC', cents: -Math.round(4000 + r() * 10000), category: 'Supermarkets' }); if (weekday === 2 || weekday === 4) rows.push({ date, description: 'STARBUCKS #4567 CHARLOTTE NC', cents: -545, category: 'Restaurants' }); }); rows.push({ date: day(3, 10), description: 'COSTCO *ANNUAL RENEWAL 800-774-2678 WA', cents: -6500, category: 'Warehouse Clubs' }); rows.push({ date: day(9, 15), description: 'BEST BUY #321 CHARLOTTE NC', cents: -48000, category: 'Merchandise' }); return rows.sort((a, b) => a.date.localeCompare(b.date)); } export function chaseRows(): Row[] { const r = random(11); const rows: Row[] = []; everyDay(MONTHS, (date, weekday) => { if (weekday === 1) rows.push({ date, description: 'SHELL OIL 57444 CHARLOTTE NC', cents: -Math.round(3000 + r() * 2500), category: 'Gas' }); if (weekday === 5 && r() < 0.6) rows.push({ date, description: `AMAZON MKTPL*${Math.floor(r() * 1e9).toString(36).toUpperCase()}`, cents: -Math.round(1500 + r() * 6000), category: 'Shopping' }); }); for (const m of MONTHS) rows.push({ date: day(m, 27), description: 'AUTOMATIC PAYMENT - THANK', cents: 40000, category: '' }); rows.push({ date: day(8, 5), description: 'DISNEY PLUS 888-905-7888 CA', cents: -1599, category: 'Entertainment' }); rows.push({ date: day(9, 5), description: 'DISNEY PLUS 888-905-7888 CA', cents: -1599, category: 'Entertainment' }); // The same Netflix plan, accidentally on a second card. rows.push({ date: day(9, 14), description: 'NETFLIX.COM 866-579-7172 CA', cents: -1799, category: 'Entertainment' }); return rows.sort((a, b) => a.date.localeCompare(b.date)); } export function checkingRows(): Row[] { const r = random(23); const rows: Row[] = []; for (let d = new Date(2026, 0, 9); d.getMonth() < 9; d.setDate(d.getDate() + 14)) { rows.push({ date: d.toLocaleDateString('en-CA'), description: 'ACME CORP DES:PAYROLL ID:XXXXX12345 INDN:DOE,JANE CO ID:XXXXX12345 PPD', cents: 245000 }); } for (const m of MONTHS) { rows.push({ date: day(m, 1), description: 'ROCKET MORTGAGE DES:MTG PYMT ID:XXXXX77777 INDN:JANE DOE CO ID:XXXXX77777 PPD', cents: -185000 }); rows.push({ date: day(m, 22), description: 'DUKE ENERGY DES:DUKEENERGY ID:XXXXX33333 INDN:JANE DOE CO ID:XXXXX33333 WEB', cents: -Math.round(9500 + r() * 8500) }); rows.push({ date: day(m, 28), description: 'DISCOVER DES:E-PAYMENT ID:XXXXX99999 INDN:JANE DOE CO ID:XXXXX99999 WEB', cents: -60000 }); rows.push({ date: day(m, 27), description: 'CHASE CREDIT CRD DES:EPAY ID:XXXXX55555 INDN:JANE DOE CO ID:XXXXX55555 WEB', cents: -40000 }); } return rows.sort((a, b) => a.date.localeCompare(b.date)); } export function discoverCsv(rows = discoverRows()): string { return ['Trans. Date,Post Date,Description,Amount,Category', ...rows.map((x) => `${us(x.date)},${us(x.date)},${x.description},${money(-x.cents)},${x.category ?? ''}`)].join('\n') + '\n'; } export function chaseCsv(rows = chaseRows()): string { return ( [ 'Transaction Date,Post Date,Description,Category,Type,Amount,Memo', ...rows.map((x) => `${us(x.date)},${us(x.date)},${x.description},${x.category ?? ''},${x.cents > 0 ? 'Payment' : 'Sale'},${money(x.cents)},`), ].join('\n') + '\n' ); } export function checkingCsv(rows = checkingRows(), opening = 500000): string { let balance = opening; const lines = rows.map((x) => { balance += x.cents; return `${us(x.date)},"${x.description}",${quoted(x.cents)},${quoted(balance)}`; }); const credits = rows.filter((x) => x.cents > 0).reduce((a, x) => a + x.cents, 0); const debits = rows.filter((x) => x.cents < 0).reduce((a, x) => a + x.cents, 0); return ( [ 'Description,,Summary Amt.', `Beginning balance as of 01/01/2026,,${quoted(opening)}`, `Total credits,,${quoted(credits)}`, `Total debits,,${quoted(debits)}`, `Ending balance as of 09/30/2026,,${quoted(balance)}`, '', 'Date,Description,Amount,Running Bal.', `01/01/2026,Beginning balance as of 01/01/2026,,${quoted(opening)}`, ...lines, ].join('\n') + '\n' ); }