From a324ad3243f871239d9209e135700c3be5dad598 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 16 Jan 2020 14:31:18 +0100
Subject: [PATCH] toulouse_axel: endpoint to pay an invoice (#39005)

---
 functests/toulouse_axel/test_toulouse_axel.py |  29 +--
 passerelle/contrib/toulouse_axel/models.py    | 178 ++++++++++++++----
 .../xsd/Dui/Q_FormPaiementDui.xsd             |  44 +++++
 .../xsd/Dui/R_FormPaiementDui.xsd             |  36 ++++
 tests/data/toulouse_axel/invoices.xml         |  12 ++
 tests/test_toulouse_axel.py                   | 178 +++++++++++++++---
 tests/test_toulouse_axel_schema.py            |  10 +
 7 files changed, 400 insertions(+), 87 deletions(-)
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/Q_FormPaiementDui.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/R_FormPaiementDui.xsd

diff --git a/functests/toulouse_axel/test_toulouse_axel.py b/functests/toulouse_axel/test_toulouse_axel.py
index 7c6e0971..b6214603 100644
--- a/functests/toulouse_axel/test_toulouse_axel.py
+++ b/functests/toulouse_axel/test_toulouse_axel.py
@@ -4,7 +4,7 @@ import requests
 
 def test_link(conn, user):
     print("Get update management dates")
-    url = conn + '/update_management_dates'
+    url = conn + '/management_dates'
     resp = requests.get(url)
     resp.raise_for_status()
     res = resp.json()
@@ -129,33 +129,6 @@ def test_link(conn, user):
         pprint.pprint(res)
         print('\n')
 
-    print("Get invoices")
-    url = conn + '/invoices?NameID=%s' % name_id
-    resp = requests.get(url)
-    resp.raise_for_status()
-    res = resp.json()
-    assert res['err'] == 0
-    pprint.pprint(res)
-    print('\n')
-
-    data = res
-    for invoice in data['data']:
-        print("GET invoice info")
-        url = conn + '/invoice/%s?NameID=%s' % (invoice['id'], name_id)
-        resp = requests.get(url)
-        resp.raise_for_status()
-        res = resp.json()
-        assert res['err'] == 0
-        pprint.pprint(res)
-        print('\n')
-
-        if res['data']['has_pdf']:
-            print("GET invoice PDF")
-            url = conn + '/invoice/%s/pdf?NameID=%s' % (invoice['id'], name_id)
-            resp = requests.get(url)
-            resp.raise_for_status()
-            print('\n')
-
     print("Deleting link")
     url = conn + '/unlink?NameID=%s' % name_id
     resp = requests.post(url)
diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index 2eedb105..c2f6e9bc 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -17,6 +17,7 @@
 import base64
 import copy
 import datetime
+import json
 import logging
 import os
 import re
@@ -50,6 +51,26 @@ boolean_type = {
         }
     ]
 }
+datetime_type = {
+    'type': 'string',
+    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
+}
+json_date_format = '%Y-%m-%d'
+json_datetime_format = '%Y-%m-%dT%H:%M:%S'
+xml_date_format = '%d/%m/%Y'
+xml_datetime_format = '%d/%m/%Y %H:%M:%S'
+
+
+PAYMENT_SCHEMA = {
+    'type': 'object',
+    'properties': {
+        'transaction_date': copy.deepcopy(datetime_type),
+        'transaction_id': {
+            'type': 'string',
+        }
+    },
+    'required': ['transaction_date', 'transaction_id']
+}
 
 
 def indent(tree, space="  ", level=0):
@@ -97,6 +118,13 @@ def encode_bool(obj):
     return obj
 
 
+def encode_datetime(obj):
+    try:
+        return datetime.datetime.strptime(obj, json_datetime_format).strftime(xml_datetime_format)
+    except ValueError:
+        return obj
+
+
 class AxelSchema(JSONSchemaFromXMLSchema):
     type_map = {
         '{urn:AllAxelTypes}DATEREQUIREDType': 'date',
@@ -114,7 +142,7 @@ class AxelSchema(JSONSchemaFromXMLSchema):
 
     def encode_date(self, obj):
         try:
-            return datetime.datetime.strptime(obj, '%Y-%m-%d').strftime('%d/%m/%Y')
+            return datetime.datetime.strptime(obj, json_date_format).strftime(xml_date_format)
         except ValueError:
             return obj
 
@@ -124,7 +152,7 @@ class AxelSchema(JSONSchemaFromXMLSchema):
         return self.encode_date(obj)
 
     def decode_date(self, data):
-        value = datetime.datetime.strptime(data.text, '%d/%m/%Y').strftime('%Y-%m-%d')
+        value = datetime.datetime.strptime(data.text, xml_date_format).strftime(json_date_format)
         return xmlschema.ElementData(tag=data.tag, text=value, content=data.content, attributes=data.attributes)
 
     def decode_date_optional(self, data):
@@ -249,6 +277,7 @@ ref_date_gestion_dui = Operation('RefDateGestionDui')
 ref_verif_dui = Operation('RefVerifDui')
 ref_famille_dui = Operation('RefFamilleDui')
 form_maj_famille_dui = Operation('FormMajFamilleDui')
+form_paiement_dui = Operation('FormPaiementDui')
 ref_facture_a_payer = Operation('RefFactureAPayer')
 ref_facture_pdf = Operation('RefFacturePDF', prefix='')
 
@@ -471,11 +500,11 @@ class ToulouseAxel(BaseResource):
             }
         }
 
-    def get_link(self, name_id, error_status=200):
+    def get_link(self, name_id):
         try:
             return self.link_set.get(name_id=name_id)
         except Link.DoesNotExist:
-            raise APIError('Person not found', err_code='not-found', http_status=error_status)
+            raise APIError('Person not found', err_code='not-found')
 
     @endpoint(
         description=_('Delete link between user and Toulouse Axel'),
@@ -724,77 +753,104 @@ class ToulouseAxel(BaseResource):
             }
         }
 
-    def get_invoices(self, name_id, error_status=200):
-        link = self.get_link(name_id, error_status=error_status)
+    def normalize_invoice(self, invoice, dui):
+        invoice_id = '%s-%s' % (dui, invoice['IDFACTURE'])
+        data = {
+            'id': invoice_id,
+            'display_id': str(invoice['IDFACTURE']),
+            'label': invoice['LIBELLE'],
+            'amount': invoice['RESTEAPAYER'],
+            'total_amount': invoice['MONTANTTOTAL'],
+            'created': invoice['DATEEMISSION'],
+            'pay_limit_date': invoice['DATEECHEANCE'],
+            'has_pdf': True if invoice['EXISTEPDF'] == '1' else False,
+            'paid': False,
+            'vendor': {'toulouse-axel': invoice},
+        }
+        pay_limit_date = datetime.datetime.strptime(invoice['DATEECHEANCE'], '%Y-%m-%d').date()
+        data['online_payment'] = data['amount'] > 0 and pay_limit_date >= datetime.date.today()
+        return data
+
+    def get_invoices(self, regie_id, dui=None, name_id=None):
+        assert name_id or dui
+        if name_id:
+            dui = self.get_link(name_id).dui
+
         try:
-            result = ref_facture_a_payer(self, {'PORTAIL': {'DUI': {'IDDUI': link.dui}}})
+            result = ref_facture_a_payer(self, {'PORTAIL': {'DUI': {'IDDUI': dui}}})
         except AxelError as e:
             raise APIError(
                 'Axel error: %s' % e,
                 err_code='error',
-                http_status=error_status,
                 data={'xml_request': e.xml_request,
                       'xml_response': e.xml_response})
+
         data = result.json_response['DATA']['PORTAIL']['DUI']
         result = []
         for facture in data.get('FACTURES', []):
-            result.append({
-                'id': facture['IDFACTURE'],
-                'label': facture['LIBELLE'],
-                'amount': facture['RESTEAPAYER'],
-                'total_amount': facture['MONTANTTOTAL'],
-                'online_payment': True,
-                'created': facture['DATEEMISSION'],
-                'pay_limit_date': facture['DATEECHEANCE'],
-                'has_pdf': True if facture['EXISTEPDF'] == '1' else False,
-                'paid': False,
-                'vendor': {'toulouse-axel': facture},
-            })
+            if facture['IDREGIE'] != regie_id:
+                continue
+            result.append(self.normalize_invoice(facture, dui))
         return result
 
-    def get_invoice(self, name_id, invoice_id, error_status=200):
-        invoices_data = self.get_invoices(name_id, error_status=error_status)
+    def get_invoice(self, regie_id, invoice_id, dui=None, name_id=None):
+        invoices_data = self.get_invoices(regie_id=regie_id, dui=dui, name_id=name_id)
         for invoice in invoices_data:
-            if str(invoice['id']) == invoice_id:
+            if invoice['display_id'] == invoice_id:
                 return invoice
 
     @endpoint(
-        description=_("Get invoices to pay"),
+        name='regie',
         perm='can_access',
+        pattern=r'^(?P<regie_id>[\w-]+)/invoices/?$',
+        example_pattern='{regie_id}/invoices/',
+        description=_("Get invoices to pay"),
         parameters={
             'NameID': {'description': _('Publik ID')},
+            'regie_id': {'description': _('Regie identifier'), 'example_value': '42-PERISCOL'}
         })
-    def invoices(self, request, NameID):
-        invoices_data = self.get_invoices(NameID)
+    def invoices(self, request, regie_id, NameID):
+        invoices_data = self.get_invoices(regie_id=regie_id, name_id=NameID)
         return {'data': invoices_data}
 
     @endpoint(
+        name='regie',
         perm='can_access',
-        pattern=r'^(?P<invoice_id>\w+)/?$',
+        pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/?$',
+        example_pattern='{regie_id}/invoice/{invoice_id}/',
         description=_('Get invoice details'),
         parameters={
             'NameID': {'description': _('Publik ID')},
-            'invoice_id': {'description': _('Invoice identifier')}
+            'regie_id': {'description': _('Regie identifier'), 'example_value': '42-PERISCOL'},
+            'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'DUI-42'}
         })
-    def invoice(self, request, invoice_id, NameID):
-        invoice = self.get_invoice(NameID, invoice_id)
+    def invoice(self, request, regie_id, invoice_id, NameID):
+        invoice_id = invoice_id.split('-')[1]
+        invoice = self.get_invoice(regie_id=regie_id, name_id=NameID, invoice_id=invoice_id)
         if invoice is None:
             raise APIError('Invoice not found', err_code='not-found')
 
         return {'data': invoice}
 
     @endpoint(
-        name='invoice',
+        name='regie',
         perm='can_access',
-        pattern=r'^(?P<invoice_id>\w+)/pdf/?$',
+        pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/pdf/?$',
+        example_pattern='{regie_id}/invoice/{invoice_id}/pdf/',
         description=_('Get invoice as a PDF file'),
         parameters={
             'NameID': {'description': _('Publik ID')},
-            'invoice_id': {'description': _('Invoice identifier')}
+            'regie_id': {'description': _('Regie identifier'), 'example_value': '42-PERISCOL'},
+            'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'DUI-42'}
         })
-    def invoice_pdf(self, request, invoice_id, NameID):
+    def invoice_pdf(self, request, regie_id, invoice_id, NameID):
         # check that invoice is related to current user
-        invoice = self.get_invoice(NameID, invoice_id, error_status=404)
+        invoice_id = invoice_id.split('-')[1]
+        try:
+            invoice = self.get_invoice(regie_id=regie_id, name_id=NameID, invoice_id=invoice_id)
+        except APIError as e:
+            e.http_status = 404
+            raise
         if invoice is None:
             raise APIError('Invoice not found', err_code='not-found', http_status=404)
         # check that PDF is available
@@ -802,7 +858,7 @@ class ToulouseAxel(BaseResource):
             raise APIError('PDF not available', err_code='not-available', http_status=404)
 
         try:
-            result = ref_facture_pdf(self, {'PORTAIL': {'FACTUREPDF': {'IDFACTURE': int(invoice_id)}}})
+            result = ref_facture_pdf(self, {'PORTAIL': {'FACTUREPDF': {'IDFACTURE': int(invoice['display_id'])}}})
         except AxelError as e:
             raise APIError(
                 'Axel error: %s' % e,
@@ -819,6 +875,56 @@ class ToulouseAxel(BaseResource):
         response.write(b64content)
         return response
 
+    @endpoint(
+        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': '42-PERISCOL'},
+            'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'DUI-42'}
+        },
+        post={
+            'request_body': {
+                'schema': {
+                    'application/json': PAYMENT_SCHEMA,
+                }
+            }
+        })
+    def pay_invoice(self, request, regie_id, invoice_id, **kwargs):
+        data = json.loads(request.body)
+        dui, invoice_id = invoice_id.split('-')
+
+        invoices_data = self.get_invoices(regie_id=regie_id, dui=dui)
+        transaction_amount = None
+        for invoice in invoices_data:
+            if invoice['display_id'] == invoice_id:
+                transaction_amount = invoice['amount']
+                break
+        if transaction_amount is None:
+            raise APIError('Invoice not found', err_code='not-found')
+
+        transaction_id = data['transaction_id']
+        transaction_date = encode_datetime(data['transaction_date'])
+        post_data = {
+            'IDFACTURE': int(invoice_id),
+            'IDREGIEENCAISSEMENT': '',
+            'MONTANTPAYE': transaction_amount,
+            'DATEPAIEMENT': transaction_date,
+            'REFERENCE': transaction_id,
+        }
+        try:
+            form_paiement_dui(self, {'PORTAIL': {'DUI': post_data}})
+        except 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(ToulouseAxel, on_delete=models.CASCADE)
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/Q_FormPaiementDui.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_FormPaiementDui.xsd
new file mode 100644
index 00000000..82157178
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_FormPaiementDui.xsd
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
+	
+	<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes"  />
+	
+	<xsd:complexType name="DUIType">
+		<xsd:sequence>
+			<xsd:element ref="IDFACTURE" />
+			<xsd:element ref="IDREGIEENCAISSEMENT"/>
+			<xsd:element ref="MONTANTPAYE"/>
+			<xsd:element ref="DATEPAIEMENT"/>
+			<xsd:element ref="REFERENCE"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="PORTAILType">
+		<xsd:sequence>
+				<xsd:element ref="DUI"/>
+		</xsd:sequence>	
+	</xsd:complexType>
+			
+	<xsd:simpleType name="CHOIXType">
+		<xsd:restriction base="xsd:string">
+			<xsd:enumeration value="1" />
+			<xsd:enumeration value="0" />
+		</xsd:restriction>
+	</xsd:simpleType>
+	
+	<xsd:simpleType name="IDENTREGIEType">
+		<xsd:restriction base="xsd:string">
+			<xsd:maxLength value="10" />
+		</xsd:restriction>
+	</xsd:simpleType>
+	
+	<xsd:element name="IDFACTURE" type="xsd:unsignedInt"/>
+	<xsd:element name="IDREGIEENCAISSEMENT" type="IDENTREGIEType"/>
+	<xsd:element name="MONTANTPAYE" type="all:MONTANTREQUIREDType"/>
+	<xsd:element name="DATEPAIEMENT" type="all:DATETIMEType"/>
+	<xsd:element name="REFERENCE" type="xsd:string"/>
+	
+	<xsd:element name="DUI" type="DUIType"/>
+	
+	<xsd:element name="PORTAIL" type="PORTAILType"/>
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/R_FormPaiementDui.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/R_FormPaiementDui.xsd
new file mode 100644
index 00000000..ea5b13b8
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/R_FormPaiementDui.xsd
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
+	
+	<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes"  />
+
+	<xsd:redefine schemaLocation="../R_ShemaResultat.xsd">
+	    <xsd:simpleType name="TYPEType">
+			<xsd:restriction base="TYPEType">
+				<xsd:enumeration value="FormPaiementDui" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+						<xsd:element ref="DUI" minOccurs="0"/>
+					</xsd:sequence>	
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>
+	
+	<xsd:complexType name="DUIType">
+		<xsd:sequence>
+			<xsd:element ref="IDDUI" />
+			<xsd:element ref="CODE" />
+		</xsd:sequence> 
+	</xsd:complexType>
+
+	<xsd:element name="CODE" type="all:unsignedInt-or-empty"/>	
+	<xsd:element name="IDDUI" type="all:IDENTType"/>
+	
+	<xsd:element name="DUI" type="DUIType" />
+	
+</xsd:schema>
\ No newline at end of file
diff --git a/tests/data/toulouse_axel/invoices.xml b/tests/data/toulouse_axel/invoices.xml
index c1830bc8..8a9295c3 100644
--- a/tests/data/toulouse_axel/invoices.xml
+++ b/tests/data/toulouse_axel/invoices.xml
@@ -27,5 +27,17 @@
       <EXISTEPDF>0</EXISTEPDF>
       <IDFACTURE>43</IDFACTURE>
     </FACTURES>
+    <FACTURES>
+      <NUMFACTURE>44</NUMFACTURE>
+      <DATEEMISSION>12/01/2020</DATEEMISSION>
+      <DATEECHEANCE>15/01/2020</DATEECHEANCE>
+      <LIBELLE>PRESTATIONS PERISCOLAIRES DECEMBRE 2019</LIBELLE>
+      <IDFACTURATION>4244-35AA</IDFACTURATION>
+      <MONTANTTOTAL>44.94</MONTANTTOTAL>
+      <RESTEAPAYER>44.94</RESTEAPAYER>
+      <IDREGIE>AUTREREGIE</IDREGIE>
+      <EXISTEPDF>1</EXISTEPDF>
+      <IDFACTURE>44</IDFACTURE>
+    </FACTURES>
   </DUI>
 </PORTAIL>
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index e46291ff..7221ca5a 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -17,6 +17,7 @@
 from contextlib import contextmanager
 import copy
 import datetime
+import decimal
 import json
 import mock
 import os
@@ -32,6 +33,7 @@ from passerelle.contrib.toulouse_axel.models import (
     OperationResult,
     ToulouseAxel,
     form_maj_famille_dui,
+    form_paiement_dui,
     ref_date_gestion_dui,
     ref_famille_dui,
     ref_facture_a_payer,
@@ -327,6 +329,25 @@ def test_operation_ref_facture_pdf(resource, content):
             })
 
 
+@pytest.mark.parametrize('content', [
+    '<PORTAIL><DUI/></PORTAIL>',
+])
+def test_operation_form_paiement_dui(resource, content):
+    with mock_getdata(content, 'FormPaiementDui'):
+        with pytest.raises(AxelError):
+            form_paiement_dui(resource, {
+                'PORTAIL': {
+                    'DUI': {
+                        'IDFACTURE': '42',
+                        'IDREGIEENCAISSEMENT': '',
+                        'MONTANTPAYE': '42.42',
+                        'DATEPAIEMENT': '01/01/2020 12:12:12',
+                        'REFERENCE': '42',
+                    }
+                }
+            })
+
+
 def test_management_dates_endpoint_axel_error(app, resource):
     with mock.patch('passerelle.contrib.toulouse_axel.models.ref_date_gestion_dui') as operation:
         operation.side_effect = AxelError('FooBar')
@@ -1195,13 +1216,13 @@ def test_invoices_endpoint_axel_error(app, resource):
     Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
     with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_a_payer') as operation:
         operation.side_effect = AxelError('FooBar')
-        resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices?NameID=yyy')
     assert resp.json['err_desc'] == "Axel error: FooBar"
     assert resp.json['err'] == 'error'
 
 
 def test_invoices_endpoint_no_result(app, resource):
-    resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+    resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices?NameID=yyy')
     assert resp.json['err_desc'] == "Person not found"
     assert resp.json['err'] == 'not-found'
 
@@ -1216,7 +1237,7 @@ def test_invoices_endpoint_no_invoices(app, resource):
     </DUI>
 </PORTAIL>'''
     with mock_getdata(content, 'RefFactureAPayer'):
-        resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices?NameID=yyy')
     assert resp.json['err'] == 0
     assert resp.json['data'] == []
 
@@ -1227,15 +1248,16 @@ def test_invoices_endpoint(app, resource):
     with open(filepath) as xml:
         content = xml.read()
     with mock_getdata(content, 'RefFactureAPayer'):
-        resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices?NameID=yyy')
     assert resp.json['err'] == 0
     assert resp.json['data'] == [
         {
-            'id': 42,
+            'id': 'XXX-42',
+            'display_id': '42',
             'label': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
             'amount': '44.94',
             'total_amount': '44.94',
-            'online_payment': True,
+            'online_payment': False,
             'created': '2019-11-12',
             'pay_limit_date': '2019-12-04',
             'has_pdf': True,
@@ -1256,11 +1278,12 @@ def test_invoices_endpoint(app, resource):
             }
         },
         {
-            'id': 43,
+            'id': 'XXX-43',
+            'display_id': '43',
             'label': 'PRESTATIONS PERISCOLAIRES NOVEMBRE 2019',
             'amount': '44.94',
             'total_amount': '44.94',
-            'online_payment': True,
+            'online_payment': False,
             'created': '2019-12-12',
             'pay_limit_date': '2020-01-04',
             'has_pdf': False,
@@ -1281,19 +1304,50 @@ def test_invoices_endpoint(app, resource):
             }
         }
     ]
+    with mock_getdata(content, 'RefFactureAPayer'):
+        resp = app.get('/toulouse-axel/test/regie/AUTREREGIE/invoices?NameID=yyy')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {
+            'id': 'XXX-44',
+            'display_id': '44',
+            'label': 'PRESTATIONS PERISCOLAIRES DECEMBRE 2019',
+            'amount': '44.94',
+            'total_amount': '44.94',
+            'online_payment': False,
+            'created': '2020-01-12',
+            'pay_limit_date': '2020-01-15',
+            'has_pdf': True,
+            'paid': False,
+            'vendor': {
+                'toulouse-axel': {
+                    'IDFACTURATION': '4244-35AA',
+                    'IDFACTURE': 44,
+                    'IDREGIE': 'AUTREREGIE',
+                    'DATEECHEANCE': '2020-01-15',
+                    'DATEEMISSION': '2020-01-12',
+                    'EXISTEPDF': '1',
+                    'LIBELLE': 'PRESTATIONS PERISCOLAIRES DECEMBRE 2019',
+                    'MONTANTTOTAL': '44.94',
+                    'NUMFACTURE': 44,
+                    'RESTEAPAYER': '44.94',
+                 }
+            }
+        }
+    ]
 
 
 def test_invoice_endpoint_axel_error(app, resource):
     Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
     with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_a_payer') as operation:
         operation.side_effect = AxelError('FooBar')
-        resp = app.get('/toulouse-axel/test/invoice/42?NameID=yyy')
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42?NameID=yyy')
     assert resp.json['err_desc'] == "Axel error: FooBar"
     assert resp.json['err'] == 'error'
 
 
 def test_invoice_endpoint_no_result(app, resource):
-    resp = app.get('/toulouse-axel/test/invoice/42?NameID=yyy')
+    resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42?NameID=yyy')
     assert resp.json['err_desc'] == "Person not found"
     assert resp.json['err'] == 'not-found'
 
@@ -1302,7 +1356,11 @@ def test_invoice_endpoint_no_result(app, resource):
     with open(filepath) as xml:
         content = xml.read()
     with mock_getdata(content, 'RefFactureAPayer'):
-        resp = app.get('/toulouse-axel/test/invoice/35?NameID=yyy')
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-35?NameID=yyy')
+    assert resp.json['err_desc'] == "Invoice not found"
+    assert resp.json['err'] == 'not-found'
+    with mock_getdata(content, 'RefFactureAPayer'):
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-44?NameID=yyy')
     assert resp.json['err_desc'] == "Invoice not found"
     assert resp.json['err'] == 'not-found'
 
@@ -1313,14 +1371,15 @@ def test_invoice_endpoint(app, resource):
     with open(filepath) as xml:
         content = xml.read()
     with mock_getdata(content, 'RefFactureAPayer'):
-        resp = app.get('/toulouse-axel/test/invoice/42?NameID=yyy')
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42?NameID=yyy')
     assert resp.json['err'] == 0
     assert resp.json['data'] == {
-        'id': 42,
+        'id': 'XXX-42',
+        'display_id': '42',
         'label': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
         'amount': '44.94',
         'total_amount': '44.94',
-        'online_payment': True,
+        'online_payment': False,
         'created': '2019-11-12',
         'pay_limit_date': '2019-12-04',
         'has_pdf': True,
@@ -1346,7 +1405,7 @@ def test_invoice_pdf_endpoint_axel_error(app, resource):
     Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
     with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_a_payer') as operation:
         operation.side_effect = AxelError('FooBar')
-        resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404)
     assert resp.json['err_desc'] == "Axel error: FooBar"
     assert resp.json['err'] == 'error'
 
@@ -1356,13 +1415,13 @@ def test_invoice_pdf_endpoint_axel_error(app, resource):
     with mock_getdata(content, 'RefFactureAPayer'):
         with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_pdf') as operation:
             operation.side_effect = AxelError('FooBar')
-            resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+            resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404)
     assert resp.json['err_desc'] == "Axel error: FooBar"
     assert resp.json['err'] == 'error'
 
 
 def test_invoice_pdf_endpoint_no_result(app, resource):
-    resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+    resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404)
     assert resp.json['err_desc'] == "Person not found"
     assert resp.json['err'] == 'not-found'
 
@@ -1371,12 +1430,17 @@ def test_invoice_pdf_endpoint_no_result(app, resource):
     with open(filepath) as xml:
         content = xml.read()
     with mock_getdata(content, 'RefFactureAPayer'):
-        resp = app.get('/toulouse-axel/test/invoice/35/pdf?NameID=yyy', status=404)
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-35/pdf?NameID=yyy', status=404)
     assert resp.json['err_desc'] == "Invoice not found"
     assert resp.json['err'] == 'not-found'
 
     with mock_getdata(content, 'RefFactureAPayer'):
-        resp = app.get('/toulouse-axel/test/invoice/43/pdf?NameID=yyy', status=404)
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-44/pdf?NameID=yyy', status=404)
+    assert resp.json['err_desc'] == "Invoice not found"
+    assert resp.json['err'] == 'not-found'
+
+    with mock_getdata(content, 'RefFactureAPayer'):
+        resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-43/pdf?NameID=yyy', status=404)
     assert resp.json['err_desc'] == "PDF not available"
     assert resp.json['err'] == 'not-available'
 
@@ -1384,9 +1448,9 @@ def test_invoice_pdf_endpoint_no_result(app, resource):
     <PDF FILE=''></PDF>
 </PORTAIL>'''
     with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_invoice') as invoice:
-        invoice.return_value = {'has_pdf': True}
+        invoice.return_value = {'has_pdf': True, 'display_id': '42'}
         with mock_getdata(pdf_content, 'RefFacturePDF'):
-            resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+            resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404)
     assert resp.json['err_desc'] == "PDF error"
     assert resp.json['err'] == 'error'
 
@@ -1397,6 +1461,74 @@ def test_invoice_pdf_endpoint(app, resource):
     <PDF FILE='aGVsbG8gd29ybGQ='></PDF>
 </PORTAIL>'''
     with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_invoice') as invoice:
-        invoice.return_value = {'has_pdf': True}
+        invoice.return_value = {'has_pdf': True, 'display_id': '42'}
         with mock_getdata(pdf_content, 'RefFacturePDF'):
-            app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy')
+            app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy')
+
+
+def test_pay_invoice_endpoint_axel_error(app, resource):
+    payload = {
+        'transaction_date': '2020-01-01T12:00:00',
+        'transaction_id': 'foo',
+    }
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_a_payer') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.post_json('/toulouse-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/toulouse_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'RefFactureAPayer'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.form_paiement_dui') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.post_json('/toulouse-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_no_result(app, resource):
+    payload = {
+        'transaction_date': '2020-01-01T12:00:00',
+        'transaction_id': 'foo',
+    }
+    filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'RefFactureAPayer'):
+        resp = app.post_json('/toulouse-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, 'RefFactureAPayer'):
+        resp = app.post_json('/toulouse-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 = {
+        'transaction_date': '2020-01-01T12:00:00',
+        'transaction_id': 'foo',
+    }
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'RefFactureAPayer'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.form_paiement_dui') as operation:
+            resp = app.post_json('/toulouse-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': {
+            'DUI': {
+                'DATEPAIEMENT': '01/01/2020 12:00:00',
+                'IDFACTURE': 42,
+                'IDREGIEENCAISSEMENT': '',
+                'MONTANTPAYE': decimal.Decimal('44.94'),
+                'REFERENCE': 'foo',
+            }
+        }
+    }
diff --git a/tests/test_toulouse_axel_schema.py b/tests/test_toulouse_axel_schema.py
index 85cb301c..ac514af6 100644
--- a/tests/test_toulouse_axel_schema.py
+++ b/tests/test_toulouse_axel_schema.py
@@ -17,6 +17,7 @@
 import os
 
 from passerelle.contrib.toulouse_axel.models import AxelSchema
+from passerelle.contrib.toulouse_axel.models import encode_datetime
 
 import pytest
 import xmlschema
@@ -60,6 +61,15 @@ def test_date_mapping(date_type):
         assert json_data['DATE'] is None
 
 
+def test_encode_datetime():
+    # wrong format
+    assert encode_datetime('foo') == 'foo'
+    assert encode_datetime('2019-12-12') == '2019-12-12'
+    assert encode_datetime('2019-12-12T12:01:72') == '2019-12-12T12:01:72'
+    # ok
+    assert encode_datetime('2019-12-12T12:01:42') == '12/12/2019 12:01:42'
+
+
 @pytest.mark.parametrize('bool_type', ['OUINONREQUIREDType', 'OUINONType'])
 @pytest.mark.parametrize('value, expected, py_expected', [
     ('OUI', 'OUI', True),
-- 
2.20.1

