Repository: liberapay/liberapay.com

Issue:
Handle payout refunds
Basically add support for `PAYOUT_REFUND_SUCCEEDED` in `www/callbacks/mangopay.spt`. Doc: https://docs.mangopay.com/payout-refund/



Discussion:
We've received our first payout refund yesterday.


Resolving patch:
diff --git a/liberapay/billing/exchanges.py b/liberapay/billing/exchanges.py
--- a/liberapay/billing/exchanges.py
+++ b/liberapay/billing/exchanges.py
@@ -35,6 +35,10 @@
 from liberapay.models.exchange_route import ExchangeRoute
 
 
+Money.__eq__ = lambda a, b: isinstance(b, Money) and a.__dict__ == b.__dict__
+Money.__repr__ = lambda m: '<Money Amount=%(Amount)r Currency=%(Currency)r>' % m.__dict__
+
+
 QUARANTINE = '%s days' % QUARANTINE.days
 
 
@@ -253,6 +257,26 @@ def payin_bank_wire(db, participant, debit_amount):
     return payin, e
 
 
+def record_payout_refund(db, payout_refund):
+    orig_payout = mangoapi.payOuts.Get(payout_refund.InitialTransactionId)
+    e_origin = db.one("SELECT * FROM exchanges WHERE id = %s" % (orig_payout.Tag,))
+    e_refund_id = db.one("SELECT id FROM exchanges WHERE refund_ref = %s", (e_origin.id,))
+    if e_refund_id:
+        # Already recorded
+        return e_refund_id
+    amount, fee, vat = -e_origin.amount, -e_origin.fee, -e_origin.vat
+    assert payout_refund.DebitedFunds == Money(int(amount * 100), 'EUR')
+    assert payout_refund.Fees == Money(int(fee * 100), 'EUR')
+    route = ExchangeRoute.from_id(e_origin.route)
+    participant = Participant.from_id(e_origin.participant)
+    return db.one("""
+        INSERT INTO exchanges
+               (amount, fee, vat, participant, status, route, note, refund_ref)
+        VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
+     RETURNING id
+    """, (amount, fee, vat, participant.id, 'created', route.id, None, e_origin.id))
+
+
 def record_exchange(db, route, amount, fee, vat, participant, status, error=None):
     """Given a Bunch of Stuff, return an int (exchange_id).
 
@@ -312,7 +336,7 @@ def record_exchange_result(db, exchange_id, status, error, participant):
         if amount < 0:
             amount = -amount + max(e.fee, 0) if status == 'failed' else 0
         else:
-            amount = amount if status == 'succeeded' else 0
+            amount = amount - min(e.fee, 0) if status == 'succeeded' else 0
         propagate_exchange(cursor, participant, e, e.route, error, amount)
 
         return e


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
@@ -4,8 +4,10 @@
 
 from mangopaysdk.entities.payin import PayIn
 from mangopaysdk.entities.payout import PayOut
+from mangopaysdk.entities.refund import Refund
+from mangopaysdk.types.refundreason import RefundReason
 
-from liberapay.billing.exchanges import record_exchange
+from liberapay.billing.exchanges import Money, record_exchange
 from liberapay.models.exchange_route import ExchangeRoute
 from liberapay.testing.emails import EmailHarness
 from liberapay.testing.mangopay import MangopayHarness
@@ -52,6 +54,54 @@ def test_payout_callback(self, Get):
                 assert 'fail' in emails[0]['subject']
             homer.update_status('active')  # reset for next loop run
 
+    @patch('mangopaysdk.tools.apipayouts.ApiPayOuts.Get')
+    @patch('mangopaysdk.tools.apirefunds.ApiRefunds.Get')
+    def test_payout_refund_callback(self, R_Get, PO_Get):
+        homer, ba = self.homer, self.homer_route
+        for status in ('failed', 'succeeded'):
+            # Create the payout
+            self.make_exchange('mango-cc', 10, 0, homer)
+            e_id = record_exchange(self.db, ba, -9, 1, 0, homer, 'pre')
+            assert homer.balance == 0
+            homer.close(None)
+            assert homer.status == 'closed'
+            payout = PayOut()
+            payout.Status = 'SUCCEEDED'
+            payout.ResultCode = '000000'
+            payout.AuthorId = homer.mangopay_user_id
+            payout.Tag = str(e_id)
+            PO_Get.return_value = payout
+            # Create the refund
+            status_up = status.upper()
+            error = 'FOO' if status == 'failed' else None
+            refund = Refund()
+            refund.DebitedFunds = Money(900, 'EUR')
+            refund.Fees = Money(-100, 'EUR')
+            refund.Status = status_up
+            refund.ResultCode = '000001' if error else '000000'
+            refund.ResultMessage = error
+            reason = refund.RefundReason = RefundReason()
+            reason.RefusedReasonMessage = 'BECAUSE 42'
+            refund.AuthorId = homer.mangopay_user_id
+            R_Get.return_value = refund
+            # Call back
+            qs = "EventType=PAYOUT_REFUND_"+status_up+"&RessourceId=123456790"
+            r = self.callback(qs)
+            assert r.code == 200, r.text
+            homer = homer.refetch()
+            if status == 'failed':
+                assert homer.balance == 0
+                assert homer.status == 'closed'
+            else:
+                assert homer.balance == 10
+                assert homer.status == 'active'
+                emails = self.get_emails()
+                assert len(emails) == 1
+                assert emails[0]['to'][0] == 'homer <%s>' % homer.email
+                assert 'fail' in emails[0]['subject']
+                assert 'BECAUSE 42' in emails[0]['text']
+            homer.update_status('active')  # reset for next loop run
+
     @patch('mangopaysdk.tools.apipayins.ApiPayIns.Get')
     def test_payin_bank_wire_callback(self, Get):
         homer = self.homer