Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utilise api recherche entreprises gouv.fr #2929

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions site/cypress/integration/mon-entreprise/english/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
9 changes: 5 additions & 4 deletions site/cypress/integration/mon-entreprise/landing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
Expand All @@ -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
Expand All @@ -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')
Expand Down
16 changes: 12 additions & 4 deletions site/netlify.base.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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_<name> is a placeholder replaced before deploy (depends on the environment)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 !
##################""


Expand Down
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@sentry/integrations": "^7.70.0",
"@sentry/react": "^7.70.0",
"algoliasearch": "^4.20.0",
"date-fns": "^2.30.0",
"date-fns": "^3.6.0",
"exoneration-covid": "workspace:^",
"focus-trap-react": "^10.2.1",
"fuse.js": "^6.6.2",
Expand Down
87 changes: 87 additions & 0 deletions site/source/api/RechercheEntreprise/RechercheEntreprisesGouvFr.ts
Original file line number Diff line number Diff line change
@@ -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<EntrepriseApi>
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<EtablissementApi>
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,
})
120 changes: 120 additions & 0 deletions site/source/api/RechercheEntreprise/fabrique-social.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { codeActivité } from '@/domain/CodeActivite'
import { codeCatégorieJuridique } from '@/domain/CodeCatégorieJuridique'
import { Entreprise } from '@/domain/Entreprise'
import { EntreprisesRepository } from '@/domain/EntreprisesRepository'
import { Établissement } from '@/domain/Établissement'
import { siren, siret } from '@/domain/Siren'

export const FabriqueSocialEntreprisesRepository: EntreprisesRepository = {
rechercheTexteLibre: searchDenominationOrSiren,
}

async function searchDenominationOrSiren(
searchTerm: string
): Promise<Array<Entreprise> | null> {
return searchFullText(searchTerm).then(
(entreprises) => entreprises?.map(fabriqueSocialEntrepriseAdapter) || null
)
}

export const fabriqueSocialEntrepriseAdapter = (
entreprise: FabriqueSocialEntreprise
): Entreprise => {
const siège = entreprise && getSiege(entreprise)

return {
nom: entreprise.label,
siren: siren(entreprise.siren),
dateDeCréation: new Date(entreprise.dateCreationUniteLegale),
codeCatégorieJuridique: codeCatégorieJuridique(
entreprise.categorieJuridiqueUniteLegale
),
activitéPrincipale: codeActivité(entreprise.activitePrincipale),
siège: siège && établissementAdapter(siège),
établissement: établissementAdapter(entreprise.firstMatchingEtablissement),
}
}

const établissementAdapter = (
fabriqueSocialEtablissement: FabriqueSocialEtablissement
): Établissement => ({
siret: siret(fabriqueSocialEtablissement.siret),
activitéPrincipale: codeActivité(
fabriqueSocialEtablissement.activitePrincipaleEtablissement
),
adresse: {
complète: fabriqueSocialEtablissement.address,
codeCommune: fabriqueSocialEtablissement.codeCommuneEtablissement,
},
})

/*
* Fields are documented in https://www.sirene.fr/static-resources/doc/Description%20fichier%20StockUniteLegaleHistorique.pdf?version=1.33.1
*/
export type FabriqueSocialEntreprise = {
activitePrincipale: string
caractereEmployeurUniteLegale?: 'N' | 'O'
categorieJuridiqueUniteLegale: string
dateCreationUniteLegale: string
conventions: Array<{
idcc: number
shortTitle: string
etat: string
id: string
texte_de_base: string
title: string
url: string
}>
etablissements: number
etatAdministratifUniteLegale: 'A' | 'C' // A: Active, C: Cessée
highlightLabel: string
label: string
simpleLabel: string
siren: string
firstMatchingEtablissement: FabriqueSocialEtablissement
allMatchingEtablissements: Array<FabriqueSocialEtablissement>
}

export type FabriqueSocialEtablissement = {
address?: string
siret: string
etatAdministratifEtablissement?: 'F' | 'A' // Fermé ou Actif
codeCommuneEtablissement: string
codePostalEtablissement: string
etablissementSiege: boolean
activitePrincipaleEtablissement: string
}

type FabriqueSocialSearchPayload = {
entreprises: Array<FabriqueSocialEntreprise>
}

const COMPANY_SEARCH_HOST =
import.meta.env.VITE_COMPANY_SEARCH_HOST ||
'https://api.recherche-entreprises.fabrique.social.gouv.fr'

const makeSearchUrl = (query: string, limit: number) =>
`${COMPANY_SEARCH_HOST}/api/v1/search?query=${query}&open=true&convention=false&employer=false&ranked=false&limit=${limit}`

async function searchFullText(
text: string,
limit = 10
): Promise<Array<FabriqueSocialEntreprise> | null> {
const response = await fetch(makeSearchUrl(text, limit))

if (!response.ok) {
return null
}

const json = (await response.json()) as FabriqueSocialSearchPayload

return json.entreprises
}

function getSiege(
entreprise: FabriqueSocialEntreprise
): FabriqueSocialEtablissement | undefined {
return entreprise.allMatchingEtablissements.find(
(etablissement) => etablissement.etablissementSiege
)
}
Loading
Loading