Repository: gratipay/gratipay.com

Issue:
settle the whole graph of tips, not just the first part
Currently, payday settles the tip graph to a depth of 2. We should settle the whole graph. [IRC](https://botbot.me/freenode/gittip/2014-08-20/?msg=20074861&page=1)

<bountysource-plugin>

---

Want to back this issue? **[Place a bounty on it!](https://www.bountysource.com/issues/3944770-settle-the-whole-graph-of-tips-not-just-the-first-part?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github).
</bountysource-plugin>



Discussion:
Though we also need to take `takes` into account. We want to settle the whole graph of `tips` _and_ `takes` together.

Previous work on this was done in #449 and #1493.

To illustrate, let's say that we have this tip graph:
- `alice` tips $100 to `bob`
- `bob` tips $10 to `carl`
- `carl` tips $1 to `dana`
- `dana` tips 10¢ to `earl`

Only `alice` has a credit card attached. The way we run now, `bob` gets his tip, and we let him tip `carl`, but then we stop. We should let `bob` tip `carl` in turn, and `carl` `dana`, and `dana` `earl`.

Would this be the place to stop carrying balances over from week to week? @Changaco and I discussed that on #2579; @nathany brought it up on #1486.


Resolving patch:
diff --git a/gratipay/billing/payday.py b/gratipay/billing/payday.py
--- a/gratipay/billing/payday.py
+++ b/gratipay/billing/payday.py
@@ -162,6 +162,7 @@ def payin(self):
         self.db.run("""
             DROP FUNCTION process_take();
             DROP FUNCTION process_tip();
+            DROP FUNCTION settle_tip_graph();
             DROP FUNCTION transfer(text, text, numeric, context_type);
         """)
 
@@ -324,6 +325,33 @@ def prepare(cursor, ts_start):
             FOR EACH ROW EXECUTE PROCEDURE process_take();
 
 
+        -- Create a function to settle whole tip graph
+
+        CREATE OR REPLACE FUNCTION settle_tip_graph() RETURNS void AS $$
+            DECLARE
+                count integer NOT NULL DEFAULT 0;
+                i integer := 0;
+            BEGIN
+                LOOP
+                    i := i + 1;
+                    WITH updated_rows AS (
+                         UPDATE payday_tips
+                            SET is_funded = true
+                          WHERE is_funded IS NOT true
+                      RETURNING *
+                    )
+                    SELECT COUNT(*) FROM updated_rows INTO count;
+                    IF (count = 0) THEN
+                        EXIT;
+                    END IF;
+                    IF (i > 50) THEN
+                        RAISE 'Reached the maximum number of iterations';
+                    END IF;
+                END LOOP;
+            END;
+        $$ LANGUAGE plpgsql;
+
+
         -- Save the stats we already have
 
         UPDATE paydays
@@ -425,9 +453,7 @@ def transfer_tips(cursor):
          WHERE p.username = t.tipper
            AND p.card_hold_ok;
 
-        UPDATE payday_tips t
-           SET is_funded = true
-         WHERE is_funded IS NOT true;
+        SELECT settle_tip_graph();
 
         """)
 
@@ -455,6 +481,8 @@ def transfer_takes(cursor, ts_start):
                    ) IS NULL
           ORDER BY t.team, t.ctime DESC;
 
+        SELECT settle_tip_graph();
+
         """, dict(ts_start=ts_start))
 
 


Test patch:
diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py
--- a/tests/py/test_billing_payday.py
+++ b/tests/py/test_billing_payday.py
@@ -366,6 +366,33 @@ def test_transfer_tips(self):
         assert Participant.from_id(self.janet.id).balance == D('0.51')
         assert Participant.from_id(self.homer.id).balance == 0
 
+    def test_transfer_tips_whole_graph(self):
+        alice = self.make_participant('alice', claimed_time='now', balance=0,
+                                      last_bill_result='')
+        alice.set_tip_to(self.homer, D('50'))
+        self.homer.set_tip_to(self.janet, D('20'))
+        self.janet.set_tip_to(self.david, D('5'))
+        self.david.set_tip_to(self.homer, D('20'))  # Should be unfunded
+
+        payday = Payday.start()
+        with self.db.get_cursor() as cursor:
+            payday.prepare(cursor, payday.ts_start)
+            cursor.run("""UPDATE payday_participants
+                             SET card_hold_ok = true
+                           WHERE id = %s
+                """, (alice.id,))
+            payday.transfer_tips(cursor)
+            cursor.run("""UPDATE payday_participants
+                             SET new_balance = 0
+                           WHERE id = %s
+                """, (alice.id,))
+            payday.update_balances(cursor)
+        alice = Participant.from_id(alice.id)
+        assert Participant.from_id(alice.id).balance == D('0')
+        assert Participant.from_id(self.homer.id).balance == D('30')
+        assert Participant.from_id(self.janet.id).balance == D('15')
+        assert Participant.from_id(self.david.id).balance == D('5')
+
     def test_transfer_takes(self):
         a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20)
         alice = self.make_participant('alice', claimed_time='now')