diff --git a/src/backend/people/settings.py b/src/backend/people/settings.py index f363aad..459d241 100755 --- a/src/backend/people/settings.py +++ b/src/backend/people/settings.py @@ -465,6 +465,21 @@ class Base(Configuration): environ_name="MAIL_PROVISIONING_API_CREDENTIALS", environ_prefix=None, ) + DNS_PROVISIONING_API_URL = values.Value( + default="https://api.scaleway.com", + environ_name="DNS_PROVISIONING_API_URL", + environ_prefix=None, + ) + DNS_PROVISIONING_RESOURCE_ID = values.Value( + default=None, + environ_name="DNS_PROVISIONING_RESOURCE_ID", + environ_prefix=None, + ) + DNS_PROVISIONING_API_CREDENTIALS = values.Value( + default=None, + environ_name="DNS_PROVISIONING_API_CREDENTIALS", + environ_prefix=None, + ) # Organizations ORGANIZATION_REGISTRATION_ID_VALIDATORS = json.loads( diff --git a/src/backend/plugins/organizations.py b/src/backend/plugins/organizations.py index 334372b..634b5b5 100644 --- a/src/backend/plugins/organizations.py +++ b/src/backend/plugins/organizations.py @@ -1,5 +1,7 @@ """Organization related plugins.""" +from django.conf import settings + import logging import requests @@ -27,14 +29,8 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin): def get_organization_name_from_results(data, siret): """Return the organization name from the results of a SIRET search.""" for result in data["results"]: - is_commune = ( - result.get("nature_juridique") == "7210" - ) # INSEE code for commune for organization in result["matching_etablissements"]: if organization.get("siret") == siret: - if is_commune: - return organization["libelle_commune"].title() - store_signs = organization.get("liste_enseignes") or [] if store_signs: return store_signs[0].title() @@ -76,3 +72,107 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin): organization.name = name organization.save(update_fields=["name", "updated_at"]) logger.info("Organization %s name updated to %s", organization, name) + +class ApiCall: + """Encapsulates a call to an external API""" + + method: str = "GET" + base: str = "" + url: str = "" + params: dict = {} + headers: dict = {} + response = None + + def execute(self): + """Call the specified API endpoint with supplied parameters and record response""" + if self.method == "POST": + self.response = requests.request( + method=self.method, + url=f"{self.base}/{self.url}", + json=self.params, + headers=self.headers, + timeout=5, + ) + + +class CommuneCreation(BaseOrganizationPlugin): + """ + This plugin handles setup tasks for French communes. + """ + + _api_url = "https://recherche-entreprises.api.gouv.fr/search?q={siret}" + + def get_organization_name_from_results(self, data, siret): + """Return the organization name from the results of a SIRET search.""" + for result in data["results"]: + nature = "nature_juridique" + commune = nature in result and result[nature] == "7210" + for organization in result["matching_etablissements"]: + if organization.get("siret") == siret: + if commune: + return organization["libelle_commune"].title() + + logger.warning("No organization name found for SIRET %s", siret) + return None + + def complete_commune_creation(self, name: str) -> ApiCall: + """Specify the tasks to be completed after a commune is created.""" + inputs = {"name": name.lower()} + + create_zone = ApiCall() + create_zone.method = "POST" + create_zone.base = "https://api.scaleway.com" + create_zone.url = "/domain/v2beta1/dns-zones" + create_zone.params = { + "project_id": settings.DNS_PROVISIONING_RESOURCE_ID, + "domain": "collectivite.fr", + "subdomain": inputs["name"], + } + create_zone.headers = { + "X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS + } + + create_domain = ApiCall() + create_domain.method = "POST" + create_domain.base = settings.MAIL_PROVISIONING_API_URL + create_domain.url = "/domains" + create_domain.params = { + "name": inputs["name"], + "delivery": "virtual", + "features": ["webmail"], + "context_name": inputs["name"], + } + create_domain.headers = { + "Authorization": f"Basic: {settings.MAIL_PROVISIONING_API_CREDENTIALS}" + } + + return [create_zone, create_domain] + + def run_after_create(self, organization): + """After creating an organization, update the organization name.""" + if not organization.registration_id_list: + # No registration ID to convert... + return + + # In the nominal case, there is only one registration ID because + # the organization as been created from it. + try: + # Retry logic as the API may be rate limited + s = requests.Session() + retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[429]) + s.mount("https://", HTTPAdapter(max_retries=retries)) + + siret = organization.registration_id_list[0] + response = s.get(self._api_url.format(siret=siret), timeout=10) + response.raise_for_status() + data = response.json() + name = self.get_organization_name_from_results(data, siret) + if not name: + return + except requests.RequestException as exc: + logger.exception("%s: Unable to fetch organization name from SIRET", exc) + return + + organization.name = name + organization.save(update_fields=["name", "updated_at"]) + logger.info("Organization %s name updated to %s", organization, name) diff --git a/src/backend/plugins/tests/organizations/test_commune_creation.py b/src/backend/plugins/tests/organizations/test_commune_creation.py new file mode 100644 index 0000000..e0fd65f --- /dev/null +++ b/src/backend/plugins/tests/organizations/test_commune_creation.py @@ -0,0 +1,99 @@ +"""Tests for the CommuneCreation plugin.""" + +from django.conf import settings + +import pytest +import responses + +from plugins.organizations import ApiCall, CommuneCreation + +pytestmark = pytest.mark.django_db + +def test_extract_name_from_org_data_when_commune(): + """Test the name is extracted correctly for a French commune.""" + data = { + "results": [ + { + "nom_complet": "COMMUNE DE VARZY", + "nom_raison_sociale": "COMMUNE DE VARZY", + "siege": { + "libelle_commune": "VARZY", + "liste_enseignes": ["MAIRIE"], + "siret": "21580304000017", + }, + "nature_juridique": "7210", + "matching_etablissements": [ + { + "siret": "21580304000017", + "libelle_commune": "VARZY", + "liste_enseignes": ["MAIRIE"], + } + ], + } + ] + } + + plugin = CommuneCreation() + name = plugin.get_organization_name_from_results(data, "21580304000017") + assert name == "Varzy" + + +def test_api_call_execution(): + """Test that calling execute() faithfully executes the API call""" + task = ApiCall() + task.method = "POST" + task.base = "https://some_host" + task.url = "some_url" + task.params = {"some_key": "some_value"} + task.headers = {"Some-Header": "Some-Header-Value"} + + with responses.RequestsMock() as rsps: + rsps.add( + rsps.POST, + url="https://some_host/some_url", + body='{"some_key": "some_value"}', + content_type="application/json", + headers={"Some-Header": "Some-Header-Value"}, + ) + + task.execute() + + +def test_tasks_on_commune_creation_include_zone_creation(): + """Test the first task in commune creation: creating the DNS sub-zone""" + plugin = CommuneCreation() + name = "Varzy" + + tasks = plugin.complete_commune_creation(name) + + assert tasks[0].base == "https://api.scaleway.com" + assert tasks[0].url == "/domain/v2beta1/dns-zones" + assert tasks[0].method == "POST" + assert tasks[0].params == { + "project_id": settings.DNS_PROVISIONING_RESOURCE_ID, + "domain": "collectivite.fr", + "subdomain": "varzy", + } + assert tasks[0].headers["X-Auth-Token"] == settings.DNS_PROVISIONING_API_CREDENTIALS + + +def test_tasks_on_commune_creation_include_dimail_domain_creation(): + """Test the second task in commune creation: creating the domain in Dimail""" + plugin = CommuneCreation() + name = "Merlaut" + + tasks = plugin.complete_commune_creation(name) + + assert tasks[1].base == settings.MAIL_PROVISIONING_API_URL + assert tasks[1].url == "/domains" + assert tasks[1].method == "POST" + assert tasks[1].params == { + "name": "merlaut", + "delivery": "virtual", + "features": ["webmail"], + "context_name": "merlaut", + } + assert ( + tasks[1].headers["Authorization"] + == f"Basic: {settings.MAIL_PROVISIONING_API_CREDENTIALS}" + ) diff --git a/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py b/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py index 37dc3a6..87806d3 100644 --- a/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py +++ b/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py @@ -139,37 +139,6 @@ def test_organization_plugins_run_after_create_name_already_set( assert organization.name == "Magic WOW" -def test_extract_name_from_org_data_when_commune( - organization_plugins_settings, -): - """Test the name is extracted correctly for a French commune.""" - data = { - "results": [ - { - "nom_complet": "COMMUNE DE VARZY", - "nom_raison_sociale": "COMMUNE DE VARZY", - "siege": { - "libelle_commune": "VARZY", - "liste_enseignes": ["MAIRIE"], - "siret": "21580304000017", - }, - "nature_juridique": "7210", - "matching_etablissements": [ - { - "siret": "21580304000017", - "libelle_commune": "VARZY", - "liste_enseignes": ["MAIRIE"], - } - ], - } - ] - } - - plugin = NameFromSiretOrganizationPlugin() - name = plugin.get_organization_name_from_results(data, "21580304000017") - assert name == "Varzy" - - @responses.activate def test_organization_plugins_run_after_create_no_list_enseignes( organization_plugins_settings,