From 29f4774d2be811366ed5d942d835d2936b82ea7c Mon Sep 17 00:00:00 2001 From: Nicolas ROCHE Date: Wed, 12 Apr 2023 18:34:20 +0200 Subject: [PATCH] toulouse-maelis: make payInvoices call asynchronous (#76394) --- passerelle/contrib/toulouse_maelis/models.py | 70 ++++++++--- tests/test_toulouse_maelis.py | 122 ++++++++++++++++++- 2 files changed, 176 insertions(+), 16 deletions(-) diff --git a/passerelle/contrib/toulouse_maelis/models.py b/passerelle/contrib/toulouse_maelis/models.py index 25edf939..3157f073 100644 --- a/passerelle/contrib/toulouse_maelis/models.py +++ b/passerelle/contrib/toulouse_maelis/models.py @@ -26,7 +26,7 @@ from urllib.parse import urljoin import zeep from dateutil import rrule from django.core.serializers.json import DjangoJSONEncoder -from django.db import models +from django.db import models, transaction from django.db.models import JSONField from django.http import Http404, HttpResponse from django.utils import dateformat @@ -278,6 +278,17 @@ class ToulouseMaelis(BaseResource, HTTPResource): else: self.logger.info('Réferentiels mis à jour.') + def notify_invoices_paid(self): + invoices = self.invoice_set.filter( + maelis_notification_date__isnull=True, + lingo_notification_date__lt=now() - datetime.timedelta(minutes=15), + ) + for invoice in invoices: + invoice.notify() + + def hourly(self): + self.notify_invoices_paid() + def get_referential(self, referential_name, id=None, q=None, limit=None, distinct=True): if id is not None: queryset = self.referential.filter(referential_name=referential_name, item_id=id) @@ -3978,21 +3989,26 @@ class ToulouseMaelis(BaseResource, HTTPResource): ) def pay_invoice(self, request, regie_id, invoice_id, post_data, **kwargs): invoice = self.get_invoice(regie_id, invoice_id) + if invoice.status() != 'created': + raise APIError('Invoice already paid') - result = self.call( - 'Invoice', - 'payInvoices', - numDossier=invoice.family_id, - codeRegie=regie_id, - amount=str( - Decimal(invoice.maelis_data['amountInvoice']) - Decimal(invoice.maelis_data['amountPaid']) - ), - datePaiement=post_data['transaction_date'], - refTransaction=post_data['transaction_id'], - numInvoices=[invoice.invoice_id], - numPerson=invoice.maelis_data['payer']['num'], + invoice.lingo_data = post_data + invoice.lingo_notification_date = now() + invoice.save(update_fields=['updated', 'lingo_notification_date', 'lingo_data']) + self.add_job( + 'notify_invoice_paid_job', + regie_id=regie_id, + invoice_id=invoice.invoice_id, + natural_id='%s/%s' % (regie_id, invoice.invoice_id), ) - return {'data': result} + return {'data': 'ok'} + + def notify_invoice_paid_job(self, regie_id, invoice_id): + try: + invoice = self.invoice_set.get(regie_id=regie_id, invoice_id=invoice_id) + except Invoice.DoesNotExist: + return + invoice.notify() @endpoint( display_category='Facture', @@ -4125,6 +4141,32 @@ class Invoice(models.Model): invoice.update({'pay_limit_date': '', 'online_payment': False}) return invoice + @transaction.atomic + def notify(self): + obj = Invoice.objects.select_for_update().get(pk=self.pk) + if obj.maelis_notification_date is not None: + return True + try: + result = obj.resource.call( + 'Invoice', + 'payInvoices', + numDossier=obj.family_id, + codeRegie=obj.regie_id, + amount=str( + Decimal(obj.maelis_data['amountInvoice']) - Decimal(obj.maelis_data['amountPaid']) + ), + datePaiement=obj.lingo_data['transaction_date'], + refTransaction=obj.lingo_data['transaction_id'], + numInvoices=[obj.invoice_id], + numPerson=obj.maelis_data['payer']['num'], + ) + except SOAPServiceUnreachable: + return False + obj.maelis_notification_date = now() + obj.maelis_notification_data = result + obj.save() + return True + class Meta: ordering = ('resource', 'regie_id', 'invoice_id') unique_together = [['resource', 'regie_id', 'invoice_id']] diff --git a/tests/test_toulouse_maelis.py b/tests/test_toulouse_maelis.py index 011f9269..3f18aad8 100644 --- a/tests/test_toulouse_maelis.py +++ b/tests/test_toulouse_maelis.py @@ -28,6 +28,7 @@ from zeep import Settings from zeep.helpers import serialize_object from passerelle.apps.base_adresse.models import BaseAdresse, CityModel +from passerelle.base.models import Job from passerelle.contrib.toulouse_maelis.models import Link, Referential, ToulouseMaelis from passerelle.contrib.toulouse_maelis.utils import get_public_criterias from passerelle.utils.jsonresponse import APIError @@ -9021,6 +9022,9 @@ def test_invoices_history(invoice_service, con, app): 'reference_id': '8', } + invoice = con.invoice_set.get(regie_id=104, invoice_id=8) + assert invoice.status() == 'notified' + def test_invoices_history_not_linked_error(con, app): url = get_endpoint('regie/104/invoices/history') @@ -9074,7 +9078,41 @@ def test_invoice_not_found_invoice(invoice_service, con, app): assert resp.json['err_desc'] == 'Invoice not found' -def test_pay_invoice(invoice_service, con, app): +def test_pay_invoice(invoice_service, con, app, freezer): + invoice_service.add_soap_response('readInvoices', get_xml_file('R_read_invoices.xml')) + + resp = app.get(get_endpoint('regie/104/invoice/1312-30') + '?family_id=1312') + assert resp.json['err'] == 0 + assert resp.json['data']['amount_paid'] == '0' + assert resp.json['data']['paid'] is False + assert resp.json['data']['amount'] == '162.3' + assert resp.json['data']['online_payment'] is True + assert resp.json['data']['pay_limit_date'] == '2023-04-30' + + url = get_endpoint('regie/104/invoice/1312-30/pay/') + data = { + 'transaction_date': '2023-03-03T18:38:00', + 'transaction_id': 'xxx', + } + + freezer.move_to('2023-03-03 18:39:00') + resp = app.post_json(url + '?NameID=ignored', params=data) + assert resp.json['err'] == 0 + invoice = con.invoice_set.get(regie_id=104, invoice_id=30) + assert invoice.lingo_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:39:00' + assert invoice.maelis_notification_date is None + assert invoice.status() == 'paid' + + resp = app.get(get_endpoint('regie/104/invoice/1312-30') + '?family_id=1312') + assert resp.json['err'] == 0 + assert resp.json['data']['amount_paid'] == '162.3' + assert resp.json['data']['paid'] is True + assert resp.json['data']['amount'] == '0.0' + assert resp.json['data']['online_payment'] is False + assert resp.json['data']['pay_limit_date'] == '' + + +def test_pay_invoice_job(invoice_service, con, app, freezer): def request_check(request): assert dict(serialize_object(request)) == { 'numDossier': 1312, @@ -9092,15 +9130,82 @@ def test_pay_invoice(invoice_service, con, app): invoice_service.add_soap_response( 'payInvoices', get_xml_file('R_pay_invoices.xml'), request_check=request_check ) + url = get_endpoint('regie/104/invoice/1312-30/pay/') data = { 'transaction_date': '2023-03-03T18:38:00', 'transaction_id': 'xxx', } + freezer.move_to('2023-03-03 18:39:00') resp = app.post_json(url + '?NameID=ignored', params=data) assert resp.json['err'] == 0 - assert resp.json['data'] == 4 + invoice = con.invoice_set.get(regie_id=104, invoice_id=30) + assert invoice.lingo_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:39:00' + assert invoice.maelis_notification_date is None + assert invoice.maelis_notification_data is None + assert invoice.status() == 'paid' + job = Job.objects.get(method_name='notify_invoice_paid_job', natural_id='104/30') + assert job.status == 'registered' + + freezer.move_to('2023-03-03 18:40:00') + con.jobs() + invoice = con.invoice_set.get(regie_id=104, invoice_id=30) + assert invoice.maelis_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:40:00' + assert invoice.maelis_notification_data == 4 + assert invoice.status() == 'notified' + job = Job.objects.get(method_name='notify_invoice_paid_job', natural_id='104/30') + assert job.status == 'completed' + assert job.update_timestamp > job.creation_timestamp + + +def test_pay_invoice_cron(invoice_service, con, app, freezer): + def request_check(request): + assert dict(serialize_object(request)) == { + 'numDossier': 1312, + 'numPerson': 261483, + 'lastName': None, + 'firstName': None, + 'codeRegie': 104, + 'amount': 162.3, + 'datePaiement': datetime.datetime(2023, 3, 3, 18, 38), + 'refTransaction': 'xxx', + 'numInvoices': [30], + } + + invoice_service.add_soap_response('readInvoices', get_xml_file('R_read_invoices.xml')) + invoice_service.add_soap_response( + 'payInvoices', get_xml_file('R_pay_invoices.xml'), request_check=request_check + ) + + url = get_endpoint('regie/104/invoice/1312-30/pay/') + data = { + 'transaction_date': '2023-03-03T18:38:00', + 'transaction_id': 'xxx', + } + + freezer.move_to('2023-03-03 18:39:00') + resp = app.post_json(url + '?NameID=ignored', params=data) + assert resp.json['err'] == 0 + invoice = con.invoice_set.get(regie_id=104, invoice_id=30) + assert invoice.lingo_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:39:00' + assert invoice.maelis_notification_date is None + assert invoice.maelis_notification_data is None + assert invoice.status() == 'paid' + + freezer.move_to('2023-03-03 18:40:00') + con.notify_invoices_paid() + invoice = con.invoice_set.get(regie_id=104, invoice_id=30) + assert invoice.maelis_notification_date is None + assert invoice.maelis_notification_data is None + assert invoice.status() == 'paid' + + freezer.move_to('2023-03-03 18:55:00') + con.notify_invoices_paid() + invoice = con.invoice_set.get(regie_id=104, invoice_id=30) + assert invoice.maelis_notification_date.strftime('%Y-%m-%d %H:%M:%S') == '2023-03-03 18:55:00' + assert invoice.maelis_notification_data == 4 + assert invoice.status() == 'notified' def test_pay_invoice_wrong_referential_key_error(con, app): @@ -9129,6 +9234,19 @@ def test_pay_invoice_not_found_invoice(invoice_service, con, app): assert resp.json['err_desc'] == 'Invoice not found' +def test_pay_historical_invoice(invoice_service, con, app): + invoice_service.add_soap_response('readInvoices', get_xml_file('R_read_invoices.xml')) + url = get_endpoint('regie/104/invoice/1312-8/pay/') + data = { + 'transaction_date': '2023-03-03T18:38:00', + 'transaction_id': 'xxx', + } + + resp = app.post_json(url + '?NameID=ignored', params=data) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == 'Invoice already paid' + + def test_invoice_pdf(invoice_service, con, app): def request_check(request): assert request.codeRegie == 104