From dccc12f1fe75712ba105d99743e17ea089f531d6 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Wed, 11 May 2022 10:17:00 +0200
Subject: [PATCH] api_entreprise: add match_mandataire_social endpoint (#64687)

---
 passerelle/apps/api_entreprise/models.py | 41 +++++++++++++++++++++++
 passerelle/apps/api_entreprise/utils.py  | 34 +++++++++++++++++++
 tests/test_api_entreprise.py             | 42 ++++++++++++++++++++++++
 3 files changed, 117 insertions(+)
 create mode 100644 passerelle/apps/api_entreprise/utils.py

diff --git a/passerelle/apps/api_entreprise/models.py b/passerelle/apps/api_entreprise/models.py
index 446a2f2ac..51f5d1392 100644
--- a/passerelle/apps/api_entreprise/models.py
+++ b/passerelle/apps/api_entreprise/models.py
@@ -33,6 +33,8 @@ from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError, exception_to_text
 from passerelle.views import WrongParameter
 
+from .utils import simple_match
+
 DOCUMENT_SIGNATURE_MAX_AGE = timedelta(days=7)
 
 
@@ -170,6 +172,14 @@ class APIEntreprise(BaseResource):
         'example_value': '2019',
     }
 
+    FIRST_NAME_PARAM = {'description': _('matched user\'s first name'), 'example_value': 'John'}
+
+    LAST_NAME_PARAM = {'description': _('matched user\'s last name'), 'example_value': 'Doe'}
+
+    BIRTHDATE_PARAM = {'description': _('matched user\'s YYYYDDMM birthdate'), 'example_value': '19700131'}
+
+    METHOD_PARAM = {'description': _('method used for user identity matching'), 'example_value': 'simple'}
+
     @endpoint(
         perm='can_access',
         pattern=r'(?P<association_id>\w+)/$',
@@ -403,3 +413,34 @@ class APIEntreprise(BaseResource):
         return self.get(
             'effectifs_mensuels_acoss_covid/%s/%s/etablissement/%s/' % (year, month, siret), **kwargs
         )
+
+    @endpoint(
+        perm='can_access',
+        pattern=r'(?P<siren>\w+)/$',
+        description=_(
+            'Match firm\'s society representative against local FranceConnect identity information'
+        ),
+        parameters={
+            'siren': SIREN_PARAM,
+            'first_name': FIRST_NAME_PARAM,
+            'last_name': LAST_NAME_PARAM,
+            'birthdate': BIRTHDATE_PARAM,
+            'method': METHOD_PARAM,
+            'object': OBJECT_PARAM,
+            'context': CONTEXT_PARAM,
+            'recipient': RECIPIENT_PARAM,
+        },
+    )
+    def match_mandataire_social(
+        self, request, siren, first_name, last_name, birthdate, method='simple', **kwargs
+    ):
+        entreprise = self.get(
+            'entreprises/%s/' % siren,
+            **kwargs,
+        )
+        if method not in ('simple',):
+            return {'err': 1, 'err_desc': 'method %s not implemented' % method}
+        for mandataire in entreprise['data'].get('entreprise', {}).get('mandataires_sociaux', []):
+            if method == 'simple' and simple_match(mandataire, first_name, last_name, birthdate):
+                return {'err': 0, 'data': mandataire}
+        return {'err': 0, 'data': {}}
diff --git a/passerelle/apps/api_entreprise/utils.py b/passerelle/apps/api_entreprise/utils.py
new file mode 100644
index 000000000..0c0f91981
--- /dev/null
+++ b/passerelle/apps/api_entreprise/utils.py
@@ -0,0 +1,34 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2022  Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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/>.
+
+from unicodedata import category
+from unicodedata import normalize as unormalize
+
+
+def normalize(s):
+    return ''.join(c for c in unormalize('NFKD', s).casefold() if category(c) not in ('Mn', 'Zs'))
+
+
+def simple_match(mandataire, first_name, last_name, birthdate):
+    if any([attr not in mandataire for attr in ['prenom', 'nom', 'date_naissance']]):
+        return False
+    if normalize(mandataire['prenom'].split(',')[0]) != normalize(first_name):
+        return False
+    if normalize(mandataire['nom']) != normalize(last_name):
+        return False
+    if mandataire['date_naissance'].replace('-', '') != birthdate:
+        return False
+    return True
diff --git a/tests/test_api_entreprise.py b/tests/test_api_entreprise.py
index 8ce36fa8a..c1116f612 100644
--- a/tests/test_api_entreprise.py
+++ b/tests/test_api_entreprise.py
@@ -386,6 +386,48 @@ def test_entreprises_endpoint(app, resource, mock_api_entreprise):
     assert data['etablissement_siege']['date_mise_a_jour'] == '2015-12-03'
 
 
+def test_match_mandataire_social(app, resource, mock_api_entreprise):
+    params = {
+        'first_name': 'françois',
+        'last_name': 'hIsQuIn',
+        'birthdate': '19650127',
+        'method': 'simple',
+    }
+    params.update(REQUEST_PARAMS)
+
+    url = '/api-entreprise/test/match_mandataire_social/443170139/'
+    response = app.get(url, params=params)
+
+    # successful matching
+    data = response.json['data']
+    assert data['nom'] == 'HISQUIN'
+    assert data['prenom'] == 'FRANCOIS'
+    assert data['date_naissance'] == '1965-01-27'
+    assert data['type'] == 'PP'
+
+    params['method'] = 'unkwown'
+    response = app.get(url, params=params)
+
+    # unsupported method
+    assert response.json['err']
+    assert 'data' not in response.json
+
+    params['method'] = 'simple'
+    params.pop('first_name')
+    response = app.get(url, params=params, status=400)
+
+    # missing parameter
+    assert not response.json['data']
+    assert 'missing parameter' in response.json['err_desc']
+
+    params['first_name'] = 'JOHN'
+    response = app.get(url, params=params)
+
+    # matching failed
+    assert not response.json['err']
+    assert not response.json['data']
+
+
 @mock.patch('passerelle.utils.Request.get')
 def test_entreprises_endpoint_include_private(mocked_get, app, resource, mock_api_entreprise):
     mocked_get.return_value = FakedResponse(content='{}', status_code=200)
-- 
2.36.1

