Charts_5
Browse files- 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
|