ray-finance 0.1.0

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/ai/agent.d.ts +2 -0
  4. package/dist/ai/agent.js +93 -0
  5. package/dist/ai/audit.d.ts +3 -0
  6. package/dist/ai/audit.js +6 -0
  7. package/dist/ai/context.d.ts +6 -0
  8. package/dist/ai/context.js +93 -0
  9. package/dist/ai/insights.d.ts +3 -0
  10. package/dist/ai/insights.js +401 -0
  11. package/dist/ai/memory.d.ts +14 -0
  12. package/dist/ai/memory.js +12 -0
  13. package/dist/ai/redactor.d.ts +2 -0
  14. package/dist/ai/redactor.js +103 -0
  15. package/dist/ai/system-prompt.d.ts +2 -0
  16. package/dist/ai/system-prompt.js +85 -0
  17. package/dist/ai/tools.d.ts +4 -0
  18. package/dist/ai/tools.js +699 -0
  19. package/dist/alerts/index.d.ts +11 -0
  20. package/dist/alerts/index.js +95 -0
  21. package/dist/auth/anthropic.d.ts +7 -0
  22. package/dist/auth/anthropic.js +85 -0
  23. package/dist/auth/pkce.d.ts +5 -0
  24. package/dist/auth/pkce.js +10 -0
  25. package/dist/auth/store.d.ts +12 -0
  26. package/dist/auth/store.js +51 -0
  27. package/dist/cli/backup.d.ts +2 -0
  28. package/dist/cli/backup.js +94 -0
  29. package/dist/cli/chat.d.ts +1 -0
  30. package/dist/cli/chat.js +203 -0
  31. package/dist/cli/commands.d.ts +13 -0
  32. package/dist/cli/commands.js +201 -0
  33. package/dist/cli/format.d.ts +14 -0
  34. package/dist/cli/format.js +144 -0
  35. package/dist/cli/index.d.ts +2 -0
  36. package/dist/cli/index.js +186 -0
  37. package/dist/cli/scheduler.d.ts +2 -0
  38. package/dist/cli/scheduler.js +114 -0
  39. package/dist/cli/setup.d.ts +1 -0
  40. package/dist/cli/setup.js +174 -0
  41. package/dist/config.d.ts +22 -0
  42. package/dist/config.js +60 -0
  43. package/dist/daily-sync.d.ts +7 -0
  44. package/dist/daily-sync.js +109 -0
  45. package/dist/db/connection.d.ts +5 -0
  46. package/dist/db/connection.js +45 -0
  47. package/dist/db/encryption.d.ts +3 -0
  48. package/dist/db/encryption.js +35 -0
  49. package/dist/db/helpers.d.ts +16 -0
  50. package/dist/db/helpers.js +45 -0
  51. package/dist/db/schema.d.ts +2 -0
  52. package/dist/db/schema.js +199 -0
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.js +1 -0
  55. package/dist/plaid/client.d.ts +2 -0
  56. package/dist/plaid/client.js +22 -0
  57. package/dist/plaid/link.d.ts +8 -0
  58. package/dist/plaid/link.js +23 -0
  59. package/dist/plaid/sync.d.ts +18 -0
  60. package/dist/plaid/sync.js +186 -0
  61. package/dist/public/favicon.png +0 -0
  62. package/dist/public/link.html +184 -0
  63. package/dist/public/ray-logo-dark.png +0 -0
  64. package/dist/queries/index.d.ts +163 -0
  65. package/dist/queries/index.js +411 -0
  66. package/dist/scoring/index.d.ts +53 -0
  67. package/dist/scoring/index.js +375 -0
  68. package/dist/server.d.ts +7 -0
  69. package/dist/server.js +172 -0
  70. package/package.json +60 -0
@@ -0,0 +1,163 @@
1
+ import type BetterSqlite3 from "libsql";
2
+ type Database = BetterSqlite3.Database;
3
+ export interface BudgetStatus {
4
+ category: string;
5
+ budget: number;
6
+ spent: number;
7
+ remaining: number;
8
+ pct_used: number;
9
+ over_budget: boolean;
10
+ }
11
+ export interface GoalStatus {
12
+ name: string;
13
+ target: number;
14
+ current: number;
15
+ remaining: number;
16
+ progress_pct: number;
17
+ target_date: string | null;
18
+ monthly_needed: number;
19
+ }
20
+ export interface TransactionFilters {
21
+ startDate?: string;
22
+ endDate?: string;
23
+ category?: string;
24
+ merchant?: string;
25
+ minAmount?: number;
26
+ maxAmount?: number;
27
+ limit?: number;
28
+ }
29
+ export declare function getNetWorth(db: Database): {
30
+ net_worth: number;
31
+ assets: number;
32
+ liabilities: number;
33
+ home_value: number;
34
+ home_equity: number;
35
+ investments: number;
36
+ cash: number;
37
+ credit_debt: number;
38
+ mortgage: number;
39
+ prev_net_worth: number | null;
40
+ };
41
+ export declare function getAccountBalances(db: Database): {
42
+ name: string;
43
+ balance: number;
44
+ type: string;
45
+ }[];
46
+ export declare function getBudgetStatuses(db: Database): BudgetStatus[];
47
+ export declare function getGoals(db: Database): GoalStatus[];
48
+ export declare function getCashFlowThisMonth(db: Database): {
49
+ income: number;
50
+ expenses: number;
51
+ net: number;
52
+ };
53
+ export declare function getRecentSpending(db: Database): {
54
+ category: string;
55
+ total: number;
56
+ }[];
57
+ export declare function getRecentTransactions(db: Database, limit?: number): {
58
+ name: string;
59
+ amount: number;
60
+ category: string;
61
+ date: string;
62
+ pending: number;
63
+ }[];
64
+ export declare function getTransactionsFiltered(db: Database, filters: TransactionFilters): {
65
+ transaction_id: string;
66
+ name: string;
67
+ merchant_name: string | null;
68
+ amount: number;
69
+ category: string;
70
+ date: string;
71
+ pending: number;
72
+ }[];
73
+ export declare function getIncome(db: Database, startDate: string, endDate: string): {
74
+ source: string;
75
+ total: number;
76
+ count: number;
77
+ }[];
78
+ export declare function searchTransactions(db: Database, query: string, limit?: number): {
79
+ transaction_id: string;
80
+ name: string;
81
+ merchant_name: string | null;
82
+ amount: number;
83
+ category: string;
84
+ date: string;
85
+ }[];
86
+ export declare function getCashFlow(db: Database, startDate: string, endDate: string): {
87
+ income: number;
88
+ expenses: number;
89
+ net: number;
90
+ savingsRate: number;
91
+ monthly: {
92
+ month: string;
93
+ income: number;
94
+ expenses: number;
95
+ net: number;
96
+ }[];
97
+ };
98
+ export declare function forecastBalance(db: Database, accountId?: string, months?: number): {
99
+ currentBalance: number;
100
+ projections: {
101
+ month: string;
102
+ projected: number;
103
+ }[];
104
+ avgMonthlyInflow: number;
105
+ avgMonthlyOutflow: number;
106
+ };
107
+ export declare function getPortfolio(db: Database): {
108
+ totalValue: number;
109
+ totalCostBasis: number;
110
+ totalGainLoss: number;
111
+ holdings: {
112
+ account: string;
113
+ security: string;
114
+ ticker: string;
115
+ quantity: number;
116
+ value: number;
117
+ costBasis: number;
118
+ gainLoss: number;
119
+ }[];
120
+ };
121
+ export declare function getInvestmentPerformance(db: Database): {
122
+ totalReturn: number;
123
+ totalReturnPct: number;
124
+ holdings: {
125
+ security: string;
126
+ ticker: string;
127
+ value: number;
128
+ costBasis: number;
129
+ returnPct: number;
130
+ }[];
131
+ };
132
+ export declare function getDebts(db: Database): {
133
+ totalDebt: number;
134
+ debts: {
135
+ name: string;
136
+ balance: number;
137
+ rate: number;
138
+ minPayment: number;
139
+ type: string;
140
+ nextDue: string | null;
141
+ }[];
142
+ };
143
+ export declare function compareSpending(db: Database, period1Start: string, period1End: string, period2Start: string, period2End: string): {
144
+ period1Total: number;
145
+ period2Total: number;
146
+ difference: number;
147
+ pctChange: number;
148
+ categories: {
149
+ category: string;
150
+ period1: number;
151
+ period2: number;
152
+ diff: number;
153
+ }[];
154
+ };
155
+ export declare function getNetWorthTrend(db: Database, limit?: number): {
156
+ date: string;
157
+ net_worth: number;
158
+ assets: number;
159
+ liabilities: number;
160
+ }[];
161
+ export declare function formatMoney(n: number): string;
162
+ export declare function categoryLabel(cat: string): string;
163
+ export {};
@@ -0,0 +1,411 @@
1
+ export function getNetWorth(db) {
2
+ const home = db
3
+ .prepare(`SELECT current_balance as value FROM accounts WHERE type = 'other' AND subtype = 'property' LIMIT 1`)
4
+ .get();
5
+ const mortgage = db
6
+ .prepare(`SELECT current_balance as value FROM accounts WHERE type = 'loan' AND subtype = 'mortgage' LIMIT 1`)
7
+ .get();
8
+ const investments = db
9
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type = 'investment'`)
10
+ .get();
11
+ const cash = db
12
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type = 'depository'`)
13
+ .get();
14
+ const credit = db
15
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type = 'credit'`)
16
+ .get();
17
+ const assets = db
18
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type IN ('depository', 'investment', 'other')`)
19
+ .get();
20
+ const liabilities = db
21
+ .prepare(`SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type IN ('credit', 'loan')`)
22
+ .get();
23
+ // Previous day's net worth
24
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
25
+ const prev = db
26
+ .prepare(`SELECT net_worth FROM net_worth_history WHERE date <= ? ORDER BY date DESC LIMIT 1`)
27
+ .get(yesterday);
28
+ const homeVal = home?.value || 0;
29
+ const mortgageVal = mortgage?.value || 0;
30
+ return {
31
+ net_worth: assets.total - liabilities.total,
32
+ assets: assets.total,
33
+ liabilities: liabilities.total,
34
+ home_value: homeVal,
35
+ home_equity: homeVal - mortgageVal,
36
+ investments: investments.total,
37
+ cash: cash.total,
38
+ credit_debt: credit.total,
39
+ mortgage: mortgageVal,
40
+ prev_net_worth: prev?.net_worth ?? null,
41
+ };
42
+ }
43
+ export function getAccountBalances(db) {
44
+ return db
45
+ .prepare(`SELECT name, current_balance as balance, type FROM accounts
46
+ WHERE type IN ('depository', 'credit') ORDER BY type, current_balance DESC`)
47
+ .all();
48
+ }
49
+ export function getBudgetStatuses(db) {
50
+ const now = new Date();
51
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
52
+ .toISOString()
53
+ .slice(0, 10);
54
+ const today = now.toISOString().slice(0, 10);
55
+ const budgets = db
56
+ .prepare(`SELECT category, monthly_limit FROM budgets`)
57
+ .all();
58
+ return budgets.map((b) => {
59
+ const spent = db
60
+ .prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
61
+ WHERE category = ? AND date BETWEEN ? AND ? AND amount > 0 AND pending = 0`)
62
+ .get(b.category, monthStart, today);
63
+ const remaining = b.monthly_limit - spent.total;
64
+ return {
65
+ category: b.category,
66
+ budget: b.monthly_limit,
67
+ spent: Math.round(spent.total * 100) / 100,
68
+ remaining: Math.round(remaining * 100) / 100,
69
+ pct_used: Math.round((spent.total / b.monthly_limit) * 100),
70
+ over_budget: spent.total > b.monthly_limit,
71
+ };
72
+ });
73
+ }
74
+ export function getGoals(db) {
75
+ const goals = db.prepare(`SELECT * FROM goals`).all();
76
+ const now = new Date();
77
+ return goals.map((g) => {
78
+ const remaining = g.target_amount - g.current_amount;
79
+ const progress = Math.round((g.current_amount / g.target_amount) * 1000) / 10;
80
+ let monthsLeft = 1;
81
+ if (g.target_date) {
82
+ const target = new Date(g.target_date);
83
+ monthsLeft = Math.max(1, (target.getFullYear() - now.getFullYear()) * 12 +
84
+ (target.getMonth() - now.getMonth()));
85
+ }
86
+ return {
87
+ name: g.name,
88
+ target: g.target_amount,
89
+ current: g.current_amount,
90
+ remaining,
91
+ progress_pct: progress,
92
+ target_date: g.target_date,
93
+ monthly_needed: Math.round((remaining / monthsLeft) * 100) / 100,
94
+ };
95
+ });
96
+ }
97
+ export function getCashFlowThisMonth(db) {
98
+ const now = new Date();
99
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
100
+ .toISOString()
101
+ .slice(0, 10);
102
+ const today = now.toISOString().slice(0, 10);
103
+ const income = db
104
+ .prepare(`SELECT COALESCE(SUM(ABS(amount)), 0) as total FROM transactions
105
+ WHERE amount < 0 AND date BETWEEN ? AND ? AND pending = 0
106
+ AND category NOT IN ('TRANSFER_IN')`)
107
+ .get(monthStart, today);
108
+ const expenses = db
109
+ .prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
110
+ WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
111
+ AND category NOT IN ('TRANSFER_OUT')`)
112
+ .get(monthStart, today);
113
+ return {
114
+ income: Math.round(income.total * 100) / 100,
115
+ expenses: Math.round(expenses.total * 100) / 100,
116
+ net: Math.round((income.total - expenses.total) * 100) / 100,
117
+ };
118
+ }
119
+ export function getRecentSpending(db) {
120
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
121
+ .toISOString()
122
+ .slice(0, 10);
123
+ const today = new Date().toISOString().slice(0, 10);
124
+ return db
125
+ .prepare(`SELECT category, SUM(amount) as total FROM transactions
126
+ WHERE amount > 0 AND date IN (?, ?)
127
+ AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS', 'LOAN_PAYMENTS_CAR_PAYMENT', 'LOAN_PAYMENTS_PERSONAL_LOAN_PAYMENT')
128
+ GROUP BY category ORDER BY total DESC`)
129
+ .all(yesterday, today);
130
+ }
131
+ export function getRecentTransactions(db, limit = 10) {
132
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
133
+ .toISOString()
134
+ .slice(0, 10);
135
+ const today = new Date().toISOString().slice(0, 10);
136
+ return db
137
+ .prepare(`SELECT name, amount, category, date, pending FROM transactions
138
+ WHERE amount > 0 AND date IN (?, ?)
139
+ AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS', 'LOAN_PAYMENTS_CAR_PAYMENT', 'LOAN_PAYMENTS_PERSONAL_LOAN_PAYMENT', 'RENT_AND_UTILITIES_RENT')
140
+ ORDER BY amount DESC LIMIT ?`)
141
+ .all(yesterday, today, limit);
142
+ }
143
+ export function getTransactionsFiltered(db, filters) {
144
+ const conditions = [];
145
+ const params = [];
146
+ if (filters.startDate) {
147
+ conditions.push(`date >= ?`);
148
+ params.push(filters.startDate);
149
+ }
150
+ if (filters.endDate) {
151
+ conditions.push(`date <= ?`);
152
+ params.push(filters.endDate);
153
+ }
154
+ if (filters.category) {
155
+ conditions.push(`(category = ? OR subcategory = ?)`);
156
+ params.push(filters.category, filters.category);
157
+ }
158
+ if (filters.merchant) {
159
+ conditions.push(`(merchant_name LIKE ? OR name LIKE ?)`);
160
+ params.push(`%${filters.merchant}%`, `%${filters.merchant}%`);
161
+ }
162
+ if (filters.minAmount !== undefined) {
163
+ conditions.push(`amount >= ?`);
164
+ params.push(filters.minAmount);
165
+ }
166
+ if (filters.maxAmount !== undefined) {
167
+ conditions.push(`amount <= ?`);
168
+ params.push(filters.maxAmount);
169
+ }
170
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
171
+ const limit = filters.limit || 50;
172
+ return db
173
+ .prepare(`SELECT transaction_id, name, merchant_name, amount, category, date, pending
174
+ FROM transactions ${where}
175
+ ORDER BY date DESC, amount DESC LIMIT ?`)
176
+ .all(...params, limit);
177
+ }
178
+ // --- Income ---
179
+ export function getIncome(db, startDate, endDate) {
180
+ return db.prepare(`SELECT COALESCE(merchant_name, name) as source, SUM(ABS(amount)) as total, COUNT(*) as count
181
+ FROM transactions
182
+ WHERE amount < 0 AND date BETWEEN ? AND ? AND pending = 0
183
+ AND category NOT IN ('TRANSFER_IN')
184
+ GROUP BY source ORDER BY total DESC`).all(startDate, endDate);
185
+ }
186
+ // --- Full-text search ---
187
+ export function searchTransactions(db, query, limit = 30) {
188
+ return db.prepare(`SELECT transaction_id, name, merchant_name, amount, category, date
189
+ FROM transactions
190
+ WHERE (name LIKE ? OR merchant_name LIKE ? OR category LIKE ?)
191
+ ORDER BY date DESC LIMIT ?`).all(`%${query}%`, `%${query}%`, `%${query}%`, limit);
192
+ }
193
+ // --- Cash flow analysis ---
194
+ export function getCashFlow(db, startDate, endDate) {
195
+ const income = db.prepare(`SELECT COALESCE(SUM(ABS(amount)), 0) as total FROM transactions
196
+ WHERE amount < 0 AND date BETWEEN ? AND ? AND pending = 0
197
+ AND category NOT IN ('TRANSFER_IN')`).get(startDate, endDate);
198
+ const expenses = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
199
+ WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
200
+ AND category NOT IN ('TRANSFER_OUT')`).get(startDate, endDate);
201
+ const net = income.total - expenses.total;
202
+ const savingsRate = income.total > 0 ? (net / income.total) * 100 : 0;
203
+ // Monthly breakdown
204
+ const rows = db.prepare(`SELECT strftime('%Y-%m', date) as month,
205
+ SUM(CASE WHEN amount < 0 AND category NOT IN ('TRANSFER_IN') THEN ABS(amount) ELSE 0 END) as income,
206
+ SUM(CASE WHEN amount > 0 AND category NOT IN ('TRANSFER_OUT') THEN amount ELSE 0 END) as expenses
207
+ FROM transactions
208
+ WHERE date BETWEEN ? AND ? AND pending = 0
209
+ GROUP BY month ORDER BY month`).all(startDate, endDate);
210
+ return {
211
+ income: Math.round(income.total * 100) / 100,
212
+ expenses: Math.round(expenses.total * 100) / 100,
213
+ net: Math.round(net * 100) / 100,
214
+ savingsRate: Math.round(savingsRate * 10) / 10,
215
+ monthly: rows.map(r => ({ ...r, net: Math.round((r.income - r.expenses) * 100) / 100 })),
216
+ };
217
+ }
218
+ // --- Balance forecast ---
219
+ export function forecastBalance(db, accountId, months = 6) {
220
+ // Get current balance
221
+ let balanceQuery = `SELECT COALESCE(SUM(current_balance), 0) as total FROM accounts WHERE type = 'depository'`;
222
+ const balanceParams = [];
223
+ if (accountId) {
224
+ balanceQuery = `SELECT current_balance as total FROM accounts WHERE account_id = ?`;
225
+ balanceParams.push(accountId);
226
+ }
227
+ const bal = db.prepare(balanceQuery).get(...balanceParams);
228
+ // Calculate 3-month average flows
229
+ const threeMonthsAgo = new Date();
230
+ threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
231
+ const startDate = threeMonthsAgo.toISOString().slice(0, 10);
232
+ const today = new Date().toISOString().slice(0, 10);
233
+ let flowCondition = "";
234
+ const flowParams = [startDate, today];
235
+ if (accountId) {
236
+ flowCondition = " AND account_id = ?";
237
+ flowParams.push(accountId);
238
+ }
239
+ const inflow = db.prepare(`SELECT COALESCE(SUM(ABS(amount)), 0) as total FROM transactions
240
+ WHERE amount < 0 AND date BETWEEN ? AND ? AND pending = 0${flowCondition}`).get(...flowParams);
241
+ const outflow = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM transactions
242
+ WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0${flowCondition}`).get(...flowParams);
243
+ const avgInflow = inflow.total / 3;
244
+ const avgOutflow = outflow.total / 3;
245
+ const monthlyNet = avgInflow - avgOutflow;
246
+ const projections = [];
247
+ let running = bal.total;
248
+ for (let i = 1; i <= months; i++) {
249
+ const d = new Date();
250
+ d.setMonth(d.getMonth() + i);
251
+ running += monthlyNet;
252
+ projections.push({
253
+ month: d.toISOString().slice(0, 7),
254
+ projected: Math.round(running * 100) / 100,
255
+ });
256
+ }
257
+ return {
258
+ currentBalance: bal.total,
259
+ projections,
260
+ avgMonthlyInflow: Math.round(avgInflow * 100) / 100,
261
+ avgMonthlyOutflow: Math.round(avgOutflow * 100) / 100,
262
+ };
263
+ }
264
+ // --- Portfolio ---
265
+ export function getPortfolio(db) {
266
+ const rows = db.prepare(`SELECT a.name as account, s.name as security, s.ticker,
267
+ h.quantity, h.value, h.cost_basis,
268
+ (h.value - COALESCE(h.cost_basis, h.value)) as gain_loss
269
+ FROM holdings h
270
+ JOIN accounts a ON h.account_id = a.account_id
271
+ LEFT JOIN securities s ON h.security_id = s.security_id
272
+ ORDER BY h.value DESC`).all();
273
+ const totalValue = rows.reduce((s, r) => s + (r.value || 0), 0);
274
+ const totalCostBasis = rows.reduce((s, r) => s + (r.cost_basis || 0), 0);
275
+ return {
276
+ totalValue: Math.round(totalValue * 100) / 100,
277
+ totalCostBasis: Math.round(totalCostBasis * 100) / 100,
278
+ totalGainLoss: Math.round((totalValue - totalCostBasis) * 100) / 100,
279
+ holdings: rows.map((r) => ({
280
+ account: r.account,
281
+ security: r.security || "Unknown",
282
+ ticker: r.ticker || "",
283
+ quantity: r.quantity,
284
+ value: r.value || 0,
285
+ costBasis: r.cost_basis || 0,
286
+ gainLoss: r.gain_loss || 0,
287
+ })),
288
+ };
289
+ }
290
+ // --- Investment performance ---
291
+ export function getInvestmentPerformance(db) {
292
+ const rows = db.prepare(`SELECT s.name as security, s.ticker,
293
+ h.value, h.cost_basis
294
+ FROM holdings h
295
+ LEFT JOIN securities s ON h.security_id = s.security_id
296
+ WHERE h.value IS NOT NULL
297
+ ORDER BY h.value DESC`).all();
298
+ const totalValue = rows.reduce((s, r) => s + (r.value || 0), 0);
299
+ const totalCost = rows.reduce((s, r) => s + (r.cost_basis || r.value || 0), 0);
300
+ const totalReturn = totalValue - totalCost;
301
+ const totalReturnPct = totalCost > 0 ? (totalReturn / totalCost) * 100 : 0;
302
+ return {
303
+ totalReturn: Math.round(totalReturn * 100) / 100,
304
+ totalReturnPct: Math.round(totalReturnPct * 10) / 10,
305
+ holdings: rows.map((r) => {
306
+ const cost = r.cost_basis || r.value || 0;
307
+ const ret = (r.value || 0) - cost;
308
+ return {
309
+ security: r.security || "Unknown",
310
+ ticker: r.ticker || "",
311
+ value: r.value || 0,
312
+ costBasis: cost,
313
+ returnPct: cost > 0 ? Math.round((ret / cost) * 1000) / 10 : 0,
314
+ };
315
+ }),
316
+ };
317
+ }
318
+ // --- Debts ---
319
+ export function getDebts(db) {
320
+ // Try liabilities table first
321
+ const liabilities = db.prepare(`SELECT a.name, l.current_balance as balance, l.interest_rate as rate,
322
+ l.minimum_payment as min_payment, l.type, l.next_payment_due as next_due
323
+ FROM liabilities l
324
+ JOIN accounts a ON l.account_id = a.account_id
325
+ WHERE l.current_balance > 0
326
+ ORDER BY l.interest_rate DESC`).all();
327
+ if (liabilities.length > 0) {
328
+ const totalDebt = liabilities.reduce((s, r) => s + (r.balance || 0), 0);
329
+ return {
330
+ totalDebt,
331
+ debts: liabilities.map((r) => ({
332
+ name: r.name,
333
+ balance: r.balance || 0,
334
+ rate: r.rate || 0,
335
+ minPayment: r.min_payment || 0,
336
+ type: r.type || "unknown",
337
+ nextDue: r.next_due || null,
338
+ })),
339
+ };
340
+ }
341
+ // Fallback: credit accounts
342
+ const credits = db.prepare(`SELECT name, current_balance as balance, type FROM accounts
343
+ WHERE type IN ('credit', 'loan') AND current_balance > 0
344
+ ORDER BY current_balance DESC`).all();
345
+ const totalDebt = credits.reduce((s, r) => s + (r.balance || 0), 0);
346
+ return {
347
+ totalDebt,
348
+ debts: credits.map((r) => ({
349
+ name: r.name,
350
+ balance: r.balance || 0,
351
+ rate: 0,
352
+ minPayment: 0,
353
+ type: r.type,
354
+ nextDue: null,
355
+ })),
356
+ };
357
+ }
358
+ // --- Spending comparison ---
359
+ export function compareSpending(db, period1Start, period1End, period2Start, period2End) {
360
+ const getByCategory = (start, end) => {
361
+ return db.prepare(`SELECT category, SUM(amount) as total FROM transactions
362
+ WHERE amount > 0 AND date BETWEEN ? AND ? AND pending = 0
363
+ AND category NOT IN ('TRANSFER_OUT', 'TRANSFER_IN', 'LOAN_PAYMENTS')
364
+ GROUP BY category`).all(start, end);
365
+ };
366
+ const p1 = getByCategory(period1Start, period1End);
367
+ const p2 = getByCategory(period2Start, period2End);
368
+ const p1Map = new Map(p1.map(r => [r.category, r.total]));
369
+ const p2Map = new Map(p2.map(r => [r.category, r.total]));
370
+ const allCats = new Set([...p1Map.keys(), ...p2Map.keys()]);
371
+ const categories = [...allCats].map(cat => ({
372
+ category: cat,
373
+ period1: Math.round((p1Map.get(cat) || 0) * 100) / 100,
374
+ period2: Math.round((p2Map.get(cat) || 0) * 100) / 100,
375
+ diff: Math.round(((p2Map.get(cat) || 0) - (p1Map.get(cat) || 0)) * 100) / 100,
376
+ })).sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff));
377
+ const period1Total = p1.reduce((s, r) => s + r.total, 0);
378
+ const period2Total = p2.reduce((s, r) => s + r.total, 0);
379
+ const difference = period2Total - period1Total;
380
+ return {
381
+ period1Total: Math.round(period1Total * 100) / 100,
382
+ period2Total: Math.round(period2Total * 100) / 100,
383
+ difference: Math.round(difference * 100) / 100,
384
+ pctChange: period1Total > 0 ? Math.round((difference / period1Total) * 1000) / 10 : 0,
385
+ categories,
386
+ };
387
+ }
388
+ // --- Net worth trend ---
389
+ export function getNetWorthTrend(db, limit = 30) {
390
+ return db.prepare(`SELECT date, net_worth, total_assets as assets, total_liabilities as liabilities
391
+ FROM net_worth_history ORDER BY date DESC LIMIT ?`).all(limit).reverse();
392
+ }
393
+ export function formatMoney(n) {
394
+ return "$" + Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
395
+ }
396
+ export function categoryLabel(cat) {
397
+ const labels = {
398
+ FOOD_AND_DRINK: "Food & Drink",
399
+ GENERAL_MERCHANDISE: "Shopping",
400
+ PERSONAL_CARE: "Personal Care",
401
+ ENTERTAINMENT: "Entertainment",
402
+ TRANSPORTATION: "Transportation",
403
+ GENERAL_SERVICES: "Services",
404
+ RENT_AND_UTILITIES: "Rent & Utilities",
405
+ LOAN_PAYMENTS: "Loan Payments",
406
+ GOVERNMENT_AND_NON_PROFIT: "Gov/Nonprofit",
407
+ MEDICAL: "Medical",
408
+ BANK_FEES: "Bank Fees",
409
+ };
410
+ return labels[cat] || cat;
411
+ }
@@ -0,0 +1,53 @@
1
+ import type BetterSqlite3 from "libsql";
2
+ type Database = BetterSqlite3.Database;
3
+ export interface DailyScore {
4
+ date: string;
5
+ score: number;
6
+ restaurant_count: number;
7
+ shopping_count: number;
8
+ food_spend: number;
9
+ total_spend: number;
10
+ zero_spend: boolean;
11
+ no_restaurant_streak: number;
12
+ no_shopping_streak: number;
13
+ on_pace_streak: number;
14
+ }
15
+ export interface Achievement {
16
+ key: string;
17
+ name: string;
18
+ description: string;
19
+ unlocked_at: string;
20
+ }
21
+ /**
22
+ * Calculate and store the daily score for a given date (defaults to yesterday).
23
+ * Should run during daily sync, after transactions are updated.
24
+ */
25
+ export declare function calculateDailyScore(db: Database, dateStr?: string): DailyScore;
26
+ /**
27
+ * Check and unlock any new achievements. Returns newly unlocked ones.
28
+ */
29
+ export declare function checkAchievements(db: Database): Achievement[];
30
+ /**
31
+ * Get monthly savings compared to the earliest full month of transaction data (dynamic baseline).
32
+ * If no full month exists yet, returns { saved: 0, ... }.
33
+ */
34
+ export declare function getMonthlySavings(db: Database): {
35
+ saved: number;
36
+ baselinePace: number;
37
+ currentPace: number;
38
+ daysCompared: number;
39
+ baselineMonth: string | null;
40
+ };
41
+ /**
42
+ * Get latest score + streaks for display.
43
+ */
44
+ export declare function getLatestScore(db: Database): DailyScore | null;
45
+ /**
46
+ * Get all unlocked achievements.
47
+ */
48
+ export declare function getAchievements(db: Database): Achievement[];
49
+ /**
50
+ * Get average score for current month.
51
+ */
52
+ export declare function getMonthAvgScore(db: Database): number | null;
53
+ export {};