feat: parse closing balance from bank statement and update profile balance
Browse files- 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 |
-
#
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
prompt = f"""You are a financial data extraction assistant.
|
| 90 |
-
Return the output STRICTLY as a JSON
|
| 91 |
-
|
| 92 |
-
- "
|
| 93 |
-
- "
|
| 94 |
-
- "
|
| 95 |
-
- "
|
| 96 |
-
- "
|
|
|
|
|
|
|
|
|
|
| 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
|
| 123 |
import re
|
| 124 |
-
|
| 125 |
-
if json_match:
|
| 126 |
-
bot_reply = json_match.group(0)
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}")
|