@proconnect-gouv/proconnect.identite 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +117 -0
  4. package/dist/data/certification/commune-code-conversion.d.ts +13 -0
  5. package/dist/data/certification/commune-code-conversion.d.ts.map +1 -0
  6. package/dist/data/certification/commune-code-conversion.js +5188 -0
  7. package/dist/data/certification/country-iso-to-cog.d.ts +12 -0
  8. package/dist/data/certification/country-iso-to-cog.d.ts.map +1 -0
  9. package/dist/data/certification/country-iso-to-cog.js +438 -0
  10. package/dist/data/certification/index.d.ts +3 -0
  11. package/dist/data/certification/index.d.ts.map +1 -0
  12. package/dist/data/certification/index.js +3 -0
  13. package/dist/managers/certification/adapters/api_entreprise.d.ts +4 -0
  14. package/dist/managers/certification/adapters/api_entreprise.d.ts.map +1 -0
  15. package/dist/managers/certification/adapters/api_entreprise.js +14 -0
  16. package/dist/managers/certification/adapters/franceconnect.d.ts +3 -0
  17. package/dist/managers/certification/adapters/franceconnect.d.ts.map +1 -0
  18. package/dist/managers/certification/adapters/franceconnect.js +15 -0
  19. package/dist/managers/certification/adapters/insee.d.ts +4 -0
  20. package/dist/managers/certification/adapters/insee.d.ts.map +1 -0
  21. package/dist/managers/certification/adapters/insee.js +29 -0
  22. package/dist/managers/certification/adapters/rne.d.ts +4 -0
  23. package/dist/managers/certification/adapters/rne.d.ts.map +1 -0
  24. package/dist/managers/certification/adapters/rne.js +45 -0
  25. package/dist/managers/certification/birthplace-conversion.d.ts +27 -0
  26. package/dist/managers/certification/birthplace-conversion.d.ts.map +1 -0
  27. package/dist/managers/certification/birthplace-conversion.js +45 -0
  28. package/dist/managers/certification/certification-score.d.ts +22 -0
  29. package/dist/managers/certification/certification-score.d.ts.map +1 -0
  30. package/dist/managers/certification/certification-score.js +72 -0
  31. package/dist/managers/certification/index.d.ts +1 -1
  32. package/dist/managers/certification/index.d.ts.map +1 -1
  33. package/dist/managers/certification/index.js +1 -1
  34. package/dist/managers/certification/is-organization-dirigeant.d.ts +38 -8
  35. package/dist/managers/certification/is-organization-dirigeant.d.ts.map +1 -1
  36. package/dist/managers/certification/is-organization-dirigeant.js +89 -52
  37. package/dist/managers/certification/normalize.d.ts +21 -0
  38. package/dist/managers/certification/normalize.d.ts.map +1 -0
  39. package/dist/managers/certification/normalize.js +64 -0
  40. package/dist/managers/franceconnect/openid-client.d.ts +1 -0
  41. package/dist/managers/franceconnect/openid-client.d.ts.map +1 -1
  42. package/dist/managers/organization/adapters/api_entreprise.d.ts +25 -0
  43. package/dist/managers/organization/adapters/api_entreprise.d.ts.map +1 -0
  44. package/dist/{mappers/organization/from-siret.js → managers/organization/adapters/api_entreprise.js} +26 -3
  45. package/dist/managers/organization/get-organization-info.d.ts +2 -2
  46. package/dist/managers/organization/get-organization-info.d.ts.map +1 -1
  47. package/dist/managers/organization/get-organization-info.js +6 -6
  48. package/dist/repositories/organization/upsert.d.ts +1 -1
  49. package/dist/repositories/organization/upsert.d.ts.map +1 -1
  50. package/dist/repositories/organization/upsert.js +26 -1
  51. package/dist/repositories/user/get-franceconnect-user-info.d.ts +1 -0
  52. package/dist/repositories/user/get-franceconnect-user-info.d.ts.map +1 -1
  53. package/dist/repositories/user/upsert-franceconnect-userinfo.d.ts +1 -0
  54. package/dist/repositories/user/upsert-franceconnect-userinfo.d.ts.map +1 -1
  55. package/dist/services/organization/index.d.ts +2 -0
  56. package/dist/services/organization/index.d.ts.map +1 -1
  57. package/dist/services/organization/index.js +2 -0
  58. package/dist/services/organization/is-public-service.d.ts +3 -0
  59. package/dist/services/organization/is-public-service.d.ts.map +1 -0
  60. package/dist/services/organization/is-public-service.js +19 -0
  61. package/dist/services/organization/is-syndicat-communal.d.ts +3 -0
  62. package/dist/services/organization/is-syndicat-communal.d.ts.map +1 -0
  63. package/dist/services/organization/is-syndicat-communal.js +13 -0
  64. package/dist/types/dirigeant.d.ts +5 -0
  65. package/dist/types/dirigeant.d.ts.map +1 -1
  66. package/dist/types/dirigeant.js +2 -0
  67. package/dist/types/franceconnect.d.ts +2 -0
  68. package/dist/types/franceconnect.d.ts.map +1 -1
  69. package/dist/types/franceconnect.js +1 -0
  70. package/dist/types/organization.d.ts +3 -3
  71. package/dist/types/organization.d.ts.map +1 -1
  72. package/package.json +9 -8
  73. package/src/data/certification/commune-code-conversion.ts +5189 -0
  74. package/src/data/certification/country-iso-to-cog.ts +439 -0
  75. package/src/data/certification/index.ts +4 -0
  76. package/src/managers/certification/adapters/api_entreprise.test.ts +68 -0
  77. package/src/managers/certification/adapters/api_entreprise.test.ts.snapshot +109 -0
  78. package/src/{mappers/certification/index.ts → managers/certification/adapters/api_entreprise.ts} +8 -5
  79. package/src/managers/certification/adapters/franceconnect.ts +21 -0
  80. package/src/managers/certification/adapters/insee.test.ts +18 -0
  81. package/src/managers/certification/adapters/insee.test.ts.snapshot +21 -0
  82. package/src/managers/certification/adapters/insee.ts +39 -0
  83. package/src/managers/certification/adapters/rne.test.ts +276 -0
  84. package/src/managers/certification/adapters/rne.ts +64 -0
  85. package/src/managers/certification/birthplace-conversion.test.ts +76 -0
  86. package/src/managers/certification/birthplace-conversion.ts +58 -0
  87. package/src/managers/certification/certification-score.test.ts +309 -0
  88. package/src/managers/certification/certification-score.ts +97 -0
  89. package/src/managers/certification/index.ts +1 -1
  90. package/src/managers/certification/is-organization-dirigeant.test.ts +144 -53
  91. package/src/managers/certification/is-organization-dirigeant.ts +132 -106
  92. package/src/managers/certification/normalize.test.ts +71 -0
  93. package/src/managers/certification/normalize.ts +72 -0
  94. package/src/managers/organization/adapters/api_entreprise.test.ts +31 -0
  95. package/src/{mappers/organization/from-siret.test.ts.snapshot → managers/organization/adapters/api_entreprise.test.ts.snapshot} +26 -3
  96. package/src/{mappers/organization/from-siret.ts → managers/organization/adapters/api_entreprise.ts} +55 -5
  97. package/src/managers/organization/get-organization-info.test.ts +2 -2
  98. package/src/managers/organization/get-organization-info.ts +10 -10
  99. package/src/repositories/organization/get-users-by-organization.test.ts.snapshot +1 -1
  100. package/src/repositories/organization/upsert.ts +69 -19
  101. package/src/repositories/user/find-by-email.test.ts +1 -1
  102. package/src/repositories/user/find-by-id.test.ts +1 -1
  103. package/src/repositories/user/get-by-id.test.ts +1 -1
  104. package/src/repositories/user/get-franceconnect-user-info.test.ts +1 -0
  105. package/src/repositories/user/upsert-franceconnect-userinfo.test.ts +2 -0
  106. package/src/services/organization/index.ts +2 -0
  107. package/src/services/organization/is-public-service.test.ts +99 -0
  108. package/src/services/organization/is-public-service.ts +35 -0
  109. package/src/services/organization/is-syndicat-communal.test.ts +31 -0
  110. package/src/services/organization/is-syndicat-communal.ts +18 -0
  111. package/src/types/dirigeant.ts +3 -0
  112. package/src/types/franceconnect.ts +1 -0
  113. package/src/types/organization-info.ts +1 -1
  114. package/src/types/organization.ts +3 -3
  115. package/testing/seed/franceconnect/index.ts +12 -9
  116. package/testing/seed/organizations/index.ts +108 -0
  117. package/tsconfig.json +6 -2
  118. package/tsconfig.lib.tsbuildinfo +1 -1
  119. package/dist/managers/certification/distance.d.ts +0 -4
  120. package/dist/managers/certification/distance.d.ts.map +0 -1
  121. package/dist/managers/certification/distance.js +0 -16
  122. package/dist/mappers/certification/index.d.ts +0 -4
  123. package/dist/mappers/certification/index.d.ts.map +0 -1
  124. package/dist/mappers/certification/index.js +0 -11
  125. package/dist/mappers/index.d.ts +0 -2
  126. package/dist/mappers/index.d.ts.map +0 -1
  127. package/dist/mappers/index.js +0 -2
  128. package/dist/mappers/organization/from-siret.d.ts +0 -5
  129. package/dist/mappers/organization/from-siret.d.ts.map +0 -1
  130. package/dist/mappers/organization/index.d.ts +0 -2
  131. package/dist/mappers/organization/index.d.ts.map +0 -1
  132. package/dist/mappers/organization/index.js +0 -2
  133. package/src/managers/certification/distance.test.ts +0 -109
  134. package/src/managers/certification/distance.ts +0 -41
  135. package/src/mappers/index.ts +0 -3
  136. package/src/mappers/organization/from-siret.test.ts +0 -26
  137. package/src/mappers/organization/index.ts +0 -3
  138. package/testing/seed/insee/index.ts +0 -22
  139. package/testing/seed/mandataires/index.ts +0 -32
@@ -1,136 +1,162 @@
1
1
  //
2
2
 
3
- import { NotFoundError } from "#src/errors";
4
- import { fromInfogreffe } from "#src/mappers/certification";
5
- import { fromSiret } from "#src/mappers/organization";
3
+ import { InvalidCertificationError, NotFoundError } from "#src/errors";
6
4
  import type { GetFranceConnectUserInfoHandler } from "#src/repositories/user";
7
- import { isEntrepriseUnipersonnelle } from "#src/services/organization";
8
- import type { IdentityVector } from "#src/types";
9
- import type {
10
- EntrepriseApiInfogreffeRepository,
11
- EntrepriseApiInseeRepository,
12
- } from "@proconnect-gouv/proconnect.entreprise/api";
13
- import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.entreprise/types";
14
- import type { InseeApiRepository } from "@proconnect-gouv/proconnect.insee/api";
15
- import { formatBirthdate } from "@proconnect-gouv/proconnect.insee/formatters";
16
- import { distance } from "./distance.js";
5
+ import type { IdentityVector, Organization } from "#src/types";
6
+ import type { ApiEntrepriseInfogreffeRepository } from "@proconnect-gouv/proconnect.api_entreprise/api";
7
+ import type { FindUniteLegaleBySirenHandler } from "@proconnect-gouv/proconnect.insee/api";
8
+ import type { FindPouvoirsBySirenHandler } from "@proconnect-gouv/proconnect.registre_national_entreprises/api";
9
+ import { match } from "ts-pattern";
10
+ import z from "zod/v4";
11
+ import * as ApiEntreprise from "./adapters/api_entreprise.js";
12
+ import * as FranceConnect from "./adapters/franceconnect.js";
13
+ import * as INSEE from "./adapters/insee.js";
14
+ import * as RNE from "./adapters/rne.js";
15
+ import { certificationScore } from "./certification-score.js";
17
16
 
18
17
  //
19
18
 
20
19
  type IsOrganizationExecutiveFactoryFactoryConfig = {
21
- EntrepriseApiInfogreffeRepository: EntrepriseApiInfogreffeRepository;
22
- EntrepriseApiInseeRepository: Pick<
23
- EntrepriseApiInseeRepository,
24
- "findBySiret"
20
+ ApiEntrepriseInfogreffeRepository: Pick<
21
+ ApiEntrepriseInfogreffeRepository,
22
+ "findMandatairesSociauxBySiren"
25
23
  >;
26
24
  EQUALITY_THRESHOLD?: number;
27
- getFranceConnectUserInfo: GetFranceConnectUserInfoHandler;
28
- InseeApiRepository: Pick<InseeApiRepository, "findBySiret">;
29
- log?: typeof console.log;
25
+ FranceConnectApiRepository: {
26
+ getFranceConnectUserInfo: GetFranceConnectUserInfoHandler;
27
+ };
28
+ InseeApiRepository: { findBySiren: FindUniteLegaleBySirenHandler };
29
+ RegistreNationalEntreprisesApiRepository: {
30
+ findPouvoirsBySiren: FindPouvoirsBySirenHandler;
31
+ };
30
32
  };
31
33
 
32
34
  //
33
35
 
36
+ async function getMandatairesSociaux(
37
+ {
38
+ RegistreNationalEntreprisesApiRepository,
39
+ ApiEntrepriseInfogreffeRepository,
40
+ }: IsOrganizationExecutiveFactoryFactoryConfig,
41
+ siren: string,
42
+ ) {
43
+ try {
44
+ const pouvoirs =
45
+ await RegistreNationalEntreprisesApiRepository.findPouvoirsBySiren(siren);
46
+ const dirigeants = pouvoirs.map(RNE.toIdentityVector);
47
+
48
+ return {
49
+ dirigeants,
50
+ source: SourceDirigeant.enum["registre-national-entreprises.inpi.fr/api"],
51
+ };
52
+ } catch {
53
+ const mandataires =
54
+ await ApiEntrepriseInfogreffeRepository.findMandatairesSociauxBySiren(
55
+ siren,
56
+ );
57
+ const dirigeants = mandataires.map(ApiEntreprise.toIdentityVector);
58
+
59
+ return {
60
+ dirigeants,
61
+ source:
62
+ SourceDirigeant.enum[
63
+ "entreprise.api.gouv.fr/v3/infogreffe/rcs/unites_legales/{siren}/mandataires_sociaux"
64
+ ],
65
+ };
66
+ }
67
+ }
68
+
34
69
  export function isOrganizationDirigeantFactory(
35
70
  config: IsOrganizationExecutiveFactoryFactoryConfig,
36
71
  ) {
37
- const {
38
- EQUALITY_THRESHOLD = 0,
39
- EntrepriseApiInseeRepository,
40
- EntrepriseApiInfogreffeRepository,
41
- InseeApiRepository,
42
- getFranceConnectUserInfo,
43
- log = () => {},
44
- } = config;
72
+ const { InseeApiRepository, FranceConnectApiRepository } = config;
45
73
 
46
74
  return async function isOrganizationDirigeant(
47
- siret: string,
75
+ organization: Organization,
48
76
  user_id: number,
49
77
  ) {
50
- const establishment = await EntrepriseApiInseeRepository.findBySiret(siret);
51
- const franceconnectUserInfo = await getFranceConnectUserInfo(user_id);
78
+ const siren = organization.siret.substring(0, 9);
79
+ const franceconnectUserInfo =
80
+ await FranceConnectApiRepository.getFranceConnectUserInfo(user_id);
52
81
  if (!franceconnectUserInfo) {
53
82
  throw new NotFoundError("FranceConnect UserInfo not found");
54
83
  }
55
84
 
56
- const sourceDirigeants =
57
- await getSourceDirigeantsFromEstablishment(establishment);
58
-
59
- if (sourceDirigeants.length === 0) {
60
- throw new NotFoundError("No mandataires found");
61
- }
62
-
63
- const distances = sourceDirigeants.map((sourceDirigeant) =>
64
- Math.abs(distance(franceconnectUserInfo, sourceDirigeant)),
65
- );
66
-
67
- const closestSourceDirigeantDistance = Math.min(...distances);
68
- const closestSourceDirigeant =
69
- sourceDirigeants[distances.indexOf(closestSourceDirigeantDistance)];
70
-
71
- log(
72
- closestSourceDirigeant,
73
- " is the closest source dirigeant to ",
74
- franceconnectUserInfo,
75
- " with a distance of ",
76
- closestSourceDirigeantDistance,
77
- );
78
-
79
- return closestSourceDirigeantDistance === EQUALITY_THRESHOLD;
85
+ const identity = FranceConnect.toIdentityVector(franceconnectUserInfo);
86
+
87
+ const prefered_source =
88
+ organization.cached_libelle_categorie_juridique ===
89
+ "Entrepreneur individuel"
90
+ ? SourceDirigeant.enum["api.insee.fr/api-sirene/private"]
91
+ : SourceDirigeant.enum["registre-national-entreprises.inpi.fr/api"];
92
+
93
+ const { dirigeants, source } = await match(prefered_source)
94
+ .with("api.insee.fr/api-sirene/private", async () => ({
95
+ dirigeants: await InseeApiRepository.findBySiren(siren)
96
+ .then(INSEE.toIdentityVector)
97
+ .then((vector) => [vector]),
98
+ source: SourceDirigeant.enum["api.insee.fr/api-sirene/private"],
99
+ }))
100
+ .with("registre-national-entreprises.inpi.fr/api", () =>
101
+ getMandatairesSociaux(config, siren),
102
+ )
103
+ .exhaustive();
104
+
105
+ const result = match_identity_to_dirigeant(identity, dirigeants);
106
+
107
+ if (result.kind === "no_candidates")
108
+ throw new InvalidCertificationError("No candidates found");
109
+ return {
110
+ details: {
111
+ ...result.closest,
112
+ identity,
113
+ source,
114
+ },
115
+ cause: result.kind,
116
+ ok: result.kind === "exact_match",
117
+ };
80
118
  };
81
-
82
- async function getSourceDirigeantsFromEstablishment(
83
- establishment: InseeSireneEstablishmentSiretResponseData,
84
- ): Promise<IdentityVector[]> {
85
- const organization = fromSiret(establishment);
86
-
87
- if (
88
- isEntrepriseUnipersonnelle({
89
- cached_libelle_categorie_juridique:
90
- organization.libelleCategorieJuridique,
91
- cached_tranche_effectifs: organization.trancheEffectifs,
92
- })
93
- ) {
94
- return getSourceDirigeantsFromInsseApi(establishment.siret);
95
- }
96
- return getSourceDirigeantsFromEntrepriseApi(
97
- establishment.unite_legale.siren,
98
- );
99
- }
100
-
101
- async function getSourceDirigeantsFromInsseApi(siret: string) {
102
- const { uniteLegale } = await InseeApiRepository.findBySiret(siret);
103
- const birthdate = formatBirthdate(
104
- String(uniteLegale?.dateNaissanceUniteLegale),
105
- );
106
-
107
- return [
108
- {
109
- birthplace: uniteLegale?.codeCommuneNaissanceUniteLegale ?? null,
110
- birthdate: isNaN(birthdate.getTime()) ? null : birthdate,
111
- family_name: uniteLegale?.nomUniteLegale ?? null,
112
- given_name: [
113
- uniteLegale?.prenom1UniteLegale,
114
- uniteLegale?.prenom2UniteLegale,
115
- uniteLegale?.prenom3UniteLegale,
116
- uniteLegale?.prenom4UniteLegale,
117
- ]
118
- .filter(Boolean)
119
- .join(" "),
120
- } satisfies IdentityVector,
121
- ];
122
- }
123
-
124
- async function getSourceDirigeantsFromEntrepriseApi(siren: string) {
125
- const mandataires =
126
- await EntrepriseApiInfogreffeRepository.findMandatairesSociauxBySiren(
127
- siren,
128
- );
129
-
130
- return mandataires.map((mandataire) => fromInfogreffe(mandataire));
131
- }
132
119
  }
133
120
 
134
121
  export type IsOrganizationDirigeantHandler = ReturnType<
135
122
  typeof isOrganizationDirigeantFactory
136
123
  >;
124
+
125
+ const SourceDirigeant = z.enum([
126
+ "api.insee.fr/api-sirene/private",
127
+ "entreprise.api.gouv.fr/v3/infogreffe/rcs/unites_legales/{siren}/mandataires_sociaux",
128
+ "registre-national-entreprises.inpi.fr/api",
129
+ ]);
130
+
131
+ function match_identity_to_dirigeant(
132
+ identity: IdentityVector,
133
+ dirigeants: IdentityVector[],
134
+ ) {
135
+ if (dirigeants.length === 0) return { kind: "no_candidates" as const };
136
+
137
+ const [closest] = dirigeants
138
+ .map((dirigeant) => ({
139
+ dirigeant,
140
+ score: certificationScore(identity, dirigeant),
141
+ }))
142
+ .toSorted((a, b) => b.score - a.score); // Sort by score descending (higher is better)
143
+
144
+ // According to the specification, only score of 5 (perfect match) is certified
145
+ return match(closest.score)
146
+ .with(5, () => ({
147
+ kind: "exact_match" as const,
148
+ closest,
149
+ }))
150
+ .with(4, () => ({
151
+ kind: "close_match" as const,
152
+ closest,
153
+ }))
154
+ .with(3, () => ({
155
+ kind: "close_match" as const,
156
+ closest,
157
+ }))
158
+ .otherwise(() => ({
159
+ kind: "below_threshold" as const,
160
+ closest,
161
+ }));
162
+ }
@@ -0,0 +1,71 @@
1
+ //
2
+
3
+ import assert from "node:assert/strict";
4
+ import { describe, it } from "node:test";
5
+ import { extractFirstName, normalizeText } from "./normalize.js";
6
+
7
+ //
8
+
9
+ describe("normalizeText", () => {
10
+ it("converts to uppercase", () => {
11
+ assert.equal(normalizeText("bernard"), "BERNARD");
12
+ });
13
+
14
+ it("removes accents and cedillas", () => {
15
+ assert.equal(normalizeText("José"), "JOSE");
16
+ assert.equal(normalizeText("François"), "FRANCOIS");
17
+ assert.equal(normalizeText("Hélène"), "HELENE");
18
+ });
19
+
20
+ it("replaces special characters with spaces", () => {
21
+ assert.equal(normalizeText("Jean-Pierre"), "JEAN PIERRE");
22
+ assert.equal(normalizeText("O'Connor"), "O CONNOR");
23
+ assert.equal(normalizeText("Marie.Anne"), "MARIE ANNE");
24
+ });
25
+
26
+ it("removes multiple spaces", () => {
27
+ assert.equal(normalizeText("Jean Pierre"), "JEAN PIERRE");
28
+ });
29
+
30
+ it("trims leading and trailing spaces", () => {
31
+ assert.equal(normalizeText(" Jean "), "JEAN");
32
+ });
33
+
34
+ it("handles null and undefined", () => {
35
+ assert.equal(normalizeText(null), "");
36
+ assert.equal(normalizeText(undefined), "");
37
+ });
38
+
39
+ it("handles complex names", () => {
40
+ assert.equal(
41
+ normalizeText("Marie-Thérèse D'Aubigné"),
42
+ "MARIE THERESE D AUBIGNE",
43
+ );
44
+ });
45
+
46
+ it("handles ligatures", () => {
47
+ assert.equal(normalizeText("Œuvre"), "OEUVRE");
48
+ assert.equal(normalizeText("Cæsar"), "CAESAR");
49
+ });
50
+ });
51
+
52
+ describe("extractFirstName", () => {
53
+ it("extracts first name before space", () => {
54
+ assert.equal(extractFirstName("Jean Pierre"), "JEAN");
55
+ assert.equal(extractFirstName("Marie Thérèse Anne"), "MARIE");
56
+ });
57
+
58
+ it("normalizes before extracting", () => {
59
+ assert.equal(extractFirstName("Jean-Pierre"), "JEAN");
60
+ assert.equal(extractFirstName("Marie-Thérèse"), "MARIE");
61
+ });
62
+
63
+ it("handles single name", () => {
64
+ assert.equal(extractFirstName("Jean"), "JEAN");
65
+ });
66
+
67
+ it("handles null and undefined", () => {
68
+ assert.equal(extractFirstName(null), "");
69
+ assert.equal(extractFirstName(undefined), "");
70
+ });
71
+ });
@@ -0,0 +1,72 @@
1
+ //
2
+
3
+ /**
4
+ * Normalizes text for identity matching:
5
+ * - Convert to uppercase
6
+ * - Replace accented characters with their simple versions
7
+ * - Replace special characters (including ' and -) with spaces
8
+ * - Remove multiple consecutive spaces
9
+ * - Trim leading and trailing spaces
10
+ *
11
+ * @param text - The text to normalize
12
+ * @returns The normalized text
13
+ */
14
+ export function normalizeText(text: string | null | undefined): string {
15
+ if (!text) return "";
16
+
17
+ // Convert to uppercase
18
+ let normalized = text.toUpperCase();
19
+
20
+ // Replace accented characters with their simple versions
21
+ const accentMap: Record<string, string> = {
22
+ À: "A",
23
+ Â: "A",
24
+ Ä: "A",
25
+ Ç: "C",
26
+ É: "E",
27
+ È: "E",
28
+ Ê: "E",
29
+ Ë: "E",
30
+ Î: "I",
31
+ Ï: "I",
32
+ Ô: "O",
33
+ Ö: "O",
34
+ Ù: "U",
35
+ Û: "U",
36
+ Ü: "U",
37
+ Ÿ: "Y",
38
+ Æ: "AE",
39
+ Œ: "OE",
40
+ };
41
+
42
+ for (const [accented, simple] of Object.entries(accentMap)) {
43
+ normalized = normalized.replaceAll(accented, simple);
44
+ }
45
+
46
+ // Replace special characters (including ' and -) with spaces
47
+ // Keep only letters, digits, and spaces
48
+ normalized = normalized.replace(/[^A-Z0-9 ]/g, " ");
49
+
50
+ // Remove multiple consecutive spaces
51
+ normalized = normalized.replace(/\s+/g, " ");
52
+
53
+ // Trim leading and trailing spaces
54
+ normalized = normalized.trim();
55
+
56
+ return normalized;
57
+ }
58
+
59
+ /**
60
+ * Extracts the first name from a normalized full name string.
61
+ * The first name is the part before the first space.
62
+ *
63
+ * @param givenName - The full given name (may contain multiple names)
64
+ * @returns The first name only
65
+ */
66
+ export function extractFirstName(givenName: string | null | undefined): string {
67
+ if (!givenName) return "";
68
+
69
+ const normalized = normalizeText(givenName);
70
+ const parts = normalized.split(" ");
71
+ return parts[0] || "";
72
+ }
@@ -0,0 +1,31 @@
1
+ //
2
+
3
+ import {
4
+ AppleEuropeInc,
5
+ MaireClamart,
6
+ Papillon,
7
+ RogalDornEntrepreneur,
8
+ } from "@proconnect-gouv/proconnect.api_entreprise/testing/seed/v3-insee-sirene-etablissements-siret";
9
+ import { suite, test } from "node:test";
10
+ import { toOrganizationInfo } from "./api_entreprise.js";
11
+
12
+ suite("toOrganizationInfo", () => {
13
+ test("AppleEuropeInc", (t) => {
14
+ const organization = toOrganizationInfo(AppleEuropeInc);
15
+ t.assert.snapshot(organization);
16
+ });
17
+
18
+ test("Commune de clamart - Mairie", (t) => {
19
+ const organization = toOrganizationInfo(MaireClamart);
20
+ t.assert.snapshot(organization);
21
+ });
22
+
23
+ test("RogalDornEntrepreneur", (t) => {
24
+ const organization = toOrganizationInfo(RogalDornEntrepreneur);
25
+ t.assert.snapshot(organization);
26
+ });
27
+ test("Papillon", (t) => {
28
+ const organization = toOrganizationInfo(Papillon);
29
+ t.assert.snapshot(organization);
30
+ });
31
+ });
@@ -1,4 +1,4 @@
1
- exports[`fromSiret > AppleEuropeInc 1`] = `
1
+ exports[`toOrganizationInfo > AppleEuropeInc 1`] = `
2
2
  {
3
3
  "activitePrincipale": "70.22Z",
4
4
  "adresse": "100 west ten wilmington delawa, 99404, ETATS-UNIS",
@@ -21,7 +21,7 @@ exports[`fromSiret > AppleEuropeInc 1`] = `
21
21
  }
22
22
  `;
23
23
 
24
- exports[`fromSiret > Commune de clamart - Mairie 1`] = `
24
+ exports[`toOrganizationInfo > Commune de clamart - Mairie 1`] = `
25
25
  {
26
26
  "activitePrincipale": "84.11Z",
27
27
  "adresse": "1 place maurice gunsbourg, 92140 Clamart",
@@ -44,7 +44,30 @@ exports[`fromSiret > Commune de clamart - Mairie 1`] = `
44
44
  }
45
45
  `;
46
46
 
47
- exports[`fromSiret > RogalDornEntrepreneur 1`] = `
47
+ exports[`toOrganizationInfo > Papillon 1`] = `
48
+ {
49
+ "activitePrincipale": "47.59A",
50
+ "adresse": "5-7, 5 rue du moulin aux moines, 72650 La chapelle-saint-aubin",
51
+ "categorieJuridique": "5710",
52
+ "codeOfficielGeographique": "72065",
53
+ "codePostal": "72650",
54
+ "enseigne": "",
55
+ "estActive": true,
56
+ "estDiffusible": true,
57
+ "etatAdministratif": "A",
58
+ "libelle": "Papillon",
59
+ "libelleActivitePrincipale": "47.59A - Commerce de détail de meubles",
60
+ "libelleCategorieJuridique": "SAS, société par actions simplifiée",
61
+ "libelleTrancheEffectif": "3 à 5 salariés, en 2022",
62
+ "nomComplet": "Papillon",
63
+ "siret": "39234600300198",
64
+ "statutDiffusion": "diffusible",
65
+ "trancheEffectifs": "02",
66
+ "trancheEffectifsUniteLegale": "11"
67
+ }
68
+ `;
69
+
70
+ exports[`toOrganizationInfo > RogalDornEntrepreneur 1`] = `
48
71
  {
49
72
  "activitePrincipale": "62.02A",
50
73
  "adresse": "06155 Vallauris",
@@ -1,19 +1,23 @@
1
1
  //
2
2
 
3
3
  import "#src/types";
4
- import { OrganizationInfoSchema, type OrganizationInfo } from "#src/types";
4
+ import {
5
+ OrganizationInfoSchema,
6
+ type Organization,
7
+ type OrganizationInfo,
8
+ } from "#src/types";
5
9
  import {
6
10
  formatAddress,
7
11
  formatMainActivity,
8
12
  formatNomComplet,
9
13
  libelleFromCodeEffectif,
10
- } from "@proconnect-gouv/proconnect.entreprise/formatters";
11
- import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.entreprise/types";
14
+ } from "@proconnect-gouv/proconnect.api_entreprise/formatters";
15
+ import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.api_entreprise/types";
12
16
  import { capitalize } from "lodash-es";
13
17
 
14
18
  //
15
19
 
16
- export function fromSiret(
20
+ export function toOrganizationInfo(
17
21
  siretData: InseeSireneEstablishmentSiretResponseData,
18
22
  ): OrganizationInfo {
19
23
  const isPartiallyNonDiffusible =
@@ -71,5 +75,51 @@ export function fromSiret(
71
75
  statutDiffusion: siretData.status_diffusion,
72
76
  trancheEffectifs,
73
77
  trancheEffectifsUniteLegale,
74
- } satisfies OrganizationInfo);
78
+ });
79
+ }
80
+
81
+ export function toPartialOrganization(organization_info: OrganizationInfo) {
82
+ const {
83
+ activitePrincipale: cached_activite_principale,
84
+ adresse: cached_adresse,
85
+ categorieJuridique: cached_categorie_juridique,
86
+ codeOfficielGeographique: cached_code_officiel_geographique,
87
+ codePostal: cached_code_postal,
88
+ enseigne: cached_enseigne,
89
+ estActive: cached_est_active,
90
+ estDiffusible: cached_est_diffusible,
91
+ etatAdministratif: cached_etat_administratif,
92
+ libelle: cached_libelle,
93
+ libelleActivitePrincipale: cached_libelle_activite_principale,
94
+ libelleCategorieJuridique: cached_libelle_categorie_juridique,
95
+ libelleTrancheEffectif: cached_libelle_tranche_effectif,
96
+ nomComplet: cached_nom_complet,
97
+ siret,
98
+ statutDiffusion: cached_statut_diffusion,
99
+ trancheEffectifs: cached_tranche_effectifs,
100
+ trancheEffectifsUniteLegale: cached_tranche_effectifs_unite_legale,
101
+ } = organization_info;
102
+ return {
103
+ cached_activite_principale,
104
+ cached_adresse,
105
+ cached_categorie_juridique,
106
+ cached_code_officiel_geographique,
107
+ cached_code_postal,
108
+ cached_enseigne,
109
+ cached_est_active,
110
+ cached_est_diffusible,
111
+ cached_etat_administratif,
112
+ cached_libelle_activite_principale,
113
+ cached_libelle_categorie_juridique,
114
+ cached_libelle_tranche_effectif,
115
+ cached_libelle,
116
+ cached_nom_complet,
117
+ cached_statut_diffusion,
118
+ cached_tranche_effectifs_unite_legale,
119
+ cached_tranche_effectifs,
120
+ siret,
121
+ } satisfies Omit<
122
+ Organization,
123
+ "created_at" | "id" | "updated_at" | "organization_info_fetched_at"
124
+ >;
75
125
  }
@@ -2,8 +2,8 @@ import { NotFoundError } from "#src/errors";
2
2
  import {
3
3
  CommunautéDeCommunes,
4
4
  RogalDornEntrepreneur,
5
- } from "@proconnect-gouv/proconnect.entreprise/testing/seed/insee/siret";
6
- import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.entreprise/types";
5
+ } from "@proconnect-gouv/proconnect.api_entreprise/testing/seed/v3-insee-sirene-etablissements-siret";
6
+ import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.api_entreprise/types";
7
7
  import assert from "node:assert/strict";
8
8
  import { suite, test } from "node:test";
9
9
  import { getOrganizationInfoFactory } from "./get-organization-info.js";
@@ -1,18 +1,18 @@
1
1
  //
2
2
 
3
3
  import { InvalidSiretError, NotFoundError } from "#src/errors";
4
- import { OrganizationInfoMapper } from "#src/mappers";
5
4
  import { type OrganizationInfo } from "#src/types";
6
- import type { EntrepriseApiInseeRepository } from "@proconnect-gouv/proconnect.entreprise/api";
5
+ import type { ApiEntrepriseInseeRepository } from "@proconnect-gouv/proconnect.api_entreprise/api";
7
6
  import {
8
- EntrepriseApiConnectionError,
9
- EntrepriseApiInvalidSiret,
10
- } from "@proconnect-gouv/proconnect.entreprise/types";
7
+ ApiEntrepriseConnectionError,
8
+ ApiEntrepriseInvalidSiret,
9
+ } from "@proconnect-gouv/proconnect.api_entreprise/types";
10
+ import * as ApiEntreprise from "./adapters/api_entreprise.js";
11
11
 
12
12
  //
13
13
 
14
14
  export function getOrganizationInfoFactory(
15
- config: EntrepriseApiInseeRepository,
15
+ config: ApiEntrepriseInseeRepository,
16
16
  ) {
17
17
  const { findBySiren, findBySiret } = config;
18
18
 
@@ -23,11 +23,11 @@ export function getOrganizationInfoFactory(
23
23
  let establishment: OrganizationInfo;
24
24
 
25
25
  if (siretOrSiren.match(/^\d{14}$/)) {
26
- establishment = OrganizationInfoMapper.fromSiret(
26
+ establishment = ApiEntreprise.toOrganizationInfo(
27
27
  await findBySiret(siretOrSiren),
28
28
  );
29
29
  } else if (siretOrSiren.match(/^\d{9}$/)) {
30
- establishment = OrganizationInfoMapper.fromSiret(
30
+ establishment = ApiEntreprise.toOrganizationInfo(
31
31
  await findBySiren(siretOrSiren),
32
32
  );
33
33
  } else {
@@ -42,10 +42,10 @@ export function getOrganizationInfoFactory(
42
42
 
43
43
  return establishment;
44
44
  } catch (e) {
45
- if (EntrepriseApiInvalidSiret.isInvalidSiret(e))
45
+ if (ApiEntrepriseInvalidSiret.isInvalidSiret(e))
46
46
  throw new InvalidSiretError();
47
47
 
48
- throw new EntrepriseApiConnectionError(
48
+ throw new ApiEntrepriseConnectionError(
49
49
  "unknown error while fetching entreprise.api.gouv.fr",
50
50
  { cause: e },
51
51
  );
@@ -3,7 +3,7 @@ exports[`getUsersByOrganizationFactory > should find users by organization id 1`
3
3
  {
4
4
  "id": 1,
5
5
  "email": "lion.eljonson@darkangels.world",
6
- "encrypted_password": "",
6
+ "encrypted_password": null,
7
7
  "reset_password_token": null,
8
8
  "reset_password_sent_at": null,
9
9
  "sign_in_count": 0,