@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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/data/certification/commune-code-conversion.d.ts +13 -0
- package/dist/data/certification/commune-code-conversion.d.ts.map +1 -0
- package/dist/data/certification/commune-code-conversion.js +5188 -0
- package/dist/data/certification/country-iso-to-cog.d.ts +12 -0
- package/dist/data/certification/country-iso-to-cog.d.ts.map +1 -0
- package/dist/data/certification/country-iso-to-cog.js +438 -0
- package/dist/data/certification/index.d.ts +3 -0
- package/dist/data/certification/index.d.ts.map +1 -0
- package/dist/data/certification/index.js +3 -0
- package/dist/managers/certification/adapters/api_entreprise.d.ts +4 -0
- package/dist/managers/certification/adapters/api_entreprise.d.ts.map +1 -0
- package/dist/managers/certification/adapters/api_entreprise.js +14 -0
- package/dist/managers/certification/adapters/franceconnect.d.ts +3 -0
- package/dist/managers/certification/adapters/franceconnect.d.ts.map +1 -0
- package/dist/managers/certification/adapters/franceconnect.js +15 -0
- package/dist/managers/certification/adapters/insee.d.ts +4 -0
- package/dist/managers/certification/adapters/insee.d.ts.map +1 -0
- package/dist/managers/certification/adapters/insee.js +29 -0
- package/dist/managers/certification/adapters/rne.d.ts +4 -0
- package/dist/managers/certification/adapters/rne.d.ts.map +1 -0
- package/dist/managers/certification/adapters/rne.js +45 -0
- package/dist/managers/certification/birthplace-conversion.d.ts +27 -0
- package/dist/managers/certification/birthplace-conversion.d.ts.map +1 -0
- package/dist/managers/certification/birthplace-conversion.js +45 -0
- package/dist/managers/certification/certification-score.d.ts +22 -0
- package/dist/managers/certification/certification-score.d.ts.map +1 -0
- package/dist/managers/certification/certification-score.js +72 -0
- package/dist/managers/certification/index.d.ts +1 -1
- package/dist/managers/certification/index.d.ts.map +1 -1
- package/dist/managers/certification/index.js +1 -1
- package/dist/managers/certification/is-organization-dirigeant.d.ts +38 -8
- package/dist/managers/certification/is-organization-dirigeant.d.ts.map +1 -1
- package/dist/managers/certification/is-organization-dirigeant.js +89 -52
- package/dist/managers/certification/normalize.d.ts +21 -0
- package/dist/managers/certification/normalize.d.ts.map +1 -0
- package/dist/managers/certification/normalize.js +64 -0
- package/dist/managers/franceconnect/openid-client.d.ts +1 -0
- package/dist/managers/franceconnect/openid-client.d.ts.map +1 -1
- package/dist/managers/organization/adapters/api_entreprise.d.ts +25 -0
- package/dist/managers/organization/adapters/api_entreprise.d.ts.map +1 -0
- package/dist/{mappers/organization/from-siret.js → managers/organization/adapters/api_entreprise.js} +26 -3
- package/dist/managers/organization/get-organization-info.d.ts +2 -2
- package/dist/managers/organization/get-organization-info.d.ts.map +1 -1
- package/dist/managers/organization/get-organization-info.js +6 -6
- package/dist/repositories/organization/upsert.d.ts +1 -1
- package/dist/repositories/organization/upsert.d.ts.map +1 -1
- package/dist/repositories/organization/upsert.js +26 -1
- package/dist/repositories/user/get-franceconnect-user-info.d.ts +1 -0
- package/dist/repositories/user/get-franceconnect-user-info.d.ts.map +1 -1
- package/dist/repositories/user/upsert-franceconnect-userinfo.d.ts +1 -0
- package/dist/repositories/user/upsert-franceconnect-userinfo.d.ts.map +1 -1
- package/dist/services/organization/index.d.ts +2 -0
- package/dist/services/organization/index.d.ts.map +1 -1
- package/dist/services/organization/index.js +2 -0
- package/dist/services/organization/is-public-service.d.ts +3 -0
- package/dist/services/organization/is-public-service.d.ts.map +1 -0
- package/dist/services/organization/is-public-service.js +19 -0
- package/dist/services/organization/is-syndicat-communal.d.ts +3 -0
- package/dist/services/organization/is-syndicat-communal.d.ts.map +1 -0
- package/dist/services/organization/is-syndicat-communal.js +13 -0
- package/dist/types/dirigeant.d.ts +5 -0
- package/dist/types/dirigeant.d.ts.map +1 -1
- package/dist/types/dirigeant.js +2 -0
- package/dist/types/franceconnect.d.ts +2 -0
- package/dist/types/franceconnect.d.ts.map +1 -1
- package/dist/types/franceconnect.js +1 -0
- package/dist/types/organization.d.ts +3 -3
- package/dist/types/organization.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/data/certification/commune-code-conversion.ts +5189 -0
- package/src/data/certification/country-iso-to-cog.ts +439 -0
- package/src/data/certification/index.ts +4 -0
- package/src/managers/certification/adapters/api_entreprise.test.ts +68 -0
- package/src/managers/certification/adapters/api_entreprise.test.ts.snapshot +109 -0
- package/src/{mappers/certification/index.ts → managers/certification/adapters/api_entreprise.ts} +8 -5
- package/src/managers/certification/adapters/franceconnect.ts +21 -0
- package/src/managers/certification/adapters/insee.test.ts +18 -0
- package/src/managers/certification/adapters/insee.test.ts.snapshot +21 -0
- package/src/managers/certification/adapters/insee.ts +39 -0
- package/src/managers/certification/adapters/rne.test.ts +276 -0
- package/src/managers/certification/adapters/rne.ts +64 -0
- package/src/managers/certification/birthplace-conversion.test.ts +76 -0
- package/src/managers/certification/birthplace-conversion.ts +58 -0
- package/src/managers/certification/certification-score.test.ts +309 -0
- package/src/managers/certification/certification-score.ts +97 -0
- package/src/managers/certification/index.ts +1 -1
- package/src/managers/certification/is-organization-dirigeant.test.ts +144 -53
- package/src/managers/certification/is-organization-dirigeant.ts +132 -106
- package/src/managers/certification/normalize.test.ts +71 -0
- package/src/managers/certification/normalize.ts +72 -0
- package/src/managers/organization/adapters/api_entreprise.test.ts +31 -0
- package/src/{mappers/organization/from-siret.test.ts.snapshot → managers/organization/adapters/api_entreprise.test.ts.snapshot} +26 -3
- package/src/{mappers/organization/from-siret.ts → managers/organization/adapters/api_entreprise.ts} +55 -5
- package/src/managers/organization/get-organization-info.test.ts +2 -2
- package/src/managers/organization/get-organization-info.ts +10 -10
- package/src/repositories/organization/get-users-by-organization.test.ts.snapshot +1 -1
- package/src/repositories/organization/upsert.ts +69 -19
- package/src/repositories/user/find-by-email.test.ts +1 -1
- package/src/repositories/user/find-by-id.test.ts +1 -1
- package/src/repositories/user/get-by-id.test.ts +1 -1
- package/src/repositories/user/get-franceconnect-user-info.test.ts +1 -0
- package/src/repositories/user/upsert-franceconnect-userinfo.test.ts +2 -0
- package/src/services/organization/index.ts +2 -0
- package/src/services/organization/is-public-service.test.ts +99 -0
- package/src/services/organization/is-public-service.ts +35 -0
- package/src/services/organization/is-syndicat-communal.test.ts +31 -0
- package/src/services/organization/is-syndicat-communal.ts +18 -0
- package/src/types/dirigeant.ts +3 -0
- package/src/types/franceconnect.ts +1 -0
- package/src/types/organization-info.ts +1 -1
- package/src/types/organization.ts +3 -3
- package/testing/seed/franceconnect/index.ts +12 -9
- package/testing/seed/organizations/index.ts +108 -0
- package/tsconfig.json +6 -2
- package/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/managers/certification/distance.d.ts +0 -4
- package/dist/managers/certification/distance.d.ts.map +0 -1
- package/dist/managers/certification/distance.js +0 -16
- package/dist/mappers/certification/index.d.ts +0 -4
- package/dist/mappers/certification/index.d.ts.map +0 -1
- package/dist/mappers/certification/index.js +0 -11
- package/dist/mappers/index.d.ts +0 -2
- package/dist/mappers/index.d.ts.map +0 -1
- package/dist/mappers/index.js +0 -2
- package/dist/mappers/organization/from-siret.d.ts +0 -5
- package/dist/mappers/organization/from-siret.d.ts.map +0 -1
- package/dist/mappers/organization/index.d.ts +0 -2
- package/dist/mappers/organization/index.d.ts.map +0 -1
- package/dist/mappers/organization/index.js +0 -2
- package/src/managers/certification/distance.test.ts +0 -109
- package/src/managers/certification/distance.ts +0 -41
- package/src/mappers/index.ts +0 -3
- package/src/mappers/organization/from-siret.test.ts +0 -26
- package/src/mappers/organization/index.ts +0 -3
- package/testing/seed/insee/index.ts +0 -22
- 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 {
|
|
8
|
-
import type {
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"findBySiret"
|
|
20
|
+
ApiEntrepriseInfogreffeRepository: Pick<
|
|
21
|
+
ApiEntrepriseInfogreffeRepository,
|
|
22
|
+
"findMandatairesSociauxBySiren"
|
|
25
23
|
>;
|
|
26
24
|
EQUALITY_THRESHOLD?: number;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
75
|
+
organization: Organization,
|
|
48
76
|
user_id: number,
|
|
49
77
|
) {
|
|
50
|
-
const
|
|
51
|
-
const franceconnectUserInfo =
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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[`
|
|
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[`
|
|
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[`
|
|
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",
|
package/src/{mappers/organization/from-siret.ts → managers/organization/adapters/api_entreprise.ts}
RENAMED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
//
|
|
2
2
|
|
|
3
3
|
import "#src/types";
|
|
4
|
-
import {
|
|
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.
|
|
11
|
-
import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.
|
|
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
|
|
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
|
-
}
|
|
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.
|
|
6
|
-
import type { InseeSireneEstablishmentSiretResponseData } from "@proconnect-gouv/proconnect.
|
|
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 {
|
|
5
|
+
import type { ApiEntrepriseInseeRepository } from "@proconnect-gouv/proconnect.api_entreprise/api";
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "@proconnect-gouv/proconnect.
|
|
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:
|
|
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 =
|
|
26
|
+
establishment = ApiEntreprise.toOrganizationInfo(
|
|
27
27
|
await findBySiret(siretOrSiren),
|
|
28
28
|
);
|
|
29
29
|
} else if (siretOrSiren.match(/^\d{9}$/)) {
|
|
30
|
-
establishment =
|
|
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 (
|
|
45
|
+
if (ApiEntrepriseInvalidSiret.isInvalidSiret(e))
|
|
46
46
|
throw new InvalidSiretError();
|
|
47
47
|
|
|
48
|
-
throw new
|
|
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,
|