Commit
·
6ec895c
1
Parent(s):
1204d36
Clean up code: Remove commented duplicate code, fix category filtering logic, improve exception handling
Browse files- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|