LogicGoInfotechSpaces commited on
Commit
1204d36
·
2 Parent(s): a600a52 fa62699

Resolve merge conflicts - keep latest improvements

Browse files
Files changed (2) hide show
  1. app/main.py +131 -0
  2. app/smart_recommendation.py +395 -0
app/main.py CHANGED
@@ -215,3 +215,134 @@ if __name__ == "__main__":
215
  import uvicorn
216
  uvicorn.run(app, host="0.0.0.0", port=8000)
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  import uvicorn
216
  uvicorn.run(app, host="0.0.0.0", port=8000)
217
 
218
+
219
+
220
+
221
+
222
+ # from fastapi import FastAPI, HTTPException, Depends
223
+ # from fastapi.middleware.cors import CORSMiddleware
224
+ # from pymongo import MongoClient
225
+ # from pymongo.errors import ConnectionFailure
226
+ # import os
227
+ # from typing import List, Optional
228
+ # from datetime import datetime, timedelta
229
+ # from app.models import BudgetRecommendation, Expense, Budget, CategoryExpense
230
+ # from app.smart_recommendation import SmartBudgetRecommender
231
+
232
+ # app = FastAPI(title="Smart Budget Recommendation API", version="1.0.0")
233
+
234
+ # # CORS middleware
235
+ # app.add_middleware(
236
+ # CORSMiddleware,
237
+ # allow_origins=["*"],
238
+ # allow_credentials=True,
239
+ # allow_methods=["*"],
240
+ # allow_headers=["*"],
241
+ # )
242
+
243
+ # # MongoDB connection - Get from environment variable (set as secret in Hugging Face)
244
+ # MONGODB_URI = os.getenv("MONGODB_URI")
245
+ # if not MONGODB_URI:
246
+ # raise ValueError("MONGODB_URI environment variable is required. Please set it in Hugging Face secrets.")
247
+
248
+ # try:
249
+ # client = MongoClient(MONGODB_URI)
250
+ # db = client.expense
251
+ # # Test connection
252
+ # client.admin.command('ping')
253
+ # print("Successfully connected to MongoDB")
254
+ # except ConnectionFailure as e:
255
+ # print(f"Failed to connect to MongoDB: {e}")
256
+ # raise
257
+
258
+ # # Initialize Smart Budget Recommender
259
+ # recommender = SmartBudgetRecommender(db)
260
+
261
+ # @app.get("/")
262
+ # async def root():
263
+ # return {"message": "Smart Budget Recommendation API", "status": "running"}
264
+
265
+ # @app.get("/health")
266
+ # async def health_check():
267
+ # try:
268
+ # client.admin.command('ping')
269
+ # return {"status": "healthy", "database": "connected"}
270
+ # except Exception as e:
271
+ # return {"status": "unhealthy", "error": str(e)}
272
+
273
+ # @app.post("/expenses", response_model=dict)
274
+ # async def create_expense(expense: Expense):
275
+ # """
276
+ # Disabled: this service does not create expenses.
277
+
278
+ # All expenses should be created by the main WalletSync app and stored
279
+ # directly in MongoDB. This API only reads existing data for analytics.
280
+ # """
281
+ # raise HTTPException(
282
+ # status_code=405,
283
+ # detail="Creating expenses is disabled. Use the main WalletSync app to add expenses.",
284
+ # )
285
+
286
+ # @app.get("/expenses", response_model=List[Expense])
287
+ # async def get_expenses(user_id: str, limit: int = 100):
288
+ # """Get expenses for a user"""
289
+ # expenses = list(db.expenses.find({"user_id": user_id}).sort("date", -1).limit(limit))
290
+ # for expense in expenses:
291
+ # expense["id"] = str(expense["_id"])
292
+ # del expense["_id"]
293
+ # return expenses
294
+
295
+ # @app.post("/budgets", response_model=dict)
296
+ # async def create_budget(budget: Budget):
297
+ # """
298
+ # Disabled: this service does not create budgets.
299
+
300
+ # All budgets should be created by the main WalletSync app and stored
301
+ # directly in MongoDB. This API only reads existing data for analytics.
302
+ # """
303
+ # raise HTTPException(
304
+ # status_code=405,
305
+ # detail="Creating budgets is disabled. Use the main WalletSync app to add budgets.",
306
+ # )
307
+
308
+ # @app.get("/budgets", response_model=List[Budget])
309
+ # async def get_budgets(user_id: str):
310
+ # """Get budgets for a user"""
311
+ # budgets = list(db.budgets.find({"user_id": user_id}))
312
+ # for budget in budgets:
313
+ # budget["id"] = str(budget["_id"])
314
+ # del budget["_id"]
315
+ # return budgets
316
+
317
+ # @app.get("/recommendations/{user_id}", response_model=List[BudgetRecommendation])
318
+ # async def get_budget_recommendations(user_id: str, month: Optional[int] = None, year: Optional[int] = None):
319
+ # """
320
+ # Get smart budget recommendations for a user based on past spending behavior.
321
+
322
+ # Example response:
323
+ # {
324
+ # "category": "Groceries",
325
+ # "average_expense": 3800,
326
+ # "recommended_budget": 4000,
327
+ # "reason": "Your average monthly grocery expense is Rs.3,800. We suggest setting your budget to Rs.4,000 for next month."
328
+ # }
329
+ # """
330
+ # if not month or not year:
331
+ # # Default to next month
332
+ # next_month = datetime.now().replace(day=1) + timedelta(days=32)
333
+ # month = next_month.month
334
+ # year = next_month.year
335
+
336
+ # recommendations = recommender.get_recommendations(user_id, month, year)
337
+ # return recommendations
338
+
339
+ # @app.get("/category-expenses/{user_id}", response_model=List[CategoryExpense])
340
+ # async def get_category_expenses(user_id: str, months: int = 3):
341
+ # """Get average expenses by category for the past N months"""
342
+ # category_expenses = recommender.get_category_averages(user_id, months)
343
+ # return category_expenses
344
+
345
+ # if __name__ == "__main__":
346
+ # import uvicorn
347
+ # uvicorn.run(app, host="0.0.0.0", port=8000)
348
+
app/smart_recommendation.py CHANGED
@@ -315,28 +315,41 @@ class SmartBudgetRecommender:
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,6 +382,11 @@ class SmartBudgetRecommender:
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,12 +402,15 @@ class SmartBudgetRecommender:
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,12 +428,17 @@ class SmartBudgetRecommender:
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,6 +448,7 @@ class SmartBudgetRecommender:
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,6 +459,14 @@ class SmartBudgetRecommender:
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,6 +518,7 @@ class SmartBudgetRecommender:
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,6 +527,11 @@ class SmartBudgetRecommender:
491
  max_amount = 0
492
  spend_amount = 0
493
  budget_amount = 0
 
 
 
 
 
494
 
495
  # Priority: spendAmount > maxAmount > budgetAmount > budget
496
  if spend_amount > 0:
@@ -579,3 +620,357 @@ class SmartBudgetRecommender:
579
  print(f"OpenAI recommendation error for {category}: {exc}")
580
  return None
581
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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
 
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
 
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
  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
 
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
  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
  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:
 
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
+