LogicGoInfotechSpaces commited on
Commit
ecfd501
·
1 Parent(s): 6a4c6a3

Add new endpoint /recommendations/by-name to accept Head Category names instead of category_id for easier frontend integration

Browse files
Files changed (3) hide show
  1. app/main.py +106 -1
  2. app/models.py +15 -0
  3. app/smart_recommendation.py +44 -0
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, RecommendationRequest, RecommendationResponse
10
  from app.smart_recommendation import SmartBudgetRecommender
11
 
12
  app = FastAPI(title="Smart Budget Recommendation API", version="1.0.0")
@@ -262,6 +262,111 @@ async def check_and_get_recommendations(request: RecommendationRequest, month: O
262
  message=f"User does not have previous data for category_id: {request.category_id}. Please provide a budget_amount or create a budget/expenses for this category first."
263
  )
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  @app.get("/recommendations/{user_id}", response_model=List[BudgetRecommendation])
266
  async def get_budget_recommendations(user_id: str, month: Optional[int] = None, year: Optional[int] = None):
267
  """
 
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, RecommendationByNameRequest, RecommendationByNameRequest
10
  from app.smart_recommendation import SmartBudgetRecommender
11
 
12
  app = FastAPI(title="Smart Budget Recommendation API", version="1.0.0")
 
262
  message=f"User does not have previous data for category_id: {request.category_id}. Please provide a budget_amount or create a budget/expenses for this category first."
263
  )
264
 
265
+ @app.post("/recommendations/by-name", response_model=RecommendationResponse)
266
+ async def get_recommendations_by_category_name(request: RecommendationByNameRequest, month: Optional[int] = None, year: Optional[int] = None):
267
+ """
268
+ Get budget recommendations by Head Category name (instead of category_id).
269
+ This is useful for frontend integration where you only have the category name.
270
+
271
+ Request body:
272
+ {
273
+ "user_id": "6741abd38d30ab5b7176397f",
274
+ "category_name": "Food & Drinks",
275
+ "budget_amount": 350.0 // optional
276
+ }
277
+
278
+ Supported Head Category names:
279
+ - Food & Drinks
280
+ - Shopping
281
+ - Housing
282
+ - Financial Expenses
283
+ - Communication, PC
284
+ - Vehicle
285
+ - Transportation
286
+ - Life & Entertainment
287
+ - Investments
288
+ - Income
289
+ - Loan Payment
290
+ - Marketing
291
+
292
+ Response:
293
+ - If user has previous data: returns recommendations based on historical data
294
+ - If user doesn't have previous data but provided budget_amount: returns recommendations based on budget_amount
295
+ - If user doesn't have previous data and no budget_amount: returns message asking for budget_amount
296
+ """
297
+ if not month or not year:
298
+ # Default to next month
299
+ next_month = datetime.now().replace(day=1) + timedelta(days=32)
300
+ month = next_month.month
301
+ year = next_month.year
302
+
303
+ # Find category_id by name
304
+ category_id = recommender._get_category_id_by_name(request.category_name)
305
+
306
+ if not category_id:
307
+ return RecommendationResponse(
308
+ has_previous_data=False,
309
+ recommendations=None,
310
+ message=f"Category '{request.category_name}' not found in database. Please check the category name or create the category first."
311
+ )
312
+
313
+ # Now use the existing check endpoint logic
314
+ has_data = recommender.check_user_has_category_data(request.user_id, category_id)
315
+
316
+ if has_data:
317
+ # User has previous data - use it for recommendations
318
+ print(f"✅ User {request.user_id} has previous data for category '{request.category_name}' (id: {category_id}) - using historical data")
319
+ recommendations = recommender.get_recommendation_for_category(
320
+ request.user_id,
321
+ category_id,
322
+ month,
323
+ year,
324
+ budget_amount=None # Don't use budget_amount if user has historical data
325
+ )
326
+
327
+ if recommendations:
328
+ return RecommendationResponse(
329
+ has_previous_data=True,
330
+ recommendations=recommendations,
331
+ message=None
332
+ )
333
+ else:
334
+ return RecommendationResponse(
335
+ has_previous_data=True,
336
+ recommendations=[],
337
+ message="User has previous data but no recommendations could be generated for this category."
338
+ )
339
+ elif request.budget_amount and request.budget_amount > 0:
340
+ # User doesn't have previous data, but provided budget_amount
341
+ print(f"ℹ️ User {request.user_id} does not have previous data for category '{request.category_name}' (id: {category_id}) - using provided budget_amount")
342
+ recommendations = recommender.get_recommendation_for_category(
343
+ request.user_id,
344
+ category_id,
345
+ month,
346
+ year,
347
+ request.budget_amount
348
+ )
349
+
350
+ if recommendations:
351
+ return RecommendationResponse(
352
+ has_previous_data=False,
353
+ recommendations=recommendations,
354
+ message="Recommendation generated based on provided budget amount. User does not have previous spending data for this category."
355
+ )
356
+ else:
357
+ return RecommendationResponse(
358
+ has_previous_data=False,
359
+ recommendations=None,
360
+ message="Could not generate recommendations even with provided budget amount."
361
+ )
362
+ else:
363
+ # User doesn't have previous data and no budget_amount provided
364
+ return RecommendationResponse(
365
+ has_previous_data=False,
366
+ recommendations=None,
367
+ message=f"User does not have previous data for category '{request.category_name}'. Please provide a budget_amount to get recommendations."
368
+ )
369
+
370
  @app.get("/recommendations/{user_id}", response_model=List[BudgetRecommendation])
371
  async def get_budget_recommendations(user_id: str, month: Optional[int] = None, year: Optional[int] = None):
372
  """
app/models.py CHANGED
@@ -63,3 +63,18 @@ class RecommendationResponse(BaseModel):
63
  message: Optional[str] = None
64
  recommendations: Optional[List[BudgetRecommendation]] = None
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  message: Optional[str] = None
64
  recommendations: Optional[List[BudgetRecommendation]] = None
65
 
66
+ class RecommendationByNameRequest(BaseModel):
67
+ user_id: str = Field(..., description="User identifier")
68
+ category_name: str = Field(..., description="Head Category name (e.g., 'Food & Drinks', 'Shopping', 'Housing')")
69
+ budget_amount: Optional[float] = Field(None, gt=0, description="Current budget amount for this category (optional)")
70
+
71
+ @validator('budget_amount')
72
+ def validate_budget_amount(cls, v):
73
+ if v is not None:
74
+ if v <= 0:
75
+ raise ValueError('budget_amount must be greater than 0')
76
+ if v > 1e12:
77
+ print(f"⚠️ Auto-capping corrupted budget_amount: {v:,.2e} -> 1,000,000,000,000")
78
+ return 1e12
79
+ return v
80
+
app/smart_recommendation.py CHANGED
@@ -778,6 +778,50 @@ class SmartBudgetRecommender:
778
  result.sort(key=lambda x: x.average_monthly_expense, reverse=True)
779
  return result
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  def _get_category_name(self, category_id) -> str:
782
  """
783
  Look up category name from headCategories and categories collections.
 
778
  result.sort(key=lambda x: x.average_monthly_expense, reverse=True)
779
  return result
780
 
781
+ def _get_category_id_by_name(self, category_name: str) -> Optional[str]:
782
+ """
783
+ Find category_id by category name from headCategories and categories collections.
784
+ Returns the first matching category_id found.
785
+ """
786
+ if not category_name:
787
+ return None
788
+
789
+ try:
790
+ # Search in headCategories collection by name
791
+ head_category = self.db.headCategories.find_one({
792
+ "$or": [
793
+ {"name": {"$regex": category_name, "$options": "i"}},
794
+ {"headCategoryName": {"$regex": category_name, "$options": "i"}},
795
+ {"categoryName": {"$regex": category_name, "$options": "i"}}
796
+ ]
797
+ })
798
+
799
+ if head_category:
800
+ # Return _id as string
801
+ category_id = str(head_category.get("_id"))
802
+ print(f"✅ Found category_id in headCategories: '{category_name}' -> {category_id}")
803
+ return category_id
804
+
805
+ # Search in categories collection by name
806
+ category = self.db.categories.find_one({
807
+ "$or": [
808
+ {"name": {"$regex": category_name, "$options": "i"}},
809
+ {"categoryName": {"$regex": category_name, "$options": "i"}}
810
+ ]
811
+ })
812
+
813
+ if category:
814
+ category_id = str(category.get("_id"))
815
+ print(f"✅ Found category_id in categories: '{category_name}' -> {category_id}")
816
+ return category_id
817
+
818
+ print(f"⚠️ Category name not found: '{category_name}'")
819
+ return None
820
+
821
+ except Exception as e:
822
+ print(f"Error looking up category_id by name '{category_name}': {e}")
823
+ return None
824
+
825
  def _get_category_name(self, category_id) -> str:
826
  """
827
  Look up category name from headCategories and categories collections.