From eba1bf05424a153b2015b46536a6e86addbacb94 Mon Sep 17 00:00:00 2001 From: Robin Monnier Date: Wed, 6 Nov 2024 15:58:29 +0100 Subject: [PATCH] feat: api route that compares inpi and ig --- ...ant-content.tsx => dirigeants-content.tsx} | 74 ++++- ...-dirigeants.tsx => dirigeants-section.tsx} | 26 +- .../_component/sections/entreprise/index.tsx | 57 +--- .../entreprise/rcs-rne-comparison.tsx | 43 --- app/api/data-fetching/routes-handlers.ts | 4 +- app/api/data-fetching/routes-paths.ts | 2 +- app/api/data-fetching/routes-scopes.ts | 3 +- .../api-entreprise/mandataires-rcs/index.ts | 38 ++- clients/api-proxy/rne/index.ts | 4 +- clients/recherche-entreprise/dirigeants.ts | 2 +- components/search-results/results-list.tsx | 2 +- models/espace-agent/dirigeants-protected.ts | 40 +++ models/espace-agent/mandataires-rcs.ts | 4 +- models/espace-agent/mergeDirigeants.test.ts | 275 ++++++++++++++++++ models/espace-agent/utils.ts | 71 +++++ models/rne/dirigeants.tsx | 4 +- models/rne/observations.ts | 4 +- models/rne/types.ts | 32 +- models/search/index.ts | 2 +- 19 files changed, 532 insertions(+), 155 deletions(-) rename app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/{dirigeant-content.tsx => dirigeants-content.tsx} (59%) rename app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/{rne-dirigeants.tsx => dirigeants-section.tsx} (86%) delete mode 100644 app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rcs-rne-comparison.tsx create mode 100644 models/espace-agent/dirigeants-protected.ts create mode 100644 models/espace-agent/mergeDirigeants.test.ts diff --git a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeant-content.tsx b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeants-content.tsx similarity index 59% rename from app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeant-content.tsx rename to app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeants-content.tsx index 1b8cb21c4..bb3261e27 100644 --- a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeant-content.tsx +++ b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeants-content.tsx @@ -1,23 +1,71 @@ +import FAQLink from '#components-ui/faq-link'; import { SeePersonPageLink } from '#components-ui/see-personn-page-link'; import { FullTable } from '#components/table/full'; import { IUniteLegale } from '#models/core/types'; -import { IDirigeants, IEtatCivil, IPersonneMorale } from '#models/rne/types'; +import { + IDirigeantsWithMetadata, + IEtatCivil, + IPersonneMorale, +} from '#models/rne/types'; import { formatDateLong, formatDatePartial, formatIntFr } from '#utils/helpers'; import { isPersonneMorale } from '../is-personne-morale'; type IDirigeantContentProps = { - dirigeants: IDirigeants; + dirigeants: IDirigeantsWithMetadata; uniteLegale: IUniteLegale; }; -export function DirigeantContent({ +const dataSourceTooltip = ({ + dataType, + isInIg, + isInInpi, +}: { + dataType: string; + isInIg?: boolean; + isInInpi?: boolean; +}) => { + if (!isInIg && !isInInpi) { + return <>; + } + + return ( + <> + {!isInIg && ( + <> + {' '} + }> + Ce {dataType} n‘apparait pas dans les données d‘Infogreffe. + + + )} + {!isInInpi && ( + <> + {' '} + }> + Ce {dataType} n‘apparait pas dans les données de l‘INPI. + + + )} + + ); +}; + +export default function DirigeantsContent({ dirigeants, uniteLegale, }: IDirigeantContentProps) { const formatDirigeant = (dirigeant: IEtatCivil | IPersonneMorale) => { if (isPersonneMorale(dirigeant)) { const infos = [ - dirigeant.role, + dirigeant.roles?.map((role) => ( + <> + {role.label} + {dataSourceTooltip({ + ...role, + dataType: 'rôle', + })} + + )) || <>{dirigeant.role}, <> {dirigeant.denomination} {dirigeant.siren ? ( @@ -32,6 +80,10 @@ export function DirigeantContent({ )}
{dirigeant.natureJuridique} + {dataSourceTooltip({ + ...dirigeant, + dataType: 'dirigeant', + })} , ]; @@ -50,7 +102,15 @@ export function DirigeantContent({ }${(dirigeant.nom || '').toUpperCase()}`; return [ - dirigeant.role, + dirigeant.roles?.map((role) => ( + <> + {role.label} + {dataSourceTooltip({ + ...role, + dataType: 'rôle', + })} + + )) || <>{dirigeant.role}, <> {nomComplet} {dirigeant.dateNaissance || dirigeant.dateNaissancePartial @@ -62,6 +122,10 @@ export function DirigeantContent({ dirigeant.lieuNaissance ? `, à ${dirigeant.lieuNaissance}` : '' }` : ''} + {dataSourceTooltip({ + ...dirigeant, + dataType: 'dirigeant', + })} , ...(dirigeant.dateNaissancePartial ? [ diff --git a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rne-dirigeants.tsx b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeants-section.tsx similarity index 86% rename from app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rne-dirigeants.tsx rename to app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeants-section.tsx index 9e7b274ee..b0edcb4fd 100644 --- a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rne-dirigeants.tsx +++ b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/dirigeants-section.tsx @@ -7,35 +7,30 @@ import { UniteLegalePageLink } from '#components/unite-legale-page-link'; import { EAdministration } from '#models/administrations/EAdministration'; import { IUniteLegale } from '#models/core/types'; import { IDirigeantsFetching } from '.'; -import { DirigeantContent } from './dirigeant-content'; +import DirigeantsContent from './dirigeants-content'; type IProps = { dirigeants: IDirigeantsFetching; uniteLegale: IUniteLegale; isProtected: boolean; - warning: JSX.Element; }; /** * Dirigeants section */ -function DirigeantsSection({ +export default function DirigeantsSection({ uniteLegale, dirigeants, isProtected, - warning, }: IProps) { - const sources = [EAdministration.INPI]; - - if (isProtected) { - sources.push(EAdministration.INFOGREFFE); - } - return ( {dirigeants.metadata?.isFallback && } - {warning ? warning : null} {isProtected ? ( - Ces informations proviennent d’ + Ces informations proviennent en partie d’ - @@ -99,5 +93,3 @@ function DirigeantsSection({ ); } - -export default DirigeantsSection; diff --git a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/index.tsx b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/index.tsx index 38780d115..c3345a281 100644 --- a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/index.tsx +++ b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/index.tsx @@ -4,43 +4,21 @@ import { HorizontalSeparator } from '#components-ui/horizontal-separator'; import BreakPageForPrint from '#components-ui/print-break-page'; import { IAPINotRespondingError } from '#models/api-not-responding'; import { IUniteLegale } from '#models/core/types'; -import { - IDataFetchingState, - isDataLoading, - isDataSuccess, - isUnauthorized, -} from '#models/data-fetching'; -import { IDirigeants } from '#models/rne/types'; +import { IDataFetchingState } from '#models/data-fetching'; +import { IDirigeantsWithMetadata } from '#models/rne/types'; +import { ApplicationRights, hasRights } from '#models/user/rights'; import { ISession } from '#models/user/session'; import { APIRoutesPaths } from 'app/api/data-fetching/routes-paths'; import { useAPIRouteData } from 'hooks/fetch/use-API-route-data'; import BeneficiairesSection from './beneficiaires'; -import RCSRNEComparison from './rcs-rne-comparison'; -import DirigeantsSection from './rne-dirigeants'; +import DirigeantsSection from './dirigeants-section'; import DirigeantSummary from './summary'; export type IDirigeantsFetching = - | IDirigeants + | IDirigeantsWithMetadata | IAPINotRespondingError | IDataFetchingState; -function mergeDirigeants( - dirigeantsRNE: IDirigeantsFetching, - dirigeantsRCS: IDirigeantsFetching -) { - if (isUnauthorized(dirigeantsRCS)) { - return { dirigeants: dirigeantsRNE, isProtected: false }; - } else { - if (isDataLoading(dirigeantsRCS) || isDataLoading(dirigeantsRNE)) { - return { dirigeants: IDataFetchingState.LOADING, isProtected: false }; - } - if (isDataSuccess(dirigeantsRCS)) { - return { dirigeants: dirigeantsRCS, isProtected: true }; - } - } - return { dirigeants: dirigeantsRNE, isProtected: false }; -} - export function DirigeantInformation({ uniteLegale, session, @@ -48,23 +26,15 @@ export function DirigeantInformation({ uniteLegale: IUniteLegale; session: ISession | null; }) { - const dirigeantsRNE = useAPIRouteData( - APIRoutesPaths.RneDirigeants, - uniteLegale.siren, - session - ); - - const mandatairesRCS = useAPIRouteData( - APIRoutesPaths.EspaceAgentRcsMandataires, + const isProtected = hasRights(session, ApplicationRights.mandatairesRCS); + const dirigeants = useAPIRouteData( + isProtected + ? APIRoutesPaths.EspaceAgentDirigeantsProtected + : APIRoutesPaths.RneDirigeants, uniteLegale.siren, session ); - const { dirigeants, isProtected } = mergeDirigeants( - dirigeantsRNE, - mandatairesRCS - ); - return ( <> @@ -72,13 +42,6 @@ export function DirigeantInformation({ uniteLegale={uniteLegale} dirigeants={dirigeants} isProtected={isProtected} - warning={ - - } /> diff --git a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rcs-rne-comparison.tsx b/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rcs-rne-comparison.tsx deleted file mode 100644 index ee305cc79..000000000 --- a/app/(header-default)/dirigeants/[slug]/_component/sections/entreprise/rcs-rne-comparison.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import routes from '#clients/routes'; -import { Warning } from '#components-ui/alerts'; -import { INPI } from '#components/administrations'; -import { IUniteLegale } from '#models/core/types'; -import { isDataSuccess } from '#models/data-fetching'; -import { IDirigeantsFetching } from '.'; - -function RCSRNEComparison({ - dirigeantsRNE, - dirigeantsRCS, - uniteLegale, -}: { - dirigeantsRNE: IDirigeantsFetching; - dirigeantsRCS: IDirigeantsFetching; - uniteLegale: IUniteLegale; -}) { - if (!isDataSuccess(dirigeantsRNE) || !isDataSuccess(dirigeantsRCS)) { - return null; - } else if (dirigeantsRNE.data.length === dirigeantsRCS.data.length) { - return null; - } - - return ( - - Les données d’Infogreffe sont issues du RNE mais il y a une différence - entre le nombre de dirigeant(s) retourné(s) par l’ - ({dirigeantsRNE.data.length}) et par Infogreffe ( - {dirigeantsRCS.data.length} - ). Pour comparer, vous pouvez consulter la page de cette entreprise sur{' '} - - data.inpi.fr - - . - - ); -} - -export default RCSRNEComparison; diff --git a/app/api/data-fetching/routes-handlers.ts b/app/api/data-fetching/routes-handlers.ts index 688735792..c5da3bffc 100644 --- a/app/api/data-fetching/routes-handlers.ts +++ b/app/api/data-fetching/routes-handlers.ts @@ -8,7 +8,7 @@ import { getOpqibi } from '#models/espace-agent/certificats/opqibi'; import { getQualibat } from '#models/espace-agent/certificats/qualibat'; import { getQualifelec } from '#models/espace-agent/certificats/qualifelec'; import { getConformiteEntreprise } from '#models/espace-agent/conformite'; -import { getMandatairesRCS } from '#models/espace-agent/mandataires-rcs'; +import { getDirigeantsProtected } from '#models/espace-agent/dirigeants-protected'; import { getDocumentsRNEProtected } from '#models/espace-agent/rne-protected/documents'; import { getDirigeantsRNE } from '#models/rne/dirigeants'; import { getRNEObservations } from '#models/rne/observations'; @@ -26,7 +26,7 @@ export const APIRoutesHandlers = { [APIRoutesPaths.EspaceAgentCnetp]: getCnetp, [APIRoutesPaths.EspaceAgentQualibat]: getQualibat, [APIRoutesPaths.EspaceAgentQualifelec]: getQualifelec, - [APIRoutesPaths.EspaceAgentRcsMandataires]: getMandatairesRCS, + [APIRoutesPaths.EspaceAgentDirigeantsProtected]: getDirigeantsProtected, [APIRoutesPaths.EspaceAgentBeneficiaires]: getBeneficiairesController, [APIRoutesPaths.EspaceAgentRneDocuments]: getDocumentsRNEProtected, [APIRoutesPaths.EspaceAgentAssociationProtected]: getAssociationProtected, diff --git a/app/api/data-fetching/routes-paths.ts b/app/api/data-fetching/routes-paths.ts index 3d91fb456..70c860f29 100644 --- a/app/api/data-fetching/routes-paths.ts +++ b/app/api/data-fetching/routes-paths.ts @@ -6,7 +6,7 @@ export enum APIRoutesPaths { EspaceAgentCnetp = 'espace-agent/cnetp', EspaceAgentQualibat = 'espace-agent/qualibat', EspaceAgentQualifelec = 'espace-agent/qualifelec', - EspaceAgentRcsMandataires = 'espace-agent/rcs-mandataires', + EspaceAgentDirigeantsProtected = 'espace-agent/dirigeants-protected', EspaceAgentBeneficiaires = 'espace-agent/beneficiaires', EspaceAgentRneDocuments = 'espace-agent/rne/documents', EspaceAgentAssociationProtected = 'espace-agent/association-protected', diff --git a/app/api/data-fetching/routes-scopes.ts b/app/api/data-fetching/routes-scopes.ts index 3481915da..fdb5b76cb 100644 --- a/app/api/data-fetching/routes-scopes.ts +++ b/app/api/data-fetching/routes-scopes.ts @@ -9,9 +9,10 @@ export const APIRoutesScopes: Record = { [APIRoutesPaths.EspaceAgentQualibat]: ApplicationRights.protectedCertificats, [APIRoutesPaths.EspaceAgentQualifelec]: ApplicationRights.protectedCertificats, - [APIRoutesPaths.EspaceAgentRcsMandataires]: ApplicationRights.mandatairesRCS, [APIRoutesPaths.EspaceAgentBeneficiaires]: ApplicationRights.beneficiaires, [APIRoutesPaths.EspaceAgentRneDocuments]: ApplicationRights.documentsRne, + [APIRoutesPaths.EspaceAgentDirigeantsProtected]: + ApplicationRights.mandatairesRCS, [APIRoutesPaths.EspaceAgentAssociationProtected]: ApplicationRights.associationProtected, [APIRoutesPaths.RneDirigeants]: ApplicationRights.opendata, diff --git a/clients/api-entreprise/mandataires-rcs/index.ts b/clients/api-entreprise/mandataires-rcs/index.ts index fe1c5b811..ba9417b0b 100644 --- a/clients/api-entreprise/mandataires-rcs/index.ts +++ b/clients/api-entreprise/mandataires-rcs/index.ts @@ -45,26 +45,24 @@ export const clientApiEntrepriseMandatairesRCS = async (siren: Siren) => { const mapToDomainObject = ( response: IAPIEntrepriseMandatairesRCS ): IDirigeants => { - return { - data: response.data.map(({ data: dirigeant }) => { - if (dirigeant.type === 'personne_physique') { - return { - sexe: null, - nom: dirigeant.nom, - prenom: dirigeant.prenom, - prenoms: dirigeant.prenom, - role: dirigeant.fonction, - lieuNaissance: dirigeant.lieu_naissance, - dateNaissance: dirigeant.date_naissance, - dateNaissancePartial: dirigeant.date_naissance?.slice(0, 7), - } as IEtatCivil; - } + return response.data.map(({ data: dirigeant }) => { + if (dirigeant.type === 'personne_physique') { return { - siren: dirigeant.numero_identification, - denomination: dirigeant.raison_sociale, - natureJuridique: null, + sexe: null, + nom: dirigeant.nom, + prenom: dirigeant.prenom, + prenoms: dirigeant.prenom, role: dirigeant.fonction, - } as IPersonneMorale; - }), - }; + lieuNaissance: dirigeant.lieu_naissance, + dateNaissance: dirigeant.date_naissance, + dateNaissancePartial: dirigeant.date_naissance?.slice(0, 7), + } as IEtatCivil; + } + return { + siren: dirigeant.numero_identification, + denomination: dirigeant.raison_sociale, + natureJuridique: null, + role: dirigeant.fonction, + } as IPersonneMorale; + }); }; diff --git a/clients/api-proxy/rne/index.ts b/clients/api-proxy/rne/index.ts index 35ac5609d..ff287af2a 100644 --- a/clients/api-proxy/rne/index.ts +++ b/clients/api-proxy/rne/index.ts @@ -20,8 +20,8 @@ type IRNEProxyResponse = { capital: string; libelleNatureJuridique: string; }; - observations: IObservations['data']; - dirigeants: IDirigeants['data']; + observations: IObservations; + dirigeants: IDirigeants; }; /** diff --git a/clients/recherche-entreprise/dirigeants.ts b/clients/recherche-entreprise/dirigeants.ts index d49ab18cb..c668cfe51 100644 --- a/clients/recherche-entreprise/dirigeants.ts +++ b/clients/recherche-entreprise/dirigeants.ts @@ -4,7 +4,7 @@ import clientSearchRechercheEntreprise from '.'; export const clientDirigeantsRechercheEntreprise = async ( siren: Siren -): Promise => { +): Promise => { const { results } = await clientSearchRechercheEntreprise({ searchTerms: siren, pageResultatsRecherche: 1, diff --git a/components/search-results/results-list.tsx b/components/search-results/results-list.tsx index b3e4ec992..a54704096 100644 --- a/components/search-results/results-list.tsx +++ b/components/search-results/results-list.tsx @@ -16,7 +16,7 @@ type IProps = { }; const DirigeantsOrElusList: React.FC<{ - dirigeantsOrElus: IDirigeants['data']; + dirigeantsOrElus: IDirigeants; }> = ({ dirigeantsOrElus }) => { const displayMax = 5; const firstFive = dirigeantsOrElus.slice(0, displayMax); diff --git a/models/espace-agent/dirigeants-protected.ts b/models/espace-agent/dirigeants-protected.ts new file mode 100644 index 000000000..9dbddd70f --- /dev/null +++ b/models/espace-agent/dirigeants-protected.ts @@ -0,0 +1,40 @@ +import { EAdministration } from '#models/administrations/EAdministration'; +import { + APINotRespondingFactory, + IAPINotRespondingError, +} from '#models/api-not-responding'; +import { hasAnyError, isDataSuccess } from '#models/data-fetching'; +import { getDirigeantsRNE } from '#models/rne/dirigeants'; +import { IDirigeantsWithMetadata } from '#models/rne/types'; +import { verifySiren } from '#utils/helpers'; +import { getMandatairesRCS } from './mandataires-rcs'; +import { mergeDirigeants } from './utils'; + +export const getDirigeantsProtected = async ( + maybeSiren: string +): Promise => { + const siren = verifySiren(maybeSiren); + + const [dirigeantsRCS, dirigeantsRNE] = await Promise.all([ + getMandatairesRCS(siren), + getDirigeantsRNE(siren), + ]); + + const dirigeantMerged = mergeDirigeants( + isDataSuccess(dirigeantsRCS) ? dirigeantsRCS : [], + isDataSuccess(dirigeantsRNE) ? dirigeantsRNE.data : [] + ); + + if (hasAnyError(dirigeantsRCS) && hasAnyError(dirigeantsRNE)) { + return APINotRespondingFactory(EAdministration.INPI, 404); + } + + return { + data: dirigeantMerged, + metadata: { + isFallback: isDataSuccess(dirigeantsRNE) + ? dirigeantsRNE?.metadata?.isFallback || false + : false, + }, + }; +}; diff --git a/models/espace-agent/mandataires-rcs.ts b/models/espace-agent/mandataires-rcs.ts index 662ab5c5f..86a37d9fd 100644 --- a/models/espace-agent/mandataires-rcs.ts +++ b/models/espace-agent/mandataires-rcs.ts @@ -14,14 +14,14 @@ export const getMandatairesRCS = async ( const siren = verifySiren(maybeSiren); try { const mandatairesRCS = await clientApiEntrepriseMandatairesRCS(siren); - if (mandatairesRCS.data.length === 0) { + if (mandatairesRCS.length === 0) { return APINotRespondingFactory(EAdministration.INFOGREFFE, 404); } return mandatairesRCS; } catch (error) { return handleApiEntrepriseError(error, { siren, - apiResource: 'MadatairesRCS', + apiResource: 'MandatairesRCS', }); } }; diff --git a/models/espace-agent/mergeDirigeants.test.ts b/models/espace-agent/mergeDirigeants.test.ts new file mode 100644 index 000000000..df2e7b2f6 --- /dev/null +++ b/models/espace-agent/mergeDirigeants.test.ts @@ -0,0 +1,275 @@ +import { IDirigeants } from '#models/rne/types'; +import { mergeDirigeants } from './utils'; + +describe('mergeDirigeants', () => { + it('same dirigeant, two roles, one source', () => { + const dirigeantsRCS: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'PRESIDENT', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'DIRECTEUR GENERAL', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const dirigeantsRNE: IDirigeants = []; + + const merged = mergeDirigeants(dirigeantsRCS, dirigeantsRNE); + + expect(merged).toHaveLength(1); + expect(merged[0]).toEqual( + expect.objectContaining({ isInIg: true, isInInpi: false }) + ); + expect(merged[0].roles).toEqual( + expect.arrayContaining([ + { label: 'PRESIDENT', isInIg: true, isInInpi: false }, + { label: 'DIRECTEUR GENERAL', isInIg: true, isInInpi: false }, + ]) + ); + }); + + it('same dirigeant, two roles, two sources', () => { + const dirigeantsRCS: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'PRESIDENT', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const dirigeantsRNE: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'DIRECTEUR GENERAL', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const merged = mergeDirigeants(dirigeantsRCS, dirigeantsRNE); + + expect(merged).toHaveLength(1); + expect(merged[0]).toEqual( + expect.objectContaining({ isInIg: true, isInInpi: true }) + ); + expect(merged[0].roles).toEqual( + expect.arrayContaining([ + { label: 'PRESIDENT', isInIg: true, isInInpi: false }, + { label: 'DIRECTEUR GENERAL', isInIg: false, isInInpi: true }, + ]) + ); + }); + + it('same dirigeant, same role, one source', () => { + const dirigeantsRCS: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'Président', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'PRESIDENT', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const dirigeantsRNE: IDirigeants = []; + + const merged = mergeDirigeants(dirigeantsRCS, dirigeantsRNE); + + expect(merged).toHaveLength(1); + expect(merged[0]).toEqual( + expect.objectContaining({ isInIg: true, isInInpi: false }) + ); + expect(merged[0].roles).toEqual( + expect.arrayContaining([ + { label: 'PRESIDENT', isInIg: true, isInInpi: false }, + ]) + ); + }); + + it('same dirigeant, same role, two sources', () => { + const dirigeantsRCS: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'Président', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const dirigeantsRNE: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'PRESIDENT', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const merged = mergeDirigeants(dirigeantsRCS, dirigeantsRNE); + + expect(merged).toHaveLength(1); + expect(merged[0]).toEqual( + expect.objectContaining({ isInIg: true, isInInpi: true }) + ); + expect(merged[0].roles).toEqual( + expect.arrayContaining([ + { label: 'PRESIDENT', isInIg: true, isInInpi: true }, + ]) + ); + }); + + it('two dirigeants (one company and one person), two sources', () => { + const dirigeantsRCS: IDirigeants = [ + { + siren: '123456789', + denomination: 'Company A', + natureJuridique: 'SARL', + role: 'DIRECTEUR GENERAL', + }, + ]; + + const dirigeantsRNE: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'PRESIDENT', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const merged = mergeDirigeants(dirigeantsRCS, dirigeantsRNE); + + expect(merged).toHaveLength(2); + expect(merged).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + siren: '123456789', + roles: [ + { label: 'DIRECTEUR GENERAL', isInIg: true, isInInpi: false }, + ], + }), + expect.objectContaining({ + nom: 'Doe', + prenom: 'John', + roles: [{ label: 'PRESIDENT', isInIg: false, isInInpi: true }], + }), + ]) + ); + }); + + it('two dirigeants (two persons), two sources', () => { + const dirigeantsRCS: IDirigeants = [ + { + sexe: 'F', + nom: 'Smith', + prenom: 'Jane', + prenoms: 'Jane', + role: 'CTO', + lieuNaissance: 'Lyon', + dateNaissance: '1990-05-15', + }, + ]; + + const dirigeantsRNE: IDirigeants = [ + { + sexe: 'M', + nom: 'Doe', + prenom: 'John', + prenoms: 'John', + role: 'CEO', + lieuNaissance: 'Paris', + dateNaissance: '1980-01-01', + }, + ]; + + const merged = mergeDirigeants(dirigeantsRCS, dirigeantsRNE); + + expect(merged).toHaveLength(2); + expect(merged).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + nom: 'Smith', + prenom: 'Jane', + roles: [{ label: 'CTO', isInIg: true, isInInpi: false }], + }), + expect.objectContaining({ + nom: 'Doe', + prenom: 'John', + roles: [{ label: 'CEO', isInIg: false, isInInpi: true }], + }), + ]) + ); + }); + + it('should not add duplicate roles for the same dirigeant', () => { + const dirigeantsRCS: IDirigeants = [ + { + sexe: 'M', + nom: 'Brown', + prenom: 'Mike', + prenoms: 'Mike', + role: 'DG', + lieuNaissance: 'Marseille', + dateNaissance: '1975-07-20', + }, + { + sexe: 'M', + nom: 'Brown', + prenom: 'Mike', + prenoms: 'Mike', + role: 'DG', + lieuNaissance: 'Marseille', + dateNaissance: '1975-07-20', + }, + ]; + + const merged = mergeDirigeants(dirigeantsRCS, []); + + expect(merged).toHaveLength(1); + expect(merged[0].roles).toHaveLength(1); + expect(merged[0].roles![0]).toEqual({ + label: 'DG', + isInIg: true, + isInInpi: false, + }); + }); +}); diff --git a/models/espace-agent/utils.ts b/models/espace-agent/utils.ts index 23e156b93..56ab648fd 100644 --- a/models/espace-agent/utils.ts +++ b/models/espace-agent/utils.ts @@ -2,6 +2,13 @@ import { HttpNotFound } from '#clients/exceptions'; import { EAdministration } from '#models/administrations/EAdministration'; import { APINotRespondingFactory } from '#models/api-not-responding'; import { FetchRessourceException, IExceptionContext } from '#models/exceptions'; +import { + IDirigeants, + IEtatCivil, + IPersonneMorale, + IRole, +} from '#models/rne/types'; +import { removeSpecialChars } from '#utils/helpers'; import logErrorInSentry from '#utils/sentry'; export function handleApiEntrepriseError( @@ -22,3 +29,67 @@ export function handleApiEntrepriseError( ); return APINotRespondingFactory(EAdministration.DINUM, e.status || 500); } + +export const mergeDirigeants = ( + dirigeantsRCS: IDirigeants, + dirigeantsRNE: IDirigeants +): IDirigeants => { + const mergedDirigeants: Record = {}; + const mergedRoles: Record> = {}; + + const createUniqueKey = (dirigeant: IEtatCivil | IPersonneMorale): string => { + if ('siren' in dirigeant) { + return `pm-${dirigeant.siren}`; + } else { + const cleanedPrenom = removeSpecialChars(dirigeant.prenom).toUpperCase(); + const cleanedNom = removeSpecialChars(dirigeant.nom).toUpperCase(); + const partialDate = + dirigeant.dateNaissancePartial || + dirigeant.dateNaissance?.slice(0, 7) || + ''; + return `pf-${cleanedPrenom}-${cleanedNom}-${partialDate}`; + } + }; + + const dirigeants = [ + ...dirigeantsRCS.map((d) => ({ ...d, isInIg: true, isInInpi: false })), + ...dirigeantsRNE.map((d) => ({ ...d, isInIg: false, isInInpi: true })), + ]; + for (const dirigeant of dirigeants) { + const { isInInpi, isInIg, role } = dirigeant; + const currentDirigeantKey = createUniqueKey(dirigeant); + + const foundDirigeant = mergedDirigeants[currentDirigeantKey]; + if (!foundDirigeant) { + mergedDirigeants[currentDirigeantKey] = { + ...dirigeant, + isInInpi, + isInIg, + }; + mergedRoles[currentDirigeantKey] = {}; + } else if (isInInpi) { + foundDirigeant.isInInpi = true; + } else if (isInIg) { + foundDirigeant.isInIg = true; + } + + const cleanedRole = removeSpecialChars(role).toUpperCase(); + const foundCleanedRole = mergedRoles[currentDirigeantKey][cleanedRole]; + if (!foundCleanedRole) { + mergedRoles[currentDirigeantKey][cleanedRole] = { + label: cleanedRole, + isInInpi, + isInIg, + }; + } else if (isInInpi) { + foundCleanedRole.isInInpi = true; + } else if (isInIg) { + foundCleanedRole.isInIg = true; + } + } + + return Object.values(mergedDirigeants).map((dirigeant) => ({ + ...dirigeant, + roles: Object.values(mergedRoles[createUniqueKey(dirigeant)]), + })); +}; diff --git a/models/rne/dirigeants.tsx b/models/rne/dirigeants.tsx index 814838b6f..58ad397fd 100644 --- a/models/rne/dirigeants.tsx +++ b/models/rne/dirigeants.tsx @@ -7,7 +7,7 @@ import { IAPINotRespondingError, } from '#models/api-not-responding'; import { verifySiren } from '#utils/helpers'; -import { IDirigeants } from './types'; +import { IDirigeantsWithMetadata } from './types'; /* * Request dirigeants from INPI's RNE @@ -15,7 +15,7 @@ import { IDirigeants } from './types'; */ export const getDirigeantsRNE = async ( maybeSiren: string -): Promise => { +): Promise => { const siren = verifySiren(maybeSiren); try { diff --git a/models/rne/observations.ts b/models/rne/observations.ts index 0a885711f..34668e86d 100644 --- a/models/rne/observations.ts +++ b/models/rne/observations.ts @@ -9,7 +9,7 @@ import { IAPINotRespondingError, } from '#models/api-not-responding'; import { verifySiren } from '#utils/helpers'; -import { IObservations } from './types'; +import { IObservationsWithMetadata } from './types'; /* * Request observations from INPI's RNE @@ -17,7 +17,7 @@ import { IObservations } from './types'; */ export const getRNEObservations = async ( maybeSiren: string -): Promise => { +): Promise => { const siren = verifySiren(maybeSiren); try { diff --git a/models/rne/types.ts b/models/rne/types.ts index 71d6abd16..7d3c91d35 100644 --- a/models/rne/types.ts +++ b/models/rne/types.ts @@ -23,10 +23,13 @@ export interface IEtatCivil { prenom: string; prenoms: string; role: string; + roles?: IRole[]; lieuNaissance: string; dateNaissancePartial?: string; dateNaissance?: string; nationalite?: string; + isInInpi?: boolean; + isInIg?: boolean; } export interface IPersonneMorale { @@ -34,22 +37,35 @@ export interface IPersonneMorale { denomination: string; natureJuridique: string | null; role: string; + roles?: IRole[]; + isInInpi?: boolean; + isInIg?: boolean; } -export interface IObservations { - data: { - numObservation: string; - dateAjout: string; - description: string; - }[]; +export type IObservations = { + numObservation: string; + dateAjout: string; + description: string; +}[]; + +export interface IObservationsWithMetadata { + data: IObservations; metadata: { isFallback: boolean; }; } -export interface IDirigeants { - data: (IEtatCivil | IPersonneMorale)[]; +export type IDirigeants = (IEtatCivil | IPersonneMorale)[]; + +export interface IDirigeantsWithMetadata { + data: IDirigeants; metadata?: { isFallback: boolean; }; } + +export interface IRole { + label: string; + isInInpi?: boolean; + isInIg?: boolean; +} diff --git a/models/search/index.ts b/models/search/index.ts index 206a98b15..d0786339e 100644 --- a/models/search/index.ts +++ b/models/search/index.ts @@ -23,7 +23,7 @@ export interface ISearchResult extends IUniteLegale { nombreEtablissementsOuverts: number; chemin: string; matchingEtablissements: IEtablissement[]; - dirigeants: IDirigeants['data']; + dirigeants: IDirigeants; } export interface ISearchResults {