LogicGoInfotechSpaces commited on
Commit
9e1b2d4
·
1 Parent(s): 5fda188

Add POST endpoint to check user category data and return recommendations

Browse files
Files changed (3) hide show
  1. app/main.py +55 -1
  2. app/models.py +9 -0
  3. app/smart_recommendation.py +131 -6
app/main.py CHANGED
@@ -6,7 +6,7 @@ import os
6
  import time
7
  from typing import List, Optional
8
  from datetime import datetime, timedelta, timezone
9
- from app.models import BudgetRecommendation, Expense, Budget, CategoryExpense
10
  from app.smart_recommendation import SmartBudgetRecommender
11
 
12
  app = FastAPI(title="Smart Budget Recommendation API", version="1.0.0")
@@ -213,6 +213,60 @@ async def get_category_expenses(user_id: str, months: int = 3):
213
  category_expenses = recommender.get_category_averages(user_id, months)
214
  return category_expenses
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  if __name__ == "__main__":
217
  import uvicorn
218
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
6
  import time
7
  from typing import List, Optional
8
  from datetime import datetime, timedelta, timezone
9
+ from app.models import BudgetRecommendation, Expense, Budget, CategoryExpense, RecommendationRequest, RecommendationResponse
10
  from app.smart_recommendation import SmartBudgetRecommender
11
 
12
  app = FastAPI(title="Smart Budget Recommendation API", version="1.0.0")
 
213
  category_expenses = recommender.get_category_averages(user_id, months)
214
  return category_expenses
215
 
216
+ @app.post("/recommendations/check", response_model=RecommendationResponse)
217
+ async def check_and_get_recommendations(request: RecommendationRequest, month: Optional[int] = None, year: Optional[int] = None):
218
+ """
219
+ Check if user has previous data for a category and return recommendations if available.
220
+
221
+ Request body:
222
+ {
223
+ "user_id": "68a834c3f4694b11efedacd2",
224
+ "category_id": "688c80ca990b63f0e945ecf1"
225
+ }
226
+
227
+ Response:
228
+ - If user has previous data: returns recommendations
229
+ - If user doesn't have previous data: returns message indicating no previous data
230
+ """
231
+ if not month or not year:
232
+ # Default to next month
233
+ next_month = datetime.now().replace(day=1) + timedelta(days=32)
234
+ month = next_month.month
235
+ year = next_month.year
236
+
237
+ # Check if user has previous data for this category
238
+ has_data = recommender.check_user_has_category_data(request.user_id, request.category_id)
239
+
240
+ if has_data:
241
+ # Get recommendations for this specific category
242
+ recommendations = recommender.get_recommendation_for_category(
243
+ request.user_id,
244
+ request.category_id,
245
+ month,
246
+ year
247
+ )
248
+
249
+ if recommendations:
250
+ return RecommendationResponse(
251
+ has_previous_data=True,
252
+ recommendations=recommendations,
253
+ message=None
254
+ )
255
+ else:
256
+ # User has data but no recommendations generated (edge case)
257
+ return RecommendationResponse(
258
+ has_previous_data=True,
259
+ recommendations=[],
260
+ message="User has previous data but no recommendations could be generated for this category."
261
+ )
262
+ else:
263
+ # User doesn't have previous data
264
+ return RecommendationResponse(
265
+ has_previous_data=False,
266
+ recommendations=None,
267
+ message=f"User does not have previous data for category_id: {request.category_id}. Please create a budget or add expenses for this category first."
268
+ )
269
+
270
  if __name__ == "__main__":
271
  import uvicorn
272
  uvicorn.run(app, host="0.0.0.0", port=8000)
app/models.py CHANGED
@@ -41,3 +41,12 @@ class CategoryExpense(BaseModel):
41
  total_expenses: int
42
  months_analyzed: int
43
 
 
 
 
 
 
 
 
 
 
 
41
  total_expenses: int
42
  months_analyzed: int
43
 
44
+ class RecommendationRequest(BaseModel):
45
+ user_id: str = Field(..., description="User identifier")
46
+ category_id: str = Field(..., description="Category ID to check for previous data")
47
+
48
+ class RecommendationResponse(BaseModel):
49
+ has_previous_data: bool
50
+ message: Optional[str] = None
51
+ recommendations: Optional[List[BudgetRecommendation]] = None
52
+
app/smart_recommendation.py CHANGED
@@ -50,7 +50,15 @@ class SmartBudgetRecommender:
50
 
51
  for category_key, data in category_data.items():
52
  # Extract category_name and category_id from data
53
- category_name = data.get("category_name", category_key.split("|")[0] if "|" in category_key else category_key)
 
 
 
 
 
 
 
 
54
  category_id = data.get("category_id")
55
  avg_expense = data["average_monthly"]
56
  confidence = self._calculate_confidence(data)
@@ -87,6 +95,115 @@ class SmartBudgetRecommender:
87
 
88
  return recommendations
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  def _calculate_category_statistics(self, expenses: List[Dict], start_date: datetime, end_date: datetime) -> Dict:
91
  """Calculate statistics for each category"""
92
  category_data = defaultdict(lambda: {
@@ -547,8 +664,10 @@ class SmartBudgetRecommender:
547
  # Only add budget if it has an amount - use category name as key
548
  # Store both category_name and category_id in the result
549
  if base_amount > 0:
550
- # Use a unique key that includes category_id to handle multiple budgets with same category name
551
- result_key = f"{category_name}|{category_id}" if category_id else category_name
 
 
552
 
553
  if result_key not in result:
554
  result[result_key] = {
@@ -570,9 +689,15 @@ class SmartBudgetRecommender:
570
  )
571
  result[result_key]["monthly_values"].append(base_amount)
572
 
573
- # Extract category names for logging
574
- category_names = [data.get("category_name", key.split("|")[0] if "|" in key else key) for key, data in result.items()]
575
- print(f"✅ Processed {len(result)} budget categories for recommendations: {category_names}")
 
 
 
 
 
 
576
  return result
577
 
578
  def _get_ai_recommendation(self, category: str, data: Dict, avg_expense: float):
 
50
 
51
  for category_key, data in category_data.items():
52
  # Extract category_name and category_id from data
53
+ # category_key format: "user_id|category_name|category_id"
54
+ key_parts = category_key.split("|")
55
+ if len(key_parts) >= 3:
56
+ # Skip user_id (first part), get category_name (second part)
57
+ category_name = data.get("category_name", key_parts[1])
58
+ elif len(key_parts) >= 2:
59
+ category_name = data.get("category_name", key_parts[1])
60
+ else:
61
+ category_name = data.get("category_name", category_key)
62
  category_id = data.get("category_id")
63
  avg_expense = data["average_monthly"]
64
  confidence = self._calculate_confidence(data)
 
95
 
96
  return recommendations
97
 
98
+ def check_user_has_category_data(self, user_id: str, category_id: str) -> bool:
99
+ """
100
+ Check if user has previous budget or expense data for a specific category.
101
+
102
+ Args:
103
+ user_id: User identifier
104
+ category_id: Category ID to check
105
+
106
+ Returns:
107
+ True if user has previous data for this category, False otherwise
108
+ """
109
+ # Check if user has budgets with this category_id
110
+ try:
111
+ # Try ObjectId format
112
+ try:
113
+ category_objid = ObjectId(category_id)
114
+ budget_query = {
115
+ "$or": [
116
+ {"createdBy": ObjectId(user_id), "category": category_objid},
117
+ {"createdBy": ObjectId(user_id), "categoryId": category_objid},
118
+ {"createdBy": ObjectId(user_id), "headCategory": category_objid},
119
+ {"createdBy": user_id, "category": category_objid},
120
+ {"createdBy": user_id, "categoryId": category_objid},
121
+ {"createdBy": user_id, "headCategory": category_objid},
122
+ {"user_id": ObjectId(user_id), "category": category_objid},
123
+ {"user_id": user_id, "category": category_objid},
124
+ ]
125
+ }
126
+ except (ValueError, TypeError):
127
+ # category_id is not a valid ObjectId, try as string
128
+ budget_query = {
129
+ "$or": [
130
+ {"createdBy": ObjectId(user_id), "category": category_id},
131
+ {"createdBy": ObjectId(user_id), "categoryId": category_id},
132
+ {"createdBy": ObjectId(user_id), "headCategory": category_id},
133
+ {"createdBy": user_id, "category": category_id},
134
+ {"createdBy": user_id, "categoryId": category_id},
135
+ {"createdBy": user_id, "headCategory": category_id},
136
+ {"user_id": ObjectId(user_id), "category": category_id},
137
+ {"user_id": user_id, "category": category_id},
138
+ ]
139
+ }
140
+
141
+ # Check budgets collection
142
+ budget_count = self.db.budgets.count_documents(budget_query)
143
+ if budget_count > 0:
144
+ print(f"✅ Found {budget_count} budget(s) for user {user_id} with category_id {category_id}")
145
+ return True
146
+
147
+ # Also check if category_id is in headCategories array
148
+ head_cat_query = {
149
+ "$or": [
150
+ {"createdBy": ObjectId(user_id), "headCategories.category": category_id},
151
+ {"createdBy": user_id, "headCategories.category": category_id},
152
+ {"user_id": ObjectId(user_id), "headCategories.category": category_id},
153
+ {"user_id": user_id, "headCategories.category": category_id},
154
+ ]
155
+ }
156
+ head_cat_count = self.db.budgets.count_documents(head_cat_query)
157
+ if head_cat_count > 0:
158
+ print(f"✅ Found {head_cat_count} budget(s) with category_id {category_id} in headCategories for user {user_id}")
159
+ return True
160
+
161
+ # Check expenses collection
162
+ try:
163
+ expense_query = {
164
+ "$or": [
165
+ {"user_id": ObjectId(user_id), "category": category_id},
166
+ {"user_id": user_id, "category": category_id},
167
+ ]
168
+ }
169
+ expense_count = self.db.expenses.count_documents(expense_query)
170
+ if expense_count > 0:
171
+ print(f"✅ Found {expense_count} expense(s) for user {user_id} with category_id {category_id}")
172
+ return True
173
+ except Exception as e:
174
+ print(f"Error checking expenses: {e}")
175
+
176
+ print(f"❌ No previous data found for user {user_id} with category_id {category_id}")
177
+ return False
178
+
179
+ except Exception as e:
180
+ print(f"Error checking user category data: {e}")
181
+ return False
182
+
183
+ def get_recommendation_for_category(self, user_id: str, category_id: str, month: int, year: int) -> List[BudgetRecommendation]:
184
+ """
185
+ Get budget recommendations for a specific category for a user.
186
+
187
+ Args:
188
+ user_id: User identifier
189
+ category_id: Category ID to get recommendations for
190
+ month: Target month (1-12)
191
+ year: Target year
192
+
193
+ Returns:
194
+ List of budget recommendations for the specific category
195
+ """
196
+ # Get all recommendations for the user
197
+ all_recommendations = self.get_recommendations(user_id, month, year)
198
+
199
+ # Filter to only include recommendations for the specified category_id
200
+ filtered_recommendations = [
201
+ rec for rec in all_recommendations
202
+ if rec.category_id == category_id or str(rec.category_id) == str(category_id)
203
+ ]
204
+
205
+ return filtered_recommendations
206
+
207
  def _calculate_category_statistics(self, expenses: List[Dict], start_date: datetime, end_date: datetime) -> Dict:
208
  """Calculate statistics for each category"""
209
  category_data = defaultdict(lambda: {
 
664
  # Only add budget if it has an amount - use category name as key
665
  # Store both category_name and category_id in the result
666
  if base_amount > 0:
667
+ # Use a unique key that includes user_id and category_id to ensure user-specific grouping
668
+ # This prevents budgets from different users with same category name from being mixed
669
+ category_id_str = str(category_id) if category_id else "none"
670
+ result_key = f"{user_id}|{category_name}|{category_id_str}"
671
 
672
  if result_key not in result:
673
  result[result_key] = {
 
689
  )
690
  result[result_key]["monthly_values"].append(base_amount)
691
 
692
+ # Extract category names for logging (skip user_id part in key)
693
+ category_names = []
694
+ for key, data in result.items():
695
+ key_parts = key.split("|")
696
+ if len(key_parts) >= 2:
697
+ category_names.append(key_parts[1]) # Get category_name (second part after user_id)
698
+ else:
699
+ category_names.append(data.get("category_name", key))
700
+ print(f"✅ Processed {len(result)} budget categories for user {user_id}: {category_names}")
701
  return result
702
 
703
  def _get_ai_recommendation(self, category: str, data: Dict, avg_expense: float):