1- import asyncio
2- from collections import defaultdict , deque
1+ from collections import defaultdict
32from datetime import datetime , timedelta
4- from typing import Any , Dict , List , Optional , Tuple
3+ from typing import Any , Dict , List , Optional
54
65from app .config import logger
76from app .database import mongodb
1514 SettlementStatus ,
1615 SplitType ,
1716)
18- from bson import ObjectId
17+ from bson import ObjectId , errors
18+ from fastapi import HTTPException
1919
2020
2121class ExpenseService :
@@ -46,15 +46,22 @@ async def create_expense(
4646 # Validate and convert group_id to ObjectId
4747 try :
4848 group_obj_id = ObjectId (group_id )
49- except Exception :
50- raise ValueError ("Group not found or user not a member" )
49+ except errors .InvalidId : # Incorrect ObjectId format
50+ logger .warning (f"Invalid group ID format: { group_id } " )
51+ raise HTTPException (status_code = 400 , detail = "Invalid group ID" )
52+ except Exception as e :
53+ logger .error (f"Unexpected error parsing groupId: { e } " )
54+ raise HTTPException (
55+ status_code = 500 , detail = "Failed to process group ID" )
5156
5257 # Verify user is member of the group
5358 group = await self .groups_collection .find_one (
5459 {"_id" : group_obj_id , "members.userId" : user_id }
5560 )
56- if not group :
57- raise ValueError ("Group not found or user not a member" )
61+ if not group : # User not a member of the group
62+ raise HTTPException (
63+ status_code = 403 , detail = "You are not a member of this group"
64+ )
5865
5966 # Create expense document
6067 expense_doc = {
@@ -231,21 +238,32 @@ async def get_expense_by_id(
231238 try :
232239 group_obj_id = ObjectId (group_id )
233240 expense_obj_id = ObjectId (expense_id )
234- except Exception :
235- raise ValueError ("Group not found or user not a member" )
241+ except errors .InvalidId : # Incorrect ObjectId format for group_id or expense_id
242+ logger .warning (
243+ f"Invalid ObjectId(s): group_id={ group_id } , expense_id={ expense_id } "
244+ )
245+ raise HTTPException (
246+ status_code = 400 , detail = "Invalid group ID or expense ID"
247+ )
248+ except Exception as e :
249+ logger .error (f"Unexpected error parsing IDs: { e } " )
250+ raise HTTPException (
251+ status_code = 500 , detail = "Unable to process IDs" )
236252
237253 # Verify user access
238254 group = await self .groups_collection .find_one (
239255 {"_id" : group_obj_id , "members.userId" : user_id }
240256 )
241- if not group :
242- raise ValueError ("Group not found or user not a member" )
257+ if not group : # Unauthorized access
258+ raise HTTPException (
259+ status_code = 403 , detail = "You are not a member of this group"
260+ )
243261
244262 expense_doc = await self .expenses_collection .find_one (
245263 {"_id" : expense_obj_id , "groupId" : group_id }
246264 )
247- if not expense_doc :
248- raise ValueError ( "Expense not found" )
265+ if not expense_doc : # Expense not found
266+ raise HTTPException ( status_code = 404 , detail = "Expense not found" )
249267
250268 expense = await self ._expense_doc_to_response (expense_doc )
251269
@@ -274,30 +292,39 @@ async def update_expense(
274292 # Validate ObjectId format
275293 try :
276294 expense_obj_id = ObjectId (expense_id )
277- except Exception as e :
278- raise ValueError (f"Invalid expense ID format: { expense_id } " )
295+ except errors .InvalidId :
296+ logger .warning (f"Invalid expense ID format: { expense_id } " )
297+ raise HTTPException (
298+ status_code = 400 , detail = "Invalid expense ID format" )
279299
280300 # Verify user access and that they created the expense
281301 expense_doc = await self .expenses_collection .find_one (
282302 {"_id" : expense_obj_id , "groupId" : group_id , "createdBy" : user_id }
283303 )
284- if not expense_doc :
285- raise ValueError ("Expense not found or not authorized to edit" )
304+ if not expense_doc : # Expense not found or user not authorized
305+ raise HTTPException (
306+ status_code = 403 ,
307+ detail = "Not authorized to update this expense or it does not exist" ,
308+ )
286309
287310 # Validate splits against current or new amount if both are being updated
288311 if updates .splits is not None and updates .amount is not None :
289312 total_split = sum (split .amount for split in updates .splits )
290313 if abs (total_split - updates .amount ) > 0.01 :
291- raise ValueError (
292- "Split amounts must sum to total expense amount" )
314+ raise HTTPException (
315+ status_code = 400 ,
316+ detail = "Split amounts must sum to total expense amount" ,
317+ )
293318
294319 # If only splits are being updated, validate against current amount
295320 elif updates .splits is not None :
296321 current_amount = expense_doc ["amount" ]
297322 total_split = sum (split .amount for split in updates .splits )
298323 if abs (total_split - current_amount ) > 0.01 :
299- raise ValueError (
300- "Split amounts must sum to current expense amount" )
324+ raise HTTPException (
325+ status_code = 400 ,
326+ detail = "Split amounts must sum to total expense amount" ,
327+ )
301328
302329 # Store original data for history
303330 original_data = {
@@ -332,7 +359,8 @@ async def update_expense(
332359 user .get (
333360 "name" , "Unknown User" ) if user else "Unknown User"
334361 )
335- except :
362+ except Exception as e :
363+ logger .warning (f"Failed to fetch user for history: { e } " )
336364 user_name = "Unknown User"
337365
338366 history_entry = {
@@ -349,16 +377,20 @@ async def update_expense(
349377 {"$set" : update_doc , "$push" : {"history" : history_entry }},
350378 )
351379
352- if result .matched_count == 0 :
353- raise ValueError ("Expense not found during update" )
380+ if result .matched_count == 0 : # Expense not found during update
381+ raise HTTPException (
382+ status_code = 404 , detail = "Expense not found during update"
383+ )
354384 else :
355385 # No actual changes, just update the timestamp
356386 result = await self .expenses_collection .update_one (
357387 {"_id" : expense_obj_id }, {"$set" : update_doc }
358388 )
359389
360390 if result .matched_count == 0 :
361- raise ValueError ("Expense not found during update" )
391+ raise HTTPException (
392+ status_code = 404 , detail = "Expense not found during update"
393+ )
362394
363395 # If splits changed, recalculate settlements
364396 if updates .splits is not None or updates .amount is not None :
@@ -378,10 +410,9 @@ async def update_expense(
378410 await self ._create_settlements_for_expense (
379411 updated_expense , user_id
380412 )
381- except Exception as e :
413+ except Exception :
382414 logger .error (
383- f"Warning: Failed to recalculate settlements: { e } " ,
384- exc_info = True ,
415+ f"Warning: Failed to recalculate settlements" , exc_info = True
385416 )
386417 # Continue anyway, as the expense update succeeded
387418
@@ -390,14 +421,26 @@ async def update_expense(
390421 {"_id" : expense_obj_id }
391422 )
392423 if not updated_expense :
393- raise ValueError ("Failed to retrieve updated expense" )
424+ raise HTTPException (
425+ status_code = 500 , detail = "Failed to retrieve updated expense"
426+ )
394427
395428 return await self ._expense_doc_to_response (updated_expense )
396429
397- except ValueError :
430+ # Allowing FastAPI exception to bubble up for proper handling
431+ except HTTPException :
398432 raise
399- except Exception as e :
400- logger .error (f"Error in update_expense: { str (e )} " , exc_info = True )
433+ except ValueError as ve :
434+ raise HTTPException (status_code = 400 , detail = str (ve ))
435+ except (
436+ Exception
437+ ) as e : # logger.exception() will provide the entire traceback, so its safe to remove traceback
438+ logger .exception (
439+ f"Unhandled error in update_expense for expense { expense_id } : { e } "
440+ )
441+ import traceback
442+
443+ traceback .print_exc ()
401444 raise Exception (f"Database error during expense update: { str (e )} " )
402445
403446 async def delete_expense (
@@ -411,7 +454,13 @@ async def delete_expense(
411454 "createdBy" : user_id }
412455 )
413456 if not expense_doc :
414- raise ValueError ("Expense not found or not authorized to delete" )
457+ logger .warning (
458+ f"Unauthorized delete attempt or missing expense: { expense_id } by user { user_id } "
459+ )
460+ raise HTTPException (
461+ status_code = 403 ,
462+ detail = "Not authorized to delete this expense or it does not exist" ,
463+ )
415464
416465 # Delete settlements for this expense
417466 await self .settlements_collection .delete_many ({"expenseId" : expense_id })
@@ -576,7 +625,12 @@ async def create_manual_settlement(
576625 {"_id" : ObjectId (group_id ), "members.userId" : user_id }
577626 )
578627 if not group :
579- raise ValueError ("Group not found or user not a member" )
628+ logger .warning (
629+ f"Unauthorized access attempt to group { group_id } by user { user_id } "
630+ )
631+ raise HTTPException (
632+ status_code = 403 , detail = "Group not found or user not a member"
633+ )
580634
581635 # Get user names
582636 users = await self .users_collection .find (
@@ -666,7 +720,12 @@ async def get_group_settlements(
666720 {"_id" : ObjectId (group_id ), "members.userId" : user_id }
667721 )
668722 if not group :
669- raise ValueError ("Group not found or user not a member" )
723+ logger .warning (
724+ f"Unauthorized access attempt to group { group_id } by user { user_id } "
725+ )
726+ raise HTTPException (
727+ status_code = 403 , detail = "Group not found or user not a member"
728+ )
670729
671730 # Build query
672731 query = {"groupId" : group_id }
@@ -705,17 +764,24 @@ async def get_settlement_by_id(
705764
706765 # Verify user access
707766 group = await self .groups_collection .find_one (
708- {"_id" : ObjectId (group_id ), "members.userId" : user_id }
767+ {
768+ "_id" : ObjectId (
769+ group_id
770+ ), # Assuming valid object ID format (same as above functions)
771+ "members.userId" : user_id ,
772+ }
709773 )
710774 if not group :
711- raise ValueError ("Group not found or user not a member" )
775+ raise HTTPException (
776+ status_code = 403 , detail = "Group not found or user not a member"
777+ )
712778
713779 settlement_doc = await self .settlements_collection .find_one (
714780 {"_id" : ObjectId (settlement_id ), "groupId" : group_id }
715781 )
716782
717783 if not settlement_doc :
718- raise ValueError ( "Settlement not found" )
784+ raise HTTPException ( status_code = 404 , detail = "Settlement not found" )
719785
720786 return Settlement (** {** settlement_doc , "_id" : str (settlement_doc ["_id" ])})
721787
@@ -740,7 +806,7 @@ async def update_settlement_status(
740806 )
741807
742808 if result .matched_count == 0 :
743- raise ValueError ( "Settlement not found" )
809+ raise HTTPException ( status_code = 404 , detail = "Settlement not found" )
744810
745811 # Get updated settlement
746812 settlement_doc = await self .settlements_collection .find_one (
@@ -759,7 +825,9 @@ async def delete_settlement(
759825 {"_id" : ObjectId (group_id ), "members.userId" : user_id }
760826 )
761827 if not group :
762- raise ValueError ("Group not found or user not a member" )
828+ raise HTTPException (
829+ status_code = 403 , detail = "Group not found or user not a member"
830+ )
763831
764832 result = await self .settlements_collection .delete_one (
765833 {"_id" : ObjectId (settlement_id ), "groupId" : group_id }
@@ -777,7 +845,9 @@ async def get_user_balance_in_group(
777845 {"_id" : ObjectId (group_id ), "members.userId" : current_user_id }
778846 )
779847 if not group :
780- raise ValueError ("Group not found or user not a member" )
848+ raise HTTPException (
849+ status_code = 403 , detail = "Group not found or user not a member"
850+ )
781851
782852 # Get user info
783853 user = await self .users_collection .find_one ({"_id" : ObjectId (target_user_id )})
@@ -1109,7 +1179,9 @@ async def get_group_analytics(
11091179 {"_id" : ObjectId (group_id ), "members.userId" : user_id }
11101180 )
11111181 if not group :
1112- raise ValueError ("Group not found or user not a member" )
1182+ raise HTTPException (
1183+ status_code = 403 , detail = "Group not found or user not a member"
1184+ )
11131185
11141186 # Build date range
11151187 if period == "month" and year and month :
0 commit comments