We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Please expand information about reversed transactions
I have a payment set up by bank transfer with my IBAN number. It has been refused/reversed several times. My bank does not recognize that they have received a request for funds transfer. Is it possible to add more information to the notification so I can copy/paste it to my bank?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `liberapay/models/exchange_route.py`
Content:
```
1 from calendar import monthrange
2 from datetime import date
3 
4 from cached_property import cached_property
5 from mangopay.resources import Card, Mandate
6 from postgres.orm import Model
7 import stripe
8 
9 from ..constants import CARD_BRANDS
10 from ..exceptions import InvalidId
11 
12 
13 class ExchangeRoute(Model):
14 
15     typname = "exchange_routes"
16 
17     def __bool__(self):
18         return self.status in ('pending', 'chargeable')
19 
20     __nonzero__ = __bool__
21 
22     def __repr__(self):
23         return '<ExchangeRoute id=%r participant=%r network=%r status=%r>' % (
24             self.id, self.participant, self.network, self.status
25         )
26 
27     @classmethod
28     def from_id(cls, participant, id, _raise=True):
29         route = cls.db.one("""
30             SELECT r
31               FROM exchange_routes r
32              WHERE r.id = %(r_id)s
33                AND r.participant = %(p_id)s
34         """, dict(r_id=id, p_id=participant.id))
35         if route is not None:
36             route.__dict__['participant'] = participant
37         elif _raise:
38             raise InvalidId(id, cls.__name__)
39         return route
40 
41     @classmethod
42     def from_network(cls, participant, network, currency=None):
43         participant_id = participant.id
44         if network.startswith('mango-'):
45             remote_user_id = participant.mangopay_user_id
46         elif network.startswith('stripe-'):
47             remote_user_id = None
48         else:
49             raise NotImplementedError(network)
50         r = cls.db.all("""
51             SELECT r
52               FROM exchange_routes r
53              WHERE participant = %(participant_id)s
54                AND COALESCE(remote_user_id = %(remote_user_id)s, true)
55                AND network::text = %(network)s
56                AND status = 'chargeable'
57                AND COALESCE(currency::text, '') = COALESCE(%(currency)s::text, '')
58                AND (one_off IS FALSE OR ctime > (current_timestamp - interval '6 hours'))
59           ORDER BY r.is_default NULLS LAST, r.id DESC
60         """, locals())
61         for route in r:
62             route.__dict__['participant'] = participant
63         return r
64 
65     @classmethod
66     def from_address(cls, participant, network, address):
67         participant_id = participant.id
68         r = cls.db.one("""
69             SELECT r
70               FROM exchange_routes r
71              WHERE participant = %(participant_id)s
72                AND network = %(network)s
73                AND address = %(address)s
74         """, locals())
75         if r:
76             r.__dict__['participant'] = participant
77         return r
78 
79     @classmethod
80     def insert(cls, participant, network, address, status,
81                one_off=False, remote_user_id=None, country=None, currency=None):
82         p_id = participant.id
83         if network.startswith('mango-'):
84             remote_user_id = participant.mangopay_user_id
85         r = cls.db.one("""
86             INSERT INTO exchange_routes AS r
87                         (participant, network, address, status,
88                          one_off, remote_user_id, country, currency)
89                  VALUES (%(p_id)s, %(network)s, %(address)s, %(status)s,
90                          %(one_off)s, %(remote_user_id)s, %(country)s, %(currency)s)
91             ON CONFLICT (participant, network, address) DO NOTHING
92               RETURNING r
93         """, locals()) or cls.db.one("""
94             SELECT r
95               FROM exchange_routes r
96              WHERE participant = %s
97                AND network = %s
98                AND address = %s
99         """, (p_id, network, address))
100         if r.status == 'chargeable' and network.startswith('stripe-'):
101             cls.db.run("""
102                 DO $$
103                 BEGIN
104                     UPDATE exchange_routes
105                        SET is_default = true
106                      WHERE id = %s;
107                 EXCEPTION
108                     WHEN unique_violation THEN RETURN;
109                 END;
110                 $$
111             """, (r.id,))
112         r.__dict__['participant'] = participant
113         return r
114 
115     @classmethod
116     def upsert_generic_route(cls, participant, network):
117         if network.startswith('mango-'):
118             remote_user_id = participant.mangopay_user_id
119         elif network == 'paypal':
120             remote_user_id = 'x'
121         r = cls.db.one("""
122             INSERT INTO exchange_routes AS r
123                         (participant, network, address, one_off, status, remote_user_id)
124                  VALUES (%s, %s, 'x', false, 'chargeable', %s)
125             ON CONFLICT (participant, network, address) DO UPDATE
126                     SET one_off = excluded.one_off
127                       , status = excluded.status
128                       , remote_user_id = excluded.remote_user_id
129               RETURNING r
130         """, (participant.id, network, remote_user_id))
131         r.__dict__['participant'] = participant
132         return r
133 
134     @classmethod
135     def attach_stripe_payment_method(cls, participant, pm, one_off):
136         if pm.type == 'card':
137             network = 'stripe-card'
138         elif pm.type == 'sepa_debit':
139             network = 'stripe-sdd'
140         else:
141             raise NotImplementedError(pm.type)
142         customer_id = cls.db.one("""
143             SELECT remote_user_id
144               FROM exchange_routes
145              WHERE participant = %s
146                AND network::text LIKE 'stripe-%%'
147              LIMIT 1
148         """, (participant.id,))
149         if customer_id:
150             pm = stripe.PaymentMethod.attach(
151                 pm.id, customer=customer_id,
152                 idempotency_key='attach_%s_to_%s' % (pm.id, customer_id),
153             )
154         else:
155             customer_id = stripe.Customer.create(
156                 email=participant.get_email_address(),
157                 payment_method=pm.id,
158                 idempotency_key='create_customer_for_participant_%i_with_%s' % (
159                     participant.id, pm.id
160                 ),
161             ).id
162         pm_country = getattr(getattr(pm, pm.type), 'country', None)
163         pm_currency = getattr(getattr(pm, pm.type), 'currency', None)
164         route = cls.insert(
165             participant, network, pm.id, 'chargeable',
166             one_off=one_off, remote_user_id=customer_id,
167             country=pm_country, currency=pm_currency,
168         )
169         route.stripe_payment_method = pm
170         return route
171 
172     @classmethod
173     def attach_stripe_source(cls, participant, source, one_off):
174         if source.type == 'sepa_debit':
175             network = 'stripe-sdd'
176         elif source.type == 'card':
177             network = 'stripe-card'
178         else:
179             raise NotImplementedError(source.type)
180         customer_id = cls.db.one("""
181             SELECT remote_user_id
182               FROM exchange_routes
183              WHERE participant = %s
184                AND network::text LIKE 'stripe-%%'
185              LIMIT 1
186         """, (participant.id,))
187         if customer_id:
188             customer = stripe.Customer.retrieve(customer_id)
189             customer.sources.create(
190                 source=source.id,
191                 idempotency_key='attach_%s_to_%s' % (source.id, customer_id),
192             )
193             del customer
194         else:
195             customer_id = stripe.Customer.create(
196                 email=source.owner.email,
197                 source=source.id,
198                 idempotency_key='create_customer_for_participant_%i_with_%s' % (
199                     participant.id, source.id
200                 ),
201             ).id
202         source_country = getattr(getattr(source, source.type), 'country', None)
203         source_currency = getattr(getattr(source, source.type), 'currency', None)
204         route = ExchangeRoute.insert(
205             participant, network, source.id, source.status,
206             one_off=one_off, remote_user_id=customer_id,
207             country=source_country, currency=source_currency,
208         )
209         route.stripe_source = source
210         return route
211 
212     def invalidate(self, obj=None):
213         if self.network.startswith('stripe-'):
214             if self.address.startswith('pm_'):
215                 stripe.PaymentMethod.detach(self.address)
216             else:
217                 try:
218                     source = stripe.Source.retrieve(self.address).detach()
219                 except stripe.error.InvalidRequestError as e:
220                     if "does not appear to be currently attached" in str(e):
221                         pass
222                     else:
223                         raise
224                 else:
225                     assert source.status not in ('chargeable', 'pending')
226                     self.update_status(source.status)
227                     return
228         elif self.network == 'mango-cc':
229             card = obj or Card.get(self.address)
230             if card.Active:
231                 card.Active = False
232                 card.save()
233                 assert card.Active is False, card.Active
234         if self.mandate:
235             mandate = Mandate.get(self.mandate)
236             if mandate.Status in ('SUBMITTED', 'ACTIVE'):
237                 mandate.cancel()
238         self.update_status('canceled')
239 
240     def set_as_default(self):
241         with self.db.get_cursor() as cursor:
242             cursor.run("""
243                 UPDATE exchange_routes
244                    SET is_default = NULL
245                  WHERE participant = %(p_id)s
246                    AND is_default IS NOT NULL;
247                 UPDATE exchange_routes
248                    SET is_default = true
249                  WHERE participant = %(p_id)s
250                    AND id = %(route_id)s
251             """, dict(p_id=self.participant.id, route_id=self.id))
252             self.participant.add_event(cursor, 'set_default_route', dict(
253                 id=self.id, network=self.network
254             ))
255 
256     def set_mandate(self, mandate_id):
257         self.db.run("""
258             UPDATE exchange_routes
259                SET mandate = %s
260              WHERE id = %s
261         """, (mandate_id, self.id))
262         self.set_attributes(mandate=mandate_id)
263 
264     def update_status(self, new_status):
265         id = self.id
266         if new_status == self.status:
267             return
268         self.db.run("""
269             UPDATE exchange_routes
270                SET status = %(new_status)s
271              WHERE id = %(id)s
272         """, locals())
273         self.set_attributes(status=new_status)
274 
275     def get_brand(self):
276         if self.network == 'stripe-card':
277             if self.address.startswith('pm_'):
278                 brand = self.stripe_payment_method.card.brand
279                 return CARD_BRANDS.get(brand, brand)
280             else:
281                 return self.stripe_source.card.brand
282         elif self.network == 'stripe-sdd':
283             if self.address.startswith('pm_'):
284                 raise NotImplementedError()
285             else:
286                 return getattr(self.stripe_source.sepa_debit, 'bank_name', '')
287         else:
288             raise NotImplementedError(self.network)
289 
290     def get_expiration_date(self):
291         if self.network == 'stripe-card':
292             if self.address.startswith('pm_'):
293                 card = self.stripe_payment_method.card
294             else:
295                 card = self.stripe_source.card
296             day = monthrange(card.exp_year, card.exp_month)[-1]
297             return date(card.exp_year, card.exp_month, day)
298         elif self.network == 'stripe-sdd':
299             return None
300         else:
301             raise NotImplementedError(self.network)
302 
303     def get_mandate_url(self):
304         if self.network == 'stripe-card':
305             return
306         elif self.network == 'stripe-sdd':
307             if self.address.startswith('pm_'):
308                 raise NotImplementedError()
309             else:
310                 return self.stripe_source.sepa_debit.mandate_url
311         else:
312             raise NotImplementedError(self.network)
313 
314     def get_partial_number(self):
315         if self.network == 'stripe-card':
316             if self.address.startswith('pm_'):
317                 return '⋯' + str(self.stripe_payment_method.card.last4)
318             else:
319                 return '⋯' + str(self.stripe_source.card.last4)
320         elif self.network == 'stripe-sdd':
321             from ..payin.stripe import get_partial_iban
322             if self.address.startswith('pm_'):
323                 raise NotImplementedError()
324             else:
325                 return get_partial_iban(self.stripe_source.sepa_debit)
326         else:
327             raise NotImplementedError(self.network)
328 
329     @cached_property
330     def stripe_payment_method(self):
331         return stripe.PaymentMethod.retrieve(self.address)
332 
333     @cached_property
334     def stripe_source(self):
335         return stripe.Source.retrieve(self.address)
336 
337     def sync_status(self):
338         if self.address.startswith('pm_'):
339             # A PaymentMethod doesn't have a status.
340             return
341         else:
342             source = stripe.Source.retrieve(self.address)
343             self.update_status(source.status)
344             self.stripe_source = source
345
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/liberapay/models/exchange_route.py b/liberapay/models/exchange_route.py
--- a/liberapay/models/exchange_route.py
+++ b/liberapay/models/exchange_route.py
@@ -326,6 +326,16 @@
         else:
             raise NotImplementedError(self.network)
 
+    def has_been_charged_successfully(self):
+        return bool(self.db.one("""
+            SELECT 1
+              FROM payins pi
+             WHERE pi.payer = %s
+               AND pi.route = %s
+               AND pi.status = 'succeeded'
+             LIMIT 1
+        """, (self.participant.id, self.id)))
+
     @cached_property
     def stripe_payment_method(self):
         return stripe.PaymentMethod.retrieve(self.address)

{"golden_diff": "diff --git a/liberapay/models/exchange_route.py b/liberapay/models/exchange_route.py\n--- a/liberapay/models/exchange_route.py\n+++ b/liberapay/models/exchange_route.py\n@@ -326,6 +326,16 @@\n         else:\n             raise NotImplementedError(self.network)\n \n+    def has_been_charged_successfully(self):\n+        return bool(self.db.one(\"\"\"\n+            SELECT 1\n+              FROM payins pi\n+             WHERE pi.payer = %s\n+               AND pi.route = %s\n+               AND pi.status = 'succeeded'\n+             LIMIT 1\n+        \"\"\", (self.participant.id, self.id)))\n+\n     @cached_property\n     def stripe_payment_method(self):\n         return stripe.PaymentMethod.retrieve(self.address)\n", "issue": "Please expand information about reversed transactions\nI have a payment set up by bank transfer with my IBAN number. It has been refused/reversed several times. My bank does not recognize that they have received a request for funds transfer. Is it possible to add more information to the notification so I can copy/paste it to my bank?\n", "before_files": [{"content": "from calendar import monthrange\nfrom datetime import date\n\nfrom cached_property import cached_property\nfrom mangopay.resources import Card, Mandate\nfrom postgres.orm import Model\nimport stripe\n\nfrom ..constants import CARD_BRANDS\nfrom ..exceptions import InvalidId\n\n\nclass ExchangeRoute(Model):\n\n    typname = \"exchange_routes\"\n\n    def __bool__(self):\n        return self.status in ('pending', 'chargeable')\n\n    __nonzero__ = __bool__\n\n    def __repr__(self):\n        return '<ExchangeRoute id=%r participant=%r network=%r status=%r>' % (\n            self.id, self.participant, self.network, self.status\n        )\n\n    @classmethod\n    def from_id(cls, participant, id, _raise=True):\n        route = cls.db.one(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE r.id = %(r_id)s\n               AND r.participant = %(p_id)s\n        \"\"\", dict(r_id=id, p_id=participant.id))\n        if route is not None:\n            route.__dict__['participant'] = participant\n        elif _raise:\n            raise InvalidId(id, cls.__name__)\n        return route\n\n    @classmethod\n    def from_network(cls, participant, network, currency=None):\n        participant_id = participant.id\n        if network.startswith('mango-'):\n            remote_user_id = participant.mangopay_user_id\n        elif network.startswith('stripe-'):\n            remote_user_id = None\n        else:\n            raise NotImplementedError(network)\n        r = cls.db.all(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE participant = %(participant_id)s\n               AND COALESCE(remote_user_id = %(remote_user_id)s, true)\n               AND network::text = %(network)s\n               AND status = 'chargeable'\n               AND COALESCE(currency::text, '') = COALESCE(%(currency)s::text, '')\n               AND (one_off IS FALSE OR ctime > (current_timestamp - interval '6 hours'))\n          ORDER BY r.is_default NULLS LAST, r.id DESC\n        \"\"\", locals())\n        for route in r:\n            route.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def from_address(cls, participant, network, address):\n        participant_id = participant.id\n        r = cls.db.one(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE participant = %(participant_id)s\n               AND network = %(network)s\n               AND address = %(address)s\n        \"\"\", locals())\n        if r:\n            r.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def insert(cls, participant, network, address, status,\n               one_off=False, remote_user_id=None, country=None, currency=None):\n        p_id = participant.id\n        if network.startswith('mango-'):\n            remote_user_id = participant.mangopay_user_id\n        r = cls.db.one(\"\"\"\n            INSERT INTO exchange_routes AS r\n                        (participant, network, address, status,\n                         one_off, remote_user_id, country, currency)\n                 VALUES (%(p_id)s, %(network)s, %(address)s, %(status)s,\n                         %(one_off)s, %(remote_user_id)s, %(country)s, %(currency)s)\n            ON CONFLICT (participant, network, address) DO NOTHING\n              RETURNING r\n        \"\"\", locals()) or cls.db.one(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE participant = %s\n               AND network = %s\n               AND address = %s\n        \"\"\", (p_id, network, address))\n        if r.status == 'chargeable' and network.startswith('stripe-'):\n            cls.db.run(\"\"\"\n                DO $$\n                BEGIN\n                    UPDATE exchange_routes\n                       SET is_default = true\n                     WHERE id = %s;\n                EXCEPTION\n                    WHEN unique_violation THEN RETURN;\n                END;\n                $$\n            \"\"\", (r.id,))\n        r.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def upsert_generic_route(cls, participant, network):\n        if network.startswith('mango-'):\n            remote_user_id = participant.mangopay_user_id\n        elif network == 'paypal':\n            remote_user_id = 'x'\n        r = cls.db.one(\"\"\"\n            INSERT INTO exchange_routes AS r\n                        (participant, network, address, one_off, status, remote_user_id)\n                 VALUES (%s, %s, 'x', false, 'chargeable', %s)\n            ON CONFLICT (participant, network, address) DO UPDATE\n                    SET one_off = excluded.one_off\n                      , status = excluded.status\n                      , remote_user_id = excluded.remote_user_id\n              RETURNING r\n        \"\"\", (participant.id, network, remote_user_id))\n        r.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def attach_stripe_payment_method(cls, participant, pm, one_off):\n        if pm.type == 'card':\n            network = 'stripe-card'\n        elif pm.type == 'sepa_debit':\n            network = 'stripe-sdd'\n        else:\n            raise NotImplementedError(pm.type)\n        customer_id = cls.db.one(\"\"\"\n            SELECT remote_user_id\n              FROM exchange_routes\n             WHERE participant = %s\n               AND network::text LIKE 'stripe-%%'\n             LIMIT 1\n        \"\"\", (participant.id,))\n        if customer_id:\n            pm = stripe.PaymentMethod.attach(\n                pm.id, customer=customer_id,\n                idempotency_key='attach_%s_to_%s' % (pm.id, customer_id),\n            )\n        else:\n            customer_id = stripe.Customer.create(\n                email=participant.get_email_address(),\n                payment_method=pm.id,\n                idempotency_key='create_customer_for_participant_%i_with_%s' % (\n                    participant.id, pm.id\n                ),\n            ).id\n        pm_country = getattr(getattr(pm, pm.type), 'country', None)\n        pm_currency = getattr(getattr(pm, pm.type), 'currency', None)\n        route = cls.insert(\n            participant, network, pm.id, 'chargeable',\n            one_off=one_off, remote_user_id=customer_id,\n            country=pm_country, currency=pm_currency,\n        )\n        route.stripe_payment_method = pm\n        return route\n\n    @classmethod\n    def attach_stripe_source(cls, participant, source, one_off):\n        if source.type == 'sepa_debit':\n            network = 'stripe-sdd'\n        elif source.type == 'card':\n            network = 'stripe-card'\n        else:\n            raise NotImplementedError(source.type)\n        customer_id = cls.db.one(\"\"\"\n            SELECT remote_user_id\n              FROM exchange_routes\n             WHERE participant = %s\n               AND network::text LIKE 'stripe-%%'\n             LIMIT 1\n        \"\"\", (participant.id,))\n        if customer_id:\n            customer = stripe.Customer.retrieve(customer_id)\n            customer.sources.create(\n                source=source.id,\n                idempotency_key='attach_%s_to_%s' % (source.id, customer_id),\n            )\n            del customer\n        else:\n            customer_id = stripe.Customer.create(\n                email=source.owner.email,\n                source=source.id,\n                idempotency_key='create_customer_for_participant_%i_with_%s' % (\n                    participant.id, source.id\n                ),\n            ).id\n        source_country = getattr(getattr(source, source.type), 'country', None)\n        source_currency = getattr(getattr(source, source.type), 'currency', None)\n        route = ExchangeRoute.insert(\n            participant, network, source.id, source.status,\n            one_off=one_off, remote_user_id=customer_id,\n            country=source_country, currency=source_currency,\n        )\n        route.stripe_source = source\n        return route\n\n    def invalidate(self, obj=None):\n        if self.network.startswith('stripe-'):\n            if self.address.startswith('pm_'):\n                stripe.PaymentMethod.detach(self.address)\n            else:\n                try:\n                    source = stripe.Source.retrieve(self.address).detach()\n                except stripe.error.InvalidRequestError as e:\n                    if \"does not appear to be currently attached\" in str(e):\n                        pass\n                    else:\n                        raise\n                else:\n                    assert source.status not in ('chargeable', 'pending')\n                    self.update_status(source.status)\n                    return\n        elif self.network == 'mango-cc':\n            card = obj or Card.get(self.address)\n            if card.Active:\n                card.Active = False\n                card.save()\n                assert card.Active is False, card.Active\n        if self.mandate:\n            mandate = Mandate.get(self.mandate)\n            if mandate.Status in ('SUBMITTED', 'ACTIVE'):\n                mandate.cancel()\n        self.update_status('canceled')\n\n    def set_as_default(self):\n        with self.db.get_cursor() as cursor:\n            cursor.run(\"\"\"\n                UPDATE exchange_routes\n                   SET is_default = NULL\n                 WHERE participant = %(p_id)s\n                   AND is_default IS NOT NULL;\n                UPDATE exchange_routes\n                   SET is_default = true\n                 WHERE participant = %(p_id)s\n                   AND id = %(route_id)s\n            \"\"\", dict(p_id=self.participant.id, route_id=self.id))\n            self.participant.add_event(cursor, 'set_default_route', dict(\n                id=self.id, network=self.network\n            ))\n\n    def set_mandate(self, mandate_id):\n        self.db.run(\"\"\"\n            UPDATE exchange_routes\n               SET mandate = %s\n             WHERE id = %s\n        \"\"\", (mandate_id, self.id))\n        self.set_attributes(mandate=mandate_id)\n\n    def update_status(self, new_status):\n        id = self.id\n        if new_status == self.status:\n            return\n        self.db.run(\"\"\"\n            UPDATE exchange_routes\n               SET status = %(new_status)s\n             WHERE id = %(id)s\n        \"\"\", locals())\n        self.set_attributes(status=new_status)\n\n    def get_brand(self):\n        if self.network == 'stripe-card':\n            if self.address.startswith('pm_'):\n                brand = self.stripe_payment_method.card.brand\n                return CARD_BRANDS.get(brand, brand)\n            else:\n                return self.stripe_source.card.brand\n        elif self.network == 'stripe-sdd':\n            if self.address.startswith('pm_'):\n                raise NotImplementedError()\n            else:\n                return getattr(self.stripe_source.sepa_debit, 'bank_name', '')\n        else:\n            raise NotImplementedError(self.network)\n\n    def get_expiration_date(self):\n        if self.network == 'stripe-card':\n            if self.address.startswith('pm_'):\n                card = self.stripe_payment_method.card\n            else:\n                card = self.stripe_source.card\n            day = monthrange(card.exp_year, card.exp_month)[-1]\n            return date(card.exp_year, card.exp_month, day)\n        elif self.network == 'stripe-sdd':\n            return None\n        else:\n            raise NotImplementedError(self.network)\n\n    def get_mandate_url(self):\n        if self.network == 'stripe-card':\n            return\n        elif self.network == 'stripe-sdd':\n            if self.address.startswith('pm_'):\n                raise NotImplementedError()\n            else:\n                return self.stripe_source.sepa_debit.mandate_url\n        else:\n            raise NotImplementedError(self.network)\n\n    def get_partial_number(self):\n        if self.network == 'stripe-card':\n            if self.address.startswith('pm_'):\n                return '\u22ef' + str(self.stripe_payment_method.card.last4)\n            else:\n                return '\u22ef' + str(self.stripe_source.card.last4)\n        elif self.network == 'stripe-sdd':\n            from ..payin.stripe import get_partial_iban\n            if self.address.startswith('pm_'):\n                raise NotImplementedError()\n            else:\n                return get_partial_iban(self.stripe_source.sepa_debit)\n        else:\n            raise NotImplementedError(self.network)\n\n    @cached_property\n    def stripe_payment_method(self):\n        return stripe.PaymentMethod.retrieve(self.address)\n\n    @cached_property\n    def stripe_source(self):\n        return stripe.Source.retrieve(self.address)\n\n    def sync_status(self):\n        if self.address.startswith('pm_'):\n            # A PaymentMethod doesn't have a status.\n            return\n        else:\n            source = stripe.Source.retrieve(self.address)\n            self.update_status(source.status)\n            self.stripe_source = source\n", "path": "liberapay/models/exchange_route.py"}], "after_files": [{"content": "from calendar import monthrange\nfrom datetime import date\n\nfrom cached_property import cached_property\nfrom mangopay.resources import Card, Mandate\nfrom postgres.orm import Model\nimport stripe\n\nfrom ..constants import CARD_BRANDS\nfrom ..exceptions import InvalidId\n\n\nclass ExchangeRoute(Model):\n\n    typname = \"exchange_routes\"\n\n    def __bool__(self):\n        return self.status in ('pending', 'chargeable')\n\n    __nonzero__ = __bool__\n\n    def __repr__(self):\n        return '<ExchangeRoute id=%r participant=%r network=%r status=%r>' % (\n            self.id, self.participant, self.network, self.status\n        )\n\n    @classmethod\n    def from_id(cls, participant, id, _raise=True):\n        route = cls.db.one(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE r.id = %(r_id)s\n               AND r.participant = %(p_id)s\n        \"\"\", dict(r_id=id, p_id=participant.id))\n        if route is not None:\n            route.__dict__['participant'] = participant\n        elif _raise:\n            raise InvalidId(id, cls.__name__)\n        return route\n\n    @classmethod\n    def from_network(cls, participant, network, currency=None):\n        participant_id = participant.id\n        if network.startswith('mango-'):\n            remote_user_id = participant.mangopay_user_id\n        elif network.startswith('stripe-'):\n            remote_user_id = None\n        else:\n            raise NotImplementedError(network)\n        r = cls.db.all(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE participant = %(participant_id)s\n               AND COALESCE(remote_user_id = %(remote_user_id)s, true)\n               AND network::text = %(network)s\n               AND status = 'chargeable'\n               AND COALESCE(currency::text, '') = COALESCE(%(currency)s::text, '')\n               AND (one_off IS FALSE OR ctime > (current_timestamp - interval '6 hours'))\n          ORDER BY r.is_default NULLS LAST, r.id DESC\n        \"\"\", locals())\n        for route in r:\n            route.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def from_address(cls, participant, network, address):\n        participant_id = participant.id\n        r = cls.db.one(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE participant = %(participant_id)s\n               AND network = %(network)s\n               AND address = %(address)s\n        \"\"\", locals())\n        if r:\n            r.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def insert(cls, participant, network, address, status,\n               one_off=False, remote_user_id=None, country=None, currency=None):\n        p_id = participant.id\n        if network.startswith('mango-'):\n            remote_user_id = participant.mangopay_user_id\n        r = cls.db.one(\"\"\"\n            INSERT INTO exchange_routes AS r\n                        (participant, network, address, status,\n                         one_off, remote_user_id, country, currency)\n                 VALUES (%(p_id)s, %(network)s, %(address)s, %(status)s,\n                         %(one_off)s, %(remote_user_id)s, %(country)s, %(currency)s)\n            ON CONFLICT (participant, network, address) DO NOTHING\n              RETURNING r\n        \"\"\", locals()) or cls.db.one(\"\"\"\n            SELECT r\n              FROM exchange_routes r\n             WHERE participant = %s\n               AND network = %s\n               AND address = %s\n        \"\"\", (p_id, network, address))\n        if r.status == 'chargeable' and network.startswith('stripe-'):\n            cls.db.run(\"\"\"\n                DO $$\n                BEGIN\n                    UPDATE exchange_routes\n                       SET is_default = true\n                     WHERE id = %s;\n                EXCEPTION\n                    WHEN unique_violation THEN RETURN;\n                END;\n                $$\n            \"\"\", (r.id,))\n        r.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def upsert_generic_route(cls, participant, network):\n        if network.startswith('mango-'):\n            remote_user_id = participant.mangopay_user_id\n        elif network == 'paypal':\n            remote_user_id = 'x'\n        r = cls.db.one(\"\"\"\n            INSERT INTO exchange_routes AS r\n                        (participant, network, address, one_off, status, remote_user_id)\n                 VALUES (%s, %s, 'x', false, 'chargeable', %s)\n            ON CONFLICT (participant, network, address) DO UPDATE\n                    SET one_off = excluded.one_off\n                      , status = excluded.status\n                      , remote_user_id = excluded.remote_user_id\n              RETURNING r\n        \"\"\", (participant.id, network, remote_user_id))\n        r.__dict__['participant'] = participant\n        return r\n\n    @classmethod\n    def attach_stripe_payment_method(cls, participant, pm, one_off):\n        if pm.type == 'card':\n            network = 'stripe-card'\n        elif pm.type == 'sepa_debit':\n            network = 'stripe-sdd'\n        else:\n            raise NotImplementedError(pm.type)\n        customer_id = cls.db.one(\"\"\"\n            SELECT remote_user_id\n              FROM exchange_routes\n             WHERE participant = %s\n               AND network::text LIKE 'stripe-%%'\n             LIMIT 1\n        \"\"\", (participant.id,))\n        if customer_id:\n            pm = stripe.PaymentMethod.attach(\n                pm.id, customer=customer_id,\n                idempotency_key='attach_%s_to_%s' % (pm.id, customer_id),\n            )\n        else:\n            customer_id = stripe.Customer.create(\n                email=participant.get_email_address(),\n                payment_method=pm.id,\n                idempotency_key='create_customer_for_participant_%i_with_%s' % (\n                    participant.id, pm.id\n                ),\n            ).id\n        pm_country = getattr(getattr(pm, pm.type), 'country', None)\n        pm_currency = getattr(getattr(pm, pm.type), 'currency', None)\n        route = cls.insert(\n            participant, network, pm.id, 'chargeable',\n            one_off=one_off, remote_user_id=customer_id,\n            country=pm_country, currency=pm_currency,\n        )\n        route.stripe_payment_method = pm\n        return route\n\n    @classmethod\n    def attach_stripe_source(cls, participant, source, one_off):\n        if source.type == 'sepa_debit':\n            network = 'stripe-sdd'\n        elif source.type == 'card':\n            network = 'stripe-card'\n        else:\n            raise NotImplementedError(source.type)\n        customer_id = cls.db.one(\"\"\"\n            SELECT remote_user_id\n              FROM exchange_routes\n             WHERE participant = %s\n               AND network::text LIKE 'stripe-%%'\n             LIMIT 1\n        \"\"\", (participant.id,))\n        if customer_id:\n            customer = stripe.Customer.retrieve(customer_id)\n            customer.sources.create(\n                source=source.id,\n                idempotency_key='attach_%s_to_%s' % (source.id, customer_id),\n            )\n            del customer\n        else:\n            customer_id = stripe.Customer.create(\n                email=source.owner.email,\n                source=source.id,\n                idempotency_key='create_customer_for_participant_%i_with_%s' % (\n                    participant.id, source.id\n                ),\n            ).id\n        source_country = getattr(getattr(source, source.type), 'country', None)\n        source_currency = getattr(getattr(source, source.type), 'currency', None)\n        route = ExchangeRoute.insert(\n            participant, network, source.id, source.status,\n            one_off=one_off, remote_user_id=customer_id,\n            country=source_country, currency=source_currency,\n        )\n        route.stripe_source = source\n        return route\n\n    def invalidate(self, obj=None):\n        if self.network.startswith('stripe-'):\n            if self.address.startswith('pm_'):\n                stripe.PaymentMethod.detach(self.address)\n            else:\n                try:\n                    source = stripe.Source.retrieve(self.address).detach()\n                except stripe.error.InvalidRequestError as e:\n                    if \"does not appear to be currently attached\" in str(e):\n                        pass\n                    else:\n                        raise\n                else:\n                    assert source.status not in ('chargeable', 'pending')\n                    self.update_status(source.status)\n                    return\n        elif self.network == 'mango-cc':\n            card = obj or Card.get(self.address)\n            if card.Active:\n                card.Active = False\n                card.save()\n                assert card.Active is False, card.Active\n        if self.mandate:\n            mandate = Mandate.get(self.mandate)\n            if mandate.Status in ('SUBMITTED', 'ACTIVE'):\n                mandate.cancel()\n        self.update_status('canceled')\n\n    def set_as_default(self):\n        with self.db.get_cursor() as cursor:\n            cursor.run(\"\"\"\n                UPDATE exchange_routes\n                   SET is_default = NULL\n                 WHERE participant = %(p_id)s\n                   AND is_default IS NOT NULL;\n                UPDATE exchange_routes\n                   SET is_default = true\n                 WHERE participant = %(p_id)s\n                   AND id = %(route_id)s\n            \"\"\", dict(p_id=self.participant.id, route_id=self.id))\n            self.participant.add_event(cursor, 'set_default_route', dict(\n                id=self.id, network=self.network\n            ))\n\n    def set_mandate(self, mandate_id):\n        self.db.run(\"\"\"\n            UPDATE exchange_routes\n               SET mandate = %s\n             WHERE id = %s\n        \"\"\", (mandate_id, self.id))\n        self.set_attributes(mandate=mandate_id)\n\n    def update_status(self, new_status):\n        id = self.id\n        if new_status == self.status:\n            return\n        self.db.run(\"\"\"\n            UPDATE exchange_routes\n               SET status = %(new_status)s\n             WHERE id = %(id)s\n        \"\"\", locals())\n        self.set_attributes(status=new_status)\n\n    def get_brand(self):\n        if self.network == 'stripe-card':\n            if self.address.startswith('pm_'):\n                brand = self.stripe_payment_method.card.brand\n                return CARD_BRANDS.get(brand, brand)\n            else:\n                return self.stripe_source.card.brand\n        elif self.network == 'stripe-sdd':\n            if self.address.startswith('pm_'):\n                raise NotImplementedError()\n            else:\n                return getattr(self.stripe_source.sepa_debit, 'bank_name', '')\n        else:\n            raise NotImplementedError(self.network)\n\n    def get_expiration_date(self):\n        if self.network == 'stripe-card':\n            if self.address.startswith('pm_'):\n                card = self.stripe_payment_method.card\n            else:\n                card = self.stripe_source.card\n            day = monthrange(card.exp_year, card.exp_month)[-1]\n            return date(card.exp_year, card.exp_month, day)\n        elif self.network == 'stripe-sdd':\n            return None\n        else:\n            raise NotImplementedError(self.network)\n\n    def get_mandate_url(self):\n        if self.network == 'stripe-card':\n            return\n        elif self.network == 'stripe-sdd':\n            if self.address.startswith('pm_'):\n                raise NotImplementedError()\n            else:\n                return self.stripe_source.sepa_debit.mandate_url\n        else:\n            raise NotImplementedError(self.network)\n\n    def get_partial_number(self):\n        if self.network == 'stripe-card':\n            if self.address.startswith('pm_'):\n                return '\u22ef' + str(self.stripe_payment_method.card.last4)\n            else:\n                return '\u22ef' + str(self.stripe_source.card.last4)\n        elif self.network == 'stripe-sdd':\n            from ..payin.stripe import get_partial_iban\n            if self.address.startswith('pm_'):\n                raise NotImplementedError()\n            else:\n                return get_partial_iban(self.stripe_source.sepa_debit)\n        else:\n            raise NotImplementedError(self.network)\n\n    def has_been_charged_successfully(self):\n        return bool(self.db.one(\"\"\"\n            SELECT 1\n              FROM payins pi\n             WHERE pi.payer = %s\n               AND pi.route = %s\n               AND pi.status = 'succeeded'\n             LIMIT 1\n        \"\"\", (self.participant.id, self.id)))\n\n    @cached_property\n    def stripe_payment_method(self):\n        return stripe.PaymentMethod.retrieve(self.address)\n\n    @cached_property\n    def stripe_source(self):\n        return stripe.Source.retrieve(self.address)\n\n    def sync_status(self):\n        if self.address.startswith('pm_'):\n            # A PaymentMethod doesn't have a status.\n            return\n        else:\n            source = stripe.Source.retrieve(self.address)\n            self.update_status(source.status)\n            self.stripe_source = source\n", "path": "liberapay/models/exchange_route.py"}]}