Projet

Général

Profil

0002-lingo-add-poll_backend-method-to-PaymentBackend-and-.patch

Benjamin Dauvergne, 30 avril 2021 02:33

Télécharger (10,9 ko)

Voir les différences:

Subject: [PATCH 2/5] lingo: add poll_backend method to PaymentBackend and
 Transaction (#49149)

Some payment backends in eopayment (like PayFiP) allow polling the
status of currently running transaction, and can signal if a running
transaction has expired. The new can_poll_backend() and poll_backend()
method on Transaction implement this conditional behaviour in lingo.
 combo/apps/lingo/__init__.py |   8 +++
 combo/apps/lingo/models.py   |  51 +++++++++++++-
 tests/test_lingo_payment.py  | 125 ++++++++++++++++++++++++-----------
 3 files changed, 145 insertions(+), 39 deletions(-)
combo/apps/lingo/__init__.py
39 39
    def hourly(self):
40 40
        self.update_transactions()
41 41
        self.notify_payments()
42
        self.poll_transaction_status()
42 43

  
43 44
    def update_transactions(self):
44 45
        from .models import EXPIRED, Transaction
......
77 78
            except:
78 79
                logger.exception('error in async notification for basket item %s', item.id)
79 80

  
81
    def poll_transaction_status(self):
82
        from .models import PaymentBackend
83

  
84
        for payment_backend in PaymentBackend.objects.all():
85
            if payment_backend.can_poll_backend():
86
                payment_backend.poll_backend()
87

  
80 88

  
81 89
default_app_config = 'combo.apps.lingo.AppConfig'
combo/apps/lingo/models.py
41 41
from django.utils.formats import localize
42 42
from django.utils.http import urlencode
43 43
from django.utils.six.moves.urllib import parse as urlparse
44
from django.utils.timezone import make_aware, utc
44
from django.utils.timezone import make_aware, now, utc
45 45
from django.utils.translation import ugettext_lazy as _
46 46
from jsonfield import JSONField
47 47
from requests import RequestException
......
231 231
        transaction.handle_backend_response(response, callback=callback)
232 232
        return transaction
233 233

  
234
    @property
235
    def backend(self):
236
        return self.get_payment()
237

  
238
    def can_poll_backend(self):
239
        return self.backend.has_payment_status
240

  
241
    def poll_backend(self, min_age=None, max_age=None):
242
        if not self.can_poll_backend():
243
            return
244
        current_time = now()
245
        # poll transactions linked to the current backend
246
        # aged between 20 minutes and 3 hours, max_age can be overriden
247
        min_age = min_age or datetime.timedelta(minutes=20)
248
        not_after = current_time - min_age
249
        max_age = max_age or datetime.timedelta(hours=3)
250
        not_before = current_time - max_age
251
        transactions = Transaction.objects.filter(
252
            regie__payment_backend=self,
253
            start_date__lt=not_after,
254
            start_date__gt=not_before,
255
            status__in=Transaction.RUNNING_STATUSES,
256
        )
257
        for transaction in transactions:
258
            transaction.poll_backend()
259

  
234 260

  
235 261
@python_2_unicode_compatible
236 262
class Regie(models.Model):
......
547 573
        regie = next(serializers.deserialize('json', json.dumps([json_regie]), ignorenonexistent=True))
548 574
        regie.save()
549 575

  
576
    def can_poll_backend(self):
577
        return self.payment_backend.can_poll_backend()
578

  
550 579

  
551 580
class BasketItem(models.Model):
552 581
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
......
731 760
    status = models.IntegerField(null=True)
732 761
    amount = models.DecimalField(default=0, max_digits=7, decimal_places=2)
733 762

  
763
    RUNNING_STATUSES = [0, eopayment.WAITING, eopayment.RECEIVED]
764

  
734 765
    def is_remote(self):
735 766
        return self.remote_items != ''
736 767

  
......
743 774
        return self.status in (eopayment.PAID, eopayment.ACCEPTED)
744 775

  
745 776
    def is_running(self):
746
        return self.status in [0, eopayment.WAITING, eopayment.RECEIVED]
777
        return self.status in self.RUNNING_STATUSES
747 778

  
748 779
    def get_status_label(self):
749 780
        return status_label(self.status)
......
890 921
        if self.remote_items:
891 922
            self.first_notify_remote_items_of_payments()
892 923

  
924
    def can_poll_backend(self):
925
        return self.regie and self.regie.can_poll_backend()
926

  
927
    def poll_backend(self):
928
        response = self.regie.payment_backend.backend.payment_status(
929
            self.order_id, transaction_date=self.start_date
930
        )
931
        logger.debug(
932
            'lingo: regie "%s" polling backend for transaction "%%s(%%s)"' % self.regie,
933
            self.order_id,
934
            self.id,
935
        )
936

  
937
        if self.status != response.result:
938
            self.handle_backend_response(response)
939

  
893 940

  
894 941
class TransactionOperation(models.Model):
895 942
    OPERATIONS = [
tests/test_lingo_payment.py
1 1
import json
2
import types
2 3
import uuid
3 4
from contextlib import contextmanager
4 5
from datetime import datetime, timedelta
......
16 17
from django.urls import reverse
17 18
from django.utils import timezone
18 19
from django.utils.six.moves.urllib import parse as urlparse
19
from django.utils.timezone import utc
20
from django.utils.timezone import now, utc
20 21
from mellon.models import UserSAMLIdentifier
21 22
from requests.exceptions import ConnectionError
22 23
from requests.models import Response
......
57 58

  
58 59

  
59 60
@pytest.fixture
60
def regie():
61
    try:
62
        payment_backend = PaymentBackend.objects.get(slug='test1')
63
    except PaymentBackend.DoesNotExist:
64
        payment_backend = PaymentBackend.objects.create(
65
            label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}
66
        )
67
    try:
68
        regie = Regie.objects.get(slug='test')
69
    except Regie.DoesNotExist:
70
        regie = Regie()
71
        regie.label = 'Test'
72
        regie.slug = 'test'
73
        regie.description = 'test'
74
        regie.can_pay_only_one_basket_item = False
75
        regie.payment_min_amount = Decimal(4.5)
76
        regie.payment_backend = payment_backend
77
        regie.save()
61
def payment_backend():
62
    return PaymentBackend.objects.create(
63
        label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}
64
    )
65

  
66

  
67
@pytest.fixture
68
def regie(payment_backend):
69
    regie = Regie()
70
    regie.label = 'Test'
71
    regie.slug = 'test'
72
    regie.description = 'test'
73
    regie.can_pay_only_one_basket_item = False
74
    regie.payment_min_amount = Decimal(4.5)
75
    regie.payment_backend = payment_backend
76
    regie.save()
78 77
    return regie
79 78

  
80 79

  
81 80
@pytest.fixture
82
def remote_regie():
83
    try:
84
        payment_backend = PaymentBackend.objects.get(slug='test1')
85
    except PaymentBackend.DoesNotExist:
86
        payment_backend = PaymentBackend.objects.create(
87
            label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}
88
        )
89
    try:
90
        regie = Regie.objects.get(slug='remote')
91
    except Regie.DoesNotExist:
92
        regie = Regie(can_pay_only_one_basket_item=False)
93
        regie.label = 'Remote'
94
        regie.slug = 'remote'
95
        regie.description = 'remote'
96
        regie.payment_min_amount = Decimal(2.0)
97
        regie.payment_backend = payment_backend
98
        regie.webservice_url = 'http://example.org/regie'  # is_remote
99
        regie.save()
81
def remote_regie(payment_backend):
82
    regie = Regie()
83
    regie.label = 'Remote'
84
    regie.slug = 'remote'
85
    regie.description = 'remote'
86
    regie.payment_min_amount = Decimal(2.0)
87
    regie.payment_backend = payment_backend
88
    regie.webservice_url = 'http://example.org/regie'  # is_remote
89
    regie.save()
100 90
    return regie
101 91

  
102 92

  
......
2039 2029
        assert response.location.startswith('http://dummy-payment.demo.entrouvert.com/')
2040 2030
        qs = parse_qs(response.location)
2041 2031
        assert qs['email'] == 'user1@example.com'
2032

  
2033

  
2034
class TestPayfip:
2035
    @pytest.fixture
2036
    def payment_backend(self, payment_backend):
2037
        payment_backend.service = 'payfip_ws'
2038
        payment_backend.save()
2039
        return payment_backend
2040

  
2041
    def test_transaction_poll_backend(self, db, regie):
2042
        item = BasketItem.objects.create(amount=10, regie=regie)
2043

  
2044
        transaction = Transaction.objects.create(order_id='1234', status=0, amount=10, regie=regie)
2045
        transaction.items.set([item])
2046

  
2047
        with mock.patch('eopayment.Payment.payment_status') as payment_status:
2048
            payment_status.return_value = eopayment.common.PaymentResponse(
2049
                order_id='1234',
2050
                result=eopayment.PAID,
2051
                transaction_date=now(),
2052
                transaction_id='4567',
2053
                bank_data={'abcd': 'xyz'},
2054
                signed=True,
2055
            )
2056
            transaction.poll_backend()
2057
        payment_status.assert_called_once_with('1234', transaction_date=transaction.start_date)
2058

  
2059
        transaction.refresh_from_db()
2060
        assert transaction.status == eopayment.PAID
2061
        assert transaction.bank_transaction_date is not None
2062
        assert transaction.bank_data == {'abcd': 'xyz'}
2063

  
2064
    def test_payment_backend_poll_backend(self, db, payment_backend, regie, freezer):
2065
        item = BasketItem.objects.create(amount=10, regie=regie)
2066

  
2067
        transaction = Transaction.objects.create(order_id='1234', status=0, amount=10, regie=regie)
2068
        transaction.items.set([item])
2069

  
2070
        # move one hour in the future
2071
        freezer.move_to(timedelta(hours=1))
2072

  
2073
        with mock.patch(
2074
            'combo.apps.lingo.models.Transaction.poll_backend', autospec=True
2075
        ) as transaction_poll_backend:
2076
            payment_backend.poll_backend()
2077
            transaction_poll_backend.assert_called_once_with(transaction)
2078

  
2079
    def test_command_poll_backend(self, db, payment_backend, regie, freezer):
2080
        item = BasketItem.objects.create(amount=10, regie=regie)
2081

  
2082
        transaction = Transaction.objects.create(order_id='1234', status=0, amount=10, regie=regie)
2083
        transaction.items.set([item])
2084

  
2085
        # move one hour in the future
2086
        freezer.move_to(timedelta(hours=1))
2087

  
2088
        with mock.patch(
2089
            'combo.apps.lingo.models.Transaction.poll_backend', autospec=True
2090
        ) as transaction_poll_backend:
2091
            payment_backend.poll_backend()
2092
            transaction_poll_backend.assert_called_once_with(transaction)
2042
-