0001-lingo-add-management-command-to-retry-payment-notifi.patch
combo/apps/lingo/management/commands/notify_payments.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# |
|
3 |
# lingo - basket and payment system |
|
4 |
# Copyright (C) 2017 Entr'ouvert |
|
5 |
# |
|
6 |
# This program is free software: you can redistribute it and/or modify it |
|
7 |
# under the terms of the GNU Affero General Public License as published |
|
8 |
# by the Free Software Foundation, either version 3 of the License, or |
|
9 |
# (at your option) any later version. |
|
10 |
# |
|
11 |
# This program is distributed in the hope that it will be useful, |
|
12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 |
# GNU Affero General Public License for more details. |
|
15 |
# |
|
16 |
# You should have received a copy of the GNU Affero General Public License |
|
17 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
18 | ||
19 |
import logging |
|
20 |
import datetime |
|
21 | ||
22 |
from django.core.management.base import BaseCommand |
|
23 |
from django.utils import timezone |
|
24 | ||
25 |
from combo.apps.lingo.models import BasketItem |
|
26 | ||
27 | ||
28 |
class Command(BaseCommand): |
|
29 | ||
30 |
def handle(self, *args, **kwargs): |
|
31 |
logger = logging.getLogger(__name__) |
|
32 |
now = timezone.now() |
|
33 |
for item in BasketItem.objects.filter( |
|
34 |
notification_date__isnull=True, |
|
35 |
cancellation_date__isnull=True, |
|
36 |
payment_date__lt=now-datetime.timedelta(minutes=5)): |
|
37 |
try: |
|
38 |
item.notify_payment() |
|
39 |
except: |
|
40 |
logger.exception('error in async notification for basket item %s', item.id) |
combo/apps/lingo/views.py | ||
---|---|---|
436 | 436 |
item.save() |
437 | 437 |
try: |
438 | 438 |
item.notify_payment() |
439 |
except RuntimeError:
|
|
440 |
# ignore errors, it should be retried later on if it fails
|
|
441 |
pass
|
|
439 |
except: |
|
440 |
# ignore errors, it will be retried later on if it fails
|
|
441 |
logger.exception('error in sync notification for basket item %s', item.id)
|
|
442 | 442 |
regie.compute_extra_fees(user=transaction.user) |
443 | 443 |
if transaction.remote_items: |
444 | 444 |
transaction.first_notify_remote_items_of_payments() |
tests/test_lingo_payment.py | ||
---|---|---|
21 | 21 |
from combo.apps.lingo.models import (Regie, BasketItem, Transaction, |
22 | 22 |
TransactionOperation, RemoteItem, EXPIRED, LingoBasketCell) |
23 | 23 |
from combo.apps.lingo.management.commands.update_transactions import Command as UpdateTransactionsCommand |
24 |
from combo.apps.lingo.management.commands.notify_payments import Command as NotifyPaymentsCommand |
|
24 | 25 |
from combo.utils import sign_url |
25 | 26 | |
26 | 27 |
pytestmark = pytest.mark.django_db |
... | ... | |
573 | 574 |
resp = client.get(callback_url, data) |
574 | 575 |
assert resp.status_code == 200 |
575 | 576 |
assert Transaction.objects.get(order_id=transaction_id).status == 3 |
577 | ||
578 |
def test_payment_callback_error(regie, user): |
|
579 |
item = BasketItem.objects.create(user=user, regie=regie, |
|
580 |
subject='test_item', amount='10.5', |
|
581 |
source_url='http://example.org/testitem/') |
|
582 |
login() |
|
583 |
resp = client.post(reverse('lingo-pay'), {'regie': regie.pk}) |
|
584 |
assert resp.status_code == 302 |
|
585 |
location = resp.get('location') |
|
586 |
parsed = urlparse.urlparse(location) |
|
587 |
qs = urlparse.parse_qs(parsed.query) |
|
588 |
transaction_id = qs['transaction_id'][0] |
|
589 |
data = {'transaction_id': transaction_id, 'signed': True, |
|
590 |
'amount': qs['amount'][0], 'ok': True} |
|
591 |
assert data['amount'] == '10.50' |
|
592 | ||
593 |
# call callback with GET |
|
594 |
callback_url = reverse('lingo-callback', kwargs={'regie_pk': regie.id}) |
|
595 |
with mock.patch('combo.utils.RequestsSession.request') as request: |
|
596 |
mock_response = mock.Mock() |
|
597 |
def kaboom(): |
|
598 |
raise Exception('kaboom') |
|
599 |
mock_response.status_code = 500 |
|
600 |
mock_response.raise_for_status = kaboom |
|
601 |
request.return_value = mock_response |
|
602 |
get_resp = client.get(callback_url, data) |
|
603 |
url = request.call_args[0][1] |
|
604 |
assert url.startswith('http://example.org/testitem/jump/trigger/paid') |
|
605 |
assert get_resp.status_code == 200 |
|
606 |
assert Transaction.objects.get(order_id=transaction_id).status == 3 |
|
607 |
assert BasketItem.objects.get(id=item.id).payment_date |
|
608 |
assert not BasketItem.objects.get(id=item.id).notification_date |
|
609 | ||
610 |
# too soon |
|
611 |
NotifyPaymentsCommand().handle() |
|
612 |
assert BasketItem.objects.get(id=item.id).payment_date |
|
613 |
assert not BasketItem.objects.get(id=item.id).notification_date |
|
614 | ||
615 |
# fake delay |
|
616 |
basket_item = BasketItem.objects.get(id=item.id) |
|
617 |
basket_item.payment_date = timezone.now() - timedelta(hours=1) |
|
618 |
basket_item.save() |
|
619 | ||
620 |
with mock.patch('combo.utils.RequestsSession.request') as request: |
|
621 |
mock_response = mock.Mock() |
|
622 |
mock_response.status_code = 200 |
|
623 |
request.return_value = mock_response |
|
624 |
NotifyPaymentsCommand().handle() |
|
625 |
url = request.call_args[0][1] |
|
626 |
assert url.startswith('http://example.org/testitem/jump/trigger/paid') |
|
627 |
assert BasketItem.objects.get(id=item.id).payment_date |
|
628 |
assert BasketItem.objects.get(id=item.id).notification_date |
|
576 |
- |