Repository: liberapay/liberapay.com

Issue:
Take throttling is too complicated
https://witches.town/@Alda/2122717


Discussion:

Resolving patch:
diff --git a/liberapay/models/_mixin_team.py b/liberapay/models/_mixin_team.py
--- a/liberapay/models/_mixin_team.py
+++ b/liberapay/models/_mixin_team.py
@@ -86,19 +86,21 @@ def get_take_for(self, member):
         )
 
     def compute_max_this_week(self, member_id, last_week):
-        """2x the member's take last week, or the member's take last week + a
-        proportional share of the leftover, or a minimum based on last week's
-        median take, or 1.
+        """2x the member's take last week, or the member's take last week + the
+        leftover, or last week's median take, or 1.00, or infinity if the takes
+        were all zero last week or if throttling is disabled.
         """
+        if not self.throttle_takes:
+            return D_INF
         sum_last_week = sum(last_week.values())
+        if sum_last_week == 0:
+            return D_INF
         initial_leftover = self.receiving - sum_last_week
         nonzero_last_week = [a for a in last_week.values() if a]
         member_last_week = last_week.get(member_id, 0)
-        leftover_share = member_last_week / (sum_last_week or D_INF)
-        leftover_share = max(leftover_share, D_UNIT / self.nmembers)
         return max(
             member_last_week * 2,
-            member_last_week + initial_leftover * leftover_share,
+            member_last_week + initial_leftover,
             median(nonzero_last_week or (0,)),
             D_UNIT
         )
@@ -116,17 +118,17 @@ def set_take_for(self, member, take, recorder, check_max=True, cursor=None):
         if not isinstance(take, (None.__class__, Decimal)):
             take = Decimal(take)
 
-        if take and check_max and take > 1:
-            last_week = self.get_takes_last_week()
-            max_this_week = self.compute_max_this_week(member.id, last_week)
-            if take > max_this_week:
-                take = max_this_week
-
         with self.db.get_cursor(cursor) as cursor:
             # Lock to avoid race conditions
             cursor.run("LOCK TABLE takes IN EXCLUSIVE MODE")
             # Compute the current takes
             old_takes = self.compute_actual_takes(cursor)
+            # Throttle the new take, if there is more than one member
+            if take and check_max and len(old_takes) > 1 and take > 1:
+                last_week = self.get_takes_last_week()
+                max_this_week = self.compute_max_this_week(member.id, last_week)
+                if take > max_this_week:
+                    take = max_this_week
             # Insert the new take
             cursor.run("""
 
@@ -234,7 +236,8 @@ def get_members(self):
             member['nominal_take'] = take['nominal_take']
             member['actual_amount'] = take['actual_amount']
             member['last_week'] = last_week.get(m_id, D_ZERO)
-            member['max_this_week'] = self.compute_max_this_week(m_id, last_week)
+            x = self.compute_max_this_week(m_id, last_week)
+            member['max_this_week'] = x if x.is_finite() else None
             members[member['id']] = member
         return members
 


Test patch:
diff --git a/tests/py/test_take.py b/tests/py/test_take.py
--- a/tests/py/test_take.py
+++ b/tests/py/test_take.py
@@ -20,6 +20,21 @@ def make_team(self, username=TEAM, **kw):
         self.warbucks.set_tip_to(team, '100')
         return team
 
+    def make_team_of_one(self, username=TEAM, **kw):
+        team = self.make_team(username=username, **kw)
+        alice = self.make_team_member(team, 'alice')
+        return team, alice
+
+    def make_team_of_two(self, username=TEAM, **kw):
+        team, alice = self.make_team_of_one(username=username, **kw)
+        bob = self.make_team_member(team, 'bob')
+        return team, alice, bob
+
+    def make_team_member(self, team, username, **kw):
+        user = self.make_participant(username, **kw)
+        team.add_member(user)
+        return user
+
     def take_last_week(self, team, member, amount, actual_amount=None):
         team.set_take_for(member, amount, team, check_max=False)
         self.db.run("INSERT INTO paydays DEFAULT VALUES")
@@ -37,59 +52,66 @@ def test_random_schmoe_is_not_member_of_team(self):
         assert not schmoe.member_of(team)
 
     def test_team_member_is_team_member(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
-        self.take_last_week(team, alice, '40.00')
+        team, alice = self.make_team_of_one()
         assert alice.member_of(team)
 
+    def test_can_take_any_amount_when_there_is_only_one_member(self):
+        team, alice = self.make_team_of_one()
+        self.take_last_week(team, alice, '0.44')
+        team.set_take_for(alice, D('333.33'), alice)
+        assert team.get_take_for(alice) == D('333.33')
+
     def test_take_can_double(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice, bob = self.make_team_of_two()
         self.take_last_week(team, alice, '40.00')
         team.set_take_for(alice, D('80.00'), alice)
         assert team.get_take_for(alice) == 80
 
     def test_take_can_double_but_not_a_penny_more(self):
-        team = self.make_team()
+        team, alice, bob = self.make_team_of_two()
         self.warbucks.set_tip_to(team, '20')
-        alice = self.make_participant('alice')
         self.take_last_week(team, alice, '40.00')
         actual = team.set_take_for(alice, D('80.01'), alice)
         assert actual == 80
 
     def test_increase_is_based_on_nominal_take_last_week(self):
-        team = self.make_team()
+        team, alice, bob = self.make_team_of_two()
         self.warbucks.set_tip_to(team, '15.03')
-        alice = self.make_participant('alice')
         self.take_last_week(team, alice, '20.00', actual_amount='15.03')
         team.set_take_for(alice, D('35.00'), team, check_max=False)
         assert team.set_take_for(alice, D('42.00'), alice) == 40
 
     def test_if_last_week_is_less_than_one_can_increase_to_one(self):
-        team = self.make_team()
+        team, alice, bob = self.make_team_of_two()
         self.warbucks.set_tip_to(team, '0.50')
-        alice = self.make_participant('alice')
         self.take_last_week(team, alice, '0.01')
         actual = team.set_take_for(alice, D('42.00'), team)
         assert actual == 1
 
-    def test_can_take_one_share_when_takes_were_all_zero_last_week(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+    def test_can_take_any_amount_when_takes_were_all_zero_last_week(self):
+        team, alice, bob = self.make_team_of_two()
         self.take_last_week(team, alice, '0.00')
-        actual = team.set_take_for(alice, D('200.00'), team)
-        assert actual == 100
+        self.take_last_week(team, bob, '0.00')
+        actual = team.set_take_for(alice, D('222.00'), team)
+        assert actual == 222
 
     def test_can_take_leftover(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice, bob = self.make_team_of_two()
         self.take_last_week(team, alice, '0.01')
         actual = team.set_take_for(alice, D('200.00'), team)
         assert actual == 100
 
+    def test_can_take_any_amount_when_throttling_is_disabled(self):
+        team, alice, bob = self.make_team_of_two(throttle_takes=False)
+        self.take_last_week(team, alice, '0.00')
+        team.set_take_for(alice, D('400.00'), alice)
+        assert team.get_take_for(alice) == 400
+        self.take_last_week(team, alice, '10.00')
+        team.set_take_for(alice, D('500.00'), alice)
+        assert team.get_take_for(alice) == 500
+
     def test_get_members(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice = self.make_team_of_one()
         self.take_last_week(team, alice, '40.00')
         team.set_take_for(alice, D('42.00'), team)
         members = team.get_members()
@@ -99,8 +121,7 @@ def test_get_members(self):
         assert members[alice.id]['actual_amount'] == 42
 
     def test_taking_and_receiving_are_updated_correctly(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice = self.make_team_of_one()
         self.take_last_week(team, alice, '40.00')
         self.warbucks.set_tip_to(team, D('42.00'))
         team.set_take_for(alice, D('42.00'), alice)
@@ -116,8 +137,7 @@ def test_taking_and_receiving_are_updated_correctly(self):
         assert alice.receiving == 60
 
     def test_taking_is_zero_for_team(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice = self.make_team_of_one()
         team.add_member(alice)
         team = Participant.from_id(team.id)
         assert team.taking == 0
@@ -130,8 +150,7 @@ def test_team_cant_take_from_other_team(self):
             a_team.add_member(b_team)
 
     def test_changes_to_team_receiving_affect_members_take(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice = self.make_team_of_one()
         self.take_last_week(team, alice, '40.00')
         team.set_take_for(alice, D('42.00'), alice)
 
@@ -140,13 +159,11 @@ def test_changes_to_team_receiving_affect_members_take(self):
         assert alice.receiving == alice.taking == 10
 
     def test_changes_to_others_take_affects_members_take(self):
-        team = self.make_team()
+        team, alice, bob = self.make_team_of_two()
 
-        alice = self.make_participant('alice')
         self.take_last_week(team, alice, '30.00')
         team.set_take_for(alice, D('40.00'), alice)
 
-        bob = self.make_participant('bob')
         self.take_last_week(team, bob, '50.00')
         team.set_take_for(bob, D('60.00'), bob)
 
@@ -157,13 +174,11 @@ def test_changes_to_others_take_affects_members_take(self):
             assert m['nominal_take'] == m['actual_amount']
 
     def test_changes_to_others_take_can_increase_members_take(self):
-        team = self.make_team()
+        team, alice, bob = self.make_team_of_two()
 
-        alice = self.make_participant('alice')
         self.take_last_week(team, alice, '30.00')
         team.set_take_for(alice, D('25.00'), alice)
 
-        bob = self.make_participant('bob')
         self.take_last_week(team, bob, '50.00')
         team.set_take_for(bob, D('100.00'), bob)
         alice = Participant.from_username('alice')
@@ -176,8 +191,7 @@ def test_changes_to_others_take_can_increase_members_take(self):
     # get_takes_last_week - gtlw
 
     def test_gtlwf_works_during_payday(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
+        team, alice = self.make_team_of_one()
         self.take_last_week(team, alice, '20.00')
         assert team.get_takes_last_week()[alice.id] == 20
         self.take_last_week(team, alice, '30.00')