From d30fd74c3d96d188cd7b0f505087b5bf053dc98c Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Fri, 21 May 2021 19:09:53 +0200
Subject: [PATCH] caluire-axel: add pay_invoice endpoint (#53963)

---
 passerelle/contrib/caluire_axel/models.py  | 50 ++++++++++-
 passerelle/contrib/caluire_axel/schemas.py | 11 +++
 tests/test_caluire_axel.py                 | 97 ++++++++++++++++++++++
 3 files changed, 157 insertions(+), 1 deletion(-)

diff --git a/passerelle/contrib/caluire_axel/models.py b/passerelle/contrib/caluire_axel/models.py
index 77e3a489..561a8af5 100644
--- a/passerelle/contrib/caluire_axel/models.py
+++ b/passerelle/contrib/caluire_axel/models.py
@@ -18,16 +18,17 @@ import base64
 import datetime
 
 from django.core.cache import cache
 from django.db import models
 from django.http import HttpResponse
 from django.utils.translation import ugettext_lazy as _
 
 from passerelle.base.models import BaseResource
+from passerelle.compat import json_loads
 from passerelle.contrib.utils import axel
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
 
 from . import schemas, utils
 
 
 class CaluireAxel(BaseResource):
@@ -421,17 +422,17 @@ class CaluireAxel(BaseResource):
                 self,
                 {
                     'PORTAIL': {
                         'GETFACTURESAPAYER': {
                             'IDENTFAMILLE': family_id,
                             'IDENTREGIEFACT': regie_id,
                         }
                     }
-                }
+                },
             )
         except axel.AxelError as e:
             raise APIError(
                 'Axel error: %s' % e,
                 err_code='error',
                 data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
             )
 
@@ -618,16 +619,63 @@ class CaluireAxel(BaseResource):
         )
         if not b64content:
             raise APIError('PDF error', err_code='error', http_status=404)
         response = HttpResponse(content_type='application/pdf')
         response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % invoice_id
         response.write(b64content)
         return response
 
+    @endpoint(
+        display_category=_('Invoices'),
+        display_order=5,
+        name='regie',
+        methods=['post'],
+        perm='can_access',
+        pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/pay/?$',
+        example_pattern='{regie_id}/invoice/{invoice_id}/pay',
+        description=_('Notify an invoice as paid'),
+        parameters={
+            'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
+            'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'},
+        },
+        post={
+            'request_body': {
+                'schema': {
+                    'application/json': schemas.PAYMENT_SCHEMA,
+                }
+            }
+        },
+    )
+    def pay_invoice(self, request, regie_id, invoice_id, **kwargs):
+        data = json_loads(request.body)
+        family_id, invoice_id = invoice_id.split('-')
+
+        invoice = self.get_invoice(regie_id=regie_id, family_id=family_id, invoice_id=invoice_id)
+        if invoice is None:
+            raise APIError('Invoice not found', err_code='not-found')
+
+        transaction_amount = invoice['amount']
+        payment_mode_id = data['payment_mode_id']
+        post_data = {
+            'IDFACTURE': int(invoice_id),
+            'IDENTREGIEENC': regie_id,
+            'MONTANT': transaction_amount,
+            'IDENTMODEREGLEMENT': payment_mode_id,
+        }
+        try:
+            schemas.set_paiement(self, {'PORTAIL': {'SETPAIEMENT': post_data}})
+        except axel.AxelError as e:
+            raise APIError(
+                'Axel error: %s' % e,
+                err_code='error',
+                data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
+            )
+        return {'data': True}
+
 
 class Link(models.Model):
     resource = models.ForeignKey(CaluireAxel, on_delete=models.CASCADE)
     name_id = models.CharField(blank=False, max_length=256)
     family_id = models.CharField(blank=False, max_length=128)
     person_id = models.CharField(blank=False, max_length=128)
 
     class Meta:
diff --git a/passerelle/contrib/caluire_axel/schemas.py b/passerelle/contrib/caluire_axel/schemas.py
index d1624bb6..ef704d3d 100644
--- a/passerelle/contrib/caluire_axel/schemas.py
+++ b/passerelle/contrib/caluire_axel/schemas.py
@@ -76,16 +76,17 @@ find_individus = Operation('FindIndividus')
 get_famille_individus = Operation('GetFamilleIndividus')
 get_individu = Operation('GetIndividu')
 get_list_ecole = Operation('GetListEcole')
 get_list_activites = Operation('GetListActivites')
 create_inscription_activite = Operation('CreateInscriptionActivite')
 get_factures_a_payer = Operation('GetFacturesaPayer')
 get_list_factures = Operation('GetListFactures')
 get_pdf_facture = Operation('GetPdfFacture')
+set_paiement = Operation('SetPaiement')
 
 
 LINK_SCHEMA = copy.deepcopy(
     find_individus.request_schema['properties']['PORTAIL']['properties']['FINDINDIVIDU']
 )
 for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']:
     LINK_SCHEMA['properties'].pop(key)
     LINK_SCHEMA['required'].remove(key)
@@ -111,8 +112,18 @@ REGISTER_ACTIVITY_SCHEMA = {
     },
     'required': [
         'child_id',
         'activity_id',
         'registration_start_date',
         'registration_end_date',
     ],
 }
+
+PAYMENT_SCHEMA = {
+    'type': 'object',
+    'properties': {
+        'payment_mode_id': {
+            'type': 'string',
+        },
+    },
+    'required': ['payment_mode_id'],
+}
diff --git a/tests/test_caluire_axel.py b/tests/test_caluire_axel.py
index 160a3b09..9dcfc80e 100644
--- a/tests/test_caluire_axel.py
+++ b/tests/test_caluire_axel.py
@@ -11,16 +11,17 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import copy
+import decimal
 import os
 import xml.etree.ElementTree as ET
 from contextlib import contextmanager
 
 import mock
 import pytest
 import utils
 import xmlschema
@@ -1237,8 +1238,104 @@ def test_invoice_pdf_endpoint(app, resource):
             app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy')
     assert invoice.call_args_list[0][1]['historical'] is False
 
     with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice:
         invoice.return_value = {'has_pdf': True, 'display_id': '42'}
         with mock_getdata(pdf_content, 'GetPdfFacture'):
             app.get('/caluire-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy')
     assert invoice.call_args_list[0][1]['historical'] is True
+
+
+def test_pay_invoice_endpoint_axel_error(app, resource):
+    payload = {
+        'payment_mode_id': '',
+    }
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.caluire_axel.schemas.get_factures_a_payer') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload)
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = (
+            '''<PORTAIL>
+  <GETFACTURESAPAYER>
+    %s
+  </GETFACTURESAPAYER>
+</PORTAIL>'''
+            % xml.read()
+        )
+    with mock_getdata(content, 'GetFacturesaPayer'):
+        with mock.patch('passerelle.contrib.caluire_axel.schemas.set_paiement') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.post_json(
+                '/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload
+            )
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+
+def test_pay_invoice_endpoint_bad_request(app, resource):
+    resp = app.post_json(
+        '/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params={}, status=400
+    )
+    assert resp.json['err_desc'] == "'payment_mode_id' is a required property"
+
+
+def test_pay_invoice_endpoint_no_result(app, resource):
+    payload = {
+        'payment_mode_id': '',
+    }
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = (
+            '''<PORTAIL>
+  <GETFACTURESAPAYER>
+    %s
+  </GETFACTURESAPAYER>
+</PORTAIL>'''
+            % xml.read()
+        )
+    with mock_getdata(content, 'GetFacturesaPayer'):
+        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-35/pay?NameID=yyy', params=payload)
+    assert resp.json['err_desc'] == "Invoice not found"
+    assert resp.json['err'] == 'not-found'
+    with mock_getdata(content, 'GetFacturesaPayer'):
+        resp = app.post_json('/caluire-axel/test/regie/MAREGIE/invoice/XXX-44/pay?NameID=yyy', params=payload)
+    assert resp.json['err_desc'] == "Invoice not found"
+    assert resp.json['err'] == 'not-found'
+
+
+def test_pay_invoice_endpoint(app, resource):
+    payload = {
+        'payment_mode_id': '',
+    }
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = (
+            '''<PORTAIL>
+  <GETFACTURESAPAYER>
+    %s
+  </GETFACTURESAPAYER>
+</PORTAIL>'''
+            % xml.read()
+        )
+    with mock_getdata(content, 'GetFacturesaPayer'):
+        with mock.patch('passerelle.contrib.caluire_axel.schemas.set_paiement') as operation:
+            resp = app.post_json(
+                '/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pay?NameID=yyy', params=payload
+            )
+    assert resp.json['err'] == 0
+    assert resp.json['data'] is True
+    assert operation.call_args_list[0][0][1] == {
+        'PORTAIL': {
+            'SETPAIEMENT': {
+                'IDFACTURE': 42,
+                'IDENTREGIEENC': 'MAREGIE',
+                'MONTANT': decimal.Decimal('4.94'),
+                'IDENTMODEREGLEMENT': '',
+            }
+        }
+    }
-- 
2.30.2

