lingo: support anonymous and no basket payment (#36876)

This commit is contained in:
Emmanuel Cazenave 2019-12-24 13:42:47 +01:00
parent 266b37db6f
commit 36588dd357
5 changed files with 495 additions and 46 deletions

View File

@ -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 %}

View File

@ -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<transaction_signature>.+)/$', 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<item_signature>.+)/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<cell_id>\w+)/$', SelfInvoiceView.as_view(),
name='lingo-self-invoice'),
]

View File

@ -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)

View File

@ -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': ''
}

View File

@ -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')