summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEmmanuel Cazenave <ecazenave@entrouvert.com>2019-12-24 12:42:47 (GMT)
committerEmmanuel Cazenave <ecazenave@entrouvert.com>2020-01-09 14:45:54 (GMT)
commit2ad46518a97e470bb605a372c2747001e54380c5 (patch)
tree362627f6a6963af6e08935d94e72da69edf35b17
parent1ec54db6ae42730fcb898986631e945827fef2a6 (diff)
downloadcombo-wip/36876-payment-wait-for-all-clean.zip
combo-wip/36876-payment-wait-for-all-clean.tar.gz
combo-wip/36876-payment-wait-for-all-clean.tar.bz2
lingo: support anonymous and no basket payment (#36876)wip/36876-payment-wait-for-all-clean
-rw-r--r--combo/apps/lingo/templates/lingo/combo/payment-status.html53
-rw-r--r--combo/apps/lingo/urls.py11
-rw-r--r--combo/apps/lingo/views.py200
-rw-r--r--tests/test_lingo_payment.py250
-rw-r--r--tests/test_lingo_remote_regie.py18
5 files changed, 484 insertions, 48 deletions
diff --git a/combo/apps/lingo/templates/lingo/combo/payment-status.html b/combo/apps/lingo/templates/lingo/combo/payment-status.html
new file mode 100644
index 0000000..d362130
--- /dev/null
+++ b/combo/apps/lingo/templates/lingo/combo/payment-status.html
@@ -0,0 +1,53 @@
+{% extends "combo/page_template.html" %}
+{% load staticfiles i18n %}
+
+{% block combo-content %}
+{% block wait-js %}
+<script>
+function display_error(message) {
+ $('#transaction-error').text(message);
+ $('#transaction-error').show();
+ $("#wait-msg").hide();
+}
+
+$(function() {
+ var next_url = '{{next_url}}';
+ var transaction_id = '{{transaction_id}}';
+ if (transaction_id === "") {
+ display_error($('#transaction-error').data('error'));
+ }
+ else {
+ $.ajax({
+ url: `/api/lingo/transaction-status/${transaction_id}/`,
+ success: function(data, status) {
+ if (!data.wait) {
+ $('#wait-msg').text($('#wait-msg').data('continue'))
+ // wait a little to show messages
+ setTimeout(function(){location.href=next_url}, 3000);
+ } else if (data.error) {
+ display_error(data.error_msg)
+ } else {
+ setTimeout(wait_payment, 3000, next_url, transaction_id);
+ }
+ },
+ error: function(error) {
+ display_error($('#transaction-status').data('error'));
+ window.console && console.log(':(', error);
+ }
+ });
+ }
+});
+</script>
+{% endblock %}
+
+{% block wait-content%}
+<div>
+ {% block wait-message %}
+ <h2 id="wait-msg" data-continue="{% trans "Wait a moment or click on 'Continue'." %}">{% trans "Please wait while your request is being processed..." %}</h2>
+ {% endblock %}
+ <div id="transaction-error" class="errornotice" data-error="{% trans 'An error occured' %}" style="display: none;"></div>
+ <p><a id="next-url" href="{{next_url}}">{% trans "Continue" %}</a></p>
+ </p>
+</div>
+{% endblock %}
+{% endblock %}
diff --git a/combo/apps/lingo/urls.py b/combo/apps/lingo/urls.py
index 902d895..92ce5c0 100644
--- a/combo/apps/lingo/urls.py
+++ b/combo/apps/lingo/urls.py
@@ -21,7 +21,8 @@ from combo.urls_utils import decorated_includes, manager_required
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
ReturnView, ItemDownloadView, ItemView, CancelItemView,
RemoveBasketItemApiView, ValidateTransactionApiView,
- CancelTransactionApiView, SelfInvoiceView, BasketItemPayView)
+ CancelTransactionApiView, SelfInvoiceView, BasketItemPayView,
+ TransactionStatusApiView, PaymentStatusView)
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
RegieDeleteView, TransactionListView, BasketItemErrorListView,
download_transactions_csv, PaymentBackendListView,
@@ -60,6 +61,10 @@ urlpatterns = [
name='api-validate-transaction'),
url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(),
name='api-cancel-transaction'),
+ url(
+ '^api/lingo/transaction-status/(?P<signature>\w+)/$', TransactionStatusApiView.as_view(),
+ name='api-transaction-status'
+ ),
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'),
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),
@@ -74,8 +79,10 @@ urlpatterns = [
ItemDownloadView.as_view(), name='download-item-pdf'),
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$',
ItemView.as_view(), name='view-item'),
- url(r'^lingo/item/(?P<item_id>\d+)/pay$',
+ url(r'^lingo/item/(?P<signature>\w+)/pay$',
BasketItemPayView.as_view(), name='basket-item-pay-view'),
+ url(r'^lingo/payment-status/(?P<signature>\w+)/$',
+ PaymentStatusView.as_view(), name='payment-status'),
url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(),
name='lingo-self-invoice'),
]
diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py
index bb90d4b..e343b7f 100644
--- a/combo/apps/lingo/views.py
+++ b/combo/apps/lingo/views.py
@@ -19,11 +19,12 @@ from decimal import Decimal, ROUND_HALF_UP
import json
import logging
import requests
+import uuid
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.urlresolvers import reverse
-from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
+from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect, HttpResponseBadRequest
from django.http import HttpResponseForbidden, Http404, JsonResponse
from django.template.response import TemplateResponse
from django.utils import timezone, dateparse, six
@@ -39,24 +40,30 @@ from django.utils.encoding import smart_text
import eopayment
from combo.data.models import Page
-from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError
+from combo.utils import check_request_signature, aes_hex_decrypt, aes_hex_encrypt, DecryptionError
from combo.profile.utils import get_user_from_name_id
+from combo.public.views import publish_page
from .models import (Regie, BasketItem, Transaction, TransactionOperation,
- LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend)
+ LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED)
-def get_eopayment_object(request, regie_or_payment_backend):
+
+def get_eopayment_object(request, regie_or_payment_backend, payment_uuid=None):
payment_backend = regie_or_payment_backend
if isinstance(regie_or_payment_backend, Regie):
payment_backend = regie_or_payment_backend.payment_backend
options = payment_backend.service_options
+ normal_return_url = reverse(
+ 'lingo-return-payment-backend',
+ kwargs={'payment_backend_pk': payment_backend.id}
+ )
+ if payment_uuid:
+ normal_return_url = "%s?publik-payment=%s" % (normal_return_url, payment_uuid)
options.update({
'automatic_return_url': request.build_absolute_uri(
reverse('lingo-callback-payment-backend',
kwargs={'payment_backend_pk': payment_backend.id})),
- 'normal_return_url': request.build_absolute_uri(
- reverse('lingo-return-payment-backend',
- kwargs={'payment_backend_pk': payment_backend.id})),
+ 'normal_return_url': request.build_absolute_uri(normal_return_url)
})
return eopayment.Payment(payment_backend.service, options)
@@ -150,7 +157,7 @@ class AddBasketItemApiView(View):
elif request.GET.get('email'):
user = User.objects.get(email=request.GET.get('email'))
else:
- raise Exception('no user specified')
+ user = None
except User.DoesNotExist:
raise Exception('unknown user')
@@ -192,9 +199,14 @@ class AddBasketItemApiView(View):
'Bad format for capture date, it should be yyyy-mm-dd.')
item.save()
- item.regie.compute_extra_fees(user=item.user)
+ if user:
+ item.regie.compute_extra_fees(user=item.user)
- payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id})
+ payment_url = reverse(
+ 'basket-item-pay-view',
+ kwargs={
+ 'signature': aes_hex_encrypt(settings.SECRET_KEY, str(item.id))
+ })
return JsonResponse({'result': 'success', 'id': str(item.id),
'payment_url': request.build_absolute_uri(payment_url)})
@@ -321,7 +333,17 @@ class CancelTransactionApiView(View):
class PayMixin(object):
@atomic
- def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''):
+ def handle_payment(
+ self, request, regie, items, remote_items, next_url='/', email='', firstname='',
+ lastname=''):
+
+ payment_uuid = str(uuid.uuid4())
+ request.session.setdefault('payment', {}).setdefault(payment_uuid, {})['items'] = [
+ item.pk for item in items
+ ]
+ request.session['payment'][payment_uuid]['next_url'] = next_url
+ request.session.modified = True
+
if remote_items:
total_amount = sum([x.amount for x in remote_items])
else:
@@ -329,7 +351,7 @@ class PayMixin(object):
if total_amount < regie.payment_min_amount:
messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount)
- return HttpResponseRedirect(next_url)
+ return HttpResponseRedirect(get_payment_status_view(payment_uuid))
for item in items:
if item.regie != regie:
@@ -344,8 +366,6 @@ class PayMixin(object):
lastname = user.last_name
else:
transaction.user = None
- firstname = ''
- lastname = ''
transaction.save()
transaction.regie = regie
@@ -354,7 +374,7 @@ class PayMixin(object):
transaction.status = 0
transaction.amount = total_amount
- payment = get_eopayment_object(request, regie)
+ payment = get_eopayment_object(request, regie, payment_uuid)
kwargs = {
'email': email, 'first_name': firstname, 'last_name': lastname
}
@@ -371,12 +391,6 @@ class PayMixin(object):
transaction.order_id = order_id
transaction.save()
- # store the next url in session in order to be able to redirect to
- # it if payment is canceled
- request.session.setdefault('lingo_next_url',
- {})[order_id] = request.build_absolute_uri(next_url)
- request.session.modified = True
-
if kind == eopayment.URL:
return HttpResponseRedirect(data)
elif kind == eopayment.FORM:
@@ -433,21 +447,44 @@ class PayView(PayMixin, View):
return self.handle_payment(request, regie, items, remote_items, next_url, email)
+def get_payment_status_view(transaction_uuid, transaction_id=None):
+ url = reverse(
+ 'payment-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction_uuid))}
+ )
+ if transaction_id:
+ url = "%s?transaction-id=%s" % (
+ url, aes_hex_encrypt(settings.SECRET_KEY, str(transaction_id))
+ )
+ return url
+
+
class BasketItemPayView(PayMixin, View):
+
def get(self, request, *args, **kwargs):
- next_url = request.GET.get('next_url') or '/'
- if not (request.user and request.user.is_authenticated):
- return HttpResponseForbidden(_('No item payment allowed for anonymous users.'))
+ next_url = request.GET.get('next_url') or None
+ email = request.GET.get('email', '')
+ firstname = request.GET.get('firstname', '')
+ lastname = request.GET.get('lastname', '')
- item = BasketItem.objects.get(pk=kwargs['item_id'])
+ signature = kwargs.get('signature')
+ try:
+ item_id = aes_hex_decrypt(settings.SECRET_KEY, signature)
+ except DecryptionError:
+ return HttpResponseForbidden(_('Invalid payment request.'))
+
+ item = BasketItem.objects.get(pk=item_id)
regie = item.regie
if regie.extra_fees_ws_url:
return HttpResponseForbidden(_('No item payment allowed as extra fees set.'))
- if item.user != request.user:
+ if item.user and item.user != request.user:
return HttpResponseForbidden(_('Wrong item: payment not allowed.'))
- return self.handle_payment(request, regie, [item], [], next_url)
+ return self.handle_payment(
+ request=request, regie=regie, items=[item], remote_items=[], next_url=next_url, email=email,
+ firstname=firstname, lastname=lastname
+ )
class PaymentException(Exception):
@@ -600,25 +637,28 @@ class ReturnView(PaymentView):
def handle_return(self, request, backend_response, **kwargs):
transaction = None
+ payment_uuid = request.GET.get('publik-payment')
try:
transaction = self.handle_response(request, backend_response, **kwargs)
except UnsignedPaymentException as e:
# some payment backends do not sign return URLs, don't mark this as
# an error, they will provide a notification to the callback
# endpoint.
- pass
+ if payment_uuid:
+ return HttpResponseRedirect(get_payment_status_view(payment_uuid, e.transaction.pk))
+
except PaymentException as e:
messages.error(request, _('We are sorry but the payment service '
'failed to provide a correct answer.'))
+ if payment_uuid:
+ return HttpResponseRedirect(get_payment_status_view(payment_uuid))
return HttpResponseRedirect(get_basket_url())
if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED):
messages.info(request, transaction.regie.get_text_on_success())
- if transaction and request.session.get('lingo_next_url'):
- redirect_url = request.session['lingo_next_url'].get(transaction.order_id)
- if redirect_url:
- return HttpResponseRedirect(redirect_url)
+ if transaction and payment_uuid:
+ return HttpResponseRedirect(get_payment_status_view(payment_uuid, transaction.pk))
# return to basket page if there are still items to pay
if request.user.is_authenticated:
@@ -765,3 +805,99 @@ class SelfInvoiceView(View):
return HttpResponseRedirect(url)
messages.warning(request, msg)
return HttpResponseRedirect(request.GET.get('page_path') or '/')
+
+
+class PaymentStatusView(View):
+
+ http_method_names = ['get']
+
+ def get(self, request, *args, **kwargs):
+ page = Page()
+ page.template_name = 'standard'
+ template_name = 'lingo/combo/payment-status.html'
+ signature = kwargs.get('signature')
+
+ try:
+ payment_uuid = aes_hex_decrypt(settings.SECRET_KEY, signature)
+ except DecryptionError:
+ return HttpResponseForbidden(_('Invalid payment signature.'))
+
+ next_url = request.session.get('payment', {}).get(payment_uuid, {}).get('next_url')
+ if not next_url:
+ items = BasketItem.objects.filter(
+ pk__in=request.session.get('payment', {}).get(payment_uuid, {}).get('items', [])
+ )
+ next_url = get_basket_url()
+ if len(set([item.source_url for item in items])) == 1:
+ next_url = items[0].source_url
+ next_url = request.build_absolute_uri(next_url)
+
+ transaction = None
+ if 'transaction-id' in request.GET:
+ try:
+ transaction_id = aes_hex_decrypt(
+ settings.SECRET_KEY, request.GET.get('transaction-id')
+ )
+ except DecryptionError:
+ return HttpResponseForbidden(_('Invalid transaction signature.'))
+ try:
+ transaction = Transaction.objects.get(pk=transaction_id)
+ except Transaction.DoesNotExist:
+ return HttpResponseForbidden(_('Invalid transaction.'))
+
+ extra_context_data = getattr(request, 'extra_context_data', {})
+ extra_context_data['transaction_id'] = ''
+ if transaction:
+ extra_context_data['transaction_id'] = \
+ aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))
+ extra_context_data['next_url'] = next_url
+ request.extra_context_data = extra_context_data
+ return publish_page(request, page, template_name=template_name)
+
+
+class TransactionStatusApiView(View):
+
+ http_method_names = ['get']
+
+ def get(self, request, *args, **kwargs):
+ signature = kwargs.get('signature')
+ try:
+ transaction_id = aes_hex_decrypt(settings.SECRET_KEY, signature)
+ except DecryptionError:
+ return HttpResponseBadRequest(_('Invalid transaction.'))
+ try:
+ transaction = Transaction.objects.get(pk=transaction_id)
+ except Transaction.DoesNotExist:
+ return HttpResponseNotFound(_('Unknown transaction.'))
+
+ user = request.user if request.user.is_authenticated() else None
+ error_msg = _('Transaction does not belong to the requesting user')
+ if user and transaction.user and user != transaction.user:
+ return HttpResponseForbidden(error_msg)
+ if not user and transaction.user:
+ return HttpResponseForbidden(error_msg)
+
+ if transaction.is_paid():
+ data = {
+ 'wait': False,
+ 'error': False,
+ 'error_msg': ''
+ }
+ return JsonResponse(data=data)
+
+ if transaction.status in (
+ eopayment.CANCELLED, eopayment.ERROR, eopayment.DENIED, EXPIRED
+ ):
+ data = {
+ 'wait': True,
+ 'error': True,
+ 'error_msg': _('Payment error, you can continue and make another payment')
+ }
+ return JsonResponse(data=data)
+
+ data = {
+ 'wait': True,
+ 'error': False,
+ 'error_msg': ''
+ }
+ return JsonResponse(data=data)
diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py
index 0a0e152..bef1a8c 100644
--- a/tests/test_lingo_payment.py
+++ b/tests/test_lingo_payment.py
@@ -6,6 +6,7 @@ import urllib
from decimal import Decimal
import json
import mock
+import os.path
from django.apps import apps
from django.contrib.auth.models import User
@@ -22,7 +23,7 @@ from combo.data.models import Page
from combo.apps.lingo.models import (
Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell,
PaymentBackend)
-from combo.utils import sign_url
+from combo.utils import aes_hex_decrypt, aes_hex_encrypt, sign_url
from .test_manager import login
@@ -120,6 +121,20 @@ def key(request, settings):
else:
return settings.LINGO_API_SIGN_KEY
+
+def assert_payment_status(url, transaction_id=None):
+ if hasattr(url, 'path'):
+ url = url.path
+
+ if transaction_id:
+ url, part = url.split('?')
+ assert 'transaction-id' in part
+ signature = part.replace('transaction-id=', '')
+ assert aes_hex_decrypt(settings.SECRET_KEY, signature) == str(transaction_id)
+
+ assert url.startswith('/lingo/payment-status/')
+
+
def test_default_regie():
payment_backend = PaymentBackend.objects.create(label='foo', slug='foo')
Regie.objects.all().delete()
@@ -218,10 +233,14 @@ def test_successfull_items_payment(app, basket_page, regie, user, with_payment_b
assert resp.status_code == 200
# simulate successful return URL
resp = app.get(qs['return_url'][0], params=args)
+ # redirect to payment status
assert resp.status_code == 302
- assert urlparse.urlparse(resp.url).path == '/test_basket_cell/'
+ assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
resp = resp.follow()
assert 'Your payment has been succesfully registered.' in resp.text
+ assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
+ '/test_basket_cell/'
+
def test_add_amount_to_basket(app, key, regie, user):
payment_backend = PaymentBackend.objects.create(
@@ -297,7 +316,9 @@ def test_add_amount_to_basket(app, key, regie, user):
assert resp.status_code == 200
response = json.loads(resp.text)
assert response['result'] == 'success'
- assert response['payment_url'].endswith('/lingo/item/%s/pay' % item.id)
+ payment_url = urlparse.urlparse(response['payment_url'])
+ assert payment_url.path.startswith('/lingo/item/')
+ assert payment_url.path.endswith('/pay')
assert BasketItem.objects.filter(amount=Decimal('22.23')).exists()
assert BasketItem.objects.filter(amount=Decimal('22.23'))[0].regie_id == other_regie.id
@@ -417,7 +438,7 @@ def test_pay_single_basket_item(app, key, regie, user, john_doe):
assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists()
payment_url = resp.json['payment_url']
resp = app.get(payment_url, status=403)
- assert 'No item payment allowed for anonymous users.' in resp.text
+ assert 'Wrong item: payment not allowed.' in resp.text
login(app, username='john.doe', password='john.doe')
resp = app.get(payment_url, status=403)
@@ -440,7 +461,6 @@ def test_pay_single_basket_item(app, key, regie, user, john_doe):
assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
assert qs['amount'] == ['12.00']
-
# simulate successful payment response from dummy backend
data = {'transaction_id': qs['transaction_id'][0], 'ok': True,
'amount': qs['amount'][0], 'signed': True}
@@ -448,9 +468,10 @@ def test_pay_single_basket_item(app, key, regie, user, john_doe):
# dummy module put that URL in return_url query string parameter).
resp = app.get(qs['return_url'][0], params=data)
# check that item is paid
- assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).exists()
- # check that user is redirected to the next_url passed previously
- assert resp.location == 'http://example.net/form/id/'
+ item = BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).first()
+ # check that user is redirected to the item payment status view
+ assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk)
+
def test_pay_multiple_regies(app, key, regie, user):
test_add_amount_to_basket(app, key, regie, user)
@@ -743,6 +764,8 @@ def test_payment_no_callback_just_return(
data = {'transaction_id': transaction_id,
'amount': qs['amount'][0], 'ok': True}
assert data['amount'] == '10.50'
+ return_qs = urlparse.parse_qs(urlparse.urlparse(qs['return_url'][0]).query)
+ publik_payment = return_qs['publik-payment'][0]
# call return with unsigned POST
with check_log(caplog, 'received unsigned payment'):
@@ -768,17 +791,21 @@ def test_payment_no_callback_just_return(
# call return with signed POST
data['signed'] = True
- return_url = get_url(with_payment_backend, 'lingo-return', regie)
+ return_url = get_url(with_payment_backend, 'lingo-return', regie) + '?publik-payment=%s' % publik_payment
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
get_resp = app.post(return_url, params=data)
url = request.call_args[0][1]
assert url.startswith('http://example.org/testitem/jump/trigger/paid')
+ # redirect to payment status
assert get_resp.status_code == 302
- assert urlparse.urlparse(get_resp['location']).path == '/test_basket_cell/'
- resp = app.get(get_resp['Location'])
+ assert urlparse.urlparse(get_resp.url).path.startswith('/lingo/payment-status/')
+ resp = get_resp.follow()
assert 'Your payment has been succesfully registered.' in resp.text
+ assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
+ '/test_basket_cell/'
assert Transaction.objects.get(order_id=transaction_id).status == eopayment.PAID
+
def test_transaction_expiration():
t1 = Transaction(status=0)
t1.save()
@@ -995,3 +1022,204 @@ def test_payment_callback_error(app, basket_page, regie, user, with_payment_back
assert url.startswith('http://example.org/testitem/jump/trigger/paid')
assert BasketItem.objects.get(id=item.id).payment_date
assert BasketItem.objects.get(id=item.id).notification_date
+
+
+@pytest.mark.parametrize("authenticated", [True, False])
+def test_payment_no_basket(app, user, regie, authenticated):
+ url = reverse('api-add-basket-item')
+ source_url = 'http://example.org/item/1'
+ data = {'amount': 10, 'display_name': 'test item', 'url': source_url}
+ if authenticated:
+ data['email'] = user.email
+ url = sign_url(url, settings.LINGO_API_SIGN_KEY)
+ resp = app.post_json(url, params=data)
+ assert resp.status_code == 200
+ payment_url = resp.json['payment_url']
+
+ item = BasketItem.objects.first()
+ assert item.user is None
+ assert item.amount == Decimal('10.00')
+ path = urlparse.urlparse(payment_url).path
+ start = '/lingo/item/'
+ end = '/pay'
+ assert path.startswith(start)
+ assert path.endswith(end)
+ signature = path.replace(start, '').replace(end, '')
+ assert aes_hex_decrypt(settings.SECRET_KEY, signature) == str(item.id)
+
+ if authenticated:
+ app = login(app)
+
+ # payment error due to too small amount
+ item.amount = Decimal('1.00')
+ item.save()
+ resp = app.get(payment_url)
+ assert_payment_status(resp.location)
+ resp = resp.follow()
+ assert 'Minimal payment amount is 4.50' in resp.text
+ # we can go back to form
+ assert source_url in resp.text
+
+ # amount ok, redirection to payment backend
+ item.amount = Decimal('10.00')
+ item.save()
+ resp = app.get(payment_url)
+ assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
+ qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
+ assert qs['amount'] == ['10.00']
+ if authenticated:
+ assert qs['email'] == ['foo@example.com']
+ else:
+ assert 'email' not in qs
+
+ # mail get be specified here for anonymous user
+ resp = app.get(
+ payment_url,
+ params={
+ 'email': 'foo@localhost',
+ }
+ )
+ assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
+ qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
+ assert qs['amount'] == ['10.00']
+ if authenticated:
+ assert qs['email'] == ['foo@example.com']
+ else:
+ assert qs['email'] == ['foo@localhost']
+
+ # simulate bad responseform payment backend, no transaction id
+ data = {'amount': qs['amount'][0], 'signed': True}
+ return_url = qs['return_url'][0]
+ assert 'publik-payment' in return_url
+ resp = app.get(return_url, params=data)
+ assert_payment_status(resp.location)
+ resp = resp.follow()
+ assert 'We are sorry but the payment service failed to provide a correct answer.' in resp.text
+ assert 'http://example.org/item/1' in resp.text
+ # check that item is not paid
+ item = BasketItem.objects.get(pk=item.pk)
+ assert not item.payment_date
+
+ # simulate successful payment response from dummy backend
+ data = {
+ 'transaction_id': qs['transaction_id'][0], 'ok': True,
+ 'amount': qs['amount'][0], 'signed': True
+ }
+ return_url = qs['return_url'][0]
+ assert 'publik-payment' in return_url
+ resp = app.get(return_url, params=data)
+ assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk)
+ # check that item is paid
+ item = BasketItem.objects.get(pk=item.pk)
+ assert item.payment_date
+ # accept redirection to item payment status view
+ resp = resp.follow()
+ # which should it self redirect to item.source_url as it is paid
+ assert 'Please wait while your request is being processed' in resp.text
+ assert source_url in resp.text
+
+
+def test_transaction_status_api(app, regie, user):
+ # invalid transaction signature
+ url = reverse('api-transaction-status', kwargs={'signature': 'xxxx'})
+ resp = app.get(url, status=400)
+ assert 'Invalid transaction.' in resp.text
+
+ # unkown transaction identifier
+ transaction_id = 1000
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction_id))}
+ )
+ resp = app.get(url, status=404)
+ assert 'Unknown transaction.' in resp.text
+
+ wait_response = {
+ 'wait': True,
+ 'error': False,
+ 'error_msg': ''
+ }
+ # anonymous user on anonymous transaction: OK
+ transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0)
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = app.get(url)
+ assert resp.json == wait_response
+
+ # authenticated user on anonymous transaction: OK
+ transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0)
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = login(app).get(url)
+ assert resp.json == wait_response
+ app.reset()
+
+ # authenticated user on his transaction: OK
+ transaction = Transaction.objects.create(
+ amount=Decimal('10.0'), regie=regie, status=0, user=user)
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = login(app).get(url)
+ assert resp.json == wait_response
+ app.reset()
+
+ error_msg = 'Transaction does not belong to the requesting user'
+ # anonymous user on other user transaction transaction: NOTOK
+ transaction = Transaction.objects.create(
+ amount=Decimal('10.0'), regie=regie, status=0, user=user)
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = app.get(url, status=403)
+ assert error_msg in resp.text
+
+ # authenticated user on other user transaction transaction: NOTOK
+ user2 = User.objects.create_user(
+ 'user2', password='user2', email='user2@example.com'
+ )
+ transaction = Transaction.objects.create(
+ amount=Decimal('10.0'), regie=regie, status=0, user=user2)
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = login(app).get(url, status=403)
+ assert error_msg in resp.text
+ app.reset()
+
+ # transaction error
+ transaction = Transaction.objects.create(
+ amount=Decimal('10.0'), regie=regie, status=eopayment.ERROR
+ )
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = app.get(url)
+ assert resp.json == {
+ 'wait': True,
+ 'error': True,
+ 'error_msg': 'Payment error, you can continue and make another payment'
+ }
+
+ # transaction paid
+ transaction = Transaction.objects.create(
+ amount=Decimal('10.0'), regie=regie, status=eopayment.PAID
+ )
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
+ )
+ resp = app.get(url)
+ assert resp.json == {
+ 'wait': False,
+ 'error': False,
+ 'error_msg': ''
+ }
diff --git a/tests/test_lingo_remote_regie.py b/tests/test_lingo_remote_regie.py
index d4e2c08..32ce967 100644
--- a/tests/test_lingo_remote_regie.py
+++ b/tests/test_lingo_remote_regie.py
@@ -211,8 +211,11 @@ def test_anonymous_successful_item_payment(mock_get, mock_pay_invoice, app, remo
kwargs={'payment_backend_pk': remote_regie.payment_backend.id}))
# simulate successful return URL
resp = app.get(qs['return_url'][0], params=args)
+ # redirect to payment status
assert resp.status_code == 302
- assert urlparse.urlparse(resp.url).path == '/'
+ assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
+ resp = resp.follow()
+ assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == '/'
# simulate successful call to callback URL
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args)
trans = Transaction.objects.all()
@@ -335,8 +338,13 @@ def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app,
# simulate payment failure
mock_get.side_effect = ConnectionError('where is my hostname?')
resp = app.get(qs['return_url'][0], params=args)
+ # redirect to payment status
assert resp.status_code == 302
- assert urlparse.urlparse(resp.url).path == '/active-remote-invoices-page/'
+ assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
+ resp = resp.follow()
+ assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
+ '/active-remote-invoices-page/'
+
# simulate successful call to callback URL
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args)
trans = Transaction.objects.all()
@@ -385,8 +393,12 @@ def test_remote_invoice_successfull_payment_redirect(mock_get, mock_pay_invoice,
'ok': True, 'reason': 'Paid'}
resp = app.get(qs['return_url'][0], params=args)
+ # redirect to payment status
assert resp.status_code == 302
- assert urlparse.urlparse(resp.location).path == '/active-remote-invoices-page/'
+ assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
+ resp = resp.follow()
+ assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
+ '/active-remote-invoices-page/'
@mock.patch('combo.apps.lingo.models.UserSAMLIdentifier')