Projet

Général

Profil

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

Emmanuel Cazenave, 14 janvier 2020 18:58

Télécharger (34,6 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                     | 201 ++++++++++++--
 tests/test_lingo_payment.py                   | 255 +++++++++++++++++-
 tests/test_lingo_remote_regie.py              |  19 +-
 5 files changed, 494 insertions(+), 45 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<transaction_signature>.+)/$', 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<item_signature>.+)/pay$',
78 83
        BasketItemPayView.as_view(), name='basket-item-pay-view'),
84
    url(r'^lingo/payment-status$',
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
23 23
from django.contrib.auth.models import User
24 24
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
25 25
from django.core.urlresolvers import reverse
26
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
26
from django.core import signing
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
30 31
from django.utils.encoding import force_text
32
from django.utils.http import urlencode
31 33
from django.views.decorators.csrf import csrf_exempt
32 34
from django.views.generic import View, DetailView, ListView, TemplateView
33 35
from django.conf import settings
......
41 43
from combo.data.models import Page
42 44
from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionError
43 45
from combo.profile.utils import get_user_from_name_id
46
from combo.public.views import publish_page
44 47

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

  
48
def get_eopayment_object(request, regie_or_payment_backend):
51

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

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

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

  
194 202
        item.save()
195
        item.regie.compute_extra_fees(user=item.user)
196

  
197
        payment_url = reverse('basket-item-pay-view', kwargs={'item_id': item.id})
203
        if user:
204
            item.regie.compute_extra_fees(user=item.user)
205
        else:
206
            if item.regie.extra_fees_ws_url:
207
                HttpResponseBadRequest('Can not compute extra fees with anonymous user.')
208

  
209
        payment_url = reverse(
210
            'basket-item-pay-view',
211
            kwargs={
212
                'item_signature': signing.dumps(item.pk)
213
            })
198 214
        return JsonResponse({'result': 'success', 'id': str(item.id),
199 215
                             'payment_url': request.build_absolute_uri(payment_url)})
200 216

  
......
321 337

  
322 338
class PayMixin(object):
323 339
    @atomic
324
    def handle_payment(self, request, regie, items, remote_items, next_url='/', email=''):
340
    def handle_payment(
341
            self, request, regie, items, remote_items, next_url='/', email='', firstname='',
342
            lastname=''):
343

  
325 344
        if remote_items:
326 345
            total_amount = sum([x.amount for x in remote_items])
327 346
        else:
......
329 348

  
330 349
        if total_amount < regie.payment_min_amount:
331 350
            messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount)
332
            return HttpResponseRedirect(next_url)
351
            return HttpResponseRedirect(get_payment_status_view(next_url=items[0].source_url))
333 352

  
334 353
        for item in items:
335 354
            if item.regie != regie:
......
344 363
            lastname = user.last_name
345 364
        else:
346 365
            transaction.user = None
347
            firstname = ''
348
            lastname = ''
349 366

  
350 367
        transaction.save()
351 368
        transaction.regie = regie
......
354 371
        transaction.status = 0
355 372
        transaction.amount = total_amount
356 373

  
357
        payment = get_eopayment_object(request, regie)
374
        payment = get_eopayment_object(request, regie, transaction.pk)
358 375
        kwargs = {
359 376
            'email': email, 'first_name': firstname, 'last_name': lastname
360 377
        }
......
373 390

  
374 391
        # store the next url in session in order to be able to redirect to
375 392
        # it if payment is canceled
376
        request.session.setdefault('lingo_next_url',
377
                                   {})[order_id] = request.build_absolute_uri(next_url)
393
        if next_url:
394
            request.session.setdefault('lingo_next_url',
395
                                       {})[str(transaction.pk)] = request.build_absolute_uri(next_url)
378 396
        request.session.modified = True
379 397

  
380 398
        if kind == eopayment.URL:
......
433 451
        return self.handle_payment(request, regie, items, remote_items, next_url, email)
434 452

  
435 453

  
454
def get_payment_status_view(transaction_id=None, next_url=None):
455
    url = reverse('payment-status')
456
    params = []
457
    if transaction_id:
458
        params.append(('transaction-id', signing.dumps(transaction_id)))
459
    if next_url:
460
        params.append(('next', next_url))
461
    return "%s?%s" % (url, urlencode(params))
462

  
463

  
436 464
class BasketItemPayView(PayMixin, View):
465

  
437 466
    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.'))
467
        next_url = request.GET.get('next_url')
468
        email = request.GET.get('email', '')
469
        firstname = request.GET.get('firstname', '')
470
        lastname = request.GET.get('lastname', '')
441 471

  
442
        item = BasketItem.objects.get(pk=kwargs['item_id'])
472
        item_signature = kwargs.get('item_signature')
473
        try:
474
            item_id = signing.loads(item_signature)
475
        except signing.BadSignature:
476
            return HttpResponseForbidden(_('Invalid payment request.'))
477

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

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

  
450
        return self.handle_payment(request, regie, [item], [], next_url)
486
        if not next_url:
487
            next_url = item.source_url
488

  
489
        return self.handle_payment(
490
            request=request, regie=regie, items=[item], remote_items=[], next_url=next_url, email=email,
491
            firstname=firstname, lastname=lastname
492
        )
451 493

  
452 494

  
453 495
class PaymentException(Exception):
......
600 642

  
601 643
    def handle_return(self, request, backend_response, **kwargs):
602 644
        transaction = None
645
        transaction_id = request.GET.get('lingo-transaction-id')
646
        if transaction_id:
647
            try:
648
                transaction_id = signing.loads(transaction_id)
649
            except signing.BadSignature:
650
                pass
603 651
        try:
604 652
            transaction = self.handle_response(request, backend_response, **kwargs)
605 653
        except UnsignedPaymentException as e:
606 654
            # some payment backends do not sign return URLs, don't mark this as
607 655
            # an error, they will provide a notification to the callback
608 656
            # endpoint.
609
            pass
657
            if transaction_id:
658
                return HttpResponseRedirect(get_payment_status_view(transaction_id))
659
            return HttpResponseRedirect(get_basket_url())
660

  
610 661
        except PaymentException as e:
611 662
            messages.error(request, _('We are sorry but the payment service '
612 663
                                      'failed to provide a correct answer.'))
664
            if transaction_id:
665
                return HttpResponseRedirect(get_payment_status_view(transaction_id))
613 666
            return HttpResponseRedirect(get_basket_url())
614 667

  
615 668
        if transaction and transaction.status in (eopayment.PAID, eopayment.ACCEPTED):
616 669
            messages.info(request, transaction.regie.get_text_on_success())
617 670

  
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)
671
        if transaction:
672
            return HttpResponseRedirect(get_payment_status_view(transaction.pk))
622 673

  
623 674
        # return to basket page if there are still items to pay
624 675
        if request.user.is_authenticated:
......
765 816
            return HttpResponseRedirect(url)
766 817
        messages.warning(request, msg)
767 818
        return HttpResponseRedirect(request.GET.get('page_path') or '/')
819

  
820

  
821
class PaymentStatusView(View):
822

  
823
    http_method_names = ['get']
824

  
825
    def get(self, request, *args, **kwargs):
826
        page = Page()
827
        page.template_name = 'standard'
828
        template_name = 'lingo/combo/payment-status.html'
829

  
830
        extra_context_data = getattr(request, 'extra_context_data', {})
831
        extra_context_data['transaction_id'] = ''
832

  
833
        transaction_id = request.GET.get('transaction-id')
834

  
835
        if not transaction_id:
836
            next_url = request.GET.get('next')
837
            if not next_url:
838
                next_url = '/'
839
            extra_context_data['next_url'] = request.build_absolute_uri(next_url)
840
            request.extra_context_data = extra_context_data
841
            return publish_page(request, page, template_name=template_name)
842

  
843
        try:
844
            transaction_id = signing.loads(transaction_id)
845
        except signing.BadSignature:
846
            return HttpResponseForbidden(_('Invalid transaction signature.'))
847

  
848
        try:
849
            transaction = Transaction.objects.get(pk=transaction_id)
850
        except Transaction.DoesNotExist:
851
            return HttpResponseForbidden(_('Invalid transaction.'))
852

  
853
        next_url = request.session.get('lingo_next_url', {}).get(str(transaction_id))
854
        if not next_url:
855
            next_url = get_basket_url()
856
            if len(set([item.source_url for item in transaction.items.all()])) == 1:
857
                next_url = transaction.items.first().source_url
858
        next_url = request.build_absolute_uri(next_url)
859

  
860
        extra_context_data['transaction_id'] = signing.dumps(transaction.pk)
861
        extra_context_data['next_url'] = next_url
862
        request.extra_context_data = extra_context_data
863
        return publish_page(request, page, template_name=template_name)
864

  
865

  
866
class TransactionStatusApiView(View):
867

  
868
    http_method_names = ['get']
869

  
870
    def get(self, request, *args, **kwargs):
871
        transaction_signature = kwargs.get('transaction_signature')
872
        try:
873
            transaction_id = signing.loads(transaction_signature)
874
        except signing.BadSignature:
875
            return HttpResponseBadRequest(_('Invalid transaction.'))
876

  
877
        try:
878
            transaction = Transaction.objects.get(pk=transaction_id)
879
        except (Transaction.DoesNotExist, ValueError):
880
            return HttpResponseNotFound(_('Unknown transaction.'))
881

  
882
        user = request.user if request.user.is_authenticated() else None
883
        error_msg = _('Transaction does not belong to the requesting user')
884
        if user and transaction.user and user != transaction.user:
885
            return HttpResponseForbidden(error_msg)
886
        if not user and transaction.user:
887
            return HttpResponseForbidden(error_msg)
888

  
889
        if transaction.is_paid():
890
            data = {
891
                'wait': False,
892
                'error': False,
893
                'error_msg': ''
894
            }
895
            return JsonResponse(data=data)
896

  
897
        if transaction.status in (
898
                eopayment.CANCELLED, eopayment.ERROR, eopayment.DENIED, EXPIRED
899
        ):
900
            data = {
901
                'wait': True,
902
                'error': True,
903
                'error_msg': _('Payment error, you can continue and make another payment')
904
            }
905
            return JsonResponse(data=data)
906

  
907
        data = {
908
            'wait': True,
909
            'error': False,
910
            'error_msg': ''
911
        }
912
        return JsonResponse(data=data)
tests/test_lingo_payment.py
9 9

  
10 10
from django.apps import apps
11 11
from django.contrib.auth.models import User
12
from django.core import signing
12 13
from django.core.urlresolvers import reverse
13 14
from django.core.wsgi import get_wsgi_application
14 15
from django.conf import settings
15 16
from django.test import override_settings
16 17
from django.utils import timezone
18
from django.utils.http import urlencode
17 19
from django.utils.six.moves.urllib import parse as urlparse
18 20
from django.contrib.messages.storage.session import SessionStorage
19 21
from webtest import TestApp
......
22 24
from combo.apps.lingo.models import (
23 25
    Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell,
24 26
    PaymentBackend)
25
from combo.utils import sign_url
27
from combo.utils import aes_hex_decrypt, sign_url
26 28

  
27 29
from .test_manager import login
28 30

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

  
125

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

  
130
    if transaction_id:
131
        url, part = url.split('?')
132
        query = urlparse.parse_qs(part)
133
        assert 'transaction-id' in query
134
        assert signing.loads(query['transaction-id'][0]) == transaction_id
135

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

  
138

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

  
225 245

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

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

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

  
444 465
    # simulate successful payment response from dummy backend
445 466
    data = {'transaction_id': qs['transaction_id'][0], 'ok': True,
446 467
            'amount': qs['amount'][0], 'signed': True}
......
448 469
    # dummy module put that URL in return_url query string parameter).
449 470
    resp = app.get(qs['return_url'][0], params=data)
450 471
    # 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/'
472
    item  = BasketItem.objects.filter(regie=regie, amount=amount, payment_date__isnull=False).first()
473
    # check that user is redirected to the item payment status view
474
    assert_payment_status(resp.location, transaction_id=item.transaction_set.last().pk)
475

  
454 476

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

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

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

  
810

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

  
1028

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

  
1041
    item = BasketItem.objects.first()
1042
    assert item.user is None
1043
    assert item.amount == Decimal('10.00')
1044
    path = urlparse.urlparse(payment_url).path
1045
    start = '/lingo/item/'
1046
    end = '/pay'
1047
    assert path.startswith(start)
1048
    assert path.endswith(end)
1049
    signature = path.replace(start, '').replace(end, '')
1050
    assert signing.loads(signature) == item.id
1051

  
1052
    if authenticated:
1053
        app = login(app)
1054

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

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

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

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

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

  
1123

  
1124
def test_transaction_status_api(app, regie, user):
1125
    # invalid transaction signature
1126
    url = reverse(
1127
        'api-transaction-status',
1128
        kwargs={'transaction_signature': signing.dumps('xxxx')}
1129

  
1130
    )
1131
    resp = app.get(url, status=404)
1132
    assert 'Unknown transaction.' in resp.text
1133

  
1134
    # unkown transaction identifier
1135
    transaction_id = 1000
1136
    url = reverse(
1137
        'api-transaction-status',
1138
        kwargs={'transaction_signature': signing.dumps(transaction_id)}
1139
    )
1140
    resp = app.get(url, status=404)
1141
    assert 'Unknown transaction.' in resp.text
1142

  
1143
    wait_response = {
1144
        'wait': True,
1145
        'error': False,
1146
        'error_msg': ''
1147
    }
1148
    # anonymous user on anonymous transaction: OK
1149
    transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0)
1150
    url = reverse(
1151
        'api-transaction-status',
1152
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1153
    )
1154
    resp = app.get(url)
1155
    assert resp.json == wait_response
1156

  
1157
    # authenticated user on anonymous transaction: OK
1158
    transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0)
1159
    url = reverse(
1160
        'api-transaction-status',
1161
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1162
    )
1163
    resp = login(app).get(url)
1164
    assert resp.json == wait_response
1165
    app.reset()
1166

  
1167
    # authenticated user on his transaction: OK
1168
    transaction = Transaction.objects.create(
1169
        amount=Decimal('10.0'), regie=regie, status=0, user=user)
1170
    url = reverse(
1171
        'api-transaction-status',
1172
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1173
    )
1174
    resp = login(app).get(url)
1175
    assert resp.json == wait_response
1176
    app.reset()
1177

  
1178
    error_msg = 'Transaction does not belong to the requesting user'
1179
    # anonymous user on other user transaction transaction: NOTOK
1180
    transaction = Transaction.objects.create(
1181
        amount=Decimal('10.0'), regie=regie, status=0, user=user)
1182
    url = reverse(
1183
        'api-transaction-status',
1184
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1185
    )
1186
    resp = app.get(url, status=403)
1187
    assert error_msg in resp.text
1188

  
1189
    # authenticated user on other user transaction transaction: NOTOK
1190
    user2 = User.objects.create_user(
1191
        'user2', password='user2', email='user2@example.com'
1192
    )
1193
    transaction = Transaction.objects.create(amount=Decimal('10.0'), regie=regie, status=0, user=user2)
1194
    url = reverse(
1195
        'api-transaction-status',
1196
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1197
    )
1198
    resp = login(app).get(url, status=403)
1199
    assert error_msg in resp.text
1200
    app.reset()
1201

  
1202
    # transaction error
1203
    transaction = Transaction.objects.create(
1204
        amount=Decimal('10.0'), regie=regie, status=eopayment.ERROR
1205
    )
1206
    url = reverse(
1207
        'api-transaction-status',
1208
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1209
    )
1210
    resp = app.get(url)
1211
    assert resp.json == {
1212
        'wait': True,
1213
        'error': True,
1214
        'error_msg': 'Payment error, you can continue and make another payment'
1215
    }
1216

  
1217
    # transaction paid
1218
    transaction = Transaction.objects.create(
1219
        amount=Decimal('10.0'), regie=regie, status=eopayment.PAID
1220
    )
1221
    url = reverse(
1222
        'api-transaction-status',
1223
        kwargs={'transaction_signature': signing.dumps(transaction.pk)}
1224
    )
1225
    resp = app.get(url)
1226
    assert resp.json == {
1227
        'wait': False,
1228
        'error': False,
1229
        'error_msg': ''
1230
    }
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()
......
383 391
    qs = urlparse.parse_qs(parsed.query)
384 392
    args = {'transaction_id': qs['transaction_id'][0], 'signed': True,
385 393
            'ok': True, 'reason': 'Paid'}
386

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

  
391 402

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