lingo: make BasketItem creation idempotent for paid invoices (#55876)

Using get_or_create() based on the remote_item_id field inside a
transaction is idempotent, the @self.items.add(...)@ operation also (if
the item is already linked it does nothing).

The new behaviour garantee that even if we cannot notify the regie's
web-service, a BasketItem is created to show the user its payment has
been recorded.
This commit is contained in:
Benjamin Dauvergne 2021-07-30 11:11:19 +02:00
parent ef770b4579
commit 18e4d09053
2 changed files with 20 additions and 9 deletions

View File

@ -857,6 +857,8 @@ class Transaction(models.Model):
for item_id in items.split(','):
try:
remote_item = regie.get_invoice(user=self.user, invoice_id=item_id, raise_4xx=True)
with atomic(savepoint=False):
self.items.add(self.create_paid_invoice_basket_item(item_id, remote_item))
regie.pay_invoice(item_id, self.order_id, self.bank_transaction_date or self.end_date)
except ObjectDoesNotExist:
# 4xx error
@ -879,21 +881,24 @@ class Transaction(models.Model):
)
else:
logger.info('notified payment for remote item %s from transaction %s', item_id, self)
self.items.add(self.create_paid_invoice_basket_item(remote_item))
self.to_be_paid_remote_items = ','.join(to_be_paid_remote_items) or None
self.save(update_fields=['to_be_paid_remote_items'])
def create_paid_invoice_basket_item(self, remote_item):
def create_paid_invoice_basket_item(self, item_id, remote_item):
subject = _('Invoice #%s') % remote_item.display_id
return BasketItem.objects.create(
user=self.user,
regie=remote_item.regie,
source_url='',
subject=subject,
amount=remote_item.amount,
payment_date=self.end_date,
basket_item, created = BasketItem.objects.get_or_create(
remote_item_id=item_id,
defaults=dict(
user=self.user,
regie=remote_item.regie,
source_url='',
subject=subject,
amount=remote_item.amount,
payment_date=self.end_date,
),
)
return basket_item
def handle_backend_response(self, response, callback=True):
logger.debug('lingo: regie "%s" handling response for transaction "%%s"' % self.regie, self.order_id)

View File

@ -1120,6 +1120,8 @@ def test_transaction_retry_failure(mock_request, remote_regie):
mock_err = mock.Mock(status_code=200)
mock_err.json.return_url = {'err': 1}
assert transaction.items.count() == 0
# error on get invoice
mock_request.side_effect = [
ConnectionError('where is my hostname?'), # get invoice 42
@ -1128,6 +1130,8 @@ def test_transaction_retry_failure(mock_request, remote_regie):
]
appconfig.update_transactions()
transaction.refresh_from_db()
assert transaction.items.count() == 1 # only 35 was found
assert set(transaction.items.values_list('remote_item_id', flat=True)) == {'35'}
assert transaction.to_be_paid_remote_items == '42' # retry for first one
# error on pay invoice
@ -1140,6 +1144,8 @@ def test_transaction_retry_failure(mock_request, remote_regie):
]
appconfig.update_transactions()
transaction.refresh_from_db()
assert transaction.items.count() == 2 # both were updated now that get_invoice worked for 42
assert set(transaction.items.values_list('remote_item_id', flat=True)) == {'35', '42'}
assert transaction.to_be_paid_remote_items == '42' # retry for first one
# unknown error on get invoice