LogicGoInfotechSpaces commited on
Commit
6ec895c
·
1 Parent(s): 1204d36

Clean up code: Remove commented duplicate code, fix category filtering logic, improve exception handling

Browse files
Files changed (1) hide show
  1. app/smart_recommendation.py +2 -398
app/smart_recommendation.py CHANGED
@@ -274,7 +274,7 @@ class SmartBudgetRecommender:
274
  else:
275
  try:
276
  category_doc = self.db.categories.find_one({"_id": ObjectId(category_id)})
277
- except:
278
  category_doc = self.db.categories.find_one({"_id": category_id})
279
 
280
  if category_doc:
@@ -315,41 +315,28 @@ class SmartBudgetRecommender:
315
  try:
316
  query_str = {"createdBy": user_id}
317
  budgets_str = list(self.db.budgets.find(query_str))
318
- <<<<<<< HEAD
319
  print(f"Pattern 2 (createdBy string): Found {len(budgets_str)} budgets")
320
  if budgets_str:
321
  budgets.extend(budgets_str)
322
  except Exception as e:
323
  print(f"Pattern 2 failed: {e}")
324
- =======
325
- if budgets_str:
326
- budgets.extend(budgets_str)
327
- except Exception:
328
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
329
  pass
330
 
331
  # Pattern 3: Try with user_id field (alternative field name) - no status filter
332
  try:
333
  query_userid = {"user_id": user_id}
334
  budgets_userid = list(self.db.budgets.find(query_userid))
335
- <<<<<<< HEAD
336
  print(f"Pattern 3 (user_id string): Found {len(budgets_userid)} budgets")
337
  if budgets_userid:
338
  budgets.extend(budgets_userid)
339
  except Exception as e:
340
  print(f"Pattern 3 failed: {e}")
341
- =======
342
- if budgets_userid:
343
- budgets.extend(budgets_userid)
344
- except Exception:
345
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
346
  pass
347
 
348
  # Pattern 4: Try ObjectId with user_id field - no status filter
349
  try:
350
  query_objid_userid = {"user_id": ObjectId(user_id)}
351
  budgets_objid_userid = list(self.db.budgets.find(query_objid_userid))
352
- <<<<<<< HEAD
353
  print(f"Pattern 4 (user_id ObjectId): Found {len(budgets_objid_userid)} budgets")
354
  if budgets_objid_userid:
355
  budgets.extend(budgets_objid_userid)
@@ -382,11 +369,6 @@ class SmartBudgetRecommender:
382
  budgets.append(budget_by_id_str)
383
  except Exception as e:
384
  print(f"Pattern 6 failed: {e}")
385
- =======
386
- if budgets_objid_userid:
387
- budgets.extend(budgets_objid_userid)
388
- except Exception:
389
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
390
  pass
391
 
392
  # Remove duplicates based on _id
@@ -402,15 +384,12 @@ class SmartBudgetRecommender:
402
 
403
  if not budgets:
404
  print(f"No budgets found for user_id: {user_id}")
405
- <<<<<<< HEAD
406
  print(f"Tried all query patterns. Checking sample budget structure...")
407
  # Get a sample budget to see the structure
408
  sample = self.db.budgets.find_one()
409
  if sample:
410
  print(f"Sample budget structure - createdBy type: {type(sample.get('createdBy')).__name__}, value: {sample.get('createdBy')}")
411
  print(f"Sample budget has user_id field: {'user_id' in sample}")
412
- =======
413
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
414
  return {}
415
 
416
  print(f"Found {len(budgets)} budgets for user_id: {user_id}")
@@ -428,17 +407,12 @@ class SmartBudgetRecommender:
428
 
429
  # Get headCategory ID and amounts
430
  head_cat_id = head_cat.get("headCategory")
431
- <<<<<<< HEAD
432
  try:
433
  head_cat_max = float(head_cat.get("maxAmount", 0) or 0)
434
  head_cat_spend = float(head_cat.get("spendAmount", 0) or 0)
435
  except (ValueError, TypeError):
436
  head_cat_max = 0
437
  head_cat_spend = 0
438
- =======
439
- head_cat_max = float(head_cat.get("maxAmount", 0) or 0)
440
- head_cat_spend = float(head_cat.get("spendAmount", 0) or 0)
441
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
442
 
443
  # Process nested categories within headCategory
444
  nested_categories = head_cat.get("categories", [])
@@ -448,7 +422,6 @@ class SmartBudgetRecommender:
448
  continue
449
 
450
  nested_cat_id = nested_cat.get("category")
451
- <<<<<<< HEAD
452
  try:
453
  nested_cat_max = float(nested_cat.get("maxAmount", 0) or 0)
454
  nested_cat_spend = float(nested_cat.get("spendAmount", 0) or 0)
@@ -459,14 +432,6 @@ class SmartBudgetRecommender:
459
 
460
  # Only include categories with limits (must have maxAmount > 0)
461
  if nested_cat_max > 0:
462
- =======
463
- nested_cat_max = float(nested_cat.get("maxAmount", 0) or 0)
464
- nested_cat_spend = float(nested_cat.get("spendAmount", 0) or 0)
465
- spend_limit_type = nested_cat.get("spendLimitType", "NO_LIMIT")
466
-
467
- # Only include categories with limits (maxAmount > 0 or spendLimitType != "NO_LIMIT")
468
- if nested_cat_max > 0 or (spend_limit_type != "NO_LIMIT" and nested_cat_spend > 0):
469
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
470
  # Look up actual category name
471
  nested_category_name = self._get_category_name(nested_cat_id)
472
  nested_base_amount = nested_cat_spend if nested_cat_spend > 0 else nested_cat_max
@@ -518,7 +483,6 @@ class SmartBudgetRecommender:
518
  budget_name = b.get("category") or b.get("title") or "Uncategorized"
519
 
520
  # Derive a base amount from WalletSync fields
521
- <<<<<<< HEAD
522
  try:
523
  max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
524
  spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
@@ -527,11 +491,6 @@ class SmartBudgetRecommender:
527
  max_amount = 0
528
  spend_amount = 0
529
  budget_amount = 0
530
- =======
531
- max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
532
- spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
533
- budget_amount = float(b.get("budget", 0) or b.get("budgetAmount", 0) or 0)
534
- >>>>>>> fa62699d4d97bb637c5b2d83906f22ae98ede56a
535
 
536
  # Priority: spendAmount > maxAmount > budgetAmount > budget
537
  if spend_amount > 0:
@@ -618,359 +577,4 @@ class SmartBudgetRecommender:
618
  return json.loads(content)
619
  except Exception as exc:
620
  print(f"OpenAI recommendation error for {category}: {exc}")
621
- return None
622
-
623
-
624
- # import json
625
- # import math
626
- # import os
627
- # from collections import defaultdict
628
- # from datetime import datetime, timedelta
629
- # from typing import Dict, List
630
-
631
- # import requests
632
- # from dotenv import load_dotenv
633
- # from bson import ObjectId
634
-
635
- # from app.models import BudgetRecommendation, CategoryExpense
636
-
637
- # load_dotenv()
638
- # OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
639
-
640
- # class SmartBudgetRecommender:
641
- # """
642
- # Smart Budget Recommendation Engine
643
-
644
- # Analyzes past spending behavior and recommends personalized budgets
645
- # for each category based on historical data.
646
- # """
647
-
648
- # def __init__(self, db):
649
- # self.db = db
650
-
651
- # def get_recommendations(self, user_id: str, month: int, year: int) -> List[BudgetRecommendation]:
652
- # """
653
- # Get budget recommendations for all categories based on past behavior.
654
-
655
- # Args:
656
- # user_id: User identifier
657
- # month: Target month (1-12)
658
- # year: Target year
659
-
660
- # Returns:
661
- # List of budget recommendations for each category
662
- # """
663
- # # 1) Try to build stats from existing budgets for this user (createdBy)
664
- # category_data = self._get_category_stats_from_budgets(user_id, month, year)
665
-
666
- # # 2) If there are no budgets, fall back to expenses history
667
- # if not category_data:
668
- # end_date = datetime(year, month, 1) - timedelta(days=1)
669
- # start_date = end_date - timedelta(days=180) # ~6 months
670
-
671
- # expenses = list(
672
- # self.db.expenses.find(
673
- # {
674
- # "user_id": user_id,
675
- # "date": {"$gte": start_date, "$lte": end_date},
676
- # "type": "expense",
677
- # }
678
- # )
679
- # )
680
-
681
- # if not expenses:
682
- # return []
683
-
684
- # # Group expenses by category and calculate monthly averages
685
- # category_data = self._calculate_category_statistics(
686
- # expenses, start_date, end_date
687
- # )
688
-
689
- # recommendations: List[BudgetRecommendation] = []
690
-
691
- # for category, data in category_data.items():
692
- # avg_expense = data["average_monthly"]
693
- # confidence = self._calculate_confidence(data)
694
-
695
- # # 1) Try OpenAI first (primary source of recommendation)
696
- # ai_result = self._get_ai_recommendation(category, data, avg_expense)
697
- # if ai_result:
698
- # recommended_budget = ai_result.get("recommended_budget")
699
- # reason = ai_result.get("reason")
700
- # action = ai_result.get("action")
701
- # else:
702
- # # 2) Fallback to rule-based recommendation
703
- # recommended_budget = self._calculate_recommended_budget(avg_expense, data)
704
- # reason = self._generate_reason(category, avg_expense, recommended_budget)
705
- # action = None
706
-
707
- # recommendations.append(BudgetRecommendation(
708
- # category=category,
709
- # average_expense=round(avg_expense, 2),
710
- # recommended_budget=round(recommended_budget or 0, 2),
711
- # reason=reason,
712
- # confidence=confidence,
713
- # action=action
714
- # ))
715
-
716
- # # Sort by average expense (highest first)
717
- # recommendations.sort(key=lambda x: x.average_expense, reverse=True)
718
-
719
- # return recommendations
720
-
721
- # def _calculate_category_statistics(self, expenses: List[Dict], start_date: datetime, end_date: datetime) -> Dict:
722
- # """Calculate statistics for each category"""
723
- # category_data = defaultdict(lambda: {
724
- # "total": 0,
725
- # "count": 0,
726
- # "months": set(),
727
- # "monthly_totals": defaultdict(float)
728
- # })
729
-
730
- # for expense in expenses:
731
- # category = expense.get("category", "Uncategorized")
732
- # amount = expense.get("amount", 0)
733
- # date = expense.get("date")
734
-
735
- # if isinstance(date, str):
736
- # date = datetime.fromisoformat(date.replace('Z', '+00:00'))
737
-
738
- # category_data[category]["total"] += amount
739
- # category_data[category]["count"] += 1
740
-
741
- # # Track monthly totals
742
- # month_key = (date.year, date.month)
743
- # category_data[category]["months"].add(month_key)
744
- # category_data[category]["monthly_totals"][month_key] += amount
745
-
746
- # # Calculate averages
747
- # result = {}
748
- # for category, data in category_data.items():
749
- # num_months = len(data["months"]) or 1
750
- # avg_monthly = data["total"] / num_months
751
-
752
- # # Calculate standard deviation for variability
753
- # monthly_values = list(data["monthly_totals"].values())
754
- # if len(monthly_values) > 1:
755
- # mean = sum(monthly_values) / len(monthly_values)
756
- # variance = sum((x - mean) ** 2 for x in monthly_values) / len(monthly_values)
757
- # std_dev = math.sqrt(variance)
758
- # else:
759
- # std_dev = 0
760
-
761
- # result[category] = {
762
- # "average_monthly": avg_monthly,
763
- # "total": data["total"],
764
- # "count": data["count"],
765
- # "months_analyzed": num_months,
766
- # "std_dev": std_dev,
767
- # "monthly_values": monthly_values
768
- # }
769
-
770
- # return result
771
-
772
- # def _calculate_recommended_budget(self, avg_expense: float, data: Dict) -> float:
773
- # """
774
- # Calculate recommended budget based on average expense.
775
-
776
- # Strategy:
777
- # - Base: Average monthly expense
778
- # - Add 5% buffer for variability
779
- # - Round to nearest 100 for cleaner numbers
780
- # """
781
- # # Add 5% buffer to handle variability
782
- # buffer = avg_expense * 0.05
783
-
784
- # # If there's high variability (std_dev > 20% of mean), add more buffer
785
- # if data["std_dev"] > 0:
786
- # coefficient_of_variation = data["std_dev"] / avg_expense if avg_expense > 0 else 0
787
- # if coefficient_of_variation > 0.2:
788
- # buffer = avg_expense * 0.10 # 10% buffer for high variability
789
-
790
- # recommended = avg_expense + buffer
791
-
792
- # # Round to nearest 100 for cleaner budget numbers
793
- # recommended = round(recommended / 100) * 100
794
-
795
- # # Ensure minimum of 100 if there was any expense
796
- # if recommended < 100 and avg_expense > 0:
797
- # recommended = 100
798
-
799
- # return recommended
800
-
801
- # def _calculate_confidence(self, data: Dict) -> float:
802
- # """
803
- # Calculate confidence score (0-1) based on data quality.
804
-
805
- # Factors:
806
- # - Number of months analyzed (more = higher confidence)
807
- # - Number of transactions (more = higher confidence)
808
- # - Consistency of spending (lower std_dev = higher confidence)
809
- # """
810
- # months_score = min(data["months_analyzed"] / 6, 1.0) # Max at 6 months
811
- # count_score = min(data["count"] / 10, 1.0) # Max at 10 transactions
812
-
813
- # # Consistency score (inverse of coefficient of variation)
814
- # if data["average_monthly"] > 0:
815
- # cv = data["std_dev"] / data["average_monthly"]
816
- # consistency_score = max(0, 1 - min(cv, 1.0)) # Lower CV = higher score
817
- # else:
818
- # consistency_score = 0.5
819
-
820
- # # Weighted average
821
- # confidence = (months_score * 0.4 + count_score * 0.3 + consistency_score * 0.3)
822
-
823
- # return round(confidence, 2)
824
-
825
- # def _generate_reason(self, category: str, avg_expense: float, recommended_budget: float) -> str:
826
- # """Generate human-readable reason for the recommendation"""
827
- # # Format amounts with currency symbol
828
- # avg_formatted = f"Rs.{avg_expense:,.0f}"
829
- # budget_formatted = f"Rs.{recommended_budget:,.0f}"
830
-
831
- # if recommended_budget > avg_expense:
832
- # buffer = recommended_budget - avg_expense
833
- # buffer_pct = (buffer / avg_expense * 100) if avg_expense > 0 else 0
834
- # return (
835
- # f"Your average monthly {category.lower()} expense is {avg_formatted}. "
836
- # f"We suggest setting your budget to {budget_formatted} for next month "
837
- # f"(includes a {buffer_pct:.0f}% buffer for variability)."
838
- # )
839
- # else:
840
- # return (
841
- # f"Your average monthly {category.lower()} expense is {avg_formatted}. "
842
- # f"We recommend a budget of {budget_formatted} for next month."
843
- # )
844
-
845
- # def get_category_averages(self, user_id: str, months: int = 3) -> List[CategoryExpense]:
846
- # """Get average expenses by category for the past N months"""
847
- # end_date = datetime.now()
848
- # start_date = end_date - timedelta(days=months * 30)
849
-
850
- # expenses = list(self.db.expenses.find({
851
- # "user_id": user_id,
852
- # "date": {"$gte": start_date, "$lte": end_date},
853
- # "type": "expense"
854
- # }))
855
-
856
- # if not expenses:
857
- # return []
858
-
859
- # category_data = self._calculate_category_statistics(expenses, start_date, end_date)
860
-
861
- # result = []
862
- # for category, data in category_data.items():
863
- # result.append(CategoryExpense(
864
- # category=category,
865
- # average_monthly_expense=round(data["average_monthly"], 2),
866
- # total_expenses=data["count"],
867
- # months_analyzed=data["months_analyzed"]
868
- # ))
869
-
870
- # result.sort(key=lambda x: x.average_monthly_expense, reverse=True)
871
- # return result
872
-
873
- # def _get_category_stats_from_budgets(
874
- # self, user_id: str, month: int, year: int
875
- # ) -> Dict:
876
- # """
877
- # Build category stats from existing budgets for this user.
878
-
879
- # We treat each budget document (e.g. \"Office Maintenance\", \"LOGICGO\")
880
- # as a spending category and derive an \"average\" from its amounts.
881
- # """
882
- # # createdBy is stored as ObjectId in WalletSync, while user_id is a string.
883
- # # Try to cast to ObjectId; if it fails, fall back to matching the raw string.
884
- # query: Dict = {"status": "OPEN"}
885
- # try:
886
- # query["createdBy"] = ObjectId(user_id)
887
- # except Exception:
888
- # query["createdBy"] = user_id
889
-
890
- # budgets = list(self.db.budgets.find(query))
891
-
892
- # if not budgets:
893
- # return {}
894
-
895
- # result: Dict[str, Dict] = {}
896
- # for b in budgets:
897
- # # Use budget \"name\" as category label
898
- # category = b.get("name", "Uncategorized")
899
-
900
- # # Derive a base amount from WalletSync fields
901
- # max_amount = float(b.get("maxAmount", 0) or 0)
902
- # spend_amount = float(b.get("spendAmount", 0) or 0)
903
-
904
- # # If there is recorded spend, use that as \"average\"; otherwise maxAmount
905
- # base_amount = spend_amount if spend_amount > 0 else max_amount
906
- # if base_amount <= 0:
907
- # continue
908
-
909
- # if category not in result:
910
- # result[category] = {
911
- # "average_monthly": base_amount,
912
- # "total": base_amount,
913
- # "count": 1,
914
- # "months_analyzed": 1,
915
- # "std_dev": 0.0,
916
- # "monthly_values": [base_amount],
917
- # }
918
- # else:
919
- # # If multiple budgets per category, average them
920
- # result[category]["total"] += base_amount
921
- # result[category]["count"] += 1
922
- # result[category]["months_analyzed"] = result[category]["count"]
923
- # result[category]["average_monthly"] = (
924
- # result[category]["total"] / result[category]["count"]
925
- # )
926
- # result[category]["monthly_values"].append(base_amount)
927
-
928
- # return result
929
-
930
- # def _get_ai_recommendation(self, category: str, data: Dict, avg_expense: float):
931
- # """Use OpenAI to refine the budget recommendation."""
932
- # if not OPENAI_API_KEY:
933
- # return None
934
-
935
- # history = ", ".join(f"{value:.0f}" for value in data["monthly_values"])
936
- # summary = (
937
- # f"Category: {category}\n"
938
- # f"Monthly totals: [{history}]\n"
939
- # f"Average spend: {avg_expense:.2f}\n"
940
- # f"Std deviation: {data['std_dev']:.2f}\n"
941
- # f"Months observed: {data['months_analyzed']}\n"
942
- # )
943
-
944
- # prompt = (
945
- # "You are an Indian personal finance coach. "
946
- # "Given the user's spending history, decide whether to increase, decrease, "
947
- # "or keep the upcoming month's budget and provide a short explanation. "
948
- # "Respond strictly as JSON with the following keys:\n"
949
- # '{ "recommended_budget": number, "action": "increase|decrease|keep", "reason": "string" }.\n'
950
- # "Use rupees for all amounts.\n\n"
951
- # f"{summary}"
952
- # )
953
-
954
- # try:
955
- # response = requests.post(
956
- # "https://api.openai.com/v1/responses",
957
- # headers={
958
- # "Authorization": f"Bearer {OPENAI_API_KEY}",
959
- # "Content-Type": "application/json",
960
- # },
961
- # json={
962
- # "model": "gpt-4.1-mini",
963
- # "input": prompt,
964
- # "temperature": 0.1,
965
- # "response_format": {"type": "json_object"},
966
- # },
967
- # timeout=30,
968
- # )
969
- # response.raise_for_status()
970
- # data = response.json()
971
- # content = data["output"][0]["content"][0]["text"]
972
- # return json.loads(content)
973
- # except Exception as exc:
974
- # print(f"OpenAI recommendation error for {category}: {exc}")
975
- # return None
976
-
 
274
  else:
275
  try:
276
  category_doc = self.db.categories.find_one({"_id": ObjectId(category_id)})
277
+ except (ValueError, TypeError):
278
  category_doc = self.db.categories.find_one({"_id": category_id})
279
 
280
  if category_doc:
 
315
  try:
316
  query_str = {"createdBy": user_id}
317
  budgets_str = list(self.db.budgets.find(query_str))
 
318
  print(f"Pattern 2 (createdBy string): Found {len(budgets_str)} budgets")
319
  if budgets_str:
320
  budgets.extend(budgets_str)
321
  except Exception as e:
322
  print(f"Pattern 2 failed: {e}")
 
 
 
 
 
323
  pass
324
 
325
  # Pattern 3: Try with user_id field (alternative field name) - no status filter
326
  try:
327
  query_userid = {"user_id": user_id}
328
  budgets_userid = list(self.db.budgets.find(query_userid))
 
329
  print(f"Pattern 3 (user_id string): Found {len(budgets_userid)} budgets")
330
  if budgets_userid:
331
  budgets.extend(budgets_userid)
332
  except Exception as e:
333
  print(f"Pattern 3 failed: {e}")
 
 
 
 
 
334
  pass
335
 
336
  # Pattern 4: Try ObjectId with user_id field - no status filter
337
  try:
338
  query_objid_userid = {"user_id": ObjectId(user_id)}
339
  budgets_objid_userid = list(self.db.budgets.find(query_objid_userid))
 
340
  print(f"Pattern 4 (user_id ObjectId): Found {len(budgets_objid_userid)} budgets")
341
  if budgets_objid_userid:
342
  budgets.extend(budgets_objid_userid)
 
369
  budgets.append(budget_by_id_str)
370
  except Exception as e:
371
  print(f"Pattern 6 failed: {e}")
 
 
 
 
 
372
  pass
373
 
374
  # Remove duplicates based on _id
 
384
 
385
  if not budgets:
386
  print(f"No budgets found for user_id: {user_id}")
 
387
  print(f"Tried all query patterns. Checking sample budget structure...")
388
  # Get a sample budget to see the structure
389
  sample = self.db.budgets.find_one()
390
  if sample:
391
  print(f"Sample budget structure - createdBy type: {type(sample.get('createdBy')).__name__}, value: {sample.get('createdBy')}")
392
  print(f"Sample budget has user_id field: {'user_id' in sample}")
 
 
393
  return {}
394
 
395
  print(f"Found {len(budgets)} budgets for user_id: {user_id}")
 
407
 
408
  # Get headCategory ID and amounts
409
  head_cat_id = head_cat.get("headCategory")
 
410
  try:
411
  head_cat_max = float(head_cat.get("maxAmount", 0) or 0)
412
  head_cat_spend = float(head_cat.get("spendAmount", 0) or 0)
413
  except (ValueError, TypeError):
414
  head_cat_max = 0
415
  head_cat_spend = 0
 
 
 
 
416
 
417
  # Process nested categories within headCategory
418
  nested_categories = head_cat.get("categories", [])
 
422
  continue
423
 
424
  nested_cat_id = nested_cat.get("category")
 
425
  try:
426
  nested_cat_max = float(nested_cat.get("maxAmount", 0) or 0)
427
  nested_cat_spend = float(nested_cat.get("spendAmount", 0) or 0)
 
432
 
433
  # Only include categories with limits (must have maxAmount > 0)
434
  if nested_cat_max > 0:
 
 
 
 
 
 
 
 
435
  # Look up actual category name
436
  nested_category_name = self._get_category_name(nested_cat_id)
437
  nested_base_amount = nested_cat_spend if nested_cat_spend > 0 else nested_cat_max
 
483
  budget_name = b.get("category") or b.get("title") or "Uncategorized"
484
 
485
  # Derive a base amount from WalletSync fields
 
486
  try:
487
  max_amount = float(b.get("maxAmount", 0) or b.get("max_amount", 0) or b.get("amount", 0) or 0)
488
  spend_amount = float(b.get("spendAmount", 0) or b.get("spend_amount", 0) or b.get("spent", 0) or 0)
 
491
  max_amount = 0
492
  spend_amount = 0
493
  budget_amount = 0
 
 
 
 
 
494
 
495
  # Priority: spendAmount > maxAmount > budgetAmount > budget
496
  if spend_amount > 0:
 
577
  return json.loads(content)
578
  except Exception as exc:
579
  print(f"OpenAI recommendation error for {category}: {exc}")
580
+ return None