MJ-Prod commited on
Commit
68283bc
·
1 Parent(s): 82f4fa3
Files changed (1) hide show
  1. plaid_client.py +253 -0
plaid_client.py CHANGED
@@ -41,6 +41,41 @@ def get_balances(access_token: str) -> str:
41
  request = AccountsBalanceGetRequest(access_token=access_token)
42
  response = plaid_client.accounts_balance_get(request)
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  lines = ["USER'S CURRENT ACCOUNT BALANCES:"]
45
  for account in response['accounts']:
46
  name = account['name']
@@ -71,6 +106,95 @@ def get_transactions(access_token: str, days: int = 30) -> str:
71
  start_date = end_date - timedelta(days=days)
72
  week_start = end_date - timedelta(days=end_date.weekday())
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  request = TransactionsGetRequest(
75
  access_token=access_token,
76
  start_date=start_date,
@@ -207,6 +331,135 @@ def exchange_public_token(public_token: str) -> str:
207
 
208
 
209
  def get_chart_data(access_token: str) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  """Returns burn-down velocity chart data."""
211
  end_date = date.today()
212
  start_date = end_date.replace(day=1) # Start of current month
 
41
  request = AccountsBalanceGetRequest(access_token=access_token)
42
  response = plaid_client.accounts_balance_get(request)
43
 
44
+ lines = ["USER'S CURRENT ACCOUNT BALANCES:"]
45
+ for account in response['accounts']:
46
+ name = account['name']
47
+ subtype = str(account['subtype'])
48
+ current = account['balances']['current']
49
+
50
+ if current is None:
51
+ continue
52
+
53
+ # Skip mortgage and line of credit — not relevant for daily budgeting
54
+ if subtype in ['mortgage', 'line of credit']:
55
+ continue
56
+
57
+ if subtype == 'credit card':
58
+ limit = account['balances']['limit'] or 0
59
+ owing = current
60
+ available = limit - current if limit > 0 else 0
61
+ lines.append(
62
+ f"- {name} (Credit Card): "
63
+ f"${owing:.2f} owing, ${available:.2f} available out of ${limit:.2f} limit"
64
+ )
65
+ elif subtype == 'checking':
66
+ available = account['balances']['available'] or current
67
+ lines.append(f"- {name} (Chequing): ${current:.2f} balance, ${available:.2f} available")
68
+ elif subtype == 'savings':
69
+ lines.append(f"- {name} (Savings): ${current:.2f}")
70
+ elif subtype == 'rrsp':
71
+ lines.append(f"- {name} (RRSP): ${current:.2f}")
72
+ else:
73
+ lines.append(f"- {name} ({subtype}): ${current:.2f}")
74
+
75
+ return "\n".join(lines)
76
+ request = AccountsBalanceGetRequest(access_token=access_token)
77
+ response = plaid_client.accounts_balance_get(request)
78
+
79
  lines = ["USER'S CURRENT ACCOUNT BALANCES:"]
80
  for account in response['accounts']:
81
  name = account['name']
 
106
  start_date = end_date - timedelta(days=days)
107
  week_start = end_date - timedelta(days=end_date.weekday())
108
 
109
+ request = TransactionsGetRequest(
110
+ access_token=access_token,
111
+ start_date=start_date,
112
+ end_date=end_date,
113
+ options=TransactionsGetRequestOptions(
114
+ count=200,
115
+ include_personal_finance_category=True
116
+ )
117
+ )
118
+ response = plaid_client.transactions_get(request)
119
+ transactions = response['transactions']
120
+
121
+ if not transactions:
122
+ return "No transactions found in the last 30 days."
123
+
124
+ today_total = 0.0
125
+ week_total = 0.0
126
+ month_total = 0.0
127
+ income_total = 0.0
128
+ category_totals: dict[str, float] = {}
129
+
130
+ for txn in transactions:
131
+ amount = txn['amount']
132
+ name = txn['name']
133
+ txn_date = txn['date']
134
+ category = txn.get('personal_finance_category', {}).get('primary', 'OTHER')
135
+
136
+ # Skip internal transfers between own accounts
137
+ if category in ['TRANSFER_IN', 'TRANSFER_OUT']:
138
+ continue
139
+
140
+ # Detect payroll mislabeled as loan payments
141
+ name_lower = name.lower()
142
+ if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']):
143
+ income_total += amount
144
+ continue
145
+
146
+ # Skip other loan payments (mortgage, LOC) — not daily spending
147
+ if category == 'LOAN_PAYMENTS':
148
+ continue
149
+
150
+ # Income (negative = money in)
151
+ if amount < 0:
152
+ income_total += abs(amount)
153
+ continue
154
+
155
+ # Spending
156
+ month_total += amount
157
+ readable = category.replace("_", " ").title()
158
+ category_totals[readable] = category_totals.get(readable, 0) + amount
159
+
160
+ if txn_date >= week_start:
161
+ week_total += amount
162
+
163
+ if txn_date == end_date:
164
+ today_total += amount
165
+
166
+ lines = ["FINANCIAL SUMMARY (pre-calculated, do NOT recalculate):"]
167
+ lines.append(f"Today's spending: ${today_total:.2f}")
168
+ lines.append(f"This week's spending (since {week_start}): ${week_total:.2f}")
169
+ lines.append(f"This month's spending (last {days} days): ${month_total:.2f}")
170
+
171
+ if income_total > 0:
172
+ lines.append(f"Income received (last {days} days): +${income_total:.2f}")
173
+
174
+ lines.append(f"Average daily spending: ${month_total / max(days, 1):.2f}")
175
+
176
+ lines.append("\nSPENDING BY CATEGORY:")
177
+ for category, total in sorted(category_totals.items(), key=lambda x: -x[1]):
178
+ pct = (total / month_total * 100) if month_total > 0 else 0
179
+ lines.append(f"- {category}: ${total:.2f} ({pct:.0f}%)")
180
+
181
+ lines.append("\nRECENT TRANSACTIONS (last 5):")
182
+ count = 0
183
+ for txn in transactions:
184
+ if count >= 5:
185
+ break
186
+ amount = txn['amount']
187
+ category = txn.get('personal_finance_category', {}).get('primary', 'OTHER')
188
+ if amount <= 0 or category in ['TRANSFER_IN', 'TRANSFER_OUT', 'LOAN_PAYMENTS']:
189
+ continue
190
+ lines.append(f" - {txn['date']} {txn['name']}: ${amount:.2f}")
191
+ count += 1
192
+
193
+ return "\n".join(lines)
194
+ end_date = date.today()
195
+ start_date = end_date - timedelta(days=days)
196
+ week_start = end_date - timedelta(days=end_date.weekday())
197
+
198
  request = TransactionsGetRequest(
199
  access_token=access_token,
200
  start_date=start_date,
 
331
 
332
 
333
  def get_chart_data(access_token: str) -> dict:
334
+ """Returns burn-down velocity chart data based on chequing account."""
335
+ end_date = date.today()
336
+ start_date = end_date.replace(day=1)
337
+
338
+ # Get balances
339
+ accounts = []
340
+ chequing_balance = 0.0
341
+ chequing_account_id = None
342
+
343
+ try:
344
+ balance_req = AccountsBalanceGetRequest(access_token=access_token)
345
+ balance_resp = plaid_client.accounts_balance_get(balance_req)
346
+ for account in balance_resp['accounts']:
347
+ current = account['balances']['current']
348
+ if current is None:
349
+ continue
350
+ subtype = str(account['subtype'])
351
+
352
+ # Find the main chequing account
353
+ if subtype == 'checking':
354
+ chequing_balance = current
355
+ chequing_account_id = account['account_id']
356
+
357
+ # Skip mortgage and line of credit from display
358
+ if subtype in ['mortgage', 'line of credit']:
359
+ continue
360
+
361
+ accounts.append({
362
+ "name": account['name'],
363
+ "type": subtype,
364
+ "balance": round(current, 2),
365
+ })
366
+ except Exception as e:
367
+ print(f"Balance error in chart_data: {e}", flush=True)
368
+
369
+ # Get transactions
370
+ burndown = []
371
+ income_total = 0.0
372
+ expense_total = 0.0
373
+
374
+ try:
375
+ request = TransactionsGetRequest(
376
+ access_token=access_token,
377
+ start_date=start_date,
378
+ end_date=end_date,
379
+ options=TransactionsGetRequestOptions(
380
+ count=300,
381
+ include_personal_finance_category=True
382
+ )
383
+ )
384
+ response = plaid_client.transactions_get(request)
385
+ transactions = response['transactions']
386
+
387
+ # Separate spending by day across chequing + credit cards
388
+ daily_net: dict[str, float] = {}
389
+
390
+ for txn in transactions:
391
+ amount = txn['amount']
392
+ txn_date = str(txn['date'])
393
+ category = txn.get('personal_finance_category', {}).get('primary', 'OTHER')
394
+
395
+ # Skip internal transfers between own accounts
396
+ if category in ['TRANSFER_IN', 'TRANSFER_OUT']:
397
+ continue
398
+
399
+ # Skip loan payments that are actually payroll
400
+ name_lower = txn['name'].lower()
401
+ if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']):
402
+ income_total += amount
403
+ daily_net[txn_date] = daily_net.get(txn_date, 0) - amount # income adds to balance
404
+ continue
405
+
406
+ if amount < 0:
407
+ # Money coming in (income)
408
+ income_total += abs(amount)
409
+ daily_net[txn_date] = daily_net.get(txn_date, 0) + abs(amount) # adds to balance
410
+ else:
411
+ # Money going out (spending)
412
+ expense_total += amount
413
+ daily_net[txn_date] = daily_net.get(txn_date, 0) - amount # subtracts from balance
414
+
415
+ # Reconstruct daily balance working BACKWARD from today's chequing balance
416
+ # today_balance = chequing_balance
417
+ # yesterday_balance = today_balance - net_change_today
418
+ # (because net_change is already applied to get to today's balance)
419
+
420
+ daily_balances: dict[str, float] = {}
421
+ running = chequing_balance
422
+
423
+ # Work backward from today
424
+ current = end_date
425
+ while current >= start_date:
426
+ day_str = str(current)
427
+ daily_balances[day_str] = running
428
+ # Undo this day's net change to get previous day's balance
429
+ net_today = daily_net.get(day_str, 0)
430
+ running = running - net_today # undo: if we spent $50 (net=-50), previous day was +50 higher
431
+ current -= timedelta(days=1)
432
+
433
+ # Now build burndown in forward order
434
+ current = start_date
435
+ while current <= end_date:
436
+ day_str = str(current)
437
+ balance = daily_balances.get(day_str, 0)
438
+ day_spend = 0
439
+
440
+ # Calculate just spending for this day (not net)
441
+ for txn in transactions:
442
+ if str(txn['date']) == day_str and txn['amount'] > 0:
443
+ cat = txn.get('personal_finance_category', {}).get('primary', 'OTHER')
444
+ if cat not in ['TRANSFER_IN', 'TRANSFER_OUT']:
445
+ day_spend += txn['amount']
446
+
447
+ burndown.append({
448
+ "date": current.strftime("%b %d"),
449
+ "balance": round(balance, 2),
450
+ "spent": round(day_spend, 2),
451
+ })
452
+ current += timedelta(days=1)
453
+
454
+ except Exception as e:
455
+ print(f"Transaction error in chart_data: {e}", flush=True)
456
+
457
+ return {
458
+ "burndown": burndown,
459
+ "income_total": round(income_total, 2),
460
+ "expense_total": round(expense_total, 2),
461
+ "accounts": accounts,
462
+ }
463
  """Returns burn-down velocity chart data."""
464
  end_date = date.today()
465
  start_date = end_date.replace(day=1) # Start of current month