CineDev commited on
Commit
70ad179
·
1 Parent(s): 378bb89

feat: parse closing balance from bank statement and update profile balance

Browse files
Files changed (1) hide show
  1. importer/views.py +60 -19
importer/views.py CHANGED
@@ -83,17 +83,23 @@ class StatementUploadView(APIView):
83
  if not hf_api_key:
84
  return Response({"error": "Hugging Face API Key is not configured on the server."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
85
 
86
- # Limit text to avoid token limits (approx 5000 chars)
87
- prompt_text = full_text[:5000]
 
 
 
88
 
89
- prompt = f"""You are a financial data extraction assistant. Extract all transactions from the following bank statement text.
90
- Return the output STRICTLY as a JSON array of objects. Do not include markdown code block formatting (like ```json), other tags, or explanations. Each object must have these exact keys:
91
- - "date": string in YYYY-MM-DD format
92
- - "merchant": string (clean name of the merchant or person. Remove raw UPI IDs, transaction codes like UPI/DR/..., and account numbers to make it look clean).
93
- - "amount": number (positive number always)
94
- - "type": "expense" or "income" (CRITICAL: Use "expense" if the transaction is a debit, withdrawal, marked with "DR", "UPI/DR", "Paid out", "Sent to", or in the Debit/Withdrawal column. Use "income" if the transaction is a credit, deposit, marked with "CR", "UPI/CR", "Received in", "Received from", or in the Credit/Deposit column. Pay extreme attention to DR/CR markers in UPI transaction descriptions as they determine the true direction of the funds!)
95
- - "category_key": one of ["income", "dining", "coffee", "groceries", "transportation", "entertainment", "shopping", "healthcare", "education", "housing", "miscellaneous"]
96
- - "category": the corresponding display name (e.g. "Food & Dining" for "dining", "Income" for "income", "Miscellaneous" for "miscellaneous")
 
 
 
97
 
98
  Text:
99
  {prompt_text}
@@ -119,17 +125,39 @@ Text:
119
  result = response.json()
120
  bot_reply = result['choices'][0]['message']['content'].strip() if ('choices' in result and len(result['choices']) > 0) else ""
121
 
122
- # Robustly extract JSON array using regex
123
  import re
124
- json_match = re.search(r'\[.*\]', bot_reply, re.DOTALL)
125
- if json_match:
126
- bot_reply = json_match.group(0)
127
 
128
- try:
129
- transactions_data = json.loads(bot_reply)
130
- except Exception as e:
131
- print(f"Failed to parse JSON from AI: {e}. Reply was: {bot_reply}")
132
- return Response({"error": "AI generated invalid JSON. Please try again."}, status=status.HTTP_502_BAD_GATEWAY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  for tx in transactions_data:
135
  try:
@@ -164,6 +192,19 @@ Text:
164
  except Exception as e:
165
  print(f"Error creating transaction from AI data: {e}")
166
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  else:
169
  print(f"AI model error: {response.text}")
 
83
  if not hf_api_key:
84
  return Response({"error": "Hugging Face API Key is not configured on the server."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
85
 
86
+ # Get the first 6000 and the last 4000 characters of the text to ensure we capture the closing balance section printed at the end
87
+ if len(full_text) <= 10000:
88
+ prompt_text = full_text
89
+ else:
90
+ prompt_text = full_text[:6000] + "\n... [TRUNCATED] ...\n" + full_text[-4000:]
91
 
92
+ prompt = f"""You are a financial data extraction assistant. Analyze the bank statement text to extract the final/closing balance of the account and all transactions.
93
+ Return the output STRICTLY as a JSON object with the following keys. Do not include markdown code block formatting (like ```json), other tags, or explanations.
94
+ Keys:
95
+ - "closing_balance": number (the final/closing/ending balance of the account as shown in the statement. Look for phrases like "Closing Balance", "Ending Balance", "Balance Carried Forward", "Available Balance", or the balance listed in the last transaction row. Do not include currency symbols or commas, just a clean float/decimal.)
96
+ - "transactions": a JSON array of objects, where each object has these exact keys:
97
+ - "date": string in YYYY-MM-DD format
98
+ - "merchant": string (clean name of the merchant or person. Remove raw UPI IDs, transaction codes like UPI/DR/..., and account numbers to make it look clean).
99
+ - "amount": number (positive number always)
100
+ - "type": "expense" or "income" (CRITICAL: Use "expense" if the transaction is a debit, withdrawal, marked with "DR", "UPI/DR", "Paid out", "Sent to", or in the Debit/Withdrawal column. Use "income" if the transaction is a credit, deposit, marked with "CR", "UPI/CR", "Received in", "Received from", or in the Credit/Deposit column. Pay extreme attention to DR/CR markers in UPI transaction descriptions as they determine the true direction of the funds!)
101
+ - "category_key": one of ["income", "dining", "coffee", "groceries", "transportation", "entertainment", "shopping", "healthcare", "education", "housing", "miscellaneous"]
102
+ - "category": the corresponding display name (e.g. "Food & Dining" for "dining", "Income" for "income", "Miscellaneous" for "miscellaneous")
103
 
104
  Text:
105
  {prompt_text}
 
125
  result = response.json()
126
  bot_reply = result['choices'][0]['message']['content'].strip() if ('choices' in result and len(result['choices']) > 0) else ""
127
 
128
+ # Robustly extract JSON using regex
129
  import re
130
+ parsed_data = None
 
 
131
 
132
+ # Try to extract the JSON object first
133
+ object_match = re.search(r'\{.*\}', bot_reply, re.DOTALL)
134
+ if object_match:
135
+ try:
136
+ parsed_data = json.loads(object_match.group(0))
137
+ except Exception as parse_err:
138
+ print(f"Failed to parse JSON object from match: {parse_err}")
139
+
140
+ # Fallback: if no object or object parsing failed, check for array
141
+ if not parsed_data:
142
+ array_match = re.search(r'\[.*\]', bot_reply, re.DOTALL)
143
+ if array_match:
144
+ try:
145
+ transactions_data = json.loads(array_match.group(0))
146
+ parsed_data = {"transactions": transactions_data, "closing_balance": None}
147
+ except Exception as parse_err:
148
+ print(f"Failed to parse JSON array from match: {parse_err}")
149
+
150
+ if not parsed_data:
151
+ print(f"Failed to parse JSON from AI reply. Reply was: {bot_reply}")
152
+ return Response({"error": "AI generated invalid JSON structure. Please try again."}, status=status.HTTP_502_BAD_GATEWAY)
153
+
154
+ # Extract components
155
+ if isinstance(parsed_data, dict):
156
+ transactions_data = parsed_data.get('transactions', [])
157
+ closing_balance = parsed_data.get('closing_balance')
158
+ else:
159
+ transactions_data = parsed_data
160
+ closing_balance = None
161
 
162
  for tx in transactions_data:
163
  try:
 
192
  except Exception as e:
193
  print(f"Error creating transaction from AI data: {e}")
194
  continue
195
+
196
+ # If a closing balance was successfully parsed, set user's profile balance to it
197
+ if closing_balance is not None:
198
+ try:
199
+ closing_dec = Decimal(str(closing_balance))
200
+ from users.models import FinancialProfile
201
+ profile, _ = FinancialProfile.objects.get_or_create(user=request.user)
202
+ profile.cash_available = closing_dec
203
+ profile.net_worth = closing_dec + profile.invested_amount - profile.credit_used
204
+ profile.save()
205
+ print(f"Successfully updated user profile cash_available to: {closing_dec}")
206
+ except Exception as profile_err:
207
+ print(f"Error updating user profile balance: {profile_err}")
208
 
209
  else:
210
  print(f"AI model error: {response.text}")