From e1d435b11c8f6c64abba59244c8b23d05a4a84ac Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 11 May 2021 18:07:29 +0200
Subject: [PATCH 4/4] caluire-axel: add invoice_pdf endpoint (#53884)

---
 passerelle/contrib/caluire_axel/models.py     |  60 ++++++++++
 passerelle/contrib/caluire_axel/schemas.py    |   1 +
 .../caluire_axel/xsd/Q_GetPdfFacture.xsd      |  20 ++++
 .../caluire_axel/xsd/R_GetPdfFacture.xsd      |  34 ++++++
 tests/test_caluire_axel.py                    | 103 ++++++++++++++++++
 5 files changed, 218 insertions(+)
 create mode 100644 passerelle/contrib/caluire_axel/xsd/Q_GetPdfFacture.xsd
 create mode 100644 passerelle/contrib/caluire_axel/xsd/R_GetPdfFacture.xsd

diff --git a/passerelle/contrib/caluire_axel/models.py b/passerelle/contrib/caluire_axel/models.py
index 6ff24447..c618af39 100644
--- a/passerelle/contrib/caluire_axel/models.py
+++ b/passerelle/contrib/caluire_axel/models.py
@@ -9,20 +9,22 @@
 # This program is distributed in the hope that it will be useful,
 # 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 base64
 import datetime
 
 from django.core.cache import cache
 from django.db import models
+from django.http import HttpResponse
 from django.utils import dateformat
 from django.utils.timezone import now
 from django.utils.translation import ugettext_lazy as _
 
 from passerelle.base.models import BaseResource
 from passerelle.contrib.utils import axel
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
@@ -682,16 +684,74 @@ class CaluireAxel(BaseResource):
             historical=historical,
             nb_mounts_limit=nb_mounts_limit,
         )
         if invoice is None:
             raise APIError('Invoice not found', err_code='not-found')
 
         return {'data': invoice}
 
+    @endpoint(
+        display_category=_('Invoices'),
+        display_order=4,
+        name='regie',
+        perm='can_access',
+        pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/pdf/?$',
+        example_pattern='{regie_id}/invoice/{invoice_id}/pdf',
+        description=_('Get invoice as a PDF file'),
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+            'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
+            'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'},
+            'nb_mounts_limit': {'description': _('Number of months of history'), 'example_value': '12'},
+        },
+    )
+    def invoice_pdf(self, request, regie_id, invoice_id, NameID, nb_mounts_limit='12'):
+        # check that invoice is related to current user
+        real_invoice_id = invoice_id.split('-')[-1]
+        historical = invoice_id.startswith('historical-')
+        try:
+            link = self.get_link(NameID)
+            invoice = self.get_invoice(
+                regie_id=regie_id,
+                invoice_id=real_invoice_id,
+                family_id=link.family_id,
+                historical=historical,
+                nb_mounts_limit=nb_mounts_limit,
+            )
+        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
+        if not invoice['has_pdf']:
+            raise APIError('PDF not available', err_code='not-available', http_status=404)
+
+        try:
+            result = schemas.get_pdf_facture(
+                self, {'PORTAIL': {'GETPDFFACTURE': {'IDFACTURE': int(invoice['display_id'])}}}
+            )
+        except axel.AxelError as e:
+            raise APIError(
+                'Axel error: %s' % e,
+                err_code='error',
+                http_status=404,
+                data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
+            )
+        b64content = base64.b64decode(
+            result.json_response['DATA']['PORTAIL']['GETPDFFACTURE']['FACTUREPDF'] or ''
+        )
+        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
+
 
 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 df5420f3..939f1f13 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', data_method='setData')
 get_agenda = Operation('GetAgenda')
 get_factures_a_payer = Operation('GetFacturesaPayer')
 get_list_factures = Operation('GetListFactures')
+get_pdf_facture = Operation('GetPdfFacture')
 
 
 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)
diff --git a/passerelle/contrib/caluire_axel/xsd/Q_GetPdfFacture.xsd b/passerelle/contrib/caluire_axel/xsd/Q_GetPdfFacture.xsd
new file mode 100644
index 00000000..60cd9452
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/xsd/Q_GetPdfFacture.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="GETPDFFACTUREType">
+		<xsd:sequence>
+			<xsd:element ref="IDFACTURE" />			
+		</xsd:sequence>
+	</xsd:complexType>
+	
+	<xsd:complexType name="PORTAILType">
+		<xsd:sequence>
+			<xsd:element ref="GETPDFFACTURE" />
+		</xsd:sequence> 
+	</xsd:complexType>
+					
+	<xsd:element name="IDFACTURE" type="xsd:positiveInteger"/>
+	<xsd:element name="GETPDFFACTURE" type="GETPDFFACTUREType"/>	
+	<xsd:element name="PORTAIL" type="PORTAILType"/>	
+		
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/caluire_axel/xsd/R_GetPdfFacture.xsd b/passerelle/contrib/caluire_axel/xsd/R_GetPdfFacture.xsd
new file mode 100644
index 00000000..35cab441
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/xsd/R_GetPdfFacture.xsd
@@ -0,0 +1,34 @@
+<?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="GetPdfFacture" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+							<xsd:element ref="GETPDFFACTURE" minOccurs="0" maxOccurs="1"/>
+					</xsd:sequence>	
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>
+	
+	<xsd:complexType name="GETPDFFACTUREType">
+		<xsd:sequence>
+			<xsd:element ref="CODE"/>
+			<xsd:element ref="FACTUREPDF"/>
+		</xsd:sequence>  
+	</xsd:complexType>
+	
+	<xsd:element name="CODE" type="xsd:integer"/>
+	<xsd:element name="FACTUREPDF" type="xsd:string"/>
+	
+	<xsd:element name="GETPDFFACTURE" type="GETPDFFACTUREType"/>
+		
+</xsd:schema>
diff --git a/tests/test_caluire_axel.py b/tests/test_caluire_axel.py
index bb181120..c4f0c428 100644
--- a/tests/test_caluire_axel.py
+++ b/tests/test_caluire_axel.py
@@ -1533,8 +1533,111 @@ def test_invoice_endpoint(app, resource):
                 'ENCAISSE': '40.00',
                 'FACTURATION': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
                 'ECHEANCE': '2019-12-04',
                 'EMISSION': '2019-11-12',
                 'EXISTEPDF': True,
             },
         },
     }
+
+
+def test_invoice_pdf_endpoint_axel_error(app, resource):
+    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.get('/caluire-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'
+
+    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_data(content, 'GetFacturesaPayer'):
+        with mock.patch('passerelle.contrib.caluire_axel.schemas.get_pdf_facture') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.get('/caluire-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_bad_request(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    with mock_data(None, 'GetListFactures'):
+        resp = app.get(
+            '/caluire-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy&nb_mounts_limit=not_a_number',
+            status=404,
+        )
+    assert resp.json['err_desc'] == "nb_mounts_limit must be an integer"
+    assert resp.json['err'] == 'bad-request'
+
+
+def test_invoice_pdf_endpoint_no_result(app, resource):
+    resp = app.get('/caluire-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'
+
+    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_data(content, 'GetFacturesaPayer'):
+        resp = app.get('/caluire-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_data(content, 'GetFacturesaPayer'):
+        resp = app.get('/caluire-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_data(content, 'GetFacturesaPayer'):
+        resp = app.get('/caluire-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'
+
+    pdf_content = '''<PORTAIL>
+    <GETPDFFACTURE>
+        <CODE>1</CODE>
+        <FACTUREPDF></FACTUREPDF>
+    </GETPDFFACTURE>
+</PORTAIL>'''
+    with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice:
+        invoice.return_value = {'has_pdf': True, 'display_id': '42'}
+        with mock_data(pdf_content, 'GetPdfFacture'):
+            resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404)
+    assert resp.json['err_desc'] == "PDF error"
+    assert resp.json['err'] == 'error'
+
+
+def test_invoice_pdf_endpoint(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    pdf_content = '''<PORTAIL>
+    <GETPDFFACTURE>
+        <CODE>1</CODE>
+        <FACTUREPDF>aGVsbG8gd29ybGQ=</FACTUREPDF>
+    </GETPDFFACTURE>
+</PORTAIL>'''
+    with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice:
+        invoice.return_value = {'has_pdf': True, 'display_id': '42'}
+        with mock_data(pdf_content, 'GetPdfFacture'):
+            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_data(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
-- 
2.30.2

