lingo: support anonymous and no basket payment (#36876)
This commit is contained in:
parent
266b37db6f
commit
36588dd357
|
@ -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 %}
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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': ''
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue