Repository: liberapay/liberapay.com

Issue:
Stripe transactions aren't recorded
To date transactions have been listed at https://liberapay.com/about/me/wallet/, which shows a user's Mangopay wallet. Stripe transactions (at least giving; I have not received anything via Stripe so I don't know about that) do not.

![image](https://user-images.githubusercontent.com/3729038/44103447-dfd3307c-9fb1-11e8-98de-09bbfdc5eeab.png)




Discussion:

Resolving patch:
diff --git a/liberapay/utils/history.py b/liberapay/utils/history.py
--- a/liberapay/utils/history.py
+++ b/liberapay/utils/history.py
@@ -1,6 +1,7 @@
 from calendar import monthrange
 from collections import namedtuple
 from datetime import datetime, timedelta
+from itertools import chain
 
 from pando import Response
 from pando.utils import utc, utcnow
@@ -111,7 +112,7 @@ def get_end_of_period_balances(db, participant, period_end, today):
     return balances
 
 
-def get_ledger(db, participant, year=None, month=-1, reverse=True, minimize=False, past_only=False):
+def get_wallet_ledger(db, participant, year=None, month=-1, reverse=True, minimize=False, past_only=False):
     """Returns a `Ledger` object representing the participant's account history.
 
     When `year` is `None` the current year is used.
@@ -407,3 +408,89 @@ def export_history(participant, year, mode, key, back_as='namedtuple', require_k
         raise Response(400, "missing `key` parameter")
     else:
         return {k: v() for k, v in out.items()}
+
+
+def get_payin_ledger(db, participant, year=None, month=-1, reverse=True, minimize=False):
+    """Returns a list of events representing the participant's payment history.
+
+    When `year` is `None` the current year is used.
+
+    When `month` is `None` the current month is used,
+    when it's `-1` the returned object includes data for the whole year.
+
+    The `reverse` argument controls the order of the returned entries, when it's
+    `True` the events are in reverse chronological order (most recent first).
+
+    The `minimize` argument controls whether failed payments are skipped or not.
+    """
+    today = get_start_of_current_utc_day()
+    year = year or today.year
+    month = month or today.month
+    if month == -1:
+        period_start = datetime(year, 1, 1, tzinfo=utc)
+        period_end = datetime(year + 1, 1, 1, tzinfo=utc)
+    else:
+        start_day = participant.join_time.day
+        max_month_day = monthrange(year, month)[1]
+        period_start = datetime(year, month, min(start_day, max_month_day), tzinfo=utc)
+        period_end = month_plus_one(year, month, start_day)
+
+    events = list(iter_payin_events(
+        db, participant, period_start, period_end, minimize
+    ))
+    if not reverse:
+        events.reverse()
+    return events
+
+
+def iter_payin_events(db, participant, period_start, period_end, minimize=False):
+    """Yields payment events for the specified participant and time frame.
+    """
+    id = participant.id
+    params = locals()
+    payins = db.all("""
+        SELECT pi.ctime, pi.amount, pi.status, pi.error, pi.amount_settled, pi.fee
+          FROM payins pi
+         WHERE pi.payer = %(id)s
+           AND pi.ctime >= %(period_start)s
+           AND pi.ctime < %(period_end)s
+           AND (pi.status = 'succeeded' OR NOT %(minimize)s)
+    """, params, back_as=dict)
+    outgoing_transfers = db.all("""
+        SELECT tr.ctime, tr.payin, tr.recipient, tr.context, tr.status, tr.error
+             , tr.amount, tr.unit_amount, tr.n_units, tr.period
+             , p.username AS recipient_username, p2.username AS team_name
+          FROM payin_transfers tr
+          JOIN participants p ON p.id = tr.recipient
+     LEFT JOIN participants p2 ON p2.id = tr.team
+         WHERE tr.payer = %(id)s
+           AND tr.ctime >= %(period_start)s
+           AND tr.ctime < %(period_end)s
+           AND (tr.status = 'succeeded' OR NOT %(minimize)s)
+    """, params, back_as=dict)
+    incoming_transfers = db.all("""
+        SELECT tr.ctime, tr.payin, tr.payer, tr.context, tr.status, tr.error
+             , tr.amount, tr.unit_amount, tr.n_units, tr.period
+             , p.username AS payer_username, p2.username AS team_name
+          FROM payin_transfers tr
+          JOIN participants p ON p.id = tr.payer
+     LEFT JOIN participants p2 ON p2.id = tr.team
+         WHERE tr.recipient = %(id)s
+           AND tr.ctime >= %(period_start)s
+           AND tr.ctime < %(period_end)s
+           AND (tr.status = 'succeeded' OR NOT %(minimize)s)
+    """, params, back_as=dict)
+
+    prev_date = None
+    get_timestamp = lambda e: e['ctime']
+    events = sorted(
+        chain(payins, incoming_transfers, outgoing_transfers),
+        key=get_timestamp, reverse=True
+    )
+    for event in events:
+        event_date = event['ctime'].date()
+        if event_date != prev_date:
+            yield dict(kind='day-end', date=event_date)
+            prev_date = event_date
+        event['kind'] = 'payin_transfer' if 'team_name' in event else 'payin'
+        yield event


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
@@ -8,7 +8,7 @@
 from liberapay.testing import EUR, USD, Harness
 from liberapay.testing.mangopay import FakeTransfersHarness
 from liberapay.utils.history import (
-    get_end_of_period_balances, get_start_of_current_utc_day, get_ledger
+    get_end_of_period_balances, get_start_of_current_utc_day, get_wallet_ledger
 )
 
 
@@ -91,7 +91,7 @@ def test_iter_payday_events(self):
                SET timestamp = "timestamp" + interval %(delta)s;
         """, dict(delta=delta))
 
-        totals, start, end, events = get_ledger(self.db, bob, now.year)
+        totals, start, end, events = get_wallet_ledger(self.db, bob, now.year)
         assert totals['kind'] == 'totals'
         assert not totals['regular_donations']['sent']
         assert totals['regular_donations']['received'] == EUR(12)
@@ -104,13 +104,13 @@ def test_iter_payday_events(self):
 
         alice = Participant.from_id(alice.id)
         assert alice.balance == 4990
-        totals, start, end, events = get_ledger(self.db, alice, now.year)
+        totals, start, end, events = get_wallet_ledger(self.db, alice, now.year)
         assert totals['regular_donations']['sent'] == EUR(10)
         assert len(events) == 10
 
         carl = Participant.from_id(carl.id)
         assert carl.balance == 0
-        totals, start, end, events = get_ledger(self.db, carl, now.year)
+        totals, start, end, events = get_wallet_ledger(self.db, carl, now.year)
         assert len(events) == 0
 
     def test_iter_payday_events_with_failed_exchanges(self):
@@ -118,7 +118,7 @@ def test_iter_payday_events_with_failed_exchanges(self):
         self.make_exchange('mango-cc', 50, 0, alice)
         self.make_exchange('mango-cc', 12, 0, alice, status='failed')
         self.make_exchange('mango-ba', -40, 0, alice, status='failed')
-        totals, start, end, events = get_ledger(self.db, alice)
+        totals, start, end, events = get_wallet_ledger(self.db, alice)
         assert len(events) == 5
         assert events[0]['kind'] == 'day-end'
         assert events[0]['balances'] == EUR(50)