From bce3e7bf73967d22d2326f069f638bd10a8e2f51 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 6 Dec 2019 13:24:31 +0100
Subject: [PATCH 2/2] toulouse_axel: get invoices (#38230)

---
 functests/toulouse_axel/test_toulouse_axel.py |  27 +++
 passerelle/contrib/toulouse_axel/models.py    | 113 ++++++++-
 .../xsd/Dui/Q_RefFactureAPayer.xsd            |  23 ++
 .../xsd/Dui/R_RefFactureAPayer.xsd            |  74 ++++++
 .../toulouse_axel/xsd/Q_RefFacturePDF.xsd     |  20 ++
 .../toulouse_axel/xsd/R_RefFacturePDF.xsd     |  30 +++
 tests/data/toulouse_axel/invoices.xml         |  31 +++
 tests/test_toulouse_axel.py                   | 226 ++++++++++++++++++
 8 files changed, 531 insertions(+), 13 deletions(-)
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/Q_RefFactureAPayer.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/R_RefFactureAPayer.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Q_RefFacturePDF.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/R_RefFacturePDF.xsd
 create mode 100644 tests/data/toulouse_axel/invoices.xml

diff --git a/functests/toulouse_axel/test_toulouse_axel.py b/functests/toulouse_axel/test_toulouse_axel.py
index ec1753d8..e9e4373f 100644
--- a/functests/toulouse_axel/test_toulouse_axel.py
+++ b/functests/toulouse_axel/test_toulouse_axel.py
@@ -40,6 +40,33 @@ 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 46b7c08d..7a7ab5fc 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -14,6 +14,7 @@
 # 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 base64
 import copy
 import datetime
 import logging
@@ -23,6 +24,7 @@ import xml.etree.ElementTree as ET
 
 
 from django.db import models
+from django.http import HttpResponse
 from django.utils.encoding import force_text
 from django.utils.translation import ugettext_lazy as _
 
@@ -70,10 +72,10 @@ def xml_schema_converter(name, root_element):
 
 
 class Operation(object):
-    def __init__(self, operation):
+    def __init__(self, operation, prefix='Dui/'):
         self.operation = operation
-        self.request_converter = xml_schema_converter('Dui/Q_%s.xsd' % operation, 'PORTAIL')
-        self.response_converter = xml_schema_converter('Dui/R_%s.xsd' % operation, 'PORTAILSERVICE')
+        self.request_converter = xml_schema_converter('%sQ_%s.xsd' % (prefix, operation), 'PORTAIL')
+        self.response_converter = xml_schema_converter('%sR_%s.xsd' % (prefix, operation), 'PORTAILSERVICE')
         self.name = re.sub(
             '(.?)([A-Z])',
             lambda s: s.group(1) + ('-' if s.group(1) else '') + s.group(2).lower(),
@@ -109,6 +111,8 @@ class Operation(object):
 
 ref_verif_dui = Operation('RefVerifDui')
 ref_famille_dui = Operation('RefFamilleDui')
+ref_facture_a_payer = Operation('RefFactureAPayer')
+ref_facture_pdf = Operation('RefFacturePDF', prefix='')
 
 
 class ToulouseAxel(BaseResource):
@@ -169,6 +173,12 @@ class ToulouseAxel(BaseResource):
             raise APIError('Data conflict', err='conflict')
         return {'link': link.pk, 'created': created, 'dui': link.dui}
 
+    def get_link(self, name_id, http_status=200):
+        try:
+            return self.link_set.get(name_id=name_id)
+        except Link.DoesNotExist:
+            raise APIError('Person not found', err='not-found', http_status=http_status)
+
     @endpoint(
         description=_('Delete link between user and Toulouse Axel'),
         methods=['post'],
@@ -177,21 +187,13 @@ class ToulouseAxel(BaseResource):
             'NameID': {'description': _('Publik ID')},
         })
     def unlink(self, request, NameID):
-        try:
-            link = self.link_set.get(name_id=NameID)
-        except Link.DoesNotExist:
-            raise APIError('Person not found', err='not-found')
-
+        link = self.get_link(NameID)
         link_id = link.pk
         link.delete()
         return {'link': link_id, 'deleted': True, 'dui': link.dui}
 
     def get_family_data(self, name_id):
-        try:
-            link = self.link_set.get(name_id=name_id)
-        except Link.DoesNotExist:
-            raise APIError('Person not found', err='not-found')
-
+        link = self.get_link(name_id)
         try:
             result = ref_famille_dui(self, {'PORTAIL': {'DUI': {'IDDUI': link.dui}}})
         except AxelError as e:
@@ -224,6 +226,91 @@ class ToulouseAxel(BaseResource):
 
         raise APIError('Child not found', err='not-found')
 
+    def get_invoices(self, name_id, http_status=200):
+        link = self.get_link(name_id, http_status=http_status)
+        try:
+            result = ref_facture_a_payer(self, {'PORTAIL': {'DUI': {'IDDUI': link.dui}}})
+        except AxelError as e:
+            raise APIError('Axel error: %s' % e, err='error', http_status=http_status)
+        data = result['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,
+                'toulouse-axel': facture,
+            })
+        return result
+
+    def get_invoice(self, name_id, invoice_id, http_status=200):
+        invoices_data = self.get_invoices(name_id, http_status=http_status)
+        for invoice in invoices_data:
+            if str(invoice['id']) == invoice_id:
+                return invoice
+
+    @endpoint(
+        description=_("Get invoices to pay"),
+        perm='can_access',
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+        })
+    def invoices(self, request, NameID):
+        invoices_data = self.get_invoices(NameID)
+        return {'data': invoices_data}
+
+    @endpoint(
+        perm='can_access',
+        pattern=r'^(?P<invoice_id>\w+)/?$',
+        description=_('Get invoice details'),
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+            'invoice_id': {'description': _('Invoice identifier')}
+        })
+    def invoice(self, request, invoice_id, NameID):
+        invoice = self.get_invoice(NameID, invoice_id)
+        if invoice is None:
+            raise APIError('Invoice not found', err='not-found')
+
+        return {'data': invoice}
+
+    @endpoint(
+        name='invoice',
+        perm='can_access',
+        pattern=r'^(?P<invoice_id>\w+)/pdf/?$',
+        description=_('Get invoice as a PDF file'),
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+            'invoice_id': {'description': _('Invoice identifier')}
+        })
+    def invoice_pdf(self, request, invoice_id, NameID):
+        # check that invoice is related to current user
+        invoice = self.get_invoice(NameID, invoice_id, http_status=404)
+        if invoice is None:
+            raise APIError('Invoice not found', err='not-found', http_status=404)
+        # check that PDF is available
+        if not invoice['has_pdf']:
+            raise APIError('PDF not available', err='not-available', http_status=404)
+
+        try:
+            result = ref_facture_pdf(self, {'PORTAIL': {'FACTUREPDF': {'IDFACTURE': int(invoice_id)}}})
+        except AxelError as e:
+            raise APIError('Axel error: %s' % e, err='error', http_status=404)
+
+        b64content = base64.b64decode(result['DATA']['PORTAIL']['PDF']['@FILE'])
+        if not b64content:
+            raise APIError('PDF error', err='error', http_status=404)
+        response = HttpResponse(content_type='application/pdf')
+        response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % invoice_id
+        response.write(b64content)
+        return response
+
 
 class Link(models.Model):
     resource = models.ForeignKey(ToulouseAxel, on_delete=models.CASCADE)
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/Q_RefFactureAPayer.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_RefFactureAPayer.xsd
new file mode 100644
index 00000000..1ec595d3
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_RefFactureAPayer.xsd
@@ -0,0 +1,23 @@
+<?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="IDDUI" />
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="PORTAILType">
+		<xsd:sequence>
+				<xsd:element ref="DUI"/>
+		</xsd:sequence>	
+	</xsd:complexType>
+	
+	<xsd:element name="IDDUI" type="all:IDENTREQUIREDType"/>
+	
+	<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_RefFactureAPayer.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/R_RefFactureAPayer.xsd
new file mode 100644
index 00000000..25aef67e
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/R_RefFactureAPayer.xsd
@@ -0,0 +1,74 @@
+<?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="RefFactureAPayer" />
+			</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:simpleType name="CHOIXType">
+		<xsd:restriction base="xsd:string">
+			<xsd:enumeration value="" />
+			<xsd:enumeration value="1" />
+			<xsd:enumeration value="0" />
+		</xsd:restriction>
+	</xsd:simpleType>
+	
+	<xsd:complexType name="FACTURESType">
+		<xsd:sequence>
+			<xsd:element ref="NUMFACTURE" />
+			<xsd:element ref="DATEEMISSION" />
+			<xsd:element ref="DATEECHEANCE" />
+			<xsd:element ref="LIBELLE" />
+			<xsd:element ref="IDFACTURATION" />
+			<xsd:element ref="MONTANTTOTAL" />
+			<xsd:element ref="RESTEAPAYER" />
+			<xsd:element ref="IDREGIE" />
+			<xsd:element ref="EXISTEPDF" />
+			<xsd:element ref="IDFACTURE" />
+		</xsd:sequence> 
+	</xsd:complexType>
+			
+	<xsd:complexType name="DUIType">
+		<xsd:sequence>
+			<xsd:element ref="IDDUI" />
+			<xsd:element ref="CHOIXDEMAT" />
+			<xsd:element ref="NBFACTURES" />
+			<xsd:element ref="FACTURES" minOccurs="0" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+
+	<xsd:element name="IDDUI" type="all:IDENTType"/>
+	<xsd:element name="CHOIXDEMAT" type="CHOIXType"/>
+	<xsd:element name="NBFACTURES" type="all:unsignedInt-or-empty"/>
+	<xsd:element name="FACTURES" type="FACTURESType"/>
+	
+	<xsd:element name="NUMFACTURE" type="xsd:unsignedInt"/>
+	<xsd:element name="DATEEMISSION" type="all:DATEREQUIREDType"/>
+	<xsd:element name="DATEECHEANCE" type="all:DATEREQUIREDType"/>
+	<xsd:element name="LIBELLE" type="xsd:string"/>
+	<xsd:element name="IDFACTURATION" type="all:IDType"/>
+	<xsd:element name="MONTANTTOTAL" type="all:MONTANTType"/>
+	<xsd:element name="RESTEAPAYER" type="all:MONTANTREQUIREDType"/>
+	<xsd:element name="IDREGIE" type="all:IDType"/>
+	<xsd:element name="EXISTEPDF" type="CHOIXType"/>
+	<xsd:element name="IDFACTURE" type="xsd:unsignedInt"/>
+	
+	
+	<xsd:element name="DUI" type="DUIType" />
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/toulouse_axel/xsd/Q_RefFacturePDF.xsd b/passerelle/contrib/toulouse_axel/xsd/Q_RefFacturePDF.xsd
new file mode 100644
index 00000000..23e97c31
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Q_RefFacturePDF.xsd
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+	<xsd:complexType name="FACTUREPDFType">
+		<xsd:sequence>
+			<xsd:element ref="IDFACTURE" />			
+		</xsd:sequence>
+	</xsd:complexType>
+	
+	<xsd:complexType name="PORTAILType">
+		<xsd:sequence>
+			<xsd:element ref="FACTUREPDF" />
+		</xsd:sequence> 
+	</xsd:complexType>
+					
+	<xsd:element name="IDFACTURE" type="xsd:positiveInteger"/>
+	<xsd:element name="FACTUREPDF" type="FACTUREPDFType"/>	
+	<xsd:element name="PORTAIL" type="PORTAILType"/>	
+		
+</xsd:schema>
diff --git a/passerelle/contrib/toulouse_axel/xsd/R_RefFacturePDF.xsd b/passerelle/contrib/toulouse_axel/xsd/R_RefFacturePDF.xsd
new file mode 100644
index 00000000..f6e3a0c1
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/R_RefFacturePDF.xsd
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+	<xsd:redefine schemaLocation="./R_ShemaResultat.xsd">
+	    <xsd:simpleType name="TYPEType">
+			<xsd:restriction base="TYPEType">
+				<xsd:enumeration value="RefFacturePDF" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+							<xsd:element ref="PDF" minOccurs="0"/>
+					</xsd:sequence>	
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>
+	
+	<xsd:complexType name="PDFType">
+		<xsd:attribute ref="FILE"/>
+	</xsd:complexType>
+		
+	<xsd:attribute name="FILE" type="xsd:string"/>
+	
+	<xsd:element name="PDF" type="PDFType"/>
+		
+</xsd:schema>
diff --git a/tests/data/toulouse_axel/invoices.xml b/tests/data/toulouse_axel/invoices.xml
new file mode 100644
index 00000000..c1830bc8
--- /dev/null
+++ b/tests/data/toulouse_axel/invoices.xml
@@ -0,0 +1,31 @@
+<PORTAIL>
+  <DUI>
+    <IDDUI>XXX</IDDUI>
+    <CHOIXDEMAT>0</CHOIXDEMAT>
+    <NBFACTURES>2</NBFACTURES>
+    <FACTURES>
+      <NUMFACTURE>42</NUMFACTURE>
+      <DATEEMISSION>12/11/2019</DATEEMISSION>
+      <DATEECHEANCE>04/12/2019</DATEECHEANCE>
+      <LIBELLE>PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019</LIBELLE>
+      <IDFACTURATION>4242-35AA</IDFACTURATION>
+      <MONTANTTOTAL>44.94</MONTANTTOTAL>
+      <RESTEAPAYER>44.94</RESTEAPAYER>
+      <IDREGIE>MAREGIE</IDREGIE>
+      <EXISTEPDF>1</EXISTEPDF>
+      <IDFACTURE>42</IDFACTURE>
+    </FACTURES>
+    <FACTURES>
+      <NUMFACTURE>43</NUMFACTURE>
+      <DATEEMISSION>12/12/2019</DATEEMISSION>
+      <DATEECHEANCE>04/01/2020</DATEECHEANCE>
+      <LIBELLE>PRESTATIONS PERISCOLAIRES NOVEMBRE 2019</LIBELLE>
+      <IDFACTURATION>4243-35AA</IDFACTURATION>
+      <MONTANTTOTAL>44.94</MONTANTTOTAL>
+      <RESTEAPAYER>44.94</RESTEAPAYER>
+      <IDREGIE>MAREGIE</IDREGIE>
+      <EXISTEPDF>0</EXISTEPDF>
+      <IDFACTURE>43</IDFACTURE>
+    </FACTURES>
+  </DUI>
+</PORTAIL>
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index c53621d9..fb1e49a3 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -25,6 +25,8 @@ from passerelle.contrib.toulouse_axel.models import (
     Link,
     ToulouseAxel,
     ref_famille_dui,
+    ref_facture_a_payer,
+    ref_facture_pdf,
     ref_verif_dui,
 )
 import utils
@@ -131,6 +133,36 @@ def test_operation_ref_famille_dui(resource, content):
             })
 
 
+@pytest.mark.parametrize('content', [
+    '<PORTAIL><DUI/></PORTAIL>',
+])
+def test_operation_ref_facture_a_payer(resource, content):
+    with mock_getdata(content, 'RefFactureAPayer'):
+        with pytest.raises(AxelError):
+            ref_facture_a_payer(resource, {
+                'PORTAIL': {
+                    'DUI': {
+                        'IDDUI': 'XXX',
+                    }
+                }
+            })
+
+
+@pytest.mark.parametrize('content', [
+    "<PORTAIL><PDF FOO='BAR'></PDF></PORTAIL>",
+])
+def test_operation_ref_facture_pdf(resource, content):
+    with mock_getdata(content, 'RefFacturePDF'):
+        with pytest.raises(AxelError):
+            ref_facture_pdf(resource, {
+                'PORTAIL': {
+                    'FACTUREPDF': {
+                        'IDFACTURE': 42,
+                    }
+                }
+            })
+
+
 def test_link_endpoint_nameid_empty(app, resource, params):
     resp = app.post_json('/toulouse-axel/test/link?NameID=', params=params, status=400)
     assert resp.json['err_desc'] == "NameID is empty"
@@ -306,3 +338,197 @@ def test_child_info_endpoint(app, resource):
         'SANITAIRE',
         'SEXE',
     ])
+
+
+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')
+        assert resp.json['err_desc'] == "Axel error: FooBar"
+
+
+def test_invoices_endpoint_no_result(app, resource):
+    resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+    assert resp.json['err_desc'] == "Person not found"
+
+
+def test_invoices_endpoint_no_invoices(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    content = '''<PORTAIL>
+    <DUI>
+        <IDDUI>XXX</IDDUI>
+        <CHOIXDEMAT>0</CHOIXDEMAT>
+        <NBFACTURES>0</NBFACTURES>
+    </DUI>
+</PORTAIL>'''
+    with mock_getdata(content, 'RefFactureAPayer'):
+        resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == []
+
+
+def test_invoices_endpoint(app, resource):
+    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'):
+        resp = app.get('/toulouse-axel/test/invoices?NameID=yyy')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {
+            'id': 42,
+            'label': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
+            'amount': '44.94',
+            'total_amount': '44.94',
+            'online_payment': True,
+            'created': '2019-11-12',
+            'pay_limit_date': '2019-12-04',
+            'has_pdf': True,
+            'paid': False,
+            'toulouse-axel': {
+                'IDFACTURATION': '4242-35AA',
+                'IDFACTURE': 42,
+                'IDREGIE': 'MAREGIE',
+                'DATEECHEANCE': '2019-12-04',
+                'DATEEMISSION': '2019-11-12',
+                'EXISTEPDF': '1',
+                'LIBELLE': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
+                'MONTANTTOTAL': '44.94',
+                'NUMFACTURE': 42,
+                'RESTEAPAYER': '44.94',
+            },
+        },
+        {
+            'id': 43,
+            'label': 'PRESTATIONS PERISCOLAIRES NOVEMBRE 2019',
+            'amount': '44.94',
+            'total_amount': '44.94',
+            'online_payment': True,
+            'created': '2019-12-12',
+            'pay_limit_date': '2020-01-04',
+            'has_pdf': False,
+            'paid': False,
+            'toulouse-axel': {
+                'IDFACTURATION': '4243-35AA',
+                'IDFACTURE': 43,
+                'DATEECHEANCE': '2020-01-04',
+                'DATEEMISSION': '2019-12-12',
+                'EXISTEPDF': '0',
+                'IDREGIE': 'MAREGIE',
+                'LIBELLE': 'PRESTATIONS PERISCOLAIRES NOVEMBRE 2019',
+                'MONTANTTOTAL': '44.94',
+                'NUMFACTURE': 43,
+                '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')
+        assert resp.json['err_desc'] == "Axel error: FooBar"
+
+
+def test_invoice_endpoint_no_result(app, resource):
+    resp = app.get('/toulouse-axel/test/invoice/42?NameID=yyy')
+    assert resp.json['err_desc'] == "Person not found"
+
+    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'):
+        resp = app.get('/toulouse-axel/test/invoice/35?NameID=yyy')
+    assert resp.json['err_desc'] == "Invoice not found"
+
+
+def test_invoice_endpoint(app, resource):
+    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'):
+        resp = app.get('/toulouse-axel/test/invoice/42?NameID=yyy')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == {
+        'id': 42,
+        'label': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
+        'amount': '44.94',
+        'total_amount': '44.94',
+        'online_payment': True,
+        'created': '2019-11-12',
+        'pay_limit_date': '2019-12-04',
+        'has_pdf': True,
+        'paid': False,
+        'toulouse-axel': {
+            'IDFACTURATION': '4242-35AA',
+            'IDFACTURE': 42,
+            'IDREGIE': 'MAREGIE',
+            'DATEECHEANCE': '2019-12-04',
+            'DATEEMISSION': '2019-11-12',
+            'EXISTEPDF': '1',
+            'LIBELLE': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
+            'MONTANTTOTAL': '44.94',
+            'NUMFACTURE': 42,
+            'RESTEAPAYER': '44.94',
+        },
+    }
+
+
+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)
+        assert resp.json['err_desc'] == "Axel error: FooBar"
+
+    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.ref_facture_pdf') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+            assert resp.json['err_desc'] == "Axel error: FooBar"
+
+
+def test_invoice_pdf_endpoint_no_result(app, resource):
+    resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+    assert resp.json['err_desc'] == "Person not found"
+
+    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'):
+        resp = app.get('/toulouse-axel/test/invoice/35/pdf?NameID=yyy', status=404)
+    assert resp.json['err_desc'] == "Invoice not found"
+
+    with mock_getdata(content, 'RefFactureAPayer'):
+        resp = app.get('/toulouse-axel/test/invoice/43/pdf?NameID=yyy', status=404)
+    assert resp.json['err_desc'] == "PDF not available"
+
+    pdf_content = '''<PORTAIL>
+    <PDF FILE=''></PDF>
+</PORTAIL>'''
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_invoice') as invoice:
+        invoice.return_value = {'has_pdf': True}
+        with mock_getdata(pdf_content, 'RefFacturePDF'):
+            resp = app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy', status=404)
+    assert resp.json['err_desc'] == "PDF error"
+
+
+def test_invoice_pdf_endpoint(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    pdf_content = '''<PORTAIL>
+    <PDF FILE='aGVsbG8gd29ybGQ='></PDF>
+</PORTAIL>'''
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_invoice') as invoice:
+        invoice.return_value = {'has_pdf': True}
+        with mock_getdata(pdf_content, 'RefFacturePDF'):
+            app.get('/toulouse-axel/test/invoice/42/pdf?NameID=yyy')
-- 
2.20.1

