0002-lingo-support-anonymous-and-no-basket-payment-36876.patch
combo/apps/lingo/templates/lingo/combo/payment-status.html | ||
---|---|---|
1 |
{% extends "combo/page_template.html" %} |
|
2 |
{% load staticfiles i18n %} |
|
3 | ||
4 |
{% block combo-content %} |
|
5 |
{% block wait-js %} |
|
6 |
<script> |
|
7 |
function display_error(message) { |
|
8 |
$('#transaction-error').text(message); |
|
9 |
$('#transaction-error').show(); |
|
10 |
$("#wait-msg").hide(); |
|
11 |
} |
|
12 | ||
13 |
$(function() { |
|
14 |
var next_url = '{{next_url}}'; |
|
15 |
var transaction_id = '{{transaction_id}}'; |
|
16 |
if (transaction_id === "") { |
|
17 |
display_error($('#transaction-error').data('error')); |
|
18 |
} |
|
19 |
else { |
|
20 |
$.ajax({ |
|
21 |
url: `/api/lingo/transaction-status/${transaction_id}/`, |
|
22 |
success: function(data, status) { |
|
23 |
if (!data.wait) { |
|
24 |
$('#wait-msg').text($('#wait-msg').data('continue')) |
|
25 |
// wait a little to show messages |
|
26 |
setTimeout(function(){location.href=next_url}, 3000); |
|
27 |
} else if (data.error) { |
|
28 |
display_error(data.error_msg) |
|
29 |
} else { |
|
30 |
setTimeout(wait_payment, 3000, next_url, transaction_id); |
|
31 |
} |
|
32 |
}, |
|
33 |
error: function(error) { |
|
34 |
display_error($('#transaction-status').data('error')); |
|
35 |
window.console && console.log(':(', error); |
|
36 |
} |
|
37 |
}); |
|
38 |
} |
|
39 |
}); |
|
40 |
</script> |
|
41 |
{% endblock %} |
|
42 | ||
43 |
{% block wait-content%} |
|
44 |
<div> |
|
45 |
{% block wait-message %} |
|
46 |
<h2 id="wait-msg" data-continue="{% trans "Wait a moment or click on 'Continue'." %}">{% trans "Please wait while your request is being processed..." %}</h2> |
|
47 |
{% endblock %} |
|
48 |
<div id="transaction-error" class="errornotice" data-error="{% trans 'An error occured' %}" style="display: none;"></div> |
|
49 |
<p><a id="next-url" href="{{next_url}}">{% trans "Continue" %}</a></p> |
|
50 |
</p> |
|
51 |
</div> |
|
52 |
{% endblock %} |
|
53 |
{% endblock %} |
combo/apps/lingo/urls.py | ||
---|---|---|
21 | 21 |
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView, |
22 | 22 |
ReturnView, ItemDownloadView, ItemView, CancelItemView, |
23 | 23 |
RemoveBasketItemApiView, ValidateTransactionApiView, |
24 |
CancelTransactionApiView, SelfInvoiceView, BasketItemPayView) |
|
24 |
CancelTransactionApiView, SelfInvoiceView, BasketItemPayView, |
|
25 |
TransactionStatusApiView, PaymentStatusView) |
|
25 | 26 |
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView, |
26 | 27 |
RegieDeleteView, TransactionListView, BasketItemErrorListView, |
27 | 28 |
download_transactions_csv, PaymentBackendListView, |
... | ... | |
60 | 61 |
name='api-validate-transaction'), |
61 | 62 |
url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(), |
62 | 63 |
name='api-cancel-transaction'), |
64 |
url( |
|
65 |
'^api/lingo/transaction-status/(?P<transaction_signature>.+)/$', TransactionStatusApiView.as_view(), |
|
66 |
name='api-transaction-status' |
|
67 |
), |
|
63 | 68 |
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'), |
64 | 69 |
url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'), |
65 | 70 |
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'), |
... | ... | |
74 | 79 |
ItemDownloadView.as_view(), name='download-item-pdf'), |
75 | 80 |
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$', |
76 | 81 |
ItemView.as_view(), name='view-item'), |
77 |
url(r'^lingo/item/(?P<item_id>\d+)/pay$',
|
|
82 |
url(r'^lingo/item/(?P<item_signature>.+)/pay$',
|
|
78 | 83 |
BasketItemPayView.as_view(), name='basket-item-pay-view'), |
84 |
url(r'^lingo/payment-status$', |
|
85 |
PaymentStatusView.as_view(), name='payment-status'), |
|
79 | 86 |
url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(), |
80 | 87 |
name='lingo-self-invoice'), |
81 | 88 |
] |
combo/apps/lingo/views.py | ||
---|---|---|
23 | 23 |
from django.contrib.auth.models import User |
24 | 24 |
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied |
25 | 25 |
from django.core.urlresolvers import reverse |
26 |
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest |
|
26 |
from django.core import signing |
|
27 |
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect, HttpResponseBadRequest |
|
27 | 28 |
from django.http import HttpResponseForbidden, Http404, JsonResponse |
28 | 29 |
from django.template.response import TemplateResponse |
29 | 30 |
from django.utils import timezone, dateparse, six |
30 | 31 |
from django.utils.encoding import force_text |
32 |
from django.utils.http import urlencode |
|
31 | 33 |
from django.views.decorators.csrf import csrf_exempt |
32 | 34 |
from django.views.generic import View, DetailView, ListView, TemplateView |
33 | 35 |
from django.conf import settings |
... | ... | |
41 | 43 |
from combo.data.models import Page |
42 | 44 |
from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError |
43 | 45 |
from combo.profile.utils import get_user_from_name_id |
46 |
from combo.public.views import publish_page |
|
44 | 47 | |
45 | 48 |
from .models import (Regie, BasketItem, Transaction, TransactionOperation, |
46 |
LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend) |
|
49 |
LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED)
|
|
47 | 50 | |
48 |
def get_eopayment_object(request, regie_or_payment_backend): |
|
51 | ||
52 |
def get_eopayment_object(request, regie_or_payment_backend, transaction_id=None): |
|
49 | 53 |
payment_backend = regie_or_payment_backend |
50 | 54 |
if isinstance(regie_or_payment_backend, Regie): |
51 | 55 |
payment_backend = regie_or_payment_backend.payment_backend |
52 | 56 |
options = payment_backend.service_options |
57 |
normal_return_url = reverse( |
|
58 |
'lingo-return-payment-backend', |
|
59 |
kwargs={'payment_backend_pk': payment_backend.id} |
|
60 |
) |
|
61 |
if transaction_id: |
|
62 |
normal_return_url = "%s?lingo-transaction-id=%s" % (normal_return_url, signing.dumps(transaction_id)) |
|
53 | 63 |
options.update({ |
54 | 64 |
'automatic_return_url': request.build_absolute_uri( |
55 | 65 |
reverse('lingo-callback-payment-backend', |
56 | 66 |
kwargs={'payment_backend_pk': payment_backend.id})), |
57 |
'normal_return_url': request.build_absolute_uri( |
|
58 |
reverse('lingo-return-payment-backend', |
|
59 |
kwargs={'payment_backend_pk': payment_backend.id})), |
|
67 |
'normal_return_url': request.build_absolute_uri(normal_return_url) |
|
60 | 68 |
}) |
61 | 69 |
return eopayment.Payment(payment_backend.service, options) |
62 | 70 | |
... | ... | |
150 | 158 |
elif request.GET.get('email'): |
151 | 159 |
user = User.objects.get(email=request.GET.get('email')) |
152 | 160 |
else: |
153 |
raise Exception('no user specified')
|
|
161 |
user = None
|
|
154 | 162 |
except User.DoesNotExist: |
155 | 163 |
raise Exception('unknown user') |
156 | 164 | |
... | ... | |
192 | 200 |
'Bad format for capture date, it should be yyyy-mm-dd.') |
193 | 201 | |
194 | 202 |
item.save() |
195 |
item.regie.compute_extra_fees(user=item.user) |
|
196 | ||
197 |
payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id}) |
|
203 |
if user: |
|
204 |
item.regie.compute_extra_fees(user=item.user) |
|
205 |
else: |
|
206 |
if item.regie.extra_fees_ws_url: |
|
207 |
HttpResponseBadRequest('Can not compute extra fees with anonymous user.') |
|
208 | ||
209 |
payment_url = reverse( |
|
210 |
'basket-item-pay-view', |
|
211 |
kwargs={ |
|
212 |
'item_signature': signing.dumps(item.pk) |
|
213 |
}) |
|
198 | 214 |
return JsonResponse({'result': 'success', 'id': str(item.id), |
199 | 215 |
'payment_url': request.build_absolute_uri(payment_url)}) |
200 | 216 | |
... | ... | |
321 | 337 | |
322 | 338 |
class PayMixin(object): |
323 | 339 |
@atomic |
324 |
def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''): |
|
340 |
def handle_payment( |
|
341 |
self, request, regie, items, remote_items, next_url='/', email='', firstname='', |
|
342 |
lastname=''): |
|
343 | ||
325 | 344 |
if remote_items: |
326 | 345 |
total_amount = sum([x.amount for x in remote_items]) |
327 | 346 |
else: |
... | ... | |
329 | 348 | |
330 | 349 |
if total_amount < regie.payment_min_amount: |
331 | 350 |
messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount) |
332 |
return HttpResponseRedirect(next_url)
|
|
351 |
return HttpResponseRedirect(get_payment_status_view(next_url=items[0].source_url))
|
|
333 | 352 | |
334 | 353 |
for item in items: |
335 | 354 |
if item.regie != regie: |
... | ... | |
344 | 363 |
lastname = user.last_name |
345 | 364 |
else: |
346 | 365 |
transaction.user = None |
347 |
firstname = '' |
|
348 |
lastname = '' |
|
349 | 366 | |
350 | 367 |
transaction.save() |
351 | 368 |
transaction.regie = regie |
... | ... | |
354 | 371 |
transaction.status = 0 |
355 | 372 |
transaction.amount = total_amount |
356 | 373 | |
357 |
payment = get_eopayment_object(request, regie) |
|
374 |
payment = get_eopayment_object(request, regie, transaction.pk)
|
|
358 | 375 |
kwargs = { |
359 | 376 |
'email': email, 'first_name': firstname, 'last_name': lastname |
360 | 377 |
} |
... | ... | |
373 | 390 | |
374 | 391 |
# store the next url in session in order to be able to redirect to |
375 | 392 |
# it if payment is canceled |
376 |
request.session.setdefault('lingo_next_url', |
|
377 |
{})[order_id] = request.build_absolute_uri(next_url) |
|
393 |
if next_url: |
|
394 |
request.session.setdefault('lingo_next_url', |
|
395 |
{})[str(transaction.pk)] = request.build_absolute_uri(next_url) |
|
378 | 396 |
request.session.modified = True |
379 | 397 | |
380 | 398 |
if kind == eopayment.URL: |
... | ... | |
433 | 451 |
return self.handle_payment(request, regie, items, remote_items, next_url, email) |
434 | 452 | |
435 | 453 | |
454 |
def get_payment_status_view(transaction_id=None, next_url=None): |
|
455 |
url = reverse('payment-status') |
|
456 |
params = [] |
|
457 |
if transaction_id: |
|
458 |
params.append(('transaction-id', signing.dumps(transaction_id))) |
|
459 |
if next_url: |
|
460 |
params.append(('next', next_url)) |
|
461 |
return "%s?%s" % (url, urlencode(params)) |
|
462 | ||
463 | ||
436 | 464 |
class BasketItemPayView(PayMixin, View): |
465 | ||
437 | 466 |
def get(self, request, *args, **kwargs): |
438 |
next_url = request.GET.get('next_url') or '/' |
|
439 |
if not (request.user and request.user.is_authenticated): |
|
440 |
return HttpResponseForbidden(_('No item payment allowed for anonymous users.')) |
|
467 |
next_url = request.GET.get('next_url') |
|
468 |
email = request.GET.get('email', '') |
|
469 |
firstname = request.GET.get('firstname', '') |
|
470 |
lastname = request.GET.get('lastname', '') |
|
441 | 471 | |
442 |
item = BasketItem.objects.get(pk=kwargs['item_id']) |
|
472 |
item_signature = kwargs.get('item_signature') |
|
473 |
try: |
|
474 |
item_id = signing.loads(item_signature) |
|
475 |
except signing.BadSignature: |
|
476 |
return HttpResponseForbidden(_('Invalid payment request.')) |
|
477 | ||
478 |
item = BasketItem.objects.get(pk=item_id) |
|
443 | 479 |
regie = item.regie |
444 | 480 |
if regie.extra_fees_ws_url: |
445 | 481 |
return HttpResponseForbidden(_('No item payment allowed as extra fees set.')) |
446 | 482 | |
447 |
if item.user != request.user: |
|
483 |
if item.user and item.user != request.user:
|
|
448 | 484 |
return HttpResponseForbidden(_('Wrong item: payment not allowed.')) |
449 | 485 | |
450 |
return self.handle_payment(request, regie, [item], [], next_url) |
|
486 |
if not next_url: |
|
487 |
next_url = item.source_url |
|
488 | ||
489 |
return self.handle_payment( |
|
490 |
request=request, regie=regie, items=[item], remote_items=[], next_url=next_url, email=email, |
|
491 |
firstname=firstname, lastname=lastname |
|
492 |
) |
|
451 | 493 | |
452 | 494 | |
453 | 495 |
class PaymentException(Exception): |
... | ... | |
600 | 642 | |
601 | 643 |
def handle_return(self, request, backend_response, **kwargs): |
602 | 644 |
transaction = None |
645 |
transaction_id = request.GET.get('lingo-transaction-id') |
|
646 |
if transaction_id: |
|
647 |
try: |
|
648 |
transaction_id = signing.loads(transaction_id) |
|
649 |
except signing.BadSignature: |
|
650 |
pass |
|
603 | 651 |
try: |
604 | 652 |
transaction = self.handle_response(request, backend_response, **kwargs) |
605 | 653 |
except UnsignedPaymentException as e: |
606 | 654 |
# some payment backends do not sign return URLs, don't mark this as |
607 | 655 |
# an error, they will provide a notification to the callback |
608 | 656 |
# endpoint. |
609 |
pass |
|
657 |
if transaction_id: |
|
658 |
return HttpResponseRedirect(get_payment_status_view(transaction_id)) |
|
659 |
return HttpResponseRedirect(get_basket_url()) |
|
660 | ||
610 | 661 |
except PaymentException as e: |
611 | 662 |
messages.error(request, _('We are sorry but the payment service ' |
612 | 663 |
'failed to provide a correct answer.')) |
664 |
if transaction_id: |
|
665 |
return HttpResponseRedirect(get_payment_status_view(transaction_id)) |
|
613 | 666 |
return HttpResponseRedirect(get_basket_url()) |
614 | 667 | |
615 | 668 |
if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED): |
616 | 669 |
messages.info(request, transaction.regie.get_text_on_success()) |
617 | 670 | |
618 |
if transaction and request.session.get('lingo_next_url'): |
|
619 |
redirect_url = request.session['lingo_next_url'].get(transaction.order_id) |
|
620 |
if redirect_url: |
|
621 |
return HttpResponseRedirect(redirect_url) |
|
671 |
if transaction: |
|
672 |
return HttpResponseRedirect(get_payment_status_view(transaction.pk)) |
|
622 | 673 | |
623 | 674 |
# return to basket page if there are still items to pay |
624 | 675 |
if request.user.is_authenticated: |
... | ... | |
765 | 816 |
return HttpResponseRedirect(url) |
766 | 817 |
messages.warning(request, msg) |
767 | 818 |
return HttpResponseRedirect(request.GET.get('page_path') or '/') |
819 | ||
820 | ||
821 |
class PaymentStatusView(View): |
|
822 | ||
823 |
http_method_names = ['get'] |
|
824 | ||
825 |
def get(self, request, *args, **kwargs): |
|
826 |
page = Page() |
|
827 |
page.template_name = 'standard' |
|
828 |
template_name = 'lingo/combo/payment-status.html' |
|
829 | ||
830 |
extra_context_data = getattr(request, 'extra_context_data', {}) |
|
831 |
extra_context_data['transaction_id'] = '' |
|
832 | ||
833 |
transaction_id = request.GET.get('transaction-id') |
|
834 | ||
835 |
if not transaction_id: |
|
836 |
next_url = request.GET.get('next') |
|
837 |
if not next_url: |
|
838 |
next_url = '/' |
|
839 |
extra_context_data['next_url'] = request.build_absolute_uri(next_url) |
|
840 |
request.extra_context_data = extra_context_data |
|
841 |
return publish_page(request, page, template_name=template_name) |
|
842 | ||
843 |
try: |
|
844 |
transaction_id = signing.loads(transaction_id) |
|
845 |
except signing.BadSignature: |
|
846 |
return HttpResponseForbidden(_('Invalid transaction signature.')) |
|
847 | ||
848 |
try: |
|
849 |
transaction = Transaction.objects.get(pk=transaction_id) |
|
850 |
except Transaction.DoesNotExist: |
|
851 |
return HttpResponseForbidden(_('Invalid transaction.')) |
|
852 | ||
853 |
next_url = request.session.get('lingo_next_url', {}).get(str(transaction_id)) |
|
854 |
if not next_url: |
|
855 |
next_url = get_basket_url() |
|
856 |
if len(set([item.source_url for item in transaction.items.all()])) == 1: |
|
857 |
next_url = transaction.items.first().source_url |
|
858 |
next_url = request.build_absolute_uri(next_url) |
|
859 | ||
860 |
extra_context_data['transaction_id'] = signing.dumps(transaction.pk) |
|
861 |
extra_context_data['next_url'] = next_url |
|
862 |
request.extra_context_data = extra_context_data |
|
863 |
return publish_page(request, page, template_name=template_name) |
|
864 | ||
865 | ||
866 |
class TransactionStatusApiView(View): |
|
867 | ||
868 |
http_method_names = ['get'] |
|
869 | ||
870 |
def get(self, request, *args, **kwargs): |
|
871 |
transaction_signature = kwargs.get('transaction_signature') |
|
872 |
try: |
|
873 |
transaction_id = signing.loads(transaction_signature) |
|
874 |
except signing.BadSignature: |
|
875 |
return HttpResponseBadRequest(_('Invalid transaction.')) |
|
876 | ||
877 |
try: |
|
878 |
transaction = Transaction.objects.get(pk=transaction_id) |
|
879 |
except (Transaction.DoesNotExist, ValueError): |
|
880 |
return HttpResponseNotFound(_('Unknown transaction.')) |
|
881 | ||
882 |
user = request.user if request.user.is_authenticated() else None |
|
883 |
error_msg = _('Transaction does not belong to the requesting user') |
|
884 |
if user and transaction.user and user != transaction.user: |
|
885 |
return HttpResponseForbidden(error_msg) |
|
886 |
if not user and transaction.user: |
|
887 |
return HttpResponseForbidden(error_msg) |
|
888 | ||
889 |
if transaction.is_paid(): |
|
890 |
data = { |
|
891 |
'wait': False, |
|
892 |
'error': False, |
|
893 |
'error_msg': '' |
|
894 |
} |
|
895 |
return JsonResponse(data=data) |
|
896 | ||
897 |
if transaction.status in ( |
|
898 |
eopayment.CANCELLED, eopayment.ERROR, eopayment.DENIED, EXPIRED |
|
899 |
): |
|
900 |
data = { |
|
901 |
'wait': True, |
|
902 |
'error': True, |
|
903 |
'error_msg': _('Payment error, you can continue and make another payment') |
|
904 |
} |
|
905 |
return JsonResponse(data=data) |
|
906 | ||
907 |
data = { |
|
908 |
'wait': True, |
|
909 |
'error': False, |
|
910 |
'error_msg': '' |
|
911 |
} |
|
912 |
return JsonResponse(data=data) |
tests/test_lingo_payment.py | ||
---|---|---|
9 | 9 | |
10 | 10 |
from django.apps import apps |
11 | 11 |
from django.contrib.auth.models import User |
12 |
from django.core import signing |
|
12 | 13 |
from django.core.urlresolvers import reverse |
13 | 14 |
from django.core.wsgi import get_wsgi_application |
14 | 15 |
from django.conf import settings |
15 | 16 |
from django.test import override_settings |
16 | 17 |
from django.utils import timezone |
18 |
from django.utils.http import urlencode |
|
17 | 19 |
from django.utils.six.moves.urllib import parse as urlparse |
18 | 20 |
from django.contrib.messages.storage.session import SessionStorage |
19 | 21 |
from webtest import TestApp |
... | ... | |
22 | 24 |
from combo.apps.lingo.models import ( |
23 | 25 |
Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell, |
24 | 26 |
PaymentBackend) |
25 |
from combo.utils import sign_url |
|
27 |
from combo.utils import aes_hex_decrypt, sign_url
|
|
26 | 28 | |
27 | 29 |
from .test_manager import login |
28 | 30 | |
... | ... | |
120 | 122 |
else: |
121 | 123 |
return settings.LINGO_API_SIGN_KEY |
122 | 124 | |
125 | ||
126 |
def assert_payment_status(url, transaction_id=None): |
|
127 |
if hasattr(url, 'path'): |
|
128 |
url = url.path |
|
129 | ||
130 |
if transaction_id: |
|
131 |
url, part = url.split('?') |
|
132 |
query = urlparse.parse_qs(part) |
|
133 |
assert 'transaction-id' in query |
|
134 |
assert signing.loads(query['transaction-id'][0]) == transaction_id |
|
135 | ||
136 |
assert url.startswith('/lingo/payment-status') |
|
137 | ||
138 | ||
123 | 139 |
def test_default_regie(): |
124 | 140 |
payment_backend = PaymentBackend.objects.create(label='foo', slug='foo') |
125 | 141 |
Regie.objects.all().delete() |
... | ... | |
218 | 234 |
assert resp.status_code == 200 |
219 | 235 |
# simulate successful return URL |
220 | 236 |
resp = app.get(qs['return_url'][0], params=args) |
237 |
# redirect to payment status |
|
221 | 238 |
assert resp.status_code == 302 |
222 |
assert urlparse.urlparse(resp.url).path == '/test_basket_cell/'
|
|
239 |
assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status')
|
|
223 | 240 |
resp = resp.follow() |
224 | 241 |
assert 'Your payment has been succesfully registered.' in resp.text |
242 |
assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ |
|
243 |
'/test_basket_cell/' |
|
244 | ||
225 | 245 | |
226 | 246 |
def test_add_amount_to_basket(app, key, regie, user): |
227 | 247 |
payment_backend = PaymentBackend.objects.create( |
... | ... | |
297 | 317 |
assert resp.status_code == 200 |
298 | 318 |
response = json.loads(resp.text) |
299 | 319 |
assert response['result'] == 'success' |
300 |
assert response['payment_url'].endswith('/lingo/item/%s/pay' % item.id) |
|
320 |
payment_url = urlparse.urlparse(response['payment_url']) |
|
321 |
assert payment_url.path.startswith('/lingo/item/') |
|
322 |
assert payment_url.path.endswith('/pay') |
|
301 | 323 |
assert BasketItem.objects.filter(amount=Decimal('22.23')).exists() |
302 | 324 |
assert BasketItem.objects.filter(amount=Decimal('22.23'))[0].regie_id == other_regie.id |
303 | 325 | |
... | ... | |
417 | 439 |
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists() |
418 | 440 |
payment_url = resp.json['payment_url'] |
419 | 441 |
resp = app.get(payment_url, status=403) |
420 |
assert 'No item payment allowed for anonymous users.' in resp.text
|
|
442 |
assert 'Wrong item: payment not allowed.' in resp.text
|
|
421 | 443 | |
422 | 444 |
login(app, username='john.doe', password='john.doe') |
423 | 445 |
resp = app.get(payment_url, status=403) |
... | ... | |
440 | 462 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
441 | 463 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
442 | 464 |
assert qs['amount'] == ['12.00'] |
443 | ||
444 | 465 |
# simulate successful payment response from dummy backend |
445 | 466 |
data = {'transaction_id': qs['transaction_id'][0], 'ok': True, |
446 | 467 |
'amount': qs['amount'][0], 'signed': True} |
... | ... | |
448 | 469 |
# dummy module put that URL in return_url query string parameter). |
449 | 470 |
resp = app.get(qs['return_url'][0], params=data) |
450 | 471 |
# check that item is paid |
451 |
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).exists() |
|
452 |
# check that user is redirected to the next_url passed previously |
|
453 |
assert resp.location == 'http://example.net/form/id/' |
|
472 |
item = BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).first() |
|
473 |
# check that user is redirected to the item payment status view |
|
474 |
assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk) |
|
475 | ||
454 | 476 | |
455 | 477 |
def test_pay_multiple_regies(app, key, regie, user): |
456 | 478 |
test_add_amount_to_basket(app, key, regie, user) |
... | ... | |
743 | 765 |
data = {'transaction_id': transaction_id, |
744 | 766 |
'amount': qs['amount'][0], 'ok': True} |
745 | 767 |
assert data['amount'] == '10.50' |
768 |
return_qs = urlparse.parse_qs(urlparse.urlparse(qs['return_url'][0]).query) |
|
769 |
lingo_transaction_id = return_qs['lingo-transaction-id'][0] |
|
746 | 770 | |
747 | 771 |
# call return with unsigned POST |
748 | 772 |
with check_log(caplog, 'received unsigned payment'): |
... | ... | |
768 | 792 | |
769 | 793 |
# call return with signed POST |
770 | 794 |
data['signed'] = True |
771 |
return_url = get_url(with_payment_backend, 'lingo-return', regie) |
|
795 |
return_url = get_url(with_payment_backend, 'lingo-return', regie) + \ |
|
796 |
'?lingo-transaction-id=%s' % lingo_transaction_id |
|
772 | 797 |
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: |
773 | 798 |
get_resp = app.post(return_url, params=data) |
774 | 799 |
url = request.call_args[0][1] |
775 | 800 |
assert url.startswith('http://example.org/testitem/jump/trigger/paid') |
801 |
# redirect to payment status |
|
776 | 802 |
assert get_resp.status_code == 302 |
777 |
assert urlparse.urlparse(get_resp['location']).path == '/test_basket_cell/'
|
|
778 |
resp = app.get(get_resp['Location'])
|
|
803 |
assert urlparse.urlparse(get_resp.url).path.startswith('/lingo/payment-status')
|
|
804 |
resp = get_resp.follow()
|
|
779 | 805 |
assert 'Your payment has been succesfully registered.' in resp.text |
806 |
assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ |
|
807 |
'/test_basket_cell/' |
|
780 | 808 |
assert Transaction.objects.get(order_id=transaction_id).status == eopayment.PAID |
781 | 809 | |
810 | ||
782 | 811 |
def test_transaction_expiration(): |
783 | 812 |
t1 = Transaction(status=0) |
784 | 813 |
t1.save() |
... | ... | |
995 | 1024 |
assert url.startswith('http://example.org/testitem/jump/trigger/paid') |
996 | 1025 |
assert BasketItem.objects.get(id=item.id).payment_date |
997 | 1026 |
assert BasketItem.objects.get(id=item.id).notification_date |
1027 | ||
1028 | ||
1029 |
@pytest.mark.parametrize("authenticated", [True, False]) |
|
1030 |
def test_payment_no_basket(app, user, regie, authenticated): |
|
1031 |
url = reverse('api-add-basket-item') |
|
1032 |
source_url = 'http://example.org/item/1' |
|
1033 |
data = {'amount': 10, 'display_name': 'test item', 'url': source_url} |
|
1034 |
if authenticated: |
|
1035 |
data['email'] = user.email |
|
1036 |
url = sign_url(url, settings.LINGO_API_SIGN_KEY) |
|
1037 |
resp = app.post_json(url, params=data) |
|
1038 |
assert resp.status_code == 200 |
|
1039 |
payment_url = resp.json['payment_url'] |
|
1040 | ||
1041 |
item = BasketItem.objects.first() |
|
1042 |
assert item.user is None |
|
1043 |
assert item.amount == Decimal('10.00') |
|
1044 |
path = urlparse.urlparse(payment_url).path |
|
1045 |
start = '/lingo/item/' |
|
1046 |
end = '/pay' |
|
1047 |
assert path.startswith(start) |
|
1048 |
assert path.endswith(end) |
|
1049 |
signature = path.replace(start, '').replace(end, '') |
|
1050 |
assert signing.loads(signature) == item.id |
|
1051 | ||
1052 |
if authenticated: |
|
1053 |
app = login(app) |
|
1054 | ||
1055 |
# payment error due to too small amount |
|
1056 |
item.amount = Decimal('1.00') |
|
1057 |
item.save() |
|
1058 |
resp = app.get(payment_url) |
|
1059 |
assert_payment_status(resp.location) |
|
1060 |
resp = resp.follow() |
|
1061 |
assert 'Minimal payment amount is 4.50' in resp.text |
|
1062 |
# we can go back to form |
|
1063 |
assert source_url in resp.text |
|
1064 | ||
1065 |
# amount ok, redirection to payment backend |
|
1066 |
item.amount = Decimal('10.00') |
|
1067 |
item.save() |
|
1068 |
resp = app.get(payment_url) |
|
1069 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
|
1070 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
|
1071 |
assert qs['amount'] == ['10.00'] |
|
1072 |
if authenticated: |
|
1073 |
assert qs['email'] == ['foo@example.com'] |
|
1074 |
else: |
|
1075 |
assert 'email' not in qs |
|
1076 | ||
1077 |
# mail can be specified here for anonymous user |
|
1078 |
resp = app.get( |
|
1079 |
payment_url, |
|
1080 |
params={ |
|
1081 |
'email': 'foo@localhost', |
|
1082 |
} |
|
1083 |
) |
|
1084 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
|
1085 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
|
1086 |
assert qs['amount'] == ['10.00'] |
|
1087 |
if authenticated: |
|
1088 |
assert qs['email'] == ['foo@example.com'] |
|
1089 |
else: |
|
1090 |
assert qs['email'] == ['foo@localhost'] |
|
1091 | ||
1092 |
# simulate bad responseform payment backend, no transaction id |
|
1093 |
data = {'amount': qs['amount'][0], 'signed': True} |
|
1094 |
return_url = qs['return_url'][0] |
|
1095 |
assert 'lingo-transaction-id' in return_url |
|
1096 |
resp = app.get(return_url, params=data) |
|
1097 |
assert_payment_status(resp.location) |
|
1098 |
resp = resp.follow() |
|
1099 |
assert 'We are sorry but the payment service failed to provide a correct answer.' in resp.text |
|
1100 |
assert 'http://example.org/item/1' in resp.text |
|
1101 |
# check that item is not paid |
|
1102 |
item = BasketItem.objects.get(pk=item.pk) |
|
1103 |
assert not item.payment_date |
|
1104 | ||
1105 |
# simulate successful payment response from dummy backend |
|
1106 |
data = { |
|
1107 |
'transaction_id': qs['transaction_id'][0], 'ok': True, |
|
1108 |
'amount': qs['amount'][0], 'signed': True |
|
1109 |
} |
|
1110 |
return_url = qs['return_url'][0] |
|
1111 |
assert 'lingo-transaction-id' in return_url |
|
1112 |
resp = app.get(return_url, params=data) |
|
1113 |
assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk) |
|
1114 |
# check that item is paid |
|
1115 |
item = BasketItem.objects.get(pk=item.pk) |
|
1116 |
assert item.payment_date |
|
1117 |
# accept redirection to item payment status view |
|
1118 |
resp = resp.follow() |
|
1119 |
# which should it self redirect to item.source_url as it is paid |
|
1120 |
assert 'Please wait while your request is being processed' in resp.text |
|
1121 |
assert source_url in resp.text |
|
1122 | ||
1123 | ||
1124 |
def test_transaction_status_api(app, regie, user): |
|
1125 |
# invalid transaction signature |
|
1126 |
url = reverse( |
|
1127 |
'api-transaction-status', |
|
1128 |
kwargs={'transaction_signature': signing.dumps('xxxx')} |
|
1129 | ||
1130 |
) |
|
1131 |
resp = app.get(url, status=404) |
|
1132 |
assert 'Unknown transaction.' in resp.text |
|
1133 | ||
1134 |
# unkown transaction identifier |
|
1135 |
transaction_id = 1000 |
|
1136 |
url = reverse( |
|
1137 |
'api-transaction-status', |
|
1138 |
kwargs={'transaction_signature': signing.dumps(transaction_id)} |
|
1139 |
) |
|
1140 |
resp = app.get(url, status=404) |
|
1141 |
assert 'Unknown transaction.' in resp.text |
|
1142 | ||
1143 |
wait_response = { |
|
1144 |
'wait': True, |
|
1145 |
'error': False, |
|
1146 |
'error_msg': '' |
|
1147 |
} |
|
1148 |
# anonymous user on anonymous transaction: OK |
|
1149 |
transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0) |
|
1150 |
url = reverse( |
|
1151 |
'api-transaction-status', |
|
1152 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1153 |
) |
|
1154 |
resp = app.get(url) |
|
1155 |
assert resp.json == wait_response |
|
1156 | ||
1157 |
# authenticated user on anonymous transaction: OK |
|
1158 |
transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0) |
|
1159 |
url = reverse( |
|
1160 |
'api-transaction-status', |
|
1161 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1162 |
) |
|
1163 |
resp = login(app).get(url) |
|
1164 |
assert resp.json == wait_response |
|
1165 |
app.reset() |
|
1166 | ||
1167 |
# authenticated user on his transaction: OK |
|
1168 |
transaction = Transaction.objects.create( |
|
1169 |
amount=Decimal('10.0'), regie=regie, status=0, user=user) |
|
1170 |
url = reverse( |
|
1171 |
'api-transaction-status', |
|
1172 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1173 |
) |
|
1174 |
resp = login(app).get(url) |
|
1175 |
assert resp.json == wait_response |
|
1176 |
app.reset() |
|
1177 | ||
1178 |
error_msg = 'Transaction does not belong to the requesting user' |
|
1179 |
# anonymous user on other user transaction transaction: NOTOK |
|
1180 |
transaction = Transaction.objects.create( |
|
1181 |
amount=Decimal('10.0'), regie=regie, status=0, user=user) |
|
1182 |
url = reverse( |
|
1183 |
'api-transaction-status', |
|
1184 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1185 |
) |
|
1186 |
resp = app.get(url, status=403) |
|
1187 |
assert error_msg in resp.text |
|
1188 | ||
1189 |
# authenticated user on other user transaction transaction: NOTOK |
|
1190 |
user2 = User.objects.create_user( |
|
1191 |
'user2', password='user2', email='user2@example.com' |
|
1192 |
) |
|
1193 |
transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0, user=user2) |
|
1194 |
url = reverse( |
|
1195 |
'api-transaction-status', |
|
1196 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1197 |
) |
|
1198 |
resp = login(app).get(url, status=403) |
|
1199 |
assert error_msg in resp.text |
|
1200 |
app.reset() |
|
1201 | ||
1202 |
# transaction error |
|
1203 |
transaction = Transaction.objects.create( |
|
1204 |
amount=Decimal('10.0'), regie=regie, status=eopayment.ERROR |
|
1205 |
) |
|
1206 |
url = reverse( |
|
1207 |
'api-transaction-status', |
|
1208 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1209 |
) |
|
1210 |
resp = app.get(url) |
|
1211 |
assert resp.json == { |
|
1212 |
'wait': True, |
|
1213 |
'error': True, |
|
1214 |
'error_msg': 'Payment error, you can continue and make another payment' |
|
1215 |
} |
|
1216 | ||
1217 |
# transaction paid |
|
1218 |
transaction = Transaction.objects.create( |
|
1219 |
amount=Decimal('10.0'), regie=regie, status=eopayment.PAID |
|
1220 |
) |
|
1221 |
url = reverse( |
|
1222 |
'api-transaction-status', |
|
1223 |
kwargs={'transaction_signature': signing.dumps(transaction.pk)} |
|
1224 |
) |
|
1225 |
resp = app.get(url) |
|
1226 |
assert resp.json == { |
|
1227 |
'wait': False, |
|
1228 |
'error': False, |
|
1229 |
'error_msg': '' |
|
1230 |
} |
tests/test_lingo_remote_regie.py | ||
---|---|---|
211 | 211 |
kwargs={'payment_backend_pk': remote_regie.payment_backend.id})) |
212 | 212 |
# simulate successful return URL |
213 | 213 |
resp = app.get(qs['return_url'][0], params=args) |
214 |
# redirect to payment status |
|
214 | 215 |
assert resp.status_code == 302 |
215 |
assert urlparse.urlparse(resp.url).path == '/' |
|
216 |
assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') |
|
217 |
resp = resp.follow() |
|
218 |
assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == '/' |
|
216 | 219 |
# simulate successful call to callback URL |
217 | 220 |
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args) |
218 | 221 |
trans = Transaction.objects.all() |
... | ... | |
335 | 338 |
# simulate payment failure |
336 | 339 |
mock_get.side_effect = ConnectionError('where is my hostname?') |
337 | 340 |
resp = app.get(qs['return_url'][0], params=args) |
341 |
# redirect to payment status |
|
338 | 342 |
assert resp.status_code == 302 |
339 |
assert urlparse.urlparse(resp.url).path == '/active-remote-invoices-page/' |
|
343 |
assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') |
|
344 |
resp = resp.follow() |
|
345 |
assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ |
|
346 |
'/active-remote-invoices-page/' |
|
347 | ||
340 | 348 |
# simulate successful call to callback URL |
341 | 349 |
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args) |
342 | 350 |
trans = Transaction.objects.all() |
... | ... | |
383 | 391 |
qs = urlparse.parse_qs(parsed.query) |
384 | 392 |
args = {'transaction_id': qs['transaction_id'][0], 'signed': True, |
385 | 393 |
'ok': True, 'reason': 'Paid'} |
386 | ||
387 | 394 |
resp = app.get(qs['return_url'][0], params=args) |
395 |
# redirect to payment status |
|
388 | 396 |
assert resp.status_code == 302 |
389 |
assert urlparse.urlparse(resp.location).path == '/active-remote-invoices-page/' |
|
397 |
assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status') |
|
398 |
resp = resp.follow() |
|
399 |
assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \ |
|
400 |
'/active-remote-invoices-page/' |
|
390 | 401 | |
391 | 402 | |
392 | 403 |
@mock.patch('combo.apps.lingo.models.UserSAMLIdentifier') |
393 |
- |