Skip to content

Commit 171147e

Browse files
committed
feat: optimize settlement aggregation for friends' balance summary
1 parent 83c4d1b commit 171147e

File tree

1 file changed

+38
-46
lines changed

1 file changed

+38
-46
lines changed

backend/tests/expenses/test_expense_service.py

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,53 +1602,45 @@ async def test_get_friends_balance_summary_success(expense_service):
16021602
},
16031603
]
16041604

1605-
# Mocking settlement aggregations for each friend in each group
1605+
# Mocking the OPTIMIZED settlement aggregation
1606+
# The new optimized version makes ONE aggregation call that returns all friends' balances
16061607
# Friend 1:
1607-
# Group Alpha: Main owes Friend1 50 (net -50 for Main)
1608-
# Group Beta: Friend1 owes Main 30 (net +30 for Main)
1609-
# Total for Friend1: Main is owed 50, owes 30. Net: Main is owed 20 by Friend1.
1608+
# Group Alpha: Main owes Friend1 50 (balance: -50 for Main)
1609+
# Group Beta: Friend1 owes Main 30 (balance: +30 for Main)
1610+
# Total for Friend1: -50 + 30 = -20 (Main owes Friend1 20)
16101611
# Friend 2:
1611-
# Group Beta: Main owes Friend2 70 (net -70 for Main)
1612-
# Total for Friend2: Main owes 70 to Friend2.
1612+
# Group Beta: Main owes Friend2 70 (balance: -70 for Main)
1613+
# Total for Friend2: -70 (Main owes Friend2 70)
16131614

1614-
# This is the side_effect for the .aggregate() call. It must be a sync function
1615-
# that returns a cursor mock (AsyncMock).
16161615
def sync_mock_settlements_aggregate_cursor_factory(pipeline, *args, **kwargs):
1617-
match_clause = pipeline[0]["$match"]
1618-
group_id_pipeline = match_clause["groupId"]
1619-
or_conditions = match_clause["$or"]
1620-
1621-
# Determine which friend is being processed based on payer/payee in OR condition
1622-
# This is a simplification; real queries are more complex
1623-
pipeline_friend_id = None
1624-
for cond in or_conditions:
1625-
if cond["payerId"] == user_id_str and cond["payeeId"] != user_id_str:
1626-
pipeline_friend_id = cond["payeeId"]
1627-
break
1628-
elif cond["payeeId"] == user_id_str and cond["payerId"] != user_id_str:
1629-
pipeline_friend_id = cond["payerId"]
1630-
break
1631-
1616+
# The optimized version returns aggregated results for all friends in one go
16321617
mock_agg_cursor = AsyncMock()
1633-
if group_id_pipeline == group1_id and pipeline_friend_id == friend1_id_str:
1634-
# Main owes Friend1 50 in Group Alpha
1635-
mock_agg_cursor.to_list.return_value = [
1636-
{"_id": None, "userOwes": 50.0, "friendOwes": 0.0}
1637-
]
1638-
elif group_id_pipeline == group2_id and pipeline_friend_id == friend1_id_str:
1639-
# Friend1 owes Main 30 in Group Beta
1640-
mock_agg_cursor.to_list.return_value = [
1641-
{"_id": None, "userOwes": 0.0, "friendOwes": 30.0}
1642-
]
1643-
elif group_id_pipeline == group2_id and pipeline_friend_id == friend2_id_str:
1644-
# Main owes Friend2 70 in Group Beta
1645-
mock_agg_cursor.to_list.return_value = [
1646-
{"_id": None, "userOwes": 70.0, "friendOwes": 0.0}
1647-
]
1648-
else:
1649-
mock_agg_cursor.to_list.return_value = [
1650-
{"_id": None, "userOwes": 0.0, "friendOwes": 0.0}
1651-
] # Default empty
1618+
mock_agg_cursor.to_list.return_value = [
1619+
{
1620+
"_id": friend1_id_str, # Friend 1
1621+
"totalBalance": -20.0, # Main owes Friend1 20 (net: -50 from G1, +30 from G2)
1622+
"groups": [
1623+
{
1624+
"groupId": group1_id,
1625+
"balance": -50.0,
1626+
}, # Main owes 50 in Group Alpha
1627+
{
1628+
"groupId": group2_id,
1629+
"balance": 30.0,
1630+
}, # Friend1 owes 30 in Group Beta
1631+
],
1632+
},
1633+
{
1634+
"_id": friend2_id_str, # Friend 2
1635+
"totalBalance": -70.0, # Main owes Friend2 70
1636+
"groups": [
1637+
{
1638+
"groupId": group2_id,
1639+
"balance": -70.0,
1640+
}, # Main owes 70 in Group Beta
1641+
],
1642+
},
1643+
]
16521644
return mock_agg_cursor
16531645

16541646
with patch("app.expenses.service.mongodb") as mock_mongodb:
@@ -1678,7 +1670,7 @@ def mock_user_find_cursor_side_effect(query, *args, **kwargs):
16781670

16791671
mock_db.users.find = MagicMock(side_effect=mock_user_find_cursor_side_effect)
16801672

1681-
# Mock settlement aggregation logic
1673+
# Mock the optimized settlement aggregation logic
16821674
# .aggregate() is sync, returns an async cursor.
16831675
mock_db.settlements.aggregate = MagicMock(
16841676
side_effect=sync_mock_settlements_aggregate_cursor_factory
@@ -1739,9 +1731,9 @@ def mock_user_find_cursor_side_effect(query, *args, **kwargs):
17391731

17401732
# Verify mocks
17411733
mock_db.groups.find.assert_called_once_with({"members.userId": user_id_str})
1742-
# settlements.aggregate is called for each friend in each group they share with user_id_str
1743-
# Friend1 is in 2 groups with user_id_str, Friend2 is in 1 group with user_id_str. Total 3 calls.
1744-
assert mock_db.settlements.aggregate.call_count == 3
1734+
# OPTIMIZED: settlements.aggregate is called ONCE (not per friend/group)
1735+
# The optimized version uses a single aggregation pipeline to get all friends' balances
1736+
assert mock_db.settlements.aggregate.call_count == 1
17451737

17461738

17471739
@pytest.mark.asyncio

0 commit comments

Comments
 (0)