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:
parent
a5b4dd37f1
commit
0665cb8bbd
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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 = [
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue