LogicGoInfotechSpaces commited on
Commit
d154aad
·
verified ·
1 Parent(s): 9db7520

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +338 -52
app.py CHANGED
@@ -2,11 +2,12 @@
2
  """
3
  Financial Health Score Service (FastAPI)
4
 
5
- FINAL VERSION:
6
- - Uses GPT-4o-mini with JSON schema
7
- - Universal-safe JSON parsing (supports all SDK formats)
8
- - Correct OpenAI Chat Completions API
9
- - Bulletproof error handling
 
10
  """
11
 
12
  import json
@@ -21,7 +22,7 @@ from pydantic import BaseModel
21
  from pymongo import MongoClient
22
 
23
  # ===========================
24
- # ENV & CLIENT SETUP
25
  # ===========================
26
  load_dotenv()
27
 
@@ -29,21 +30,19 @@ MONGO_URI = os.getenv("MONGO_URI")
29
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
30
 
31
  if not MONGO_URI:
32
- raise RuntimeError("❌ MONGO_URI missing in environment variables")
33
 
34
  if not OPENAI_API_KEY:
35
- raise RuntimeError("❌ OPENAI_API_KEY missing in environment variables")
36
 
37
  mongo_client = MongoClient(MONGO_URI)
38
  default_db = mongo_client.get_default_database()
39
- if default_db is None:
40
- raise RuntimeError("Unable to determine default database from MONGO_URI")
41
 
42
- budget_collection = default_db["budget"]
43
  transaction_collection = default_db["transactions"]
44
  currencies_collection = default_db["currencies"]
 
45
 
46
- # OpenAI Client
47
  openai = OpenAI(api_key=OPENAI_API_KEY)
48
 
49
  app = FastAPI(title="Financial Health Score Service")
@@ -99,24 +98,17 @@ def normalize_transactions(txns):
99
  date_val = txn.get("date")
100
  date_str = date_val.date().isoformat() if isinstance(date_val, datetime) else None
101
 
102
- # Resolve currency
103
  currency_code = None
104
  currency_id = txn.get("currency")
105
 
106
  try:
107
  if isinstance(currency_id, ObjectId):
108
  doc = currencies_collection.find_one({"_id": currency_id})
109
- elif isinstance(currency_id, dict) and "$oid" in currency_id:
110
- doc = currencies_collection.find_one({"_id": ObjectId(currency_id["$oid"])})
111
- elif isinstance(currency_id, str):
112
  try:
113
  doc = currencies_collection.find_one({"_id": ObjectId(currency_id)})
114
  except:
115
- doc = currencies_collection.find_one({"code": currency_id}) or \
116
- currencies_collection.find_one({"currency": currency_id})
117
- else:
118
- doc = None
119
-
120
  if doc:
121
  currency_code = doc.get("code") or doc.get("currency")
122
  except:
@@ -131,36 +123,73 @@ def normalize_transactions(txns):
131
  return out
132
 
133
 
134
- def build_prompt(budgets, transactions):
135
- return f"""
136
- You are a financial wellness expert. Use the user's budgets and last 30 days of transactions
137
- to compute a financial health score from 0 to 100.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- Rules:
140
- - ALWAYS prefix currency amounts with currency code (e.g., "INR 10,000").
141
- - Keep explanation short (1–2 sentences).
142
- - Score must reflect spending, budget control, and overall balance.
143
- - Respond ONLY in the JSON schema required below.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  Budgets:
146
  {json.dumps(budgets, indent=2)}
147
 
148
  Transactions:
149
  {json.dumps(transactions, indent=2)}
 
 
 
150
  """
151
 
152
 
153
  # ===========================
154
  # ROUTES
155
  # ===========================
156
- @app.get("/")
157
- def root():
158
- return {"status": "Financial Health Score service running"}
159
-
160
-
161
  @app.post("/financial-score")
162
  def financial_score(payload: ScoreRequest):
163
- # Validate ObjectId
164
  try:
165
  user_id = ObjectId(payload.userId)
166
  except:
@@ -175,23 +204,24 @@ def financial_score(payload: ScoreRequest):
175
  txns_raw = list(
176
  transaction_collection.find(
177
  {"user": user_id, "date": {"$gte": thirty_days_ago}}
178
- ).sort("date", -1).limit(150)
179
  )
180
  transactions = normalize_transactions(txns_raw)
181
 
182
- # If no data
183
- if not budgets and not transactions:
 
 
 
 
184
  return {
185
  "userId": payload.userId,
186
  "score": 0,
187
- "explanation": "No budgets or recent transactions found."
188
  }
189
 
190
- prompt = build_prompt(budgets, transactions)
191
 
192
- # ===========================
193
- # GPT-4o-mini WITH JSON SCHEMA
194
- # ===========================
195
  try:
196
  response = openai.chat.completions.create(
197
  model="gpt-4o-mini",
@@ -212,24 +242,20 @@ def financial_score(payload: ScoreRequest):
212
  }
213
  },
214
  messages=[
215
- {"role": "system", "content": "You are a financial scoring engine."},
216
  {"role": "user", "content": prompt}
217
  ]
218
  )
219
  except Exception as exc:
220
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
221
 
222
- # ===========================
223
- # UNIVERSAL JSON PARSING (supports ALL SDK formats)
224
- # ===========================
225
  try:
226
  content = response.choices[0].message.content
227
 
228
- # Case A — list of dicts (new SDK)
229
  if isinstance(content, list):
230
  raw_json = content[0].get("text", content[0])
231
  else:
232
- # Case B — plain string (older SDK)
233
  raw_json = content
234
 
235
  if not isinstance(raw_json, str):
@@ -243,13 +269,11 @@ def financial_score(payload: ScoreRequest):
243
  detail=f"Could not parse JSON output: {e}"
244
  )
245
 
246
- # Score normalization
247
  score_val = parsed.get("score", 0)
248
  try:
249
  score_val = int(float(score_val))
250
  except:
251
  score_val = 0
252
-
253
  score_val = max(0, min(100, score_val))
254
 
255
  return {
@@ -257,3 +281,265 @@ def financial_score(payload: ScoreRequest):
257
  "score": score_val,
258
  "explanation": parsed.get("explanation")
259
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
  Financial Health Score Service (FastAPI)
4
 
5
+ FINAL + CREDIT CARD VERSION:
6
+ - Budget score
7
+ - Transaction score
8
+ - Credit card utilization & repayment score
9
+ - GPT-4o-mini with JSON schema
10
+ - Universal JSON parsing
11
  """
12
 
13
  import json
 
22
  from pymongo import MongoClient
23
 
24
  # ===========================
25
+ # ENV SETUP
26
  # ===========================
27
  load_dotenv()
28
 
 
30
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
31
 
32
  if not MONGO_URI:
33
+ raise RuntimeError("❌ MONGO_URI missing")
34
 
35
  if not OPENAI_API_KEY:
36
+ raise RuntimeError("❌ OPENAI_API_KEY missing")
37
 
38
  mongo_client = MongoClient(MONGO_URI)
39
  default_db = mongo_client.get_default_database()
 
 
40
 
41
+ budget_collection = default_db["budgets"]
42
  transaction_collection = default_db["transactions"]
43
  currencies_collection = default_db["currencies"]
44
+ creditcards_collection = default_db["creditcards"]
45
 
 
46
  openai = OpenAI(api_key=OPENAI_API_KEY)
47
 
48
  app = FastAPI(title="Financial Health Score Service")
 
98
  date_val = txn.get("date")
99
  date_str = date_val.date().isoformat() if isinstance(date_val, datetime) else None
100
 
 
101
  currency_code = None
102
  currency_id = txn.get("currency")
103
 
104
  try:
105
  if isinstance(currency_id, ObjectId):
106
  doc = currencies_collection.find_one({"_id": currency_id})
107
+ else:
 
 
108
  try:
109
  doc = currencies_collection.find_one({"_id": ObjectId(currency_id)})
110
  except:
111
+ doc = None
 
 
 
 
112
  if doc:
113
  currency_code = doc.get("code") or doc.get("currency")
114
  except:
 
123
  return out
124
 
125
 
126
+ def normalize_creditcards(cards):
127
+ out = []
128
+ for c in cards:
129
+ try:
130
+ start_date = c.get("billing_cycle", {}).get("start_date")
131
+ end_date = c.get("billing_cycle", {}).get("end_date")
132
+ start_date = start_date.date().isoformat() if isinstance(start_date, datetime) else None
133
+ end_date = end_date.date().isoformat() if isinstance(end_date, datetime) else None
134
+
135
+ due_date = c.get("due_date")
136
+ due_date = due_date.date().isoformat() if isinstance(due_date, datetime) else None
137
+ except:
138
+ start_date = end_date = due_date = None
139
+
140
+ out.append({
141
+ "card_name": c.get("card_name"),
142
+ "credit_limit": safe_number(c.get("credit_limit")),
143
+ "current_balance": safe_number(c.get("current_balance")),
144
+ "total_due_amount": safe_number(c.get("total_due_amount")),
145
+ "minimum_due": safe_number(c.get("minimum_due")),
146
+ "billing_cycle_start": start_date,
147
+ "billing_cycle_end": end_date,
148
+ "due_date": due_date,
149
+ })
150
+ return out
151
+
152
 
153
+ def build_prompt(budgets, transactions, creditcards):
154
+ return f"""
155
+ You are a financial health scoring engine.
156
+
157
+ Compute a score (0–100) considering:
158
+ 1. Budgets usage
159
+ 2. Spending patterns from recent transactions (last 30 days)
160
+ 3. Credit card health:
161
+ - utilization ratio (balance / credit_limit)
162
+ - due amount status
163
+ - repayment behavior
164
+ - risk from minimum-due payments
165
+ - future risk if due_date is near
166
+
167
+ Scoring rules:
168
+ - A lower credit utilization (<30%) increases score.
169
+ - High utilization (>70%) lowers score sharply.
170
+ - Overdue or large due amounts reduce score.
171
+ - Good budget management increases score.
172
+ - Overspending reduces score.
173
+ - Make explanation short (2 sentences max).
174
+ - MUST follow JSON schema.
175
 
176
  Budgets:
177
  {json.dumps(budgets, indent=2)}
178
 
179
  Transactions:
180
  {json.dumps(transactions, indent=2)}
181
+
182
+ CreditCards:
183
+ {json.dumps(creditcards, indent=2)}
184
  """
185
 
186
 
187
  # ===========================
188
  # ROUTES
189
  # ===========================
 
 
 
 
 
190
  @app.post("/financial-score")
191
  def financial_score(payload: ScoreRequest):
192
+ # Validate ID
193
  try:
194
  user_id = ObjectId(payload.userId)
195
  except:
 
204
  txns_raw = list(
205
  transaction_collection.find(
206
  {"user": user_id, "date": {"$gte": thirty_days_ago}}
207
+ ).sort("date", -1).limit(200)
208
  )
209
  transactions = normalize_transactions(txns_raw)
210
 
211
+ # Fetch credit cards
212
+ cards_raw = list(creditcards_collection.find({"user_id": user_id}))
213
+ creditcards = normalize_creditcards(cards_raw)
214
+
215
+ # No data
216
+ if not budgets and not transactions and not creditcards:
217
  return {
218
  "userId": payload.userId,
219
  "score": 0,
220
+ "explanation": "No financial data found."
221
  }
222
 
223
+ prompt = build_prompt(budgets, transactions, creditcards)
224
 
 
 
 
225
  try:
226
  response = openai.chat.completions.create(
227
  model="gpt-4o-mini",
 
242
  }
243
  },
244
  messages=[
245
+ {"role": "system", "content": "You calculate financial health."},
246
  {"role": "user", "content": prompt}
247
  ]
248
  )
249
  except Exception as exc:
250
  raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
251
 
252
+ # UNIVERSAL JSON PARSING
 
 
253
  try:
254
  content = response.choices[0].message.content
255
 
 
256
  if isinstance(content, list):
257
  raw_json = content[0].get("text", content[0])
258
  else:
 
259
  raw_json = content
260
 
261
  if not isinstance(raw_json, str):
 
269
  detail=f"Could not parse JSON output: {e}"
270
  )
271
 
 
272
  score_val = parsed.get("score", 0)
273
  try:
274
  score_val = int(float(score_val))
275
  except:
276
  score_val = 0
 
277
  score_val = max(0, min(100, score_val))
278
 
279
  return {
 
281
  "score": score_val,
282
  "explanation": parsed.get("explanation")
283
  }
284
+
285
+
286
+
287
+ # # main.py
288
+ # """
289
+ # Financial Health Score Service (FastAPI)
290
+
291
+ # FINAL VERSION:
292
+ # - Uses GPT-4o-mini with JSON schema
293
+ # - Universal-safe JSON parsing (supports all SDK formats)
294
+ # - Correct OpenAI Chat Completions API
295
+ # - Bulletproof error handling
296
+ # """
297
+
298
+ # import json
299
+ # import os
300
+ # from datetime import datetime, timedelta
301
+
302
+ # from bson import ObjectId
303
+ # from dotenv import load_dotenv
304
+ # from fastapi import FastAPI, HTTPException
305
+ # from openai import OpenAI
306
+ # from pydantic import BaseModel
307
+ # from pymongo import MongoClient
308
+
309
+ # # ===========================
310
+ # # ENV & CLIENT SETUP
311
+ # # ===========================
312
+ # load_dotenv()
313
+
314
+ # MONGO_URI = os.getenv("MONGO_URI")
315
+ # OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
316
+
317
+ # if not MONGO_URI:
318
+ # raise RuntimeError("❌ MONGO_URI missing in environment variables")
319
+
320
+ # if not OPENAI_API_KEY:
321
+ # raise RuntimeError("❌ OPENAI_API_KEY missing in environment variables")
322
+
323
+ # mongo_client = MongoClient(MONGO_URI)
324
+ # default_db = mongo_client.get_default_database()
325
+ # if default_db is None:
326
+ # raise RuntimeError("Unable to determine default database from MONGO_URI")
327
+
328
+ # budget_collection = default_db["budget"]
329
+ # transaction_collection = default_db["transactions"]
330
+ # currencies_collection = default_db["currencies"]
331
+
332
+ # # OpenAI Client
333
+ # openai = OpenAI(api_key=OPENAI_API_KEY)
334
+
335
+ # app = FastAPI(title="Financial Health Score Service")
336
+
337
+
338
+ # # ===========================
339
+ # # MODELS
340
+ # # ===========================
341
+ # class ScoreRequest(BaseModel):
342
+ # userId: str
343
+
344
+
345
+ # # ===========================
346
+ # # HELPERS
347
+ # # ===========================
348
+ # def safe_number(v):
349
+ # try:
350
+ # return float(v)
351
+ # except:
352
+ # return None
353
+
354
+
355
+ # def normalize_budgets(budgets):
356
+ # out = []
357
+ # for b in budgets:
358
+ # heads = []
359
+ # for h in b.get("headCategories", []) or []:
360
+ # heads.append({
361
+ # "spendLimitType": h.get("spendLimitType"),
362
+ # "spendAmount": safe_number(h.get("spendAmount")),
363
+ # "maxAmount": safe_number(h.get("maxAmount")),
364
+ # "remainingAmount": safe_number(h.get("remainingAmount")),
365
+ # "notifications": h.get("notifications") or []
366
+ # })
367
+
368
+ # out.append({
369
+ # "name": b.get("name"),
370
+ # "status": b.get("status"),
371
+ # "period": b.get("period"),
372
+ # "maxAmount": safe_number(b.get("maxAmount")),
373
+ # "spendAmount": safe_number(b.get("spendAmount")),
374
+ # "remainingAmount": safe_number(b.get("remainingAmount")),
375
+ # "rollover": b.get("rollover"),
376
+ # "notifications": b.get("notifications") or [],
377
+ # "headCategories": heads,
378
+ # })
379
+ # return out
380
+
381
+
382
+ # def normalize_transactions(txns):
383
+ # out = []
384
+ # for txn in txns:
385
+ # date_val = txn.get("date")
386
+ # date_str = date_val.date().isoformat() if isinstance(date_val, datetime) else None
387
+
388
+ # # Resolve currency
389
+ # currency_code = None
390
+ # currency_id = txn.get("currency")
391
+
392
+ # try:
393
+ # if isinstance(currency_id, ObjectId):
394
+ # doc = currencies_collection.find_one({"_id": currency_id})
395
+ # elif isinstance(currency_id, dict) and "$oid" in currency_id:
396
+ # doc = currencies_collection.find_one({"_id": ObjectId(currency_id["$oid"])})
397
+ # elif isinstance(currency_id, str):
398
+ # try:
399
+ # doc = currencies_collection.find_one({"_id": ObjectId(currency_id)})
400
+ # except:
401
+ # doc = currencies_collection.find_one({"code": currency_id}) or \
402
+ # currencies_collection.find_one({"currency": currency_id})
403
+ # else:
404
+ # doc = None
405
+
406
+ # if doc:
407
+ # currency_code = doc.get("code") or doc.get("currency")
408
+ # except:
409
+ # currency_code = None
410
+
411
+ # out.append({
412
+ # "type": txn.get("type"),
413
+ # "amount": safe_number(txn.get("amount")),
414
+ # "currency": currency_code,
415
+ # "date": date_str,
416
+ # })
417
+ # return out
418
+
419
+
420
+ # def build_prompt(budgets, transactions):
421
+ # return f"""
422
+ # You are a financial wellness expert. Use the user's budgets and last 30 days of transactions
423
+ # to compute a financial health score from 0 to 100.
424
+
425
+ # Rules:
426
+ # - ALWAYS prefix currency amounts with currency code (e.g., "INR 10,000").
427
+ # - Keep explanation short (1–2 sentences).
428
+ # - Score must reflect spending, budget control, and overall balance.
429
+ # - Respond ONLY in the JSON schema required below.
430
+
431
+ # Budgets:
432
+ # {json.dumps(budgets, indent=2)}
433
+
434
+ # Transactions:
435
+ # {json.dumps(transactions, indent=2)}
436
+ # """
437
+
438
+
439
+ # # ===========================
440
+ # # ROUTES
441
+ # # ===========================
442
+ # @app.get("/")
443
+ # def root():
444
+ # return {"status": "Financial Health Score service running"}
445
+
446
+
447
+ # @app.post("/financial-score")
448
+ # def financial_score(payload: ScoreRequest):
449
+ # # Validate ObjectId
450
+ # try:
451
+ # user_id = ObjectId(payload.userId)
452
+ # except:
453
+ # raise HTTPException(status_code=400, detail="Invalid userId")
454
+
455
+ # # Fetch budgets
456
+ # budgets_raw = list(budget_collection.find({"createdBy": user_id}))
457
+ # budgets = normalize_budgets(budgets_raw)
458
+
459
+ # # Fetch last 30 days transactions
460
+ # thirty_days_ago = datetime.utcnow() - timedelta(days=30)
461
+ # txns_raw = list(
462
+ # transaction_collection.find(
463
+ # {"user": user_id, "date": {"$gte": thirty_days_ago}}
464
+ # ).sort("date", -1).limit(150)
465
+ # )
466
+ # transactions = normalize_transactions(txns_raw)
467
+
468
+ # # If no data
469
+ # if not budgets and not transactions:
470
+ # return {
471
+ # "userId": payload.userId,
472
+ # "score": 0,
473
+ # "explanation": "No budgets or recent transactions found."
474
+ # }
475
+
476
+ # prompt = build_prompt(budgets, transactions)
477
+
478
+ # # ===========================
479
+ # # GPT-4o-mini WITH JSON SCHEMA
480
+ # # ===========================
481
+ # try:
482
+ # response = openai.chat.completions.create(
483
+ # model="gpt-4o-mini",
484
+ # temperature=0.6,
485
+ # response_format={
486
+ # "type": "json_schema",
487
+ # "json_schema": {
488
+ # "name": "financial_score",
489
+ # "schema": {
490
+ # "type": "object",
491
+ # "properties": {
492
+ # "score": {"type": "number"},
493
+ # "explanation": {"type": "string"}
494
+ # },
495
+ # "required": ["score", "explanation"],
496
+ # "additionalProperties": False
497
+ # }
498
+ # }
499
+ # },
500
+ # messages=[
501
+ # {"role": "system", "content": "You are a financial scoring engine."},
502
+ # {"role": "user", "content": prompt}
503
+ # ]
504
+ # )
505
+ # except Exception as exc:
506
+ # raise HTTPException(status_code=502, detail=f"OpenAI request failed: {exc}")
507
+
508
+ # # ===========================
509
+ # # UNIVERSAL JSON PARSING (supports ALL SDK formats)
510
+ # # ===========================
511
+ # try:
512
+ # content = response.choices[0].message.content
513
+
514
+ # # Case A — list of dicts (new SDK)
515
+ # if isinstance(content, list):
516
+ # raw_json = content[0].get("text", content[0])
517
+ # else:
518
+ # # Case B — plain string (older SDK)
519
+ # raw_json = content
520
+
521
+ # if not isinstance(raw_json, str):
522
+ # raw_json = str(raw_json)
523
+
524
+ # parsed = json.loads(raw_json)
525
+
526
+ # except Exception as e:
527
+ # raise HTTPException(
528
+ # status_code=502,
529
+ # detail=f"Could not parse JSON output: {e}"
530
+ # )
531
+
532
+ # # Score normalization
533
+ # score_val = parsed.get("score", 0)
534
+ # try:
535
+ # score_val = int(float(score_val))
536
+ # except:
537
+ # score_val = 0
538
+
539
+ # score_val = max(0, min(100, score_val))
540
+
541
+ # return {
542
+ # "userId": payload.userId,
543
+ # "score": score_val,
544
+ # "explanation": parsed.get("explanation")
545
+ # }