diff --git a/package-lock.json b/package-lock.json index f6acfb32..659bd5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2592,14 +2592,12 @@ "version": "4.17.10", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz", "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/lodash-es": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "dev": true, "dependencies": { "@types/lodash": "*" } @@ -10419,6 +10417,7 @@ "version": "0.2.0", "license": "MIT", "dependencies": { + "@types/lodash-es": "^4.17.12", "@zootools/email-spell-checker": "^1.12.0", "axios": "^1.7.7", "is-disposable-email-domain": "^1.0.7", @@ -10428,7 +10427,6 @@ }, "devDependencies": { "@tsconfig/node22": "^22.0.0", - "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.10", "@types/node": "^22.10.2", "chai": "^5.1.2", @@ -10478,6 +10476,7 @@ "devDependencies": { "@electric-sql/pglite": "^0.2.15", "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@gouvfr-lasuite/proconnect.insee": "^0.2.0", "@tsconfig/node22": "^22.0.0", "@types/mocha": "^10.0.10", "@types/node": "^22.10.2", diff --git a/packages/identite/package.json b/packages/identite/package.json index 076fcc36..f42eecba 100644 --- a/packages/identite/package.json +++ b/packages/identite/package.json @@ -51,9 +51,11 @@ "devDependencies": { "@electric-sql/pglite": "^0.2.15", "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@gouvfr-lasuite/proconnect.insee": "^0.2.0", "@tsconfig/node22": "^22.0.0", "@types/mocha": "^10.0.10", "@types/node": "^22.10.2", + "await-to-js": "^3.0.0", "chai": "^5.1.2", "mocha": "^11.0.1", "node-pg-migrate": "^7.6.1", diff --git a/packages/identite/src/organization/get-organization-info.test.ts b/packages/identite/src/organization/get-organization-info.test.ts new file mode 100644 index 00000000..c4bb6957 --- /dev/null +++ b/packages/identite/src/organization/get-organization-info.test.ts @@ -0,0 +1,253 @@ +import { getOrganizationInfoFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; +import { + InseeConnectionError, + InseeNotFoundError, +} from "@gouvfr-lasuite/proconnect.insee/errors"; +import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; +import { AxiosError, type AxiosResponse } from "axios"; +import * as chai from "chai"; +import chaiAsPromised from "chai-as-promised"; + +chai.use(chaiAsPromised); +const assert = chai.assert; + +describe("getOrganizationInfo", () => { + const diffusibleOrganizationInfo = { + siret: "20007184300060", + libelle: "Cc du vexin normand", + nomComplet: "Cc du vexin normand", + enseigne: "", + trancheEffectifs: "22", + trancheEffectifsUniteLegale: "22", + libelleTrancheEffectif: "100 à 199 salariés, en 2021", + etatAdministratif: "A", + estActive: true, + statutDiffusion: "O", + estDiffusible: true, + adresse: "3 rue maison de vatimesnil, 27150 Etrepagny", + codePostal: "27150", + codeOfficielGeographique: "27226", + activitePrincipale: "84.11Z", + libelleActivitePrincipale: "84.11Z - Administration publique générale", + categorieJuridique: "7346", + libelleCategorieJuridique: "Communauté de communes", + }; + + it("should return valid payload for diffusible établissement", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve({ + adresseEtablissement: { + numeroVoieEtablissement: "3", + typeVoieEtablissement: "RUE", + libelleVoieEtablissement: "MAISON DE VATIMESNIL", + codePostalEtablissement: "27150", + libelleCommuneEtablissement: "ETREPAGNY", + codeCommuneEtablissement: "27226", + }, + periodesEtablissement: [ + { + activitePrincipaleEtablissement: "84.11Z", + etatAdministratifEtablissement: "A", + }, + ], + siret: "20007184300060", + statutDiffusionEtablissement: "O", + trancheEffectifsEtablissement: "22", + anneeEffectifsEtablissement: "2021", + uniteLegale: { + categorieJuridiqueUniteLegale: 7346, + denominationUniteLegale: "Cc du vexin normand", + trancheEffectifsUniteLegale: "22", + }, + } as InseeEtablissement), + }); + + await assert.eventually.deepEqual( + getOrganizationInfo("20007184300060"), + diffusibleOrganizationInfo, + ); + }); + + it("should return valid payload for diffusible établissement", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => + Promise.resolve({ + adresseEtablissement: { + numeroVoieEtablissement: "3", + typeVoieEtablissement: "RUE", + libelleVoieEtablissement: "MAISON DE VATIMESNIL", + codePostalEtablissement: "27150", + libelleCommuneEtablissement: "ETREPAGNY", + codeCommuneEtablissement: "27226", + }, + periodesEtablissement: [ + { + activitePrincipaleEtablissement: "84.11Z", + etatAdministratifEtablissement: "A", + }, + ], + siret: "20007184300060", + statutDiffusionEtablissement: "O", + trancheEffectifsEtablissement: "22", + anneeEffectifsEtablissement: "2021", + uniteLegale: { + categorieJuridiqueUniteLegale: 7346, + denominationUniteLegale: "Cc du vexin normand", + trancheEffectifsUniteLegale: "22", + }, + } as InseeEtablissement), + findBySiret: () => Promise.reject(), + }); + + await assert.eventually.deepEqual( + getOrganizationInfo("200071843"), + diffusibleOrganizationInfo, + ); + }); + + it("should show partial data for partially non diffusible établissement", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve({ + adresseEtablissement: { + numeroVoieEtablissement: "12", + typeVoieEtablissement: "AV", + libelleVoieEtablissement: "DE LA GARE", + codePostalEtablissement: "06220", + libelleCommuneEtablissement: "VALLAURIS", + codeCommuneEtablissement: "06155", + }, + periodesEtablissement: [ + { + activitePrincipaleEtablissement: "62.02A", + etatAdministratifEtablissement: "A", + }, + ], + siret: "94957325700019", + statutDiffusionEtablissement: "P", + trancheEffectifsEtablissement: null, + uniteLegale: { + activitePrincipaleUniteLegale: "62.02A", + categorieJuridiqueUniteLegale: 1000, + trancheEffectifsUniteLegale: null, + sexeUniteLegale: "M", + nomUniteLegale: "DUBIGNY", + prenom1UniteLegale: "RAPHAËL", + prenomUsuelUniteLegale: "RAPHAËL", + }, + } as InseeEtablissement), + }); + + await assert.eventually.deepEqual(getOrganizationInfo("94957325700019"), { + siret: "94957325700019", + libelle: "Nom inconnu", + nomComplet: "Nom inconnu", + enseigne: "", + trancheEffectifs: null, + trancheEffectifsUniteLegale: null, + libelleTrancheEffectif: "", + etatAdministratif: "A", + estActive: true, + statutDiffusion: "P", + estDiffusible: false, + adresse: "06220 Vallauris", + codePostal: "06220", + codeOfficielGeographique: "06155", + activitePrincipale: "62.02A", + libelleActivitePrincipale: + "62.02A - Conseil en systèmes et logiciels informatiques", + categorieJuridique: "1000", + libelleCategorieJuridique: "Entrepreneur individuel", + }); + }); + + it("should throw for totally non diffusible établissement", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve({ + statutDiffusionEtablissement: "N", + } as InseeEtablissement), + }); + + await assert.isRejected( + getOrganizationInfo("53512638700013"), + InseeNotFoundError, + ); + }); + + it("should throw on Axios 403 Http Error", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.reject( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 403, + } as AxiosResponse), + ), + }); + + await assert.isRejected( + getOrganizationInfo("53512638700013"), + InseeNotFoundError, + ); + }); + + it("should throw on Axios 404 Http Error", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => + Promise.reject( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 404, + } as AxiosResponse), + ), + findBySiret: () => Promise.reject(), + }); + + await assert.isRejected( + getOrganizationInfo("200071843"), + InseeNotFoundError, + ); + }); + + it("should throw a connecction error on Axios ECONNABORTED Error", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.reject(new AxiosError(undefined, AxiosError.ECONNABORTED)), + }); + + await assert.isRejected( + getOrganizationInfo("53512638700013"), + InseeConnectionError, + ); + }); + + it("should throw a connecction error on Axios ERR_BAD_RESPONSE Error", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.reject(new AxiosError(undefined, AxiosError.ERR_BAD_RESPONSE)), + }); + + await assert.isRejected( + getOrganizationInfo("53512638700013"), + InseeConnectionError, + ); + }); + + it("should throw a connecction error on Axios EAI_AGAIN Error", async () => { + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => Promise.reject(new AxiosError(undefined, "EAI_AGAIN")), + }); + + await assert.isRejected( + getOrganizationInfo("53512638700013"), + InseeConnectionError, + ); + }); +}); diff --git a/packages/identite/src/organization/get-organization-info.ts b/packages/identite/src/organization/get-organization-info.ts new file mode 100644 index 00000000..caef96ea --- /dev/null +++ b/packages/identite/src/organization/get-organization-info.ts @@ -0,0 +1,256 @@ +// + +import type { OrganizationInfo } from "#src/types"; +import type { + FindBySirenHandler, + FindBySiretHandler, +} from "@gouvfr-lasuite/proconnect.insee/entreprises"; +import { + InseeConnectionError, + InseeNotFoundError, +} from "@gouvfr-lasuite/proconnect.insee/errors"; +import { + formatAdresseEtablissement, + formatEnseigne, + formatNomComplet, + libelleFromCategoriesJuridiques, + libelleFromCodeEffectif, + libelleFromCodeNaf, +} from "@gouvfr-lasuite/proconnect.insee/formatters"; +import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; +import { to } from "await-to-js"; +import { AxiosError } from "axios"; +import { cloneDeep, set } from "lodash-es"; + +// + +export class InvalidSiretError extends Error {} + +// +type FactoryDependencies = { + findBySiret: FindBySiretHandler; + findBySiren: FindBySirenHandler; +}; + +export function getOrganizationInfoFactory(ctx: FactoryDependencies) { + const { findBySiren, findBySiret } = ctx; + + const siret = [/^\d{14}$/, findBySiret] as const; + const siren = [/^\d{9}$/, findBySiren] as const; + + const strategies = [siret, siren]; + + return async function getOrganizationInfo( + siretOrSiren: string, + ): Promise { + const [_, finder] = + strategies.find(([pattern]) => pattern.test(siretOrSiren)) ?? []; + + if (!finder) { + throw new InvalidSiretError(); + } + + const [finder_error, etablissement] = await to(finder(siretOrSiren)); + if (finder_error) { + if ( + finder_error instanceof AxiosError && + finder_error.response && + [403, 404].includes(finder_error.response.status) + ) { + throw new InseeNotFoundError(); + } + + if ( + finder_error instanceof AxiosError && + (finder_error.code === "ECONNABORTED" || + finder_error.code === "ERR_BAD_RESPONSE" || + finder_error.code === "EAI_AGAIN") + ) { + throw new InseeConnectionError(); + } + + throw finder_error; + } + + const { statutDiffusionEtablissement } = etablissement; + + if (statutDiffusionEtablissement === "N") { + throw new InseeNotFoundError(); + } + + return etablissementToOrganizationInfo( + statutDiffusionEtablissement === "P" + ? hideNonDiffusibleData(etablissement) + : etablissement, + ); + }; +} + +function etablissementToOrganizationInfo( + etablissement: InseeEtablissement, +): OrganizationInfo { + const { + adresseEtablissement, + anneeEffectifsEtablissement, + periodesEtablissement, + siret: siretFromInseeApi, + statutDiffusionEtablissement, + trancheEffectifsEtablissement, + uniteLegale, + } = etablissement; + + const { + categorieJuridiqueUniteLegale, + denominationUniteLegale, + sigleUniteLegale, + nomUniteLegale, + nomUsageUniteLegale, + prenomUsuelUniteLegale, + trancheEffectifsUniteLegale, + } = uniteLegale; + + // get last period to obtain most recent data + const { + activitePrincipaleEtablissement, + enseigne1Etablissement, + enseigne2Etablissement, + enseigne3Etablissement, + etatAdministratifEtablissement, + } = periodesEtablissement[0]; + + const { codePostalEtablissement, codeCommuneEtablissement } = + adresseEtablissement; + + const enseigne = formatEnseigne( + enseigne1Etablissement, + enseigne2Etablissement, + enseigne3Etablissement, + ); + + const nomComplet = formatNomComplet({ + denominationUniteLegale, + prenomUsuelUniteLegale, + nomUniteLegale, + nomUsageUniteLegale, + sigleUniteLegale, + }); + + const organizationLabel = `${nomComplet}${enseigne ? ` - ${enseigne}` : ""}`; + + return { + siret: siretFromInseeApi, + libelle: organizationLabel, + nomComplet, + enseigne, + trancheEffectifs: trancheEffectifsEtablissement, + trancheEffectifsUniteLegale, + libelleTrancheEffectif: + libelleFromCodeEffectif( + trancheEffectifsEtablissement, + anneeEffectifsEtablissement, + ) ?? "", + etatAdministratif: etatAdministratifEtablissement, + estActive: etatAdministratifEtablissement === "A", + statutDiffusion: statutDiffusionEtablissement, + estDiffusible: statutDiffusionEtablissement === "O", + adresse: formatAdresseEtablissement(adresseEtablissement), + codePostal: codePostalEtablissement, + codeOfficielGeographique: codeCommuneEtablissement, + activitePrincipale: activitePrincipaleEtablissement, + libelleActivitePrincipale: libelleFromCodeNaf( + activitePrincipaleEtablissement, + ), + categorieJuridique: `${categorieJuridiqueUniteLegale}`, + libelleCategorieJuridique: + libelleFromCategoriesJuridiques(categorieJuridiqueUniteLegale) ?? "", + }; +} + +const hideNonDiffusibleData = ( + etablissement: InseeEtablissement, +): InseeEtablissement => { + const hiddenEtablissement = cloneDeep(etablissement); + set(hiddenEtablissement, "uniteLegale.denominationUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.sigleUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.denominationUsuelle1UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.denominationUsuelle2UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.denominationUsuelle3UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.sexeUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.nomUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.nomUsageUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom1UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom2UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom3UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenom4UniteLegale", null); + set(hiddenEtablissement, "uniteLegale.prenomUsuelUniteLegale", null); + set(hiddenEtablissement, "uniteLegale.pseudonymeUniteLegale", null); + set( + hiddenEtablissement, + "adresseEtablissement.complementAdresseEtablissement", + null, + ); + set( + hiddenEtablissement, + "adresseEtablissement.numeroVoieEtablissement", + null, + ); + set( + hiddenEtablissement, + "adresseEtablissement.indiceRepetitionEtablissement", + null, + ); + set(hiddenEtablissement, "adresseEtablissement.typeVoieEtablissement", null); + set( + hiddenEtablissement, + "adresseEtablissement.libelleVoieEtablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.complementAdresse2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.numeroVoie2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.indiceRepetition2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.typeVoie2Etablissement", + null, + ); + set( + hiddenEtablissement, + "adresse2Etablissement.libelleVoie2Etablissement", + null, + ); + + set( + hiddenEtablissement, + "periodesEtablissement.0.enseigne1Etablissement", + null, + ); + set( + hiddenEtablissement, + "periodesEtablissement.0.enseigne2Etablissement", + null, + ); + set( + hiddenEtablissement, + "periodesEtablissement.0.enseigne3Etablissement", + null, + ); + set( + hiddenEtablissement, + "periodesEtablissement.0.denominationUsuelleEtablissement", + null, + ); + + return hiddenEtablissement; +}; diff --git a/packages/identite/src/organization/index.ts b/packages/identite/src/organization/index.ts index 8ed86e54..4ee2827f 100644 --- a/packages/identite/src/organization/index.ts +++ b/packages/identite/src/organization/index.ts @@ -1,3 +1,4 @@ // +export * from "./get-organization-info.js"; export * from "./upsert.js"; diff --git a/packages/identite/src/types/index.ts b/packages/identite/src/types/index.ts index 68505bb0..1c97d28b 100644 --- a/packages/identite/src/types/index.ts +++ b/packages/identite/src/types/index.ts @@ -3,5 +3,4 @@ export * from "./contexts.js"; export * from "./organization-info.js"; export * from "./organization.js"; -export * from "./tranche-effectifs.js"; export * from "./user.js"; diff --git a/packages/identite/src/types/organization-info.ts b/packages/identite/src/types/organization-info.ts index 4ee941a7..598a0b9f 100644 --- a/packages/identite/src/types/organization-info.ts +++ b/packages/identite/src/types/organization-info.ts @@ -1,6 +1,6 @@ // -import type { TrancheEffectifs } from "./tranche-effectifs.js"; +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; // diff --git a/packages/identite/src/types/organization.ts b/packages/identite/src/types/organization.ts index 2b21b95f..ded1fce8 100644 --- a/packages/identite/src/types/organization.ts +++ b/packages/identite/src/types/organization.ts @@ -1,6 +1,6 @@ // -import type { TrancheEffectifs } from "./tranche-effectifs.js"; +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; // diff --git a/packages/insee/package.json b/packages/insee/package.json index dc9dbf98..1856b893 100644 --- a/packages/insee/package.json +++ b/packages/insee/package.json @@ -13,8 +13,8 @@ "type": "module", "imports": { "#src/*": { - "types": "./src/*", - "default": "./dist/*" + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" } }, "exports": { diff --git a/src/connectors/api-sirene/categories-juridiques.ts b/packages/insee/src/data/categories-juridiques.ts similarity index 99% rename from src/connectors/api-sirene/categories-juridiques.ts rename to packages/insee/src/data/categories-juridiques.ts index 0d25fce5..5c7bc198 100644 --- a/src/connectors/api-sirene/categories-juridiques.ts +++ b/packages/insee/src/data/categories-juridiques.ts @@ -1,3 +1,6 @@ +// + +export type CategoriesJuridique = keyof typeof categoriesJuridiques; export const categoriesJuridiques = { 1: "Entrepreneur individuel", 2: "Groupement de droit privé non doté de la personnalité morale", diff --git a/src/connectors/api-sirene/codes-effectifs.ts b/packages/insee/src/data/codes-effectifs.ts similarity index 90% rename from src/connectors/api-sirene/codes-effectifs.ts rename to packages/insee/src/data/codes-effectifs.ts index 3d4b00cb..a4697609 100644 --- a/src/connectors/api-sirene/codes-effectifs.ts +++ b/packages/insee/src/data/codes-effectifs.ts @@ -1,4 +1,8 @@ -import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.identite/types"; +// + +import type { TrancheEffectifs } from "#src/types/tranche-effectifs.js"; + +// export const codesEffectifs: { [K in NonNullable]: string } = { diff --git a/src/connectors/api-sirene/codes-naf.ts b/packages/insee/src/data/codes-naf.ts similarity index 99% rename from src/connectors/api-sirene/codes-naf.ts rename to packages/insee/src/data/codes-naf.ts index 7d4a326a..18229ee0 100644 --- a/src/connectors/api-sirene/codes-naf.ts +++ b/packages/insee/src/data/codes-naf.ts @@ -1,3 +1,7 @@ +// + +export type CodeNaf = keyof typeof codesNaf; + export const codesNaf = { "01.11Z": "Culture de céréales (à l’exception du riz), de légumineuses et de graines oléagineuses", diff --git a/src/connectors/api-sirene/codes-voie.ts b/packages/insee/src/data/codes-voie.ts similarity index 95% rename from src/connectors/api-sirene/codes-voie.ts rename to packages/insee/src/data/codes-voie.ts index 07a9a8a0..013db1bf 100644 --- a/src/connectors/api-sirene/codes-voie.ts +++ b/packages/insee/src/data/codes-voie.ts @@ -1,3 +1,6 @@ +// + +export type CodeVoie = keyof typeof codesVoies; export const codesVoies = { AIRE: "Aire", ALL: "Allée", diff --git a/packages/insee/src/data/index.ts b/packages/insee/src/data/index.ts new file mode 100644 index 00000000..a2b127a8 --- /dev/null +++ b/packages/insee/src/data/index.ts @@ -0,0 +1,6 @@ +// + +export * from "./categories-juridiques.js"; +export * from "./codes-effectifs.js"; +export * from "./codes-naf.js"; +export * from "./codes-voie.js"; diff --git a/packages/insee/src/entreprises/find-by-siren.ts b/packages/insee/src/entreprises/find-by-siren.ts new file mode 100644 index 00000000..b8a21365 --- /dev/null +++ b/packages/insee/src/entreprises/find-by-siren.ts @@ -0,0 +1,40 @@ +// + +import { InvalidSirenError } from "#src/errors"; +import type { GetInseeAccessTokenHandler } from "#src/token"; +import type { EtablissementSearchResponse } from "#src/types"; +import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; + +// + +type FactoryDependencies = { + getAccessToken: GetInseeAccessTokenHandler; + config?: AxiosRequestConfig; +}; + +export function findBySirenFactory({ + getAccessToken, + config, +}: FactoryDependencies) { + return async function findBySiren(siren: string) { + const token = await getAccessToken(); + const { data }: AxiosResponse = + await axios.get( + `https://api.insee.fr/entreprises/sirene/siret?q=siren:${siren} AND etablissementSiege:true`, + { + headers: { Authorization: `Bearer ${token}` }, + ...config, + }, + ); + + const etablissement = data.etablissements.at(0); + + if (!etablissement) { + throw new InvalidSirenError(); + } + + return etablissement; + }; +} + +export type FindBySirenHandler = ReturnType; diff --git a/packages/insee/src/entreprises/find-by-siret.ts b/packages/insee/src/entreprises/find-by-siret.ts new file mode 100644 index 00000000..b2012515 --- /dev/null +++ b/packages/insee/src/entreprises/find-by-siret.ts @@ -0,0 +1,34 @@ +// + +import type { GetInseeAccessTokenHandler } from "#src/token"; +import type { EtablissementSearchBySiretResponse } from "#src/types"; +import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import axios from "axios"; + +// + +type FactoryDependencies = { + getAccessToken: GetInseeAccessTokenHandler; + config?: AxiosRequestConfig; +}; + +export function findBySiretFactory({ + getAccessToken, + config, +}: FactoryDependencies) { + return async function findBySiret(siret: string) { + const token = await getAccessToken(); + const { data }: AxiosResponse = + await axios.get( + `https://api.insee.fr/entreprises/sirene/siret/${siret}`, + { + headers: { Authorization: `Bearer ${token}` }, + ...config, + }, + ); + + return data.etablissement; + }; +} + +export type FindBySiretHandler = ReturnType; diff --git a/packages/insee/src/entreprises/index.ts b/packages/insee/src/entreprises/index.ts new file mode 100644 index 00000000..a70b285c --- /dev/null +++ b/packages/insee/src/entreprises/index.ts @@ -0,0 +1,4 @@ +// + +export * from "./find-by-siren.js"; +export * from "./find-by-siret.js"; diff --git a/packages/insee/src/errors/index.ts b/packages/insee/src/errors/index.ts new file mode 100644 index 00000000..6930b5e7 --- /dev/null +++ b/packages/insee/src/errors/index.ts @@ -0,0 +1,8 @@ +// + +export class InseeNotFoundError extends Error {} + +export class InvalidSiretError extends Error {} +export class InvalidSirenError extends Error {} + +export class InseeConnectionError extends Error {} diff --git a/packages/insee/src/formatters/adresse-etablissement.ts b/packages/insee/src/formatters/adresse-etablissement.ts new file mode 100644 index 00000000..94ae9f58 --- /dev/null +++ b/packages/insee/src/formatters/adresse-etablissement.ts @@ -0,0 +1,75 @@ +// + +import { codesVoies, type CodeVoie } from "#src/data"; +import type { InseeEtablissement } from "#src/types"; +import { capitalize } from "lodash-es"; + +// + +export const formatAdresseEtablissement = ({ + complementAdresseEtablissement, + numeroVoieEtablissement, + indiceRepetitionEtablissement, + typeVoieEtablissement, + libelleVoieEtablissement, + distributionSpecialeEtablissement, + codePostalEtablissement, + libelleCommuneEtablissement, + codeCedexEtablissement, + libelleCedexEtablissement, + libelleCommuneEtrangerEtablissement, + codePaysEtrangerEtablissement, + libellePaysEtrangerEtablissement, +}: InseeEtablissement["adresseEtablissement"]) => { + if ( + !complementAdresseEtablissement && + !numeroVoieEtablissement && + !typeVoieEtablissement && + !libelleCommuneEtablissement && + !distributionSpecialeEtablissement && + !codePostalEtablissement && + !codeCedexEtablissement && + !libelleVoieEtablissement && + !libelleCommuneEtrangerEtablissement && + !codePaysEtrangerEtablissement && + !libellePaysEtrangerEtablissement + ) { + return ""; + } + + const fullLibelleFromTypeVoie = libelleFromTypeVoie(typeVoieEtablissement); + + return [ + wrapWord(complementAdresseEtablissement, ", ", true), + wrapWord(numeroVoieEtablissement), + wrapWord(indiceRepetitionEtablissement), + wrapWord(fullLibelleFromTypeVoie), + wrapWord(libelleVoieEtablissement, ", "), + wrapWord(distributionSpecialeEtablissement, ", "), + wrapWord(codePostalEtablissement || codeCedexEtablissement), + wrapWord( + libelleCommuneEtablissement || + libelleCedexEtablissement || + libelleCommuneEtrangerEtablissement, + "", + true, + ), + libellePaysEtrangerEtablissement + ? `, ${wrapWord(libellePaysEtrangerEtablissement, "", true)}` + : "", + ].join(""); +}; + +const libelleFromTypeVoie = (codeVoie: CodeVoie) => { + return codesVoies[codeVoie] || codeVoie; +}; + +const wrapWord = (word: string | null, punct = " ", caps = false) => { + if (!word) { + return ""; + } + if (caps) { + return capitalize(word) + punct; + } + return word.toString().toLowerCase() + punct; +}; diff --git a/packages/insee/src/formatters/enseigne.ts b/packages/insee/src/formatters/enseigne.ts new file mode 100644 index 00000000..3f861952 --- /dev/null +++ b/packages/insee/src/formatters/enseigne.ts @@ -0,0 +1,8 @@ +// + +import { capitalize, isEmpty } from "lodash-es"; + +// + +export const formatEnseigne = (...args: (string | null)[]) => + capitalize(args.filter((e) => !isEmpty(e)).join(" ")) || ""; diff --git a/packages/insee/src/formatters/index.ts b/packages/insee/src/formatters/index.ts new file mode 100644 index 00000000..bc58830c --- /dev/null +++ b/packages/insee/src/formatters/index.ts @@ -0,0 +1,8 @@ +// + +export * from "./adresse-etablissement.js"; +export * from "./enseigne.js"; +export * from "./libelle-from-categories-juridiques.js"; +export * from "./libelle-from-code-effectif.js"; +export * from "./libelle-from-code-naf.js"; +export * from "./nom-complet.js"; diff --git a/packages/insee/src/formatters/libelle-from-categories-juridiques.ts b/packages/insee/src/formatters/libelle-from-categories-juridiques.ts new file mode 100644 index 00000000..10fec25c --- /dev/null +++ b/packages/insee/src/formatters/libelle-from-categories-juridiques.ts @@ -0,0 +1,9 @@ +// + +import { categoriesJuridiques, type CategoriesJuridique } from "#src/data"; + +// + +export const libelleFromCategoriesJuridiques = ( + categorie: CategoriesJuridique, +) => categoriesJuridiques[categorie] || null; diff --git a/packages/insee/src/formatters/libelle-from-code-effectif.ts b/packages/insee/src/formatters/libelle-from-code-effectif.ts new file mode 100644 index 00000000..b16a92f6 --- /dev/null +++ b/packages/insee/src/formatters/libelle-from-code-effectif.ts @@ -0,0 +1,25 @@ +// + +import { codesEffectifs } from "#src/data"; +import type { TrancheEffectifs } from "#src/types/tranche-effectifs.js"; + +// + +export const libelleFromCodeEffectif = ( + codeEffectif: NonNullable, + anneeEffectif: string, + characterEmployeurUniteLegale?: string, +) => { + const libelle = codesEffectifs[codeEffectif]; + + if (libelle && anneeEffectif) { + return `${libelle}, en ${anneeEffectif}`; + } + if (libelle) { + return libelle; + } + if (characterEmployeurUniteLegale === "N") { + return "Unité non employeuse"; + } + return null; +}; diff --git a/packages/insee/src/formatters/libelle-from-code-naf.ts b/packages/insee/src/formatters/libelle-from-code-naf.ts new file mode 100644 index 00000000..f445530e --- /dev/null +++ b/packages/insee/src/formatters/libelle-from-code-naf.ts @@ -0,0 +1,10 @@ +// + +import { codesNaf, type CodeNaf } from "#src/data"; + +// + +export const libelleFromCodeNaf = (codeNaf: CodeNaf, addCode = true) => { + const label = codesNaf[codeNaf] || "Activité inconnue"; + return addCode && codeNaf ? `${codeNaf} - ${label}` : label; +}; diff --git a/packages/insee/src/formatters/nom-complet.ts b/packages/insee/src/formatters/nom-complet.ts new file mode 100644 index 00000000..8a0cc8e0 --- /dev/null +++ b/packages/insee/src/formatters/nom-complet.ts @@ -0,0 +1,47 @@ +// + +import { capitalize } from "lodash-es"; + +// + +type FormatNomCompletArgs = { + denominationUniteLegale: string; + prenomUsuelUniteLegale: string | null; + nomUniteLegale: string | null; + nomUsageUniteLegale: string | null; + sigleUniteLegale: string | null; +}; + +export const formatNomComplet = ({ + denominationUniteLegale, + prenomUsuelUniteLegale, + nomUniteLegale, + nomUsageUniteLegale, + sigleUniteLegale, +}: FormatNomCompletArgs) => { + const formattedFirstName = formatFirstNames([prenomUsuelUniteLegale ?? ""]); + const formattedName = formatNameFull( + nomUniteLegale ?? "", + nomUsageUniteLegale ?? "", + ); + return `${ + capitalize(denominationUniteLegale) || + [formattedFirstName, formattedName].filter((e) => !!e).join(" ") || + "Nom inconnu" + }${sigleUniteLegale ? ` (${sigleUniteLegale})` : ""}`; +}; + +const formatFirstNames = (firstNames: string[], nameCount = 0) => { + const formatted = firstNames.map(capitalize).filter((name) => !!name); + if (nameCount > 0 && nameCount < firstNames.length) { + return formatted.slice(0, nameCount).join(", "); + } + return formatted.join(", "); +}; + +const formatNameFull = (nomPatronymique = "", nomUsage = "") => { + if (nomUsage && nomPatronymique) { + return `${capitalize(nomUsage)} (${capitalize(nomPatronymique)})`; + } + return capitalize(nomUsage || nomPatronymique || ""); +}; diff --git a/packages/insee/src/token/get-insee-access-token.ts b/packages/insee/src/token/get-insee-access-token.ts index 6f4ec0ff..381106a9 100644 --- a/packages/insee/src/token/get-insee-access-token.ts +++ b/packages/insee/src/token/get-insee-access-token.ts @@ -40,3 +40,7 @@ export function getInseeAccessTokenFactory( return access_token; }; } + +export type GetInseeAccessTokenHandler = ReturnType< + typeof getInseeAccessTokenFactory +>; diff --git a/packages/insee/src/types/index.ts b/packages/insee/src/types/index.ts new file mode 100644 index 00000000..1d4896b8 --- /dev/null +++ b/packages/insee/src/types/index.ts @@ -0,0 +1,159 @@ +// + +import type { CategoriesJuridique, CodeNaf, CodeVoie } from "#src/data"; +import type { TrancheEffectifs } from "./tranche-effectifs.js"; + +// + +export * from "./tranche-effectifs.js"; + +// + +export type EtablissementSearchBySiretResponse = { + etablissement: InseeEtablissement; +}; + +export type EtablissementSearchResponse = { + header: { + total: number; + debut: number; + nombre: number; + }; + etablissements: InseeEtablissement[]; +}; + +export type InseeEtablissement = { + // ex: '217400563' + siren: string; + // ex: '00011' + nic: string; + // ex: '21740056300011' + siret: string; + // ex: 'O' + statutDiffusionEtablissement: "O" | "P" | "N"; + // ex: '1983-03-01' + dateCreationEtablissement: string; + // ex: '32' + trancheEffectifsEtablissement: TrancheEffectifs; + // ex: '2020' + anneeEffectifsEtablissement: string; + activitePrincipaleRegistreMetiersEtablissement: string | null; + // ex: '2022-08-29T09:08:45' + dateDernierTraitementEtablissement: string; + // ex: true + etablissementSiege: boolean; + // ex: 4 + nombrePeriodesEtablissement: number; + uniteLegale: { + // ex: 'A' + etatAdministratifUniteLegale: string; + // ex: 'O' + statutDiffusionUniteLegale: "O" | "P" | "N"; + // ex: '1982-01-01' + dateCreationUniteLegale: string; + // ex: '7210' + categorieJuridiqueUniteLegale: CategoriesJuridique; + // ex: 'COMMUNE DE CHAMONIX MONT BLANC' + denominationUniteLegale: string; + sigleUniteLegale: string | null; + denominationUsuelle1UniteLegale: string | null; + denominationUsuelle2UniteLegale: string | null; + denominationUsuelle3UniteLegale: string | null; + sexeUniteLegale: string | null; + nomUniteLegale: string | null; + nomUsageUniteLegale: string | null; + prenom1UniteLegale: string | null; + prenom2UniteLegale: string | null; + prenom3UniteLegale: string | null; + prenom4UniteLegale: string | null; + prenomUsuelUniteLegale: string | null; + pseudonymeUniteLegale: string | null; + // ex: '84.11Z' + activitePrincipaleUniteLegale: string; + nomenclatureActivitePrincipaleUniteLegale: "NAFRev2"; + identifiantAssociationUniteLegale: string | null; + // ex: 'N' + economieSocialeSolidaireUniteLegale: string; + // ex: 'N' + societeMissionUniteLegale: string; + // ex: 'O' + caractereEmployeurUniteLegale: string; + // ex: '32' + trancheEffectifsUniteLegale: TrancheEffectifs; + // ex: '2020' + anneeEffectifsUniteLegale: string; + // ex: '00011' + nicSiegeUniteLegale: string; + // ex: '2023-03-01T20:13:11' + dateDernierTraitementUniteLegale: string; + // ex: 'ETI' + categorieEntreprise: string; + // ex: '2020' + anneeCategorieEntreprise: string; + }; + adresseEtablissement: { + complementAdresseEtablissement: string | null; + // ex: '38' + numeroVoieEtablissement: string; + indiceRepetitionEtablissement: string | null; + // ex: 'PL' + typeVoieEtablissement: CodeVoie; + // ex: 'DE L EGLISE' + libelleVoieEtablissement: string; + // ex: '74400' + codePostalEtablissement: string; + // ex: 'CHAMONIX-MONT-BLANC' + libelleCommuneEtablissement: string; + libelleCommuneEtrangerEtablissement: string | null; + distributionSpecialeEtablissement: string | null; + // ex: '74056' + codeCommuneEtablissement: string; + codeCedexEtablissement: string | null; + libelleCedexEtablissement: string | null; + codePaysEtrangerEtablissement: string | null; + libellePaysEtrangerEtablissement: string | null; + }; + adresse2Etablissement: { + complementAdresse2Etablissement: null; + numeroVoie2Etablissement: null; + indiceRepetition2Etablissement: null; + typeVoie2Etablissement: null; + libelleVoie2Etablissement: null; + codePostal2Etablissement: null; + libelleCommune2Etablissement: null; + libelleCommuneEtranger2Etablissement: null; + distributionSpeciale2Etablissement: null; + codeCommune2Etablissement: null; + codeCedex2Etablissement: null; + libelleCedex2Etablissement: null; + codePaysEtranger2Etablissement: null; + libellePaysEtranger2Etablissement: null; + }; + periodesEtablissement: { + dateFin: string | null; + // ex: '2008-01-01' + dateDebut: string; + // ex: 'A' + etatAdministratifEtablissement: string; + // ex: false + changementEtatAdministratifEtablissement: boolean; + // ex: 'MAIRIE CHAMONIX - ARGENTIERE' + enseigne1Etablissement: string; + enseigne2Etablissement: null; + enseigne3Etablissement: null; + // ex: false + changementEnseigneEtablissement: boolean; + denominationUsuelleEtablissement: null; + // ex: false + changementDenominationUsuelleEtablissement: boolean; + // ex: '84.11Z' + activitePrincipaleEtablissement: CodeNaf; + nomenclatureActivitePrincipaleEtablissement: "NAFRev2"; + // ex: true + changementActivitePrincipaleEtablissement: boolean; + // ex: 'O' + caractereEmployeurEtablissement: string; + // ex: false + changementCaractereEmployeurEtablissement: boolean; + }[]; +}; diff --git a/packages/identite/src/types/tranche-effectifs.ts b/packages/insee/src/types/tranche-effectifs.ts similarity index 100% rename from packages/identite/src/types/tranche-effectifs.ts rename to packages/insee/src/types/tranche-effectifs.ts diff --git a/src/connectors/api-sirene.ts b/src/connectors/api-sirene.ts new file mode 100644 index 00000000..e18cc5f1 --- /dev/null +++ b/src/connectors/api-sirene.ts @@ -0,0 +1,44 @@ +// + +import { getOrganizationInfoFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; +import { + findBySirenFactory, + findBySiretFactory, +} from "@gouvfr-lasuite/proconnect.insee/entreprises"; +import { getInseeAccessTokenFactory } from "@gouvfr-lasuite/proconnect.insee/token"; +import { + HTTP_CLIENT_TIMEOUT, + INSEE_CONSUMER_KEY, + INSEE_CONSUMER_SECRET, +} from "../config/env"; + +// + +export const getAccessToken = getInseeAccessTokenFactory( + { + consumerKey: INSEE_CONSUMER_KEY, + consumerSecret: INSEE_CONSUMER_SECRET, + }, + { + timeout: HTTP_CLIENT_TIMEOUT, + }, +); + +export const findBySiret = findBySiretFactory({ + getAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +export const findBySiren = findBySirenFactory({ + getAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +export const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren, + findBySiret, +}); diff --git a/src/connectors/api-sirene/formatters.js b/src/connectors/api-sirene/formatters.js deleted file mode 100644 index b10cec8a..00000000 --- a/src/connectors/api-sirene/formatters.js +++ /dev/null @@ -1,134 +0,0 @@ -import { capitalize, isEmpty } from "lodash-es"; -import { categoriesJuridiques } from "./categories-juridiques"; -import { codesEffectifs } from "./codes-effectifs"; -import { codesNaf } from "./codes-naf"; -import { codesVoies } from "./codes-voie"; - -export const formatEnseigne = (...args) => - capitalize(args.filter((e) => !isEmpty(e)).join(" ")) || ""; - -export const formatNomComplet = ({ - denominationUniteLegale, - prenomUsuelUniteLegale, - nomUniteLegale, - nomUsageUniteLegale, - sigleUniteLegale, -}) => { - const formattedFirstName = formatFirstNames([prenomUsuelUniteLegale]); - const formattedName = formatNameFull(nomUniteLegale, nomUsageUniteLegale); - return `${ - capitalize(denominationUniteLegale) || - [formattedFirstName, formattedName].filter((e) => !!e).join(" ") || - "Nom inconnu" - }${sigleUniteLegale ? ` (${sigleUniteLegale})` : ""}`; -}; - -export const formatNameFull = (nomPatronymique = "", nomUsage = "") => { - if (nomUsage && nomPatronymique) { - return `${capitalize(nomUsage)} (${capitalize(nomPatronymique)})`; - } - return capitalize(nomUsage || nomPatronymique || ""); -}; - -export const formatFirstNames = (firstNames, nameCount = 0) => { - const formatted = firstNames.map(capitalize).filter((name) => !!name); - if (nameCount > 0 && nameCount < firstNames.length) { - return formatted.slice(0, nameCount).join(", "); - } - return formatted.join(", "); -}; - -const wrapWord = (word, punct = " ", caps = false) => { - if (!word) { - return ""; - } - if (caps) { - return capitalize(word) + punct; - } - return word.toString().toLowerCase() + punct; -}; - -const libelleFromTypeVoie = (codeVoie) => { - return codesVoies[codeVoie] || codeVoie; -}; - -export const formatAdresseEtablissement = ({ - complementAdresseEtablissement, - numeroVoieEtablissement, - indiceRepetitionEtablissement, - typeVoieEtablissement, - libelleVoieEtablissement, - distributionSpecialeEtablissement, - codePostalEtablissement, - libelleCommuneEtablissement, - codeCedexEtablissement, - libelleCedexEtablissement, - libelleCommuneEtrangerEtablissement, - codePaysEtrangerEtablissement, - libellePaysEtrangerEtablissement, -}) => { - if ( - !complementAdresseEtablissement && - !numeroVoieEtablissement && - !typeVoieEtablissement && - !libelleCommuneEtablissement && - !distributionSpecialeEtablissement && - !codePostalEtablissement && - !codeCedexEtablissement && - !libelleVoieEtablissement && - !libelleCommuneEtrangerEtablissement && - !codePaysEtrangerEtablissement && - !libellePaysEtrangerEtablissement - ) { - return ""; - } - - const fullLibelleFromTypeVoie = libelleFromTypeVoie(typeVoieEtablissement); - - return [ - wrapWord(complementAdresseEtablissement, ", ", true), - wrapWord(numeroVoieEtablissement), - wrapWord(indiceRepetitionEtablissement), - wrapWord(fullLibelleFromTypeVoie), - wrapWord(libelleVoieEtablissement, ", "), - wrapWord(distributionSpecialeEtablissement, ", "), - wrapWord(codePostalEtablissement || codeCedexEtablissement), - wrapWord( - libelleCommuneEtablissement || - libelleCedexEtablissement || - libelleCommuneEtrangerEtablissement, - "", - true, - ), - libellePaysEtrangerEtablissement - ? `, ${wrapWord(libellePaysEtrangerEtablissement, "", true)}` - : "", - ].join(""); -}; - -export const libelleFromCodeNaf = (codeNaf = "", addCode = true) => { - const label = codesNaf[codeNaf] || "Activité inconnue"; - return addCode && codeNaf ? `${codeNaf} - ${label}` : label; -}; - -export const libelleFromCategoriesJuridiques = (categorie) => - categoriesJuridiques[categorie] || null; - -export const libelleFromCodeEffectif = ( - codeEffectif, - anneeEffectif, - characterEmployeurUniteLegale, -) => { - const libelle = codesEffectifs[codeEffectif]; - - if (libelle && anneeEffectif) { - return `${libelle}, en ${anneeEffectif}`; - } - if (libelle) { - return libelle; - } - if (characterEmployeurUniteLegale === "N") { - return "Unité non employeuse"; - } - return null; -}; diff --git a/src/connectors/api-sirene/index.ts b/src/connectors/api-sirene/index.ts deleted file mode 100644 index 659d798e..00000000 --- a/src/connectors/api-sirene/index.ts +++ /dev/null @@ -1,420 +0,0 @@ -import type { - OrganizationInfo, - TrancheEffectifs, -} from "@gouvfr-lasuite/proconnect.identite/types"; -import { getInseeAccessTokenFactory } from "@gouvfr-lasuite/proconnect.insee/token"; -import axios, { AxiosError, type AxiosResponse } from "axios"; -import { cloneDeep, set } from "lodash-es"; -import { - HTTP_CLIENT_TIMEOUT, - INSEE_CONSUMER_KEY, - INSEE_CONSUMER_SECRET, -} from "../../config/env"; -import { - InseeConnectionError, - InseeNotFoundError, - InvalidSiretError, -} from "../../config/errors"; -import { - formatAdresseEtablissement, - formatEnseigne, - formatNomComplet, - libelleFromCategoriesJuridiques, - libelleFromCodeEffectif, - libelleFromCodeNaf, -} from "./formatters"; - -type InseeEtablissement = { - // ex: '217400563' - siren: string; - // ex: '00011' - nic: string; - // ex: '21740056300011' - siret: string; - // ex: 'O' - statutDiffusionEtablissement: "O" | "P" | "N"; - // ex: '1983-03-01' - dateCreationEtablissement: string; - // ex: '32' - trancheEffectifsEtablissement: TrancheEffectifs; - // ex: '2020' - anneeEffectifsEtablissement: string; - activitePrincipaleRegistreMetiersEtablissement: string | null; - // ex: '2022-08-29T09:08:45' - dateDernierTraitementEtablissement: string; - // ex: true - etablissementSiege: boolean; - // ex: 4 - nombrePeriodesEtablissement: number; - uniteLegale: { - // ex: 'A' - etatAdministratifUniteLegale: string; - // ex: 'O' - statutDiffusionUniteLegale: "O" | "P" | "N"; - // ex: '1982-01-01' - dateCreationUniteLegale: string; - // ex: '7210' - categorieJuridiqueUniteLegale: string; - // ex: 'COMMUNE DE CHAMONIX MONT BLANC' - denominationUniteLegale: string; - sigleUniteLegale: string | null; - denominationUsuelle1UniteLegale: string | null; - denominationUsuelle2UniteLegale: string | null; - denominationUsuelle3UniteLegale: string | null; - sexeUniteLegale: string | null; - nomUniteLegale: string | null; - nomUsageUniteLegale: string | null; - prenom1UniteLegale: string | null; - prenom2UniteLegale: string | null; - prenom3UniteLegale: string | null; - prenom4UniteLegale: string | null; - prenomUsuelUniteLegale: string | null; - pseudonymeUniteLegale: string | null; - // ex: '84.11Z' - activitePrincipaleUniteLegale: string; - nomenclatureActivitePrincipaleUniteLegale: "NAFRev2"; - identifiantAssociationUniteLegale: string | null; - // ex: 'N' - economieSocialeSolidaireUniteLegale: string; - // ex: 'N' - societeMissionUniteLegale: string; - // ex: 'O' - caractereEmployeurUniteLegale: string; - // ex: '32' - trancheEffectifsUniteLegale: TrancheEffectifs; - // ex: '2020' - anneeEffectifsUniteLegale: string; - // ex: '00011' - nicSiegeUniteLegale: string; - // ex: '2023-03-01T20:13:11' - dateDernierTraitementUniteLegale: string; - // ex: 'ETI' - categorieEntreprise: string; - // ex: '2020' - anneeCategorieEntreprise: string; - }; - adresseEtablissement: { - complementAdresseEtablissement: string | null; - // ex: '38' - numeroVoieEtablissement: string; - indiceRepetitionEtablissement: string | null; - // ex: 'PL' - typeVoieEtablissement: string; - // ex: 'DE L EGLISE' - libelleVoieEtablissement: string; - // ex: '74400' - codePostalEtablissement: string; - // ex: 'CHAMONIX-MONT-BLANC' - libelleCommuneEtablissement: string; - libelleCommuneEtrangerEtablissement: string | null; - distributionSpecialeEtablissement: string | null; - // ex: '74056' - codeCommuneEtablissement: string; - codeCedexEtablissement: string | null; - libelleCedexEtablissement: string | null; - codePaysEtrangerEtablissement: string | null; - libellePaysEtrangerEtablissement: string | null; - }; - adresse2Etablissement: { - complementAdresse2Etablissement: null; - numeroVoie2Etablissement: null; - indiceRepetition2Etablissement: null; - typeVoie2Etablissement: null; - libelleVoie2Etablissement: null; - codePostal2Etablissement: null; - libelleCommune2Etablissement: null; - libelleCommuneEtranger2Etablissement: null; - distributionSpeciale2Etablissement: null; - codeCommune2Etablissement: null; - codeCedex2Etablissement: null; - libelleCedex2Etablissement: null; - codePaysEtranger2Etablissement: null; - libellePaysEtranger2Etablissement: null; - }; - periodesEtablissement: { - dateFin: string | null; - // ex: '2008-01-01' - dateDebut: string; - // ex: 'A' - etatAdministratifEtablissement: string; - // ex: false - changementEtatAdministratifEtablissement: boolean; - // ex: 'MAIRIE CHAMONIX - ARGENTIERE' - enseigne1Etablissement: string; - enseigne2Etablissement: null; - enseigne3Etablissement: null; - // ex: false - changementEnseigneEtablissement: boolean; - denominationUsuelleEtablissement: null; - // ex: false - changementDenominationUsuelleEtablissement: boolean; - // ex: '84.11Z' - activitePrincipaleEtablissement: string; - nomenclatureActivitePrincipaleEtablissement: "NAFRev2"; - // ex: true - changementActivitePrincipaleEtablissement: boolean; - // ex: 'O' - caractereEmployeurEtablissement: string; - // ex: false - changementCaractereEmployeurEtablissement: boolean; - }[]; -}; - -type EtablissementSearchBySiretResponse = { - etablissement: InseeEtablissement; -}; - -type EtablissementSearchResponse = { - header: { - total: number; - debut: number; - nombre: number; - }; - etablissements: InseeEtablissement[]; -}; - -const hideNonDiffusibleData = ( - etablissement: EtablissementSearchBySiretResponse["etablissement"], -): EtablissementSearchBySiretResponse["etablissement"] => { - const hiddenEtablissement = cloneDeep(etablissement); - set(hiddenEtablissement, "uniteLegale.denominationUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.sigleUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.denominationUsuelle1UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.denominationUsuelle2UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.denominationUsuelle3UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.sexeUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.nomUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.nomUsageUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom1UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom2UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom3UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenom4UniteLegale", null); - set(hiddenEtablissement, "uniteLegale.prenomUsuelUniteLegale", null); - set(hiddenEtablissement, "uniteLegale.pseudonymeUniteLegale", null); - set( - hiddenEtablissement, - "adresseEtablissement.complementAdresseEtablissement", - null, - ); - set( - hiddenEtablissement, - "adresseEtablissement.numeroVoieEtablissement", - null, - ); - set( - hiddenEtablissement, - "adresseEtablissement.indiceRepetitionEtablissement", - null, - ); - set(hiddenEtablissement, "adresseEtablissement.typeVoieEtablissement", null); - set( - hiddenEtablissement, - "adresseEtablissement.libelleVoieEtablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.complementAdresse2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.numeroVoie2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.indiceRepetition2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.typeVoie2Etablissement", - null, - ); - set( - hiddenEtablissement, - "adresse2Etablissement.libelleVoie2Etablissement", - null, - ); - - set( - hiddenEtablissement, - "periodesEtablissement.0.enseigne1Etablissement", - null, - ); - set( - hiddenEtablissement, - "periodesEtablissement.0.enseigne2Etablissement", - null, - ); - set( - hiddenEtablissement, - "periodesEtablissement.0.enseigne3Etablissement", - null, - ); - set( - hiddenEtablissement, - "periodesEtablissement.0.denominationUsuelleEtablissement", - null, - ); - - return hiddenEtablissement; -}; - -export const getInseeAccessToken = getInseeAccessTokenFactory( - { - consumerKey: INSEE_CONSUMER_KEY, - consumerSecret: INSEE_CONSUMER_SECRET, - }, - { - timeout: HTTP_CLIENT_TIMEOUT, - }, -); - -export const getOrganizationInfo = async ( - siretOrSiren: string, - provided_access_token?: string, -): Promise => { - try { - let access_token = provided_access_token; - if (!access_token) { - access_token = await getInseeAccessToken(); - } - - let etablissement: InseeEtablissement; - - if (siretOrSiren.match(/^\d{14}$/)) { - let { data }: AxiosResponse = - await axios.get( - `https://api.insee.fr/entreprises/sirene/siret/${siretOrSiren}`, - { - headers: { Authorization: `Bearer ${access_token}` }, - timeout: HTTP_CLIENT_TIMEOUT, - }, - ); - - etablissement = data.etablissement; - } else if (siretOrSiren.match(/^\d{9}$/)) { - // siretOrSiren is a siren, we fetch the data of the siege social - let { data }: AxiosResponse = - await axios.get( - `https://api.insee.fr/entreprises/sirene/siret?q=siren:${siretOrSiren} AND etablissementSiege:true`, - { - headers: { Authorization: `Bearer ${access_token}` }, - timeout: HTTP_CLIENT_TIMEOUT, - }, - ); - - etablissement = data.etablissements[0]; - } else { - throw new InvalidSiretError(); - } - - const { statutDiffusionEtablissement } = etablissement; - - if (statutDiffusionEtablissement === "N") { - throw new InseeNotFoundError(); - } - - if (statutDiffusionEtablissement === "P") { - etablissement = hideNonDiffusibleData(etablissement); - } - - const { - siret: siretFromInseeApi, - trancheEffectifsEtablissement, - anneeEffectifsEtablissement, - adresseEtablissement, - periodesEtablissement, - uniteLegale, - } = etablissement; - - const { - categorieJuridiqueUniteLegale, - denominationUniteLegale, - sigleUniteLegale, - nomUniteLegale, - nomUsageUniteLegale, - prenomUsuelUniteLegale, - trancheEffectifsUniteLegale, - } = uniteLegale; - - // get last period to obtain most recent data - const { - activitePrincipaleEtablissement, - enseigne1Etablissement, - enseigne2Etablissement, - enseigne3Etablissement, - etatAdministratifEtablissement, - } = periodesEtablissement[0]; - - const { codePostalEtablissement, codeCommuneEtablissement } = - adresseEtablissement; - - const enseigne = formatEnseigne( - enseigne1Etablissement, - enseigne2Etablissement, - enseigne3Etablissement, - ); - - const nomComplet = formatNomComplet({ - denominationUniteLegale, - prenomUsuelUniteLegale, - nomUniteLegale, - nomUsageUniteLegale, - sigleUniteLegale, - }); - - const organizationLabel = `${nomComplet}${ - enseigne ? ` - ${enseigne}` : "" - }`; - - return { - siret: siretFromInseeApi, - libelle: organizationLabel, - nomComplet, - enseigne, - trancheEffectifs: trancheEffectifsEtablissement, - trancheEffectifsUniteLegale, - libelleTrancheEffectif: libelleFromCodeEffectif( - trancheEffectifsEtablissement, - anneeEffectifsEtablissement, - ), - etatAdministratif: etatAdministratifEtablissement, - estActive: etatAdministratifEtablissement === "A", - statutDiffusion: statutDiffusionEtablissement, - estDiffusible: statutDiffusionEtablissement === "O", - adresse: formatAdresseEtablissement(adresseEtablissement), - codePostal: codePostalEtablissement, - codeOfficielGeographique: codeCommuneEtablissement, - activitePrincipale: activitePrincipaleEtablissement, - libelleActivitePrincipale: libelleFromCodeNaf( - activitePrincipaleEtablissement, - ), - categorieJuridique: categorieJuridiqueUniteLegale, - libelleCategorieJuridique: libelleFromCategoriesJuridiques( - categorieJuridiqueUniteLegale, - ), - }; - } catch (e) { - if ( - e instanceof AxiosError && - e.response && - [403, 404].includes(e.response.status) - ) { - throw new InseeNotFoundError(); - } - - if ( - e instanceof AxiosError && - (e.code === "ECONNABORTED" || - e.code === "ERR_BAD_RESPONSE" || - e.code === "EAI_AGAIN") - ) { - throw new InseeConnectionError(); - } - - throw e; - } -}; diff --git a/test/api-sirene.test.ts b/test/api-sirene.test.ts deleted file mode 100644 index 4189e18b..00000000 --- a/test/api-sirene.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -import nock from "nock"; -import { InseeNotFoundError } from "../src/config/errors"; -import { getOrganizationInfo } from "../src/connectors/api-sirene"; -import diffusible from "./api-sirene-data/diffusible.json"; -import partiallyNonDiffusible from "./api-sirene-data/partially-non-diffusible.json"; -import searchBySiren from "./api-sirene-data/search-by-siren.json"; - -chai.use(chaiAsPromised); -const assert = chai.assert; - -describe("getOrganizationInfo", () => { - beforeEach(() => { - nock("https://api.insee.fr").post("/token").reply(200, { - access_token: "08e42802-9ac9-3403-a2a9-b5be11ce446c", - scope: "am_application_scope default", - token_type: "Bearer", - expires_in: 521596, - }); - }); - - const diffusibleOrganizationInfo = { - siret: "20007184300060", - libelle: "Cc du vexin normand", - nomComplet: "Cc du vexin normand", - enseigne: "", - trancheEffectifs: "22", - trancheEffectifsUniteLegale: "22", - libelleTrancheEffectif: "100 à 199 salariés, en 2021", - etatAdministratif: "A", - estActive: true, - statutDiffusion: "O", - estDiffusible: true, - adresse: "3 rue maison de vatimesnil, 27150 Etrepagny", - codePostal: "27150", - codeOfficielGeographique: "27226", - activitePrincipale: "84.11Z", - libelleActivitePrincipale: "84.11Z - Administration publique générale", - categorieJuridique: "7346", - libelleCategorieJuridique: "Communauté de communes", - }; - - it("should return valid payload for diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/20007184300060") - .reply(200, diffusible); - await assert.eventually.deepEqual( - getOrganizationInfo("20007184300060"), - diffusibleOrganizationInfo, - ); - }); - - it("should return valid payload for diffusible établissement", async () => { - nock("https://api.insee.fr") - .get( - "/entreprises/sirene/siret?q=siren:200071843 AND etablissementSiege:true", - ) - .reply(200, searchBySiren); - await assert.eventually.deepEqual( - getOrganizationInfo("200071843"), - diffusibleOrganizationInfo, - ); - }); - - it("should show partial data for partially non diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/94957325700019") - .reply(200, partiallyNonDiffusible); - - await assert.eventually.deepEqual(getOrganizationInfo("94957325700019"), { - siret: "94957325700019", - libelle: "Nom inconnu", - nomComplet: "Nom inconnu", - enseigne: "", - trancheEffectifs: null, - trancheEffectifsUniteLegale: null, - libelleTrancheEffectif: null, - etatAdministratif: "A", - estActive: true, - statutDiffusion: "P", - estDiffusible: false, - adresse: "06220 Vallauris", - codePostal: "06220", - codeOfficielGeographique: "06155", - activitePrincipale: "62.02A", - libelleActivitePrincipale: - "62.02A - Conseil en systèmes et logiciels informatiques", - categorieJuridique: "1000", - libelleCategorieJuridique: "Entrepreneur individuel", - }); - }); - - it("should throw for totally non diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/53512638700013") - .reply(403, { - header: { - statut: 403, - message: "Établissement non diffusable (53512638700013)", - }, - }); - await assert.isRejected( - getOrganizationInfo("53512638700013"), - InseeNotFoundError, - ); - }); -});