From 7e528cb9bfdcebad52a10397f0cce94682de7f32 Mon Sep 17 00:00:00 2001 From: Douglas Duteil Date: Tue, 7 Jan 2025 15:51:16 +0100 Subject: [PATCH] refactor(insee): extract insee connector to individual pkg (#899) --- Dockerfile | 2 + package-lock.json | 62 ++++- package.json | 7 +- packages/core/package.json | 2 +- packages/email/package.json | 2 +- packages/identite/package.json | 69 +++++ .../organization/__mocks__}/diffusible.json | 0 .../__mocks__}/partially-non-diffusible.json | 0 .../__mocks__}/search-by-siren.json | 0 .../get-organization-info.test.ts | 56 ++-- .../src/organization/get-organization-info.ts | 241 ++++++++++++++++ packages/identite/src/organization/index.ts | 4 + packages/identite/src/organization/upsert.ts | 140 ++++++++++ .../identite/src/organization/upset.test.ts | 54 ++++ .../src/services/hash-to-postgres-params.ts | 34 +++ packages/identite/src/services/index.ts | 3 + packages/identite/src/types/contexts.ts | 9 + packages/identite/src/types/index.ts | 6 + .../identite/src/types/organization-info.ts | 6 +- .../identite/src/types/organization.ts | 8 +- .../identite/src/types/user.ts | 40 +-- packages/identite/src/user/create.test.ts | 31 +++ packages/identite/src/user/create.ts | 42 +++ .../identite/src/user/find-by-email.test.ts | 45 +++ packages/identite/src/user/find-by-email.ts | 20 ++ packages/identite/src/user/index.ts | 5 + packages/identite/src/user/update.test.ts | 36 +++ packages/identite/src/user/update.ts | 29 ++ packages/identite/tsconfig.json | 24 ++ packages/identite/tsconfig.lib.json | 9 + packages/insee/package.json | 2 +- packages/insee/src/data/codes-effectifs.ts | 6 +- scripts/import-accounts-coop.ts | 18 +- scripts/import-domains.ts | 2 +- src/connectors/api-sirene.ts | 44 +++ src/connectors/api-sirene/index.ts | 262 ------------------ src/managers/moderation.ts | 1 + src/managers/organization/join.ts | 6 +- src/managers/organization/main.ts | 2 +- src/managers/session/authenticated.ts | 1 + src/managers/user.ts | 1 + src/repositories/organization/getters.ts | 5 +- src/repositories/organization/setters.ts | 134 +-------- src/repositories/user.ts | 80 +----- src/services/organization.ts | 2 +- src/services/script-helpers.ts | 2 +- test/organization.test.ts | 2 +- tsconfig.json | 6 +- 48 files changed, 1023 insertions(+), 539 deletions(-) create mode 100644 packages/identite/package.json rename {test/api-sirene-data => packages/identite/src/organization/__mocks__}/diffusible.json (100%) rename {test/api-sirene-data => packages/identite/src/organization/__mocks__}/partially-non-diffusible.json (100%) rename {test/api-sirene-data => packages/identite/src/organization/__mocks__}/search-by-siren.json (100%) rename test/api-sirene.test.ts => packages/identite/src/organization/get-organization-info.test.ts (64%) create mode 100644 packages/identite/src/organization/get-organization-info.ts create mode 100644 packages/identite/src/organization/index.ts create mode 100644 packages/identite/src/organization/upsert.ts create mode 100644 packages/identite/src/organization/upset.test.ts create mode 100644 packages/identite/src/services/hash-to-postgres-params.ts create mode 100644 packages/identite/src/services/index.ts create mode 100644 packages/identite/src/types/contexts.ts create mode 100644 packages/identite/src/types/index.ts rename src/types/organization-info.d.ts => packages/identite/src/types/organization-info.ts (83%) rename src/types/organization.d.ts => packages/identite/src/types/organization.ts (87%) rename src/types/user.d.ts => packages/identite/src/types/user.ts (96%) create mode 100644 packages/identite/src/user/create.test.ts create mode 100644 packages/identite/src/user/create.ts create mode 100644 packages/identite/src/user/find-by-email.test.ts create mode 100644 packages/identite/src/user/find-by-email.ts create mode 100644 packages/identite/src/user/index.ts create mode 100644 packages/identite/src/user/update.test.ts create mode 100644 packages/identite/src/user/update.ts create mode 100644 packages/identite/tsconfig.json create mode 100644 packages/identite/tsconfig.lib.json create mode 100644 src/connectors/api-sirene.ts delete mode 100644 src/connectors/api-sirene/index.ts diff --git a/Dockerfile b/Dockerfile index de2ffb24f..b77e295df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ + --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ --mount=type=cache,target=/root/.npm \ npm ci --omit=dev @@ -17,6 +18,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ --mount=type=bind,source=packages/core/package.json,target=packages/core/package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ + --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ --mount=type=cache,target=/root/.npm \ npm ci diff --git a/package-lock.json b/package-lock.json index b9d2d815f..00a2a3fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@gouvfr-lasuite/crisp": "https://github.com/douglasduteil/crisp/releases/download/v1.6.1/douglasduteil-crisp-1.6.1.tgz", "@gouvfr-lasuite/proconnect.core": "workspace:*", "@gouvfr-lasuite/proconnect.email": "workspace:*", + "@gouvfr-lasuite/proconnect.identite": "workspace:*", "@gouvfr-lasuite/proconnect.insee": "workspace:*", "@gouvfr/dsfr": "^1.12.1", "@kitajs/html": "^4.2.4", @@ -876,6 +877,13 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.15.tgz", + "integrity": "sha512-Jiq31Dnk+rg8rMhcSxs4lQvHTyizNo5b269c1gCC3ldQ0sCLrNVPGzy+KnmonKy1ZArTUuXZf23/UamzFMKVaA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1287,6 +1295,10 @@ "resolved": "packages/email", "link": true }, + "node_modules/@gouvfr-lasuite/proconnect.identite": { + "resolved": "packages/identite", + "link": true + }, "node_modules/@gouvfr-lasuite/proconnect.insee": { "resolved": "packages/insee", "link": true @@ -9153,6 +9165,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sql-template-tag": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/sql-template-tag/-/sql-template-tag-5.2.1.tgz", + "integrity": "sha512-lFdvXCOqWhV40A7w4oQVDyuaNFb5yO+dhsHStZzOdtDJWCBWYv4+hhATK5nPpY5v/T1OMVcLMPeN4519qIyb9Q==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -10405,6 +10426,7 @@ "version": "0.2.0", "license": "MIT", "dependencies": { + "@types/lodash-es": "^4.17.12", "@zootools/email-spell-checker": "^1.12.0", "bcryptjs": "^2.4.3", "is-disposable-email-domain": "^1.0.7", @@ -10456,15 +10478,43 @@ "vite": "^5.4.8" } }, - "packages/free-email": { - "name": "@gouvfr-lasuite/moncomptepro.free-email", - "version": "0.0.0", - "extraneous": true, + "packages/identite": { + "name": "@gouvfr-lasuite/proconnect.identite", + "version": "0.2.0", + "license": "MIT", "dependencies": { - "is-disposable-email-domain": "^1.0.7" + "sql-template-tag": "^5.2.1" }, "devDependencies": { - "@tsconfig/node22": "^22.0.0" + "@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", + "pg": "^8.13.0", + "tsx": "^4.19.2" + } + }, + "packages/identity-repository": { + "name": "@gouvfr-lasuite/proconnect.identity-repository", + "version": "0.2.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@electric-sql/pglite": "^0.2.15", + "@gouvfr-lasuite/proconnect.core": "^0.2.0", + "@tsconfig/node22": "^22.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.2", + "chai": "^5.1.2", + "mocha": "^11.0.1", + "pg": "^8.13.0", + "tsx": "^4.19.2" } }, "packages/insee": { diff --git a/package.json b/package.json index fdbd26a21..d36addbed 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ }, "main": "src/index.js", "workspaces": [ - "packages/*" + "packages/core", + "packages/email", + "packages/insee", + "packages/identite" ], "scripts": { "build": "run-s build:**", @@ -39,6 +42,7 @@ "watch:node": "tsx --watch src/index.ts", "watch:workspaces:core": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.core", "watch:workspaces:email": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.email", + "watch:workspaces:identite": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.identite", "watch:workspaces:insee": "npm run dev --if-present --workspace=@gouvfr-lasuite/proconnect.insee" }, "prettier": { @@ -52,6 +56,7 @@ "@gouvfr-lasuite/crisp": "https://github.com/douglasduteil/crisp/releases/download/v1.6.1/douglasduteil-crisp-1.6.1.tgz", "@gouvfr-lasuite/proconnect.core": "workspace:*", "@gouvfr-lasuite/proconnect.email": "workspace:*", + "@gouvfr-lasuite/proconnect.identite": "workspace:*", "@gouvfr-lasuite/proconnect.insee": "workspace:*", "@gouvfr/dsfr": "^1.12.1", "@kitajs/html": "^4.2.4", diff --git a/packages/core/package.json b/packages/core/package.json index fe191c1a2..85028bf7f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,7 +32,7 @@ } }, "scripts": { - "build": "tsc --project tsconfig.lib.json", + "build": "tsc --build tsconfig.lib.json", "check": "npm run build -- --noEmit", "dev": "npm run build -- --watch --preserveWatchOutput", "test": "mocha" diff --git a/packages/email/package.json b/packages/email/package.json index f44656d53..9356c3190 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -27,7 +27,7 @@ } }, "scripts": { - "build": "tsc --project ./tsconfig.lib.json", + "build": "tsc --build ./tsconfig.lib.json", "dev": "npm run build -- --watch --preserveWatchOutput", "storybook": "vite", "test": "tsc --noEmit" diff --git a/packages/identite/package.json b/packages/identite/package.json new file mode 100644 index 000000000..75465a207 --- /dev/null +++ b/packages/identite/package.json @@ -0,0 +1,69 @@ +{ + "name": "@gouvfr-lasuite/proconnect.identite", + "version": "0.2.0", + "homepage": "https://github.com/numerique-gouv/moncomptepro/tree/master/packages/identite#readme", + "bugs": "https://github.com/numerique-gouv/moncomptepro/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/numerique-gouv/moncomptepro.git", + "directory": "packages/identite" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "imports": { + "#src/*": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "exports": { + "./*": { + "require": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + }, + "import": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + }, + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "scripts": { + "build": "tsc --build tsconfig.lib.json", + "clean": "rm -rf dist tsconfig*.tsbuildinfo", + "check": "npm run build -- --noEmit", + "dev": "npm run build -- --watch --preserveWatchOutput", + "test": "mocha" + }, + "mocha": { + "reporter": "spec", + "require": [ + "tsx" + ], + "spec": "src/**/*.test.ts" + }, + "dependencies": { + "sql-template-tag": "^5.2.1" + }, + "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", + "pg": "^8.13.0", + "tsx": "^4.19.2" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/test/api-sirene-data/diffusible.json b/packages/identite/src/organization/__mocks__/diffusible.json similarity index 100% rename from test/api-sirene-data/diffusible.json rename to packages/identite/src/organization/__mocks__/diffusible.json diff --git a/test/api-sirene-data/partially-non-diffusible.json b/packages/identite/src/organization/__mocks__/partially-non-diffusible.json similarity index 100% rename from test/api-sirene-data/partially-non-diffusible.json rename to packages/identite/src/organization/__mocks__/partially-non-diffusible.json diff --git a/test/api-sirene-data/search-by-siren.json b/packages/identite/src/organization/__mocks__/search-by-siren.json similarity index 100% rename from test/api-sirene-data/search-by-siren.json rename to packages/identite/src/organization/__mocks__/search-by-siren.json diff --git a/test/api-sirene.test.ts b/packages/identite/src/organization/get-organization-info.test.ts similarity index 64% rename from test/api-sirene.test.ts rename to packages/identite/src/organization/get-organization-info.test.ts index 1eb2b83e8..2f24b0111 100644 --- a/test/api-sirene.test.ts +++ b/packages/identite/src/organization/get-organization-info.test.ts @@ -1,11 +1,12 @@ +import { InseeNotFoundError } from "@gouvfr-lasuite/proconnect.insee/errors"; +import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; 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"; +import diffusible from "./__mocks__/diffusible.json" with { type: "json" }; +import partiallyNonDiffusible from "./__mocks__/partially-non-diffusible.json" with { type: "json" }; +import searchBySiren from "./__mocks__/search-by-siren.json" with { type: "json" }; +import { getOrganizationInfoFactory } from "./get-organization-info.js"; chai.use(chaiAsPromised); const assert = chai.assert; @@ -42,9 +43,11 @@ describe("getOrganizationInfo", () => { }; it("should return valid payload for diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/20007184300060") - .reply(200, diffusible); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve(diffusible.etablissement as any as InseeEtablissement), + }); await assert.eventually.deepEqual( getOrganizationInfo("20007184300060"), diffusibleOrganizationInfo, @@ -52,11 +55,13 @@ describe("getOrganizationInfo", () => { }); 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); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => + Promise.resolve( + searchBySiren.etablissements[0] as any as InseeEtablissement, + ), + findBySiret: () => Promise.reject(), + }); await assert.eventually.deepEqual( getOrganizationInfo("200071843"), diffusibleOrganizationInfo, @@ -64,9 +69,13 @@ describe("getOrganizationInfo", () => { }); it("should show partial data for partially non diffusible établissement", async () => { - nock("https://api.insee.fr") - .get("/entreprises/sirene/siret/94957325700019") - .reply(200, partiallyNonDiffusible); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve( + partiallyNonDiffusible.etablissement as any as InseeEtablissement, + ), + }); await assert.eventually.deepEqual(getOrganizationInfo("94957325700019"), { siret: "94957325700019", @@ -92,14 +101,13 @@ describe("getOrganizationInfo", () => { }); 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)", - }, - }); + const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren: () => Promise.reject(), + findBySiret: () => + Promise.resolve({ + statutDiffusionEtablissement: "N", + } as InseeEtablissement), + }); await assert.isRejected( getOrganizationInfo("53512638700013"), InseeNotFoundError, 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 000000000..0090e3175 --- /dev/null +++ b/packages/identite/src/organization/get-organization-info.ts @@ -0,0 +1,241 @@ +import type { OrganizationInfo } from "@gouvfr-lasuite/proconnect.identite/types"; +import { + type FindBySirenHandler, + type FindBySiretHandler, +} from "@gouvfr-lasuite/proconnect.insee/api"; +import { + InseeConnectionError, + InseeNotFoundError, + InvalidSiretError, +} 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 { AxiosError } from "axios"; +import { cloneDeep, set } from "lodash-es"; + +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; +}; + +type FactoryDependencies = { + findBySiret: FindBySiretHandler; + findBySiren: FindBySirenHandler; +}; + +export function getOrganizationInfoFactory(dependencies: FactoryDependencies) { + const { findBySiren, findBySiret } = dependencies; + return async function getOrganizationInfo( + siretOrSiren: string, + ): Promise { + try { + let etablissement: InseeEtablissement; + + if (siretOrSiren.match(/^\d{14}$/)) { + etablissement = await findBySiret(siretOrSiren); + } else if (siretOrSiren.match(/^\d{9}$/)) { + etablissement = await findBySiren(siretOrSiren); + } 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: trancheEffectifsEtablissement + ? (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: String(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/packages/identite/src/organization/index.ts b/packages/identite/src/organization/index.ts new file mode 100644 index 000000000..4ee2827f7 --- /dev/null +++ b/packages/identite/src/organization/index.ts @@ -0,0 +1,4 @@ +// + +export * from "./get-organization-info.js"; +export * from "./upsert.js"; diff --git a/packages/identite/src/organization/upsert.ts b/packages/identite/src/organization/upsert.ts new file mode 100644 index 000000000..a42c8054b --- /dev/null +++ b/packages/identite/src/organization/upsert.ts @@ -0,0 +1,140 @@ +// + +import type { + DatabaseContext, + Organization, + OrganizationInfo, +} from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function upsertFactory({ pg }: DatabaseContext) { + return async function upsert({ + siret, + organizationInfo: { + libelle: cached_libelle, + nomComplet: cached_nom_complet, + enseigne: cached_enseigne, + trancheEffectifs: cached_tranche_effectifs, + trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale, + libelleTrancheEffectif: cached_libelle_tranche_effectif, + etatAdministratif: cached_etat_administratif, + estActive: cached_est_active, + statutDiffusion: cached_statut_diffusion, + estDiffusible: cached_est_diffusible, + adresse: cached_adresse, + codePostal: cached_code_postal, + codeOfficielGeographique: cached_code_officiel_geographique, + activitePrincipale: cached_activite_principale, + libelleActivitePrincipale: cached_libelle_activite_principale, + categorieJuridique: cached_categorie_juridique, + libelleCategorieJuridique: cached_libelle_categorie_juridique, + }, + }: { + siret: string; + organizationInfo: OrganizationInfo; + }) { + const { rows }: QueryResult = await pg.query( + ` + INSERT INTO organizations + ( + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + organization_info_fetched_at, + updated_at, + created_at + ) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) + ON CONFLICT (siret) + DO UPDATE + SET ( + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + organization_info_fetched_at, + updated_at + ) = ( + EXCLUDED.siret, + EXCLUDED.cached_libelle, + EXCLUDED.cached_nom_complet, + EXCLUDED.cached_enseigne, + EXCLUDED.cached_tranche_effectifs, + EXCLUDED.cached_tranche_effectifs_unite_legale, + EXCLUDED.cached_libelle_tranche_effectif, + EXCLUDED.cached_etat_administratif, + EXCLUDED.cached_est_active, + EXCLUDED.cached_statut_diffusion, + EXCLUDED.cached_est_diffusible, + EXCLUDED.cached_adresse, + EXCLUDED.cached_code_postal, + EXCLUDED.cached_code_officiel_geographique, + EXCLUDED.cached_activite_principale, + EXCLUDED.cached_libelle_activite_principale, + EXCLUDED.cached_categorie_juridique, + EXCLUDED.cached_libelle_categorie_juridique, + EXCLUDED.organization_info_fetched_at, + EXCLUDED.updated_at + ) + RETURNING * + `, + [ + siret, + cached_libelle, + cached_nom_complet, + cached_enseigne, + cached_tranche_effectifs, + cached_tranche_effectifs_unite_legale, + cached_libelle_tranche_effectif, + cached_etat_administratif, + cached_est_active, + cached_statut_diffusion, + cached_est_diffusible, + cached_adresse, + cached_code_postal, + cached_code_officiel_geographique, + cached_activite_principale, + cached_libelle_activite_principale, + cached_categorie_juridique, + cached_libelle_categorie_juridique, + new Date(), + new Date(), + new Date(), + ], + ); + + return rows.shift()!; + }; +} diff --git a/packages/identite/src/organization/upset.test.ts b/packages/identite/src/organization/upset.test.ts new file mode 100644 index 000000000..c440652b8 --- /dev/null +++ b/packages/identite/src/organization/upset.test.ts @@ -0,0 +1,54 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { upsertFactory } from "./upsert.js"; + +// + +const pg = new PGlite(); +const upset = upsertFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + migrationsTable: "pg-migrate", + log: noop, + }); +}); + +describe("upset", () => { + it("should create the Tau Empire organization", async () => { + const organization = await upset({ + organizationInfo: { + libelle: "Tau Empire", + nomComplet: "Tau Empire", + } as any, + siret: "👽️", + }); + expect(organization.created_at).to.deep.equal(organization.updated_at); + }); + + it("should update the Necron organization", async () => { + await pg.sql`insert into organizations + (siret, created_at, updated_at) + VALUES + ('⚰️', '1967-12-19', '1967-12-19'); + `; + const organization = await upset({ + organizationInfo: { + libelle: "Necron", + nomComplet: "Necrontyr", + } as any, + siret: "⚰️", + }); + expect(organization.created_at).to.not.deep.equal(organization.updated_at); + expect(organization.cached_libelle).to.equal("Necron"); + }); +}); diff --git a/packages/identite/src/services/hash-to-postgres-params.ts b/packages/identite/src/services/hash-to-postgres-params.ts new file mode 100644 index 000000000..d3f6d570b --- /dev/null +++ b/packages/identite/src/services/hash-to-postgres-params.ts @@ -0,0 +1,34 @@ +// + +import { chain } from "lodash-es"; + +// + +export function hashToPostgresParams(fieldsToUpdate: Partial): { + // postgres column-list syntax + paramsString: string; + // postgres column-list syntax for prepared statement + valuesString: string; + values: any[]; +} { + const paramsString = "(" + Object.keys(fieldsToUpdate).join(", ") + ")"; + // 'email, encrypted_password' + + const valuesString = + "(" + + chain(fieldsToUpdate) + // { email: 'email@xy.z', encrypted_password: 'hash' } + .toPairs() + // [[ 'email', 'email@xy.z'], ['encrypted_password', 'hash' ]] + .map((_value, index) => `$${index + 1}`) + // [ '$1', '$2' ] + .join(", ") + // '$1, $2' + .value() + + ")"; + + const values = Object.values(fieldsToUpdate); + // [ 'email@xy.z', 'hash' ] + + return { paramsString, valuesString, values }; +} diff --git a/packages/identite/src/services/index.ts b/packages/identite/src/services/index.ts new file mode 100644 index 000000000..8d8320223 --- /dev/null +++ b/packages/identite/src/services/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./hash-to-postgres-params.js"; diff --git a/packages/identite/src/types/contexts.ts b/packages/identite/src/types/contexts.ts new file mode 100644 index 000000000..c55b4067d --- /dev/null +++ b/packages/identite/src/types/contexts.ts @@ -0,0 +1,9 @@ +// + +import Pg from "pg"; + +// + +export type DatabaseContext = { + pg: Pg.Pool; +}; diff --git a/packages/identite/src/types/index.ts b/packages/identite/src/types/index.ts new file mode 100644 index 000000000..1c97d28bb --- /dev/null +++ b/packages/identite/src/types/index.ts @@ -0,0 +1,6 @@ +// + +export * from "./contexts.js"; +export * from "./organization-info.js"; +export * from "./organization.js"; +export * from "./user.js"; diff --git a/src/types/organization-info.d.ts b/packages/identite/src/types/organization-info.ts similarity index 83% rename from src/types/organization-info.d.ts rename to packages/identite/src/types/organization-info.ts index 7745efae0..2bf6ac3f6 100644 --- a/src/types/organization-info.d.ts +++ b/packages/identite/src/types/organization-info.ts @@ -1,8 +1,10 @@ // source : https://www.sirene.fr/sirene/public/variable/trancheEffectifsEtablissement -import { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; -interface OrganizationInfo { +// + +export interface OrganizationInfo { siret: string; libelle: string; nomComplet: string; diff --git a/src/types/organization.d.ts b/packages/identite/src/types/organization.ts similarity index 87% rename from src/types/organization.d.ts rename to packages/identite/src/types/organization.ts index a966b5db8..ded1fce86 100644 --- a/src/types/organization.d.ts +++ b/packages/identite/src/types/organization.ts @@ -1,6 +1,10 @@ -import { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; +// -interface Organization { +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; + +// + +export interface Organization { id: number; siret: string; created_at: Date; diff --git a/src/types/user.d.ts b/packages/identite/src/types/user.ts similarity index 96% rename from src/types/user.d.ts rename to packages/identite/src/types/user.ts index f927ce2cd..683b91f19 100644 --- a/src/types/user.d.ts +++ b/packages/identite/src/types/user.ts @@ -1,27 +1,29 @@ -interface User { - id: number; - email: string; - encrypted_password: string | null; - reset_password_token: string | null; - reset_password_sent_at: Date | null; - sign_in_count: number; - last_sign_in_at: Date | null; +// + +export interface User { created_at: Date; - updated_at: Date; + current_challenge: string | null; + email_verified_at: Date | null; email_verified: boolean; - verify_email_token: string | null; - verify_email_sent_at: Date | null; - given_name: string | null; + email: string; + encrypted_password: string | null; + encrypted_totp_key: string | null; family_name: string | null; - phone_number: string | null; + force_2fa: boolean; + given_name: string | null; + id: number; job: string | null; - magic_link_token: string | null; + last_sign_in_at: Date | null; magic_link_sent_at: Date | null; - email_verified_at: Date | null; - current_challenge: string | null; - needs_inclusionconnect_welcome_page: boolean; + magic_link_token: string | null; needs_inclusionconnect_onboarding_help: boolean; - encrypted_totp_key: string | null; + needs_inclusionconnect_welcome_page: boolean; + phone_number: string | null; + reset_password_sent_at: Date | null; + reset_password_token: string | null; + sign_in_count: number; totp_key_verified_at: Date | null; - force_2fa: boolean; + updated_at: Date; + verify_email_sent_at: Date | null; + verify_email_token: string | null; } diff --git a/packages/identite/src/user/create.test.ts b/packages/identite/src/user/create.test.ts new file mode 100644 index 000000000..0396e2b26 --- /dev/null +++ b/packages/identite/src/user/create.test.ts @@ -0,0 +1,31 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { createUserFactory } from "./create.js"; + +// + +const pg = new PGlite(); +const createUser = createUserFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + log: noop, + migrationsTable: "pg-migrate", + }); +}); + +describe("CreateUser", () => { + it("should create the god-emperor of mankind", async () => { + const user = await createUser({ email: "god-emperor@mankind" }); + expect(user.email).to.equal("god-emperor@mankind"); + }); +}); diff --git a/packages/identite/src/user/create.ts b/packages/identite/src/user/create.ts new file mode 100644 index 000000000..d7a6ea6b3 --- /dev/null +++ b/packages/identite/src/user/create.ts @@ -0,0 +1,42 @@ +// + +import { hashToPostgresParams } from "#src/services"; +import type { DatabaseContext, User } from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function createUserFactory({ pg }: DatabaseContext) { + return async function createUser({ + email, + encrypted_password = null, + }: { + email: string; + encrypted_password?: string | null; + }) { + const userWithTimestamps = { + email, + email_verified: false, + verify_email_token: null, + verify_email_sent_at: null, + encrypted_password, + magic_link_token: null, + magic_link_sent_at: null, + reset_password_token: null, + reset_password_sent_at: null, + sign_in_count: 0, + last_sign_in_at: null, + created_at: new Date(), + updated_at: new Date(), + }; + + const { paramsString, valuesString, values } = + hashToPostgresParams(userWithTimestamps); + + const { rows }: QueryResult = await pg.query( + `INSERT INTO users ${paramsString} VALUES ${valuesString} RETURNING *;`, + values, + ); + return rows.shift()!; + }; +} diff --git a/packages/identite/src/user/find-by-email.test.ts b/packages/identite/src/user/find-by-email.test.ts new file mode 100644 index 000000000..03872c226 --- /dev/null +++ b/packages/identite/src/user/find-by-email.test.ts @@ -0,0 +1,45 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { findByEmailFactory } from "./find-by-email.js"; + +// + +const pg = new PGlite(); +const findByEmail = findByEmailFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + migrationsTable: "pg-migrate", + log: noop, + }); +}); + +describe("FindByEmail", () => { + it("should find a user by email", async () => { + await pg.sql`insert into users + (id, email, created_at, updated_at, given_name, family_name, phone_number, job) + values + (1, 'lion.eljonson@darkangels.world', '4444-04-04', '4444-04-04', 'lion', 'el''jonson', 'i', 'primarque'), + (2, 'perturabo@ironwarriors.world', '4444-04-04', '4444-04-04', 'lion', 'el''jonson', 'iv', 'primarque'); + `; + + const user = await findByEmail("lion.eljonson@darkangels.world"); + + expect(user?.email).to.equal("lion.eljonson@darkangels.world"); + }); + + it("❎ fail to find the God-Emperor of Mankind", async () => { + const user = await findByEmail("the God-Emperor of Mankind"); + + expect(user).to.be.undefined; + }); +}); diff --git a/packages/identite/src/user/find-by-email.ts b/packages/identite/src/user/find-by-email.ts new file mode 100644 index 000000000..75ba3e85a --- /dev/null +++ b/packages/identite/src/user/find-by-email.ts @@ -0,0 +1,20 @@ +// + +import type { DatabaseContext, User } from "#src/types"; +import { type QueryResult } from "pg"; + +// + +export function findByEmailFactory({ pg }: DatabaseContext) { + return async function findByEmail(email: string) { + const { rows }: QueryResult = await pg.query( + ` + SELECT * + FROM users WHERE email = $1 + `, + [email], + ); + + return rows.shift(); + }; +} diff --git a/packages/identite/src/user/index.ts b/packages/identite/src/user/index.ts new file mode 100644 index 000000000..466f4bcea --- /dev/null +++ b/packages/identite/src/user/index.ts @@ -0,0 +1,5 @@ +// + +export * from "./create.js"; +export * from "./find-by-email.js"; +export * from "./update.js"; diff --git a/packages/identite/src/user/update.test.ts b/packages/identite/src/user/update.test.ts new file mode 100644 index 000000000..5bba1028a --- /dev/null +++ b/packages/identite/src/user/update.test.ts @@ -0,0 +1,36 @@ +// + +import { PGlite } from "@electric-sql/pglite"; +import { expect } from "chai"; +import { noop } from "lodash-es"; +import { before, describe, it } from "mocha"; +import { runner } from "node-pg-migrate"; +import { join } from "path"; +import { updateUserFactory } from "./update.js"; + +// + +const pg = new PGlite(); +const updateUser = updateUserFactory({ pg: pg as any }); + +before(async function migrate() { + await runner({ + dbClient: pg as any, + dir: join(import.meta.dirname, "../../../../migrations"), + direction: "up", + log: noop, + migrationsTable: "pg-migrate", + }); +}); + +describe("UpdateUser", () => { + it("should update the user job", async () => { + await pg.sql`INSERT INTO users + (id, email, created_at, updated_at, given_name, family_name, phone_number, job) + VALUES + (1, 'lion.eljonson@darkangels.world', '4444-04-04', '4444-04-04', 'Lion', 'El''Jonson', 'I', 'Primarque'); + `; + const user = await updateUser(1, { job: "Chevalier de l'Ordre" }); + expect(user.job).to.equal("Chevalier de l'Ordre"); + }); +}); diff --git a/packages/identite/src/user/update.ts b/packages/identite/src/user/update.ts new file mode 100644 index 000000000..e1299fbe5 --- /dev/null +++ b/packages/identite/src/user/update.ts @@ -0,0 +1,29 @@ +// + +import { hashToPostgresParams } from "#src/services"; +import type { DatabaseContext, User } from "#src/types"; +import type { QueryResult } from "pg"; + +// + +export function updateUserFactory({ pg }: DatabaseContext) { + return async function updateUser(id: number, fieldsToUpdate: Partial) { + const fieldsToUpdateWithTimestamps = { + ...fieldsToUpdate, + updated_at: new Date(), + }; + + const { paramsString, valuesString, values } = hashToPostgresParams( + fieldsToUpdateWithTimestamps, + ); + + const { rows }: QueryResult = await pg.query( + `UPDATE users SET ${paramsString} = ${valuesString} WHERE id = $${ + values.length + 1 + } RETURNING *`, + [...values, id], + ); + + return rows.shift()!; + }; +} diff --git a/packages/identite/tsconfig.json b/packages/identite/tsconfig.json new file mode 100644 index 000000000..c66deae2b --- /dev/null +++ b/packages/identite/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "nodenext", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "src", + "types": ["node"], + "verbatimModuleSyntax": true, + "paths": { + "#src/*": ["./src/*"] + } + }, + "extends": "@tsconfig/node22/tsconfig.json", + "references": [ + { "path": "../core/tsconfig.lib.json" }, + { "path": "../insee/tsconfig.lib.json" } + ], + "include": ["./src/**/*", "./src/**/*.json"] +} diff --git a/packages/identite/tsconfig.lib.json b/packages/identite/tsconfig.lib.json new file mode 100644 index 000000000..3ba354418 --- /dev/null +++ b/packages/identite/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["src/**/*.test.ts"], + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/packages/insee/package.json b/packages/insee/package.json index 1856b8930..c9e4a9ff2 100644 --- a/packages/insee/package.json +++ b/packages/insee/package.json @@ -32,7 +32,7 @@ } }, "scripts": { - "build": "tsc --project tsconfig.lib.json", + "build": "tsc --build tsconfig.lib.json", "check": "npm run build -- --noEmit", "dev": "npm run build -- --watch --preserveWatchOutput", "test": "mocha" diff --git a/packages/insee/src/data/codes-effectifs.ts b/packages/insee/src/data/codes-effectifs.ts index a46976090..f860a11a9 100644 --- a/packages/insee/src/data/codes-effectifs.ts +++ b/packages/insee/src/data/codes-effectifs.ts @@ -1,8 +1,4 @@ -// - -import type { TrancheEffectifs } from "#src/types/tranche-effectifs.js"; - -// +import type { TrancheEffectifs } from "@gouvfr-lasuite/proconnect.insee/types"; export const codesEffectifs: { [K in NonNullable]: string } = { diff --git a/scripts/import-accounts-coop.ts b/scripts/import-accounts-coop.ts index ab671286a..47dcf631d 100644 --- a/scripts/import-accounts-coop.ts +++ b/scripts/import-accounts-coop.ts @@ -5,18 +5,23 @@ import { isPhoneNumberValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; +import { + createUserFactory, + findByEmailFactory, + updateUserFactory, +} from "@gouvfr-lasuite/proconnect.identite/user"; import { AxiosError } from "axios"; import { parse, stringify, transform } from "csv"; import fs from "fs"; import { isEmpty, isString, some, toInteger } from "lodash-es"; import { z } from "zod"; import { getOrganizationInfo } from "../src/connectors/api-sirene"; +import { getDatabaseConnection } from "../src/connectors/postgres"; import { findByUserId } from "../src/repositories/organization/getters"; import { linkUserToOrganization, upsert, } from "../src/repositories/organization/setters"; -import { create, findByEmail, update } from "../src/repositories/user"; import { logger } from "../src/services/log"; import { getNumberOfLineInFile, @@ -26,6 +31,15 @@ import { throttleApiCall, } from "../src/services/script-helpers"; +// + +const pg = getDatabaseConnection(); +const findByEmail = findByEmailFactory({ pg }); +const create = createUserFactory({ pg }); +const update = updateUserFactory({ pg }); + +// + const { INPUT_FILE, OUTPUT_FILE } = z .object({ INPUT_FILE: z.string().default("./input.csv"), @@ -102,10 +116,10 @@ const maxInseeCallRateInMs = rateInMsFromArgs !== 0 ? rateInMsFromArgs : 125; const start = startDurationMesure(); try { const { + coordinateur, prenom: given_name, nom: family_name, téléphone: phone_number, - coordinateur, "email professionnel secondaire": professional_email, "SIRET structure": siret, } = data; diff --git a/scripts/import-domains.ts b/scripts/import-domains.ts index 9094eca03..a67973005 100644 --- a/scripts/import-domains.ts +++ b/scripts/import-domains.ts @@ -3,6 +3,7 @@ import { isDomainValid, isSiretValid, } from "@gouvfr-lasuite/proconnect.core/security"; +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { AxiosError } from "axios"; import { parse, stringify, transform } from "csv"; import fs from "fs"; @@ -24,7 +25,6 @@ import { startDurationMesure, throttleApiCall, } from "../src/services/script-helpers"; -import type { Organization } from "../src/types/organization"; const { INPUT_FILE, OUTPUT_FILE } = z .object({ diff --git a/src/connectors/api-sirene.ts b/src/connectors/api-sirene.ts new file mode 100644 index 000000000..17076d0f8 --- /dev/null +++ b/src/connectors/api-sirene.ts @@ -0,0 +1,44 @@ +// + +import { getOrganizationInfoFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; +import { + findBySirenFactory, + findBySiretFactory, + getInseeAccessTokenFactory, +} from "@gouvfr-lasuite/proconnect.insee/api"; +import { + HTTP_CLIENT_TIMEOUT, + INSEE_CONSUMER_KEY, + INSEE_CONSUMER_SECRET, +} from "../config/env"; + +// + +export const getInseeAccessToken = getInseeAccessTokenFactory( + { + consumerKey: INSEE_CONSUMER_KEY, + consumerSecret: INSEE_CONSUMER_SECRET, + }, + { + timeout: HTTP_CLIENT_TIMEOUT, + }, +); + +export const findBySiret = findBySiretFactory({ + getInseeAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +export const findBySiren = findBySirenFactory({ + getInseeAccessToken, + config: { + timeout: HTTP_CLIENT_TIMEOUT, + }, +}); + +export const getOrganizationInfo = getOrganizationInfoFactory({ + findBySiren, + findBySiret, +}); diff --git a/src/connectors/api-sirene/index.ts b/src/connectors/api-sirene/index.ts deleted file mode 100644 index a24fe8973..000000000 --- a/src/connectors/api-sirene/index.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - findBySirenFactory, - findBySiretFactory, - getInseeAccessTokenFactory, -} from "@gouvfr-lasuite/proconnect.insee/api"; -import { - formatAdresseEtablissement, - formatEnseigne, - formatNomComplet, - libelleFromCategoriesJuridiques, - libelleFromCodeEffectif, - libelleFromCodeNaf, -} from "@gouvfr-lasuite/proconnect.insee/formatters"; -import type { InseeEtablissement } from "@gouvfr-lasuite/proconnect.insee/types"; -import { AxiosError } 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 type { OrganizationInfo } from "../../types/organization-info"; - -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; -}; - -export const getInseeAccessToken = getInseeAccessTokenFactory( - { - consumerKey: INSEE_CONSUMER_KEY, - consumerSecret: INSEE_CONSUMER_SECRET, - }, - { - timeout: HTTP_CLIENT_TIMEOUT, - }, -); - -const findBySiret = findBySiretFactory({ - getInseeAccessToken, - config: { - timeout: HTTP_CLIENT_TIMEOUT, - }, -}); - -const findBySiren = findBySirenFactory({ - getInseeAccessToken, - config: { - timeout: HTTP_CLIENT_TIMEOUT, - }, -}); - -export const getOrganizationInfo = async ( - siretOrSiren: string, -): Promise => { - try { - let etablissement: InseeEtablissement; - - if (siretOrSiren.match(/^\d{14}$/)) { - etablissement = await findBySiret(siretOrSiren); - } else if (siretOrSiren.match(/^\d{9}$/)) { - etablissement = await findBySiren(siretOrSiren); - } 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: String(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/src/managers/moderation.ts b/src/managers/moderation.ts index ff8984282..239add3e6 100644 --- a/src/managers/moderation.ts +++ b/src/managers/moderation.ts @@ -1,4 +1,5 @@ import { ModerationProcessed } from "@gouvfr-lasuite/proconnect.email"; +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty } from "lodash-es"; import { HOST } from "../config/env"; import { ForbiddenError, NotFoundError } from "../config/errors"; diff --git a/src/managers/organization/join.ts b/src/managers/organization/join.ts index 9d2857c3b..2c76ffa54 100644 --- a/src/managers/organization/join.ts +++ b/src/managers/organization/join.ts @@ -1,5 +1,9 @@ import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; import { Welcome } from "@gouvfr-lasuite/proconnect.email"; +import type { + Organization, + OrganizationInfo, +} from "@gouvfr-lasuite/proconnect.identite/types"; import * as Sentry from "@sentry/node"; import { isEmpty, some } from "lodash-es"; import { @@ -54,8 +58,6 @@ import { isEtablissementScolaireDuPremierEtSecondDegre, isSmallAssociation, } from "../../services/organization"; -import type { Organization } from "../../types/organization"; -import type { OrganizationInfo } from "../../types/organization-info"; import { unableToAutoJoinOrganizationMd } from "../../views/mails/unable-to-auto-join-organization"; import { getOrganizationsByUserId, markDomainAsVerified } from "./main"; diff --git a/src/managers/organization/main.ts b/src/managers/organization/main.ts index 4082d0532..6a676aa70 100644 --- a/src/managers/organization/main.ts +++ b/src/managers/organization/main.ts @@ -1,3 +1,4 @@ +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty, some } from "lodash-es"; import { NotFoundError } from "../../config/errors"; import { @@ -16,7 +17,6 @@ import { } from "../../repositories/organization/setters"; import { setSelectedOrganizationId } from "../../repositories/redis/selected-organization"; import { getEmailDomain } from "../../services/email"; -import type { Organization } from "../../types/organization"; export const getOrganizationsByUserId = findByUserId; export const getOrganizationById = findOrganizationById; diff --git a/src/managers/session/authenticated.ts b/src/managers/session/authenticated.ts index 13062eb07..fca525e2b 100644 --- a/src/managers/session/authenticated.ts +++ b/src/managers/session/authenticated.ts @@ -1,3 +1,4 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import * as Sentry from "@sentry/node"; import type { Request, Response } from "express"; import { Session, type SessionData } from "express-session"; diff --git a/src/managers/user.ts b/src/managers/user.ts index e79be9d17..4d52ba5a9 100644 --- a/src/managers/user.ts +++ b/src/managers/user.ts @@ -19,6 +19,7 @@ import { UpdateTotpApplication, VerifyEmail, } from "@gouvfr-lasuite/proconnect.email"; +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import { isEmpty } from "lodash-es"; import { HOST, diff --git a/src/repositories/organization/getters.ts b/src/repositories/organization/getters.ts index 1107cf957..7d66fa2eb 100644 --- a/src/repositories/organization/getters.ts +++ b/src/repositories/organization/getters.ts @@ -1,6 +1,9 @@ +import type { + Organization, + User, +} from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; -import type { Organization } from "../../types/organization"; export const findById = async (id: number) => { const connection = getDatabaseConnection(); diff --git a/src/repositories/organization/setters.ts b/src/repositories/organization/setters.ts index 3a0adfe2f..3429b39d7 100644 --- a/src/repositories/organization/setters.ts +++ b/src/repositories/organization/setters.ts @@ -1,138 +1,10 @@ +import { upsertFactory } from "@gouvfr-lasuite/proconnect.identite/organization"; +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../../connectors/postgres"; import { hashToPostgresParams } from "../../services/hash-to-postgres-params"; -import type { Organization } from "../../types/organization"; -import type { OrganizationInfo } from "../../types/organization-info"; -export const upsert = async ({ - siret, - organizationInfo: { - libelle: cached_libelle, - nomComplet: cached_nom_complet, - enseigne: cached_enseigne, - trancheEffectifs: cached_tranche_effectifs, - trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale, - libelleTrancheEffectif: cached_libelle_tranche_effectif, - etatAdministratif: cached_etat_administratif, - estActive: cached_est_active, - statutDiffusion: cached_statut_diffusion, - estDiffusible: cached_est_diffusible, - adresse: cached_adresse, - codePostal: cached_code_postal, - codeOfficielGeographique: cached_code_officiel_geographique, - activitePrincipale: cached_activite_principale, - libelleActivitePrincipale: cached_libelle_activite_principale, - categorieJuridique: cached_categorie_juridique, - libelleCategorieJuridique: cached_libelle_categorie_juridique, - }, -}: { - siret: string; - organizationInfo: OrganizationInfo; -}) => { - const connection = getDatabaseConnection(); - - const { rows }: QueryResult = await connection.query( - ` -INSERT INTO organizations - ( - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - organization_info_fetched_at, - updated_at, - created_at - ) -VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) -ON CONFLICT (siret) -DO UPDATE - SET ( - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - organization_info_fetched_at, - updated_at - ) = ( - EXCLUDED.siret, - EXCLUDED.cached_libelle, - EXCLUDED.cached_nom_complet, - EXCLUDED.cached_enseigne, - EXCLUDED.cached_tranche_effectifs, - EXCLUDED.cached_tranche_effectifs_unite_legale, - EXCLUDED.cached_libelle_tranche_effectif, - EXCLUDED.cached_etat_administratif, - EXCLUDED.cached_est_active, - EXCLUDED.cached_statut_diffusion, - EXCLUDED.cached_est_diffusible, - EXCLUDED.cached_adresse, - EXCLUDED.cached_code_postal, - EXCLUDED.cached_code_officiel_geographique, - EXCLUDED.cached_activite_principale, - EXCLUDED.cached_libelle_activite_principale, - EXCLUDED.cached_categorie_juridique, - EXCLUDED.cached_libelle_categorie_juridique, - EXCLUDED.organization_info_fetched_at, - EXCLUDED.updated_at - ) -RETURNING * -`, - [ - siret, - cached_libelle, - cached_nom_complet, - cached_enseigne, - cached_tranche_effectifs, - cached_tranche_effectifs_unite_legale, - cached_libelle_tranche_effectif, - cached_etat_administratif, - cached_est_active, - cached_statut_diffusion, - cached_est_diffusible, - cached_adresse, - cached_code_postal, - cached_code_officiel_geographique, - cached_activite_principale, - cached_libelle_activite_principale, - cached_categorie_juridique, - cached_libelle_categorie_juridique, - new Date(), - new Date(), - new Date(), - ], - ); - - return rows.shift()!; -}; +export const upsert = upsertFactory({ pg: getDatabaseConnection() }); export const linkUserToOrganization = async ({ organization_id, diff --git a/src/repositories/user.ts b/src/repositories/user.ts index 55254512a..75783d273 100644 --- a/src/repositories/user.ts +++ b/src/repositories/user.ts @@ -1,7 +1,11 @@ +import type { User } from "@gouvfr-lasuite/proconnect.identite/types"; +import { + createUserFactory, + findByEmailFactory, + updateUserFactory, +} from "@gouvfr-lasuite/proconnect.identite/user"; import type { QueryResult } from "pg"; import { getDatabaseConnection } from "../connectors/postgres"; -import { hashToPostgresParams } from "../services/hash-to-postgres-params"; - export const findById = async (id: number) => { const connection = getDatabaseConnection(); @@ -16,19 +20,7 @@ FROM users WHERE id = $1 return rows.shift(); }; -export const findByEmail = async (email: string) => { - const connection = getDatabaseConnection(); - - const { rows }: QueryResult = await connection.query( - ` -SELECT * -FROM users WHERE email = $1 -`, - [email], - ); - - return rows.shift(); -}; +export const findByEmail = findByEmailFactory({ pg: getDatabaseConnection() }); export const findByMagicLinkToken = async (magic_link_token: string) => { const connection = getDatabaseConnection(); @@ -60,63 +52,9 @@ FROM users WHERE reset_password_token = $1 return rows.shift(); }; -export const update = async (id: number, fieldsToUpdate: Partial) => { - const connection = getDatabaseConnection(); - - const fieldsToUpdateWithTimestamps = { - ...fieldsToUpdate, - updated_at: new Date(), - }; - - const { paramsString, valuesString, values } = hashToPostgresParams( - fieldsToUpdateWithTimestamps, - ); - - const { rows }: QueryResult = await connection.query( - `UPDATE users SET ${paramsString} = ${valuesString} WHERE id = $${ - values.length + 1 - } RETURNING *`, - [...values, id], - ); - - return rows.shift()!; -}; - -export const create = async ({ - email, - encrypted_password = null, -}: { - email: string; - encrypted_password?: string | null; -}) => { - const connection = getDatabaseConnection(); - - const userWithTimestamps = { - email, - email_verified: false, - verify_email_token: null, - verify_email_sent_at: null, - encrypted_password, - magic_link_token: null, - magic_link_sent_at: null, - reset_password_token: null, - reset_password_sent_at: null, - sign_in_count: 0, - last_sign_in_at: null, - created_at: new Date(), - updated_at: new Date(), - }; +export const update = updateUserFactory({ pg: getDatabaseConnection() }); - const { paramsString, valuesString, values } = - hashToPostgresParams(userWithTimestamps); - - const { rows }: QueryResult = await connection.query( - `INSERT INTO users ${paramsString} VALUES ${valuesString} RETURNING *;`, - values, - ); - - return rows.shift()!; -}; +export const create = createUserFactory({ pg: getDatabaseConnection() }); export const deleteUser = async (id: number) => { const connection = getDatabaseConnection(); diff --git a/src/services/organization.ts b/src/services/organization.ts index db2aaccd1..2eeea3f25 100644 --- a/src/services/organization.ts +++ b/src/services/organization.ts @@ -1,5 +1,5 @@ import { isDomainValid } from "@gouvfr-lasuite/proconnect.core/security"; -import type { Organization } from "../types/organization"; +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; /** * These fonctions return approximate results. As the data tranche effectifs is diff --git a/src/services/script-helpers.ts b/src/services/script-helpers.ts index 9a954cc4c..994bd0f85 100644 --- a/src/services/script-helpers.ts +++ b/src/services/script-helpers.ts @@ -1,7 +1,7 @@ // from https://ipirozhenko.com/blog/measuring-requests-duration-nodejs-express/ +import type { OrganizationInfo } from "@gouvfr-lasuite/proconnect.identite/types"; import fs from "fs"; import { isEmpty } from "lodash-es"; -import type { OrganizationInfo } from "../types/organization-info"; export const startDurationMesure = () => { return process.hrtime(); diff --git a/test/organization.test.ts b/test/organization.test.ts index 596e54822..44d73f4b5 100644 --- a/test/organization.test.ts +++ b/test/organization.test.ts @@ -1,3 +1,4 @@ +import type { Organization } from "@gouvfr-lasuite/proconnect.identite/types"; import { assert } from "chai"; import { isCommune, @@ -8,7 +9,6 @@ import { isSmallAssociation, isWasteManagementOrganization, } from "../src/services/organization"; -import type { Organization } from "../src/types/organization"; const association_org_info = { siret: "83511518900010", diff --git a/tsconfig.json b/tsconfig.json index 8a16caacc..e5aae435e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,9 +21,9 @@ "exclude": ["packages/*", "build/*"], "references": [ { "path": "./packages/core/tsconfig.lib.json" }, - { - "path": "./packages/email/tsconfig.lib.json" - } + { "path": "./packages/email/tsconfig.lib.json" }, + { "path": "./packages/identite/tsconfig.lib.json" }, + { "path": "./packages/insee/tsconfig.lib.json" } ], "extends": "@tsconfig/node22/tsconfig.json" }