0001-lingo-support-anonymous-and-no-basket-payment-36876.patch
combo/apps/lingo/static/js/wait.payment.js | ||
---|---|---|
1 |
function display_error(message) { |
|
2 |
$('#transaction-status').text(message); |
|
3 |
$("#transaction-status").attr('class', 'errornotice'); |
|
4 |
} |
|
5 | ||
6 |
function wait_payment(source_url, transaction_id) { |
|
7 |
if (transaction_id === "") { |
|
8 |
display_error($('#transaction-status').data('error')); |
|
9 |
} |
|
10 |
else { |
|
11 |
$.ajax({ |
|
12 |
url: `/api/lingo/transaction-status/${transaction_id}/`, |
|
13 |
success: function(data, status) { |
|
14 |
if (data.redirect) { |
|
15 |
window.location.replace(source_url); |
|
16 |
} else if (data.transaction_error) { |
|
17 |
display_error(data.msg) |
|
18 |
} else { |
|
19 |
$('#transaction-status').text(data.msg); |
|
20 |
setTimeout(wait_payment, 3000, source_url, transaction_id); |
|
21 |
} |
|
22 |
}, |
|
23 |
oerror: function(error) { |
|
24 |
display_error($('#transaction-status').data('error')); |
|
25 |
window.console && console.log(':(', error); |
|
26 |
} |
|
27 |
}); |
|
28 |
} |
|
29 |
}; |
combo/apps/lingo/templates/lingo/combo/item-wait-payment.html | ||
---|---|---|
1 |
{% extends "combo/page_template.html" %} |
|
2 |
{% load staticfiles i18n %} |
|
3 | ||
4 |
{% block combo-content %} |
|
5 |
<script src="{% static "js/wait.payment.js" %}"></script> |
|
6 |
<script>$(function() { wait_payment('{{source_url}}', '{{transaction_id}}'); });</script> |
|
7 | ||
8 | ||
9 |
<div> |
|
10 |
<div id="transaction-status" class="infonotice" data-error="{% trans 'An error occured' %}"></div> |
|
11 |
<p><a href="{{source_url}}">{% trans "Go back to your form" %}</a></p> |
|
12 |
</p> |
|
13 |
</div> |
|
14 |
{% 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, BasketItemPaymentStatusView) |
|
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/item/(?P<signature>\w+)/payment-status$', |
|
85 |
BasketItemPaymentStatusView.as_view(), name='basket-item-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 | ||
---|---|---|
39 | 39 |
import eopayment |
40 | 40 | |
41 | 41 |
from combo.data.models import Page |
42 |
from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError |
|
42 |
from combo.utils import check_request_signature, aes_hex_decrypt, aes_hex_encrypt, DecryptionError
|
|
43 | 43 |
from combo.profile.utils import get_user_from_name_id |
44 |
from combo.public.views import publish_page |
|
44 | 45 | |
45 | 46 |
from .models import (Regie, BasketItem, Transaction, TransactionOperation, |
46 |
LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend) |
|
47 |
LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED)
|
|
47 | 48 | |
48 |
def get_eopayment_object(request, regie_or_payment_backend): |
|
49 | ||
50 |
def get_eopayment_object(request, regie_or_payment_backend, unique_item=None): |
|
49 | 51 |
payment_backend = regie_or_payment_backend |
50 | 52 |
if isinstance(regie_or_payment_backend, Regie): |
51 | 53 |
payment_backend = regie_or_payment_backend.payment_backend |
52 | 54 |
options = payment_backend.service_options |
55 |
normal_return_url = reverse( |
|
56 |
'lingo-return-payment-backend', |
|
57 |
kwargs={'payment_backend_pk': payment_backend.id} |
|
58 |
) |
|
59 |
if unique_item: |
|
60 |
normal_return_url = "%s?item-id=%s" % (normal_return_url, unique_item.pk) |
|
53 | 61 |
options.update({ |
54 | 62 |
'automatic_return_url': request.build_absolute_uri( |
55 | 63 |
reverse('lingo-callback-payment-backend', |
56 | 64 |
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})), |
|
65 |
'normal_return_url': request.build_absolute_uri(normal_return_url) |
|
60 | 66 |
}) |
61 | 67 |
return eopayment.Payment(payment_backend.service, options) |
62 | 68 | |
... | ... | |
150 | 156 |
elif request.GET.get('email'): |
151 | 157 |
user = User.objects.get(email=request.GET.get('email')) |
152 | 158 |
else: |
153 |
raise Exception('no user specified')
|
|
159 |
user = None
|
|
154 | 160 |
except User.DoesNotExist: |
155 | 161 |
raise Exception('unknown user') |
156 | 162 | |
... | ... | |
192 | 198 |
'Bad format for capture date, it should be yyyy-mm-dd.') |
193 | 199 | |
194 | 200 |
item.save() |
195 |
item.regie.compute_extra_fees(user=item.user) |
|
201 |
if user: |
|
202 |
item.regie.compute_extra_fees(user=item.user) |
|
196 | 203 | |
197 |
payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id}) |
|
204 |
payment_url = reverse( |
|
205 |
'basket-item-pay-view', |
|
206 |
kwargs={ |
|
207 |
'signature': aes_hex_encrypt(settings.SECRET_KEY, str(item.id)) |
|
208 |
}) |
|
198 | 209 |
return JsonResponse({'result': 'success', 'id': str(item.id), |
199 | 210 |
'payment_url': request.build_absolute_uri(payment_url)}) |
200 | 211 | |
... | ... | |
321 | 332 | |
322 | 333 |
class PayMixin(object): |
323 | 334 |
@atomic |
324 |
def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''): |
|
335 |
def handle_payment( |
|
336 |
self, request, regie, items, remote_items, next_url='/', email='', firstname='', |
|
337 |
lastname='', basket=True): |
|
338 | ||
339 |
single_item = None |
|
340 |
if not basket: |
|
341 |
single_item = items[0] |
|
342 | ||
325 | 343 |
if remote_items: |
326 | 344 |
total_amount = sum([x.amount for x in remote_items]) |
327 | 345 |
else: |
... | ... | |
329 | 347 | |
330 | 348 |
if total_amount < regie.payment_min_amount: |
331 | 349 |
messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount) |
332 |
return HttpResponseRedirect(next_url) |
|
350 |
if basket: |
|
351 |
return HttpResponseRedirect(next_url) |
|
352 |
return HttpResponseRedirect(get_single_item_payment_status_view(single_item.pk)) |
|
333 | 353 | |
334 | 354 |
for item in items: |
335 | 355 |
if item.regie != regie: |
... | ... | |
344 | 364 |
lastname = user.last_name |
345 | 365 |
else: |
346 | 366 |
transaction.user = None |
347 |
firstname = '' |
|
348 |
lastname = '' |
|
349 | 367 | |
350 | 368 |
transaction.save() |
351 | 369 |
transaction.regie = regie |
... | ... | |
354 | 372 |
transaction.status = 0 |
355 | 373 |
transaction.amount = total_amount |
356 | 374 | |
357 |
payment = get_eopayment_object(request, regie) |
|
375 |
payment = get_eopayment_object(request, regie, single_item)
|
|
358 | 376 |
kwargs = { |
359 | 377 |
'email': email, 'first_name': firstname, 'last_name': lastname |
360 | 378 |
} |
... | ... | |
373 | 391 | |
374 | 392 |
# store the next url in session in order to be able to redirect to |
375 | 393 |
# it if payment is canceled |
394 |
lingo_next_url = request.build_absolute_uri(next_url) |
|
395 |
if not basket: |
|
396 |
lingo_next_url = get_single_item_payment_status_view(single_item.pk, transaction.pk) |
|
397 | ||
376 | 398 |
request.session.setdefault('lingo_next_url', |
377 |
{})[order_id] = request.build_absolute_uri(next_url)
|
|
399 |
{})[order_id] = lingo_next_url
|
|
378 | 400 |
request.session.modified = True |
379 | 401 | |
380 | 402 |
if kind == eopayment.URL: |
... | ... | |
433 | 455 |
return self.handle_payment(request, regie, items, remote_items, next_url, email) |
434 | 456 | |
435 | 457 | |
458 |
def get_single_item_payment_status_view(item_id, transaction_id=None): |
|
459 |
url = reverse( |
|
460 |
'basket-item-payment-status', |
|
461 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(item_id))} |
|
462 |
) |
|
463 |
if transaction_id: |
|
464 |
url = "%s?transaction-id=%s" % ( |
|
465 |
url, aes_hex_encrypt(settings.SECRET_KEY, str(transaction_id)) |
|
466 |
) |
|
467 |
return url |
|
468 | ||
469 | ||
436 | 470 |
class BasketItemPayView(PayMixin, View): |
471 | ||
437 | 472 |
def get(self, request, *args, **kwargs): |
438 | 473 |
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.')) |
|
474 |
email = request.GET.get('email', '') |
|
475 |
firstname = request.GET.get('firstname', '') |
|
476 |
lastname = request.GET.get('lastname', '') |
|
441 | 477 | |
442 |
item = BasketItem.objects.get(pk=kwargs['item_id']) |
|
478 |
signature = kwargs.get('signature') |
|
479 |
try: |
|
480 |
item_id = aes_hex_decrypt(settings.SECRET_KEY, signature) |
|
481 |
except DecryptionError: |
|
482 |
return HttpResponseForbidden(_('Invalid payment request.')) |
|
483 | ||
484 |
item = BasketItem.objects.get(pk=item_id) |
|
443 | 485 |
regie = item.regie |
444 | 486 |
if regie.extra_fees_ws_url: |
445 | 487 |
return HttpResponseForbidden(_('No item payment allowed as extra fees set.')) |
446 | 488 | |
447 |
if item.user != request.user: |
|
489 |
if item.user and item.user != request.user:
|
|
448 | 490 |
return HttpResponseForbidden(_('Wrong item: payment not allowed.')) |
449 | 491 | |
450 |
return self.handle_payment(request, regie, [item], [], next_url) |
|
492 |
return self.handle_payment( |
|
493 |
request, regie, [item], [], next_url, email, firstname, lastname, basket=False |
|
494 |
) |
|
451 | 495 | |
452 | 496 | |
453 | 497 |
class PaymentException(Exception): |
... | ... | |
597 | 641 | |
598 | 642 |
def handle_return(self, request, backend_response, **kwargs): |
599 | 643 |
transaction = None |
644 |
basket_item_id = request.GET.get('item-id') |
|
600 | 645 |
try: |
601 | 646 |
transaction = self.handle_response(request, backend_response, **kwargs) |
647 |
messages.error(request, _('We are sorry but the payment service ' |
|
648 |
'failed to provide a correct answer.')) |
|
649 | ||
602 | 650 |
except UnsignedPaymentException as e: |
603 | 651 |
# some payment backends do not sign return URLs, don't mark this as |
604 | 652 |
# an error, they will provide a notification to the callback |
... | ... | |
607 | 655 |
except PaymentException as e: |
608 | 656 |
messages.error(request, _('We are sorry but the payment service ' |
609 | 657 |
'failed to provide a correct answer.')) |
610 |
return HttpResponseRedirect(get_basket_url()) |
|
658 |
url = get_basket_url() |
|
659 |
if basket_item_id: |
|
660 |
url = get_single_item_payment_status_view(basket_item_id) |
|
661 |
return HttpResponseRedirect(url) |
|
611 | 662 | |
612 | 663 |
if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED): |
613 | 664 |
messages.info(request, transaction.regie.get_text_on_success()) |
... | ... | |
762 | 813 |
return HttpResponseRedirect(url) |
763 | 814 |
messages.warning(request, msg) |
764 | 815 |
return HttpResponseRedirect(request.GET.get('page_path') or '/') |
816 | ||
817 | ||
818 |
class BasketItemPaymentStatusView(View): |
|
819 | ||
820 |
http_method_names = ['get'] |
|
821 | ||
822 |
def get(self, request, *args, **kwargs): |
|
823 |
page = Page() |
|
824 |
page.template_name = 'standard' |
|
825 |
template_name = 'lingo/combo/item-wait-payment.html' |
|
826 |
signature = kwargs.get('signature') |
|
827 | ||
828 |
try: |
|
829 |
item_id = aes_hex_decrypt(settings.SECRET_KEY, signature) |
|
830 |
except DecryptionError: |
|
831 |
return HttpResponseForbidden(_('Invalid item signature.')) |
|
832 | ||
833 |
try: |
|
834 |
item = BasketItem.objects.get( |
|
835 |
pk=item_id, cancellation_date__isnull=True |
|
836 |
) |
|
837 |
except BasketItem.DoesNotExist: |
|
838 |
return HttpResponseForbidden(_('Invalid basket item')) |
|
839 | ||
840 |
transaction = None |
|
841 |
if 'transaction-id' in request.GET: |
|
842 |
try: |
|
843 |
transaction_id = aes_hex_decrypt( |
|
844 |
settings.SECRET_KEY, request.GET.get('transaction-id') |
|
845 |
) |
|
846 |
except DecryptionError: |
|
847 |
return HttpResponseForbidden(_('Invalid transaction signature.')) |
|
848 |
try: |
|
849 |
transaction = Transaction.objects.get(pk=transaction_id) |
|
850 |
except Transaction.DoesNotExist: |
|
851 |
return HttpResponseForbidden(_('Invalid transaction.')) |
|
852 | ||
853 |
if transaction and transaction.is_paid(): |
|
854 |
return HttpResponseRedirect(item.source_url) |
|
855 | ||
856 |
extra_context_data = getattr(request, 'extra_context_data', {}) |
|
857 |
extra_context_data['transaction_id'] = '' |
|
858 |
if transaction: |
|
859 |
extra_context_data['transaction_id'] = \ |
|
860 |
aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk)) |
|
861 |
extra_context_data['source_url'] = item.source_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 |
signature = kwargs.get('signature') |
|
872 |
try: |
|
873 |
transaction_id = aes_hex_decrypt(settings.SECRET_KEY, signature) |
|
874 |
except DecryptionError: |
|
875 |
return HttpResponseForbidden(_('Invalid transaction.')) |
|
876 | ||
877 |
try: |
|
878 |
transaction = Transaction.objects.get(pk=transaction_id) |
|
879 |
except Transaction.DoesNotExist: |
|
880 |
return HttpResponseForbidden(_('Invalid 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 |
msg = _('Wait a moment, we are waiting for bank notification') |
|
890 |
transaction_error = False |
|
891 |
redirect = False |
|
892 | ||
893 |
if transaction.is_paid(): |
|
894 |
redirect = True |
|
895 |
msg = _('Paiment received') |
|
896 | ||
897 |
elif transaction.status in ( |
|
898 |
eopayment.CANCELLED, eopayment.ERROR, eopayment.DENIED, EXPIRED |
|
899 |
): |
|
900 |
transaction_error = True |
|
901 |
msg = _('Transaction error, you can go back to your form and make another payment') |
|
902 | ||
903 |
return JsonResponse( |
|
904 |
{'transaction_error': transaction_error, 'redirect': redirect, 'msg': msg} |
|
905 |
) |
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, item_id, 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 |
url, part = os.path.split(url) |
|
136 |
assert part == 'payment-status' |
|
137 |
url, part = os.path.split(url) |
|
138 |
assert aes_hex_decrypt(settings.SECRET_KEY, part) == str(item_id) |
|
139 |
url, part = os.path.split(url) |
|
140 |
assert part == 'item' |
|
141 |
url, part = os.path.split(url) |
|
142 |
assert part == 'lingo' |
|
143 | ||
144 | ||
123 | 145 |
def test_default_regie(): |
124 | 146 |
payment_backend = PaymentBackend.objects.create(label='foo', slug='foo') |
125 | 147 |
Regie.objects.all().delete() |
... | ... | |
297 | 319 |
assert resp.status_code == 200 |
298 | 320 |
response = json.loads(resp.text) |
299 | 321 |
assert response['result'] == 'success' |
300 |
assert response['payment_url'].endswith('/lingo/item/%s/pay' % item.id) |
|
322 |
payment_url = urlparse.urlparse(response['payment_url']) |
|
323 |
assert payment_url.path.startswith('/lingo/item/') |
|
324 |
assert payment_url.path.endswith('/pay') |
|
301 | 325 |
assert BasketItem.objects.filter(amount=Decimal('22.23')).exists() |
302 | 326 |
assert BasketItem.objects.filter(amount=Decimal('22.23'))[0].regie_id == other_regie.id |
303 | 327 | |
... | ... | |
417 | 441 |
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists() |
418 | 442 |
payment_url = resp.json['payment_url'] |
419 | 443 |
resp = app.get(payment_url, status=403) |
420 |
assert 'No item payment allowed for anonymous users.' in resp.text
|
|
444 |
assert 'Wrong item: payment not allowed.' in resp.text
|
|
421 | 445 | |
422 | 446 |
login(app, username='john.doe', password='john.doe') |
423 | 447 |
resp = app.get(payment_url, status=403) |
... | ... | |
440 | 464 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
441 | 465 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
442 | 466 |
assert qs['amount'] == ['12.00'] |
443 | ||
444 | 467 |
# simulate successful payment response from dummy backend |
445 | 468 |
data = {'transaction_id': qs['transaction_id'][0], 'ok': True, |
446 | 469 |
'amount': qs['amount'][0], 'signed': True} |
... | ... | |
448 | 471 |
# dummy module put that URL in return_url query string parameter). |
449 | 472 |
resp = app.get(qs['return_url'][0], params=data) |
450 | 473 |
# 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/' |
|
474 |
item = BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).first() |
|
475 |
# check that user is redirected to the item payment status view |
|
476 |
assert_payment_status(resp.location, item.pk, transaction_id=item.transaction_set.last().pk) |
|
477 | ||
454 | 478 | |
455 | 479 |
def test_pay_multiple_regies(app, key, regie, user): |
456 | 480 |
test_add_amount_to_basket(app, key, regie, user) |
... | ... | |
995 | 1019 |
assert url.startswith('http://example.org/testitem/jump/trigger/paid') |
996 | 1020 |
assert BasketItem.objects.get(id=item.id).payment_date |
997 | 1021 |
assert BasketItem.objects.get(id=item.id).notification_date |
1022 | ||
1023 | ||
1024 |
@pytest.mark.parametrize("authenticated", [True, False]) |
|
1025 |
def test_payment_no_basket(app, user, regie, authenticated): |
|
1026 |
url = reverse('api-add-basket-item') |
|
1027 |
source_url = 'http://example.org/item/1' |
|
1028 |
data = {'amount': 10, 'display_name': 'test item', 'url': source_url} |
|
1029 |
if authenticated: |
|
1030 |
data['email'] = user.email |
|
1031 |
url = sign_url(url, settings.LINGO_API_SIGN_KEY) |
|
1032 |
resp = app.post_json(url, params=data) |
|
1033 |
assert resp.status_code == 200 |
|
1034 |
payment_url = resp.json['payment_url'] |
|
1035 | ||
1036 |
item = BasketItem.objects.first() |
|
1037 |
assert item.user is None |
|
1038 |
assert item.amount == Decimal('10.00') |
|
1039 |
path = urlparse.urlparse(payment_url).path |
|
1040 |
start = '/lingo/item/' |
|
1041 |
end = '/pay' |
|
1042 |
assert path.startswith(start) |
|
1043 |
assert path.endswith(end) |
|
1044 |
signature = path.replace(start, '').replace(end, '') |
|
1045 |
assert aes_hex_decrypt(settings.SECRET_KEY, signature) == str(item.id) |
|
1046 | ||
1047 |
if authenticated: |
|
1048 |
app = login(app) |
|
1049 | ||
1050 |
# payment error due to too small amount |
|
1051 |
item.amount = Decimal('1.00') |
|
1052 |
item.save() |
|
1053 |
resp = app.get(payment_url) |
|
1054 |
assert_payment_status(resp.location, item.pk) |
|
1055 |
resp = resp.follow() |
|
1056 |
assert 'Minimal payment amount is 4.50' in resp.text |
|
1057 |
# we can go back to form |
|
1058 |
assert source_url in resp.text |
|
1059 | ||
1060 |
# amount ok, redirection to payment backend |
|
1061 |
item.amount = Decimal('10.00') |
|
1062 |
item.save() |
|
1063 |
resp = app.get(payment_url) |
|
1064 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
|
1065 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
|
1066 |
assert qs['amount'] == ['10.00'] |
|
1067 |
if authenticated: |
|
1068 |
assert qs['email'] == ['foo@example.com'] |
|
1069 |
else: |
|
1070 |
assert 'email' not in qs |
|
1071 | ||
1072 |
# mail get be specified here for anonymous user |
|
1073 |
resp = app.get( |
|
1074 |
payment_url, |
|
1075 |
params={ |
|
1076 |
'email': 'foo@localhost', |
|
1077 |
} |
|
1078 |
) |
|
1079 |
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/') |
|
1080 |
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query) |
|
1081 |
assert qs['amount'] == ['10.00'] |
|
1082 |
if authenticated: |
|
1083 |
assert qs['email'] == ['foo@example.com'] |
|
1084 |
else: |
|
1085 |
assert qs['email'] == ['foo@localhost'] |
|
1086 | ||
1087 |
# simulate bad responseform payment backend, no transaction id |
|
1088 |
data = {'amount': qs['amount'][0], 'signed': True} |
|
1089 |
return_url = qs['return_url'][0] |
|
1090 |
assert return_url.endswith('?item-id=%s' % item.pk) |
|
1091 |
resp = app.get(return_url, params=data) |
|
1092 |
assert_payment_status(resp.location, item.pk) |
|
1093 |
resp = resp.follow() |
|
1094 |
assert 'We are sorry but the payment service failed to provide a correct answer.' in resp.text |
|
1095 |
assert 'http://example.org/item/1' in resp.text |
|
1096 |
# check that item is not paid |
|
1097 |
item = BasketItem.objects.get(pk=item.pk) |
|
1098 |
assert not item.payment_date |
|
1099 | ||
1100 |
# simulate successful payment response from dummy backend |
|
1101 |
data = { |
|
1102 |
'transaction_id': qs['transaction_id'][0], 'ok': True, |
|
1103 |
'amount': qs['amount'][0], 'signed': True |
|
1104 |
} |
|
1105 |
return_url = qs['return_url'][0] |
|
1106 |
assert return_url.endswith('?item-id=%s' % item.pk) |
|
1107 |
resp = app.get(return_url, params=data) |
|
1108 |
assert_payment_status(resp.location, item.pk, transaction_id=item.transaction_set.last().pk) |
|
1109 |
# check that item is paid |
|
1110 |
item = BasketItem.objects.get(pk=item.pk) |
|
1111 |
assert item.payment_date |
|
1112 |
# accept redirection to item payment status view |
|
1113 |
resp = resp.follow() |
|
1114 |
# which should it self redirect to item.source_url as it is paid |
|
1115 |
assert resp.location == source_url |
|
1116 | ||
1117 | ||
1118 |
def test_transaction_status_api(app, regie, user): |
|
1119 |
# invalid transaction signature |
|
1120 |
url = reverse('api-transaction-status', kwargs={'signature': 'xxxx'}) |
|
1121 |
resp = app.get(url, status=403) |
|
1122 |
assert 'Invalid transaction.' in resp.text |
|
1123 | ||
1124 |
# unkown transaction identifier |
|
1125 |
transaction_id = 1000 |
|
1126 |
url = reverse( |
|
1127 |
'api-transaction-status', |
|
1128 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction_id))} |
|
1129 |
) |
|
1130 |
resp = app.get(url, status=403) |
|
1131 |
assert 'Invalid transaction.' in resp.text |
|
1132 | ||
1133 |
wait_response = { |
|
1134 |
'transaction_error': False, |
|
1135 |
'redirect': False, |
|
1136 |
'msg': 'Wait a moment, we are waiting for bank notification' |
|
1137 |
} |
|
1138 | ||
1139 |
# anonymous user on anonymous transaction: OK |
|
1140 |
transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0) |
|
1141 |
url = reverse( |
|
1142 |
'api-transaction-status', |
|
1143 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1144 |
) |
|
1145 |
resp = app.get(url) |
|
1146 |
assert resp.json == wait_response |
|
1147 | ||
1148 |
# authenticated 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={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1153 |
) |
|
1154 |
resp = login(app).get(url) |
|
1155 |
assert resp.json == wait_response |
|
1156 |
app.reset() |
|
1157 | ||
1158 |
# authenticated user on his transaction: OK |
|
1159 |
transaction = Transaction.objects.create( |
|
1160 |
amount=Decimal('10.0'), regie=regie, status=0, user=user) |
|
1161 |
url = reverse( |
|
1162 |
'api-transaction-status', |
|
1163 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1164 |
) |
|
1165 |
resp = login(app).get(url) |
|
1166 |
assert resp.json == wait_response |
|
1167 |
app.reset() |
|
1168 | ||
1169 |
# anonymous user on other user transaction transaction: NOTOK |
|
1170 |
transaction = Transaction.objects.create( |
|
1171 |
amount=Decimal('10.0'), regie=regie, status=0, user=user) |
|
1172 |
url = reverse( |
|
1173 |
'api-transaction-status', |
|
1174 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1175 |
) |
|
1176 |
resp = app.get(url, status=403) |
|
1177 |
assert 'Transaction does not belong to the requesting user' in resp.text |
|
1178 | ||
1179 |
# authenticated user on other user transaction transaction: NOTOK |
|
1180 |
user2 = User.objects.create_user( |
|
1181 |
'user2', password='user2', email='user2@example.com' |
|
1182 |
) |
|
1183 |
transaction = Transaction.objects.create( |
|
1184 |
amount=Decimal('10.0'), regie=regie, status=0, user=user2) |
|
1185 |
url = reverse( |
|
1186 |
'api-transaction-status', |
|
1187 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1188 |
) |
|
1189 |
resp = login(app).get(url, status=403) |
|
1190 |
assert 'Transaction does not belong to the requesting user' in resp.text |
|
1191 |
app.reset() |
|
1192 | ||
1193 |
# transaction error |
|
1194 |
transaction = Transaction.objects.create( |
|
1195 |
amount=Decimal('10.0'), regie=regie, status=eopayment.ERROR |
|
1196 |
) |
|
1197 |
url = reverse( |
|
1198 |
'api-transaction-status', |
|
1199 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1200 |
) |
|
1201 |
resp = app.get(url) |
|
1202 |
assert resp.json == { |
|
1203 |
'transaction_error': True, |
|
1204 |
'redirect': False, |
|
1205 |
'msg': 'Transaction error, you can go back to your form and make another payment' |
|
1206 |
} |
|
1207 | ||
1208 |
# transaction paid |
|
1209 |
transaction = Transaction.objects.create( |
|
1210 |
amount=Decimal('10.0'), regie=regie, status=eopayment.PAID |
|
1211 |
) |
|
1212 |
url = reverse( |
|
1213 |
'api-transaction-status', |
|
1214 |
kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))} |
|
1215 |
) |
|
1216 |
resp = app.get(url) |
|
1217 |
assert resp.json == { |
|
1218 |
'transaction_error': False, |
|
1219 |
'redirect': True, |
|
1220 |
'msg': 'Paiment received' |
|
1221 |
} |
|
998 |
- |