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

refactor: introduit des types du domain pour découpler notre code de l'API Fabrique Social #2895

Closed
Show file tree
Hide file tree
Changes from 1 commit
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
56 changes: 48 additions & 8 deletions site/source/api/fabrique-social.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,49 @@
import { Company } from '@/store/reducers/companySituationReducer'
import { codeActivité } from '@/domain/CodeActivite'
import { codeCatégorieJuridique } from '@/domain/CodeCatégorieJuridique'
import { Entreprise } from '@/domain/Entreprise'
import { Établissement } from '@/domain/Établissement'
import { siren, siret } from '@/domain/Siren'

export async function searchDenominationOrSiren(value: string) {
return searchFullText(value)
export 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,
codePostal: fabriqueSocialEtablissement.codePostalEtablissement,
codeCommune: fabriqueSocialEtablissement.codeCommuneEtablissement,
},
})

/*
* Fields are documented in https://www.sirene.fr/static-resources/doc/Description%20fichier%20StockUniteLegaleHistorique.pdf?version=1.33.1
*/
Expand Down Expand Up @@ -67,10 +107,10 @@ async function searchFullText(
return json.entreprises
}

export function getSiegeOrFirstEtablissement(
entreprise: FabriqueSocialEntreprise | Company
): FabriqueSocialEtablissement {
return (entreprise.allMatchingEtablissements.find(
function getSiege(
entreprise: FabriqueSocialEntreprise
): FabriqueSocialEtablissement | undefined {
return entreprise.allMatchingEtablissements.find(
(etablissement) => etablissement.etablissementSiege
) || entreprise.firstMatchingEtablissement)!
)
}
62 changes: 18 additions & 44 deletions site/source/components/company/SearchDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { Fragment, useMemo } from 'react'
import { useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'

import {
FabriqueSocialEntreprise,
getSiegeOrFirstEtablissement,
} from '@/api/fabrique-social'
import { Spacing } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
import { H4 } from '@/design-system/typography/heading'
import {
Entreprise,
établissementEstDifférentDuSiège,
} from '@/domain/Entreprise'

export default function CompanySearchDetails({
export default function EntrepriseSearchDetails({
entreprise,
}: {
entreprise: FabriqueSocialEntreprise
entreprise: Entreprise
}) {
const { i18n } = useTranslation()

const { siren, label, dateCreationUniteLegale } = entreprise
const { nom, siren, siège, établissement, dateDeCréation } = entreprise

const DateFormatter = useMemo(
() =>
Expand All @@ -29,8 +29,6 @@ export default function CompanySearchDetails({
[i18n.language]
)

const siegeOrFirstEtablissement = getSiegeOrFirstEtablissement(entreprise)

return (
<CompanyContainer>
<H4
Expand All @@ -40,49 +38,25 @@ export default function CompanySearchDetails({
}}
>
<>
{'highlightLabel' in entreprise
? highlightLabelToJSX(entreprise.highlightLabel)
: label}{' '}
<small>({siren})</small>
{nom} <small>({siren})</small>
</>
</H4>
<Spacing sm />
<Trans>Crée le :</Trans>{' '}
<Strong>{DateFormatter.format(new Date(dateCreationUniteLegale))}</Strong>
<Strong>{DateFormatter.format(dateDeCréation)}</Strong>
{établissementEstDifférentDuSiège(entreprise) && (
<>
<br />
<Trans>Siège :</Trans> <Strong>{siège?.adresse.complète}</Strong>
</>
)}
<br />
<Trans>Domiciliée à l'adresse :</Trans>{' '}
<Strong>{siegeOrFirstEtablissement.address}</Strong>
<Trans>Établissement recherché:</Trans>{' '}
<Strong>{établissement?.adresse.complète}</Strong>
</CompanyContainer>
)
}

function highlightLabelToJSX(highlightLabel: string) {
const highlightRE = /(.*?)<b><u>(.+?)<\/u><\/b>/gm
let parsedLength = 0
const result = []
let matches: RegExpExecArray | null = null
while ((matches = highlightRE.exec(highlightLabel)) !== null) {
parsedLength += matches[0].length
result.push(
<Fragment key={matches[2]}>
{matches[1]}
<Highlight>{matches[2]}</Highlight>
</Fragment>
)
}
result.push(highlightLabel.slice(parsedLength))

return result
}

const Highlight = styled.strong`
background-color: ${({ theme }) =>
theme.darkMode
? theme.colors.bases.secondary[600]
: theme.colors.bases.secondary[100]};
color: inherit;
`

const CompanyContainer = styled.div`
text-align: left;
`
26 changes: 13 additions & 13 deletions site/source/components/company/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ReactNode, useEffect, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'

import { FabriqueSocialEntreprise } from '@/api/fabrique-social'
import { ForceThemeProvider } from '@/components/utils/DarkModeContext'
import { Message } from '@/design-system'
import { Card } from '@/design-system/card'
Expand All @@ -15,10 +14,11 @@ import { Strong } from '@/design-system/typography'
import { StyledLink } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
import { Entreprise } from '@/domain/Entreprise'
import useSearchCompany from '@/hooks/useSearchCompany'

import { Appear, FromTop } from '../ui/animate'
import CompanySearchDetails from './SearchDetails'
import EntrepriseSearchDetails from './SearchDetails'

const StyledCard = styled(Card)`
flex-direction: row; // for Safari <= 13
Expand All @@ -28,14 +28,14 @@ const StyledCard = styled(Card)`
}
`

export function CompanySearchField(props: {
export function EntrepriseSearchField(props: {
label?: ReactNode
onValue?: () => void
onClear?: () => void
onSubmit?: (search: FabriqueSocialEntreprise | null) => void
onSubmit?: (search: Entreprise | null) => void
}) {
const { t } = useTranslation()
const refResults = useRef<FabriqueSocialEntreprise[] | null>(null)
const refResults = useRef<Entreprise[] | null>(null)

const searchFieldProps = {
...props,
Expand Down Expand Up @@ -102,8 +102,8 @@ function Results({
results,
onSubmit,
}: {
results: Array<FabriqueSocialEntreprise>
onSubmit?: (établissement: FabriqueSocialEntreprise) => void
results: Array<Entreprise>
onSubmit?: (entreprise: Entreprise) => void
}) {
const { t } = useTranslation()

Expand Down Expand Up @@ -152,14 +152,14 @@ function Results({
<FromTop>
<ForceThemeProvider>
<Ul noMarker data-test-id="company-search-results">
{results.map((etablissement) => (
<Li key={etablissement.siren}>
{results.map((entreprise) => (
<Li key={entreprise.siren}>
<StyledCard
onPress={() => onSubmit?.(etablissement)}
onClick={() => onSubmit?.(etablissement)}
onPress={() => onSubmit?.(entreprise)}
onClick={() => onSubmit?.(entreprise)}
compact
bodyAs="div"
aria-label={`${etablissement.label}, Selectionner cette entreprise`}
aria-label={`${entreprise.nom}, Selectionner cette entreprise`}
ctaLabel={
<ChevronIcon
style={{
Expand All @@ -170,7 +170,7 @@ function Results({
/>
}
>
<CompanySearchDetails entreprise={etablissement} />
<EntrepriseSearchDetails entreprise={entreprise} />
</StyledCard>
</Li>
))}
Expand Down
5 changes: 5 additions & 0 deletions site/source/domain/Adresse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Adresse {
complète?: string
codePostal: string
codeCommune: string
}
1 change: 1 addition & 0 deletions site/source/domain/Brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Brand<T, U extends string> = T & { __tag: U }
6 changes: 6 additions & 0 deletions site/source/domain/CodeActivite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Brand } from '@/domain/Brand'

export type CodeActivite = Brand<string, 'CodeActivite'>
// Pourrait être inféré des données de fetchBénéfice

export const codeActivité = (code: string) => code as CodeActivite
6 changes: 6 additions & 0 deletions site/source/domain/CodeCatégorieJuridique.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Brand } from '@/domain/Brand'

export type CodeCatégorieJuridique = Brand<string, 'CodeCatégorieJuridique'>

export const codeCatégorieJuridique = (code: string) =>
code as CodeCatégorieJuridique
17 changes: 17 additions & 0 deletions site/source/domain/Date.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'

import { formatDate, parsePublicodesDateString } from '@/domain/Date'

describe('parsePublicodesDateString', () => {
it('comprend 24-12-2024 comme le 24 décembre 2024', () => {
expect(parsePublicodesDateString('24/12/2024')).toEqual(
new Date(2024, 11, 24)
)
})
})

describe('formatDate', () => {
it("écrit le 15 août 1980 comme '15/08/1980' (format Publicodes)", () => {
expect(formatDate(new Date('1980-08-15'))).toEqual('15/08/1980')
})
})
10 changes: 10 additions & 0 deletions site/source/domain/Date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { format, parse } from 'date-fns/fp'
johangirod marked this conversation as resolved.
Show resolved Hide resolved

export const publicodesStandardDateFormat = 'dd/MM/yyyy'

export const formatDate = format(publicodesStandardDateFormat)
JalilArfaoui marked this conversation as resolved.
Show resolved Hide resolved

export const parsePublicodesDateString = parse(
new Date(),
publicodesStandardDateFormat
)
24 changes: 24 additions & 0 deletions site/source/domain/Entreprise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CodeActivite } from '@/domain/CodeActivite'
import { CodeCatégorieJuridique } from '@/domain/CodeCatégorieJuridique'
import { Établissement } from '@/domain/Établissement'
import { Siren } from '@/domain/Siren'

export interface Entreprise {
nom: string
siren: Siren
dateDeCréation: Date
codeCatégorieJuridique: CodeCatégorieJuridique
activitéPrincipale: CodeActivite
siège?: Établissement
établissement: Établissement
}

export const établissementEstLeSiège = (entreprise: Entreprise): boolean =>
!!entreprise.siège &&
!!entreprise.siège.adresse.complète &&
entreprise.siège.adresse.complète ===
entreprise.établissement.adresse.complète

export const établissementEstDifférentDuSiège = (
entreprise: Entreprise
): boolean => !établissementEstLeSiège(entreprise)
JalilArfaoui marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions site/source/domain/Siren.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Brand } from '@/domain/Brand'

export type Siren = Brand<string, 'Siren'>
export const siren = (value: string): Siren => value as Siren

export type Siret = Brand<string, 'Siret'>
export const siret = (value: string): Siret => value as Siret
9 changes: 9 additions & 0 deletions site/source/domain/Établissement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Adresse } from '@/domain/Adresse'
import { CodeActivite } from '@/domain/CodeActivite'
import { Siret } from '@/domain/Siren'

export interface Établissement {
siret: Siret
adresse: Adresse
activitéPrincipale: CodeActivite
}
13 changes: 5 additions & 8 deletions site/source/hooks/useSearchCompany.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { useEffect, useState } from 'react'

import {
FabriqueSocialEntreprise,
searchDenominationOrSiren,
} from '@/api/fabrique-social'
import { searchDenominationOrSiren } from '@/api/fabrique-social'
import { Entreprise } from '@/domain/Entreprise'

import { useDebounce } from './useDebounce'

export default function useSearchCompany(
value: string
): [boolean, Array<FabriqueSocialEntreprise>] {
const [result, setResult] = useState<Array<FabriqueSocialEntreprise>>([])
): [boolean, Array<Entreprise>] {
const [result, setResult] = useState<Array<Entreprise>>([])
const [searchPending, setSearchPending] = useState(Boolean(value))
const debouncedValue = useDebounce(value, 300)

useEffect(() => {
setSearchPending(Boolean(value))

if (!value) {
setResult([])
}
Expand All @@ -28,7 +25,7 @@ export default function useSearchCompany(
}

searchDenominationOrSiren(debouncedValue)
.then((entreprise: Array<FabriqueSocialEntreprise> | null) => {
.then((entreprise: Array<Entreprise> | null) => {
setResult(entreprise || [])
setSearchPending(false)
})
Expand Down
Loading
Loading