@@ -953,141 +953,201 @@ async def get_user_balance_in_group(
953953 }
954954
955955 async def get_friends_balance_summary (self , user_id : str ) -> Dict [str , Any ]:
956- """Get cross-group friend balances for a user"""
956+ """
957+ Get cross-group friend balances using optimized aggregation pipeline.
957958
958- # Get all groups user belongs to
959+ Performance: Optimized to use single aggregation query instead of N×M queries.
960+ Example: 20 friends × 5 groups = 3 queries total (vs 100+ with naive approach).
961+
962+ Uses MongoDB aggregation to calculate all balances at once, then batch enriches
963+ with user and group details for optimal performance.
964+ """
965+
966+ # First, get all groups user belongs to (need this to filter friends properly)
959967 groups = await self .groups_collection .find ({"members.userId" : user_id }).to_list (
960- None
968+ length = 500
961969 )
962970
963- friends_balance = []
964- user_totals = {"totalOwedToYou" : 0 , "totalYouOwe" : 0 }
971+ if not groups :
972+ return {
973+ "friendsBalance" : [],
974+ "summary" : {
975+ "totalOwedToYou" : 0 ,
976+ "totalYouOwe" : 0 ,
977+ "netBalance" : 0 ,
978+ "friendCount" : 0 ,
979+ "activeGroups" : 0 ,
980+ },
981+ }
965982
966- # Get all unique friends across groups
967- friend_ids = set ()
983+ # Extract group IDs and friend IDs (only from user's groups)
984+ group_ids = [str (g ["_id" ]) for g in groups ]
985+ friend_ids_in_groups = set ()
968986 for group in groups :
969987 for member in group ["members" ]:
970988 if member ["userId" ] != user_id :
971- friend_ids .add (member ["userId" ])
989+ friend_ids_in_groups .add (member ["userId" ])
972990
973- # Get user names & images
974- users = await self .users_collection .find (
975- {"_id" : {"$in" : [ObjectId (uid ) for uid in friend_ids ]}}
976- ).to_list (None )
977- user_names = {str (user ["_id" ]): user .get ("name" , "Unknown" ) for user in users }
978- user_images = {str (user ["_id" ]): user .get ("imageUrl" ) for user in users }
991+ # OPTIMIZATION: Single aggregation to calculate all friend balances at once
992+ # Only for friends in user's groups and groups user belongs to
993+ pipeline = [
994+ # Step 1: Match settlements in user's groups involving the user
995+ {
996+ "$match" : {
997+ "groupId" : {"$in" : group_ids },
998+ "$or" : [
999+ {
1000+ "payerId" : user_id ,
1001+ "payeeId" : {"$in" : list (friend_ids_in_groups )},
1002+ },
1003+ {
1004+ "payeeId" : user_id ,
1005+ "payerId" : {"$in" : list (friend_ids_in_groups )},
1006+ },
1007+ ],
1008+ }
1009+ },
1010+ # Step 2: Calculate net balance per friend per group
1011+ {
1012+ "$group" : {
1013+ "_id" : {
1014+ "friendId" : {
1015+ "$cond" : [
1016+ {"$eq" : ["$payerId" , user_id ]},
1017+ "$payeeId" ,
1018+ "$payerId" ,
1019+ ]
1020+ },
1021+ "groupId" : "$groupId" ,
1022+ },
1023+ "balance" : {
1024+ "$sum" : {
1025+ "$cond" : [
1026+ # If user is payer, friend owes user (positive)
1027+ {"$eq" : ["$payerId" , user_id ]},
1028+ "$amount" ,
1029+ # If user is payee, user owes friend (negative)
1030+ {"$multiply" : ["$amount" , - 1 ]},
1031+ ]
1032+ }
1033+ },
1034+ }
1035+ },
1036+ # Step 3: Group by friend to get total balance across all groups
1037+ {
1038+ "$group" : {
1039+ "_id" : "$_id.friendId" ,
1040+ "totalBalance" : {"$sum" : "$balance" },
1041+ "groups" : {
1042+ "$push" : {"groupId" : "$_id.groupId" , "balance" : "$balance" }
1043+ },
1044+ }
1045+ },
1046+ # Step 4: Filter out friends with zero balance
1047+ {"$match" : {"$expr" : {"$gt" : [{"$abs" : "$totalBalance" }, 0.01 ]}}},
1048+ ]
9791049
980- for friend_id in friend_ids :
981- friend_balance_data = {
982- "userId" : friend_id ,
983- "userName" : user_names .get (friend_id , "Unknown" ),
984- # Populate image directly from users collection to avoid extra client round-trips
985- "userImageUrl" : user_images .get (friend_id ),
986- "netBalance" : 0 ,
987- "owesYou" : False ,
988- "breakdown" : [],
989- "lastActivity" : datetime .utcnow (),
1050+ # Execute aggregation - Single query for all friend balances
1051+ try :
1052+ results = await self .settlements_collection .aggregate (pipeline ).to_list (
1053+ length = 500
1054+ )
1055+ except Exception as e :
1056+ logger .error (f"Error in optimized friends balance aggregation: { e } " )
1057+ results = []
1058+
1059+ if not results :
1060+ # No balances found
1061+ return {
1062+ "friendsBalance" : [],
1063+ "summary" : {
1064+ "totalOwedToYou" : 0 ,
1065+ "totalYouOwe" : 0 ,
1066+ "netBalance" : 0 ,
1067+ "friendCount" : 0 ,
1068+ "activeGroups" : len (groups ),
1069+ },
9901070 }
9911071
992- total_friend_balance = 0
1072+ # Extract unique friend IDs for batch fetching
1073+ friend_ids = list (set (result ["_id" ] for result in results ))
9931074
994- # Calculate balance for each group
995- for group in groups :
996- group_id = str (group ["_id" ])
1075+ # Build group map from groups we already fetched
1076+ groups_map = {str (g ["_id" ]): g .get ("name" , "Unknown Group" ) for g in groups }
9971077
998- # Check if friend is in this group
999- friend_in_group = any (
1000- member ["userId" ] == friend_id for member in group ["members" ]
1001- )
1002- if not friend_in_group :
1003- continue
1078+ # OPTIMIZATION: Batch fetch all friend details in one query
1079+ try :
1080+ friends_cursor = self .users_collection .find (
1081+ {"_id" : {"$in" : [ObjectId (fid ) for fid in friend_ids ]}},
1082+ {"_id" : 1 , "name" : 1 , "imageUrl" : 1 },
1083+ )
1084+ friends_list = await friends_cursor .to_list (length = 500 )
1085+ friends_map = {str (f ["_id" ]): f for f in friends_list }
1086+ except Exception as e :
1087+ logger .error (f"Error batch fetching friend details: { e } " )
1088+ friends_map = {}
10041089
1005- # Calculate net balance between user and friend in this group
1006- pipeline = [
1007- {
1008- "$match" : {
1009- "groupId" : group_id ,
1010- "$or" : [
1011- {"payerId" : user_id , "payeeId" : friend_id },
1012- {"payerId" : friend_id , "payeeId" : user_id },
1013- ],
1014- }
1015- },
1016- {
1017- "$group" : {
1018- "_id" : None ,
1019- "userOwes" : {
1020- "$sum" : {
1021- "$cond" : [
1022- {
1023- "$and" : [
1024- {"$eq" : ["$payerId" , friend_id ]},
1025- {"$eq" : ["$payeeId" , user_id ]},
1026- ]
1027- },
1028- "$amount" ,
1029- 0 ,
1030- ]
1031- }
1032- },
1033- "friendOwes" : {
1034- "$sum" : {
1035- "$cond" : [
1036- {
1037- "$and" : [
1038- {"$eq" : ["$payerId" , user_id ]},
1039- {"$eq" : ["$payeeId" , friend_id ]},
1040- ]
1041- },
1042- "$amount" ,
1043- 0 ,
1044- ]
1045- }
1046- },
1047- }
1048- },
1049- ]
1090+ # Post-process results to build final response
1091+ friends_balance = []
1092+ user_totals = {"totalOwedToYou" : 0 , "totalYouOwe" : 0 }
10501093
1051- result = await self .settlements_collection .aggregate (pipeline ).to_list (
1052- None
1053- )
1054- balance_data = result [0 ] if result else {"userOwes" : 0 , "friendOwes" : 0 }
1094+ for result in results :
1095+ friend_id = result ["_id" ]
1096+ total_balance = result ["totalBalance" ]
10551097
1056- group_balance = balance_data [ "friendOwes" ] - balance_data [ "userOwes" ]
1057- total_friend_balance += group_balance
1098+ # Get friend details from map
1099+ friend_details = friends_map . get ( friend_id )
10581100
1059- if (
1060- abs (group_balance ) > 0.01
1061- ): # Only include if there's a significant balance
1062- friend_balance_data ["breakdown" ].append (
1101+ # Build breakdown by group
1102+ breakdown = []
1103+ for group_item in result .get ("groups" , []):
1104+ group_id = group_item ["groupId" ]
1105+ group_balance = group_item ["balance" ]
1106+
1107+ # Only include groups with significant balance
1108+ if abs (group_balance ) > 0.01 :
1109+ breakdown .append (
10631110 {
10641111 "groupId" : group_id ,
1065- "groupName" : group [ "name" ] ,
1066- "balance" : group_balance ,
1112+ "groupName" : groups_map . get ( group_id , "Unknown Group" ) ,
1113+ "balance" : round ( group_balance , 2 ) ,
10671114 "owesYou" : group_balance > 0 ,
10681115 }
10691116 )
10701117
1071- if (
1072- abs (total_friend_balance ) > 0.01
1073- ): # Only include friends with non-zero balance
1074- friend_balance_data ["netBalance" ] = total_friend_balance
1075- friend_balance_data ["owesYou" ] = total_friend_balance > 0
1118+ # Build friend balance object
1119+ friend_data = {
1120+ "userId" : friend_id ,
1121+ "userName" : (
1122+ friend_details .get ("name" , "Unknown" )
1123+ if friend_details
1124+ else "Unknown"
1125+ ),
1126+ "userImageUrl" : (
1127+ friend_details .get ("imageUrl" ) if friend_details else None
1128+ ),
1129+ "netBalance" : round (total_balance , 2 ),
1130+ "owesYou" : total_balance > 0 ,
1131+ "breakdown" : breakdown ,
1132+ "lastActivity" : datetime .utcnow (), # TODO: Calculate actual last activity
1133+ }
10761134
1077- if total_friend_balance > 0 :
1078- user_totals ["totalOwedToYou" ] += total_friend_balance
1079- else :
1080- user_totals ["totalYouOwe" ] += abs (total_friend_balance )
1135+ friends_balance .append (friend_data )
10811136
1082- friends_balance .append (friend_balance_data )
1137+ # Update totals
1138+ if total_balance > 0 :
1139+ user_totals ["totalOwedToYou" ] += total_balance
1140+ else :
1141+ user_totals ["totalYouOwe" ] += abs (total_balance )
10831142
10841143 return {
10851144 "friendsBalance" : friends_balance ,
10861145 "summary" : {
1087- "totalOwedToYou" : user_totals ["totalOwedToYou" ],
1088- "totalYouOwe" : user_totals ["totalYouOwe" ],
1089- "netBalance" : user_totals ["totalOwedToYou" ]
1090- - user_totals ["totalYouOwe" ],
1146+ "totalOwedToYou" : round (user_totals ["totalOwedToYou" ], 2 ),
1147+ "totalYouOwe" : round (user_totals ["totalYouOwe" ], 2 ),
1148+ "netBalance" : round (
1149+ user_totals ["totalOwedToYou" ] - user_totals ["totalYouOwe" ], 2
1150+ ),
10911151 "friendCount" : len (friends_balance ),
10921152 "activeGroups" : len (groups ),
10931153 },
0 commit comments