0003-lingo-add-poll_backend-method-to-PaymentBackend-and-.patch
combo/apps/lingo/forms.py | ||
---|---|---|
100 | 100 |
def __init__(self, *args, **kwargs): |
101 | 101 |
super(RegieForm, self).__init__(*args, **kwargs) |
102 | 102 |
fields, initial = create_form_fields( |
103 |
self.instance.payment_backend.get_payment().get_parameters(scope='transaction'),
|
|
103 |
self.instance.eopayment.get_parameters(scope='transaction'),
|
|
104 | 104 |
self.instance.transaction_options, |
105 | 105 |
) |
106 | 106 |
self.fields.update(fields) |
... | ... | |
109 | 109 |
def save(self): |
110 | 110 |
instance = super(RegieForm, self).save() |
111 | 111 |
instance.transaction_options = compute_json_field( |
112 |
self.instance.payment_backend.get_payment().get_parameters(scope='transaction'), self.cleaned_data
|
|
112 |
self.instance.eopayment.get_parameters(scope='transaction'), self.cleaned_data
|
|
113 | 113 |
) |
114 | 114 |
instance.save() |
115 | 115 |
return instance |
... | ... | |
123 | 123 |
def __init__(self, *args, **kwargs): |
124 | 124 |
super(PaymentBackendForm, self).__init__(*args, **kwargs) |
125 | 125 |
fields, initial = create_form_fields( |
126 |
self.instance.get_payment().get_parameters(scope='global'), self.instance.service_options
|
|
126 |
self.instance.eopayment.get_parameters(scope='global'), self.instance.service_options
|
|
127 | 127 |
) |
128 | 128 |
self.fields.update(fields) |
129 | 129 |
self.initial.update(initial) |
... | ... | |
133 | 133 |
def save(self): |
134 | 134 |
instance = super(PaymentBackendForm, self).save() |
135 | 135 |
instance.service_options = compute_json_field( |
136 |
self.instance.get_payment().get_parameters(scope='global'), self.cleaned_data
|
|
136 |
self.instance.eopayment.get_parameters(scope='global'), self.cleaned_data
|
|
137 | 137 |
) |
138 | 138 |
instance.save() |
139 | 139 |
return instance |
combo/apps/lingo/management/commands/lingo-poll-backend.py | ||
---|---|---|
1 |
# lingo - basket and payment system |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import datetime |
|
18 |
import logging |
|
19 | ||
20 |
from django.core.management.base import BaseCommand, CommandError |
|
21 | ||
22 |
from combo.apps.lingo.models import PaymentBackend, PaymentException |
|
23 | ||
24 |
logger = logging.getLogger('combo.apps.lingo') |
|
25 | ||
26 | ||
27 |
class Command(BaseCommand): |
|
28 |
def add_arguments(self, parser): |
|
29 |
parser.add_argument('--backend', default=None, help='slug of the backend') |
|
30 | ||
31 |
parser.add_argument('--all-backends', default=False, action='store_true', help='target all backends') |
|
32 | ||
33 |
parser.add_argument( |
|
34 |
'--noinput', |
|
35 |
'--no-input', |
|
36 |
action='store_false', |
|
37 |
dest='interactive', |
|
38 |
help='Tells Django to NOT prompt the user for input of any kind.', |
|
39 |
) |
|
40 | ||
41 |
parser.add_argument( |
|
42 |
'--max-age-in-days', default=3, type=int, help='max age of the transaction in days' |
|
43 |
) |
|
44 | ||
45 |
def handle( |
|
46 |
self, |
|
47 |
*args, |
|
48 |
backend=None, |
|
49 |
all_backends=False, |
|
50 |
max_age_in_days=None, |
|
51 |
interactive=True, |
|
52 |
verbosity=1, |
|
53 |
**options, |
|
54 |
): |
|
55 |
qs = PaymentBackend.objects.all() |
|
56 |
if backend and all_backends: |
|
57 |
raise CommandError('--backend and --all-baskends cannot be used together') |
|
58 |
elif backend: |
|
59 |
try: |
|
60 |
backend = qs.get(slug=backend) |
|
61 |
except PaymentBackend.DoesNotExist: |
|
62 |
raise CommandError('no backend with slug "%s".' % backend) |
|
63 |
else: |
|
64 |
if not backend.can_poll_backend(): |
|
65 |
raise CommandError('backend "%s" cannot be polled.' % backend) |
|
66 |
backends = [backend] |
|
67 |
else: |
|
68 |
backends = [backend for backend in qs if backend.can_poll_backend()] |
|
69 |
if not backends: |
|
70 |
raise CommandError('no backend found.') |
|
71 |
if not all_backends and interactive: |
|
72 |
print('Choose backend by slug:') |
|
73 |
while True: |
|
74 |
for backend in backends: |
|
75 |
print(' - %s: %s' % (backend.slug, backend)) |
|
76 |
print('> ', end=' ') |
|
77 |
slug = input().strip() |
|
78 |
if not slug: |
|
79 |
continue |
|
80 |
filtered_backends = qs.filter(slug__icontains=slug) |
|
81 |
if filtered_backends: |
|
82 |
backends = filtered_backends |
|
83 |
break |
|
84 | ||
85 |
for backend in backends: |
|
86 |
if verbosity >= 1: |
|
87 |
print('Polling backend', backend, '... ', end='') |
|
88 |
try: |
|
89 |
backend.poll_backend(max_age=max_age_in_days and datetime.timedelta(days=max_age_in_days)) |
|
90 |
except PaymentException: |
|
91 |
logger.exception('polling failed') |
|
92 |
if interactive: |
|
93 |
# show error |
|
94 |
raise |
|
95 |
if verbosity >= 1: |
|
96 |
print('DONE') |
combo/apps/lingo/models.py | ||
---|---|---|
27 | 27 |
from dateutil import parser |
28 | 28 |
from django import template |
29 | 29 |
from django.conf import settings |
30 |
from django.contrib.auth.models import User |
|
31 | 30 |
from django.core import serializers |
32 |
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
|
|
31 |
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied |
|
33 | 32 |
from django.core.mail import EmailMultiAlternatives |
34 | 33 |
from django.db import models |
34 |
from django.db.transaction import atomic |
|
35 | 35 |
from django.forms import Select |
36 | 36 |
from django.forms import models as model_forms |
37 | 37 |
from django.template.loader import render_to_string |
... | ... | |
39 | 39 |
from django.utils import dateparse, six, timezone |
40 | 40 |
from django.utils.encoding import force_bytes, python_2_unicode_compatible |
41 | 41 |
from django.utils.formats import localize |
42 |
from django.utils.http import urlencode |
|
43 | 42 |
from django.utils.six.moves.urllib import parse as urlparse |
44 |
from django.utils.timezone import make_aware, utc |
|
43 |
from django.utils.timezone import make_aware, now, utc
|
|
45 | 44 |
from django.utils.translation import ugettext_lazy as _ |
46 | 45 |
from jsonfield import JSONField |
47 | 46 |
from requests import RequestException |
... | ... | |
156 | 155 |
def __str__(self): |
157 | 156 |
return self.label |
158 | 157 | |
159 |
def get_payment(self): |
|
158 |
@property |
|
159 |
def eopayment(self): |
|
160 |
return self.make_eopayment() |
|
161 | ||
162 |
def make_eopayment(self, *, request=None, automatic_return_url=None, normal_return_url=None, **kwargs): |
|
160 | 163 |
options = self.service_options or {} |
161 | 164 |
if isinstance(options, six.string_types): |
162 | 165 |
# backward compatibility when used againt postgresql < 9.4 and |
... | ... | |
167 | 170 |
pass |
168 | 171 |
if not isinstance(options, dict): |
169 | 172 |
options = {} |
173 |
if request: |
|
174 |
if not automatic_return_url: |
|
175 |
automatic_return_url = reverse( |
|
176 |
'lingo-callback-payment-backend', kwargs={'payment_backend_pk': self.id} |
|
177 |
) |
|
178 | ||
179 |
if automatic_return_url: |
|
180 |
automatic_return_url = request.build_absolute_uri(automatic_return_url) |
|
181 |
if normal_return_url: |
|
182 |
normal_return_url = request.build_absolute_uri(normal_return_url) |
|
183 |
options['automatic_return_url'] = automatic_return_url |
|
184 |
options['normal_return_url'] = normal_return_url |
|
185 |
else: |
|
186 |
assert ( |
|
187 |
not automatic_return_url and not normal_return_url |
|
188 |
), 'make_eopayment must be used with a request to set automatic_return_url or normal_return_url' |
|
189 |
options.update(**kwargs) |
|
170 | 190 |
return eopayment.Payment(self.service, options) |
171 | 191 | |
172 | 192 |
def natural_key(self): |
... | ... | |
231 | 251 |
transaction.handle_backend_response(response, callback=callback) |
232 | 252 |
return transaction |
233 | 253 | |
254 |
def can_poll_backend(self): |
|
255 |
return self.eopayment.has_payment_status |
|
256 | ||
257 |
def poll_backend(self, min_age=None, max_age=None): |
|
258 |
if not self.can_poll_backend(): |
|
259 |
return |
|
260 |
current_time = now() |
|
261 |
# poll transactions linked to the current backend |
|
262 |
# aged between 5 minutes and 3 hours, max_age can be overriden |
|
263 |
min_age = min_age or datetime.timedelta(minutes=5) |
|
264 |
not_after = current_time - min_age |
|
265 |
max_age = max_age or datetime.timedelta(hours=3) |
|
266 |
not_before = current_time - max_age |
|
267 |
transactions = Transaction.objects.filter( |
|
268 |
regie__payment_backend=self, |
|
269 |
start_date__lt=not_after, |
|
270 |
start_date__gt=not_before, |
|
271 |
status__in=Transaction.RUNNING_STATUSES, |
|
272 |
).order_by('pk') |
|
273 |
last_pk = -1 |
|
274 |
while True: |
|
275 |
# lock each transaction before trying to poll it |
|
276 |
with atomic(): |
|
277 |
transaction = transactions.filter(pk__gt=last_pk).select_for_update(skip_locked=True).first() |
|
278 |
if not transaction: |
|
279 |
break |
|
280 |
last_pk = transaction.pk |
|
281 |
transaction.poll_backend(ignore_errors=False) |
|
282 | ||
234 | 283 | |
235 | 284 |
@python_2_unicode_compatible |
236 | 285 |
class Regie(models.Model): |
... | ... | |
547 | 596 |
regie = next(serializers.deserialize('json', json.dumps([json_regie]), ignorenonexistent=True)) |
548 | 597 |
regie.save() |
549 | 598 | |
599 |
def can_poll_backend(self): |
|
600 |
return self.payment_backend.can_poll_backend() |
|
601 | ||
602 |
@property |
|
603 |
def eopayment(self): |
|
604 |
return self.make_eopayment() |
|
605 | ||
606 |
def make_eopayment(self, **kwargs): |
|
607 |
return self.payment_backend.make_eopayment(**kwargs) |
|
608 | ||
550 | 609 | |
551 | 610 |
class BasketItem(models.Model): |
552 | 611 |
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True) |
... | ... | |
572 | 631 | |
573 | 632 |
@classmethod |
574 | 633 |
def get_items_to_be_paid(cls, user): |
575 |
return cls.objects.filter(
|
|
634 |
qs = cls.objects.filter(
|
|
576 | 635 |
user=user, payment_date__isnull=True, waiting_date__isnull=True, cancellation_date__isnull=True |
577 | 636 |
) |
637 |
for transaction in Transaction.objects.filter(items__in=qs): |
|
638 |
if transaction.can_poll_backend(): |
|
639 |
transaction.poll_backend() |
|
640 |
return qs |
|
578 | 641 | |
579 | 642 |
def notify(self, status): |
580 | 643 |
if not self.source_url: |
... | ... | |
656 | 719 |
self.reference_id = reference_id |
657 | 720 |
if payment_date: |
658 | 721 |
self.payment_date = parser.parse(payment_date) |
722 |
self.waiting_date = None |
|
659 | 723 | |
660 | 724 |
@property |
661 | 725 |
def no_online_payment_reason_details(self): |
... | ... | |
673 | 737 |
return aes_hex_encrypt(settings.SECRET_KEY, force_bytes(str(self.id))) |
674 | 738 | |
675 | 739 |
@classmethod |
676 |
def update_paid(cls, regie, remote_items):
|
|
677 |
remote_item_ids = [remote_item.id for remote_item in remote_items if not remote_item.paid]
|
|
740 |
def transactions_for_remote_items(cls, queryset, remote_items):
|
|
741 |
remote_item_ids = set(remote_item.id for remote_item in remote_items if not remote_item.paid)
|
|
678 | 742 |
if not remote_item_ids: |
679 |
return |
|
743 |
return Transaction.objects.none()
|
|
680 | 744 | |
681 |
paid_items = {} |
|
682 | 745 |
# filter transactions by regie, status and contained remote_item id |
683 |
transaction_qs = Transaction.objects.filter( |
|
684 |
regie=regie, status__in=[eopayment.PAID, eopayment.ACCEPTED] |
|
685 |
) |
|
686 | 746 |
query = reduce( |
687 | 747 |
models.Q.__or__, |
688 | 748 |
(models.Q(remote_items__contains=remote_item_id) for remote_item_id in remote_item_ids), |
689 | 749 |
) |
690 | 750 | |
691 | 751 |
# accumulate in paid_items each remote_item earliest payment_date |
692 |
for transaction in transaction_qs.filter(query): |
|
752 |
for transaction in queryset.filter(query): |
|
753 |
for remote_item_id in transaction.remote_items.split(','): |
|
754 |
if remote_item_id in remote_item_ids: |
|
755 |
yield transaction |
|
756 |
break |
|
757 | ||
758 |
@classmethod |
|
759 |
def update_paid(cls, regie, remote_items): |
|
760 |
paid_items = {} |
|
761 |
waiting_items = {} |
|
762 |
transaction_qs = Transaction.objects.filter(regie=regie) |
|
763 | ||
764 |
can_poll_backend = regie.can_poll_backend() |
|
765 | ||
766 |
# accumulate in paid_items each remote_item earliest payment_date |
|
767 |
for transaction in cls.transactions_for_remote_items(transaction_qs, remote_items): |
|
768 |
if transaction.is_running() and can_poll_backend: |
|
769 |
transaction.poll_backend() |
|
693 | 770 |
for remote_item in transaction.remote_items.split(','): |
694 |
if remote_item not in paid_items: |
|
695 |
paid_items[remote_item] = transaction.end_date |
|
696 |
else: |
|
697 |
paid_items[remote_item] = min(transaction.end_date, paid_items[remote_item]) |
|
771 |
if transaction.end_date and transaction.is_paid(): |
|
772 |
if remote_item not in paid_items: |
|
773 |
paid_items[remote_item] = transaction.end_date |
|
774 |
else: |
|
775 |
paid_items[remote_item] = min(transaction.end_date, paid_items[remote_item]) |
|
776 |
elif transaction.status == eopayment.WAITING and can_poll_backend: |
|
777 |
waiting_items[remote_item] = transaction.start_date |
|
698 | 778 | |
699 | 779 |
# update remote_item.paid using paid_items |
700 | 780 |
for remote_item in remote_items: |
... | ... | |
703 | 783 |
if remote_item.id in paid_items: |
704 | 784 |
remote_item.paid = True |
705 | 785 |
remote_item.payment_date = paid_items[remote_item.id] |
786 |
elif remote_item.id in waiting_items: |
|
787 |
remote_item.waiting_date = waiting_items[remote_item.id] |
|
706 | 788 | |
707 | 789 | |
708 | 790 |
def status_label(status): |
... | ... | |
731 | 813 |
status = models.IntegerField(null=True) |
732 | 814 |
amount = models.DecimalField(default=0, max_digits=7, decimal_places=2) |
733 | 815 | |
816 |
RUNNING_STATUSES = [0, eopayment.WAITING, eopayment.RECEIVED] |
|
817 |
PAID_STATUSES = [eopayment.PAID, eopayment.ACCEPTED] |
|
818 | ||
734 | 819 |
def is_remote(self): |
735 | 820 |
return self.remote_items != '' |
736 | 821 | |
... | ... | |
740 | 825 |
return _('Anonymous User') |
741 | 826 | |
742 | 827 |
def is_paid(self): |
743 |
return self.status in (eopayment.PAID, eopayment.ACCEPTED)
|
|
828 |
return self.status in self.PAID_STATUSES
|
|
744 | 829 | |
745 | 830 |
def is_running(self): |
746 |
return self.status in [0, eopayment.WAITING, eopayment.RECEIVED]
|
|
831 |
return self.status in self.RUNNING_STATUSES
|
|
747 | 832 | |
748 | 833 |
def get_status_label(self): |
749 | 834 |
return status_label(self.status) |
... | ... | |
890 | 975 |
if self.remote_items: |
891 | 976 |
self.first_notify_remote_items_of_payments() |
892 | 977 | |
978 |
@property |
|
979 |
def eopayment(self): |
|
980 |
return self.regie.eopayment |
|
981 | ||
982 |
def make_eopayment(self, **kwargs): |
|
983 |
normal_return_url = reverse( |
|
984 |
'lingo-return-payment-backend', |
|
985 |
kwargs={ |
|
986 |
'payment_backend_pk': self.regie.payment_backend.id, |
|
987 |
'transaction_signature': signing_dumps(self.pk), |
|
988 |
}, |
|
989 |
) |
|
990 |
return self.regie.make_eopayment(normal_return_url=normal_return_url, **kwargs) |
|
991 | ||
992 |
def can_poll_backend(self): |
|
993 |
return self.regie and self.regie.can_poll_backend() |
|
994 | ||
995 |
def poll_backend(self, ignore_errors=True): |
|
996 |
with atomic(): |
|
997 |
# lock the transaction |
|
998 |
Transaction.objects.filter(pk=self.pk).select_for_update().first() |
|
999 |
try: |
|
1000 |
response = self.eopayment.payment_status(self.order_id, transaction_date=self.start_date) |
|
1001 |
except eopayment.PaymentException: |
|
1002 |
if ignore_errors: |
|
1003 |
logger.warning( |
|
1004 |
'lingo: regie "%s" polling backend for transaction "%%s(%%s)" failed' % self.regie, |
|
1005 |
self.order_id, |
|
1006 |
self.id, |
|
1007 |
exc_info=True, |
|
1008 |
) |
|
1009 |
return |
|
1010 |
raise PaymentException('polling failed') |
|
1011 | ||
1012 |
logger.debug( |
|
1013 |
'lingo: regie "%s" polling backend for transaction "%%s(%%s)"' % self.regie, |
|
1014 |
self.order_id, |
|
1015 |
self.id, |
|
1016 |
) |
|
1017 | ||
1018 |
if self.status != response.result: |
|
1019 |
self.handle_backend_response(response) |
|
1020 | ||
893 | 1021 | |
894 | 1022 |
class TransactionOperation(models.Model): |
895 | 1023 |
OPERATIONS = [ |
... | ... | |
968 | 1096 |
# list transactions : |
969 | 1097 |
# * paid by the user |
970 | 1098 |
# * or linked to a BasketItem of the user |
971 |
return Transaction.objects.filter(models.Q(user=user) | models.Q(items__user=user)).filter(
|
|
1099 |
qs = Transaction.objects.filter(models.Q(user=user) | models.Q(items__user=user)).filter(
|
|
972 | 1100 |
start_date__gte=timezone.now() - datetime.timedelta(days=7) |
973 | 1101 |
) |
1102 |
for transaction in qs: |
|
1103 |
if transaction.can_poll_backend() and transaction.is_running(): |
|
1104 |
transaction.poll_backend() |
|
1105 |
return qs |
|
974 | 1106 | |
975 | 1107 |
def is_relevant(self, context): |
976 | 1108 |
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated): |
combo/apps/lingo/templates/lingo/combo/item.html | ||
---|---|---|
48 | 48 |
{% if item.no_online_payment_reason_details %} |
49 | 49 |
<div class="no-online-payment-reason"><span>{{ item.no_online_payment_reason_details }}</span></div> |
50 | 50 |
{% endif %} |
51 |
{% if not item.paid and item.online_payment and item.amount >= regie.payment_min_amount %} |
|
51 |
{% if item.waiting_date and not item.paid %} |
|
52 |
<div class="paid paid-info">{% trans "Waiting for payment." %}</div> |
|
53 |
{% endif %} |
|
54 |
{% if not item.paid and item.online_payment and item.amount >= regie.payment_min_amount and not item.waiting_date %} |
|
52 | 55 |
{% csrf_token %} |
53 | 56 |
{% if not user.is_authenticated %} |
54 | 57 |
<div class="email"> |
combo/apps/lingo/templates/lingo/combo/items.html | ||
---|---|---|
51 | 51 |
{% if item.regie.is_remote %} |
52 | 52 |
<td> |
53 | 53 |
<a href="{% url 'view-item' regie_id=item.regie.pk item_crypto_id=item.crypto_id %}?page={{ cell.page.pk }}" rel="popup" class="icon-view">{% trans "View" %} |
54 |
{% if item.online_payment and item.amount >= item.regie.payment_min_amount %}{% trans "and pay" %}{% endif %}
|
|
54 |
{% if item.online_payment and item.amount >= item.regie.payment_min_amount and not item.waiting_date %}{% trans "and pay" %}{% endif %}
|
|
55 | 55 |
</a> |
56 | 56 |
{% if item.has_pdf %} |
57 | 57 |
<br/><a href="{% url 'download-item-pdf' regie_id=item.regie.pk item_crypto_id=item.crypto_id %}" class="icon-pdf" |
combo/apps/lingo/views.py | ||
---|---|---|
58 | 58 |
PaymentException, |
59 | 59 |
Regie, |
60 | 60 |
RemoteInvoiceException, |
61 |
RemoteItem, |
|
61 | 62 |
SelfDeclaredInvoicePayment, |
62 | 63 |
Transaction, |
63 | 64 |
TransactionOperation, |
... | ... | |
79 | 80 |
status_code = 400 |
80 | 81 | |
81 | 82 | |
82 |
def get_eopayment_object(request, regie_or_payment_backend, transaction_id=None): |
|
83 |
payment_backend = regie_or_payment_backend |
|
84 |
if isinstance(regie_or_payment_backend, Regie): |
|
85 |
payment_backend = regie_or_payment_backend.payment_backend |
|
86 |
options = payment_backend.service_options |
|
87 |
options.update( |
|
88 |
{ |
|
89 |
'automatic_return_url': request.build_absolute_uri( |
|
90 |
reverse('lingo-callback-payment-backend', kwargs={'payment_backend_pk': payment_backend.id}) |
|
91 |
), |
|
92 |
} |
|
93 |
) |
|
94 | ||
95 |
if transaction_id: |
|
96 |
options['normal_return_url'] = request.build_absolute_uri( |
|
97 |
reverse( |
|
98 |
'lingo-return-payment-backend', |
|
99 |
kwargs={ |
|
100 |
'payment_backend_pk': payment_backend.id, |
|
101 |
'transaction_signature': signing_dumps(transaction_id), |
|
102 |
}, |
|
103 |
) |
|
104 |
) |
|
105 |
return eopayment.Payment(payment_backend.service, options) |
|
106 | ||
107 | ||
108 | 83 |
def get_basket_url(): |
109 | 84 |
basket_cell = LingoBasketCell.objects.filter(page__snapshot__isnull=True).first() |
110 | 85 |
if basket_cell: |
... | ... | |
323 | 298 |
) |
324 | 299 |
raise Http404 |
325 | 300 | |
326 |
payment = get_eopayment_object(request, transaction.regie) |
|
327 | 301 |
amount = LocaleDecimal(request.GET['amount']) |
328 | 302 | |
329 | 303 |
logger.info(u'validating amount %s for transaction %s', amount, smart_text(transaction.id)) |
330 | 304 |
try: |
331 |
result = payment.backend.validate(amount, transaction.bank_data) |
|
305 |
result = transaction.make_eopayment(request=request).backend.validate( |
|
306 |
amount, transaction.bank_data |
|
307 |
) |
|
332 | 308 |
except eopayment.ResponseError as e: |
333 | 309 |
logger.error(u'failed in validation operation: %s', e) |
334 | 310 |
return JsonResponse({'err': 1, 'e': force_text(e)}) |
... | ... | |
361 | 337 |
) |
362 | 338 |
raise Http404 |
363 | 339 | |
364 |
payment = get_eopayment_object(request, transaction.regie) |
|
365 | 340 |
amount = LocaleDecimal(request.GET['amount']) |
366 | 341 | |
367 | 342 |
logger.info(u'cancelling amount %s for transaction %s', amount, smart_text(transaction.id)) |
368 | 343 |
try: |
369 |
result = payment.backend.cancel(amount, transaction.bank_data)
|
|
344 |
result = transaction.make_eopayment(request=request).backend.cancel(amount, transaction.bank_data)
|
|
370 | 345 |
except eopayment.ResponseError as e: |
371 | 346 |
logger.error(u'failed in cancel operation: %s', e) |
372 | 347 |
return JsonResponse({'err': 1, 'e': force_text(e)}) |
... | ... | |
389 | 364 |
if bool(len(items)) == bool(len(remote_items)): |
390 | 365 |
messages.error(request, _('Items to pay are missing or are not of the same type (local/remote).')) |
391 | 366 |
return HttpResponseRedirect(next_url) |
367 | ||
368 |
if ( |
|
369 |
regie.payment_backend.can_poll_backend() |
|
370 |
and self.poll_for_newly_paid_or_still_running_transactions(regie, items, remote_items) |
|
371 |
): |
|
372 |
messages.error(request, _('Some items are already paid or are being paid.')) |
|
373 |
return HttpResponseRedirect(next_url) |
|
374 | ||
392 | 375 |
if regie.can_pay_only_one_basket_item and (len(items) > 1 or len(remote_items) > 1): |
393 | 376 |
messages.error(request, _('This regie allows to pay only one item.')) |
394 | 377 |
return HttpResponseRedirect(next_url) |
... | ... | |
425 | 408 |
transaction.status = 0 |
426 | 409 |
transaction.amount = total_amount |
427 | 410 | |
428 |
payment = get_eopayment_object(request, regie, transaction.pk) |
|
429 | 411 |
kwargs = {'email': email, 'first_name': firstname, 'last_name': lastname} |
430 | 412 |
kwargs['merchant_name'] = settings.TEMPLATE_VARS.get('global_title') or 'Compte Citoyen' |
431 | 413 |
kwargs['items_info'] = [] |
... | ... | |
463 | 445 |
if regie.transaction_options: |
464 | 446 |
kwargs.update(regie.transaction_options) |
465 | 447 |
try: |
466 |
(order_id, kind, data) = payment.request(total_amount, **kwargs) |
|
448 |
(order_id, kind, data) = transaction.make_eopayment(request=request).request( |
|
449 |
total_amount, **kwargs |
|
450 |
) |
|
467 | 451 |
except eopayment.PaymentException as e: |
468 | 452 |
logger.error('failed to initiate payment request: %s', e) |
469 | 453 |
messages.error(request, _('Failed to initiate payment request')) |
... | ... | |
491 | 475 | |
492 | 476 |
raise NotImplementedError() |
493 | 477 | |
478 |
def poll_for_newly_paid_or_still_running_transactions(self, regie, items, remote_items): |
|
479 |
'''Verify if any open transaction is not already paid.''' |
|
480 |
qs = Transaction.objects.filter(regie=regie, status__in=Transaction.RUNNING_STATUSES) |
|
481 |
if items: |
|
482 |
transactions = qs.filter(items__in=items) |
|
483 |
else: |
|
484 |
transactions = RemoteItem.transactions_for_remote_items(qs, remote_items) |
|
485 | ||
486 |
newly_paid_or_still_running = False |
|
487 |
for transaction in transactions: |
|
488 |
transaction.poll_backend() |
|
489 |
newly_paid_or_still_running |= transaction.is_paid() or transaction.is_running() |
|
490 |
return newly_paid_or_still_running |
|
491 | ||
494 | 492 | |
495 | 493 |
class PayView(PayMixin, View): |
496 | 494 |
def post(self, request, *args, **kwargs): |
... | ... | |
619 | 617 |
else: |
620 | 618 |
return HttpResponseBadRequest("A payment backend or regie primary key must be specified") |
621 | 619 | |
622 |
payment = get_eopayment_object(request, payment_backend)
|
|
620 |
payment = payment_backend.make_eopayment(request=request)
|
|
623 | 621 |
logger.info(u'received payment response: %r', backend_response) |
624 | 622 |
try: |
625 | 623 |
eopayment_response_kwargs = {'redirect': not callback} |
debian/combo.cron.d | ||
---|---|---|
1 | 1 |
MAILTO=root |
2 | 2 | |
3 | 3 |
0 8 * * * combo /usr/bin/combo-manage tenant_command notify_new_remote_invoices --all-tenants -v0 |
4 |
*/10 * * * * combo /usr/bin/combo-manage tenant_command lingo-poll-backends --all-tenants -v0 --no-input |
tests/test_lingo_manager.py | ||
---|---|---|
675 | 675 | |
676 | 676 | |
677 | 677 |
def test_use_old_service_options_safely(app, admin_user): |
678 |
PaymentBackend(service='dummy', service_options='xx').get_payment() |
|
679 |
PaymentBackend(service='dummy', service_options='"xx"').get_payment() |
|
680 |
PaymentBackend(service='dummy', service_options=None).get_payment() |
|
678 |
PaymentBackend(service='dummy', service_options='xx').eopayment |
|
679 |
PaymentBackend(service='dummy', service_options='"xx"').eopayment |
|
680 |
PaymentBackend(service='dummy', service_options=None).eopayment |
tests/test_lingo_payment.py | ||
---|---|---|
11 | 11 |
from django.apps import apps |
12 | 12 |
from django.conf import settings |
13 | 13 |
from django.contrib.auth.models import User |
14 |
from django.core.management import CommandError, call_command |
|
15 |
from django.db import transaction |
|
14 | 16 |
from django.http.request import QueryDict |
15 | 17 |
from django.test import override_settings |
16 | 18 |
from django.urls import reverse |
17 | 19 |
from django.utils import timezone |
18 | 20 |
from django.utils.six.moves.urllib import parse as urlparse |
19 |
from django.utils.timezone import utc |
|
21 |
from django.utils.timezone import now, utc
|
|
20 | 22 |
from mellon.models import UserSAMLIdentifier |
21 | 23 |
from requests.exceptions import ConnectionError |
22 |
from requests.models import Response |
|
23 | 24 | |
24 | 25 |
from combo.apps.lingo.models import ( |
25 | 26 |
EXPIRED, |
26 | 27 |
BasketItem, |
27 | 28 |
LingoBasketCell, |
29 |
LingoRecentTransactionsCell, |
|
28 | 30 |
PaymentBackend, |
31 |
PaymentException, |
|
29 | 32 |
Regie, |
30 | 33 |
Transaction, |
31 | 34 |
TransactionOperation, |
... | ... | |
57 | 60 | |
58 | 61 | |
59 | 62 |
@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() |
|
63 |
def payment_backend(): |
|
64 |
return PaymentBackend.objects.create( |
|
65 |
label='test1', slug='test1', service='dummy', service_options={'siret': '1234'} |
|
66 |
) |
|
67 | ||
68 | ||
69 |
@pytest.fixture |
|
70 |
def regie(payment_backend): |
|
71 |
regie = Regie() |
|
72 |
regie.label = 'Test' |
|
73 |
regie.slug = 'test' |
|
74 |
regie.description = 'test' |
|
75 |
regie.can_pay_only_one_basket_item = False |
|
76 |
regie.payment_min_amount = Decimal(4.5) |
|
77 |
regie.payment_backend = payment_backend |
|
78 |
regie.save() |
|
78 | 79 |
return regie |
79 | 80 | |
80 | 81 | |
81 | 82 |
@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() |
|
83 |
def remote_regie(payment_backend): |
|
84 |
regie = Regie() |
|
85 |
regie.label = 'Remote' |
|
86 |
regie.slug = 'remote' |
|
87 |
regie.description = 'remote' |
|
88 |
regie.payment_min_amount = Decimal(2.0) |
|
89 |
regie.payment_backend = payment_backend |
|
90 |
regie.webservice_url = 'http://example.org/regie' # is_remote |
|
91 |
regie.save() |
|
100 | 92 |
return regie |
101 | 93 | |
102 | 94 | |
... | ... | |
217 | 209 |
) |
218 | 210 |
BasketItem.objects.create(user=user, regie=regie, subject='item1', amount='1.5', source_url='/item/1') |
219 | 211 | |
220 |
class MockPayment(object): |
|
221 |
request = mock.Mock(return_value=(9876, 3, {})) |
|
222 | ||
223 |
def get_eopayment_object(*args, **kwargs): |
|
224 |
return MockPayment |
|
225 | ||
226 |
import combo.apps.lingo.views |
|
227 | ||
228 |
monkeypatch.setattr(combo.apps.lingo.views, 'get_eopayment_object', get_eopayment_object) |
|
229 | ||
230 |
resp = login(app).get('/test_basket_cell/') |
|
231 |
resp = resp.form.submit() |
|
232 |
assert MockPayment.request.call_args[1]['manual_validation'] is True |
|
212 |
with mock.patch('eopayment.Payment') as MockPayment: |
|
213 |
MockPayment.return_value.request.return_value = (9876, 3, {}) |
|
214 |
resp = login(app).get('/test_basket_cell/') |
|
215 |
resp = resp.form.submit() |
|
216 |
assert MockPayment.return_value.request.call_args[1]['manual_validation'] is True |
|
233 | 217 | |
234 | 218 | |
235 | 219 |
@pytest.mark.parametrize('with_payment_backend', [False, True]) |
... | ... | |
2039 | 2023 |
assert response.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
2040 | 2024 |
qs = parse_qs(response.location) |
2041 | 2025 |
assert qs['email'] == 'user1@example.com' |
2026 | ||
2027 | ||
2028 |
class TestPolling: |
|
2029 |
@pytest.fixture |
|
2030 |
def payment_backend(self, payment_backend): |
|
2031 |
with mock.patch( |
|
2032 |
'eopayment.payfip_ws.Payment.request', return_value=(1, eopayment.URL, 'https://payfip/') |
|
2033 |
): |
|
2034 |
payment_backend.service = 'payfip_ws' |
|
2035 |
payment_backend.save() |
|
2036 |
yield payment_backend |
|
2037 | ||
2038 |
class TestPollBackendCommand: |
|
2039 |
@pytest.fixture(autouse=True) |
|
2040 |
def setup(self, regie, payment_backend): |
|
2041 |
item = BasketItem.objects.create(amount=10, regie=regie) |
|
2042 | ||
2043 |
transaction = Transaction.objects.create(order_id='1234', status=0, amount=10, regie=regie) |
|
2044 |
transaction.items.set([item]) |
|
2045 | ||
2046 |
@pytest.fixture |
|
2047 |
def payment_status(self): |
|
2048 |
with mock.patch('eopayment.Payment.payment_status') as payment_status: |
|
2049 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
2050 |
order_id='1234', |
|
2051 |
result=eopayment.PAID, |
|
2052 |
transaction_date=now(), |
|
2053 |
transaction_id='4567', |
|
2054 |
bank_data={'abcd': 'xyz'}, |
|
2055 |
signed=True, |
|
2056 |
) |
|
2057 |
yield payment_status |
|
2058 | ||
2059 |
@pytest.mark.parametrize('cmd_options', [{'all_backends': True}, {'backend': 'test1'}]) |
|
2060 |
def test_ok(self, payment_status, freezer, cmd_options): |
|
2061 |
# transactions are polled after 5 minutes. |
|
2062 |
freezer.move_to(timedelta(minutes=4)) |
|
2063 |
call_command('lingo-poll-backend', **cmd_options) |
|
2064 |
assert payment_status.call_count == 0 |
|
2065 |
freezer.move_to(timedelta(minutes=1, seconds=1)) |
|
2066 |
call_command('lingo-poll-backend', **cmd_options) |
|
2067 | ||
2068 |
transaction = Transaction.objects.get() |
|
2069 |
payment_status.assert_called_once_with('1234', transaction_date=transaction.start_date) |
|
2070 | ||
2071 |
transaction.refresh_from_db() |
|
2072 |
assert transaction.status == eopayment.PAID |
|
2073 |
assert transaction.bank_transaction_date is not None |
|
2074 |
assert transaction.bank_data == {'abcd': 'xyz'} |
|
2075 | ||
2076 |
def test_max_age(self, payment_status, freezer): |
|
2077 |
# transaction older than 1 day are ignored |
|
2078 |
freezer.move_to(timedelta(days=1, minutes=1)) |
|
2079 |
call_command('lingo-poll-backend', all_backends=True, max_age_in_days=1) |
|
2080 |
assert payment_status.call_count == 0 |
|
2081 | ||
2082 |
# default is 3 days |
|
2083 |
freezer.move_to(timedelta(days=2)) |
|
2084 |
call_command('lingo-poll-backend', all_backends=True) |
|
2085 |
assert payment_status.call_count == 0 |
|
2086 | ||
2087 |
def test_payment_exception(self, payment_status, freezer): |
|
2088 |
payment_status.side_effect = eopayment.PaymentException('boom!!') |
|
2089 |
# transactions are polled after 5 minutes. |
|
2090 |
freezer.move_to(timedelta(minutes=5, seconds=1)) |
|
2091 |
call_command('lingo-poll-backend', interactive=False, all_backends=True) |
|
2092 |
assert payment_status.call_count == 1 |
|
2093 |
transaction = Transaction.objects.get() |
|
2094 |
assert transaction.status != eopayment.PAID |
|
2095 | ||
2096 |
with pytest.raises(PaymentException): # from combo.apps.lingo.models |
|
2097 |
call_command('lingo-poll-backend', all_backends=True) |
|
2098 | ||
2099 |
def test_cli_ok(self): |
|
2100 |
with mock.patch('combo.apps.lingo.models.PaymentBackend.poll_backend') as mock_poll_backend: |
|
2101 |
call_command('lingo-poll-backend', backend='test1') |
|
2102 |
assert mock_poll_backend.call_count == 1 |
|
2103 | ||
2104 |
with mock.patch('combo.apps.lingo.models.PaymentBackend.poll_backend') as mock_poll_backend: |
|
2105 |
with mock.patch( |
|
2106 |
'combo.apps.lingo.management.commands.lingo-poll-backend.input' |
|
2107 |
) as mock_input: |
|
2108 |
mock_input.return_value = 'test1' |
|
2109 |
call_command('lingo-poll-backend') |
|
2110 |
assert mock_poll_backend.call_count == 1 |
|
2111 | ||
2112 |
def test_cli_errors(self): |
|
2113 |
call_command('lingo-poll-backend', backend='test1') |
|
2114 | ||
2115 |
with mock.patch('combo.apps.lingo.management.commands.lingo-poll-backend.input') as mock_input: |
|
2116 |
mock_input.return_value = 'test1' |
|
2117 |
call_command('lingo-poll-backend') |
|
2118 | ||
2119 |
with pytest.raises(CommandError): |
|
2120 |
call_command('lingo-poll-backend', all_backends=True, backend='test1') |
|
2121 | ||
2122 |
with pytest.raises(CommandError): |
|
2123 |
call_command('lingo-poll-backend', backend='coin') |
|
2124 | ||
2125 |
with transaction.atomic(): |
|
2126 |
PaymentBackend.objects.all().delete() |
|
2127 |
with pytest.raises(CommandError): |
|
2128 |
call_command('lingo-poll-backend', all_backends=True) |
|
2129 | ||
2130 |
class TestRecentTransactionsCell: |
|
2131 |
@pytest.fixture(autouse=True) |
|
2132 |
def setup(self, app, user, basket_page, mono_regie): |
|
2133 |
BasketItem.objects.create( |
|
2134 |
user=user, |
|
2135 |
regie=mono_regie, |
|
2136 |
amount=42, |
|
2137 |
subject='foo item', |
|
2138 |
request_data={'refdet': 'F20201030', 'exer': '2020'}, |
|
2139 |
) |
|
2140 |
cell = LingoRecentTransactionsCell(page=basket_page, placeholder='content', order=1) |
|
2141 |
cell.save() |
|
2142 | ||
2143 |
@pytest.fixture |
|
2144 |
def app(self, app, user): |
|
2145 |
login(app) |
|
2146 |
return app |
|
2147 | ||
2148 |
@mock.patch('eopayment.payfip_ws.Payment.payment_status') |
|
2149 |
def test_refresh_status_through_polling( |
|
2150 |
self, |
|
2151 |
payment_status, |
|
2152 |
app, |
|
2153 |
): |
|
2154 |
# Try to pay |
|
2155 |
pay_resp = app.get('/test_basket_cell/') |
|
2156 |
assert 'foo item' in pay_resp |
|
2157 |
assert 'Running' not in pay_resp |
|
2158 |
resp = pay_resp.click('Pay') |
|
2159 |
# we are redirect to payfip |
|
2160 |
assert resp.location == 'https://payfip/' |
|
2161 | ||
2162 |
transaction = Transaction.objects.get() |
|
2163 | ||
2164 |
# Simulate still running status on polling |
|
2165 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
2166 |
signed=True, |
|
2167 |
result=eopayment.WAITING, |
|
2168 |
order_id=transaction.order_id, |
|
2169 |
) |
|
2170 | ||
2171 |
# Try to pay again |
|
2172 |
resp = app.get('/test_basket_cell/') |
|
2173 |
assert 'foo item' not in resp |
|
2174 |
assert 'Pay' not in resp |
|
2175 |
assert 'Running' in resp |
|
2176 |
resp = pay_resp.click('Pay').follow() |
|
2177 |
assert 'Some items are already paid or' in resp |
|
2178 |
assert 'foo item' not in resp |
|
2179 |
assert 'Running' in resp |
|
2180 | ||
2181 |
# Simulate paid status on polling |
|
2182 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
2183 |
signed=True, |
|
2184 |
result=eopayment.PAID, |
|
2185 |
order_id=transaction.order_id, |
|
2186 |
) |
|
2187 | ||
2188 |
# Try to pay again |
|
2189 |
resp = app.get('/test_basket_cell/') |
|
2190 |
assert 'foo item: 42.00' in resp |
|
2191 |
assert 'Pay' not in resp |
|
2192 |
assert 'Running' not in resp |
|
2193 | ||
2194 |
@mock.patch('eopayment.payfip_ws.Payment.payment_status') |
|
2195 |
def test_exception_during_polling( |
|
2196 |
self, |
|
2197 |
payment_status, |
|
2198 |
app, |
|
2199 |
caplog, |
|
2200 |
): |
|
2201 |
# Try to pay |
|
2202 |
pay_resp = app.get('/test_basket_cell/') |
|
2203 |
assert 'foo item' in pay_resp |
|
2204 |
assert 'Running' not in pay_resp |
|
2205 |
resp = pay_resp.click('Pay') |
|
2206 |
# we are redirect to payfip |
|
2207 |
assert resp.location == 'https://payfip/' |
|
2208 | ||
2209 |
# Simulate polling failure |
|
2210 |
payment_status.side_effect = eopayment.PaymentException('boom!') |
|
2211 | ||
2212 |
# Try to pay again |
|
2213 |
resp = app.get('/test_basket_cell/') |
|
2214 |
assert 'foo item' in pay_resp |
|
2215 |
assert 'Running' not in pay_resp |
|
2216 |
last_record = caplog.records[-1] |
|
2217 |
assert last_record.levelname == 'WARNING' |
|
2218 |
assert 'polling backend for transaction' in last_record.message |
tests/test_lingo_remote_regie.py | ||
---|---|---|
5 | 5 |
from decimal import Decimal |
6 | 6 | |
7 | 7 |
import eopayment |
8 |
import httmock |
|
8 | 9 |
import mock |
9 | 10 |
import pytest |
10 | 11 |
from django.apps import apps |
... | ... | |
32 | 33 |
from combo.data.models import Page |
33 | 34 |
from combo.utils import aes_hex_encrypt, check_query |
34 | 35 | |
36 |
from .test_manager import login |
|
37 | ||
35 | 38 |
pytestmark = pytest.mark.django_db |
36 | 39 | |
37 | 40 | |
... | ... | |
742 | 745 |
assert 'http://localhost' in html_message |
743 | 746 |
assert mailoutbox[0].attachments[0][0] == '01.pdf' |
744 | 747 |
assert mailoutbox[0].attachments[0][2] == 'application/pdf' |
748 | ||
749 | ||
750 |
@pytest.fixture |
|
751 |
def remote_invoices_httmock(): |
|
752 |
invoices = [] |
|
753 |
invoice = {} |
|
754 | ||
755 |
netloc = 'remote.regie.example.com' |
|
756 | ||
757 |
@httmock.urlmatch(netloc=netloc, path='^/invoice/') |
|
758 |
def invoice_mock(url, request): |
|
759 |
return json.dumps({'err': 0, 'data': invoice}) |
|
760 | ||
761 |
@httmock.urlmatch(netloc=netloc, path='^/invoices/') |
|
762 |
def invoices_mock(url, request): |
|
763 |
return json.dumps({'err': 0, 'data': invoices}) |
|
764 | ||
765 |
context_manager = httmock.HTTMock(invoices_mock, invoice_mock) |
|
766 |
context_manager.url = 'https://%s/' % netloc |
|
767 |
context_manager.invoices = invoices |
|
768 |
context_manager.invoice = invoice |
|
769 |
with context_manager: |
|
770 |
yield context_manager |
|
771 | ||
772 | ||
773 |
class TestPolling: |
|
774 |
@mock.patch('eopayment.payfip_ws.Payment.payment_status') |
|
775 |
@mock.patch('eopayment.payfip_ws.Payment.request', return_value=(1, eopayment.URL, 'https://payfip/')) |
|
776 |
def test_in_active_items_cell( |
|
777 |
self, |
|
778 |
payment_request, |
|
779 |
payment_status, |
|
780 |
app, |
|
781 |
remote_regie, |
|
782 |
settings, |
|
783 |
remote_invoices_httmock, |
|
784 |
synchronous_cells, |
|
785 |
): |
|
786 | ||
787 |
remote_invoices_httmock.invoices.extend(INVOICES) |
|
788 |
remote_invoices_httmock.invoice.update(INVOICES[0]) |
|
789 |
remote_regie.webservice_url = remote_invoices_httmock.url |
|
790 |
remote_regie.save() |
|
791 |
# use payfip |
|
792 |
remote_regie.payment_backend.service = 'payfip_ws' |
|
793 |
remote_regie.payment_backend.save() |
|
794 | ||
795 |
User.objects.create_user('admin', password='admin', email='foo@example.com') |
|
796 |
page = Page.objects.create(title='xxx', slug='test_basket_cell', template_name='standard') |
|
797 |
ActiveItems.objects.create(regie='remote', page=page, placeholder='content', order=0) |
|
798 | ||
799 |
login(app) |
|
800 | ||
801 |
assert Transaction.objects.count() == 0 |
|
802 | ||
803 |
resp = app.get('/test_basket_cell/') |
|
804 |
assert 'F-2016-One' in resp |
|
805 | ||
806 |
resp = resp.click('pay', index=0) |
|
807 |
pay_resp = resp |
|
808 |
resp = resp.form.submit('Pay') |
|
809 | ||
810 |
transaction = Transaction.objects.get() |
|
811 |
assert transaction.status == 0 |
|
812 | ||
813 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
814 |
signed=True, |
|
815 |
result=eopayment.WAITING, |
|
816 |
order_id=transaction.order_id, |
|
817 |
) |
|
818 | ||
819 |
assert payment_status.call_count == 0 |
|
820 |
resp = app.get('/test_basket_cell/') |
|
821 |
assert 'F-2016-One' in resp |
|
822 |
assert payment_status.call_count == 1 |
|
823 |
transaction.refresh_from_db() |
|
824 |
assert transaction.status == eopayment.WAITING |
|
825 | ||
826 |
resp = resp.click('pay', index=0) |
|
827 |
assert 'Waiting for payment' in resp |
|
828 |
assert 'button' not in resp |
|
829 | ||
830 |
resp = pay_resp.form.submit('Pay').follow() |
|
831 |
assert 'Some items are already paid' in resp |
|
832 | ||
833 |
payment_status.return_value = eopayment.common.PaymentResponse( |
|
834 |
signed=True, |
|
835 |
result=eopayment.PAID, |
|
836 |
order_id=transaction.order_id, |
|
837 |
) |
|
838 | ||
839 |
resp = app.get('/test_basket_cell/') |
|
840 |
assert 'F-2016-One' not in resp |
|
841 |
transaction.refresh_from_db() |
|
842 |
assert transaction.status == eopayment.PAID |
|
745 |
- |