Projet

Général

Profil

0002-lingo-support-anonymous-and-no-basket-payment-36876.patch

Emmanuel Cazenave, 09 janvier 2020 15:49

Télécharger (34,2 ko)

Voir les différences:

Subject: [PATCH 2/2] lingo: support anonymous and no basket payment (#36876)

 .../templates/lingo/combo/payment-status.html |  53 ++++
 combo/apps/lingo/urls.py                      |  11 +-
 combo/apps/lingo/views.py                     | 200 +++++++++++---
 tests/test_lingo_payment.py                   | 250 +++++++++++++++++-
 tests/test_lingo_remote_regie.py              |  18 +-
 5 files changed, 484 insertions(+), 48 deletions(-)
 create mode 100644 combo/apps/lingo/templates/lingo/combo/payment-status.html
combo/apps/lingo/templates/lingo/combo/payment-status.html
1
{% extends "combo/page_template.html" %}
2
{% load staticfiles i18n %}
3

  
4
{% block combo-content %}
5
{% block wait-js %}
6
<script>
7
function display_error(message) {
8
    $('#transaction-error').text(message);
9
    $('#transaction-error').show();
10
    $("#wait-msg").hide();
11
}
12

  
13
$(function() {
14
    var next_url = '{{next_url}}';
15
    var transaction_id = '{{transaction_id}}';
16
    if (transaction_id === "") {
17
        display_error($('#transaction-error').data('error'));
18
    }
19
    else {
20
        $.ajax({
21
            url: `/api/lingo/transaction-status/${transaction_id}/`,
22
            success: function(data, status) {
23
                if (!data.wait) {
24
                    $('#wait-msg').text($('#wait-msg').data('continue'))
25
                    // wait a little to show messages
26
                    setTimeout(function(){location.href=next_url}, 3000);
27
                } else if (data.error) {
28
                    display_error(data.error_msg)
29
                } else {
30
                    setTimeout(wait_payment, 3000, next_url, transaction_id);
31
                }
32
            },
33
            error: function(error) {
34
                display_error($('#transaction-status').data('error'));
35
                window.console && console.log(':(', error);
36
            }
37
        });
38
    }
39
});
40
</script>
41
{% endblock %}
42

  
43
{% block wait-content%}
44
<div>
45
  {% block wait-message %}
46
  <h2 id="wait-msg" data-continue="{% trans "Wait a moment or click on 'Continue'." %}">{% trans "Please wait while your request is being processed..." %}</h2>
47
  {% endblock %}
48
   <div id="transaction-error" class="errornotice" data-error="{% trans 'An error occured' %}" style="display: none;"></div>
49
   <p><a id="next-url" href="{{next_url}}">{% trans "Continue" %}</a></p>
50
   </p>
51
</div>
52
{% endblock %}
53
{% endblock %}
combo/apps/lingo/urls.py
21 21
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
22 22
                    ReturnView, ItemDownloadView, ItemView, CancelItemView,
23 23
                    RemoveBasketItemApiView, ValidateTransactionApiView,
24
                    CancelTransactionApiView, SelfInvoiceView, BasketItemPayView)
24
                    CancelTransactionApiView, SelfInvoiceView, BasketItemPayView,
25
                    TransactionStatusApiView, PaymentStatusView)
25 26
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
26 27
                            RegieDeleteView, TransactionListView, BasketItemErrorListView,
27 28
                            download_transactions_csv, PaymentBackendListView,
......
60 61
        name='api-validate-transaction'),
61 62
    url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(),
62 63
        name='api-cancel-transaction'),
64
    url(
65
        '^api/lingo/transaction-status/(?P<signature>\w+)/$', TransactionStatusApiView.as_view(),
66
        name='api-transaction-status'
67
    ),
63 68
    url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
64 69
    url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'),
65 70
    url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),
......
74 79
        ItemDownloadView.as_view(), name='download-item-pdf'),
75 80
    url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$',
76 81
        ItemView.as_view(), name='view-item'),
77
    url(r'^lingo/item/(?P<item_id>\d+)/pay$',
82
    url(r'^lingo/item/(?P<signature>\w+)/pay$',
78 83
        BasketItemPayView.as_view(), name='basket-item-pay-view'),
84
    url(r'^lingo/payment-status/(?P<signature>\w+)/$',
85
        PaymentStatusView.as_view(), name='payment-status'),
79 86
    url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(),
80 87
        name='lingo-self-invoice'),
81 88
]
combo/apps/lingo/views.py
19 19
import json
20 20
import logging
21 21
import requests
22
import uuid
22 23

  
23 24
from django.contrib.auth.models import User
24 25
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
25 26
from django.core.urlresolvers import reverse
26
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
27
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect, HttpResponseBadRequest
27 28
from django.http import HttpResponseForbidden, Http404, JsonResponse
28 29
from django.template.response import TemplateResponse
29 30
from django.utils import timezone, dateparse, six
......
39 40
import eopayment
40 41

  
41 42
from combo.data.models import Page
42
from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError
43
from combo.utils import check_request_signature, aes_hex_decrypt, aes_hex_encrypt, DecryptionError
43 44
from combo.profile.utils import get_user_from_name_id
45
from combo.public.views import publish_page
44 46

  
45 47
from .models import (Regie, BasketItem, Transaction, TransactionOperation,
46
                     LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend)
48
                     LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED)
47 49

  
48
def get_eopayment_object(request, regie_or_payment_backend):
50

  
51
def get_eopayment_object(request, regie_or_payment_backend, payment_uuid=None):
49 52
    payment_backend = regie_or_payment_backend
50 53
    if isinstance(regie_or_payment_backend, Regie):
51 54
        payment_backend = regie_or_payment_backend.payment_backend
52 55
    options = payment_backend.service_options
56
    normal_return_url = reverse(
57
        'lingo-return-payment-backend',
58
        kwargs={'payment_backend_pk': payment_backend.id}
59
    )
60
    if payment_uuid:
61
        normal_return_url = "%s?publik-payment=%s" % (normal_return_url, payment_uuid)
53 62
    options.update({
54 63
        'automatic_return_url': request.build_absolute_uri(
55 64
                reverse('lingo-callback-payment-backend',
56 65
                        kwargs={'payment_backend_pk': payment_backend.id})),
57
        'normal_return_url': request.build_absolute_uri(
58
                reverse('lingo-return-payment-backend',
59
                        kwargs={'payment_backend_pk': payment_backend.id})),
66
        'normal_return_url': request.build_absolute_uri(normal_return_url)
60 67
    })
61 68
    return eopayment.Payment(payment_backend.service, options)
62 69

  
......
150 157
            elif request.GET.get('email'):
151 158
                user = User.objects.get(email=request.GET.get('email'))
152 159
            else:
153
                raise Exception('no user specified')
160
                user = None
154 161
        except User.DoesNotExist:
155 162
            raise Exception('unknown user')
156 163

  
......
192 199
                    'Bad format for capture date, it should be yyyy-mm-dd.')
193 200

  
194 201
        item.save()
195
        item.regie.compute_extra_fees(user=item.user)
202
        if user:
203
            item.regie.compute_extra_fees(user=item.user)
196 204

  
197
        payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id})
205
        payment_url = reverse(
206
            'basket-item-pay-view',
207
            kwargs={
208
                'signature': aes_hex_encrypt(settings.SECRET_KEY, str(item.id))
209
            })
198 210
        return JsonResponse({'result': 'success', 'id': str(item.id),
199 211
                             'payment_url': request.build_absolute_uri(payment_url)})
200 212

  
......
321 333

  
322 334
class PayMixin(object):
323 335
    @atomic
324
    def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''):
336
    def handle_payment(
337
            self, request, regie, items, remote_items, next_url='/', email='', firstname='',
338
            lastname=''):
339

  
340
        payment_uuid = str(uuid.uuid4())
341
        request.session.setdefault('payment', {}).setdefault(payment_uuid, {})['items'] = [
342
            item.pk for item in items
343
        ]
344
        request.session['payment'][payment_uuid]['next_url'] = next_url
345
        request.session.modified = True
346

  
325 347
        if remote_items:
326 348
            total_amount = sum([x.amount for x in remote_items])
327 349
        else:
......
329 351

  
330 352
        if total_amount < regie.payment_min_amount:
331 353
            messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount)
332
            return HttpResponseRedirect(next_url)
354
            return HttpResponseRedirect(get_payment_status_view(payment_uuid))
333 355

  
334 356
        for item in items:
335 357
            if item.regie != regie:
......
344 366
            lastname = user.last_name
345 367
        else:
346 368
            transaction.user = None
347
            firstname = ''
348
            lastname = ''
349 369

  
350 370
        transaction.save()
351 371
        transaction.regie = regie
......
354 374
        transaction.status = 0
355 375
        transaction.amount = total_amount
356 376

  
357
        payment = get_eopayment_object(request, regie)
377
        payment = get_eopayment_object(request, regie, payment_uuid)
358 378
        kwargs = {
359 379
            'email': email, 'first_name': firstname, 'last_name': lastname
360 380
        }
......
371 391
        transaction.order_id = order_id
372 392
        transaction.save()
373 393

  
374
        # store the next url in session in order to be able to redirect to
375
        # it if payment is canceled
376
        request.session.setdefault('lingo_next_url',
377
                                   {})[order_id] = request.build_absolute_uri(next_url)
378
        request.session.modified = True
379

  
380 394
        if kind == eopayment.URL:
381 395
            return HttpResponseRedirect(data)
382 396
        elif kind == eopayment.FORM:
......
433 447
        return self.handle_payment(request, regie, items, remote_items, next_url, email)
434 448

  
435 449

  
450
def get_payment_status_view(transaction_uuid, transaction_id=None):
451
    url = reverse(
452
        'payment-status',
453
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction_uuid))}
454
    )
455
    if transaction_id:
456
        url = "%s?transaction-id=%s" % (
457
            url, aes_hex_encrypt(settings.SECRET_KEY, str(transaction_id))
458
        )
459
    return url
460

  
461

  
436 462
class BasketItemPayView(PayMixin, View):
463

  
437 464
    def get(self, request, *args, **kwargs):
438
        next_url = request.GET.get('next_url') or '/'
439
        if not (request.user and request.user.is_authenticated):
440
            return HttpResponseForbidden(_('No item payment allowed for anonymous users.'))
465
        next_url = request.GET.get('next_url') or None
466
        email = request.GET.get('email', '')
467
        firstname = request.GET.get('firstname', '')
468
        lastname = request.GET.get('lastname', '')
441 469

  
442
        item = BasketItem.objects.get(pk=kwargs['item_id'])
470
        signature = kwargs.get('signature')
471
        try:
472
            item_id = aes_hex_decrypt(settings.SECRET_KEY, signature)
473
        except DecryptionError:
474
            return HttpResponseForbidden(_('Invalid payment request.'))
475

  
476
        item = BasketItem.objects.get(pk=item_id)
443 477
        regie = item.regie
444 478
        if regie.extra_fees_ws_url:
445 479
            return HttpResponseForbidden(_('No item payment allowed as extra fees set.'))
446 480

  
447
        if item.user != request.user:
481
        if item.user and item.user != request.user:
448 482
            return HttpResponseForbidden(_('Wrong item: payment not allowed.'))
449 483

  
450
        return self.handle_payment(request, regie, [item], [], next_url)
484
        return self.handle_payment(
485
            request=request, regie=regie, items=[item], remote_items=[], next_url=next_url, email=email,
486
            firstname=firstname, lastname=lastname
487
        )
451 488

  
452 489

  
453 490
class PaymentException(Exception):
......
600 637

  
601 638
    def handle_return(self, request, backend_response, **kwargs):
602 639
        transaction = None
640
        payment_uuid = request.GET.get('publik-payment')
603 641
        try:
604 642
            transaction = self.handle_response(request, backend_response, **kwargs)
605 643
        except UnsignedPaymentException as e:
606 644
            # some payment backends do not sign return URLs, don't mark this as
607 645
            # an error, they will provide a notification to the callback
608 646
            # endpoint.
609
            pass
647
            if payment_uuid:
648
                return HttpResponseRedirect(get_payment_status_view(payment_uuid, e.transaction.pk))
649

  
610 650
        except PaymentException as e:
611 651
            messages.error(request, _('We are sorry but the payment service '
612 652
                                      'failed to provide a correct answer.'))
653
            if payment_uuid:
654
                return HttpResponseRedirect(get_payment_status_view(payment_uuid))
613 655
            return HttpResponseRedirect(get_basket_url())
614 656

  
615 657
        if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED):
616 658
            messages.info(request, transaction.regie.get_text_on_success())
617 659

  
618
        if transaction and request.session.get('lingo_next_url'):
619
            redirect_url = request.session['lingo_next_url'].get(transaction.order_id)
620
            if redirect_url:
621
                return HttpResponseRedirect(redirect_url)
660
        if transaction and payment_uuid:
661
            return HttpResponseRedirect(get_payment_status_view(payment_uuid, transaction.pk))
622 662

  
623 663
        # return to basket page if there are still items to pay
624 664
        if request.user.is_authenticated:
......
765 805
            return HttpResponseRedirect(url)
766 806
        messages.warning(request, msg)
767 807
        return HttpResponseRedirect(request.GET.get('page_path') or '/')
808

  
809

  
810
class PaymentStatusView(View):
811

  
812
    http_method_names = ['get']
813

  
814
    def get(self, request, *args, **kwargs):
815
        page = Page()
816
        page.template_name = 'standard'
817
        template_name = 'lingo/combo/payment-status.html'
818
        signature = kwargs.get('signature')
819

  
820
        try:
821
            payment_uuid = aes_hex_decrypt(settings.SECRET_KEY, signature)
822
        except DecryptionError:
823
            return HttpResponseForbidden(_('Invalid payment signature.'))
824

  
825
        next_url = request.session.get('payment', {}).get(payment_uuid, {}).get('next_url')
826
        if not next_url:
827
            items = BasketItem.objects.filter(
828
                pk__in=request.session.get('payment', {}).get(payment_uuid, {}).get('items', [])
829
            )
830
            next_url = get_basket_url()
831
            if len(set([item.source_url for item in items])) == 1:
832
                next_url = items[0].source_url
833
        next_url = request.build_absolute_uri(next_url)
834

  
835
        transaction = None
836
        if 'transaction-id' in request.GET:
837
            try:
838
                transaction_id = aes_hex_decrypt(
839
                    settings.SECRET_KEY, request.GET.get('transaction-id')
840
                )
841
            except DecryptionError:
842
                return HttpResponseForbidden(_('Invalid transaction signature.'))
843
            try:
844
                transaction = Transaction.objects.get(pk=transaction_id)
845
            except Transaction.DoesNotExist:
846
                return HttpResponseForbidden(_('Invalid transaction.'))
847

  
848
        extra_context_data = getattr(request, 'extra_context_data', {})
849
        extra_context_data['transaction_id'] = ''
850
        if transaction:
851
            extra_context_data['transaction_id'] = \
852
                aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))
853
        extra_context_data['next_url'] = next_url
854
        request.extra_context_data = extra_context_data
855
        return publish_page(request, page, template_name=template_name)
856

  
857

  
858
class TransactionStatusApiView(View):
859

  
860
    http_method_names = ['get']
861

  
862
    def get(self, request, *args, **kwargs):
863
        signature = kwargs.get('signature')
864
        try:
865
            transaction_id = aes_hex_decrypt(settings.SECRET_KEY, signature)
866
        except DecryptionError:
867
            return HttpResponseBadRequest(_('Invalid transaction.'))
868
        try:
869
            transaction = Transaction.objects.get(pk=transaction_id)
870
        except Transaction.DoesNotExist:
871
            return HttpResponseNotFound(_('Unknown transaction.'))
872

  
873
        user = request.user if request.user.is_authenticated() else None
874
        error_msg = _('Transaction does not belong to the requesting user')
875
        if user and transaction.user and user != transaction.user:
876
            return HttpResponseForbidden(error_msg)
877
        if not user and transaction.user:
878
            return HttpResponseForbidden(error_msg)
879

  
880
        if transaction.is_paid():
881
            data = {
882
                'wait': False,
883
                'error': False,
884
                'error_msg': ''
885
            }
886
            return JsonResponse(data=data)
887

  
888
        if transaction.status in (
889
                eopayment.CANCELLED, eopayment.ERROR, eopayment.DENIED, EXPIRED
890
        ):
891
            data = {
892
                'wait': True,
893
                'error': True,
894
                'error_msg': _('Payment error, you can continue and make another payment')
895
            }
896
            return JsonResponse(data=data)
897

  
898
        data = {
899
            'wait': True,
900
            'error': False,
901
            'error_msg': ''
902
        }
903
        return JsonResponse(data=data)
tests/test_lingo_payment.py
6 6
from decimal import Decimal
7 7
import json
8 8
import mock
9
import os.path
9 10

  
10 11
from django.apps import apps
11 12
from django.contrib.auth.models import User
......
22 23
from combo.apps.lingo.models import (
23 24
    Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell,
24 25
    PaymentBackend)
25
from combo.utils import sign_url
26
from combo.utils import aes_hex_decrypt, aes_hex_encrypt, sign_url
26 27

  
27 28
from .test_manager import login
28 29

  
......
120 121
    else:
121 122
        return settings.LINGO_API_SIGN_KEY
122 123

  
124

  
125
def assert_payment_status(url, transaction_id=None):
126
    if hasattr(url, 'path'):
127
        url = url.path
128

  
129
    if transaction_id:
130
        url, part = url.split('?')
131
        assert 'transaction-id' in part
132
        signature = part.replace('transaction-id=', '')
133
        assert aes_hex_decrypt(settings.SECRET_KEY, signature) == str(transaction_id)
134

  
135
    assert url.startswith('/lingo/payment-status/')
136

  
137

  
123 138
def test_default_regie():
124 139
    payment_backend = PaymentBackend.objects.create(label='foo', slug='foo')
125 140
    Regie.objects.all().delete()
......
218 233
    assert resp.status_code == 200
219 234
    # simulate successful return URL
220 235
    resp = app.get(qs['return_url'][0], params=args)
236
    # redirect to payment status
221 237
    assert resp.status_code == 302
222
    assert urlparse.urlparse(resp.url).path == '/test_basket_cell/'
238
    assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
223 239
    resp = resp.follow()
224 240
    assert 'Your payment has been succesfully registered.' in resp.text
241
    assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
242
        '/test_basket_cell/'
243

  
225 244

  
226 245
def test_add_amount_to_basket(app, key, regie, user):
227 246
    payment_backend = PaymentBackend.objects.create(
......
297 316
    assert resp.status_code == 200
298 317
    response = json.loads(resp.text)
299 318
    assert response['result'] == 'success'
300
    assert response['payment_url'].endswith('/lingo/item/%s/pay' % item.id)
319
    payment_url = urlparse.urlparse(response['payment_url'])
320
    assert payment_url.path.startswith('/lingo/item/')
321
    assert payment_url.path.endswith('/pay')
301 322
    assert BasketItem.objects.filter(amount=Decimal('22.23')).exists()
302 323
    assert BasketItem.objects.filter(amount=Decimal('22.23'))[0].regie_id == other_regie.id
303 324

  
......
417 438
    assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=True).exists()
418 439
    payment_url = resp.json['payment_url']
419 440
    resp = app.get(payment_url, status=403)
420
    assert 'No item payment allowed for anonymous users.' in resp.text
441
    assert 'Wrong item: payment not allowed.' in resp.text
421 442

  
422 443
    login(app, username='john.doe', password='john.doe')
423 444
    resp = app.get(payment_url, status=403)
......
440 461
    assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
441 462
    qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
442 463
    assert qs['amount'] == ['12.00']
443

  
444 464
    # simulate successful payment response from dummy backend
445 465
    data = {'transaction_id': qs['transaction_id'][0], 'ok': True,
446 466
            'amount': qs['amount'][0], 'signed': True}
......
448 468
    # dummy module put that URL in return_url query string parameter).
449 469
    resp = app.get(qs['return_url'][0], params=data)
450 470
    # check that item is paid
451
    assert BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).exists()
452
    # check that user is redirected to the next_url passed previously
453
    assert resp.location == 'http://example.net/form/id/'
471
    item  = BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).first()
472
    # check that user is redirected to the item payment status view
473
    assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk)
474

  
454 475

  
455 476
def test_pay_multiple_regies(app, key, regie, user):
456 477
    test_add_amount_to_basket(app, key, regie, user)
......
743 764
    data = {'transaction_id': transaction_id,
744 765
            'amount': qs['amount'][0], 'ok': True}
745 766
    assert data['amount'] == '10.50'
767
    return_qs = urlparse.parse_qs(urlparse.urlparse(qs['return_url'][0]).query)
768
    publik_payment = return_qs['publik-payment'][0]
746 769

  
747 770
    # call return with unsigned POST
748 771
    with check_log(caplog, 'received unsigned payment'):
......
768 791

  
769 792
    # call return with signed POST
770 793
    data['signed'] = True
771
    return_url = get_url(with_payment_backend, 'lingo-return', regie)
794
    return_url = get_url(with_payment_backend, 'lingo-return', regie) + '?publik-payment=%s' % publik_payment
772 795
    with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request:
773 796
        get_resp = app.post(return_url, params=data)
774 797
        url = request.call_args[0][1]
775 798
        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
799
    # redirect to payment status
776 800
    assert get_resp.status_code == 302
777
    assert urlparse.urlparse(get_resp['location']).path == '/test_basket_cell/'
778
    resp = app.get(get_resp['Location'])
801
    assert urlparse.urlparse(get_resp.url).path.startswith('/lingo/payment-status/')
802
    resp = get_resp.follow()
779 803
    assert 'Your payment has been succesfully registered.' in resp.text
804
    assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
805
        '/test_basket_cell/'
780 806
    assert Transaction.objects.get(order_id=transaction_id).status == eopayment.PAID
781 807

  
808

  
782 809
def test_transaction_expiration():
783 810
    t1 = Transaction(status=0)
784 811
    t1.save()
......
995 1022
        assert url.startswith('http://example.org/testitem/jump/trigger/paid')
996 1023
    assert BasketItem.objects.get(id=item.id).payment_date
997 1024
    assert BasketItem.objects.get(id=item.id).notification_date
1025

  
1026

  
1027
@pytest.mark.parametrize("authenticated", [True, False])
1028
def test_payment_no_basket(app, user, regie, authenticated):
1029
    url = reverse('api-add-basket-item')
1030
    source_url = 'http://example.org/item/1'
1031
    data = {'amount': 10, 'display_name': 'test item', 'url': source_url}
1032
    if authenticated:
1033
        data['email'] = user.email
1034
    url = sign_url(url, settings.LINGO_API_SIGN_KEY)
1035
    resp = app.post_json(url, params=data)
1036
    assert resp.status_code == 200
1037
    payment_url = resp.json['payment_url']
1038

  
1039
    item = BasketItem.objects.first()
1040
    assert item.user is None
1041
    assert item.amount == Decimal('10.00')
1042
    path = urlparse.urlparse(payment_url).path
1043
    start = '/lingo/item/'
1044
    end = '/pay'
1045
    assert path.startswith(start)
1046
    assert path.endswith(end)
1047
    signature = path.replace(start, '').replace(end, '')
1048
    assert aes_hex_decrypt(settings.SECRET_KEY, signature) == str(item.id)
1049

  
1050
    if authenticated:
1051
        app = login(app)
1052

  
1053
    # payment error due to too small amount
1054
    item.amount = Decimal('1.00')
1055
    item.save()
1056
    resp = app.get(payment_url)
1057
    assert_payment_status(resp.location)
1058
    resp = resp.follow()
1059
    assert 'Minimal payment amount is 4.50' in resp.text
1060
    # we can go back to form
1061
    assert source_url in resp.text
1062

  
1063
    # amount ok, redirection to payment backend
1064
    item.amount = Decimal('10.00')
1065
    item.save()
1066
    resp = app.get(payment_url)
1067
    assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
1068
    qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
1069
    assert qs['amount'] == ['10.00']
1070
    if authenticated:
1071
        assert qs['email'] == ['foo@example.com']
1072
    else:
1073
        assert 'email' not in qs
1074

  
1075
    # mail get be specified here for anonymous user
1076
    resp = app.get(
1077
        payment_url,
1078
        params={
1079
            'email': 'foo@localhost',
1080
        }
1081
    )
1082
    assert resp.location.startswith('http://dummy-payment.demo.entrouvert.com/')
1083
    qs = urlparse.parse_qs(urlparse.urlparse(resp.location).query)
1084
    assert qs['amount'] == ['10.00']
1085
    if authenticated:
1086
        assert qs['email'] == ['foo@example.com']
1087
    else:
1088
        assert qs['email'] == ['foo@localhost']
1089

  
1090
    # simulate bad responseform payment backend, no transaction id
1091
    data = {'amount': qs['amount'][0], 'signed': True}
1092
    return_url = qs['return_url'][0]
1093
    assert 'publik-payment' in return_url
1094
    resp = app.get(return_url, params=data)
1095
    assert_payment_status(resp.location)
1096
    resp = resp.follow()
1097
    assert 'We are sorry but the payment service failed to provide a correct answer.' in resp.text
1098
    assert 'http://example.org/item/1' in resp.text
1099
    # check that item is not paid
1100
    item = BasketItem.objects.get(pk=item.pk)
1101
    assert not item.payment_date
1102

  
1103
    # simulate successful payment response from dummy backend
1104
    data = {
1105
        'transaction_id': qs['transaction_id'][0], 'ok': True,
1106
        'amount': qs['amount'][0], 'signed': True
1107
    }
1108
    return_url = qs['return_url'][0]
1109
    assert 'publik-payment' in return_url
1110
    resp = app.get(return_url, params=data)
1111
    assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk)
1112
    # check that item is paid
1113
    item = BasketItem.objects.get(pk=item.pk)
1114
    assert item.payment_date
1115
    # accept redirection to item payment status view
1116
    resp = resp.follow()
1117
    # which should it self redirect to item.source_url as it is paid
1118
    assert 'Please wait while your request is being processed' in resp.text
1119
    assert source_url in resp.text
1120

  
1121

  
1122
def test_transaction_status_api(app, regie, user):
1123
    # invalid transaction signature
1124
    url = reverse('api-transaction-status', kwargs={'signature': 'xxxx'})
1125
    resp = app.get(url, status=400)
1126
    assert 'Invalid transaction.' in resp.text
1127

  
1128
    # unkown transaction identifier
1129
    transaction_id = 1000
1130
    url = reverse(
1131
        'api-transaction-status',
1132
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction_id))}
1133
    )
1134
    resp = app.get(url, status=404)
1135
    assert 'Unknown transaction.' in resp.text
1136

  
1137
    wait_response = {
1138
        'wait': True,
1139
        'error': False,
1140
        'error_msg': ''
1141
    }
1142
    # anonymous user on anonymous transaction: OK
1143
    transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0)
1144
    url = reverse(
1145
        'api-transaction-status',
1146
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1147
    )
1148
    resp = app.get(url)
1149
    assert resp.json == wait_response
1150

  
1151
    # authenticated user on anonymous transaction: OK
1152
    transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0)
1153
    url = reverse(
1154
        'api-transaction-status',
1155
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1156
    )
1157
    resp = login(app).get(url)
1158
    assert resp.json == wait_response
1159
    app.reset()
1160

  
1161
    # authenticated user on his transaction: OK
1162
    transaction = Transaction.objects.create(
1163
        amount=Decimal('10.0'), regie=regie, status=0, user=user)
1164
    url = reverse(
1165
        'api-transaction-status',
1166
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1167
    )
1168
    resp = login(app).get(url)
1169
    assert resp.json == wait_response
1170
    app.reset()
1171

  
1172
    error_msg = 'Transaction does not belong to the requesting user'
1173
    # anonymous user on other user transaction transaction: NOTOK
1174
    transaction = Transaction.objects.create(
1175
        amount=Decimal('10.0'), regie=regie, status=0, user=user)
1176
    url = reverse(
1177
        'api-transaction-status',
1178
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1179
    )
1180
    resp = app.get(url, status=403)
1181
    assert error_msg in resp.text
1182

  
1183
    # authenticated user on other user transaction transaction: NOTOK
1184
    user2 = User.objects.create_user(
1185
        'user2', password='user2', email='user2@example.com'
1186
    )
1187
    transaction = Transaction.objects.create(
1188
        amount=Decimal('10.0'), regie=regie, status=0, user=user2)
1189
    url = reverse(
1190
        'api-transaction-status',
1191
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1192
    )
1193
    resp = login(app).get(url, status=403)
1194
    assert error_msg in resp.text
1195
    app.reset()
1196

  
1197
    # transaction error
1198
    transaction = Transaction.objects.create(
1199
        amount=Decimal('10.0'), regie=regie, status=eopayment.ERROR
1200
    )
1201
    url = reverse(
1202
        'api-transaction-status',
1203
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1204
    )
1205
    resp = app.get(url)
1206
    assert resp.json == {
1207
        'wait': True,
1208
        'error': True,
1209
        'error_msg': 'Payment error, you can continue and make another payment'
1210
    }
1211

  
1212
    # transaction paid
1213
    transaction = Transaction.objects.create(
1214
        amount=Decimal('10.0'), regie=regie, status=eopayment.PAID
1215
    )
1216
    url = reverse(
1217
        'api-transaction-status',
1218
        kwargs={'signature': aes_hex_encrypt(settings.SECRET_KEY, str(transaction.pk))}
1219
    )
1220
    resp = app.get(url)
1221
    assert resp.json == {
1222
        'wait': False,
1223
        'error': False,
1224
        'error_msg': ''
1225
    }
tests/test_lingo_remote_regie.py
211 211
                kwargs={'payment_backend_pk': remote_regie.payment_backend.id}))
212 212
    # simulate successful return URL
213 213
    resp = app.get(qs['return_url'][0], params=args)
214
    # redirect to payment status
214 215
    assert resp.status_code == 302
215
    assert urlparse.urlparse(resp.url).path == '/'
216
    assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
217
    resp = resp.follow()
218
    assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == '/'
216 219
    # simulate successful call to callback URL
217 220
    resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args)
218 221
    trans = Transaction.objects.all()
......
335 338
    # simulate payment failure
336 339
    mock_get.side_effect = ConnectionError('where is my hostname?')
337 340
    resp = app.get(qs['return_url'][0], params=args)
341
    # redirect to payment status
338 342
    assert resp.status_code == 302
339
    assert urlparse.urlparse(resp.url).path == '/active-remote-invoices-page/'
343
    assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
344
    resp = resp.follow()
345
    assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
346
        '/active-remote-invoices-page/'
347

  
340 348
    # simulate successful call to callback URL
341 349
    resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args)
342 350
    trans = Transaction.objects.all()
......
385 393
            'ok': True, 'reason': 'Paid'}
386 394

  
387 395
    resp = app.get(qs['return_url'][0], params=args)
396
    # redirect to payment status
388 397
    assert resp.status_code == 302
389
    assert urlparse.urlparse(resp.location).path == '/active-remote-invoices-page/'
398
    assert urlparse.urlparse(resp.url).path.startswith('/lingo/payment-status/')
399
    resp = resp.follow()
400
    assert urlparse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == \
401
        '/active-remote-invoices-page/'
390 402

  
391 403

  
392 404
@mock.patch('combo.apps.lingo.models.UserSAMLIdentifier')
393
-