Repository: gratipay/gratipay.com

Issue:
break history page into multiple pages
Each week should get its own page, with paging nav added in the box.

break history page into multiple pages
Each week should get its own page, with paging nav added in the box.

Total Given on history pages for teams includes takes
On the history pages for teams, the "Total Given" amount includes both tips to people **and** takes by members, which is not _correct_. 



Discussion:
Hm. I don't see a problem with paginating it, but if we're going to make it one week per page, can we add a drop-down menu instead to allow people to view each week separately? It would be less annoying than having to click all the way back to your first couple weeks if you joined early.

Sure, I had figured on making the main /me/history/ page be a listing of all "statements."

Ah cool. No need to worry about my suggestion then.

Could make sense to tackle #838 along with this.

We should also provide yearly totals for tax purposes (was #1984).

Dropped to ★★☆.

+1 for yearly totals from https://gratipay.freshdesk.com/helpdesk/tickets/1749 with the additional requests of showing totals for each tippee and providing a CSV export

Hm. I don't see a problem with paginating it, but if we're going to make it one week per page, can we add a drop-down menu instead to allow people to view each week separately? It would be less annoying than having to click all the way back to your first couple weeks if you joined early.

Sure, I had figured on making the main /me/history/ page be a listing of all "statements."

Ah cool. No need to worry about my suggestion then.

Could make sense to tackle #838 along with this.

We should also provide yearly totals for tax purposes (was #1984).

Dropped to ★★☆.

+1 for yearly totals from https://gratipay.freshdesk.com/helpdesk/tickets/1749 with the additional requests of showing totals for each tippee and providing a CSV export



Resolving patch:
diff --git a/gratipay/utils/history.py b/gratipay/utils/history.py
--- a/gratipay/utils/history.py
+++ b/gratipay/utils/history.py
@@ -1,16 +1,83 @@
-def iter_payday_events(db, participant):
+from datetime import datetime
+from decimal import Decimal
+
+from psycopg2 import IntegrityError
+
+
+def get_end_of_year_balance(db, participant, year, current_year):
+    if year == current_year:
+        return participant.balance
+    if year < participant.claimed_time.year:
+        return Decimal('0.00')
+
+    balance = db.one("""
+        SELECT balance
+          FROM balances_at
+         WHERE participant = %s
+           AND "at" = %s
+    """, (participant.id, datetime(year+1, 1, 1)))
+    if balance is not None:
+        return balance
+
+    username = participant.username
+    start_balance = get_end_of_year_balance(db, participant, year-1, current_year)
+    delta = db.one("""
+        SELECT (
+                  SELECT COALESCE(sum(amount), 0) AS a
+                    FROM exchanges
+                   WHERE participant = %(username)s
+                     AND extract(year from timestamp) = %(year)s
+                     AND amount > 0
+                     AND (status is null OR status = 'succeeded')
+               ) + (
+                  SELECT COALESCE(sum(amount-fee), 0) AS a
+                    FROM exchanges
+                   WHERE participant = %(username)s
+                     AND extract(year from timestamp) = %(year)s
+                     AND amount < 0
+                     AND (status is null OR status <> 'failed')
+               ) + (
+                  SELECT COALESCE(sum(-amount), 0) AS a
+                    FROM transfers
+                   WHERE tipper = %(username)s
+                     AND extract(year from timestamp) = %(year)s
+               ) + (
+                  SELECT COALESCE(sum(amount), 0) AS a
+                    FROM transfers
+                   WHERE tippee = %(username)s
+                     AND extract(year from timestamp) = %(year)s
+               ) AS delta
+    """, locals())
+    balance = start_balance + delta
+    try:
+        db.run("""
+            INSERT INTO balances_at
+                        (participant, at, balance)
+                 VALUES (%s, %s, %s)
+        """, (participant.id, datetime(year+1, 1, 1), balance))
+    except IntegrityError:
+        pass
+    return balance
+
+
+def iter_payday_events(db, participant, year=None):
     """Yields payday events for the given participant.
     """
+    current_year = datetime.utcnow().year
+    year = year or current_year
+
     username = participant.username
     exchanges = db.all("""
         SELECT *
           FROM exchanges
-         WHERE participant=%s
-    """, (username,), back_as=dict)
+         WHERE participant=%(username)s
+           AND extract(year from timestamp) = %(year)s
+    """, locals(), back_as=dict)
     transfers = db.all("""
         SELECT *
           FROM transfers
-         WHERE tipper=%(username)s OR tippee=%(username)s
+         WHERE (tipper=%(username)s OR tippee=%(username)s)
+           AND extract(year from timestamp) = %(year)s
     """, locals(), back_as=dict)
 
     if not (exchanges or transfers):
@@ -19,7 +86,7 @@ def iter_payday_events(db, participant):
     if transfers:
         yield dict(
             kind='totals',
-            given=sum(t['amount'] for t in transfers if t['tipper'] == username),
+            given=sum(t['amount'] for t in transfers if t['tipper'] == username and t['context'] != 'take'),
             received=sum(t['amount'] for t in transfers if t['tippee'] == username),
         )
 
@@ -29,7 +96,7 @@ def iter_payday_events(db, participant):
       ORDER BY ts_start ASC
     """)
 
-    balance = participant.balance
+    balance = get_end_of_year_balance(db, participant, year, current_year)
     prev_date = None
     get_timestamp = lambda e: e['timestamp']
     events = sorted(exchanges+transfers, key=get_timestamp, reverse=True)
@@ -70,4 +137,4 @@ def iter_payday_events(db, participant):
 
         yield event
 
-    yield dict(kind='day-close', balance='0.00')
+    yield dict(kind='day-close', balance=balance)


Test patch:
diff --git a/tests/py/test_history.py b/tests/py/test_history.py
--- a/tests/py/test_history.py
+++ b/tests/py/test_history.py
@@ -54,7 +54,7 @@ def test_iter_payday_events(self):
         assert events[1]['payday_number'] == 2
         assert events[2]['balance'] == 12
         assert events[-1]['kind'] == 'day-close'
-        assert events[-1]['balance'] == '0.00'
+        assert events[-1]['balance'] == 0
 
         alice = Participant.from_id(alice.id)
         assert alice.balance == 4990
@@ -83,4 +83,4 @@ def test_iter_payday_events_with_failed_exchanges(self):
         assert events[3]['kind'] == 'charge'
         assert events[3]['balance'] == 50
         assert events[4]['kind'] == 'day-close'
-        assert events[4]['balance'] == '0.00'
+        assert events[4]['balance'] == 0
diff --git a/tests/py/test_record_an_exchange.py b/tests/py/test_record_an_exchange.py
--- a/tests/py/test_record_an_exchange.py
+++ b/tests/py/test_record_an_exchange.py
@@ -30,19 +30,11 @@ def test_success_is_302(self):
         actual = self.record_an_exchange('10', '0', 'foo').code
         assert actual == 302
 
-    def test_non_admin_is_404(self):
+    def test_non_admin_is_403(self):
         self.make_participant('alice', claimed_time=utcnow())
         self.make_participant('bob', claimed_time=utcnow())
         actual = self.record_an_exchange('10', '0', 'foo', make_participants=False).code
-        assert actual == 404
-
-    def test_non_post_is_405(self):
-        self.make_participant('alice', claimed_time=utcnow(), is_admin=True)
-        self.make_participant('bob', claimed_time=utcnow())
-        actual = self.client.GxT( '/bob/history/record-an-exchange'
-                                , auth_as='alice'
-                                 ).code
-        assert actual == 405
+        assert actual == 403
 
     def test_bad_amount_is_400(self):
         actual = self.record_an_exchange('cheese', '0', 'foo').code