ray-finance 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ import { LOGOS } from "./logos.js";
2
+ // ─── Date Helpers ─── //
3
+ const now = new Date();
4
+ function today() {
5
+ return now.toISOString().slice(0, 10);
6
+ }
7
+ function yesterday() {
8
+ return daysAgo(1);
9
+ }
10
+ function daysAgo(n) {
11
+ const d = new Date(now);
12
+ d.setDate(d.getDate() - n);
13
+ return d.toISOString().slice(0, 10);
14
+ }
15
+ /** First of the month, N months ago (0 = this month) */
16
+ function monthStart(monthsAgo) {
17
+ const d = new Date(now.getFullYear(), now.getMonth() - monthsAgo, 1);
18
+ return d.toISOString().slice(0, 10);
19
+ }
20
+ /** Specific day of the month, N months ago. For current month (0), clamps to today. */
21
+ function monthDay(monthsAgo, day) {
22
+ const d = new Date(now.getFullYear(), now.getMonth() - monthsAgo, day);
23
+ if (monthsAgo === 0 && d > now) {
24
+ // Clamp future current-month dates to recent past
25
+ return daysAgo(Math.max(1, day % (now.getDate()) + 1));
26
+ }
27
+ return d.toISOString().slice(0, 10);
28
+ }
29
+ /** Date N months from now */
30
+ function monthsFromNow(n) {
31
+ const d = new Date(now.getFullYear(), now.getMonth() + n, now.getDate());
32
+ return d.toISOString().slice(0, 10);
33
+ }
34
+ /** Date N years ago */
35
+ function yearsAgo(n) {
36
+ const d = new Date(now.getFullYear() - n, now.getMonth(), now.getDate());
37
+ return d.toISOString().slice(0, 10);
38
+ }
39
+ // ─── Institutions ─── //
40
+ export const institutions = [
41
+ { item_id: "demo-chase", access_token: "demo-access-chase", name: "Chase", products: '["transactions","liabilities"]', logo: LOGOS.chase, primary_color: "#003087" },
42
+ { item_id: "demo-robinhood", access_token: "demo-access-robinhood", name: "Robinhood", products: '["transactions","investments"]', logo: LOGOS.robinhood, primary_color: "#00C805" },
43
+ { item_id: "demo-schwab", access_token: "demo-access-schwab", name: "Charles Schwab", products: '["transactions","investments"]', logo: LOGOS.schwab, primary_color: "#00A0DF" },
44
+ { item_id: "demo-amex", access_token: "demo-access-amex", name: "American Express", products: '["transactions","liabilities"]', logo: LOGOS.amex, primary_color: "#006FCF" },
45
+ ];
46
+ // ─── Accounts ─── //
47
+ export const accounts = [
48
+ { account_id: "demo-chase-checking", item_id: "demo-chase", name: "Total Checking", official_name: "Chase Total Checking", type: "depository", subtype: "checking", mask: "4521", current_balance: 4200, available_balance: 4200, currency: "USD", balance_limit: null },
49
+ { account_id: "demo-chase-savings", item_id: "demo-chase", name: "Chase Savings", official_name: "Chase Savings", type: "depository", subtype: "savings", mask: "7890", current_balance: 12500, available_balance: 12500, currency: "USD", balance_limit: null },
50
+ { account_id: "demo-robinhood-brokerage", item_id: "demo-robinhood", name: "Individual", official_name: "Robinhood Individual", type: "investment", subtype: "brokerage", mask: "3344", current_balance: 34000, available_balance: null, currency: "USD", balance_limit: null },
51
+ { account_id: "demo-schwab-401k", item_id: "demo-schwab", name: "401(k)", official_name: "Schwab 401(k)", type: "investment", subtype: "401k", mask: "9012", current_balance: 67000, available_balance: null, currency: "USD", balance_limit: null },
52
+ { account_id: "demo-amex-gold", item_id: "demo-amex", name: "Gold Card", official_name: "American Express Gold Card", type: "credit", subtype: "credit card", mask: "1008", current_balance: 1830, available_balance: 13170, currency: "USD", balance_limit: 15000 },
53
+ { account_id: "demo-chase-mortgage", item_id: "demo-chase", name: "Home Mortgage", official_name: "Chase Home Mortgage", type: "loan", subtype: "mortgage", mask: "6601", current_balance: 285000, available_balance: null, currency: "USD", balance_limit: null },
54
+ { account_id: "demo-chase-home", item_id: "demo-chase", name: "Primary Residence", official_name: null, type: "other", subtype: "property", mask: null, current_balance: 425000, available_balance: null, currency: "USD", balance_limit: null },
55
+ ];
56
+ // ─── Transactions ─── //
57
+ let txCounter = 0;
58
+ function txId() {
59
+ return `demo-tx-${++txCounter}`;
60
+ }
61
+ const CHK = "demo-chase-checking";
62
+ const AMEX = "demo-amex-gold";
63
+ export const transactions = [
64
+ // ── Income (negative = inflow in Plaid) ──
65
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(0, 1), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
66
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(0, 15), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
67
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(1, 1), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
68
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(1, 15), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
69
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(2, 1), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
70
+ { transaction_id: txId(), account_id: CHK, amount: -4250, date: monthDay(2, 15), name: "Payroll - Acme Corp", merchant_name: "Acme Corp", category: "INCOME", subcategory: "INCOME_WAGES", pending: 0, payment_channel: "online" },
71
+ { transaction_id: txId(), account_id: CHK, amount: -800, date: monthDay(1, 20), name: "Wire Transfer - Client LLC", merchant_name: "Client LLC", category: "INCOME", subcategory: "INCOME_OTHER", pending: 0, payment_channel: "online" },
72
+ // ── Rent (3 months) ──
73
+ { transaction_id: txId(), account_id: CHK, amount: 1850, date: monthDay(0, 1), name: "Rent Payment", merchant_name: null, category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", pending: 0, payment_channel: "online" },
74
+ { transaction_id: txId(), account_id: CHK, amount: 1850, date: monthDay(1, 1), name: "Rent Payment", merchant_name: null, category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", pending: 0, payment_channel: "online" },
75
+ { transaction_id: txId(), account_id: CHK, amount: 1850, date: monthDay(2, 1), name: "Rent Payment", merchant_name: null, category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", pending: 0, payment_channel: "online" },
76
+ // ── Utilities ──
77
+ { transaction_id: txId(), account_id: CHK, amount: 138.42, date: monthDay(0, 15), name: "PG&E", merchant_name: "PG&E", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", pending: 0, payment_channel: "online" },
78
+ { transaction_id: txId(), account_id: CHK, amount: 125.80, date: monthDay(1, 15), name: "PG&E", merchant_name: "PG&E", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", pending: 0, payment_channel: "online" },
79
+ { transaction_id: txId(), account_id: CHK, amount: 142.15, date: monthDay(2, 15), name: "PG&E", merchant_name: "PG&E", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", pending: 0, payment_channel: "online" },
80
+ { transaction_id: txId(), account_id: CHK, amount: 79.99, date: monthDay(0, 12), name: "Comcast Internet", merchant_name: "Comcast", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", pending: 0, payment_channel: "online" },
81
+ { transaction_id: txId(), account_id: CHK, amount: 79.99, date: monthDay(1, 12), name: "Comcast Internet", merchant_name: "Comcast", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", pending: 0, payment_channel: "online" },
82
+ { transaction_id: txId(), account_id: CHK, amount: 79.99, date: monthDay(2, 12), name: "Comcast Internet", merchant_name: "Comcast", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", pending: 0, payment_channel: "online" },
83
+ // ── Food & Drink (this month — targeting ~$450 spend) ──
84
+ { transaction_id: txId(), account_id: AMEX, amount: 89.42, date: monthDay(0, 3), name: "Whole Foods Market", merchant_name: "Whole Foods", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
85
+ { transaction_id: txId(), account_id: AMEX, amount: 52.18, date: monthDay(0, 7), name: "Trader Joe's", merchant_name: "Trader Joe's", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
86
+ { transaction_id: txId(), account_id: AMEX, amount: 34.67, date: monthDay(0, 14), name: "Safeway", merchant_name: "Safeway", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
87
+ { transaction_id: txId(), account_id: AMEX, amount: 67.30, date: monthDay(0, 5), name: "Olive Garden", merchant_name: "Olive Garden", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
88
+ { transaction_id: txId(), account_id: AMEX, amount: 42.80, date: monthDay(0, 10), name: "Thai Kitchen", merchant_name: "Thai Kitchen", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
89
+ { transaction_id: txId(), account_id: AMEX, amount: 12.45, date: monthDay(0, 8), name: "Chipotle", merchant_name: "Chipotle", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_FAST_FOOD", pending: 0, payment_channel: "in store" },
90
+ { transaction_id: txId(), account_id: AMEX, amount: 6.50, date: yesterday(), name: "Starbucks", merchant_name: "Starbucks", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_COFFEE", pending: 0, payment_channel: "in store" },
91
+ { transaction_id: txId(), account_id: AMEX, amount: 5.75, date: today(), name: "Blue Bottle Coffee", merchant_name: "Blue Bottle", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_COFFEE", pending: 0, payment_channel: "in store" },
92
+ { transaction_id: txId(), account_id: AMEX, amount: 78.50, date: monthDay(0, 18), name: "Whole Foods Market", merchant_name: "Whole Foods", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
93
+ { transaction_id: txId(), account_id: AMEX, amount: 58.35, date: daysAgo(3), name: "Sushi Roku", merchant_name: "Sushi Roku", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
94
+ // ── Food & Drink (last month) ──
95
+ { transaction_id: txId(), account_id: AMEX, amount: 92.10, date: monthDay(1, 5), name: "Whole Foods Market", merchant_name: "Whole Foods", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
96
+ { transaction_id: txId(), account_id: AMEX, amount: 48.75, date: monthDay(1, 12), name: "Trader Joe's", merchant_name: "Trader Joe's", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_GROCERIES", pending: 0, payment_channel: "in store" },
97
+ { transaction_id: txId(), account_id: AMEX, amount: 55.20, date: monthDay(1, 18), name: "Nobu", merchant_name: "Nobu", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_RESTAURANTS", pending: 0, payment_channel: "in store" },
98
+ { transaction_id: txId(), account_id: AMEX, amount: 14.30, date: monthDay(1, 22), name: "Chipotle", merchant_name: "Chipotle", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_FAST_FOOD", pending: 0, payment_channel: "in store" },
99
+ { transaction_id: txId(), account_id: AMEX, amount: 6.50, date: monthDay(1, 8), name: "Starbucks", merchant_name: "Starbucks", category: "FOOD_AND_DRINK", subcategory: "FOOD_AND_DRINK_COFFEE", pending: 0, payment_channel: "in store" },
100
+ // ── Shopping (this month — large Apple purchase triggers alert) ──
101
+ { transaction_id: txId(), account_id: AMEX, amount: 34.99, date: daysAgo(12), name: "Amazon", merchant_name: "Amazon", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ONLINE_MARKETPLACES", pending: 0, payment_channel: "online" },
102
+ { transaction_id: txId(), account_id: AMEX, amount: 67.23, date: daysAgo(8), name: "Target", merchant_name: "Target", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_SUPERSTORES", pending: 0, payment_channel: "in store" },
103
+ { transaction_id: txId(), account_id: AMEX, amount: 29.99, date: daysAgo(4), name: "Uniqlo", merchant_name: "Uniqlo", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_CLOTHING_AND_ACCESSORIES", pending: 0, payment_channel: "in store" },
104
+ { transaction_id: txId(), account_id: AMEX, amount: 849.99, date: today(), name: "Apple Store", merchant_name: "Apple", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ELECTRONICS", pending: 0, payment_channel: "in store" },
105
+ // ── Shopping (last month) ──
106
+ { transaction_id: txId(), account_id: AMEX, amount: 299.99, date: monthDay(1, 10), name: "Best Buy", merchant_name: "Best Buy", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ELECTRONICS", pending: 0, payment_channel: "in store" },
107
+ { transaction_id: txId(), account_id: AMEX, amount: 42.50, date: monthDay(1, 16), name: "Amazon", merchant_name: "Amazon", category: "GENERAL_MERCHANDISE", subcategory: "GENERAL_MERCHANDISE_ONLINE_MARKETPLACES", pending: 0, payment_channel: "online" },
108
+ // ── Transportation (this month — targeting ~$100) ──
109
+ { transaction_id: txId(), account_id: CHK, amount: 52.40, date: monthDay(0, 6), name: "Shell", merchant_name: "Shell", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_GAS", pending: 0, payment_channel: "in store" },
110
+ { transaction_id: txId(), account_id: CHK, amount: 18.75, date: monthDay(0, 11), name: "Uber", merchant_name: "Uber", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES", pending: 0, payment_channel: "online" },
111
+ { transaction_id: txId(), account_id: CHK, amount: 24.30, date: yesterday(), name: "Uber", merchant_name: "Uber", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES", pending: 0, payment_channel: "online" },
112
+ // ── Transportation (last month) ──
113
+ { transaction_id: txId(), account_id: CHK, amount: 48.90, date: monthDay(1, 8), name: "Shell", merchant_name: "Shell", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_GAS", pending: 0, payment_channel: "in store" },
114
+ { transaction_id: txId(), account_id: CHK, amount: 35.00, date: monthDay(1, 14), name: "Clipper Card", merchant_name: "BART", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_PUBLIC_TRANSIT", pending: 0, payment_channel: "online" },
115
+ { transaction_id: txId(), account_id: CHK, amount: 22.15, date: monthDay(1, 25), name: "Uber", merchant_name: "Uber", category: "TRANSPORTATION", subcategory: "TRANSPORTATION_TAXIS_AND_RIDE_SHARES", pending: 0, payment_channel: "online" },
116
+ // ── Entertainment (this month — targeting ~$85) ──
117
+ { transaction_id: txId(), account_id: CHK, amount: 15.99, date: monthDay(0, 2), name: "Netflix", merchant_name: "Netflix", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", pending: 0, payment_channel: "online" },
118
+ { transaction_id: txId(), account_id: CHK, amount: 10.99, date: monthDay(0, 2), name: "Spotify", merchant_name: "Spotify", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_MUSIC_AND_AUDIO", pending: 0, payment_channel: "online" },
119
+ { transaction_id: txId(), account_id: CHK, amount: 28.50, date: monthDay(0, 16), name: "AMC Theaters", merchant_name: "AMC", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", pending: 0, payment_channel: "in store" },
120
+ { transaction_id: txId(), account_id: CHK, amount: 49.99, date: daysAgo(5), name: "Steam", merchant_name: "Steam", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_GAMES", pending: 0, payment_channel: "online" },
121
+ // ── Entertainment (last month) ──
122
+ { transaction_id: txId(), account_id: CHK, amount: 15.99, date: monthDay(1, 2), name: "Netflix", merchant_name: "Netflix", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", pending: 0, payment_channel: "online" },
123
+ { transaction_id: txId(), account_id: CHK, amount: 10.99, date: monthDay(1, 2), name: "Spotify", merchant_name: "Spotify", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_MUSIC_AND_AUDIO", pending: 0, payment_channel: "online" },
124
+ // ── Personal Care ──
125
+ { transaction_id: txId(), account_id: CHK, amount: 35.00, date: monthDay(0, 17), name: "Supercuts", merchant_name: "Supercuts", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_HAIR_AND_BEAUTY", pending: 0, payment_channel: "in store" },
126
+ { transaction_id: txId(), account_id: CHK, amount: 22.47, date: daysAgo(2), name: "CVS Pharmacy", merchant_name: "CVS", category: "MEDICAL", subcategory: "MEDICAL_PHARMACIES_AND_SUPPLEMENTS", pending: 0, payment_channel: "in store" },
127
+ // ── Loan Payments ──
128
+ { transaction_id: txId(), account_id: CHK, amount: 2100, date: monthDay(0, 1), name: "Chase Mortgage Payment", merchant_name: "Chase", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", pending: 0, payment_channel: "online" },
129
+ { transaction_id: txId(), account_id: CHK, amount: 2100, date: monthDay(1, 1), name: "Chase Mortgage Payment", merchant_name: "Chase", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", pending: 0, payment_channel: "online" },
130
+ { transaction_id: txId(), account_id: CHK, amount: 2100, date: monthDay(2, 1), name: "Chase Mortgage Payment", merchant_name: "Chase", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", pending: 0, payment_channel: "online" },
131
+ // ── Gym ──
132
+ { transaction_id: txId(), account_id: CHK, amount: 95, date: monthDay(0, 5), name: "Equinox", merchant_name: "Equinox", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_GYMS_AND_FITNESS_CENTERS", pending: 0, payment_channel: "online" },
133
+ { transaction_id: txId(), account_id: CHK, amount: 95, date: monthDay(1, 5), name: "Equinox", merchant_name: "Equinox", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_GYMS_AND_FITNESS_CENTERS", pending: 0, payment_channel: "online" },
134
+ // ── Car Insurance ──
135
+ { transaction_id: txId(), account_id: CHK, amount: 145, date: monthDay(0, 20), name: "GEICO", merchant_name: "GEICO", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_INSURANCE", pending: 0, payment_channel: "online" },
136
+ { transaction_id: txId(), account_id: CHK, amount: 145, date: monthDay(1, 20), name: "GEICO", merchant_name: "GEICO", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_INSURANCE", pending: 0, payment_channel: "online" },
137
+ // ── iCloud ──
138
+ { transaction_id: txId(), account_id: CHK, amount: 2.99, date: monthDay(0, 3), name: "Apple iCloud", merchant_name: "Apple", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_OTHER_GENERAL_SERVICES", pending: 0, payment_channel: "online" },
139
+ { transaction_id: txId(), account_id: CHK, amount: 2.99, date: monthDay(1, 3), name: "Apple iCloud", merchant_name: "Apple", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_OTHER_GENERAL_SERVICES", pending: 0, payment_channel: "online" },
140
+ ];
141
+ // ─── Securities ─── //
142
+ export const securities = [
143
+ { security_id: "demo-sec-aapl", name: "Apple Inc", ticker: "AAPL", type: "equity", close_price: 227.50, close_price_as_of: daysAgo(1) },
144
+ { security_id: "demo-sec-tsla", name: "Tesla Inc", ticker: "TSLA", type: "equity", close_price: 172.30, close_price_as_of: daysAgo(1) },
145
+ { security_id: "demo-sec-voo", name: "Vanguard S&P 500 ETF", ticker: "VOO", type: "etf", close_price: 532.80, close_price_as_of: daysAgo(1) },
146
+ { security_id: "demo-sec-vtsax", name: "Vanguard Total Stock Market Index", ticker: "VTSAX", type: "mutual fund", close_price: 118.45, close_price_as_of: daysAgo(1) },
147
+ { security_id: "demo-sec-vbtlx", name: "Vanguard Total Bond Market Index", ticker: "VBTLX", type: "mutual fund", close_price: 10.82, close_price_as_of: daysAgo(1) },
148
+ { security_id: "demo-sec-vttvx", name: "Vanguard Target Retirement 2035", ticker: "VTTVX", type: "mutual fund", close_price: 28.15, close_price_as_of: daysAgo(1) },
149
+ ];
150
+ // ─── Holdings ─── //
151
+ export const holdings = [
152
+ { account_id: "demo-robinhood-brokerage", security_id: "demo-sec-aapl", quantity: 50, value: 11375, cost_basis: 8500, price: 227.50, price_as_of: daysAgo(1), vested_value: null, vested_quantity: null },
153
+ { account_id: "demo-robinhood-brokerage", security_id: "demo-sec-tsla", quantity: 35, value: 6030.50, cost_basis: 7000, price: 172.30, price_as_of: daysAgo(1), vested_value: null, vested_quantity: null },
154
+ { account_id: "demo-robinhood-brokerage", security_id: "demo-sec-voo", quantity: 31, value: 16516.80, cost_basis: 14200, price: 532.80, price_as_of: daysAgo(1), vested_value: null, vested_quantity: null },
155
+ { account_id: "demo-schwab-401k", security_id: "demo-sec-vtsax", quantity: 280, value: 33166, cost_basis: 28000, price: 118.45, price_as_of: daysAgo(1), vested_value: 33166, vested_quantity: 280 },
156
+ { account_id: "demo-schwab-401k", security_id: "demo-sec-vbtlx", quantity: 1200, value: 12984, cost_basis: 12500, price: 10.82, price_as_of: daysAgo(1), vested_value: 12984, vested_quantity: 1200 },
157
+ { account_id: "demo-schwab-401k", security_id: "demo-sec-vttvx", quantity: 740, value: 20831, cost_basis: 18500, price: 28.15, price_as_of: daysAgo(1), vested_value: 20831, vested_quantity: 740 },
158
+ ];
159
+ // ─── Liabilities ─── //
160
+ export const liabilities = [
161
+ {
162
+ account_id: "demo-amex-gold", type: "credit", interest_rate: 24.99, origination_date: null,
163
+ original_balance: null, current_balance: 1830, minimum_payment: 35,
164
+ next_payment_due: monthDay(0, 28), last_payment_amount: 500, last_payment_date: monthDay(1, 25),
165
+ credit_limit: 15000, last_statement_issue_date: monthDay(1, 28), is_overdue: 0,
166
+ apr_type: "variable", maturity_date: null, loan_type: null, property_address: null,
167
+ escrow_balance: null, loan_status: null, loan_name: null, repayment_plan: null,
168
+ expected_payoff_date: null, ytd_interest_paid: null, ytd_principal_paid: null,
169
+ },
170
+ {
171
+ account_id: "demo-chase-mortgage", type: "mortgage", interest_rate: 6.875,
172
+ origination_date: yearsAgo(3), original_balance: 320000, current_balance: 285000,
173
+ minimum_payment: 2100, next_payment_due: monthDay(-1, 1),
174
+ last_payment_amount: 2100, last_payment_date: monthDay(0, 1),
175
+ credit_limit: null, last_statement_issue_date: null, is_overdue: 0,
176
+ apr_type: "fixed", maturity_date: monthsFromNow(27 * 12), loan_type: "conventional",
177
+ property_address: "123 Main St, San Francisco, CA 94102",
178
+ escrow_balance: 4200, loan_status: "active", loan_name: "30yr Fixed",
179
+ repayment_plan: null, expected_payoff_date: monthsFromNow(27 * 12),
180
+ ytd_interest_paid: 6125, ytd_principal_paid: 2175,
181
+ },
182
+ ];
183
+ // ─── Recurring ─── //
184
+ export const recurring = [
185
+ { stream_id: "demo-rec-salary", account_id: CHK, merchant_name: "Acme Corp", description: "Payroll - Acme Corp", frequency: "BIWEEKLY", category: "INCOME", subcategory: "INCOME_WAGES", avg_amount: -4250, last_amount: -4250, first_date: yearsAgo(2), last_date: monthDay(0, 15), is_active: 1, status: "MATURE", stream_type: "inflow" },
186
+ { stream_id: "demo-rec-freelance", account_id: CHK, merchant_name: "Client LLC", description: "Wire Transfer - Client LLC", frequency: "MONTHLY", category: "INCOME", subcategory: "INCOME_OTHER", avg_amount: -800, last_amount: -800, first_date: yearsAgo(1), last_date: monthDay(1, 20), is_active: 1, status: "MATURE", stream_type: "inflow" },
187
+ { stream_id: "demo-rec-rent", account_id: CHK, merchant_name: null, description: "Rent Payment", frequency: "MONTHLY", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_RENT", avg_amount: 1850, last_amount: 1850, first_date: yearsAgo(2), last_date: monthDay(0, 1), is_active: 1, status: "MATURE", stream_type: "outflow" },
188
+ { stream_id: "demo-rec-netflix", account_id: CHK, merchant_name: "Netflix", description: "Netflix", frequency: "MONTHLY", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_TV_AND_MOVIES", avg_amount: 15.99, last_amount: 15.99, first_date: yearsAgo(3), last_date: monthDay(0, 2), is_active: 1, status: "MATURE", stream_type: "outflow" },
189
+ { stream_id: "demo-rec-spotify", account_id: CHK, merchant_name: "Spotify", description: "Spotify Premium", frequency: "MONTHLY", category: "ENTERTAINMENT", subcategory: "ENTERTAINMENT_MUSIC_AND_AUDIO", avg_amount: 10.99, last_amount: 10.99, first_date: yearsAgo(2), last_date: monthDay(0, 2), is_active: 1, status: "MATURE", stream_type: "outflow" },
190
+ { stream_id: "demo-rec-electric", account_id: CHK, merchant_name: "PG&E", description: "PG&E Electric", frequency: "MONTHLY", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_GAS_AND_ELECTRICITY", avg_amount: 135, last_amount: 138.42, first_date: yearsAgo(2), last_date: monthDay(0, 15), is_active: 1, status: "MATURE", stream_type: "outflow" },
191
+ { stream_id: "demo-rec-internet", account_id: CHK, merchant_name: "Comcast", description: "Comcast Internet", frequency: "MONTHLY", category: "RENT_AND_UTILITIES", subcategory: "RENT_AND_UTILITIES_INTERNET_AND_CABLE", avg_amount: 79.99, last_amount: 79.99, first_date: yearsAgo(2), last_date: monthDay(0, 12), is_active: 1, status: "MATURE", stream_type: "outflow" },
192
+ { stream_id: "demo-rec-mortgage", account_id: CHK, merchant_name: "Chase", description: "Mortgage Payment", frequency: "MONTHLY", category: "LOAN_PAYMENTS", subcategory: "LOAN_PAYMENTS_MORTGAGE_PAYMENT", avg_amount: 2100, last_amount: 2100, first_date: yearsAgo(3), last_date: monthDay(0, 1), is_active: 1, status: "MATURE", stream_type: "outflow" },
193
+ { stream_id: "demo-rec-gym", account_id: CHK, merchant_name: "Equinox", description: "Equinox Membership", frequency: "MONTHLY", category: "PERSONAL_CARE", subcategory: "PERSONAL_CARE_GYMS_AND_FITNESS_CENTERS", avg_amount: 95, last_amount: 95, first_date: yearsAgo(1), last_date: monthDay(0, 5), is_active: 1, status: "MATURE", stream_type: "outflow" },
194
+ { stream_id: "demo-rec-icloud", account_id: CHK, merchant_name: "Apple", description: "iCloud Storage", frequency: "MONTHLY", category: "GENERAL_SERVICES", subcategory: "GENERAL_SERVICES_OTHER_GENERAL_SERVICES", avg_amount: 2.99, last_amount: 2.99, first_date: yearsAgo(4), last_date: monthDay(0, 3), is_active: 1, status: "MATURE", stream_type: "outflow" },
195
+ ];
196
+ // ─── Budgets ─── //
197
+ export const budgets = [
198
+ { category: "FOOD_AND_DRINK", monthly_limit: 600, period: "monthly" },
199
+ { category: "GENERAL_MERCHANDISE", monthly_limit: 400, period: "monthly" },
200
+ { category: "ENTERTAINMENT", monthly_limit: 150, period: "monthly" },
201
+ { category: "TRANSPORTATION", monthly_limit: 200, period: "monthly" },
202
+ ];
203
+ // ─── Goals ─── //
204
+ export const goals = [
205
+ { name: "Emergency Fund", target_amount: 15000, current_amount: 6200, target_date: monthsFromNow(8), status: "active" },
206
+ { name: "Japan Vacation", target_amount: 5000, current_amount: 2800, target_date: monthsFromNow(5), status: "active" },
207
+ { name: "New Car Down Payment", target_amount: 8000, current_amount: 1200, target_date: monthsFromNow(14), status: "active" },
208
+ ];
209
+ // ─── Daily Scores (14 days, building streaks) ─── //
210
+ export const dailyScores = [
211
+ { date: daysAgo(13), score: 72, restaurant_count: 1, shopping_count: 0, food_spend: 55.20, total_spend: 180.40, zero_spend: 0, no_restaurant_streak: 0, no_shopping_streak: 1, on_pace_streak: 1 },
212
+ { date: daysAgo(12), score: 68, restaurant_count: 0, shopping_count: 1, food_spend: 12.30, total_spend: 210.50, zero_spend: 0, no_restaurant_streak: 1, no_shopping_streak: 0, on_pace_streak: 0 },
213
+ { date: daysAgo(11), score: 75, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 95.00, zero_spend: 0, no_restaurant_streak: 2, no_shopping_streak: 1, on_pace_streak: 1 },
214
+ { date: daysAgo(10), score: 92, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 0, zero_spend: 1, no_restaurant_streak: 3, no_shopping_streak: 2, on_pace_streak: 2 },
215
+ { date: daysAgo(9), score: 78, restaurant_count: 1, shopping_count: 0, food_spend: 42.80, total_spend: 120.30, zero_spend: 0, no_restaurant_streak: 0, no_shopping_streak: 3, on_pace_streak: 3 },
216
+ { date: daysAgo(8), score: 65, restaurant_count: 0, shopping_count: 1, food_spend: 6.50, total_spend: 250.00, zero_spend: 0, no_restaurant_streak: 1, no_shopping_streak: 0, on_pace_streak: 0 },
217
+ { date: daysAgo(7), score: 80, restaurant_count: 0, shopping_count: 0, food_spend: 34.67, total_spend: 34.67, zero_spend: 0, no_restaurant_streak: 2, no_shopping_streak: 1, on_pace_streak: 1 },
218
+ { date: daysAgo(6), score: 85, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 15.99, zero_spend: 0, no_restaurant_streak: 3, no_shopping_streak: 2, on_pace_streak: 2 },
219
+ { date: daysAgo(5), score: 70, restaurant_count: 0, shopping_count: 0, food_spend: 0, total_spend: 49.99, zero_spend: 0, no_restaurant_streak: 4, no_shopping_streak: 3, on_pace_streak: 3 },
220
+ { date: daysAgo(4), score: 82, restaurant_count: 0, shopping_count: 0, food_spend: 29.99, total_spend: 29.99, zero_spend: 0, no_restaurant_streak: 5, no_shopping_streak: 4, on_pace_streak: 4 },
221
+ { date: daysAgo(3), score: 74, restaurant_count: 1, shopping_count: 0, food_spend: 58.35, total_spend: 58.35, zero_spend: 0, no_restaurant_streak: 0, no_shopping_streak: 5, on_pace_streak: 5 },
222
+ { date: daysAgo(2), score: 83, restaurant_count: 0, shopping_count: 0, food_spend: 22.47, total_spend: 22.47, zero_spend: 0, no_restaurant_streak: 1, no_shopping_streak: 6, on_pace_streak: 6 },
223
+ { date: yesterday(), score: 88, restaurant_count: 0, shopping_count: 0, food_spend: 6.50, total_spend: 30.80, zero_spend: 0, no_restaurant_streak: 2, no_shopping_streak: 7, on_pace_streak: 7 },
224
+ { date: today(), score: 76, restaurant_count: 0, shopping_count: 0, food_spend: 5.75, total_spend: 855.74, zero_spend: 0, no_restaurant_streak: 3, no_shopping_streak: 8, on_pace_streak: 0 },
225
+ ];
226
+ // ─── Achievements ─── //
227
+ export const achievements = [
228
+ { key: "on_pace_7", name: "Clean Week", description: "7 consecutive days with all budgets on pace", unlocked_at: yesterday() },
229
+ { key: "no_shopping_7", name: "Window Shopper", description: "7 days with zero shopping purchases", unlocked_at: daysAgo(2) },
230
+ { key: "no_restaurant_7", name: "Kitchen Hero", description: "7-day no-restaurant streak", unlocked_at: daysAgo(5) },
231
+ { key: "zero_hero", name: "Zero Hero", description: "A zero-spend day", unlocked_at: daysAgo(10) },
232
+ ];
233
+ // ─── Net Worth History (30 days) ─── //
234
+ export const netWorthHistory = [];
235
+ {
236
+ const baseAssets = 539000;
237
+ const baseLiabilities = 287200;
238
+ for (let i = 29; i >= 0; i--) {
239
+ const jitter = Math.sin(i * 0.7) * 150 + (29 - i) * 130;
240
+ const assets = Math.round((baseAssets + jitter) * 100) / 100;
241
+ const liabJitter = Math.sin(i * 0.5) * 50 - (29 - i) * 8;
242
+ const liab = Math.round((baseLiabilities + liabJitter) * 100) / 100;
243
+ netWorthHistory.push({
244
+ date: daysAgo(i),
245
+ total_assets: assets,
246
+ total_liabilities: liab,
247
+ net_worth: Math.round((assets - liab) * 100) / 100,
248
+ });
249
+ }
250
+ }
251
+ // ─── Investment Transactions ─── //
252
+ export const investmentTransactions = [
253
+ { investment_transaction_id: "demo-inv-tx-1", account_id: "demo-robinhood-brokerage", security_id: "demo-sec-aapl", date: daysAgo(30), name: "Buy AAPL", quantity: 5, amount: -1100.75, price: 220.15, fees: 0, type: "buy", subtype: "buy", iso_currency_code: "USD" },
254
+ { investment_transaction_id: "demo-inv-tx-2", account_id: "demo-robinhood-brokerage", security_id: "demo-sec-voo", date: daysAgo(45), name: "Buy VOO", quantity: 10, amount: -5284.00, price: 528.40, fees: 0, type: "buy", subtype: "buy", iso_currency_code: "USD" },
255
+ { investment_transaction_id: "demo-inv-tx-3", account_id: "demo-robinhood-brokerage", security_id: "demo-sec-voo", date: daysAgo(15), name: "Dividend VOO", quantity: 0, amount: -32.50, price: 0, fees: 0, type: "cash", subtype: "dividend", iso_currency_code: "USD" },
256
+ { investment_transaction_id: "demo-inv-tx-4", account_id: "demo-schwab-401k", security_id: "demo-sec-vtsax", date: daysAgo(14), name: "401k Contribution", quantity: 8.5, amount: -1006.83, price: 118.45, fees: 0, type: "buy", subtype: "contribution", iso_currency_code: "USD" },
257
+ { investment_transaction_id: "demo-inv-tx-5", account_id: "demo-schwab-401k", security_id: "demo-sec-vtsax", date: daysAgo(28), name: "401k Contribution", quantity: 8.5, amount: -1006.83, price: 118.45, fees: 0, type: "buy", subtype: "contribution", iso_currency_code: "USD" },
258
+ { investment_transaction_id: "demo-inv-tx-6", account_id: "demo-schwab-401k", security_id: "demo-sec-vbtlx", date: daysAgo(20), name: "Dividend VBTLX", quantity: 0, amount: -18.40, price: 0, fees: 0, type: "cash", subtype: "dividend", iso_currency_code: "USD" },
259
+ ];
260
+ // ─── Recurring Bills ─── //
261
+ export const recurringBills = [
262
+ { name: "Rent", amount: 1850, day_of_month: 1, type: "housing", account_id: CHK },
263
+ { name: "Mortgage", amount: 2100, day_of_month: 1, type: "housing", account_id: CHK },
264
+ { name: "Electric", amount: 135, day_of_month: 15, type: "utility", account_id: CHK },
265
+ { name: "Internet", amount: 79.99, day_of_month: 12, type: "utility", account_id: CHK },
266
+ { name: "Car Insurance", amount: 145, day_of_month: 20, type: "insurance", account_id: CHK },
267
+ ];
268
+ // ─── Memories ─── //
269
+ export const memories = [
270
+ { content: "User prefers index fund investing and dollar-cost averaging", category: "preference" },
271
+ { content: "User is saving for a trip to Japan next year", category: "goal" },
272
+ ];
@@ -0,0 +1,8 @@
1
+ /** Generate a minimal 4x4 solid-color PNG as base64 from a hex color string. */
2
+ export declare function colorPng(hex: string): string;
3
+ export declare const LOGOS: {
4
+ chase: string;
5
+ robinhood: string;
6
+ schwab: string;
7
+ amex: string;
8
+ };
@@ -0,0 +1,58 @@
1
+ import { deflateSync } from "zlib";
2
+ /** Generate a minimal 4x4 solid-color PNG as base64 from a hex color string. */
3
+ export function colorPng(hex) {
4
+ const r = parseInt(hex.slice(1, 3), 16);
5
+ const g = parseInt(hex.slice(3, 5), 16);
6
+ const b = parseInt(hex.slice(5, 7), 16);
7
+ // PNG signature
8
+ const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
9
+ // IHDR chunk: 4x4, 8-bit RGB
10
+ const ihdrData = Buffer.alloc(13);
11
+ ihdrData.writeUInt32BE(4, 0); // width
12
+ ihdrData.writeUInt32BE(4, 4); // height
13
+ ihdrData[8] = 8; // bit depth
14
+ ihdrData[9] = 2; // color type: RGB
15
+ const ihdr = makeChunk("IHDR", ihdrData);
16
+ // IDAT chunk: raw pixel data (filter byte 0 + 4 RGB pixels per row)
17
+ const rowLen = 1 + 4 * 3; // filter byte + 4 pixels * 3 channels
18
+ const raw = Buffer.alloc(rowLen * 4);
19
+ for (let y = 0; y < 4; y++) {
20
+ const offset = y * rowLen;
21
+ raw[offset] = 0; // no filter
22
+ for (let x = 0; x < 4; x++) {
23
+ raw[offset + 1 + x * 3] = r;
24
+ raw[offset + 2 + x * 3] = g;
25
+ raw[offset + 3 + x * 3] = b;
26
+ }
27
+ }
28
+ const compressed = deflateSync(raw);
29
+ const idat = makeChunk("IDAT", compressed);
30
+ // IEND chunk
31
+ const iend = makeChunk("IEND", Buffer.alloc(0));
32
+ return Buffer.concat([signature, ihdr, idat, iend]).toString("base64");
33
+ }
34
+ function makeChunk(type, data) {
35
+ const len = Buffer.alloc(4);
36
+ len.writeUInt32BE(data.length);
37
+ const typeBuffer = Buffer.from(type, "ascii");
38
+ const crcInput = Buffer.concat([typeBuffer, data]);
39
+ const crc = Buffer.alloc(4);
40
+ crc.writeUInt32BE(crc32(crcInput) >>> 0);
41
+ return Buffer.concat([len, typeBuffer, data, crc]);
42
+ }
43
+ function crc32(buf) {
44
+ let c = 0xffffffff;
45
+ for (let i = 0; i < buf.length; i++) {
46
+ c ^= buf[i];
47
+ for (let j = 0; j < 8; j++) {
48
+ c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0);
49
+ }
50
+ }
51
+ return c ^ 0xffffffff;
52
+ }
53
+ export const LOGOS = {
54
+ chase: colorPng("#003087"),
55
+ robinhood: colorPng("#00C805"),
56
+ schwab: colorPng("#00A0DF"),
57
+ amex: colorPng("#006FCF"),
58
+ };
@@ -0,0 +1 @@
1
+ export declare function seedDemoDb(dbPath: string, encryptionKey?: string): void;
@@ -0,0 +1,125 @@
1
+ import Database from "libsql";
2
+ import { resolve, dirname } from "path";
3
+ import { mkdirSync, existsSync, unlinkSync } from "fs";
4
+ import { migrate } from "../db/schema.js";
5
+ import { institutions, accounts, transactions, securities, holdings, liabilities, recurring, budgets, goals, dailyScores, achievements, netWorthHistory, investmentTransactions, recurringBills, memories, } from "./data.js";
6
+ export function seedDemoDb(dbPath, encryptionKey) {
7
+ const resolved = resolve(dbPath);
8
+ const dir = dirname(resolved);
9
+ mkdirSync(dir, { recursive: true });
10
+ // Remove existing demo DB for a clean slate
11
+ for (const suffix of ["", "-wal", "-shm"]) {
12
+ const f = resolved + suffix;
13
+ if (existsSync(f))
14
+ unlinkSync(f);
15
+ }
16
+ const opts = {};
17
+ if (encryptionKey) {
18
+ opts.encryptionCipher = "aes256cbc";
19
+ opts.encryptionKey = encryptionKey;
20
+ }
21
+ const db = new Database(dbPath, opts);
22
+ db.pragma("journal_mode = WAL");
23
+ db.pragma("foreign_keys = ON");
24
+ migrate(db);
25
+ const seed = db.transaction(() => {
26
+ // Institutions
27
+ const instStmt = db.prepare(`INSERT INTO institutions (item_id, access_token, name, products, logo, primary_color) VALUES (?, ?, ?, ?, ?, ?)`);
28
+ for (const i of institutions) {
29
+ instStmt.run(i.item_id, i.access_token, i.name, i.products, i.logo, i.primary_color);
30
+ }
31
+ // Accounts
32
+ const acctStmt = db.prepare(`INSERT INTO accounts (account_id, item_id, name, official_name, type, subtype, mask, current_balance, available_balance, currency, balance_limit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
33
+ for (const a of accounts) {
34
+ acctStmt.run(a.account_id, a.item_id, a.name, a.official_name, a.type, a.subtype, a.mask, a.current_balance, a.available_balance, a.currency, a.balance_limit);
35
+ }
36
+ // Transactions
37
+ const txStmt = db.prepare(`INSERT INTO transactions (transaction_id, account_id, amount, date, name, merchant_name, category, subcategory, pending, payment_channel) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
38
+ for (const t of transactions) {
39
+ txStmt.run(t.transaction_id, t.account_id, t.amount, t.date, t.name, t.merchant_name, t.category, t.subcategory, t.pending, t.payment_channel);
40
+ }
41
+ // Securities
42
+ const secStmt = db.prepare(`INSERT INTO securities (security_id, name, ticker, type, close_price, close_price_as_of) VALUES (?, ?, ?, ?, ?, ?)`);
43
+ for (const s of securities) {
44
+ secStmt.run(s.security_id, s.name, s.ticker, s.type, s.close_price, s.close_price_as_of);
45
+ }
46
+ // Holdings
47
+ const holdStmt = db.prepare(`INSERT INTO holdings (account_id, security_id, quantity, value, cost_basis, price, price_as_of, vested_value, vested_quantity) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
48
+ for (const h of holdings) {
49
+ holdStmt.run(h.account_id, h.security_id, h.quantity, h.value, h.cost_basis, h.price, h.price_as_of, h.vested_value, h.vested_quantity);
50
+ }
51
+ // Liabilities
52
+ const liabStmt = db.prepare(`INSERT INTO liabilities (account_id, type, interest_rate, origination_date, original_balance, current_balance, minimum_payment, next_payment_due, last_payment_amount, last_payment_date, credit_limit, last_statement_issue_date, is_overdue, apr_type, maturity_date, loan_type, property_address, escrow_balance, loan_status, loan_name, repayment_plan, expected_payoff_date, ytd_interest_paid, ytd_principal_paid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
53
+ for (const l of liabilities) {
54
+ liabStmt.run(l.account_id, l.type, l.interest_rate, l.origination_date, l.original_balance, l.current_balance, l.minimum_payment, l.next_payment_due, l.last_payment_amount, l.last_payment_date, l.credit_limit, l.last_statement_issue_date, l.is_overdue, l.apr_type, l.maturity_date, l.loan_type, l.property_address, l.escrow_balance, l.loan_status, l.loan_name, l.repayment_plan, l.expected_payoff_date, l.ytd_interest_paid, l.ytd_principal_paid);
55
+ }
56
+ // Recurring
57
+ const recStmt = db.prepare(`INSERT INTO recurring (stream_id, account_id, merchant_name, description, frequency, category, subcategory, avg_amount, last_amount, first_date, last_date, is_active, status, stream_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
58
+ for (const r of recurring) {
59
+ recStmt.run(r.stream_id, r.account_id, r.merchant_name, r.description, r.frequency, r.category, r.subcategory, r.avg_amount, r.last_amount, r.first_date, r.last_date, r.is_active, r.status, r.stream_type);
60
+ }
61
+ // Budgets
62
+ const budgetStmt = db.prepare(`INSERT INTO budgets (category, monthly_limit, period) VALUES (?, ?, ?)`);
63
+ for (const b of budgets) {
64
+ budgetStmt.run(b.category, b.monthly_limit, b.period);
65
+ }
66
+ // Goals
67
+ const goalStmt = db.prepare(`INSERT INTO goals (name, target_amount, current_amount, target_date, status) VALUES (?, ?, ?, ?, ?)`);
68
+ for (const g of goals) {
69
+ goalStmt.run(g.name, g.target_amount, g.current_amount, g.target_date, g.status);
70
+ }
71
+ // Daily Scores
72
+ const scoreStmt = db.prepare(`INSERT INTO daily_scores (date, score, restaurant_count, shopping_count, food_spend, total_spend, zero_spend, no_restaurant_streak, no_shopping_streak, on_pace_streak) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
73
+ for (const s of dailyScores) {
74
+ scoreStmt.run(s.date, s.score, s.restaurant_count, s.shopping_count, s.food_spend, s.total_spend, s.zero_spend, s.no_restaurant_streak, s.no_shopping_streak, s.on_pace_streak);
75
+ }
76
+ // Achievements
77
+ const achStmt = db.prepare(`INSERT INTO achievements (key, name, description, unlocked_at) VALUES (?, ?, ?, ?)`);
78
+ for (const a of achievements) {
79
+ achStmt.run(a.key, a.name, a.description, a.unlocked_at);
80
+ }
81
+ // Net Worth History
82
+ const nwStmt = db.prepare(`INSERT INTO net_worth_history (date, total_assets, total_liabilities, net_worth) VALUES (?, ?, ?, ?)`);
83
+ for (const nw of netWorthHistory) {
84
+ nwStmt.run(nw.date, nw.total_assets, nw.total_liabilities, nw.net_worth);
85
+ }
86
+ // Investment Transactions
87
+ const invTxStmt = db.prepare(`INSERT INTO investment_transactions (investment_transaction_id, account_id, security_id, date, name, quantity, amount, price, fees, type, subtype, iso_currency_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
88
+ for (const it of investmentTransactions) {
89
+ invTxStmt.run(it.investment_transaction_id, it.account_id, it.security_id, it.date, it.name, it.quantity, it.amount, it.price, it.fees, it.type, it.subtype, it.iso_currency_code);
90
+ }
91
+ // Recurring Bills
92
+ const billStmt = db.prepare(`INSERT INTO recurring_bills (name, amount, day_of_month, type, account_id) VALUES (?, ?, ?, ?, ?)`);
93
+ for (const b of recurringBills) {
94
+ billStmt.run(b.name, b.amount, b.day_of_month, b.type, b.account_id);
95
+ }
96
+ // Memories
97
+ const memStmt = db.prepare(`INSERT INTO memories (content, category) VALUES (?, ?)`);
98
+ for (const m of memories) {
99
+ memStmt.run(m.content, m.category);
100
+ }
101
+ });
102
+ seed();
103
+ console.log("Demo database seeded successfully!\n");
104
+ console.log(` Institutions: ${institutions.length}`);
105
+ console.log(` Accounts: ${accounts.length}`);
106
+ console.log(` Transactions: ${transactions.length}`);
107
+ console.log(` Securities: ${securities.length}`);
108
+ console.log(` Holdings: ${holdings.length}`);
109
+ console.log(` Liabilities: ${liabilities.length}`);
110
+ console.log(` Recurring: ${recurring.length}`);
111
+ console.log(` Budgets: ${budgets.length}`);
112
+ console.log(` Goals: ${goals.length}`);
113
+ console.log(` Daily Scores: ${dailyScores.length}`);
114
+ console.log(` Achievements: ${achievements.length}`);
115
+ console.log(` Net Worth Days: ${netWorthHistory.length}`);
116
+ console.log(` Invest. Txns: ${investmentTransactions.length}`);
117
+ console.log(` Recurring Bills: ${recurringBills.length}`);
118
+ console.log(` Memories: ${memories.length}`);
119
+ console.log(`\n Database: ${resolve(dbPath)}`);
120
+ console.log(`\n Try it out:`);
121
+ console.log(` ray --demo status`);
122
+ console.log(` ray --demo accounts`);
123
+ console.log(` ray --demo spending`);
124
+ db.close();
125
+ }
@@ -0,0 +1,23 @@
1
+ import type BetterSqlite3 from "libsql";
2
+ type Database = BetterSqlite3.Database;
3
+ /** Scrape the Redfin Estimate from a Redfin listing URL */
4
+ export declare function scrapeRedfinEstimate(url: string): Promise<number | null>;
5
+ /** Add a manual account */
6
+ export declare function addManualAccount(db: Database, name: string, type: "asset" | "liability", balance: number, listingUrl?: string): {
7
+ accountId: string;
8
+ };
9
+ /** Remove a manual account */
10
+ export declare function removeManualAccount(db: Database, id: string): void;
11
+ /** List all manual accounts */
12
+ export declare function getManualAccounts(db: Database): {
13
+ account_id: string;
14
+ name: string;
15
+ type: string;
16
+ current_balance: number;
17
+ listing_url: string | null;
18
+ }[];
19
+ /** Refresh all property values from stored listing URLs (called during daily sync) */
20
+ export declare function refreshPropertyValues(db: Database): Promise<void>;
21
+ /** Check if any listing URLs are configured */
22
+ export declare function hasListingUrls(db: Database): boolean;
23
+ export {};
@@ -0,0 +1,80 @@
1
+ import { createHash } from "crypto";
2
+ const MANUAL_ITEM_ID = "manual-assets";
3
+ const LISTING_URL_PREFIX = "listing_url:";
4
+ function accountId(name) {
5
+ const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
6
+ return `manual-${hash}`;
7
+ }
8
+ /** Scrape the Redfin Estimate from a Redfin listing URL */
9
+ export async function scrapeRedfinEstimate(url) {
10
+ const resp = await fetch(url, {
11
+ headers: {
12
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
13
+ "Accept": "text/html",
14
+ },
15
+ });
16
+ if (!resp.ok)
17
+ return null;
18
+ const html = await resp.text();
19
+ // Prefer Redfin Estimate (current market value) over sold/list price
20
+ const match = html.match(/EstimateValueHeader[^>]*>.*?class="price[^"]*">\$([\d,]+)/) ||
21
+ html.match(/statsValue price[^>]*>[^$]*\$([\d,]+)/) ||
22
+ html.match(/Redfin Estimate[^$]*\$([\d,]+)/) ||
23
+ html.match(/"estimatedValue":\s*([\d.]+)/);
24
+ if (!match)
25
+ return null;
26
+ const value = parseFloat(match[1].replace(/,/g, ""));
27
+ return isNaN(value) ? null : value;
28
+ }
29
+ /** Ensure the manual institution exists */
30
+ function ensureManualInstitution(db) {
31
+ db.prepare(`INSERT INTO institutions (item_id, access_token, name, products) VALUES (?, 'manual', 'Manual Accounts', '[]')
32
+ ON CONFLICT(item_id) DO NOTHING`).run(MANUAL_ITEM_ID);
33
+ }
34
+ /** Add a manual account */
35
+ export function addManualAccount(db, name, type, balance, listingUrl) {
36
+ ensureManualInstitution(db);
37
+ const id = accountId(name);
38
+ const dbType = type === "asset" ? "other" : "loan";
39
+ const subtype = listingUrl ? "property" : (type === "asset" ? "other" : "other");
40
+ db.prepare(`INSERT INTO accounts (account_id, item_id, name, type, subtype, current_balance, updated_at)
41
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
42
+ ON CONFLICT(account_id) DO UPDATE SET current_balance = excluded.current_balance, updated_at = datetime('now')`).run(id, MANUAL_ITEM_ID, name, dbType, subtype, balance);
43
+ if (listingUrl) {
44
+ db.prepare(`INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(LISTING_URL_PREFIX + id, listingUrl);
45
+ }
46
+ return { accountId: id };
47
+ }
48
+ /** Remove a manual account */
49
+ export function removeManualAccount(db, id) {
50
+ db.prepare(`DELETE FROM accounts WHERE account_id = ?`).run(id);
51
+ db.prepare(`DELETE FROM settings WHERE key = ?`).run(LISTING_URL_PREFIX + id);
52
+ }
53
+ /** List all manual accounts */
54
+ export function getManualAccounts(db) {
55
+ const rows = db.prepare(`SELECT account_id, name, type, current_balance FROM accounts WHERE item_id = ?`).all(MANUAL_ITEM_ID);
56
+ return rows.map(r => {
57
+ const urlRow = db.prepare(`SELECT value FROM settings WHERE key = ?`).get(LISTING_URL_PREFIX + r.account_id);
58
+ return { ...r, listing_url: urlRow?.value ?? null };
59
+ });
60
+ }
61
+ /** Refresh all property values from stored listing URLs (called during daily sync) */
62
+ export async function refreshPropertyValues(db) {
63
+ const urls = db.prepare(`SELECT key, value FROM settings WHERE key LIKE ?`).all(LISTING_URL_PREFIX + "%");
64
+ for (const { key, value: url } of urls) {
65
+ const id = key.slice(LISTING_URL_PREFIX.length);
66
+ try {
67
+ const val = await scrapeRedfinEstimate(url);
68
+ if (val) {
69
+ db.prepare(`UPDATE accounts SET current_balance = ?, updated_at = datetime('now') WHERE account_id = ?`).run(val, id);
70
+ }
71
+ }
72
+ catch {
73
+ // Non-fatal
74
+ }
75
+ }
76
+ }
77
+ /** Check if any listing URLs are configured */
78
+ export function hasListingUrls(db) {
79
+ return !!(db.prepare(`SELECT 1 FROM settings WHERE key LIKE ? LIMIT 1`).get(LISTING_URL_PREFIX + "%"));
80
+ }