From 269ca6fbcc9d132ea420ff8707058ba7c01819c6 Mon Sep 17 00:00:00 2001 From: Jalil Arfaoui Date: Mon, 15 Apr 2024 17:49:23 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20passe=20=C3=A0=20l'API=20de=20recherche?= =?UTF-8?q?=20d=E2=80=99entreprises=20de=20api.gouv.fr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mon-entreprise/english/navigation.ts | 9 +- .../integration/mon-entreprise/landing.ts | 9 +- site/netlify.base.toml | 16 +++- .../RechercheEntreprisesGouvFr.ts | 87 +++++++++++++++++++ .../RechercheEntreprise/fabrique-social.ts | 1 - .../components/company/SearchDetails.tsx | 2 +- site/source/contexts/RepositoriesContext.ts | 4 +- site/source/domain/Adresse.ts | 1 - site/source/domain/Date.ts | 9 ++ site/source/locales/ui-en.yaml | 3 +- site/source/locales/ui-fr.yaml | 3 +- site/test/fabrique-social.fixtures.ts | 2 +- site/test/fabrique-social.test.ts | 12 +-- 13 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 site/source/api/RechercheEntreprise/RechercheEntreprisesGouvFr.ts diff --git a/site/cypress/integration/mon-entreprise/english/navigation.ts b/site/cypress/integration/mon-entreprise/english/navigation.ts index 8773aa9b7..e5279d38a 100644 --- a/site/cypress/integration/mon-entreprise/english/navigation.ts +++ b/site/cypress/integration/mon-entreprise/english/navigation.ts @@ -36,6 +36,7 @@ describe(`Navigation to income simulator using company name (${ let pendingRequests = new Set() let responses = {} const hostnamesToRecord = [ + 'recherche-entreprises.api.gouv.fr', 'api.recherche-entreprises.fabrique.social.gouv.fr', 'geo.api.gouv.fr', ] @@ -64,8 +65,8 @@ describe(`Navigation to income simulator using company name (${ it('should allow to retrieve company and show link corresponding to the legal status', function () { cy.intercept({ method: 'GET', - hostname: 'api.recherche-entreprises.fabrique.social.gouv.fr', - url: '/api/v1/search*', + hostname: 'recherche-entreprises.api.gouv.fr', + url: '/search?q=*', }).as('search') cy.get('input[data-test-id="company-search-input"]').first().type('menoz') @@ -93,8 +94,8 @@ describe(`Navigation to income simulator using company name (${ it('should allow auto entrepreneur to access the corresponding income simulator', function () { cy.intercept({ method: 'GET', - hostname: 'api.recherche-entreprises.fabrique.social.gouv.fr', - url: '/api/v1/search*', + hostname: 'recherche-entreprises.api.gouv.fr', + url: '/search?q=*', }).as('search') cy.get('input[data-test-id="company-search-input"]').type('jeremy rialland') diff --git a/site/cypress/integration/mon-entreprise/landing.ts b/site/cypress/integration/mon-entreprise/landing.ts index 0536324b9..ce0b4d34c 100644 --- a/site/cypress/integration/mon-entreprise/landing.ts +++ b/site/cypress/integration/mon-entreprise/landing.ts @@ -21,6 +21,7 @@ describe('Landing page', function () { let pendingRequests = new Set() let responses = {} const hostnamesToRecord = [ + 'recherche-entreprises.api.gouv.fr', 'api.recherche-entreprises.fabrique.social.gouv.fr', 'geo.api.gouv.fr', ] @@ -41,13 +42,13 @@ describe('Landing page', function () { cy.get(searchInputPath).should('have.attr', 'placeholder') cy.get(searchInputPath).invoke('attr', 'type').should('equal', 'search') - cy.get(searchInputPath).first().type('noima') + cy.get(searchInputPath).first().type('menoz') cy.intercept( { method: 'GET', - hostname: 'api.recherche-entreprises.fabrique.social.gouv.fr', - url: '/api/v1/search?*', + hostname: 'recherche-entreprises.api.gouv.fr', + url: '/search?q=*', }, (req) => { req.responseTimeout = 10 * 1000 @@ -57,7 +58,7 @@ describe('Landing page', function () { cy.wait('@search') - cy.get(searchResultsPath).children().should('have.length', 6) + cy.get(searchResultsPath).children().should('have.length.at.least', 4) cy.get(searchResultsPath).children().first().click() cy.url().should('include', '/pour-mon-entreprise') diff --git a/site/netlify.base.toml b/site/netlify.base.toml index b8ad4e2a8..f0e32f1ec 100644 --- a/site/netlify.base.toml +++ b/site/netlify.base.toml @@ -1,7 +1,15 @@ [[headers]] for = "/*" [headers.values] -Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self' 'unsafe-inline' mon-entreprise.zammad.com; connect-src 'self' *.incubateur.net raw.githubusercontent.com tm.urssaf.fr mon-entreprise.zammad.com api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; form-action 'self' *.sibforms.com *.incubateur.net mon-entreprise.zammad.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com mon-entreprise.zammad.com polyfill.io; img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com jedonnemonavis.numerique.gouv.fr; frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com" +Content-Security-Policy = """\ + default-src 'self' mon-entreprise.fr; \ + style-src 'self' 'unsafe-inline' mon-entreprise.zammad.com; \ + connect-src 'self' *.incubateur.net raw.githubusercontent.com tm.urssaf.fr mon-entreprise.zammad.com recherche-entreprises.api.gouv.fr api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; \ + form-action 'self' *.sibforms.com *.incubateur.net mon-entreprise.zammad.com; \ + script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com mon-entreprise.zammad.com polyfill.io; \ + img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com jedonnemonavis.numerique.gouv.fr; \ + frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com \ + """ [dev] autoLaunch = false @@ -33,7 +41,7 @@ Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self ############ # Redirects following architectural changes -# DO NOT MOVE THIS SECTION ! ORDER MATTERS IN REDIRECTS ! +# DO NOT MOVE THIS SECTION ! ORDER MATTERS IN REDIRECTS ! # :SITE_ is a placeholder replaced before deploy (depends on the environment) @@ -80,7 +88,7 @@ Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self from=":SITE_FR/simulateurs/%C3%A9conomie-collaborative/*" to=":SITE_FR/assistants/%C3%A9conomie-collaborative/:splat" status = 301 - + ## Older changes # FR | coronavirus -> simulateurs/chômage-partiel @@ -163,7 +171,7 @@ Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self #################### # Redirect following huge refacto in modele-social -# DO NOT MOVE THIS SECTION ! ORDER MATTERS IN REDIRECTS ! +# DO NOT MOVE THIS SECTION ! ORDER MATTERS IN REDIRECTS ! ##################"" diff --git a/site/source/api/RechercheEntreprise/RechercheEntreprisesGouvFr.ts b/site/source/api/RechercheEntreprise/RechercheEntreprisesGouvFr.ts new file mode 100644 index 000000000..cb62d3f67 --- /dev/null +++ b/site/source/api/RechercheEntreprise/RechercheEntreprisesGouvFr.ts @@ -0,0 +1,87 @@ +import { CodeActivite } from '@/domain/CodeActivite' +import { CodeCatégorieJuridique } from '@/domain/CodeCatégorieJuridique' +import { IsoDate, parseIsoDateString } from '@/domain/Date' +import { Entreprise } from '@/domain/Entreprise' +import { EntreprisesRepository } from '@/domain/EntreprisesRepository' +import { Établissement } from '@/domain/Établissement' +import { Siren, Siret } from '@/domain/Siren' + +/** + * @see https://api.gouv.fr/documentation/api-recherche-entreprises + */ +export const RechercheEntreprisesGouvFr: EntreprisesRepository = { + rechercheTexteLibre, +} + +const makeSearchUrl = (q: string, limit: number) => + `https://recherche-entreprises.api.gouv.fr/search?q=${q}&etat_administratif=A&per_page=${limit}` + +async function rechercheTexteLibre(text: string, limit = 10) { + const response = await fetch(makeSearchUrl(text, limit)) + + if (!response.ok) { + return null + } + + const json = (await response.json()) as ApiResponse + + return json.results.map(EntrepriseFromApiAdapter) +} + +interface ApiResponse { + results: Array + total_results: number + page: number + per_page: number + total_pages: number +} + +interface EntrepriseApi { + siren: Siren + nom_complet: string + nom_raison_sociale: string + sigle: string + date_creation: IsoDate + nature_juridique: CodeCatégorieJuridique + activite_principale: CodeActivite + siege: EtablissementApi + matching_etablissements: Array + nombre_etablissements: number + nombre_etablissements_ouverts: number +} + +const EntrepriseFromApiAdapter = (api: EntrepriseApi): Entreprise => { + const siège = ÉtablissementFromApiAdapter(api.siege) + const établissement = api.matching_etablissements.length + ? ÉtablissementFromApiAdapter(api.matching_etablissements[0]) + : siège + + return { + nom: api.nom_complet, + siren: api.siren, + dateDeCréation: parseIsoDateString(api.date_creation), + siège, + établissement, + activitéPrincipale: api.activite_principale, + codeCatégorieJuridique: api.nature_juridique, + } +} + +interface EtablissementApi { + siret: Siret + adresse: string + commune: string + libelle_commune: string + activite_principale: CodeActivite + est_siege: boolean + nom_commercial: string +} + +const ÉtablissementFromApiAdapter = (api: EtablissementApi): Établissement => ({ + siret: api.siret, + adresse: { + complète: api.adresse, + codeCommune: api.commune, + }, + activitéPrincipale: api.activite_principale, +}) diff --git a/site/source/api/RechercheEntreprise/fabrique-social.ts b/site/source/api/RechercheEntreprise/fabrique-social.ts index a6c93ce8e..28e8f3953 100644 --- a/site/source/api/RechercheEntreprise/fabrique-social.ts +++ b/site/source/api/RechercheEntreprise/fabrique-social.ts @@ -44,7 +44,6 @@ const établissementAdapter = ( ), adresse: { complète: fabriqueSocialEtablissement.address, - codePostal: fabriqueSocialEtablissement.codePostalEtablissement, codeCommune: fabriqueSocialEtablissement.codeCommuneEtablissement, }, }) diff --git a/site/source/components/company/SearchDetails.tsx b/site/source/components/company/SearchDetails.tsx index d5ef25ab1..a90cc8340 100644 --- a/site/source/components/company/SearchDetails.tsx +++ b/site/source/components/company/SearchDetails.tsx @@ -51,7 +51,7 @@ export default function EntrepriseSearchDetails({ )}
- Établissement recherché:{' '} + Établissement recherché :{' '} {établissement?.adresse.complète} ) diff --git a/site/source/contexts/RepositoriesContext.ts b/site/source/contexts/RepositoriesContext.ts index e74891da5..7b131b2f0 100644 --- a/site/source/contexts/RepositoriesContext.ts +++ b/site/source/contexts/RepositoriesContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react' -import { FabriqueSocialEntreprisesRepository } from '@/api/RechercheEntreprise/fabrique-social' +import { RechercheEntreprisesGouvFr } from '@/api/RechercheEntreprise/RechercheEntreprisesGouvFr' import { EntreprisesRepository } from '@/domain/EntreprisesRepository' interface Repositories { @@ -8,5 +8,5 @@ interface Repositories { } export const RepositoriesContext = createContext({ - entreprises: FabriqueSocialEntreprisesRepository, + entreprises: RechercheEntreprisesGouvFr, }) diff --git a/site/source/domain/Adresse.ts b/site/source/domain/Adresse.ts index e728b1208..b2ca9b512 100644 --- a/site/source/domain/Adresse.ts +++ b/site/source/domain/Adresse.ts @@ -1,5 +1,4 @@ export interface Adresse { complète?: string - codePostal: string codeCommune: string } diff --git a/site/source/domain/Date.ts b/site/source/domain/Date.ts index 963840b66..e8299eb7d 100644 --- a/site/source/domain/Date.ts +++ b/site/source/domain/Date.ts @@ -4,6 +4,13 @@ export type PublicodeDate = `${number}${number}/${number}${number}/${number}${number}${number}${number}` export const publicodesStandardDateFormat = 'dd/MM/yyyy' +/** + * @example 2024-12-31 + */ +export type IsoDate = + `${number}${number}${number}${number}-${number}${number}-${number}${number}` +export const isoDateFormat = 'yyyy-MM-dd' + type DateToPublicodeDate = (d: Date) => PublicodeDate export const toPublicodeDate = format( @@ -16,3 +23,5 @@ export const parsePublicodesDateString = parse( new Date(), publicodesStandardDateFormat ) as PublicodeDateToDate + +export const parseIsoDateString = parse(new Date(), isoDateFormat) diff --git a/site/source/locales/ui-en.yaml b/site/source/locales/ui-en.yaml index 100e8812f..494d676b4 100644 --- a/site/source/locales/ui-en.yaml +++ b/site/source/locales/ui-en.yaml @@ -86,7 +86,6 @@ Coût de création: Cost of creation Dividendes nets: Net dividends Documentation: Documentation Documentation des simulateurs: Simulator documentation -"Domiciliée à l'adresse :": "Domiciled at address :" Donner votre avis: Give your opinion Décrivez votre projet ou votre problème en donnant quelques éléments de contexte. Notre partenaire Place des Entreprises identifiera, parmi l’ensemble des partenaires publics et parapublics, le conseiller compétent pour votre demande. Celui-ci vous contactera par téléphone sous 5 jours et vous accompagnera en fonction de votre situation.: Describe your project or problem and provide some background information. Our @@ -285,6 +284,7 @@ Simulateurs: Simulators Simulateurs et Assistants: Simulators and Assistants Simulation en cours: Simulation in progress Situation personnelle: Personal situation +"Siège :": "Headquarters:" Soit <2><0> avant impôts: Or <2><0> before tax Statistiques: Statistics Statut du conjoint: Spouse's status @@ -1721,6 +1721,7 @@ warning: À quoi servent mes cotisations ?: What are my contributions used for? Échanger avec un conseiller: Talk to an advisor Échec: Failure +"Établissement recherché :": "Establishment sought :" Étape complétée: Step completed Étape non complétée: Step not completed Étape {{ total }} sur {{ maxValue }}: Step {{ total }} on {{ maxValue }} diff --git a/site/source/locales/ui-fr.yaml b/site/source/locales/ui-fr.yaml index 079a76d28..b05bf2c0c 100644 --- a/site/source/locales/ui-fr.yaml +++ b/site/source/locales/ui-fr.yaml @@ -91,7 +91,6 @@ Coût de création: Coût de création Dividendes nets: Dividendes nets Documentation: Documentation Documentation des simulateurs: Documentation des simulateurs -"Domiciliée à l'adresse :": "Domiciliée à l'adresse :" Donner votre avis: Donner votre avis Décrivez votre projet ou votre problème en donnant quelques éléments de contexte. Notre partenaire Place des Entreprises identifiera, parmi l’ensemble des partenaires publics et parapublics, le conseiller compétent pour votre demande. Celui-ci vous contactera par téléphone sous 5 jours et vous accompagnera en fonction de votre situation.: Décrivez votre projet ou votre problème en donnant quelques éléments de @@ -299,6 +298,7 @@ Simulateurs: Simulateurs Simulateurs et Assistants: Simulateurs et Assistants Simulation en cours: Simulation en cours Situation personnelle: Situation personnelle +"Siège :": "Siège :" Soit <2><0> avant impôts: Soit <2><0> avant impôts Statistiques: Statistiques Statut du conjoint: Statut du conjoint @@ -1827,6 +1827,7 @@ warning: À quoi servent mes cotisations ?: À quoi servent mes cotisations ? Échanger avec un conseiller: Échanger avec un conseiller Échec: Échec +"Établissement recherché :": "Établissement recherché :" Étape complétée: Étape complétée Étape non complétée: Étape non complétée Étape {{ total }} sur {{ maxValue }}: Étape {{ total }} sur {{ maxValue }} diff --git a/site/test/fabrique-social.fixtures.ts b/site/test/fabrique-social.fixtures.ts index 13dd04b3c..6914bcd59 100644 --- a/site/test/fabrique-social.fixtures.ts +++ b/site/test/fabrique-social.fixtures.ts @@ -1,4 +1,4 @@ -import { FabriqueSocialEntreprise } from '@/api/fabrique-social' +import { FabriqueSocialEntreprise } from '@/api/RechercheEntreprise/fabrique-social' export const fabriqueSocialWithSiege: FabriqueSocialEntreprise = { activitePrincipale: 'Agences immobilières', diff --git a/site/test/fabrique-social.test.ts b/site/test/fabrique-social.test.ts index c0cba7dc5..d26ff5e60 100644 --- a/site/test/fabrique-social.test.ts +++ b/site/test/fabrique-social.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { fabriqueSocialEntrepriseAdapter } from '@/api/fabrique-social' +import { fabriqueSocialEntrepriseAdapter } from '@/api/RechercheEntreprise/fabrique-social' import { fabriqueSocialWithoutSiege, @@ -15,16 +15,16 @@ describe('Fabrique Social', () => { ) it('retourne le siren', () => { - expect(entreprise.siren).to.equal('849074190') + expect(entreprise.siren).toBe('849074190') }) it("a l'établissement demandé dans 'établissement'", () => { - expect(entreprise.siège?.adresse.complète).to.equal( + expect(entreprise.siège?.adresse.complète).toBe( '23 RUE DE MOGADOR 75009 PARIS 9' ) }) it("a le siège dans 'siège'", () => { - expect(entreprise.établissement.adresse.complète).to.equal( + expect(entreprise.établissement.adresse.complète).toBe( '4 RUE VOLTAIRE 44000 NANTES' ) }) @@ -35,10 +35,10 @@ describe('Fabrique Social', () => { ) it("n'a pas de siège", () => { - expect(entreprise.siège?.adresse.complète).to.equal(undefined) + expect(entreprise.siège?.adresse.complète).toBe(undefined) }) it('a l’établissement demandé', () => { - expect(entreprise.établissement.adresse.complète).to.equal( + expect(entreprise.établissement.adresse.complète).toBe( '4 RUE VOLTAIRE 44000 NANTES' ) })