From af2d522a5e9c4a0143a12e5a83ec0cafcbd6e941 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Thu, 19 Dec 2019 13:30:12 +0100
Subject: [PATCH] lingo: support anonymous and no basket payment (#36876)

---
 .../lingo/combo/item-wait-payment.html        |  51 ++++
 combo/apps/lingo/urls.py                      |  11 +-
 combo/apps/lingo/views.py                     | 186 ++++++++++++--
 tests/test_lingo_payment.py                   | 239 +++++++++++++++++-
 4 files changed, 456 insertions(+), 31 deletions(-)
 create mode 100644 combo/apps/lingo/templates/lingo/combo/item-wait-payment.html

diff --git a/combo/apps/lingo/templates/lingo/combo/item-wait-payment.html b/combo/apps/lingo/templates/lingo/combo/item-wait-payment.html
new file mode 100644
index 0000000..bd6aecc
--- /dev/null
+++ b/combo/apps/lingo/templates/lingo/combo/item-wait-payment.html
@@ -0,0 +1,51 @@
+{% 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 source_url = '{{source_url}}';
+    var transaction_id = '{{transaction_id}}';
+    if (transaction_id === "") {
+        display_error($('#transaction-status').data('error'));
+    }
+    else {
+        $.ajax({
+            url: `/api/lingo/transaction-status/${transaction_id}/`,
+            success: function(data, status) {
+                if (!data.wait) {
+                    window.location.replace(source_url);
+                } else if (data.error) {
+                    display_error(data.error_msg)
+                } else {
+                    setTimeout(wait_payment, 3000, source_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">{% 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 href="{{source_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..612741e 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, BasketItemPaymentStatusView)
 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/item/(?P<signature>\w+)/payment-status$',
+        BasketItemPaymentStatusView.as_view(), name='basket-item-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 9b20c02..a6e6edd 100644
--- a/combo/apps/lingo/views.py
+++ b/combo/apps/lingo/views.py
@@ -23,7 +23,7 @@ import requests
 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 +39,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, unique_item=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 unique_item:
+        normal_return_url = "%s?item-id=%s" % (normal_return_url, unique_item.pk)
     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 +156,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 +198,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 +332,14 @@ 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='', basket=True):
+
+        single_item = None
+        if not basket:
+            single_item = items[0]
+
         if remote_items:
             total_amount = sum([x.amount for x in remote_items])
         else:
@@ -329,7 +347,9 @@ 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)
+            if basket:
+                return HttpResponseRedirect(next_url)
+            return HttpResponseRedirect(get_single_item_payment_status_view(single_item.pk))
 
         for item in items:
             if item.regie != regie:
@@ -344,8 +364,6 @@ class PayMixin(object):
             lastname = user.last_name
         else:
             transaction.user = None
-            firstname = ''
-            lastname = ''
 
         transaction.save()
         transaction.regie = regie
@@ -354,7 +372,7 @@ class PayMixin(object):
         transaction.status = 0
         transaction.amount = total_amount
 
-        payment = get_eopayment_object(request, regie)
+        payment = get_eopayment_object(request, regie, single_item)
         kwargs = {
             'email': email, 'first_name': firstname, 'last_name': lastname
         }
@@ -373,8 +391,12 @@ class PayMixin(object):
 
         # store the next url in session in order to be able to redirect to
         # it if payment is canceled
+        lingo_next_url = request.build_absolute_uri(next_url)
+        if not basket:
+            lingo_next_url = get_single_item_payment_status_view(single_item.pk, transaction.pk)
+
         request.session.setdefault('lingo_next_url',
-                                   {})[order_id] = request.build_absolute_uri(next_url)
+                                   {})[order_id] = lingo_next_url
         request.session.modified = True
 
         if kind == eopayment.URL:
@@ -433,21 +455,44 @@ class PayView(PayMixin, View):
         return self.handle_payment(request, regie, items, remote_items, next_url, email)
 
 
+def get_single_item_payment_status_view(item_id, transaction_id=None):
+    url = reverse(
+        'basket-item-payment-status',
+        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(item_id))}
+    )
+    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.'))
+        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, basket=False
+        )
 
 
 class PaymentException(Exception):
@@ -597,6 +642,7 @@ class ReturnView(PaymentView):
 
     def handle_return(self, request, backend_response, **kwargs):
         transaction = None
+        basket_item_id = request.GET.get('item-id')
         try:
             transaction = self.handle_response(request, backend_response, **kwargs)
         except UnsignedPaymentException as e:
@@ -607,7 +653,10 @@ class ReturnView(PaymentView):
         except PaymentException as e:
             messages.error(request, _('We are sorry but the payment service '
                                       'failed to provide a correct answer.'))
-            return HttpResponseRedirect(get_basket_url())
+            url = get_basket_url()
+            if basket_item_id:
+                url = get_single_item_payment_status_view(basket_item_id)
+            return HttpResponseRedirect(url)
 
         if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED):
             messages.info(request, transaction.regie.get_text_on_success())
@@ -762,3 +811,96 @@ class SelfInvoiceView(View):
             return HttpResponseRedirect(url)
         messages.warning(request, msg)
         return HttpResponseRedirect(request.GET.get('page_path') or '/')
+
+
+class BasketItemPaymentStatusView(View):
+
+    http_method_names = ['get']
+
+    def get(self, request, *args, **kwargs):
+        page = Page()
+        page.template_name = 'standard'
+        template_name = 'lingo/combo/item-wait-payment.html'
+        signature = kwargs.get('signature')
+
+        try:
+            item_id = aes_hex_decrypt(settings.SECRET_KEY, signature)
+        except DecryptionError:
+            return HttpResponseForbidden(_('Invalid item signature.'))
+
+        try:
+            item = BasketItem.objects.get(
+                pk=item_id, cancellation_date__isnull=True
+            )
+        except BasketItem.DoesNotExist:
+            return HttpResponseForbidden(_('Invalid basket item'))
+
+        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['source_url'] = item.source_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..8fafc65 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,27 @@ def key(request, settings):
     else:
         return settings.LINGO_API_SIGN_KEY
 
+
+def assert_payment_status(url, item_id, 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)
+
+    url, part = os.path.split(url)
+    assert part == 'payment-status'
+    url, part = os.path.split(url)
+    assert aes_hex_decrypt(settings.SECRET_KEY, part) == str(item_id)
+    url, part = os.path.split(url)
+    assert part == 'item'
+    url, part = os.path.split(url)
+    assert part == 'lingo'
+
+
 def test_default_regie():
     payment_backend = PaymentBackend.objects.create(label='foo', slug='foo')
     Regie.objects.all().delete()
@@ -297,7 +319,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 +441,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 +464,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 +471,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, item.pk, 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)
@@ -995,3 +1019,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, item.pk)
+    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 return_url.endswith('?item-id=%s' % item.pk)
+    resp = app.get(return_url, params=data)
+    assert_payment_status(resp.location, item.pk)
+    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 return_url.endswith('?item-id=%s' % item.pk)
+    resp = app.get(return_url, params=data)
+    assert_payment_status(resp.location, item.pk, 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': ''
+    }
-- 
2.20.1

