@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,21 @@
1
+ exports[`toIdentityVector > LiElJonsonEstablishment 1`] = `
2
+ {
3
+ "birthcountry": null,
4
+ "birthdate": "+028500-02-05T00:00:00.000Z",
5
+ "birthplace": "75056",
6
+ "family_name": "EL'JONSON",
7
+ "gender": "male",
8
+ "given_name": "LION"
9
+ }
10
+ `;
11
+
12
+ exports[`toIdentityVector > RogalDornEstablishment 1`] = `
13
+ {
14
+ "birthcountry": "99102",
15
+ "birthdate": "+029000-01-07T00:00:00.000Z",
16
+ "birthplace": null,
17
+ "family_name": "DORN",
18
+ "gender": "male",
19
+ "given_name": "ROGAL"
20
+ }
21
+ `;
@@ -0,0 +1,39 @@
1
+ import type { IdentityVector } from "#src/types";
2
+ import { formatBirthdate } from "@proconnect-gouv/proconnect.insee/formatters";
3
+ import type { InseSirenUniteLegale } from "@proconnect-gouv/proconnect.insee/types";
4
+ import { match } from "ts-pattern";
5
+
6
+ //
7
+
8
+ export function toIdentityVector(
9
+ uniteLegale: InseSirenUniteLegale,
10
+ ): IdentityVector {
11
+ let birthdate: Date | null = null;
12
+ const nomUniteLegale = uniteLegale.periodesUniteLegale?.at(0)?.nomUniteLegale;
13
+
14
+ if (uniteLegale?.dateNaissanceUniteLegale) {
15
+ const parsedDate = formatBirthdate(
16
+ String(uniteLegale.dateNaissanceUniteLegale),
17
+ );
18
+ birthdate = isNaN(parsedDate.getTime()) ? null : parsedDate;
19
+ }
20
+
21
+ return {
22
+ birthcountry: uniteLegale?.codePaysNaissanceUniteLegale ?? null,
23
+ birthdate,
24
+ birthplace: uniteLegale?.codeCommuneNaissanceUniteLegale ?? null,
25
+ family_name: nomUniteLegale ?? null,
26
+ gender: match(uniteLegale?.sexeUniteLegale)
27
+ .with("M", () => "male" as const)
28
+ .with("F", () => "female" as const)
29
+ .otherwise(() => null),
30
+ given_name: [
31
+ uniteLegale?.prenom1UniteLegale,
32
+ uniteLegale?.prenom2UniteLegale,
33
+ uniteLegale?.prenom3UniteLegale,
34
+ uniteLegale?.prenom4UniteLegale,
35
+ ]
36
+ .filter(Boolean)
37
+ .join(" "),
38
+ };
39
+ }
@@ -0,0 +1,276 @@
1
+ //
2
+
3
+ import {
4
+ RogalDornPouvoir,
5
+ UlysseTosiPouvoir,
6
+ } from "@proconnect-gouv/proconnect.registre_national_entreprises/testing/seed";
7
+ import assert from "node:assert/strict";
8
+ import { suite, test } from "node:test";
9
+ import { toIdentityVector } from "./rne.js";
10
+
11
+ suite("RNE adapter - toIdentityVector", () => {
12
+ test("converts valid pouvoir with all fields", () => {
13
+ const result = toIdentityVector(UlysseTosiPouvoir);
14
+
15
+ assert.deepEqual(result, {
16
+ birthcountry: "99100",
17
+ birthdate: new Date(Date.UTC(1992, 8, 7)), // September (month 8) 7, 1992
18
+ birthplace: "13055",
19
+ family_name: "TOSI",
20
+ gender: "male",
21
+ given_name: "Ulysse",
22
+ });
23
+ });
24
+
25
+ test("converts pouvoir with multiple prenoms", () => {
26
+ const pouvoir = {
27
+ individu: {
28
+ descriptionPersonne: {
29
+ dateDeNaissance: "1985-06-15",
30
+ lieuDeNaissance: "Paris",
31
+ nom: "DUPONT",
32
+ prenoms: ["Jean", "Paul", "Marie"],
33
+ genre: "1",
34
+ },
35
+ },
36
+ };
37
+
38
+ const result = toIdentityVector(pouvoir);
39
+
40
+ assert.equal(result.given_name, "Jean Paul Marie");
41
+ assert.equal(result.family_name, "DUPONT");
42
+ assert.equal(result.gender, "male");
43
+ });
44
+
45
+ test("handles invalid date format - returns null birthdate", () => {
46
+ const pouvoir = {
47
+ individu: {
48
+ descriptionPersonne: {
49
+ dateDeNaissance: "invalid-date",
50
+ lieuDeNaissance: "Lyon",
51
+ nom: "MARTIN",
52
+ prenoms: ["Alice"],
53
+ },
54
+ },
55
+ };
56
+
57
+ const result = toIdentityVector(pouvoir);
58
+
59
+ assert.equal(result.birthdate, null);
60
+ assert.equal(result.family_name, "MARTIN");
61
+ });
62
+
63
+ test("handles missing optional fields - returns null", () => {
64
+ const result = toIdentityVector({});
65
+
66
+ assert.deepEqual(result, {
67
+ birthcountry: null,
68
+ birthdate: null,
69
+ birthplace: null,
70
+ family_name: null,
71
+ gender: null,
72
+ given_name: null,
73
+ });
74
+ });
75
+
76
+ test("handles empty prenoms array", () => {
77
+ const pouvoir = {
78
+ individu: {
79
+ descriptionPersonne: {
80
+ dateDeNaissance: "1990-01-01",
81
+ lieuDeNaissance: "Marseille",
82
+ nom: "BERNARD",
83
+ prenoms: [],
84
+ },
85
+ },
86
+ };
87
+
88
+ const result = toIdentityVector(pouvoir);
89
+
90
+ assert.equal(result.given_name, "");
91
+ assert.equal(result.family_name, "BERNARD");
92
+ });
93
+
94
+ test("handles far future date (RogalDorn from year 29000)", () => {
95
+ const result = toIdentityVector(RogalDornPouvoir);
96
+
97
+ // Year 29000 should be parsed correctly
98
+ assert.deepEqual(result, {
99
+ birthcountry: "99102", // ISL (Iceland) converts to 99102
100
+ birthdate: new Date(Date.UTC(29000, 0, 7)), // January 7, 29000
101
+ birthplace: null,
102
+ family_name: "DORN",
103
+ gender: "male",
104
+ given_name: "ROGAL",
105
+ });
106
+ });
107
+
108
+ test("handles partial date components", () => {
109
+ const pouvoir = {
110
+ individu: {
111
+ descriptionPersonne: {
112
+ dateDeNaissance: "2000-12-31",
113
+ lieuDeNaissance: "Bordeaux",
114
+ nom: "LAURENT",
115
+ prenoms: ["Sophie"],
116
+ },
117
+ },
118
+ };
119
+
120
+ const result = toIdentityVector(pouvoir);
121
+
122
+ assert.deepEqual(result.birthdate, new Date(Date.UTC(2000, 11, 31))); // December 31, 2000
123
+ });
124
+
125
+ test("handles undefined individu fields gracefully", () => {
126
+ const pouvoir = {};
127
+
128
+ const result = toIdentityVector(pouvoir);
129
+
130
+ assert.deepEqual(result, {
131
+ birthcountry: null,
132
+ birthdate: null,
133
+ birthplace: null,
134
+ family_name: null,
135
+ gender: null,
136
+ given_name: null,
137
+ });
138
+ });
139
+
140
+ test("maps genre 1 to male", () => {
141
+ const pouvoir = {
142
+ individu: {
143
+ descriptionPersonne: {
144
+ dateDeNaissance: "1990-01-01",
145
+ lieuDeNaissance: "Paris",
146
+ nom: "DUPONT",
147
+ prenoms: ["Jean"],
148
+ genre: "1",
149
+ },
150
+ },
151
+ };
152
+
153
+ const result = toIdentityVector(pouvoir);
154
+ assert.equal(result.gender, "male");
155
+ });
156
+
157
+ test("maps genre 2 to female", () => {
158
+ const pouvoir = {
159
+ individu: {
160
+ descriptionPersonne: {
161
+ dateDeNaissance: "1990-01-01",
162
+ lieuDeNaissance: "Paris",
163
+ nom: "MARTIN",
164
+ prenoms: ["Marie"],
165
+ genre: "2",
166
+ },
167
+ },
168
+ };
169
+
170
+ const result = toIdentityVector(pouvoir);
171
+ assert.equal(result.gender, "female");
172
+ });
173
+
174
+ test("returns null for unknown genre", () => {
175
+ const pouvoir = {
176
+ individu: {
177
+ descriptionPersonne: {
178
+ dateDeNaissance: "1990-01-01",
179
+ lieuDeNaissance: "Paris",
180
+ nom: "BERNARD",
181
+ prenoms: ["Alex"],
182
+ genre: "3",
183
+ },
184
+ },
185
+ };
186
+
187
+ const result = toIdentityVector(pouvoir);
188
+ assert.equal(result.gender, null);
189
+ });
190
+
191
+ test("ignores codeInseeGeographique when codePostalNaissance is missing", () => {
192
+ const pouvoir = {
193
+ individu: {
194
+ descriptionPersonne: {
195
+ dateDeNaissance: "1990-01-01",
196
+ lieuDeNaissance: "Lyon",
197
+ nom: "DUPONT",
198
+ prenoms: ["Jean"],
199
+ codeInseeGeographique: "69123",
200
+ },
201
+ },
202
+ };
203
+
204
+ const result = toIdentityVector(pouvoir);
205
+ assert.equal(result.birthplace, null);
206
+ });
207
+
208
+ test("ignores codeInseeGeographique when format is invalid", () => {
209
+ const pouvoir = {
210
+ individu: {
211
+ descriptionPersonne: {
212
+ dateDeNaissance: "1990-01-01",
213
+ lieuDeNaissance: "Lyon",
214
+ nom: "DUPONT",
215
+ prenoms: ["Jean"],
216
+ codeInseeGeographique: "1234", // Invalid: only 4 digits
217
+ codePostalNaissance: "69001",
218
+ },
219
+ },
220
+ };
221
+
222
+ const result = toIdentityVector(pouvoir);
223
+ assert.equal(result.birthplace, null);
224
+ });
225
+
226
+ test("ignores codeInseeGeographique when it starts with 99 (foreign country)", () => {
227
+ const pouvoir = {
228
+ individu: {
229
+ descriptionPersonne: {
230
+ dateDeNaissance: "1990-01-01",
231
+ lieuDeNaissance: "Foreign",
232
+ nom: "DUPONT",
233
+ prenoms: ["Jean"],
234
+ codeInseeGeographique: "99123", // Foreign country code
235
+ codePostalNaissance: "69001",
236
+ },
237
+ },
238
+ };
239
+
240
+ const result = toIdentityVector(pouvoir);
241
+ assert.equal(result.birthplace, null);
242
+ });
243
+
244
+ test("converts codePaysNaissance from ISO 3166 to COG format", () => {
245
+ const pouvoir = {
246
+ individu: {
247
+ descriptionPersonne: {
248
+ dateDeNaissance: "1990-01-01",
249
+ lieuDeNaissance: "Dublin",
250
+ nom: "O'CONNOR",
251
+ prenoms: ["Sean"],
252
+ codePaysNaissance: "IRL",
253
+ },
254
+ },
255
+ };
256
+
257
+ const result = toIdentityVector(pouvoir);
258
+ assert.equal(result.birthcountry, "99136");
259
+ });
260
+
261
+ test("returns null for birthcountry when codePaysNaissance is invalid", () => {
262
+ const pouvoir = {
263
+ individu: {
264
+ descriptionPersonne: {
265
+ dateDeNaissance: "1990-01-01",
266
+ nom: "DUPONT",
267
+ prenoms: ["Jean"],
268
+ codePaysNaissance: "XXX",
269
+ },
270
+ },
271
+ };
272
+
273
+ const result = toIdentityVector(pouvoir);
274
+ assert.equal(result.birthcountry, null);
275
+ });
276
+ });
@@ -0,0 +1,64 @@
1
+ import type { IdentityVector } from "#src/types";
2
+ import type { FindPouvoirsBySirenHandler } from "@proconnect-gouv/proconnect.registre_national_entreprises/api";
3
+ import { match } from "ts-pattern";
4
+ import {
5
+ convertCountryIsoToCog,
6
+ isValidBirthplaceFormat,
7
+ } from "../birthplace-conversion.js";
8
+
9
+ //
10
+
11
+ export function toIdentityVector(
12
+ pouvoir: NonNullable<Awaited<ReturnType<FindPouvoirsBySirenHandler>>[number]>,
13
+ ): IdentityVector {
14
+ const descriptionPersonne = pouvoir.individu?.descriptionPersonne;
15
+
16
+ const dateMatch = String(descriptionPersonne?.dateDeNaissance).match(
17
+ /^(?<year>\d+)-(?<month>\d{2})-(?<day>\d{2})$/,
18
+ );
19
+ const { day, month, year } = dateMatch?.groups ?? {
20
+ day: NaN,
21
+ month: NaN,
22
+ year: NaN,
23
+ };
24
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
25
+ const birthdate = isNaN(date.getTime()) ? null : date;
26
+
27
+ // For RNE:
28
+ // birthplace: Use codeInseeGeographique only if:
29
+ // - codePostalNaissance is present
30
+ // - codeInseeGeographique has valid format (5 digits or 2A/2B + 3 digits)
31
+ // - first 2 chars are not "99" (not a foreign country code)
32
+ let birthplace: string | null = null;
33
+ const codeInseeGeographique = descriptionPersonne?.codeInseeGeographique;
34
+ const codePostalNaissance = descriptionPersonne?.codePostalNaissance;
35
+
36
+ if (
37
+ codePostalNaissance &&
38
+ codeInseeGeographique &&
39
+ isValidBirthplaceFormat(codeInseeGeographique) &&
40
+ !codeInseeGeographique.startsWith("99")
41
+ ) {
42
+ birthplace = codeInseeGeographique;
43
+ }
44
+
45
+ // birthcountry: Convert paysNaissance from ISO 3166 to COG format
46
+ let birthcountry: string | null = null;
47
+ const codePaysNaissance = descriptionPersonne?.codePaysNaissance;
48
+
49
+ if (codePaysNaissance) {
50
+ birthcountry = convertCountryIsoToCog(codePaysNaissance);
51
+ }
52
+
53
+ return {
54
+ birthcountry,
55
+ birthdate,
56
+ birthplace,
57
+ family_name: descriptionPersonne?.nom ?? null,
58
+ gender: match(descriptionPersonne?.genre)
59
+ .with("1", () => "male" as const)
60
+ .with("2", () => "female" as const)
61
+ .otherwise(() => null),
62
+ given_name: descriptionPersonne?.prenoms?.join(" ") ?? null,
63
+ };
64
+ }
@@ -0,0 +1,76 @@
1
+ //
2
+
3
+ import assert from "node:assert/strict";
4
+ import { describe, it } from "node:test";
5
+ import {
6
+ convertCommuneCode,
7
+ convertCountryIsoToCog,
8
+ isValidBirthplaceFormat,
9
+ } from "./birthplace-conversion.js";
10
+
11
+ //
12
+
13
+ describe("convertCommuneCode", () => {
14
+ it("converts old commune code to new code", () => {
15
+ assert.equal(convertCommuneCode("75050"), "92050");
16
+ });
17
+
18
+ it("returns original code if no conversion exists", () => {
19
+ assert.equal(convertCommuneCode("75056"), "75056");
20
+ });
21
+
22
+ it("handles null and undefined", () => {
23
+ assert.equal(convertCommuneCode(null), null);
24
+ assert.equal(convertCommuneCode(undefined), null);
25
+ });
26
+ });
27
+
28
+ describe("convertCountryIsoToCog", () => {
29
+ it("converts France", () => {
30
+ assert.equal(convertCountryIsoToCog("FRA"), "99100");
31
+ });
32
+
33
+ it("converts Ireland", () => {
34
+ assert.equal(convertCountryIsoToCog("IRL"), "99136");
35
+ });
36
+
37
+ it("converts Iceland with 2-letter code", () => {
38
+ assert.equal(convertCountryIsoToCog("IS"), "99102");
39
+ });
40
+
41
+ it("handles lowercase", () => {
42
+ assert.equal(convertCountryIsoToCog("fra"), "99100");
43
+ });
44
+
45
+ it("returns null for unknown country", () => {
46
+ assert.equal(convertCountryIsoToCog("XXX"), null);
47
+ });
48
+
49
+ it("handles null and undefined", () => {
50
+ assert.equal(convertCountryIsoToCog(null), null);
51
+ assert.equal(convertCountryIsoToCog(undefined), null);
52
+ });
53
+ });
54
+
55
+ describe("isValidBirthplaceFormat", () => {
56
+ it("validates 5-digit codes", () => {
57
+ assert.equal(isValidBirthplaceFormat("75001"), true);
58
+ assert.equal(isValidBirthplaceFormat("92050"), true);
59
+ });
60
+
61
+ it("validates Corsican codes (2A/2B)", () => {
62
+ assert.equal(isValidBirthplaceFormat("2A001"), true);
63
+ assert.equal(isValidBirthplaceFormat("2B042"), true);
64
+ });
65
+
66
+ it("rejects invalid formats", () => {
67
+ assert.equal(isValidBirthplaceFormat("1234"), false);
68
+ assert.equal(isValidBirthplaceFormat("123456"), false);
69
+ assert.equal(isValidBirthplaceFormat("ABC12"), false);
70
+ });
71
+
72
+ it("handles null and undefined", () => {
73
+ assert.equal(isValidBirthplaceFormat(null), false);
74
+ assert.equal(isValidBirthplaceFormat(undefined), false);
75
+ });
76
+ });
@@ -0,0 +1,58 @@
1
+ //
2
+
3
+ import {
4
+ COMMUNE_CODE_CONVERSION,
5
+ COUNTRY_ISO_TO_COG,
6
+ } from "#src/data/certification";
7
+
8
+ /**
9
+ * Converts a commune code to a canonical representation,
10
+ * ensuring consistency across historical and current versions.
11
+ * If the code is not in the conversion table, returns the original code.
12
+ *
13
+ * @param communeCode - The commune code to convert (format: XXXXX or 2AXXX or 2BXXX)
14
+ * @returns The converted code, or the original if no conversion is needed
15
+ */
16
+ export function convertCommuneCode(
17
+ communeCode: string | null | undefined,
18
+ ): string | null {
19
+ if (!communeCode) return null;
20
+
21
+ // Check if the code needs conversion
22
+ const converted = COMMUNE_CODE_CONVERSION[communeCode];
23
+ return converted || communeCode;
24
+ }
25
+
26
+ /**
27
+ * Converts an ISO 3166 country code (2 or 3-letter) to COG format (5-digit).
28
+ *
29
+ * @param iso3166Code - The ISO 3166 country code (e.g., "IRL", "FRA")
30
+ * @returns The COG code (e.g., "99136"), or null if not found
31
+ */
32
+ export function convertCountryIsoToCog(
33
+ iso3166Code: string | null | undefined,
34
+ ): string | null {
35
+ if (!iso3166Code) return null;
36
+
37
+ const cogCode = COUNTRY_ISO_TO_COG[iso3166Code.toUpperCase()];
38
+ return cogCode || null;
39
+ }
40
+
41
+ /**
42
+ * Validates if a birthplace code has the correct COG format.
43
+ * Valid formats:
44
+ * - 5 digits (e.g., "75001")
45
+ * - 2A or 2B + 3 digits (e.g., "2A001", "2B042" for Corsica)
46
+ *
47
+ * @param birthplaceCode - The birthplace code to validate
48
+ * @returns true if the format is valid, false otherwise
49
+ */
50
+ export function isValidBirthplaceFormat(
51
+ birthplaceCode: string | null | undefined,
52
+ ): boolean {
53
+ if (!birthplaceCode) return false;
54
+
55
+ // Match either 5 digits or 2A/2B + 3 digits
56
+ const validFormat = /^(\d{5}|2[AB]\d{3})$/;
57
+ return validFormat.test(birthplaceCode);
58
+ }