lingo: notify remote_item of payments asynchronously (#14627)

In case the remote_item cannot be notified synchronously of a payment, we keep
around a list of items to notify in the Transaction.to_be_paid_remote_items
field. They will be notified by the update_transactions command launched by
cron.
This commit is contained in:
Benjamin Dauvergne 2017-01-17 21:05:23 +01:00 committed by Frédéric Péters
parent a5b4dd37f1
commit 0665cb8bbd
5 changed files with 126 additions and 15 deletions

View File

@ -17,3 +17,6 @@ class Command(BaseCommand):
logger.info('transaction %r is expired', transaction.order_id)
transaction.status = EXPIRED
transaction.save()
for transaction in Transaction.objects.filter(to_be_paid_remote_items__isnull=False):
transaction.retry_notify_remote_items_of_payments()

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0029_auto_20170528_1334'),
]
operations = [
migrations.AddField(
model_name='transaction',
name='to_be_paid_remote_items',
field=models.CharField(max_length=512, null=True),
preserve_default=True,
),
]

View File

@ -299,6 +299,7 @@ class Transaction(models.Model):
regie = models.ForeignKey(Regie, null=True)
items = models.ManyToManyField(BasketItem, blank=True)
remote_items = models.CharField(max_length=512)
to_be_paid_remote_items = models.CharField(max_length=512, null=True)
start_date = models.DateTimeField(auto_now_add=True)
end_date = models.DateTimeField(null=True)
bank_data = JSONField(blank=True)
@ -327,6 +328,42 @@ class Transaction(models.Model):
EXPIRED: _('Expired')
}.get(self.status) or _('Unknown')
def first_notify_remote_items_of_payments(self):
self.notify_remote_items_of_payments(self.remote_items)
def retry_notify_remote_items_of_payments(self):
self.notify_remote_items_of_payments(self.to_be_paid_remote_items)
def notify_remote_items_of_payments(self, items):
logger = logging.getLogger(__name__)
if not items:
return
regie = self.regie
to_be_paid_remote_items = []
for item_id in items.split(','):
try:
remote_item = regie.get_invoice(user=self.user, invoice_id=item_id)
regie.pay_invoice(item_id, self.order_id, self.end_date)
except Exception:
to_be_paid_remote_items.append(item_id)
logger.exception(u'unable to notify payment for remote item %s from transaction %s',
item_id, self)
else:
logger.info(u'notified payment for remote item %s from transaction %s',
item_id, self)
subject = _('Invoice #%s') % remote_item.display_id
local_item = BasketItem.objects.create(user=self.user,
regie=regie,
source_url='',
subject=subject,
amount=remote_item.amount,
payment_date=self.end_date)
self.items.add(local_item)
self.to_be_paid_remote_items = ','.join(to_be_paid_remote_items) or None
self.save(update_fields=['to_be_paid_remote_items'])
class TransactionOperation(models.Model):
OPERATIONS = [

View File

@ -438,17 +438,7 @@ class CallbackView(View):
pass
regie.compute_extra_fees(user=transaction.user)
if transaction.remote_items:
for item_id in transaction.remote_items.split(','):
remote_item = regie.get_invoice(user=transaction.user, invoice_id=item_id)
regie.pay_invoice(item_id, transaction.order_id, transaction.end_date)
local_item = BasketItem.objects.create(user=transaction.user,
regie=regie,
source_url='',
subject=_('Invoice #%s') % remote_item.display_id,
amount=remote_item.amount,
payment_date=transaction.end_date)
transaction.items.add(local_item)
transaction.first_notify_remote_items_of_payments()
return HttpResponse()
def get(self, request, *args, **kwargs):

View File

@ -2,18 +2,18 @@ import pytest
import mock
import urlparse
from decimal import Decimal
from requests.exceptions import ConnectionError
from django.contrib.auth.models import User
from django.test.client import RequestFactory
from django.template import Context
from django.core.urlresolvers import reverse
from django.conf import settings
from django.utils import timezone
from django.core.management import call_command
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)
from combo.apps.lingo.models import (Regie, ActiveItems, ItemsHistory, SelfDeclaredInvoicePayment,
Transaction, BasketItem)
pytestmark = pytest.mark.django_db
@ -267,3 +267,64 @@ def test_self_declared_invoice(mock_get, app, remote_regie):
path = urlparse.urlparse(resp.location).path
assert path.startswith('/lingo/item/%s/' % remote_regie.id)
resp = resp.follow()
@mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
@mock.patch('combo.apps.lingo.models.requests.get')
@mock.patch('combo.apps.lingo.models.requests.post')
def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app, remote_regie):
assert remote_regie.is_remote()
encrypt_id = aes_hex_encrypt(settings.SECRET_KEY, 'F201601')
mock_json = mock.Mock()
mock_json.json.return_value = {'err': 0, 'data': INVOICES[0]}
mock_get.return_value = mock_json
mock_pay_invoice.return_value = mock.Mock(status_code=200)
resp = app.get('/lingo/item/%s/%s/' % (remote_regie.id, encrypt_id))
form = resp.form
assert 'email' in form.fields
assert form['email'].value == ''
assert 'item_url' in form.fields
assert form['item_url'].value == '/lingo/item/%s/%s/' % (remote_regie.id, encrypt_id)
assert 'item' in form.fields
assert form['item'].value == 'F201601'
assert 'regie' in form.fields
assert form['regie'].value == unicode(remote_regie.pk)
form['email'] = 'test@example.net'
resp = form.submit()
assert resp.status_code == 302
location = resp.location
assert 'dummy-payment' in location
parsed = urlparse.urlparse(location)
# get return_url and transaction id from location
qs = urlparse.parse_qs(parsed.query)
args = {'transaction_id': qs['transaction_id'][0], 'signed': True,
'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}))
# simulate successful return URL
resp = app.get(qs['return_url'][0], params=args)
assert resp.status_code == 302
assert urlparse.urlparse(resp.url).path == '/'
# simulate successful call to callback URL
mock_get.side_effect = ConnectionError('where is my hostname?')
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args)
trans = Transaction.objects.all()
b_item = BasketItem.objects.all()
assert trans.count() == 1
assert not b_item
assert trans[0].to_be_paid_remote_items
assert resp.status_code == 200
mock_get.side_effect = None
call_command('update_transactions')
assert Transaction.objects.count() == 1
assert BasketItem.objects.count() == 1
assert Transaction.objects.all()[0].to_be_paid_remote_items is None
call_command('update_transactions')