@proconnect-gouv/proconnect.identite 1.1.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 (119) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/data/certification/commune-code-conversion.d.ts +13 -0
  3. package/dist/data/certification/commune-code-conversion.d.ts.map +1 -0
  4. package/dist/data/certification/commune-code-conversion.js +5188 -0
  5. package/dist/data/certification/country-iso-to-cog.d.ts +12 -0
  6. package/dist/data/certification/country-iso-to-cog.d.ts.map +1 -0
  7. package/dist/data/certification/country-iso-to-cog.js +438 -0
  8. package/dist/data/certification/index.d.ts +3 -0
  9. package/dist/data/certification/index.d.ts.map +1 -0
  10. package/dist/data/certification/index.js +3 -0
  11. package/dist/{mappers/certification/index.d.ts → managers/certification/adapters/api_entreprise.d.ts} +2 -2
  12. package/dist/managers/certification/adapters/api_entreprise.d.ts.map +1 -0
  13. package/dist/managers/certification/adapters/api_entreprise.js +14 -0
  14. package/dist/managers/certification/adapters/franceconnect.d.ts +3 -0
  15. package/dist/managers/certification/adapters/franceconnect.d.ts.map +1 -0
  16. package/dist/managers/certification/adapters/franceconnect.js +15 -0
  17. package/dist/managers/certification/adapters/insee.d.ts +4 -0
  18. package/dist/managers/certification/adapters/insee.d.ts.map +1 -0
  19. package/dist/managers/certification/adapters/insee.js +29 -0
  20. package/dist/managers/certification/adapters/rne.d.ts +4 -0
  21. package/dist/managers/certification/adapters/rne.d.ts.map +1 -0
  22. package/dist/managers/certification/adapters/rne.js +45 -0
  23. package/dist/managers/certification/birthplace-conversion.d.ts +27 -0
  24. package/dist/managers/certification/birthplace-conversion.d.ts.map +1 -0
  25. package/dist/managers/certification/birthplace-conversion.js +45 -0
  26. package/dist/managers/certification/certification-score.d.ts +22 -0
  27. package/dist/managers/certification/certification-score.d.ts.map +1 -0
  28. package/dist/managers/certification/certification-score.js +72 -0
  29. package/dist/managers/certification/index.d.ts +1 -1
  30. package/dist/managers/certification/index.d.ts.map +1 -1
  31. package/dist/managers/certification/index.js +1 -1
  32. package/dist/managers/certification/is-organization-dirigeant.d.ts +38 -8
  33. package/dist/managers/certification/is-organization-dirigeant.d.ts.map +1 -1
  34. package/dist/managers/certification/is-organization-dirigeant.js +89 -52
  35. package/dist/managers/certification/normalize.d.ts +21 -0
  36. package/dist/managers/certification/normalize.d.ts.map +1 -0
  37. package/dist/managers/certification/normalize.js +64 -0
  38. package/dist/managers/franceconnect/openid-client.d.ts +1 -0
  39. package/dist/managers/franceconnect/openid-client.d.ts.map +1 -1
  40. package/dist/managers/organization/adapters/api_entreprise.d.ts +25 -0
  41. package/dist/managers/organization/adapters/api_entreprise.d.ts.map +1 -0
  42. package/dist/{mappers/organization/from-siret.js → managers/organization/adapters/api_entreprise.js} +25 -2
  43. package/dist/managers/organization/get-organization-info.d.ts.map +1 -1
  44. package/dist/managers/organization/get-organization-info.js +3 -3
  45. package/dist/repositories/organization/upsert.d.ts +1 -1
  46. package/dist/repositories/organization/upsert.d.ts.map +1 -1
  47. package/dist/repositories/organization/upsert.js +26 -1
  48. package/dist/repositories/user/get-franceconnect-user-info.d.ts +1 -0
  49. package/dist/repositories/user/get-franceconnect-user-info.d.ts.map +1 -1
  50. package/dist/repositories/user/upsert-franceconnect-userinfo.d.ts +1 -0
  51. package/dist/repositories/user/upsert-franceconnect-userinfo.d.ts.map +1 -1
  52. package/dist/services/organization/is-syndicat-communal.d.ts.map +1 -1
  53. package/dist/services/organization/is-syndicat-communal.js +2 -0
  54. package/dist/types/dirigeant.d.ts +5 -0
  55. package/dist/types/dirigeant.d.ts.map +1 -1
  56. package/dist/types/dirigeant.js +2 -0
  57. package/dist/types/franceconnect.d.ts +2 -0
  58. package/dist/types/franceconnect.d.ts.map +1 -1
  59. package/dist/types/franceconnect.js +1 -0
  60. package/dist/types/organization.d.ts +2 -2
  61. package/dist/types/organization.d.ts.map +1 -1
  62. package/package.json +6 -6
  63. package/src/data/certification/commune-code-conversion.ts +5189 -0
  64. package/src/data/certification/country-iso-to-cog.ts +439 -0
  65. package/src/data/certification/index.ts +4 -0
  66. package/src/managers/certification/adapters/api_entreprise.test.ts +68 -0
  67. package/src/managers/certification/adapters/api_entreprise.test.ts.snapshot +109 -0
  68. package/src/{mappers/certification/index.ts → managers/certification/adapters/api_entreprise.ts} +7 -4
  69. package/src/managers/certification/adapters/franceconnect.ts +21 -0
  70. package/src/managers/certification/adapters/insee.test.ts +18 -0
  71. package/src/managers/certification/adapters/insee.test.ts.snapshot +21 -0
  72. package/src/managers/certification/adapters/insee.ts +39 -0
  73. package/src/managers/certification/adapters/rne.test.ts +276 -0
  74. package/src/managers/certification/adapters/rne.ts +64 -0
  75. package/src/managers/certification/birthplace-conversion.test.ts +76 -0
  76. package/src/managers/certification/birthplace-conversion.ts +58 -0
  77. package/src/managers/certification/certification-score.test.ts +309 -0
  78. package/src/managers/certification/certification-score.ts +97 -0
  79. package/src/managers/certification/index.ts +1 -1
  80. package/src/managers/certification/is-organization-dirigeant.test.ts +139 -48
  81. package/src/managers/certification/is-organization-dirigeant.ts +132 -106
  82. package/src/managers/certification/normalize.test.ts +71 -0
  83. package/src/managers/certification/normalize.ts +72 -0
  84. package/src/managers/organization/adapters/api_entreprise.test.ts +31 -0
  85. package/src/{mappers/organization/from-siret.test.ts.snapshot → managers/organization/adapters/api_entreprise.test.ts.snapshot} +26 -3
  86. package/src/{mappers/organization/from-siret.ts → managers/organization/adapters/api_entreprise.ts} +53 -3
  87. package/src/managers/organization/get-organization-info.test.ts +1 -1
  88. package/src/managers/organization/get-organization-info.ts +3 -3
  89. package/src/repositories/organization/upsert.ts +69 -19
  90. package/src/repositories/user/get-franceconnect-user-info.test.ts +1 -0
  91. package/src/repositories/user/upsert-franceconnect-userinfo.test.ts +2 -0
  92. package/src/services/organization/is-syndicat-communal.ts +2 -0
  93. package/src/types/dirigeant.ts +3 -0
  94. package/src/types/franceconnect.ts +1 -0
  95. package/src/types/organization.ts +2 -2
  96. package/testing/seed/franceconnect/index.ts +12 -9
  97. package/testing/seed/organizations/index.ts +34 -0
  98. package/tsconfig.json +5 -2
  99. package/tsconfig.lib.tsbuildinfo +1 -1
  100. package/dist/managers/certification/distance.d.ts +0 -4
  101. package/dist/managers/certification/distance.d.ts.map +0 -1
  102. package/dist/managers/certification/distance.js +0 -16
  103. package/dist/mappers/certification/index.d.ts.map +0 -1
  104. package/dist/mappers/certification/index.js +0 -11
  105. package/dist/mappers/index.d.ts +0 -2
  106. package/dist/mappers/index.d.ts.map +0 -1
  107. package/dist/mappers/index.js +0 -2
  108. package/dist/mappers/organization/from-siret.d.ts +0 -5
  109. package/dist/mappers/organization/from-siret.d.ts.map +0 -1
  110. package/dist/mappers/organization/index.d.ts +0 -2
  111. package/dist/mappers/organization/index.d.ts.map +0 -1
  112. package/dist/mappers/organization/index.js +0 -2
  113. package/src/managers/certification/distance.test.ts +0 -109
  114. package/src/managers/certification/distance.ts +0 -41
  115. package/src/mappers/index.ts +0 -3
  116. package/src/mappers/organization/from-siret.test.ts +0 -26
  117. package/src/mappers/organization/index.ts +0 -3
  118. package/testing/seed/insee/index.ts +0 -22
  119. package/testing/seed/mandataires/index.ts +0 -32
@@ -0,0 +1,309 @@
1
+ //
2
+
3
+ import assert from "node:assert/strict";
4
+ import { describe, it } from "node:test";
5
+ import { certificationScore } from "./certification-score.js";
6
+
7
+ //
8
+
9
+ describe("certification scoring", () => {
10
+ it("perfect match - all 5 criteria", () => {
11
+ // All criteria match: family name, first name, gender, birthdate, birthplace
12
+ assert.equal(
13
+ certificationScore(
14
+ {
15
+ birthcountry: null,
16
+ birthdate: new Date("1946-08-17"),
17
+ birthplace: "75001",
18
+ family_name: "Bernard",
19
+ gender: "male",
20
+ given_name: "Stéphane",
21
+ },
22
+ {
23
+ birthcountry: null,
24
+ birthdate: new Date("1946-08-17"),
25
+ birthplace: "75001",
26
+ family_name: "Bernard",
27
+ gender: "male",
28
+ given_name: "Stéphane",
29
+ },
30
+ ),
31
+ 5,
32
+ );
33
+ });
34
+
35
+ it("family name mismatch", () => {
36
+ // Different family name = -1 point
37
+ assert.equal(
38
+ certificationScore(
39
+ {
40
+ birthcountry: null,
41
+ birthdate: new Date("1946-08-17"),
42
+ birthplace: "75001",
43
+ family_name: "DuMoulin",
44
+ gender: "male",
45
+ given_name: "Stéphane",
46
+ },
47
+ {
48
+ birthcountry: null,
49
+ birthdate: new Date("1946-08-17"),
50
+ birthplace: "75001",
51
+ family_name: "Bernard",
52
+ gender: "male",
53
+ given_name: "Stéphane",
54
+ },
55
+ ),
56
+ 4,
57
+ );
58
+ });
59
+
60
+ it("different birthdate", () => {
61
+ // Different birthdate = -1 point
62
+ assert.equal(
63
+ certificationScore(
64
+ {
65
+ birthcountry: null,
66
+ birthdate: new Date("1946-08-17"),
67
+ birthplace: "75001",
68
+ family_name: "Bernard",
69
+ gender: "male",
70
+ given_name: "Stéphane",
71
+ },
72
+ {
73
+ birthcountry: null,
74
+ birthdate: new Date("1986-08-17"),
75
+ birthplace: "75001",
76
+ family_name: "Bernard",
77
+ gender: "male",
78
+ given_name: "Stéphane",
79
+ },
80
+ ),
81
+ 4,
82
+ );
83
+ });
84
+
85
+ it("different birthplace", () => {
86
+ // Different birthplace = -1 point
87
+ assert.equal(
88
+ certificationScore(
89
+ {
90
+ birthcountry: null,
91
+ birthdate: new Date("1946-08-17"),
92
+ birthplace: "75001",
93
+ family_name: "Bernard",
94
+ gender: "male",
95
+ given_name: "Stéphane",
96
+ },
97
+ {
98
+ birthcountry: null,
99
+ birthdate: new Date("1946-08-17"),
100
+ birthplace: "13001",
101
+ family_name: "Bernard",
102
+ gender: "male",
103
+ given_name: "Stéphane",
104
+ },
105
+ ),
106
+ 4,
107
+ );
108
+ });
109
+
110
+ it("swapped first and family names", () => {
111
+ // Both names don't match = -2 points
112
+ assert.equal(
113
+ certificationScore(
114
+ {
115
+ birthcountry: null,
116
+ birthdate: new Date("1946-08-17"),
117
+ birthplace: "75001",
118
+ family_name: "Stéphane",
119
+ gender: "male",
120
+ given_name: "Bernard",
121
+ },
122
+ {
123
+ birthcountry: null,
124
+ birthdate: new Date("1946-08-17"),
125
+ birthplace: "75001",
126
+ family_name: "Bernard",
127
+ gender: "male",
128
+ given_name: "Stéphane",
129
+ },
130
+ ),
131
+ 3,
132
+ );
133
+ });
134
+
135
+ it("different gender still matches when source gender is null", () => {
136
+ // Gender is null in source = still gets point
137
+ assert.equal(
138
+ certificationScore(
139
+ {
140
+ birthcountry: null,
141
+ birthdate: new Date("1946-08-17"),
142
+ birthplace: "75001",
143
+ family_name: "Bernard",
144
+ gender: "male",
145
+ given_name: "Stéphane",
146
+ },
147
+ {
148
+ birthcountry: null,
149
+ birthdate: new Date("1946-08-17"),
150
+ birthplace: "75001",
151
+ family_name: "Bernard",
152
+ gender: null,
153
+ given_name: "Stéphane",
154
+ },
155
+ ),
156
+ 5,
157
+ );
158
+ });
159
+
160
+ it("different gender when source has gender", () => {
161
+ // Different gender = -1 point
162
+ assert.equal(
163
+ certificationScore(
164
+ {
165
+ birthcountry: null,
166
+ birthdate: new Date("1946-08-17"),
167
+ birthplace: "75001",
168
+ family_name: "Bernard",
169
+ gender: "male",
170
+ given_name: "Stéphane",
171
+ },
172
+ {
173
+ birthcountry: null,
174
+ birthdate: new Date("1946-08-17"),
175
+ birthplace: "75001",
176
+ family_name: "Bernard",
177
+ gender: "female",
178
+ given_name: "Stéphane",
179
+ },
180
+ ),
181
+ 4,
182
+ );
183
+ });
184
+
185
+ it("matches first name with hyphen normalization", () => {
186
+ // Jean-Pierre and Jean Pierre should match (first name is "Jean")
187
+ assert.equal(
188
+ certificationScore(
189
+ {
190
+ birthcountry: null,
191
+ birthdate: new Date("1946-08-17"),
192
+ birthplace: "75001",
193
+ family_name: "Bernard",
194
+ gender: "male",
195
+ given_name: "Jean-Pierre",
196
+ },
197
+ {
198
+ birthcountry: null,
199
+ birthdate: new Date("1946-08-17"),
200
+ birthplace: "75001",
201
+ family_name: "Bernard",
202
+ gender: "male",
203
+ given_name: "Jean Marc",
204
+ },
205
+ ),
206
+ 5,
207
+ );
208
+ });
209
+
210
+ it("birthplace with commune code conversion", () => {
211
+ // 75050 should convert to 92050
212
+ assert.equal(
213
+ certificationScore(
214
+ {
215
+ birthcountry: null,
216
+ birthdate: new Date("1946-08-17"),
217
+ birthplace: "75050",
218
+ family_name: "Bernard",
219
+ gender: "male",
220
+ given_name: "Stéphane",
221
+ },
222
+ {
223
+ birthcountry: null,
224
+ birthdate: new Date("1946-08-17"),
225
+ birthplace: "92050",
226
+ family_name: "Bernard",
227
+ gender: "male",
228
+ given_name: "Stéphane",
229
+ },
230
+ ),
231
+ 5,
232
+ );
233
+ });
234
+
235
+ it("null birthplace in source still matches", () => {
236
+ // Null birthplace in source = still gets point
237
+ assert.equal(
238
+ certificationScore(
239
+ {
240
+ birthcountry: null,
241
+ birthdate: new Date("1946-08-17"),
242
+ birthplace: "75001",
243
+ family_name: "Bernard",
244
+ gender: "male",
245
+ given_name: "Stéphane",
246
+ },
247
+ {
248
+ birthcountry: null,
249
+ birthdate: new Date("1946-08-17"),
250
+ birthplace: null,
251
+ family_name: "Bernard",
252
+ gender: "male",
253
+ given_name: "Stéphane",
254
+ },
255
+ ),
256
+ 5,
257
+ );
258
+ });
259
+
260
+ it("foreign person with matching country", () => {
261
+ // For foreigners, check country code
262
+ assert.equal(
263
+ certificationScore(
264
+ {
265
+ birthcountry: "99136",
266
+ birthdate: new Date("1946-08-17"),
267
+ birthplace: null,
268
+ family_name: "O'Connor",
269
+ gender: "male",
270
+ given_name: "Patrick",
271
+ },
272
+ {
273
+ birthcountry: "99136",
274
+ birthdate: new Date("1946-08-17"),
275
+ birthplace: null,
276
+ family_name: "O'Connor",
277
+ gender: "male",
278
+ given_name: "Patrick",
279
+ },
280
+ ),
281
+ 5,
282
+ );
283
+ });
284
+
285
+ it("foreign person with different country", () => {
286
+ // Different country = -1 point
287
+ assert.equal(
288
+ certificationScore(
289
+ {
290
+ birthcountry: "99136", // Ireland
291
+ birthdate: new Date("1946-08-17"),
292
+ birthplace: null,
293
+ family_name: "O'Connor",
294
+ gender: "male",
295
+ given_name: "Patrick",
296
+ },
297
+ {
298
+ birthcountry: "99132", // UK
299
+ birthdate: new Date("1946-08-17"),
300
+ birthplace: null,
301
+ family_name: "O'Connor",
302
+ gender: "male",
303
+ given_name: "Patrick",
304
+ },
305
+ ),
306
+ 4,
307
+ );
308
+ });
309
+ });
@@ -0,0 +1,97 @@
1
+ //
2
+
3
+ import type { IdentityVector } from "#src/types";
4
+ import { convertCommuneCode } from "./birthplace-conversion.js";
5
+ import { extractFirstName, normalizeText } from "./normalize.js";
6
+
7
+ //
8
+
9
+ /**
10
+ * Calculates a certification score between identity from FranceConnect (IdentitePivot)
11
+ * and executive identity from business registries (SourceDirigeant).
12
+ *
13
+ * The scoring system awards 1 point for each matching criterion:
14
+ * - Family name (after normalization): 1 point
15
+ * - First name (first name only, after normalization): 1 point
16
+ * - Gender (match or absent in source): 1 point
17
+ * - Birth date (exact match): 1 point
18
+ * - Birth place:
19
+ * - For French (birthcountry = 99100 or null): commune match or absent in source: 1 point
20
+ * - For foreigners: country match: 1 point
21
+ *
22
+ * Maximum score: 5 points (required for certification)
23
+ *
24
+ * @param identitePivot - Identity from FranceConnect
25
+ * @param sourceDirigeant - Executive identity from business registry
26
+ * @returns Score from 0 to 5
27
+ */
28
+ export function certificationScore(
29
+ identitePivot: IdentityVector,
30
+ sourceDirigeant: IdentityVector,
31
+ ): number {
32
+ let score = 0;
33
+
34
+ // 1. Family name match (after normalization)
35
+ const normalizedFamilyNamePivot = normalizeText(identitePivot.family_name);
36
+ const normalizedFamilyNameSource = normalizeText(sourceDirigeant.family_name);
37
+
38
+ if (
39
+ normalizedFamilyNamePivot &&
40
+ normalizedFamilyNameSource &&
41
+ normalizedFamilyNamePivot === normalizedFamilyNameSource
42
+ ) {
43
+ score += 1;
44
+ }
45
+
46
+ // 2. First name match (first name only, after normalization)
47
+ const firstNamePivot = extractFirstName(identitePivot.given_name);
48
+ const firstNameSource = extractFirstName(sourceDirigeant.given_name);
49
+
50
+ if (firstNamePivot && firstNameSource && firstNamePivot === firstNameSource) {
51
+ score += 1;
52
+ }
53
+
54
+ // 3. Gender match (match or absent in source)
55
+ if (
56
+ !sourceDirigeant.gender ||
57
+ identitePivot.gender === sourceDirigeant.gender
58
+ ) {
59
+ score += 1;
60
+ }
61
+
62
+ // 4. Birth date match (exact match)
63
+ if (
64
+ identitePivot.birthdate &&
65
+ sourceDirigeant.birthdate &&
66
+ identitePivot.birthdate.getTime() === sourceDirigeant.birthdate.getTime()
67
+ ) {
68
+ score += 1;
69
+ }
70
+
71
+ // 5. Birth place match (different logic for French vs foreigners)
72
+ const isFrench =
73
+ !identitePivot.birthcountry || identitePivot.birthcountry === "99100";
74
+
75
+ if (isFrench) {
76
+ // For French: check commune code (with conversion)
77
+ const communePivot = convertCommuneCode(identitePivot.birthplace);
78
+ const communeSource = convertCommuneCode(sourceDirigeant.birthplace);
79
+
80
+ // Match if equal or if source has no commune data
81
+ if (!communeSource || (communePivot && communePivot === communeSource)) {
82
+ score += 1;
83
+ }
84
+ } else {
85
+ // For foreigners: check country code (with conversion if needed from RNE)
86
+ // Both should already be in COG format (99XXX)
87
+ if (
88
+ identitePivot.birthcountry &&
89
+ sourceDirigeant.birthcountry &&
90
+ identitePivot.birthcountry === sourceDirigeant.birthcountry
91
+ ) {
92
+ score += 1;
93
+ }
94
+ }
95
+
96
+ return score;
97
+ }
@@ -1,4 +1,4 @@
1
1
  //
2
2
 
3
- export * from "./distance.js";
3
+ export * from "./certification-score.js";
4
4
  export * from "./is-organization-dirigeant.js";
@@ -1,19 +1,27 @@
1
1
  //
2
2
 
3
- import { NotFoundError } from "#src/errors";
3
+ import { InvalidCertificationError, NotFoundError } from "#src/errors";
4
+ import { IdentityVectorSchema } from "#src/types";
4
5
  import {
5
6
  LiElJonsonFranceConnectUserInfo,
6
7
  RogalDornFranceConnectUserInfo,
7
8
  } from "#testing/seed/franceconnect";
9
+ import {
10
+ papillon_org_info,
11
+ rogal_dorn_org_info,
12
+ } from "#testing/seed/organizations";
13
+ import {
14
+ RogalDornMandataire,
15
+ UlysseToriMandataire,
16
+ } from "@proconnect-gouv/proconnect.api_entreprise/testing/seed/v3-infogreffe-rcs-unites_legales-siren-mandataires_sociaux";
8
17
  import {
9
18
  LiElJonsonEstablishment,
10
19
  RogalDornEstablishment,
11
- } from "#testing/seed/insee";
12
- import { UlysseToriMandataire } from "#testing/seed/mandataires";
20
+ } from "@proconnect-gouv/proconnect.insee/testing/seed";
13
21
  import {
14
- Papillon,
15
- RogalDornEntrepreneur as RogalDornSireneEntrepreneur,
16
- } from "@proconnect-gouv/proconnect.api_entreprise/testing/seed/insee/siret";
22
+ RogalDornPouvoir,
23
+ UlysseTosiPouvoir,
24
+ } from "@proconnect-gouv/proconnect.registre_national_entreprises/testing/seed";
17
25
  import assert from "node:assert/strict";
18
26
  import { describe, it } from "node:test";
19
27
  import { isOrganizationDirigeantFactory } from "./is-organization-dirigeant.js";
@@ -26,20 +34,33 @@ describe("isOrganizationDirigeantFactory", () => {
26
34
  ApiEntrepriseInfogreffeRepository: {
27
35
  findMandatairesSociauxBySiren: () => Promise.reject(new Error("💣")),
28
36
  },
29
- ApiEntrepriseInseeRepository: {
30
- findBySiret: () => Promise.resolve(RogalDornSireneEntrepreneur),
37
+ RegistreNationalEntreprisesApiRepository: {
38
+ findPouvoirsBySiren: () => Promise.reject(new Error("💣")),
31
39
  },
32
40
  InseeApiRepository: {
33
- findBySiret: () => Promise.resolve(RogalDornEstablishment),
41
+ findBySiren: () => Promise.resolve(RogalDornEstablishment),
42
+ },
43
+ FranceConnectApiRepository: {
44
+ getFranceConnectUserInfo: () =>
45
+ Promise.resolve(RogalDornFranceConnectUserInfo),
34
46
  },
35
- getFranceConnectUserInfo: () =>
36
- Promise.resolve(RogalDornFranceConnectUserInfo),
37
47
  });
38
- const isDirigeant = await isOrganizationDirigeant(
39
- RogalDornSireneEntrepreneur.siret,
40
- 1,
41
- );
42
- assert.equal(isDirigeant, true);
48
+
49
+ const isDirigeant = await isOrganizationDirigeant(rogal_dorn_org_info, 1);
50
+
51
+ assert.deepEqual(isDirigeant, {
52
+ cause: "exact_match",
53
+ details: {
54
+ dirigeant: {
55
+ ...IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
56
+ birthplace: null, // INSEE adapter returns null for foreigners
57
+ },
58
+ score: 5,
59
+ identity: IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
60
+ source: "api.insee.fr/api-sirene/private",
61
+ },
62
+ ok: true,
63
+ });
43
64
  });
44
65
 
45
66
  it("should not match another mandataire", async () => {
@@ -47,39 +68,105 @@ describe("isOrganizationDirigeantFactory", () => {
47
68
  ApiEntrepriseInfogreffeRepository: {
48
69
  findMandatairesSociauxBySiren: () => Promise.reject(new Error("💣")),
49
70
  },
50
- ApiEntrepriseInseeRepository: {
51
- findBySiret: () => Promise.resolve(RogalDornSireneEntrepreneur),
71
+ RegistreNationalEntreprisesApiRepository: {
72
+ findPouvoirsBySiren: () => Promise.reject(new Error("💣")),
52
73
  },
53
74
  InseeApiRepository: {
54
- findBySiret: () => Promise.resolve(LiElJonsonEstablishment),
75
+ findBySiren: () => Promise.resolve(LiElJonsonEstablishment),
76
+ },
77
+ FranceConnectApiRepository: {
78
+ getFranceConnectUserInfo: () =>
79
+ Promise.resolve(RogalDornFranceConnectUserInfo),
55
80
  },
56
- getFranceConnectUserInfo: () =>
57
- Promise.resolve(RogalDornFranceConnectUserInfo),
58
81
  });
59
- const isDirigeant = await isOrganizationDirigeant(
60
- RogalDornSireneEntrepreneur.siret,
61
- 1,
62
- );
63
- assert.equal(isDirigeant, false);
82
+
83
+ const isDirigeant = await isOrganizationDirigeant(rogal_dorn_org_info, 1);
84
+
85
+ assert.deepEqual(isDirigeant, {
86
+ cause: "below_threshold",
87
+ details: {
88
+ dirigeant: {
89
+ ...IdentityVectorSchema.parse(LiElJonsonFranceConnectUserInfo),
90
+ birthcountry: null, // INSEE adapter returns null for French people
91
+ },
92
+ score: 1,
93
+ identity: IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
94
+ source: "api.insee.fr/api-sirene/private",
95
+ },
96
+ ok: false,
97
+ });
64
98
  });
65
99
 
66
- it("should match Ulysse Tori as an executive of Papillon", async () => {
100
+ it("should match Rogal Dorn among the executive of Papillon in RNE", async () => {
101
+ const isOrganizationDirigeant = isOrganizationDirigeantFactory({
102
+ ApiEntrepriseInfogreffeRepository: {
103
+ findMandatairesSociauxBySiren: () => Promise.reject(new Error("💣")),
104
+ },
105
+ RegistreNationalEntreprisesApiRepository: {
106
+ findPouvoirsBySiren: () =>
107
+ Promise.resolve([UlysseTosiPouvoir, RogalDornPouvoir]),
108
+ },
109
+ InseeApiRepository: {
110
+ findBySiren: () => Promise.reject(new Error("💣")),
111
+ },
112
+ FranceConnectApiRepository: {
113
+ getFranceConnectUserInfo: () =>
114
+ Promise.resolve(RogalDornFranceConnectUserInfo),
115
+ },
116
+ });
117
+
118
+ const isDirigeant = await isOrganizationDirigeant(papillon_org_info, 1);
119
+
120
+ assert.deepEqual(isDirigeant, {
121
+ cause: "exact_match",
122
+ details: {
123
+ dirigeant: {
124
+ ...IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
125
+ birthplace: null, // RNE adapter returns null for foreigners
126
+ },
127
+ score: 5,
128
+ identity: IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
129
+ source: "registre-national-entreprises.inpi.fr/api",
130
+ },
131
+ ok: true,
132
+ });
133
+ });
134
+
135
+ it("should match Rogal Dorn among the executive of Papillon in Infogreffe", async () => {
67
136
  const isOrganizationDirigeant = isOrganizationDirigeantFactory({
68
137
  ApiEntrepriseInfogreffeRepository: {
69
138
  findMandatairesSociauxBySiren: () =>
70
- Promise.resolve([UlysseToriMandataire]),
139
+ Promise.resolve([UlysseToriMandataire, RogalDornMandataire]),
71
140
  },
72
- ApiEntrepriseInseeRepository: {
73
- findBySiret: () => Promise.resolve(Papillon),
141
+ RegistreNationalEntreprisesApiRepository: {
142
+ findPouvoirsBySiren: () => Promise.reject(new Error("💣")),
74
143
  },
75
144
  InseeApiRepository: {
76
- findBySiret: () => Promise.reject(new Error("💣")),
145
+ findBySiren: () => Promise.reject(new Error("💣")),
146
+ },
147
+ FranceConnectApiRepository: {
148
+ getFranceConnectUserInfo: () =>
149
+ Promise.resolve(RogalDornFranceConnectUserInfo),
77
150
  },
78
- getFranceConnectUserInfo: () =>
79
- Promise.resolve(RogalDornFranceConnectUserInfo),
80
151
  });
81
- const isDirigeant = await isOrganizationDirigeant(Papillon.siret, 1);
82
- assert.equal(isDirigeant, false);
152
+
153
+ const isDirigeant = await isOrganizationDirigeant(papillon_org_info, 1);
154
+
155
+ assert.deepEqual(isDirigeant, {
156
+ cause: "exact_match",
157
+ details: {
158
+ dirigeant: {
159
+ ...IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
160
+ birthplace: null, // API Entreprise adapter doesn't provide birthplace
161
+ gender: null, // API Entreprise adapter doesn't provide gender
162
+ },
163
+ score: 5,
164
+ identity: IdentityVectorSchema.parse(RogalDornFranceConnectUserInfo),
165
+ source:
166
+ "entreprise.api.gouv.fr/v3/infogreffe/rcs/unites_legales/{siren}/mandataires_sociaux",
167
+ },
168
+ ok: true,
169
+ });
83
170
  });
84
171
 
85
172
  it("❎ fail with no franceconnect user info", async () => {
@@ -87,17 +174,19 @@ describe("isOrganizationDirigeantFactory", () => {
87
174
  ApiEntrepriseInfogreffeRepository: {
88
175
  findMandatairesSociauxBySiren: () => Promise.reject(new Error("💣")),
89
176
  },
90
- ApiEntrepriseInseeRepository: {
91
- findBySiret: () => Promise.resolve(RogalDornSireneEntrepreneur),
177
+ RegistreNationalEntreprisesApiRepository: {
178
+ findPouvoirsBySiren: () => Promise.reject(new Error("💣")),
92
179
  },
93
180
  InseeApiRepository: {
94
- findBySiret: () => Promise.reject(new Error("💣")),
181
+ findBySiren: () => Promise.reject(new Error("💣")),
182
+ },
183
+ FranceConnectApiRepository: {
184
+ getFranceConnectUserInfo: () => Promise.resolve(undefined),
95
185
  },
96
- getFranceConnectUserInfo: () => Promise.resolve(undefined),
97
186
  });
98
187
 
99
188
  await assert.rejects(
100
- isOrganizationDirigeant("94957325700019", 1),
189
+ isOrganizationDirigeant(rogal_dorn_org_info, 1),
101
190
  new NotFoundError("FranceConnect UserInfo not found"),
102
191
  );
103
192
  });
@@ -105,21 +194,23 @@ describe("isOrganizationDirigeantFactory", () => {
105
194
  it("❎ fail with no mandataires", async () => {
106
195
  const isOrganizationDirigeant = isOrganizationDirigeantFactory({
107
196
  ApiEntrepriseInfogreffeRepository: {
108
- findMandatairesSociauxBySiren: () => Promise.resolve([]),
197
+ findMandatairesSociauxBySiren: () => Promise.reject(new Error("💣")),
109
198
  },
110
- ApiEntrepriseInseeRepository: {
111
- findBySiret: () => Promise.resolve(Papillon),
199
+ RegistreNationalEntreprisesApiRepository: {
200
+ findPouvoirsBySiren: () => Promise.resolve([]),
112
201
  },
113
202
  InseeApiRepository: {
114
- findBySiret: () => Promise.reject(new Error("💣")),
203
+ findBySiren: () => Promise.reject(new Error("💣")),
204
+ },
205
+ FranceConnectApiRepository: {
206
+ getFranceConnectUserInfo: () =>
207
+ Promise.resolve(LiElJonsonFranceConnectUserInfo),
115
208
  },
116
- getFranceConnectUserInfo: () =>
117
- Promise.resolve(LiElJonsonFranceConnectUserInfo),
118
209
  });
119
210
 
120
211
  await assert.rejects(
121
- isOrganizationDirigeant(Papillon.siret, 1),
122
- new NotFoundError("No mandataires found"),
212
+ isOrganizationDirigeant(papillon_org_info, 1),
213
+ new InvalidCertificationError("No candidates found"),
123
214
  );
124
215
  });
125
216
  });