From 5b4e07fe1f0a618939a2be00fe79cd3d1c39426b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Thu, 17 Aug 2017 08:57:47 +0200 Subject: [PATCH] lingo: add management command to retry payment notifications (#6638) --- .../lingo/management/commands/notify_payments.py | 40 ++++++++++++++++ combo/apps/lingo/views.py | 6 +-- tests/test_lingo_payment.py | 53 ++++++++++++++++++++++ 3 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 combo/apps/lingo/management/commands/notify_payments.py diff --git a/combo/apps/lingo/management/commands/notify_payments.py b/combo/apps/lingo/management/commands/notify_payments.py new file mode 100644 index 0000000..8a543ad --- /dev/null +++ b/combo/apps/lingo/management/commands/notify_payments.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# lingo - basket and payment system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging +import datetime + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from combo.apps.lingo.models import BasketItem + + +class Command(BaseCommand): + + def handle(self, *args, **kwargs): + logger = logging.getLogger(__name__) + now = timezone.now() + for item in BasketItem.objects.filter( + notification_date__isnull=True, + cancellation_date__isnull=True, + payment_date__lt=now-datetime.timedelta(minutes=5)): + try: + item.notify_payment() + except: + logger.exception('error in async notification for basket item %s', item.id) diff --git a/combo/apps/lingo/views.py b/combo/apps/lingo/views.py index 9edd40c..5b25545 100644 --- a/combo/apps/lingo/views.py +++ b/combo/apps/lingo/views.py @@ -436,9 +436,9 @@ class CallbackView(View): item.save() try: item.notify_payment() - except RuntimeError: - # ignore errors, it should be retried later on if it fails - pass + except: + # ignore errors, it will be retried later on if it fails + logger.exception('error in sync notification for basket item %s', item.id) regie.compute_extra_fees(user=transaction.user) if transaction.remote_items: transaction.first_notify_remote_items_of_payments() diff --git a/tests/test_lingo_payment.py b/tests/test_lingo_payment.py index fee5815..52bbbc4 100644 --- a/tests/test_lingo_payment.py +++ b/tests/test_lingo_payment.py @@ -21,6 +21,7 @@ from combo.data.models import Page from combo.apps.lingo.models import (Regie, BasketItem, Transaction, TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell) from combo.apps.lingo.management.commands.update_transactions import Command as UpdateTransactionsCommand +from combo.apps.lingo.management.commands.notify_payments import Command as NotifyPaymentsCommand from combo.utils import sign_url pytestmark = pytest.mark.django_db @@ -573,3 +574,55 @@ def test_extra_fees(key, regie, user): resp = client.get(callback_url, data) assert resp.status_code == 200 assert Transaction.objects.get(order_id=transaction_id).status == 3 + +def test_payment_callback_error(regie, user): + item = BasketItem.objects.create(user=user, regie=regie, + subject='test_item', amount='10.5', + source_url='http://example.org/testitem/') + login() + resp = client.post(reverse('lingo-pay'), {'regie': regie.pk}) + assert resp.status_code == 302 + location = resp.get('location') + parsed = urlparse.urlparse(location) + qs = urlparse.parse_qs(parsed.query) + transaction_id = qs['transaction_id'][0] + data = {'transaction_id': transaction_id, 'signed': True, + 'amount': qs['amount'][0], 'ok': True} + assert data['amount'] == '10.50' + + # call callback with GET + callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) + with mock.patch('combo.utils.RequestsSession.request') as request: + mock_response = mock.Mock() + def kaboom(): + raise Exception('kaboom') + mock_response.status_code = 500 + mock_response.raise_for_status = kaboom + request.return_value = mock_response + get_resp = client.get(callback_url, data) + url = request.call_args[0][1] + assert url.startswith('http://example.org/testitem/jump/trigger/paid') + assert get_resp.status_code == 200 + assert Transaction.objects.get(order_id=transaction_id).status == 3 + assert BasketItem.objects.get(id=item.id).payment_date + assert not BasketItem.objects.get(id=item.id).notification_date + + # too soon + NotifyPaymentsCommand().handle() + assert BasketItem.objects.get(id=item.id).payment_date + assert not BasketItem.objects.get(id=item.id).notification_date + + # fake delay + basket_item = BasketItem.objects.get(id=item.id) + basket_item.payment_date = timezone.now() - timedelta(hours=1) + basket_item.save() + + with mock.patch('combo.utils.RequestsSession.request') as request: + mock_response = mock.Mock() + mock_response.status_code = 200 + request.return_value = mock_response + NotifyPaymentsCommand().handle() + url = request.call_args[0][1] + 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 -- 2.14.1