MJ-Prod commited on
Commit
e800f23
·
1 Parent(s): ad8bcbc
Files changed (2) hide show
  1. app.py +19 -0
  2. plaid_client.py +109 -1
app.py CHANGED
@@ -258,6 +258,25 @@ def plaid_update_link_token(
258
  raise HTTPException(status_code=500, detail=str(e))
259
 
260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  @app.delete("/account")
262
  def delete_account(
263
  credentials: HTTPAuthorizationCredentials = Depends(security),
 
258
  raise HTTPException(status_code=500, detail=str(e))
259
 
260
 
261
+ @app.post("/plaid/chart_data")
262
+ def plaid_chart_data(
263
+ request: ChatRequest,
264
+ credentials: HTTPAuthorizationCredentials = Depends(security),
265
+ ):
266
+ user = verify_token(credentials.credentials)
267
+ if not user:
268
+ raise HTTPException(status_code=401, detail="Unauthorized")
269
+
270
+ access_token = request.access_token if request.access_token else SANDBOX_ACCESS_TOKEN
271
+
272
+ try:
273
+ from plaid_client import get_chart_data
274
+ data = get_chart_data(access_token)
275
+ return data
276
+ except Exception as e:
277
+ print(f"Chart data error: {e}", flush=True)
278
+ raise HTTPException(status_code=500, detail=str(e))
279
+
280
  @app.delete("/account")
281
  def delete_account(
282
  credentials: HTTPAuthorizationCredentials = Depends(security),
plaid_client.py CHANGED
@@ -203,4 +203,112 @@ def exchange_public_token(public_token: str) -> str:
203
  """Exchanges a public token for a permanent access token."""
204
  request = ItemPublicTokenExchangeRequest(public_token=public_token)
205
  response = plaid_client.item_public_token_exchange(request)
206
- return response["access_token"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  """Exchanges a public token for a permanent access token."""
204
  request = ItemPublicTokenExchangeRequest(public_token=public_token)
205
  response = plaid_client.item_public_token_exchange(request)
206
+ return response["access_token"]
207
+
208
+
209
+ def get_chart_data(access_token: str) -> dict:
210
+ """Returns structured JSON for frontend charts."""
211
+ end_date = date.today()
212
+ start_date = end_date - timedelta(days=30)
213
+
214
+ # Get transactions
215
+ request = TransactionsGetRequest(
216
+ access_token=access_token,
217
+ start_date=start_date,
218
+ end_date=end_date,
219
+ options=TransactionsGetRequestOptions(
220
+ count=200,
221
+ include_personal_finance_category=True
222
+ )
223
+ )
224
+ response = plaid_client.transactions_get(request)
225
+ transactions = response['transactions']
226
+
227
+ # Get balances
228
+ balance_req = AccountsBalanceGetRequest(access_token=access_token)
229
+ balance_resp = plaid_client.accounts_balance_get(balance_req)
230
+
231
+ # 1. Spending by category
232
+ category_totals: dict[str, float] = {}
233
+ income_total = 0.0
234
+ expense_total = 0.0
235
+
236
+ # 2. Daily spending for weekly breakdown
237
+ daily_spending: dict[str, float] = {}
238
+ daily_income: dict[str, float] = {}
239
+
240
+ for txn in transactions:
241
+ amount = txn['amount']
242
+ txn_date = str(txn['date'])
243
+ category = txn.get('personal_finance_category', {}).get('primary', 'OTHER')
244
+
245
+ if amount < 0:
246
+ income_total += abs(amount)
247
+ daily_income[txn_date] = daily_income.get(txn_date, 0) + abs(amount)
248
+ continue
249
+
250
+ if category in ['TRANSFER_IN', 'TRANSFER_OUT']:
251
+ continue
252
+
253
+ name_lower = txn['name'].lower()
254
+ if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']):
255
+ income_total += amount
256
+ daily_income[txn_date] = daily_income.get(txn_date, 0) + amount
257
+ continue
258
+
259
+ expense_total += amount
260
+ readable = category.replace("_", " ").title()
261
+ category_totals[readable] = category_totals.get(readable, 0) + amount
262
+ daily_spending[txn_date] = daily_spending.get(txn_date, 0) + amount
263
+
264
+ # Build category chart data (sorted by amount)
265
+ category_chart = [
266
+ {"name": cat, "value": round(total, 2)}
267
+ for cat, total in sorted(category_totals.items(), key=lambda x: -x[1])
268
+ ]
269
+
270
+ # Build weekly chart data (last 4 weeks)
271
+ weekly_data = []
272
+ for i in range(4):
273
+ week_end = end_date - timedelta(weeks=i)
274
+ week_start = week_end - timedelta(days=6)
275
+ week_label = f"{week_start.strftime('%b %d')} - {week_end.strftime('%b %d')}"
276
+
277
+ week_spending = sum(
278
+ v for k, v in daily_spending.items()
279
+ if str(week_start) <= k <= str(week_end)
280
+ )
281
+ week_income = sum(
282
+ v for k, v in daily_income.items()
283
+ if str(week_start) <= k <= str(week_end)
284
+ )
285
+
286
+ weekly_data.append({
287
+ "week": week_label,
288
+ "spending": round(week_spending, 2),
289
+ "income": round(week_income, 2),
290
+ })
291
+
292
+ weekly_data.reverse()
293
+
294
+ # Build account balances
295
+ accounts = []
296
+ for account in balance_resp['accounts']:
297
+ current = account['balances']['current']
298
+ if current is None:
299
+ continue
300
+ accounts.append({
301
+ "name": account['name'],
302
+ "type": str(account['subtype']),
303
+ "balance": round(current, 2),
304
+ })
305
+
306
+ return {
307
+ "category_chart": category_chart,
308
+ "weekly_chart": weekly_data,
309
+ "income_vs_expenses": {
310
+ "income": round(income_total, 2),
311
+ "expenses": round(expense_total, 2),
312
+ },
313
+ "accounts": accounts,
314
+ }