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 00000000..d3621300
--- /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 %}
+
+{% endblock %}
+
+{% block wait-content%}
+
+ {% block wait-message %}
+
{% trans "Please wait while your request is being processed..." %}
+ {% endblock %}
+
+
{% trans "Continue" %}
+
+
+{% endblock %}
+{% endblock %}
diff --git a/combo/apps/lingo/urls.py b/combo/apps/lingo/urls.py
index 902d8954..1a1bd31c 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.+)/$', TransactionStatusApiView.as_view(),
+ name='api-transaction-status'
+ ),
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
url(r'^lingo/cancel/(?P\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'),
url(r'^lingo/callback/(?P\w+)/$', CallbackView.as_view(), name='lingo-callback'),
@@ -74,8 +79,10 @@ urlpatterns = [
ItemDownloadView.as_view(), name='download-item-pdf'),
url(r'^lingo/item/(?P[\w,-]+)/(?P[\w,-]+)/$',
ItemView.as_view(), name='view-item'),
- url(r'^lingo/item/(?P\d+)/pay$',
+ url(r'^lingo/item/(?P.+)/pay$',
BasketItemPayView.as_view(), name='basket-item-pay-view'),
+ url(r'^lingo/payment-status$',
+ PaymentStatusView.as_view(), name='payment-status'),
url(r'^lingo/self-invoice/(?P\w+)/$', SelfInvoiceView.as_view(),
name='lingo-self-invoice'),
]
diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py
index bb90d4b4..4656beab 100644
--- a/combo/apps/lingo/views.py
+++ b/combo/apps/lingo/views.py
@@ -23,11 +23,13 @@ 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.core import signing
+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
from django.utils.encoding import force_text
+from django.utils.http import urlencode
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View, DetailView, ListView, TemplateView
from django.conf import settings
@@ -41,22 +43,28 @@ import eopayment
from combo.data.models import Page
from combo.utils import check_request_signature, aes_hex_decrypt, 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, transaction_id=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 transaction_id:
+ normal_return_url = "%s?lingo-transaction-id=%s" % (normal_return_url, signing.dumps(transaction_id))
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 +158,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 +200,17 @@ 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)
+ else:
+ if item.regie.extra_fees_ws_url:
+ HttpResponseBadRequest('Can not compute extra fees with anonymous user.')
- payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id})
+ payment_url = reverse(
+ 'basket-item-pay-view',
+ kwargs={
+ 'item_signature': signing.dumps(item.pk)
+ })
return JsonResponse({'result': 'success', 'id': str(item.id),
'payment_url': request.build_absolute_uri(payment_url)})
@@ -321,7 +337,10 @@ 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=''):
+
if remote_items:
total_amount = sum([x.amount for x in remote_items])
else:
@@ -329,7 +348,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(next_url=items[0].source_url))
for item in items:
if item.regie != regie:
@@ -344,8 +363,6 @@ class PayMixin(object):
lastname = user.last_name
else:
transaction.user = None
- firstname = ''
- lastname = ''
transaction.save()
transaction.regie = regie
@@ -354,7 +371,7 @@ class PayMixin(object):
transaction.status = 0
transaction.amount = total_amount
- payment = get_eopayment_object(request, regie)
+ payment = get_eopayment_object(request, regie, transaction.pk)
kwargs = {
'email': email, 'first_name': firstname, 'last_name': lastname
}
@@ -373,8 +390,9 @@ class PayMixin(object):
# 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)
+ if next_url:
+ request.session.setdefault('lingo_next_url',
+ {})[str(transaction.pk)] = request.build_absolute_uri(next_url)
request.session.modified = True
if kind == eopayment.URL:
@@ -433,21 +451,45 @@ class PayView(PayMixin, View):
return self.handle_payment(request, regie, items, remote_items, next_url, email)
-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.'))
+def get_payment_status_view(transaction_id=None, next_url=None):
+ url = reverse('payment-status')
+ params = []
+ if transaction_id:
+ params.append(('transaction-id', signing.dumps(transaction_id)))
+ if next_url:
+ params.append(('next', next_url))
+ return "%s?%s" % (url, urlencode(params))
- item = BasketItem.objects.get(pk=kwargs['item_id'])
+
+class BasketItemPayView(PayMixin, View):
+
+ def get(self, request, *args, **kwargs):
+ next_url = request.GET.get('next_url')
+ email = request.GET.get('email', '')
+ firstname = request.GET.get('firstname', '')
+ lastname = request.GET.get('lastname', '')
+
+ item_signature = kwargs.get('item_signature')
+ try:
+ item_id = signing.loads(item_signature)
+ except signing.BadSignature:
+ 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)
+ if not next_url:
+ next_url = item.source_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 +642,34 @@ class ReturnView(PaymentView):
def handle_return(self, request, backend_response, **kwargs):
transaction = None
+ transaction_id = request.GET.get('lingo-transaction-id')
+ if transaction_id:
+ try:
+ transaction_id = signing.loads(transaction_id)
+ except signing.BadSignature:
+ pass
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 transaction_id:
+ return HttpResponseRedirect(get_payment_status_view(transaction_id))
+ return HttpResponseRedirect(get_basket_url())
+
except PaymentException as e:
messages.error(request, _('We are sorry but the payment service '
'failed to provide a correct answer.'))
+ if transaction_id:
+ return HttpResponseRedirect(get_payment_status_view(transaction_id))
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:
+ return HttpResponseRedirect(get_payment_status_view(transaction.pk))
# return to basket page if there are still items to pay
if request.user.is_authenticated:
@@ -765,3 +816,97 @@ 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'
+
+ extra_context_data = getattr(request, 'extra_context_data', {})
+ extra_context_data['transaction_id'] = ''
+
+ transaction_id = request.GET.get('transaction-id')
+
+ if not transaction_id:
+ next_url = request.GET.get('next')
+ if not next_url:
+ next_url = '/'
+ extra_context_data['next_url'] = request.build_absolute_uri(next_url)
+ request.extra_context_data = extra_context_data
+ return publish_page(request, page, template_name=template_name)
+
+ try:
+ transaction_id = signing.loads(transaction_id)
+ except signing.BadSignature:
+ return HttpResponseForbidden(_('Invalid transaction signature.'))
+
+ try:
+ transaction = Transaction.objects.get(pk=transaction_id)
+ except Transaction.DoesNotExist:
+ return HttpResponseForbidden(_('Invalid transaction.'))
+
+ next_url = request.session.get('lingo_next_url', {}).get(str(transaction_id))
+ if not next_url:
+ next_url = get_basket_url()
+ if len(set([item.source_url for item in transaction.items.all()])) == 1:
+ next_url = transaction.items.first().source_url
+ next_url = request.build_absolute_uri(next_url)
+
+ extra_context_data['transaction_id'] = signing.dumps(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):
+ transaction_signature = kwargs.get('transaction_signature')
+ try:
+ transaction_id = signing.loads(transaction_signature)
+ except signing.BadSignature:
+ return HttpResponseBadRequest(_('Invalid transaction.'))
+
+ try:
+ transaction = Transaction.objects.get(pk=transaction_id)
+ except (Transaction.DoesNotExist, ValueError):
+ 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 0a0e1529..09da44f2 100644
--- a/tests/test_lingo_payment.py
+++ b/tests/test_lingo_payment.py
@@ -9,11 +9,13 @@ import mock
from django.apps import apps
from django.contrib.auth.models import User
+from django.core import signing
from django.core.urlresolvers import reverse
from django.core.wsgi import get_wsgi_application
from django.conf import settings
from django.test import override_settings
from django.utils import timezone
+from django.utils.http import urlencode
from django.utils.six.moves.urllib import parse as urlparse
from django.contrib.messages.storage.session import SessionStorage
from webtest import TestApp
@@ -22,7 +24,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, sign_url
from .test_manager import login
@@ -120,6 +122,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('?')
+ query = urlparse.parse_qs(part)
+ assert 'transaction-id' in query
+ assert signing.loads(query['transaction-id'][0]) == 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 +234,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 +317,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 +439,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 +462,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 +469,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 +765,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)
+ lingo_transaction_id = return_qs['lingo-transaction-id'][0]
# call return with unsigned POST
with check_log(caplog, 'received unsigned payment'):
@@ -768,17 +792,22 @@ 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) + \
+ '?lingo-transaction-id=%s' % lingo_transaction_id
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 +1024,207 @@ 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 signing.loads(signature) == 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 can 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 'lingo-transaction-id' 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 'lingo-transaction-id' 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={'transaction_signature': signing.dumps('xxxx')}
+
+ )
+ resp = app.get(url, status=404)
+ assert 'Unknown transaction.' in resp.text
+
+ # unkown transaction identifier
+ transaction_id = 1000
+ url = reverse(
+ 'api-transaction-status',
+ kwargs={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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={'transaction_signature': signing.dumps(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 d4e2c085..3cd6b6f3 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()
@@ -383,10 +391,13 @@ def test_remote_invoice_successfull_payment_redirect(mock_get, mock_pay_invoice,
qs = urlparse.parse_qs(parsed.query)
args = {'transaction_id': qs['transaction_id'][0], 'signed': True,
'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')