0002-lingo-add-poll_backend-method-to-PaymentBackend-and-.patch
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 |
- |