Repository: liberapay/liberapay.com

Issue:
Bank wire expiration can lead to confusion
- [ ] modify `emails/payin_bankwire_failed.spt` to include the creation date of the payin
- [ ] don't send a notification if the user did another payin on the same day and it was successful


Discussion:
Related issue: #413.

Resolving patch:
diff --git a/liberapay/constants.py b/liberapay/constants.py
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -94,6 +94,7 @@ def check_bits(bits):
     Event('team_invite', 32, _("When someone invites me to join a team")),
     Event('payin_bankwire_failed', 64, _("When a bank wire transfer to my Liberapay wallet fails")),
     Event('payin_bankwire_succeeded', 128, _("When a bank wire transfer to my Liberapay wallet succeeds")),
+    Event('payin_bankwire_expired', 256, _("When a bank wire transfer to my Liberapay wallet expires")),
 ]
 check_bits([e.bit for e in EVENTS])
 EVENTS = OrderedDict((e.name, e) for e in EVENTS)
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -808,17 +808,24 @@ def queue_email(self, spt_name, **context):
 
     @classmethod
     def dequeue_emails(cls):
-        fetch_messages = lambda: cls.db.all("""
+        with cls.db.get_cursor() as cursor:
+            cls._dequeue_emails(cursor)
+
+    @classmethod
+    def _dequeue_emails(cls, cursor):
+        fetch_messages = lambda last_id: cursor.all("""
             SELECT *
               FROM email_queue
+             WHERE id > %s
           ORDER BY id ASC
              LIMIT 60
-        """)
+        """, (last_id,))
         delete = lambda m: cls.db.run(
             "DELETE FROM email_queue WHERE id = %s", (m.id,)
         )
+        last_id = 0
         while True:
-            messages = fetch_messages()
+            messages = fetch_messages(last_id)
             if not messages:
                 break
             for msg in messages:
@@ -836,6 +843,7 @@ def dequeue_emails(cls):
                 else:
                     delete(msg)
                 sleep(1)
+            last_id = messages[-1].id
 
     def set_email_lang(self, accept_lang, cursor=None):
         if not accept_lang:
@@ -954,7 +962,7 @@ def fill_notification_context(self, context):
 
     def get_notifs(self):
         return self.db.all("""
-            SELECT id, event, context, is_new
+            SELECT id, event, context, is_new, ts
               FROM notification_queue
              WHERE participant = %s
           ORDER BY is_new DESC, id DESC
@@ -969,16 +977,17 @@ def render_notifications(self, state, notifs=None):
         notifs = notifs or self.get_notifs()
 
         r = []
-        for id, event, notif_context, is_new in notifs:
+        for id, event, notif_context, is_new, ts in notifs:
             try:
                 notif_context = deserialize(notif_context)
                 context = dict(state)
                 self.fill_notification_context(context)
                 context.update(notif_context)
                 spt = website.emails[event]
+                subject = spt['subject'].render(context).strip()
                 html = spt['text/html'].render(context).strip()
                 typ = notif_context.get('type', 'info')
-                r.append(dict(id=id, html=html, type=typ, is_new=is_new))
+                r.append(dict(id=id, subject=subject, html=html, type=typ, is_new=is_new, ts=ts))
             except Exception as e:
                 website.tell_sentry(e, state)
         return r


Test patch:
diff --git a/tests/py/test_callbacks.py b/tests/py/test_callbacks.py
--- a/tests/py/test_callbacks.py
+++ b/tests/py/test_callbacks.py
@@ -217,9 +217,13 @@ def test_payout_refund_callback(self, R_Get, PO_Get):
     def test_payin_bank_wire_callback(self, Get):
         homer = self.homer
         route = ExchangeRoute.insert(homer, 'mango-bw', 'x')
-        for status in ('failed', 'succeeded'):
+        cases = (
+            ('failed', '000001', 'FOO'),
+            ('failed', '101109', 'The payment period has expired'),
+            ('succeeded', '000000', None),
+        )
+        for status, result_code, error in cases:
             status_up = status.upper()
-            error = 'FOO' if status == 'failed' else None
             e_id = record_exchange(self.db, route, 11, 0, 0, homer, 'pre')
             assert homer.balance == 0
             homer.close(None)
@@ -227,7 +231,7 @@ def test_payin_bank_wire_callback(self, Get):
             qs = "EventType=PAYIN_NORMAL_"+status_up+"&RessourceId=123456790"
             payin = BankWirePayIn()
             payin.Status = status_up
-            payin.ResultCode = '000001' if error else '000000'
+            payin.ResultCode = result_code
             payin.ResultMessage = error
             payin.AuthorId = homer.mangopay_user_id
             payin.PaymentType = 'BANK_WIRE'
@@ -248,7 +252,8 @@ def test_payin_bank_wire_callback(self, Get):
             emails = self.get_emails()
             assert len(emails) == 1
             assert emails[0]['to'][0] == 'homer <%s>' % homer.email
-            assert status[:4] in emails[0]['subject']
+            expected = 'expired' if result_code == '101109' else status[:4]
+            assert expected in emails[0]['subject']
             self.db.self_check()
             homer.update_status('active')  # reset for next loop run
 
diff --git a/tests/py/test_notifications.py b/tests/py/test_notifications.py
--- a/tests/py/test_notifications.py
+++ b/tests/py/test_notifications.py
@@ -27,7 +27,9 @@ def test_remove_notification(self):
 
     def test_render_notifications(self):
         self.client.website.emails['test_event'] = {
-            'subject': 'Test notification',
+            'subject': SimplateLoader(None, """
+                Test notification subject
+            """).load(jinja_env_html, None),
             'text/html': SimplateLoader(None, """
                 Test that builtins are available: len([]) = {{ len([]) }}.
                 Test escaping: {{ _("{0}", '<test-escaping>'|safe) }}
@@ -48,6 +50,7 @@ def test_render_notifications(self):
         assert ' alert-&lt;type ' not in r
         assert 'alert-info' in r
         assert '<test-escaping>' in r, r
+        assert 'Test notification subject' in r
 
     def test_render_unknown_notification(self):
         alice = self.make_participant('alice')