diff --git a/combo/apps/lingo/urls.py b/combo/apps/lingo/urls.py index eede96a0..bc5aba40 100644 --- a/combo/apps/lingo/urls.py +++ b/combo/apps/lingo/urls.py @@ -49,7 +49,11 @@ urlpatterns = [ url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'), url(r'^lingo/cancel/(?P\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'), url(r'^lingo/callback/(?P\w+)/$', CallbackView.as_view(), name='lingo-callback'), + url(r'^lingo/callback-payment-backend/(?P\w+)/$', + CallbackView.as_view(), name='lingo-callback-payment-backend'), url(r'^lingo/return/(?P\w+)/$', ReturnView.as_view(), name='lingo-return'), + url(r'^lingo/return-payment-backend/(?P\w+)/$', + ReturnView.as_view(), name='lingo-return-payment-backend'), url(r'^manage/lingo/', decorated_includes(manager_required, include(lingo_manager_urls))), url(r'^lingo/item/(?P[\w,-]+)/(?P[\w,-]+)/pdf$', diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py index dde06906..cb4f0895 100644 --- a/combo/apps/lingo/views.py +++ b/combo/apps/lingo/views.py @@ -43,17 +43,22 @@ from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionErro from combo.profile.utils import get_user_from_name_id from .models import (Regie, BasketItem, Transaction, TransactionOperation, - LingoBasketCell, SelfDeclaredInvoicePayment) + LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend) -def get_eopayment_object(request, regie): - options = regie.service_options +def get_eopayment_object(request, regie_or_payment_backend): + 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 options.update({ 'automatic_return_url': request.build_absolute_uri( - reverse('lingo-callback', kwargs={'regie_pk': regie.id})), + reverse('lingo-callback-payment-backend', + kwargs={'payment_backend_pk': payment_backend.id})), 'normal_return_url': request.build_absolute_uri( - reverse('lingo-return', kwargs={'regie_pk': regie.id})), + reverse('lingo-return-payment-backend', + kwargs={'payment_backend_pk': payment_backend.id})), }) - return eopayment.Payment(regie.service, options) + return eopayment.Payment(payment_backend.service, options) def get_basket_url(): @@ -434,8 +439,14 @@ class UnknownPaymentException(PaymentException): class PaymentView(View): def handle_response(self, request, backend_response, **kwargs): - regie = Regie.objects.get(id=kwargs.get('regie_pk')) - payment = get_eopayment_object(request, regie) + if 'regie_pk' in kwargs: + payment_backend = Regie.objects.get(id=kwargs['regie_pk']).payment_backend + elif 'payment_backend_pk' in kwargs: + payment_backend = PaymentBackend.objects.get(id=kwargs['payment_backend_pk']) + else: + raise Exception("A payment backend or regie primary key must be specified") + + payment = get_eopayment_object(request, payment_backend) logger = logging.getLogger(__name__) logger.info(u'received payment response: %r', backend_response) try: @@ -484,9 +495,10 @@ class PaymentView(View): transaction.status, payment_response.result)) # check if transaction belongs to right regie - if not transaction.regie == regie: - logger.warning(u'received payment for inappropriate regie ' - '(expecteds: %s, received: %s)' % (transaction.regie, regie)) + if not transaction.regie.payment_backend == payment_backend: + logger.warning(u'received payment for inappropriate payment backend ' + '(expecteds: %s, received: %s)' % ( + transaction.regie.payment_backend, payment_backend)) raise PaymentException('Invalid payment regie') transaction.status = payment_response.result diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py index ac1d4f57..45666ce5 100644 --- a/tests/test_lingo_payment.py +++ b/tests/test_lingo_payment.py @@ -18,8 +18,9 @@ from django.contrib.messages.storage.session import SessionStorage from webtest import TestApp from combo.data.models import Page -from combo.apps.lingo.models import (Regie, BasketItem, Transaction, - TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell) +from combo.apps.lingo.models import ( + Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell, + PaymentBackend) from combo.utils import sign_url from .test_manager import login @@ -27,6 +28,14 @@ from .test_manager import login pytestmark = pytest.mark.django_db +def get_url(with_payment_backend, view_name, regie): + if with_payment_backend: + return reverse( + view_name + '-payment-backend', + kwargs={'payment_backend_pk': regie.payment_backend.id}) + return reverse(view_name, kwargs={'regie_pk': regie.id}) + + @contextmanager def check_log(caplog, message): idx = len(caplog.records) @@ -37,6 +46,11 @@ def check_log(caplog, message): @pytest.fixture def regie(): + try: + payment_backend = PaymentBackend.objects.get(slug='test1') + except PaymentBackend.DoesNotExist: + payment_backend = PaymentBackend.objects.create( + label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}) try: regie = Regie.objects.get(slug='test') except Regie.DoesNotExist: @@ -45,8 +59,7 @@ def regie(): regie.slug = 'test' regie.description = 'test' regie.payment_min_amount = Decimal(4.5) - regie.service = 'dummy' - regie.service_options = {'siret': '1234'} + regie.payment_backend = payment_backend regie.save() return regie @@ -85,11 +98,12 @@ def key(request, settings): return settings.LINGO_API_SIGN_KEY def test_default_regie(): + payment_backend = PaymentBackend.objects.create(label='foo', slug='foo') Regie.objects.all().delete() - regie1 = Regie(label='foo', slug='foo') + regie1 = Regie(label='foo', slug='foo', payment_backend=payment_backend) regie1.save() assert bool(regie1.is_default) is True - regie2 = Regie(label='bar', slug='bar') + regie2 = Regie(label='bar', slug='bar', payment_backend=payment_backend) regie2.save() assert bool(regie2.is_default) is False @@ -121,7 +135,9 @@ def test_payment_min_amount(app, basket_page, regie, user): resp = resp.form.submit() assert resp.status_code == 302 -def test_successfull_items_payment(app, basket_page, regie, user): + +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_successfull_items_payment(app, basket_page, regie, user, with_payment_backend): items = {'item1': {'amount': '10.5', 'source_url': 'http://example.org/item/1'}, 'item2': {'amount': '42', 'source_url': 'http://example.org/item/2'}, 'item3': {'amount': '100', 'source_url': 'http://example.org/item/3'}, @@ -145,10 +161,11 @@ def test_successfull_items_payment(app, basket_page, regie, user): 'ok': True, 'reason': 'Paid'} # make sure return url is the user return URL assert urlparse.urlparse(qs['return_url'][0]).path.startswith( - reverse('lingo-return', kwargs={'regie_pk': regie.id})) + reverse('lingo-return-payment-backend', + kwargs={'payment_backend_pk': regie.payment_backend.id})) # simulate successful call to callback URL with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: - resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': regie.id}), params=args) + resp = app.get(get_url(with_payment_backend, 'lingo-callback', regie), params=args) assert resp.status_code == 200 # simulate successful return URL resp = app.get(qs['return_url'][0], params=args) @@ -158,7 +175,9 @@ def test_successfull_items_payment(app, basket_page, regie, user): assert 'Your payment has been succesfully registered.' in resp.text def test_add_amount_to_basket(app, key, regie, user): - other_regie = Regie(label='test2', slug='test2', service='dummy', service_options={'siret': '1234'}) + payment_backend = PaymentBackend.objects.create( + label='test2', slug='test2', service='dummy', service_options={'siret': '1234'}) + other_regie = Regie(label='test2', slug='test2', payment_backend=payment_backend) other_regie.save() user_email = 'foo@example.com' @@ -479,7 +498,9 @@ def test_cancel_basket_item_from_cell(app, key, regie, user): app.get(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=404) app.post(reverse('lingo-cancel-item', kwargs={'pk': basket_item_id}), status=403) -def test_payment_callback(app, basket_page, regie, user): + +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_payment_callback(app, basket_page, regie, user, with_payment_backend): page = Page(title='xxx', slug='index', template_name='standard') page.save() item = BasketItem.objects.create(user=user, regie=regie, @@ -497,7 +518,7 @@ def test_payment_callback(app, basket_page, regie, user): assert data['amount'] == '10.50' # call callback with GET - callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) + callback_url = get_url(with_payment_backend, 'lingo-callback', regie) with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: get_resp = app.get(callback_url, params=data) url = request.call_args[0][1] @@ -526,12 +547,14 @@ def test_payment_callback(app, basket_page, regie, user): assert Transaction.objects.get(order_id=transaction_id).status == 3 # call return view - get_resp = app.get(reverse('lingo-return', kwargs={'regie_pk': regie.pk}), params=data) + return_url = get_url(with_payment_backend, 'lingo-return', regie) + get_resp = app.get(return_url, params=data) assert get_resp.status_code == 302 resp = app.get(get_resp['Location']) assert 'Your payment has been succesfully registered.' in resp.text -def test_payment_callback_no_regie(app, basket_page, regie, user): +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_payment_callback_no_regie(app, basket_page, regie, user, with_payment_backend): item = BasketItem.objects.create(user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/') @@ -547,7 +570,7 @@ def test_payment_callback_no_regie(app, basket_page, regie, user): assert data['amount'] == '10.50' # call callback with GET - callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) + callback_url = get_url(with_payment_backend, 'lingo-callback', regie) with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: get_resp = app.get(callback_url, params=data) url = request.call_args[0][1] @@ -569,16 +592,20 @@ def test_payment_callback_no_regie(app, basket_page, regie, user): 'amount': qs['amount'][0], 'ok': True} assert data['amount'] == '11.50' -def test_nonexisting_transaction(app, regie, user): + +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_nonexisting_transaction(app, regie, user, with_payment_backend): app = login(app) data = {'transaction_id': 'unknown', 'signed': True, 'amount': '23', 'ok': True} # call callback with GET - callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) + callback_url = get_url(with_payment_backend, 'lingo-callback', regie) app.get(callback_url, params=data, status=404) -def test_payment_callback_waiting(app, basket_page, regie, user): + +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_payment_callback_waiting(app, basket_page, regie, user, with_payment_backend): item = BasketItem.objects.create(user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/') @@ -594,7 +621,7 @@ def test_payment_callback_waiting(app, basket_page, regie, user): assert data['amount'] == '10.50' # callback with WAITING state - callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) + callback_url = get_url(with_payment_backend, 'lingo-callback', regie) resp = app.get(callback_url, params=data) assert resp.status_code == 200 assert Transaction.objects.get(order_id=transaction_id).status == eopayment.WAITING @@ -615,7 +642,10 @@ def test_payment_callback_waiting(app, basket_page, regie, user): assert BasketItem.objects.get(id=item.id).payment_date assert BasketItem.get_items_to_be_paid(user).count() == 0 -def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user): + +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_payment_no_callback_just_return( + caplog, app, basket_page, regie, user, with_payment_backend): item = BasketItem.objects.create(user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/') @@ -632,7 +662,7 @@ def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user): # call return with unsigned POST with check_log(caplog, 'received unsigned payment'): - return_url = reverse('lingo-return', kwargs={'regie_pk': regie.id}) + return_url = get_url(with_payment_backend, 'lingo-return', regie) with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: get_resp = app.post(return_url, params=data) assert request.call_count == 0 @@ -654,7 +684,7 @@ def test_payment_no_callback_just_return(caplog, app, basket_page, regie, user): # call return with signed POST data['signed'] = True - return_url = reverse('lingo-return', kwargs={'regie_pk': regie.id}) + return_url = get_url(with_payment_backend, 'lingo-return', regie) 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] @@ -827,7 +857,9 @@ def test_extra_fees(app, basket_page, key, regie, user): assert resp.status_code == 200 assert Transaction.objects.get(order_id=transaction_id).status == 3 -def test_payment_callback_error(app, basket_page, regie, user): + +@pytest.mark.parametrize('with_payment_backend', [False, True]) +def test_payment_callback_error(app, basket_page, regie, user, with_payment_backend): item = BasketItem.objects.create(user=user, regie=regie, subject='test_item', amount='10.5', source_url='http://example.org/testitem/') @@ -843,7 +875,7 @@ def test_payment_callback_error(app, basket_page, regie, user): assert data['amount'] == '10.50' # call callback with GET - callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) + callback_url = get_url(with_payment_backend, 'lingo-callback', regie) with mock.patch('combo.utils.requests_wrapper.RequestsSession.request') as request: mock_response = mock.Mock() def kaboom(): diff --git a/tests/test_lingo_remote_regie.py b/tests/test_lingo_remote_regie.py index 6ae70878..b26240dc 100644 --- a/tests/test_lingo_remote_regie.py +++ b/tests/test_lingo_remote_regie.py @@ -20,7 +20,7 @@ from django.contrib.auth.models import User from combo.utils import check_query, aes_hex_encrypt from combo.data.models import Page from combo.apps.lingo.models import (Regie, ActiveItems, ItemsHistory, SelfDeclaredInvoicePayment, - Transaction, BasketItem) + Transaction, BasketItem, PaymentBackend) pytestmark = pytest.mark.django_db @@ -55,6 +55,11 @@ def admin(): @pytest.fixture def remote_regie(): + try: + payment_backend = PaymentBackend.objects.get(slug='test1') + except PaymentBackend.DoesNotExist: + payment_backend = PaymentBackend.objects.create( + label='test1', slug='test1', service='dummy', service_options={'siret': '1234'}) try: regie = Regie.objects.get(slug='remote') except Regie.DoesNotExist: @@ -63,8 +68,7 @@ def remote_regie(): regie.slug = 'remote' regie.description = 'remote' regie.payment_min_amount = Decimal(2.0) - regie.service = 'dummy' - regie.service_options = {'siret': '1234'} + regie.payment_backend = payment_backend regie.webservice_url = 'http://example.org/regie' # is_remote regie.save() return regie @@ -204,7 +208,8 @@ def test_anonymous_successful_item_payment(mock_get, mock_pay_invoice, app, remo 'ok': True, 'reason': 'Paid'} # make sure return url is the user return URL assert urlparse.urlparse(qs['return_url'][0]).path.startswith( - reverse('lingo-return', kwargs={'regie_pk': remote_regie.id})) + reverse('lingo-return-payment-backend', + kwargs={'payment_backend_pk': remote_regie.payment_backend.id})) # simulate successful return URL resp = app.get(qs['return_url'][0], params=args) assert resp.status_code == 302 @@ -326,7 +331,8 @@ def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app, 'ok': True, 'reason': 'Paid'} # make sure return url is the user return URL assert urlparse.urlparse(qs['return_url'][0]).path.startswith( - reverse('lingo-return', kwargs={'regie_pk': remote_regie.id})) + reverse('lingo-return-payment-backend', + kwargs={'payment_backend_pk': remote_regie.payment_backend.id})) # simulate payment failure mock_get.side_effect = ConnectionError('where is my hostname?') resp = app.get(qs['return_url'][0], params=args)