lingo: remonter le montant payé et toutes les données de la transaction (#76572) #78
|
@ -43,7 +43,7 @@ from django.urls import reverse
|
|||
from django.utils import dateparse, timezone
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.formats import localize
|
||||
from django.utils.timezone import make_aware, now, utc
|
||||
from django.utils.timezone import localtime, make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from requests import RequestException
|
||||
|
@ -459,31 +459,6 @@ class Regie(models.Model):
|
|||
)
|
||||
raise PermissionDenied
|
||||
|
||||
def pay_invoice(self, invoice_id, transaction_id, transaction_date):
|
||||
assert timezone.is_aware(transaction_date), 'transaction_date must be an aware date'
|
||||
|
||||
url = self.webservice_url + '/invoice/%s/pay/' % invoice_id
|
||||
transaction_date = transaction_date.astimezone(utc)
|
||||
data = {
|
||||
'transaction_id': transaction_id,
|
||||
'transaction_date': transaction_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
}
|
||||
headers = {'content-type': 'application/json'}
|
||||
try:
|
||||
response = requests.post(url, remote_service='auto', data=json.dumps(data), headers=headers)
|
||||
if 400 <= response.status_code < 500:
|
||||
raise ObjectDoesNotExist()
|
||||
response.raise_for_status()
|
||||
except RequestException as e:
|
||||
raise RemoteInvoiceException from e
|
||||
try:
|
||||
resp = response.json()
|
||||
except ValueError as e:
|
||||
raise RemoteInvoiceException from e
|
||||
if resp.get('err'):
|
||||
raise RemoteInvoiceException
|
||||
return resp
|
||||
|
||||
def get_lingo_elements(self, element_type, user, payer_external_id=None, history=False):
|
||||
if not self.is_remote():
|
||||
return []
|
||||
|
@ -808,13 +783,7 @@ class BasketItem(models.Model):
|
|||
message = {'result': 'ok'}
|
||||
if status == 'paid':
|
||||
transaction = self.transaction_set.filter(status__in=(eopayment.ACCEPTED, eopayment.PAID))[0]
|
||||
message['transaction_id'] = transaction.id
|
||||
message['order_id'] = transaction.order_id
|
||||
message['bank_transaction_id'] = transaction.bank_transaction_id
|
||||
bank_transaction_date = transaction.bank_transaction_date or transaction.end_date
|
||||
bank_transaction_date = bank_transaction_date.astimezone(utc)
|
||||
message['bank_transaction_date'] = bank_transaction_date.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
message['bank_data'] = transaction.bank_data
|
||||
message.update(transaction.notification_message())
|
||||
headers = {'content-type': 'application/json'}
|
||||
r = requests.post(url, remote_service='auto', data=json.dumps(message), headers=headers, timeout=15)
|
||||
r.raise_for_status()
|
||||
|
@ -1055,6 +1024,48 @@ class Transaction(models.Model):
|
|||
def retry_notify_remote_items_of_payments(self):
|
||||
self.notify_remote_items_of_payments(self.to_be_paid_remote_items)
|
||||
|
||||
@property
|
||||
def effective_transaction_date(self):
|
||||
return localtime(self.bank_transaction_date or self.end_date)
|
||||
|
||||
def notification_message(self):
|
||||
date = self.effective_transaction_date.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
return {
|
||||
# pay_invoice expects order_id in transaction_id
|
||||
# notify did put Transaction.id here, but as Transaction.id has no
|
||||
# meaning, it's just a serial primary key from postgres, we can
|
||||
# replace it by self.order_id in notify() without a problem
|
||||
'transaction_id': self.order_id,
|
||||
# date field expected by pay_invoice
|
||||
'transaction_date': date,
|
||||
# date field expected by notify
|
||||
'bank_transaction_date': date,
|
||||
'order_id': self.order_id,
|
||||
'bank_transaction_id': self.bank_transaction_id,
|
||||
'bank_data': self.bank_data,
|
||||
'amount': str(self.amount),
|
||||
}
|
||||
|
||||
def pay_invoice(self, invoice_id):
|
||||
'''Notify a remote regie of the payment of an invoice'''
|
||||
url = self.regie.webservice_url + '/invoice/%s/pay/' % invoice_id
|
||||
data = self.notification_message()
|
||||
headers = {'content-type': 'application/json'}
|
||||
try:
|
||||
response = requests.post(url, remote_service='auto', data=json.dumps(data), headers=headers)
|
||||
if 400 <= response.status_code < 500:
|
||||
raise ObjectDoesNotExist()
|
||||
response.raise_for_status()
|
||||
except RequestException as e:
|
||||
raise RemoteInvoiceException from e
|
||||
try:
|
||||
resp = response.json()
|
||||
except ValueError as e:
|
||||
raise RemoteInvoiceException from e
|
||||
if resp.get('err'):
|
||||
raise RemoteInvoiceException
|
||||
return resp
|
||||
|
||||
def notify_remote_items_of_payments(self, items):
|
||||
if not items:
|
||||
return
|
||||
|
@ -1068,7 +1079,7 @@ class Transaction(models.Model):
|
|||
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)
|
||||
self.pay_invoice(item_id)
|
||||
except ObjectDoesNotExist:
|
||||
# 4xx error
|
||||
logger.error(
|
||||
|
|
|
@ -10,6 +10,7 @@ from unittest import mock
|
|||
import eopayment
|
||||
import httmock
|
||||
import pytest
|
||||
import responses
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -230,12 +231,16 @@ def test_transaction_manual_validation(mock_trigger_request, app, basket_page, u
|
|||
assert mock_trigger_request.call_args[0][1].startswith('http://example.org/item/1/jump/trigger/paid')
|
||||
|
||||
|
||||
@mock.patch('combo.utils.requests_wrapper.RequestsSession.request')
|
||||
@responses.activate
|
||||
@pytest.mark.parametrize('with_payment_backend', [False, True])
|
||||
def test_successfull_items_payment(mock_trigger_request, app, basket_page, regie, user, with_payment_backend):
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 403
|
||||
mock_trigger_request.return_value = mock_response
|
||||
def test_successfull_items_payment(app, basket_page, regie, user, freezer, with_payment_backend):
|
||||
freezer.move_to('2023-04-13T12:00:00+02:00')
|
||||
mock = responses.add(
|
||||
responses.GET,
|
||||
re.compile(r'http://example.org/item/\d/jump/trigger/paid'),
|
||||
json={'err': 0},
|
||||
status=403,
|
||||
)
|
||||
items = {
|
||||
'item1': {'amount': '10.5', 'source_url': 'http://example.org/item/1/'},
|
||||
'item2': {'amount': '42', 'source_url': 'http://example.org/item/2/'},
|
||||
|
@ -249,22 +254,47 @@ def test_successfull_items_payment(mock_trigger_request, app, basket_page, regie
|
|||
|
||||
resp = login(app).get('/test_basket_cell/')
|
||||
resp = resp.form.submit()
|
||||
assert mock_trigger_request.call_count == 4 # source_url/jump/trigger/paid items had been verified
|
||||
assert mock.call_count == 4 # source_url/jump/trigger/paid items had been verified
|
||||
assert resp.status_code == 302
|
||||
location = resp.location
|
||||
assert '://dummy-payment' in location
|
||||
parsed = urllib.parse.urlparse(location)
|
||||
# get return_url and transaction id from location
|
||||
qs = urllib.parse.parse_qs(parsed.query)
|
||||
args = {'transaction_id': qs['transaction_id'][0], 'signed': True, 'ok': True, 'reason': 'Paid'}
|
||||
transaction_id = qs['transaction_id'][0]
|
||||
args = {'transaction_id': transaction_id, 'signed': True, 'ok': True, 'reason': 'Paid'}
|
||||
# make sure return url is the user return URL
|
||||
assert urllib.parse.urlparse(qs['return_url'][0]).path.startswith(
|
||||
'/lingo/return-payment-backend/%s/' % regie.payment_backend.id
|
||||
)
|
||||
# simulate successful call to callback URL
|
||||
with mock.patch('combo.utils.requests_wrapper.RequestsSession.request'):
|
||||
resp = app.get(get_url(with_payment_backend, 'lingo-callback', regie), params=args)
|
||||
mock = responses.add(
|
||||
responses.POST,
|
||||
re.compile(r'http://example.org/item/\d/jump/trigger/paid'),
|
||||
json={'err': 0},
|
||||
status=200,
|
||||
)
|
||||
resp = app.get(get_url(with_payment_backend, 'lingo-callback', regie), params=args)
|
||||
assert resp.status_code == 200
|
||||
assert json.loads(responses.calls[-1].request.body) == {
|
||||
'result': 'ok',
|
||||
'amount': '506.50',
|
||||
# transaction_id and order_id should be the same
|
||||
'transaction_id': transaction_id,
|
||||
'order_id': transaction_id,
|
||||
'bank_transaction_id': transaction_id,
|
||||
# default timezone in combo is UTC :/
|
||||
'bank_transaction_date': '2023-04-13T10:00:00',
|
||||
'transaction_date': '2023-04-13T10:00:00',
|
||||
'bank_data': {
|
||||
'__bank_id': transaction_id,
|
||||
'ok': ['True'],
|
||||
'reason': ['Paid'],
|
||||
'signed': ['True'],
|
||||
'transaction_id': [transaction_id],
|
||||
},
|
||||
}
|
||||
|
||||
# simulate successful return URL
|
||||
resp = app.get(qs['return_url'][0], params=args)
|
||||
# redirect to payment status
|
||||
|
|
|
@ -7,6 +7,7 @@ from unittest import mock
|
|||
import eopayment
|
||||
import httmock
|
||||
import pytest
|
||||
import responses
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -564,9 +565,9 @@ def test_remote_regie_past_invoices_cell_failure(mock_get, app, remote_regie):
|
|||
assert 'Regie "Remote" is unavailable, please retry later.' in content
|
||||
|
||||
|
||||
@mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
|
||||
@mock.patch('combo.utils.requests_wrapper.RequestsSession.send')
|
||||
def test_anonymous_successful_item_payment(mock_send, mock_pay_invoice, app, remote_regie):
|
||||
@responses.activate
|
||||
def test_anonymous_successful_item_payment(app, remote_regie, freezer):
|
||||
freezer.move_to('2023-04-13T12:00:00+02:00') # April 13th 2022 12:00:00 Europe/Paris tz
|
||||
page = Page.objects.create(title='xxx', slug='index', template_name='standard')
|
||||
cell = InvoicesCell.objects.create(
|
||||
regie='remote',
|
||||
|
@ -585,16 +586,17 @@ def test_anonymous_successful_item_payment(mock_send, mock_pay_invoice, app, rem
|
|||
invoices = copy.deepcopy(INVOICES)
|
||||
invoices[0]['amount'] = '100.00'
|
||||
invoices[0]['amount_paid'] = '23.45'
|
||||
mock_response = mock.Mock(status_code=200)
|
||||
mock_response.json.return_value = {'err': 0, 'data': invoices[0]}
|
||||
mock_send.return_value = mock_response
|
||||
mock_pay_invoice.return_value = mock.Mock(status_code=200)
|
||||
|
||||
invoice_response = responses.add(
|
||||
responses.GET, 'http://example.org/regie/invoice/F201601/', json={'err': 0, 'data': invoices[0]}
|
||||
)
|
||||
|
||||
resp = app.get('/lingo/item/%s/%s/%s/' % (remote_regie.id, encrypt_id, encrypt_reference))
|
||||
assert '<button>Pay</button>' in resp
|
||||
assert 'Total amount: <span class="amount">123.45€</span>' in resp.text
|
||||
assert 'Amount to pay: <span class="amount">100.00€</span>' in resp.text
|
||||
assert 'Amount already paid: <span class="amount">23.45€</span>' in resp.text
|
||||
url = mock_send.call_args[0][0].url
|
||||
url = invoice_response.calls[0].request.url
|
||||
scheme, netloc, path, dummy, querystring, dummy = urllib.parse.urlparse(url)
|
||||
assert scheme == 'http'
|
||||
assert netloc == 'example.org'
|
||||
|
@ -604,13 +606,13 @@ def test_anonymous_successful_item_payment(mock_send, mock_pay_invoice, app, rem
|
|||
assert 'payer_external_id' not in query
|
||||
|
||||
# with payer_external_id
|
||||
mock_send.reset_mock()
|
||||
invoice_response.calls.reset()
|
||||
resp = app.get(
|
||||
'/lingo/item/%s/%s/%s/?payer_external_id=%s'
|
||||
% (remote_regie.id, encrypt_id, encrypt_reference, encrypt_payer)
|
||||
)
|
||||
assert '<button>Pay</button>' in resp
|
||||
url = mock_send.call_args[0][0].url
|
||||
url = invoice_response.calls[0].request.url
|
||||
scheme, netloc, path, dummy, querystring, dummy = urllib.parse.urlparse(url)
|
||||
assert scheme == 'http'
|
||||
assert netloc == 'example.org'
|
||||
|
@ -633,10 +635,9 @@ def test_anonymous_successful_item_payment(mock_send, mock_pay_invoice, app, rem
|
|||
# invoice without amount_paid
|
||||
cell.include_pay_button = True
|
||||
cell.save()
|
||||
mock_response = mock.Mock(status_code=200)
|
||||
mock_response.json.return_value = {'err': 0, 'data': INVOICES[0]}
|
||||
mock_send.return_value = mock_response
|
||||
mock_pay_invoice.return_value = mock.Mock(status_code=200)
|
||||
responses.replace(
|
||||
responses.GET, 'http://example.org/regie/invoice/F201601/', json={'err': 0, 'data': INVOICES[0]}
|
||||
)
|
||||
resp = app.get('/lingo/item/%s/%s/%s/' % (remote_regie.id, encrypt_id, encrypt_reference))
|
||||
assert 'Total amount: <span class="amount">123.45€</span>' in resp.text
|
||||
assert 'Amount to pay: <span class="amount">123.45€</span>' in resp.text
|
||||
|
@ -677,10 +678,14 @@ def test_anonymous_successful_item_payment(mock_send, mock_pay_invoice, app, rem
|
|||
parsed = urllib.parse.urlparse(location)
|
||||
# get return_url and transaction id from location
|
||||
qs = urllib.parse.parse_qs(parsed.query)
|
||||
args = {'transaction_id': qs['transaction_id'][0], 'signed': True, 'ok': True, 'reason': 'Paid'}
|
||||
transaction_id = qs['transaction_id'][0]
|
||||
args = {'transaction_id': transaction_id, 'signed': True, 'ok': True, 'reason': 'Paid'}
|
||||
# make sure return url is the user return URL
|
||||
return_url = qs['return_url'][0]
|
||||
assert urllib.parse.urlparse(return_url).path.startswith('/lingo/return-payment-backend')
|
||||
payment_response = responses.add(
|
||||
responses.POST, 'http://example.org/regie/invoice/F201601/pay/', json={'err': 0}
|
||||
)
|
||||
# simulate successful return URL
|
||||
resp = app.get(qs['return_url'][0], params=args)
|
||||
# redirect to payment status
|
||||
|
@ -690,6 +695,25 @@ def test_anonymous_successful_item_payment(mock_send, mock_pay_invoice, app, rem
|
|||
assert urllib.parse.urlparse(resp.html.find('a', {'id': 'next-url'})['href']).path == '/'
|
||||
# simulate successful call to callback URL
|
||||
resp = app.get(reverse('lingo-callback', kwargs={'regie_pk': remote_regie.id}), params=args)
|
||||
assert json.loads(payment_response.calls[0].request.body) == {
|
||||
# transaction_id and order_id should be the same
|
||||
'transaction_id': transaction_id,
|
||||
'order_id': transaction_id,
|
||||
'bank_transaction_id': transaction_id,
|
||||
# date fields should be the same (same reason as above)
|
||||
# default timezone in combo is UTC :/
|
||||
'bank_transaction_date': '2023-04-13T10:00:00',
|
||||
'transaction_date': '2023-04-13T10:00:00',
|
||||
'amount': '123.45',
|
||||
'bank_data': {
|
||||
'__bank_id': transaction_id,
|
||||
'ok': ['True'],
|
||||
'reason': ['Paid'],
|
||||
'signed': ['True'],
|
||||
'transaction_id': [transaction_id],
|
||||
},
|
||||
}
|
||||
|
||||
trans = Transaction.objects.all()
|
||||
b_item = BasketItem.objects.all()
|
||||
|
||||
|
@ -830,7 +854,7 @@ def test_remote_item_failure(mock_get, app, remote_regie):
|
|||
assert '<h2>Technical error: impossible to retrieve invoices.</h2>' in resp.text
|
||||
|
||||
|
||||
@mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
|
||||
@mock.patch('combo.apps.lingo.models.Transaction.pay_invoice')
|
||||
@mock.patch('combo.apps.lingo.models.requests.get')
|
||||
def test_pay_remote_item_failure(mock_get, mock_pay_invoice, app, remote_regie):
|
||||
page = Page.objects.create(title='xxx', slug='lingo', template_name='standard')
|
||||
|
@ -949,7 +973,7 @@ def test_self_declared_invoice(mock_get, app, remote_regie):
|
|||
assert '<button>Pay</button>' in resp
|
||||
|
||||
|
||||
@mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
|
||||
@mock.patch('combo.apps.lingo.models.Transaction.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):
|
||||
|
@ -1029,7 +1053,7 @@ def test_remote_item_payment_failure(mock_post, mock_get, mock_pay_invoice, app,
|
|||
|
||||
|
||||
@pytest.mark.parametrize('can_pay_only_one_basket_item', [False, True])
|
||||
@mock.patch('combo.apps.lingo.models.Regie.pay_invoice')
|
||||
@mock.patch('combo.apps.lingo.models.Transaction.pay_invoice')
|
||||
@mock.patch('combo.apps.lingo.models.requests.get')
|
||||
def test_remote_invoice_successfull_payment_redirect(
|
||||
mock_get, mock_pay_invoice, can_pay_only_one_basket_item, app, remote_regie
|
||||
|
|
Loading…
Reference in New Issue