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