From bcbc38d8ac88b7f37771824c36004d8708b65cc4 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 20 Dec 2022 16:26:14 +0100
Subject: [PATCH 3/3] toulouse-maelis: provide add-supplied-document endpoint
 (#72532)

---
 functests/toulouse_maelis/conftest.py         |   3 +-
 functests/toulouse_maelis/data/201x201.jpg    | Bin 0 -> 795 bytes
 functests/toulouse_maelis/test_family.py      |  39 +++++++
 .../contrib/toulouse_maelis/family_schemas.py |  60 ++++++++++
 passerelle/contrib/toulouse_maelis/models.py  |  32 +++++
 tests/data/toulouse_maelis/201x201.jpg        | Bin 0 -> 795 bytes
 .../R_add_supplied_document.xml               |   7 ++
 tests/test_toulouse_maelis.py                 | 110 ++++++++++++++++++
 8 files changed, 250 insertions(+), 1 deletion(-)
 create mode 100644 functests/toulouse_maelis/data/201x201.jpg
 create mode 100644 tests/data/toulouse_maelis/201x201.jpg
 create mode 100644 tests/data/toulouse_maelis/R_add_supplied_document.xml

diff --git a/functests/toulouse_maelis/conftest.py b/functests/toulouse_maelis/conftest.py
index 0eaf89de..fe60abf6 100644
--- a/functests/toulouse_maelis/conftest.py
+++ b/functests/toulouse_maelis/conftest.py
@@ -408,19 +408,20 @@ def create_data(request, conn):
     create_result = resp.json()
     assert create_result['err'] == 0
     print('\ncreate DUI: %s' % str(create_result['data']['number']))
     data = diff_family(conn, name_id, 'test_create_family.json')
 
     return {
         'name_id': name_id,  # linked
         'family_id': str(create_result['data']['number']),
+        'family_payload': create_family_payload,
         'lastname': lastname,
         'rl1_num': data['RL1']['num'],
-        'family_payload': create_family_payload,
+        'bart_num': data['childList'][0]['num'],
         'data': data,
     }
 
 
 @pytest.fixture(scope='session')
 def update_data(request, conn):
     name_id = request.config.getoption('--nameid')
     lastname = request.config.getoption('--lastname')
diff --git a/functests/toulouse_maelis/data/201x201.jpg b/functests/toulouse_maelis/data/201x201.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..521972f88f263b38a03920011f0af494ca8f4373
GIT binary patch
diff --git a/functests/toulouse_maelis/test_family.py b/functests/toulouse_maelis/test_family.py
index 51ef82a3..88ecd13e 100644
--- a/functests/toulouse_maelis/test_family.py
+++ b/functests/toulouse_maelis/test_family.py
@@ -1,12 +1,14 @@
+import base64
 import copy
 
 import pytest
 import requests
+from django.utils.encoding import force_str
 
 from .conftest import diff, diff_child, diff_family, diff_rlg, link, read_family, unlink
 
 FAMILY_RESET_PAYLOAD = {
     'category': 'AUTR',
     'situation': 'AUTR',
     'rl1': {
         'civility': 'MR',  # no effect
@@ -871,8 +873,45 @@ def test_read_family_members(conn, update_data):
         child_id,
         person_id,
     )
     resp = requests.get(url)
     resp.raise_for_status()
     res = resp.json()
     assert res['err'] == 0
     assert res['data']['personInfo']['firstname'] == 'ABRAHAM JEBEDIAH'
+
+
+def test_add_supplied_document(conn, create_data):
+    unlink(conn, create_data['name_id'])
+    link(conn, create_data)
+
+    # push on family
+    payload = {
+        'documentList/0/code': '46',
+        'documentList/0/depositDate': '2022-12-20',
+        'documentList/0/file': {  # w.c.s. file field
+            'filename': '201x201.jpg',
+            'content_type': 'image/jpeg',
+            'content': force_str(base64.b64encode(open('data/201x201.jpg', 'rb').read())),
+        },
+    }
+    url = conn + '/add-supplied-document?NameID=%s' % create_data['name_id']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 0
+
+    # push on RL
+    payload['numPerson'] = create_data['rl1_num']
+    url = conn + '/add-supplied-document?NameID=%s' % create_data['name_id']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 0
+
+    # push on childe
+    payload['numPerson'] = create_data['bart_num']
+    url = conn + '/add-supplied-document?NameID=%s' % create_data['name_id']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 0
diff --git a/passerelle/contrib/toulouse_maelis/family_schemas.py b/passerelle/contrib/toulouse_maelis/family_schemas.py
index a43ff695..28f4212a 100644
--- a/passerelle/contrib/toulouse_maelis/family_schemas.py
+++ b/passerelle/contrib/toulouse_maelis/family_schemas.py
@@ -712,8 +712,68 @@ UPDATE_QUOTIENT_SCHEMA = {
         'cdquo': {
             'description': 'Code du quotient (depuis référentiel)',
             'type': 'string',
             'pattern': '.+',
         },
     },
     'additionalProperties': False,
 }
+
+WCS_FILE_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-04/schema#',
+    'title': 'WCS File',
+    'description': 'Champ W.C.S. de type ficher',
+    'type': 'object',
+    'required': ['filename', 'content_type', 'content'],
+    'properties': {
+        'filename': {
+            'description': "Nom du ficher",
+            'type': 'string',
+        },
+        'content_type': {
+            'description': "Type MIME",
+            'type': 'string',
+        },
+        'content': {
+            'description': "Contenu",
+            'type': 'string',
+        },
+    },
+}
+
+SUPPLIED_DOCUMENT_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-04/schema#',
+    'title': 'Supplied document',
+    'description': 'Ajoute un document pour une famille, un responsable légal ou un enfant',
+    'type': 'object',
+    'required': ['code', 'file'],
+    'properties': {
+        'code': {
+            'description': 'Code de la pièce (depuis référentiel)',
+            'type': 'string',
+            'pattern': '.+',
+        },
+        'depositDate': {
+            'description': 'Date de dépôt (date du jour si non transmise) ',
+            'type': 'string',
+            'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2}){0,1}$',
+        },
+        'file': WCS_FILE_SCHEMA,
+    },
+}
+
+SUPPLIED_DOCUMENTS_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-04/schema#',
+    'title': 'Supplied documents',
+    'description': 'Ajoute des documents pour une famille, un responsable légal ou un enfant',
+    'type': 'object',
+    'required': ['documentList'],
+    'properties': {
+        'numPerson': {
+            'description': "Numéro du responsable légal de l'enfant",
+            'type': 'string',
+        },
+        'documentList': {'type': 'array', 'items': SUPPLIED_DOCUMENT_SCHEMA},
+    },
+    'unflatten': True,
+    'additionalProperties': False,
+}
diff --git a/passerelle/contrib/toulouse_maelis/models.py b/passerelle/contrib/toulouse_maelis/models.py
index d6147fb3..eb05b5c1 100644
--- a/passerelle/contrib/toulouse_maelis/models.py
+++ b/passerelle/contrib/toulouse_maelis/models.py
@@ -8,16 +8,17 @@
 # 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
 from urllib.parse import urljoin
 
 import zeep
 from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.utils.timezone import now
 from zeep.helpers import serialize_object
 from zeep.wsse.username import UsernameToken
@@ -1557,16 +1558,47 @@ class ToulouseMaelis(BaseResource, HTTPResource):
     )
     def update_child_indicator(self, request, NameID, child_id, post_data):
         self.get_link(NameID)
         self.assert_update_indicator_payload_in_referential('ChildIndicator', post_data)
 
         self.call('Family', 'updatePersonIndicatorList', numPerson=child_id, **post_data)
         return {'data': 'ok'}
 
+    @endpoint(
+        display_category='Famille',
+        description='Ajoute un document pour une famille, un responsable légal ou un enfant',
+        name='add-supplied-document',
+        perm='can_access',
+        parameters={
+            'NameID': {'description': 'Publik NameID'},
+        },
+        post={'request_body': {'schema': {'application/json': family_schemas.SUPPLIED_DOCUMENTS_SCHEMA}}},
+    )
+    def add_supplied_document(self, request, NameID, post_data):
+        family_id = self.get_link(NameID).family_id
+        for i in range(0, len(post_data.get('documentList', []))):
+            self.assert_post_data_in_referential('Document', post_data, ['documentList', i, 'code'])
+
+        for item in post_data['documentList']:
+            file = item.pop('file')
+            item['filename'] = file['filename']
+            item['fileSupplied'] = {
+                'dataHandler': base64.b64decode(file['content']),
+                'fileType': file['content_type'],
+                'name': file['filename'],
+            }
+        payload = {'addSuppliedDocumentRequestBean': post_data}
+        payload['addSuppliedDocumentRequestBean']['numDossier'] = family_id
+        response = self.call('Family', 'addSuppliedDocument', **payload)
+        data = serialize_object(response)
+        if data != 'OK':
+            raise APIError('maelis fails to add the supplied document')
+        return {'data': 'ok'}
+
     @endpoint(
         display_category='Facture',
         description="Ajout d'autorisation de prélèvement",
         name='add-direct-debit-order',
         perm='can_access',
         parameters={'NameID': {'description': 'Publik NameID'}},
         post={
             'request_body': {'schema': {'application/json': invoice_schemas.ADD_DIRECT_DEBIT_ORDER_SCHEMA}}
diff --git a/tests/data/toulouse_maelis/201x201.jpg b/tests/data/toulouse_maelis/201x201.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..521972f88f263b38a03920011f0af494ca8f4373
GIT binary patch
diff --git a/tests/data/toulouse_maelis/R_add_supplied_document.xml b/tests/data/toulouse_maelis/R_add_supplied_document.xml
new file mode 100644
index 00000000..9189e718
--- /dev/null
+++ b/tests/data/toulouse_maelis/R_add_supplied_document.xml
@@ -0,0 +1,7 @@
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+  <soap:Body>
+    <ns2:addSuppliedDocumentResponse xmlns:ns2="family.ws.maelis.sigec.com">
+      <addSuppliedDocumentResultBean>%s</addSuppliedDocumentResultBean>
+    </ns2:addSuppliedDocumentResponse>
+  </soap:Body>
+</soap:Envelope>
diff --git a/tests/test_toulouse_maelis.py b/tests/test_toulouse_maelis.py
index c83b686b..a9feaa73 100644
--- a/tests/test_toulouse_maelis.py
+++ b/tests/test_toulouse_maelis.py
@@ -8,38 +8,45 @@
 # 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 logging
 import os
 from unittest import mock
 
 import pytest
 import responses
+from django.utils.encoding import force_str
 from requests.exceptions import ConnectionError
 from zeep import Settings
 
 from passerelle.contrib.toulouse_maelis.models import Link, Referential, ToulouseMaelis
 from passerelle.utils.jsonresponse import APIError
 from passerelle.utils.soap import SOAPError
 from tests.utils import FakedResponse, ResponsesSoap, generic_endpoint_url, setup_access_rights
 
 TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'toulouse_maelis')
 
 
 def get_xml_file(filename):
     with open(os.path.join(TEST_BASE_DIR, filename), 'rb') as desc:
         return desc.read()
 
 
+def get_media_file(filename):
+    with open(os.path.join(TEST_BASE_DIR, "%s" % filename), 'rb') as desc:
+        return desc.read()
+
+
 CONNECTION_ERROR = ConnectionError('No address associated with hostname')
 FAMILY_SERVICE_WSDL = FakedResponse(content=get_xml_file('FamilyService.wsdl'), status_code=200)
 ACTIVITY_SERVICE_WSDL = FakedResponse(content=get_xml_file('ActivityService.wsdl'), status_code=200)
 INVOICE_SERVICE_WSDL = FakedResponse(content=get_xml_file('InvoiceService.wsdl'), status_code=200)
 FAILED_AUTH = FakedResponse(content=get_xml_file('R_failed_authentication.xml'), status_code=500)
 ISWSRUNNING_TRUE = FakedResponse(content=get_xml_file('R_is_ws_running.xml') % b'true', status_code=200)
 ISWSRUNNING_FALSE = FakedResponse(content=get_xml_file('R_is_ws_running.xml') % b'false', status_code=200)
 
@@ -3302,16 +3309,119 @@ def test_update_child_indicator_wrong_referential_key_error(con, app):
     resp = app.post_json(url + '?NameID=local&child_id=613880', params=params)
     assert resp.json['err'] == 'wrong-key'
     assert (
         resp.json['err_desc']
         == "indicatorList/0/code key value 'plop' do not belong to 'ChildIndicator' required referential"
     )
 
 
+def test_add_supplied_document(family_service, con, app):
+    def request_check(request):
+        assert request.documentList[0]['fileSupplied']['dataHandler'] == get_media_file('201x201.jpg')
+
+    family_service.add_soap_response(
+        'addSuppliedDocument',
+        get_xml_file('R_add_supplied_document.xml') % b'OK',
+        request_check=request_check,
+    )
+    url = get_endpoint('add-supplied-document')
+    params = {
+        'numPerson': '613880',
+        'documentList/0/code': '46',
+        'documentList/0/depositDate': '2022-12-20',
+        'documentList/0/file': {  # w.c.s. file field
+            'filename': '201x201.jpg',
+            'content_type': 'image/jpeg',
+            'content': force_str(base64.b64encode(get_media_file('201x201.jpg'))),
+        },
+    }
+
+    Link.objects.create(resource=con, family_id='1312', name_id='local')
+    resp = app.post_json(url + '?NameID=local', params=params)
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == 'ok'
+
+
+def test_add_supplied_document_not_linked_error(con, app):
+    url = get_endpoint('add-supplied-document')
+    params = {
+        'documentList': [],
+    }
+
+    resp = app.post_json(url + '?NameID=local', params=params)
+    assert resp.json['err'] == 'not-linked'
+    assert resp.json['err_desc'] == 'User not linked to family'
+
+
+def test_add_supplied_document_empty_referential_key_error(con, app):
+    url = get_endpoint('add-supplied-document')
+    params = {
+        'documentList/0/code': '',
+        'documentList/0/depositDate': '2022-12-20',
+        'documentList/0/file': {  # w.c.s. file field
+            'filename': '201x201.jpg',
+            'content_type': 'image/jpeg',
+            'content': force_str(base64.b64encode(get_media_file('201x201.jpg'))),
+        },
+    }
+
+    Link.objects.create(resource=con, family_id='1312', name_id='local')
+    resp = app.post_json(url + '?NameID=local', params=params, status=400)
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == "documentList/0/code: '' does not match '.+'"
+
+
+def test_add_supplied_document_wrong_referential_key_error(con, app):
+    url = get_endpoint('add-supplied-document')
+    params = {
+        'documentList/0/code': 'plop',
+        'documentList/0/depositDate': '2022-12-20',
+        'documentList/0/file': {  # w.c.s. file field
+            'filename': '201x201.jpg',
+            'content_type': 'image/jpeg',
+            'content': force_str(base64.b64encode(get_media_file('201x201.jpg'))),
+        },
+    }
+
+    Link.objects.create(resource=con, family_id='1312', name_id='local')
+    resp = app.post_json(url + '?NameID=local', params=params)
+    assert resp.json['err'] == 'wrong-key'
+    assert (
+        resp.json['err_desc']
+        == "documentList/0/code key value 'plop' do not belong to 'Document' required referential"
+    )
+
+
+def test_add_supplied_document_maelis_error(family_service, con, app):
+    def request_check(request):
+        assert request.documentList[0]['fileSupplied']['dataHandler'] == get_media_file('201x201.jpg')
+
+    family_service.add_soap_response(
+        'addSuppliedDocument',
+        get_xml_file('R_add_supplied_document.xml') % b'KO',
+        request_check=request_check,
+    )
+    url = get_endpoint('add-supplied-document')
+    params = {
+        'documentList/0/code': '46',
+        'documentList/0/depositDate': '2022-12-20',
+        'documentList/0/file': {  # w.c.s. file field
+            'filename': '201x201.jpg',
+            'content_type': 'image/jpeg',
+            'content': force_str(base64.b64encode(get_media_file('201x201.jpg'))),
+        },
+    }
+
+    Link.objects.create(resource=con, family_id='1312', name_id='local')
+    resp = app.post_json(url + '?NameID=local', params=params)
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'maelis fails to add the supplied document'
+
+
 def test_add_direct_debit_order(invoice_service, con, app):
     def request_check(request):
         assert request.dossierNumber == 1312
 
     invoice_service.add_soap_response(
         'addDirectDebitOrder', get_xml_file('R_add_direct_debit_order.xml'), request_check=request_check
     )
     url = get_endpoint('add-direct-debit-order')
-- 
2.30.2

