diff --git a/assets/input/data-inclusion/data-inclusion.config.json b/assets/input/data-inclusion/data-inclusion.config.json index bf336643..e1e636c8 100644 --- a/assets/input/data-inclusion/data-inclusion.config.json +++ b/assets/input/data-inclusion/data-inclusion.config.json @@ -46,205 +46,230 @@ }, "labels_nationaux": [ { - "colonnes": ["labels_nat"], + "colonnes": ["labels_nationaux"], "termes": ["conseiller-numerique"], "cible": "CNFS" }, { - "colonnes": ["labels_nat"], + "colonnes": ["labels_nationaux"], "termes": ["france-service"], "cible": "France Services" }, { - "colonnes": ["labels_nat"], + "colonnes": ["labels_nationaux"], "termes": ["aidants-connect"], "cible": "Aidants Connect" + }, + { + "colonnes": ["labels_nationaux"], + "termes": ["aptic"], + "cible": "APTIC" + }, + { + "colonnes": ["labels_nationaux"], + "termes": ["campus-connecte"], + "cible": "Campus connecté" + }, + { + "colonnes": ["labels_nationaux"], + "termes": ["fabrique-de-territoire"], + "cible": "Fabriques de Territoire" + }, + { + "colonnes": ["labels_nationaux"], + "termes": ["french-tech"], + "cible": "French Tech" + }, + { + "colonnes": ["labels_nationaux"], + "termes": ["grandes-ecoles-du-numerique"], + "cible": "Grandes écoles du numérique" } ], "conditions_acces": [ { - "colonnes": ["conditions_acces"], + "colonnes": ["frais"], "termes": ["gratuit"], "cible": "Gratuit : Je peux accéder gratuitement au lieu et à ses services" }, { - "colonnes": ["conditionAcces"], + "colonnes": ["frais"], "termes": ["gratuit-sous-conditions"], "cible": "Gratuit sous condition : La gratuité est conditionnée à des critères (situation familiale, convention avec un organisme social...)" }, { - "colonnes": ["conditions_acces"], + "colonnes": ["frais"], "termes": ["adhesion"], "cible": "Adhésion : L'accès au lieu et/ou à ses services nécessite d'y adhérer" }, { - "colonnes": ["conditions_acces"], + "colonnes": ["frais"], "termes": ["payant"], "cible": "Payant : L'accès au lieu et/ou à ses services est payant" }, { - "colonnes": ["conditions_acces"], + "colonnes": ["frais"], "termes": ["pass-numerique"], "cible": "Accepte le Pass numérique : Il est possible d'utiliser un Pass numérique pour accéder au lieu" } ], "modalites_accompagnement": [ { - "colonnes": ["modalites_accompagnement"], + "colonnes": ["types"], "termes": ["atelier"], "cible": "Dans un atelier : j'apprends collectivement à utiliser le numérique" }, { - "colonnes": ["modalites_accompagnement"], + "colonnes": ["types"], "termes": ["delegation"], "cible": "A ma place : une personne habilitée fait les démarches à ma place" }, { - "colonnes": ["modalites_accompagnement"], + "colonnes": ["types"], "termes": ["accompagnement"], "cible": "Avec de l'aide : je suis accompagné seul dans l'usage du numérique" }, { - "colonnes": ["modalites_accompagnement"], + "colonnes": ["types"], "termes": ["autonomie"], "cible": "Seul : j'ai accès à du matériel et une connexion" } ], "publics_accueillis": [ { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["adultes"], "cible": "Adultes" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["familles-enfants"], "cible": "Familles/enfants" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["jeunes-16-26"], "cible": "Jeunes (16-26 ans)" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["seniors-65"], "cible": "Seniors (+ 65 ans)" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["surdite"], "cible": "Surdité" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["handicaps-mentaux"], "cible": "Handicaps mentaux : déficiences limitant les activités d'une personne" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["personnes-en-situation-illettrisme"], "cible": "Personnes en situation d'illettrisme" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["public-langues-etrangeres"], "cible": "Public langues étrangères" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["femmes"], "cible": "Uniquement femmes" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["handicaps-psychiques"], "cible": "Handicaps psychiques : troubles psychiatriques donnant lieu à des atteintes comportementales" }, { - "colonnes": ["publics_accueillis"], + "colonnes": ["profils"], "termes": ["deficience-visuelle"], "cible": "Déficience visuelle" } ], "services": [ { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--devenir-autonome-dans-les-demarches-administratives"], "cible": "Devenir autonome dans les démarches administratives" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--realiser-des-demarches-administratives-avec-un-accompagnement"], "cible": "Réaliser des démarches administratives avec un accompagnement" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--prendre-en-main-un-smartphone-ou-une-tablette"], "cible": "Prendre en main un smartphone ou une tablette" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--prendre-en-main-un-ordinateur"], "cible": "Prendre en main un ordinateur" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--utiliser-le-numerique-au-quotidien"], "cible": "Utiliser le numérique au quotidien" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--approfondir-ma-culture-numerique"], "cible": "Approfondir ma culture numérique" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--favoriser-mon-insertion-professionnelle"], "cible": "Favoriser mon insertion professionnelle" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--acceder-a-une-connexion-internet"], "cible": "Accéder à une connexion internet" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--acceder-a-du-materiel"], "cible": "Accéder à du matériel" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--s-equiper-en-materiel-informatique"], "cible": "S'équiper en matériel informatique" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--creer-et-developper-mon-entreprise"], "cible": "Créer et développer mon entreprise" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--creer-avec-le-numerique"], "cible": "Créer avec le numérique" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--accompagner-les-demarches-de-sante"], "cible": "Accompagner les démarches de santé" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--promouvoir-la-citoyennete-numerique"], "cible": "Promouvoir la citoyenneté numérique" }, { - "colonnes": ["services"], + "colonnes": ["thematiques"], "termes": ["numerique--soutenir-la-parentalite-et-l-education-avec-le-numerique"], "cible": "Soutenir la parentalité et l'éducation avec le numérique" } ], "horaires": { - "semaine": "horaires_ouverture" + "semaine": "horaires" } } diff --git a/package.json b/package.json index 5455e78b..bb6ceaad 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "ts-node": "^10.9.1" }, "dependencies": { - "@gouvfr-anct/lieux-de-mediation-numerique": "^1.12.3", + "@gouvfr-anct/lieux-de-mediation-numerique": "^1.15.0", "@gouvfr-anct/timetable-to-osm-opening-hours": "^1.0.1", "axios": "^1.2.2", "commander": "^10.0.0", diff --git a/src/data-inclusion/data-inclusion-merged.ts b/src/data-inclusion/data-inclusion-merged.ts new file mode 100644 index 00000000..3f7a6e40 --- /dev/null +++ b/src/data-inclusion/data-inclusion-merged.ts @@ -0,0 +1,138 @@ +/* eslint-disable @typescript-eslint/naming-convention, camelcase */ + +import { SchemaServiceDataInclusion, SchemaStructureDataInclusion } from '@gouvfr-anct/lieux-de-mediation-numerique'; + +type DataInclusionMergedGeneral = { + id: string; + nom: string; + pivot: string; + structure_parente?: string; + thematiques?: string; + typologie?: string; + accessibilite?: string; +}; + +type DataInclusionMergedLocalisation = { + latitude?: number; + longitude?: number; +}; + +type DataInclusionMergedLabels = { + labels_nationaux?: string; + labels_autres?: string; +}; + +type DataInclusionMergedContact = { + courriel?: string; + telephone?: string; + site_web?: string; + prise_rdv?: string; +}; + +type DataInclusionMergedAddress = { + code_postal: string; + commune: string; + adresse: string; + complement_adresse?: string; + code_insee?: string; +}; + +type DataInclusionMergedPresentation = { + presentation_detail?: string; + presentation_resume?: string; +}; + +type DataInclusionMergedCollecte = { + source: string; + date_maj: string; +}; + +type DataInclusionMergedAcces = { + frais?: string; + profils?: string; + types?: string; + horaires?: string; +}; + +export type DataInclusionMerged = DataInclusionMergedAcces & + DataInclusionMergedAddress & + DataInclusionMergedCollecte & + DataInclusionMergedContact & + DataInclusionMergedGeneral & + DataInclusionMergedLabels & + DataInclusionMergedLocalisation & + DataInclusionMergedPresentation; + +const dataInclusionMergedGeneral = ( + structure: SchemaStructureDataInclusion, + service: SchemaServiceDataInclusion +): DataInclusionMergedGeneral => ({ + id: structure.id, + nom: structure.nom, + pivot: structure.siret ?? '', + ...(structure.structure_parente == null ? {} : { structure_parente: structure.structure_parente }), + thematiques: service.thematiques?.join(',') ?? '', + ...(structure.typologie == null ? {} : { typologie: structure.typologie }), + ...(structure.accessibilite == null ? {} : { accessibilite: structure.accessibilite }) +}); + +const dataInclusionMergedAddress = (structure: SchemaStructureDataInclusion): DataInclusionMergedAddress => ({ + adresse: structure.adresse, + code_postal: structure.code_postal, + commune: structure.commune, + ...(structure.code_insee == null ? {} : { code_insee: structure.code_insee }), + ...(structure.complement_adresse == null ? {} : { complement_adresse: structure.complement_adresse }) +}); + +const dataInclusionMergedLocalisation = (structure: SchemaStructureDataInclusion): DataInclusionMergedLocalisation => ({ + ...(structure.latitude == null ? {} : { latitude: structure.latitude }), + ...(structure.longitude == null ? {} : { longitude: structure.longitude }) +}); + +const dataInclusionMergedContact = ( + structure: SchemaStructureDataInclusion, + service: SchemaServiceDataInclusion +): DataInclusionMergedContact => ({ + ...(structure.courriel == null ? {} : { courriel: structure.courriel }), + ...(structure.telephone == null ? {} : { telephone: structure.telephone }), + ...(structure.site_web == null ? {} : { site_web: structure.site_web }), + ...(service.prise_rdv == null ? {} : { prise_rdv: service.prise_rdv }) +}); + +const dataInclusionMergedCollecte = (structure: SchemaStructureDataInclusion): DataInclusionMergedCollecte => ({ + date_maj: new Date(structure.date_maj).toISOString(), + source: 'Hubik' +}); + +const dataInclusionMergedPresentation = (structure: SchemaStructureDataInclusion): DataInclusionMergedPresentation => ({ + ...(structure.presentation_detail == null ? {} : { presentation_detail: structure.presentation_detail }), + ...(structure.presentation_resume == null ? {} : { presentation_resume: structure.presentation_resume }) +}); + +const dataInclusionMergedLabels = (structure: SchemaStructureDataInclusion): DataInclusionMergedLabels => ({ + ...(structure.labels_autres == null ? {} : { labels_autres: structure.labels_autres.join(',') }), + ...(structure.labels_nationaux == null ? {} : { labels_nationaux: structure.labels_nationaux.join(',') }) +}); +const dataInclusionMergedAcces = ( + structure: SchemaStructureDataInclusion, + service: SchemaServiceDataInclusion +): DataInclusionMergedAcces => ({ + ...(service.frais == null || service.frais.length === 0 ? {} : { frais: service.frais.join(',') }), + ...(service.profils == null || service.profils.length === 0 ? {} : { profils: service.profils.join(',') }), + ...(service.types == null || service.types.length === 0 ? {} : { types: service.types.join(',') }), + ...(structure.horaires_ouverture == null ? {} : { horaires: structure.horaires_ouverture }) +}); + +export const mergeStructureAndService = ( + structure: SchemaStructureDataInclusion, + service: SchemaServiceDataInclusion +): DataInclusionMerged => ({ + ...dataInclusionMergedGeneral(structure, service), + ...dataInclusionMergedAddress(structure), + ...dataInclusionMergedLocalisation(structure), + ...dataInclusionMergedContact(structure, service), + ...dataInclusionMergedCollecte(structure), + ...dataInclusionMergedPresentation(structure), + ...dataInclusionMergedLabels(structure), + ...dataInclusionMergedAcces(structure, service) +}); diff --git a/src/data-inclusion/main.ts b/src/data-inclusion/main.ts index 822448a3..c65462d9 100644 --- a/src/data-inclusion/main.ts +++ b/src/data-inclusion/main.ts @@ -1,167 +1,24 @@ -/* eslint-disable @typescript-eslint/no-restricted-imports,max-lines-per-function,@typescript-eslint/naming-convention, camelcase, max-lines, @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-restricted-imports, @typescript-eslint/no-floating-promises */ import * as fs from 'fs'; -import { - CommuneError, - MandatorySiretOrRnaError, - SchemaServiceDataInclusion, - SchemaStructureDataInclusion, - ServicesError, - Url, - UrlError, - VoieError -} from '@gouvfr-anct/lieux-de-mediation-numerique'; -import { DataInclusionMerged, mergeServicesInStructure } from './merge-services-in-structure'; import axios, { AxiosResponse } from 'axios'; +import { structuresWithServicesNumeriques } from './merge-services-in-structure'; -export type SchemaStructureDataInclusionWithServices = SchemaStructureDataInclusion & { - services: string; - labels_nat: string; - labels_autre: string; -}; - -const NAME: string = 'data-inclusion'; - -const onlyDefindedSchemaStructureDataInclusion = ( - schemaStructureDataInclusion?: SchemaStructureDataInclusionWithServices -): schemaStructureDataInclusion is SchemaStructureDataInclusionWithServices => schemaStructureDataInclusion != null; - -const onlyWithServices = (schemaStructureDataInclusion: SchemaStructureDataInclusionWithServices): boolean => - schemaStructureDataInclusion.services.includes('numerique'); - -const mergeThematiques = (thematiques?: string[], thematiquesToAdd?: string[]): { thematiques: string[] } => ({ - thematiques: Array.from(new Set([...(thematiques ?? []), ...(thematiquesToAdd ?? [])])) -}); - -const mergeProfils = (profils?: string[], profilsToAdd?: string[]): { profils: string[] } => ({ - profils: Array.from(new Set([...(profils ?? []), ...(profilsToAdd ?? [])])) -}); - -const mergeTypes = (types?: string[], typesToAdd?: string[]): { types: string[] } => ({ - types: Array.from(new Set([...(types ?? []), ...(typesToAdd ?? [])])) -}); - -const mergePriseRdv = (priseRdv?: string, priseRdvToAdd?: string): { prise_rdv?: string } => - priseRdv == null && priseRdvToAdd == null ? {} : { prise_rdv: priseRdvToAdd ?? priseRdv ?? '' }; - -const fraisIfDefined = (frais?: string[]): { frais?: string[] } => (frais == null ? {} : { frais }); - -const mergeFrais = (frais?: string[], fraisToAdd?: string[]): { frais?: string[] } => - fraisIfDefined(Array.from(new Set([...(frais ?? []), ...(fraisToAdd ?? [])]))); - -const toSingleService = ( - mergedService: SchemaServiceDataInclusion, - service: SchemaServiceDataInclusion -): SchemaServiceDataInclusion => ({ - ...mergedService, - ...mergeThematiques(mergedService.thematiques, service.thematiques), - ...mergeFrais(mergedService.frais, service.frais), - ...mergeProfils(mergedService.profils, service.profils), - ...mergeTypes(mergedService.types, service.types), - ...mergePriseRdv(mergedService.prise_rdv, service.prise_rdv) -}); - -const mergeServices = ( - services: SchemaServiceDataInclusion[], - structure: SchemaStructureDataInclusion -): SchemaServiceDataInclusion => - services.reduce(toSingleService, { - id: `${structure.id}-mediation-numerique`, - nom: 'Médiation numérique', - source: structure.source ?? '', - structure_id: structure.id, - thematiques: [] - }); - -const getPriseRdv = (priseRdv?: string): { prise_rdv?: Url } => (priseRdv == null ? {} : { prise_rdv: Url(priseRdv) }); - -const getPublicsAccueillis = (profils?: string[]): { publics_accueillis?: string } => - profils == null || profils.length === 0 - ? {} - : { - publics_accueillis: profils.map((profil: string): string => profil).join(';') - }; - -const getServices = (thematiques?: string[]): { services: string } => ({ - services: thematiques?.map((thematique: string): string => thematique).join(';') ?? '' -}); - -const getModalitesAccompagnement = (types?: string[]): { modalites_accompagnement?: string } => - types == null || types.length === 0 - ? {} - : { - modalites_accompagnement: types.map((type: string): string => type).join(';') - }; - -const getFrais = (conditionAcces?: string[]): { conditions_acces?: string } => - conditionAcces == null || conditionAcces.length === 0 - ? {} - : { - conditions_acces: conditionAcces.map((frais: string): string => frais).join(';') - }; - -const getSource = (source?: string): string => source?.split('mediation-numerique-')[1] ?? source ?? ''; - -const processFields = ( - structure: SchemaStructureDataInclusion, - service: SchemaServiceDataInclusion -): SchemaStructureDataInclusionWithServices => ({ - ...structure, - id: `${structure.id}-${structure.source}`, - date_maj: new Date(structure.date_maj).toLocaleDateString('fr'), - source: getSource(structure.source), - labels_nat: structure.labels_nationaux?.join(';') ?? '', - labels_autre: structure.labels_autres?.join(';') ?? '', - ...getServices(service.thematiques), - ...getPublicsAccueillis(service.profils), - ...getModalitesAccompagnement(service.types), - ...getFrais(service.frais), - ...getPriseRdv(service.prise_rdv) -}); - -const invalidLieuErrors: unknown[] = [ - UrlError, // todo: fix instead of drop - VoieError, - ServicesError, - CommuneError, - MandatorySiretOrRnaError -]; - -const matchActual = - (error: Error) => - (invalidLieuError: unknown): boolean => - error instanceof (invalidLieuError as typeof Error); - -const toMergeDataInclusionWithServices = - (dataInclusionServices: SchemaServiceDataInclusion[]) => - (structure: SchemaStructureDataInclusion): SchemaStructureDataInclusionWithServices | undefined => { - try { - const dataInclusionMerged: DataInclusionMerged = mergeServicesInStructure(dataInclusionServices, structure); - return processFields( - dataInclusionMerged.structure, - mergeServices(dataInclusionMerged.services, dataInclusionMerged.structure) - ); - } catch (error: unknown) { - if (error instanceof Error && invalidLieuErrors.some(matchActual(error))) return undefined; - - throw error; - } - }; +const NAME: 'data-inclusion' = 'data-inclusion' as const; const dataInclusionFetch = async (): Promise => { const responseStructures: AxiosResponse = await axios.get( 'https://www.data.gouv.fr/fr/datasets/r/4fc64287-e869-4550-8fb9-b1e0b7809ffa' ); - const dataInclusionStructures: SchemaStructureDataInclusion[] = responseStructures.data; + const responseServices: AxiosResponse = await axios.get( 'https://www.data.gouv.fr/fr/datasets/r/0eac1faa-66f9-4e49-8fb3-f0721027d89f' ); - const dataInclusionServices: SchemaServiceDataInclusion[] = responseServices.data; - const schemaDataInclusionWithServices: SchemaStructureDataInclusionWithServices[] = dataInclusionStructures - .map(toMergeDataInclusionWithServices(dataInclusionServices)) - .filter(onlyDefindedSchemaStructureDataInclusion) - .filter(onlyWithServices); - fs.writeFileSync(`./assets/input/${NAME}/${NAME}.json`, JSON.stringify(schemaDataInclusionWithServices), 'utf8'); + fs.writeFileSync( + `./assets/input/${NAME}/${NAME}.json`, + JSON.stringify(structuresWithServicesNumeriques(responseStructures.data, responseServices.data)), + 'utf8' + ); }; dataInclusionFetch(); diff --git a/src/data-inclusion/merge-services-in-structure.spec.ts b/src/data-inclusion/merge-services-in-structure.spec.ts index e4a764f2..3144854e 100644 --- a/src/data-inclusion/merge-services-in-structure.spec.ts +++ b/src/data-inclusion/merge-services-in-structure.spec.ts @@ -1,7 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention, camelcase */ -import { SchemaServiceDataInclusion, SchemaStructureDataInclusion } from '@gouvfr-anct/lieux-de-mediation-numerique'; -import { DataInclusionMerged, mergeServicesInStructure } from './merge-services-in-structure'; +import { + SchemaServiceDataInclusion, + SchemaStructureDataInclusion, + SchemaStructureDataInclusionAdresseFields, + SchemaStructureDataInclusionLocalisationFields, + Typologie +} from '@gouvfr-anct/lieux-de-mediation-numerique'; +import { DataInclusionMerged } from './data-inclusion-merged'; +import { structuresWithServicesNumeriques } from './merge-services-in-structure'; describe('merge services in structure', (): void => { it('should merge a single service in a single structure', (): void => { @@ -11,7 +18,9 @@ describe('merge services in structure', (): void => { commune: 'Paris', date_maj: '2022-11-07', id: 'structure-1', - nom: 'Médiation république' + nom: 'Médiation république', + siret: '43493312300029', + source: 'Hubik' }; const service: SchemaServiceDataInclusion = { @@ -19,15 +28,158 @@ describe('merge services in structure', (): void => { nom: 'Médiation numérique', source: 'Hubik', structure_id: 'structure-1', - thematiques: ['numerique-devenir-autonome-dans-les-demarches-administratives'] + thematiques: ['numerique--devenir-autonome-dans-les-demarches-administratives'] }; - const merged: DataInclusionMerged = mergeServicesInStructure([service], structure); + const dataInclusionMerged: DataInclusionMerged[] = structuresWithServicesNumeriques([structure], [service]); - expect(merged).toStrictEqual({ - structure, - services: [service] - }); + expect(dataInclusionMerged).toStrictEqual([ + { + code_postal: '75013', + commune: 'Paris', + adresse: '51 rue de la république', + date_maj: '2022-11-07T00:00:00.000Z', + id: 'structure-1', + nom: 'Médiation république', + pivot: '43493312300029', + thematiques: 'numerique--devenir-autonome-dans-les-demarches-administratives', + source: 'Hubik' + } + ]); + }); + + it('should merge a single full service with a single full structure', (): void => { + const structure: SchemaStructureDataInclusion = { + adresse: '12 BIS RUE DE LECLERCQ', + code_postal: '51100', + code_insee: '51454', + complement_adresse: 'Le patio du bois de l’Aulne', + commune: 'Reims', + date_maj: new Date('2022-10-10').toISOString(), + id: 'structure-1', + nom: 'Anonymal', + siret: '43493312300029', + source: 'Hubik', + accessibilite: 'https://acceslibre.beta.gouv.fr/app/29-lampaul-plouarzel/a/bibliotheque-mediatheque/erp/mediatheque-13/', + courriel: 'contact@laquincaillerie.tl', + telephone: '+33180059880', + site_web: 'https://www.laquincaillerie.tl/;https://m.facebook.com/laquincaillerienumerique/', + horaires_ouverture: 'Mo-Fr 09:00-12:00,14:00-18:30; Sa 08:30-12:00', + labels_nationaux: ['france-service', 'aptic'], + labels_autres: ['SudLabs', 'Nièvre médiation numérique'], + latitude: 43.52609, + longitude: 5.41423, + presentation_detail: + 'Notre parcours d’initiation permet l’acquisition de compétences numériques de base. Nous proposons également un accompagnement à destination des personnes déjà initiées qui souhaiteraient approfondir leurs connaissances. Du matériel informatique est en libre accès pour nos adhérents tous les après-midis. En plus de d’accueillir les personnes dans notre lieu en semaine (sur rendez-vous), nous assurons une permanence le samedi matin dans la médiathèque XX.', + presentation_resume: 'Notre association propose des formations aux outils numériques à destination des personnes âgées.', + structure_parente: 'Pôle emploi', + typologie: Typologie.TIERS_LIEUX + }; + + const service: SchemaServiceDataInclusion = { + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + source: 'Hubik', + structure_id: 'structure-1', + thematiques: [ + 'numerique', + 'numerique--devenir-autonome-dans-les-demarches-administratives', + 'numerique--realiser-des-demarches-administratives-avec-un-accompagnement', + 'numerique--prendre-en-main-un-smartphone-ou-une-tablette', + 'numerique--prendre-en-main-un-ordinateur', + 'numerique--utiliser-le-numerique-au-quotidien', + 'numerique--approfondir-ma-culture-numerique', + 'numerique--favoriser-mon-insertion-professionnelle', + 'numerique--acceder-a-une-connexion-internet', + 'numerique--acceder-a-du-materiel', + 'numerique--s-equiper-en-materiel-informatique', + 'numerique--creer-et-developper-mon-entreprise', + 'numerique--creer-avec-le-numerique', + 'numerique--accompagner-les-demarches-de-sante', + 'numerique--promouvoir-la-citoyennete-numerique', + 'numerique--soutenir-la-parentalite-et-l-education-avec-le-numerique' + ], + frais: ['gratuit-sous-conditions'], + types: ['autonomie', 'delegation', 'accompagnement', 'atelier'], + prise_rdv: 'https://www.rdv-solidarites.fr/', + profils: [ + 'seniors-65', + 'familles-enfants', + 'adultes', + 'jeunes-16-26', + 'public-langues-etrangeres', + 'deficience-visuelle', + 'surdite', + 'handicaps-psychiques', + 'handicaps-mentaux', + 'femmes', + 'personnes-en-situation-illettrisme' + ] + }; + + const dataInclusionMerged: DataInclusionMerged[] = structuresWithServicesNumeriques([structure], [service]); + + expect(dataInclusionMerged).toStrictEqual([ + { + code_postal: '51100', + code_insee: '51454', + complement_adresse: 'Le patio du bois de l’Aulne', + commune: 'Reims', + adresse: '12 BIS RUE DE LECLERCQ', + latitude: 43.52609, + longitude: 5.41423, + courriel: 'contact@laquincaillerie.tl', + telephone: '+33180059880', + site_web: 'https://www.laquincaillerie.tl/;https://m.facebook.com/laquincaillerienumerique/', + date_maj: '2022-10-10T00:00:00.000Z', + id: 'structure-1', + nom: 'Anonymal', + pivot: '43493312300029', + thematiques: + 'numerique,numerique--devenir-autonome-dans-les-demarches-administratives,numerique--realiser-des-demarches-administratives-avec-un-accompagnement,numerique--prendre-en-main-un-smartphone-ou-une-tablette,numerique--prendre-en-main-un-ordinateur,numerique--utiliser-le-numerique-au-quotidien,numerique--approfondir-ma-culture-numerique,numerique--favoriser-mon-insertion-professionnelle,numerique--acceder-a-une-connexion-internet,numerique--acceder-a-du-materiel,numerique--s-equiper-en-materiel-informatique,numerique--creer-et-developper-mon-entreprise,numerique--creer-avec-le-numerique,numerique--accompagner-les-demarches-de-sante,numerique--promouvoir-la-citoyennete-numerique,numerique--soutenir-la-parentalite-et-l-education-avec-le-numerique', + presentation_detail: + 'Notre parcours d’initiation permet l’acquisition de compétences numériques de base. Nous proposons également un accompagnement à destination des personnes déjà initiées qui souhaiteraient approfondir leurs connaissances. Du matériel informatique est en libre accès pour nos adhérents tous les après-midis. En plus de d’accueillir les personnes dans notre lieu en semaine (sur rendez-vous), nous assurons une permanence le samedi matin dans la médiathèque XX.', + presentation_resume: + 'Notre association propose des formations aux outils numériques à destination des personnes âgées.', + source: 'Hubik', + structure_parente: 'Pôle emploi', + horaires: 'Mo-Fr 09:00-12:00,14:00-18:30; Sa 08:30-12:00', + accessibilite: + 'https://acceslibre.beta.gouv.fr/app/29-lampaul-plouarzel/a/bibliotheque-mediatheque/erp/mediatheque-13/', + labels_nationaux: 'france-service,aptic', + labels_autres: 'SudLabs,Nièvre médiation numérique', + typologie: Typologie.TIERS_LIEUX, + frais: 'gratuit-sous-conditions', + types: 'autonomie,delegation,accompagnement,atelier', + prise_rdv: 'https://www.rdv-solidarites.fr/', + profils: + 'seniors-65,familles-enfants,adultes,jeunes-16-26,public-langues-etrangeres,deficience-visuelle,surdite,handicaps-psychiques,handicaps-mentaux,femmes,personnes-en-situation-illettrisme' + } + ]); + }); + + it('should not merge a single service in a single structure when there is no numerique service', (): void => { + const structure: SchemaStructureDataInclusion = { + adresse: '51 rue de la république', + code_postal: '75013', + commune: 'Paris', + date_maj: '2022-11-07', + id: 'structure-1', + nom: 'Médiation république', + siret: '43493312300029' + }; + + const service: SchemaServiceDataInclusion = { + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + source: 'Hubik', + structure_id: 'structure-1', + thematiques: ['mobilite-acces-a-des-transports-en-commun'] + }; + + const dataInclusionMerged: DataInclusionMerged[] = structuresWithServicesNumeriques([structure], [service]); + + expect(dataInclusionMerged).toStrictEqual([]); }); it('should not merge the service when the structure id do not match', (): void => { @@ -45,14 +197,121 @@ describe('merge services in structure', (): void => { nom: 'Médiation numérique', source: 'Hubik', structure_id: 'structure-2', - thematiques: ['numerique-devenir-autonome-dans-les-demarches-administratives'] + thematiques: ['numerique--devenir-autonome-dans-les-demarches-administratives'] + }; + + const dataInclusionMerged: DataInclusionMerged[] = structuresWithServicesNumeriques([structure], [service]); + + expect(dataInclusionMerged).toStrictEqual([]); + }); + + it('should merge two services with the same structure id', (): void => { + const structure: SchemaStructureDataInclusion = { + adresse: '51 rue de la république', + code_postal: '75013', + commune: 'Paris', + date_maj: '2022-11-07', + id: 'structure-1', + nom: 'Médiation république', + siret: '43493312300029' + }; + + const service1: SchemaServiceDataInclusion = { + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + source: 'Hubik', + structure_id: 'structure-1', + thematiques: ['numerique--devenir-autonome-dans-les-demarches-administratives'] + }; + + const service2: SchemaServiceDataInclusion = { + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + source: 'Hubik', + structure_id: 'structure-1', + thematiques: ['numerique--prendre-en-main-un-smartphone-ou-une-tablette'] + }; + + const dataInclusionMerged: DataInclusionMerged[] = structuresWithServicesNumeriques([structure], [service1, service2]); + + expect(dataInclusionMerged).toStrictEqual([ + { + code_postal: '75013', + commune: 'Paris', + adresse: '51 rue de la république', + date_maj: '2022-11-07T00:00:00.000Z', + id: 'structure-1', + nom: 'Médiation république', + pivot: '43493312300029', + source: 'Hubik', + thematiques: + 'numerique--devenir-autonome-dans-les-demarches-administratives,numerique--prendre-en-main-un-smartphone-ou-une-tablette' + } + ]); + }); + + it('should not merge two services with the same structure id when service has its own location', (): void => { + const structure: SchemaStructureDataInclusion = { + adresse: '51 rue de la république', + code_postal: '75013', + commune: 'Paris', + date_maj: '2022-11-07', + id: 'structure-1', + nom: 'Médiation république', + siret: '43493312300029' + }; + + const service1: SchemaServiceDataInclusion = { + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + source: 'Hubik', + structure_id: 'structure-1', + thematiques: ['numerique--devenir-autonome-dans-les-demarches-administratives'] + }; + + const service2: Partial & + SchemaServiceDataInclusion & + SchemaStructureDataInclusionLocalisationFields = { + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + source: 'Hubik', + structure_id: 'structure-1', + thematiques: ['numerique--prendre-en-main-un-smartphone-ou-une-tablette'], + adresse: '12 rue Baudricourt', + code_postal: '75013', + commune: 'Paris', + latitude: 4.8375548, + longitude: 45.7665478 }; - const merged: DataInclusionMerged = mergeServicesInStructure([service], structure); + const dataInclusionMerged: DataInclusionMerged[] = structuresWithServicesNumeriques([structure], [service1, service2]); - expect(merged).toStrictEqual({ - structure, - services: [] - }); + expect(dataInclusionMerged).toStrictEqual([ + { + code_postal: '75013', + commune: 'Paris', + adresse: '51 rue de la république', + date_maj: '2022-11-07T00:00:00.000Z', + id: 'structure-1', + nom: 'Médiation république', + pivot: '43493312300029', + source: 'Hubik', + thematiques: 'numerique--devenir-autonome-dans-les-demarches-administratives' + }, + { + code_postal: '75013', + commune: 'Paris', + adresse: '12 rue Baudricourt', + latitude: 4.8375548, + longitude: 45.7665478, + date_maj: '2022-11-07T00:00:00.000Z', + id: 'structure-1-mediation-numerique', + nom: 'Médiation numérique', + pivot: '43493312300029', + source: 'Hubik', + structure_parente: 'structure-1', + thematiques: 'numerique--prendre-en-main-un-smartphone-ou-une-tablette' + } + ]); }); }); diff --git a/src/data-inclusion/merge-services-in-structure.ts b/src/data-inclusion/merge-services-in-structure.ts index c041aa6c..2ee1efe7 100644 --- a/src/data-inclusion/merge-services-in-structure.ts +++ b/src/data-inclusion/merge-services-in-structure.ts @@ -1,19 +1,104 @@ -import { SchemaServiceDataInclusion, SchemaStructureDataInclusion } from '@gouvfr-anct/lieux-de-mediation-numerique'; +import { + SchemaServiceDataInclusion, + SchemaStructureDataInclusion, + SchemaStructureDataInclusionAdresseFields, + SchemaStructureDataInclusionLocalisationFields, + isServiceWithAdresse, + toStructureDataInclusion, + SchemaServiceDataInclusionWithAdresse, + mergeServices +} from '@gouvfr-anct/lieux-de-mediation-numerique'; +import { DataInclusionMerged, mergeStructureAndService } from './data-inclusion-merged'; -export type DataInclusionMerged = { +export type DataInclusionStructureAndServices = { structure: SchemaStructureDataInclusion; services: SchemaServiceDataInclusion[]; }; -const onlyMatchingStructureId = - (structureId: string) => - (service: SchemaServiceDataInclusion): boolean => - service.structure_id === structureId; +const onlyWithNumeriqueServices = (service: SchemaServiceDataInclusion): boolean => + service.thematiques?.some((thematique: string): boolean => thematique.includes('numerique')) ?? false; -export const mergeServicesInStructure = ( - services: SchemaServiceDataInclusion[], +const matchingService = + (service: SchemaServiceDataInclusion) => + (structure: SchemaStructureDataInclusion): boolean => + structure.id === service.structure_id; + +const toDataInclusionMerged = (dataInclusionStructureAndServices: DataInclusionStructureAndServices): DataInclusionMerged => + mergeStructureAndService( + dataInclusionStructureAndServices.structure, + mergeServices(dataInclusionStructureAndServices.services, dataInclusionStructureAndServices.structure) + ); + +const onlyWithSameIdAs = + (structure?: SchemaStructureDataInclusion) => + (dataInclusionStructureAndServices: DataInclusionStructureAndServices): boolean => + dataInclusionStructureAndServices.structure.id === structure?.id; + +const toDataInclusionStructuresWithNewService = + (service: SchemaServiceDataInclusion, structure: SchemaStructureDataInclusion) => + (dataInclusionStructureAndServices: DataInclusionStructureAndServices): DataInclusionStructureAndServices => + dataInclusionStructureAndServices.structure.id === structure.id + ? { + structure: dataInclusionStructureAndServices.structure, + services: [...dataInclusionStructureAndServices.services, service] + } + : dataInclusionStructureAndServices; + +const appendService = ( + dataInclusionStructureAndServices: DataInclusionStructureAndServices[], + service: SchemaServiceDataInclusion, + structure: SchemaStructureDataInclusion +): DataInclusionStructureAndServices[] => + dataInclusionStructureAndServices.find(onlyWithSameIdAs(structure)) == null + ? [...dataInclusionStructureAndServices, { services: [service], structure }] + : dataInclusionStructureAndServices.map(toDataInclusionStructuresWithNewService(service, structure)); + +const serviceAsStructure = ( + dataInclusionStructureAndServices: DataInclusionStructureAndServices[], + service: SchemaServiceDataInclusion, structure: SchemaStructureDataInclusion -): DataInclusionMerged => ({ - services: services.filter(onlyMatchingStructureId(structure.id)), - structure -}); +): DataInclusionStructureAndServices[] => [ + ...dataInclusionStructureAndServices, + { structure: toStructureDataInclusion(service as SchemaServiceDataInclusionWithAdresse, structure), services: [service] } +]; + +const updateDataInclusionStructuresAndServices = ( + service: SchemaServiceDataInclusion, + dataInclusionStructureAndServices: DataInclusionStructureAndServices[], + structure: SchemaStructureDataInclusion +): DataInclusionStructureAndServices[] => + isServiceWithAdresse(service) + ? serviceAsStructure(dataInclusionStructureAndServices, service, structure) + : appendService(dataInclusionStructureAndServices, service, structure); + +const processStructureAndServices = ( + service: SchemaServiceDataInclusion, + structure: SchemaStructureDataInclusion | undefined, + dataInclusionStructureAndServices: DataInclusionStructureAndServices[] +): DataInclusionStructureAndServices[] => + structure == null + ? dataInclusionStructureAndServices + : updateDataInclusionStructuresAndServices(service, dataInclusionStructureAndServices, structure); + +const toDataInclusionStructureAndServices = + (dataInclusionStructures: SchemaStructureDataInclusion[]) => + ( + dataInclusionStructureAndServices: DataInclusionStructureAndServices[], + service: Partial & + SchemaServiceDataInclusion & + SchemaStructureDataInclusionLocalisationFields + ): DataInclusionStructureAndServices[] => + processStructureAndServices( + service, + dataInclusionStructures.find(matchingService(service)), + dataInclusionStructureAndServices + ); + +export const structuresWithServicesNumeriques = ( + dataInclusionStructures: SchemaStructureDataInclusion[], + dataInclusionServices: SchemaServiceDataInclusion[] +): DataInclusionMerged[] => + dataInclusionServices + .filter(onlyWithNumeriqueServices) + .reduce(toDataInclusionStructureAndServices(dataInclusionStructures), []) + .map(toDataInclusionMerged); diff --git a/src/extract/cli/action/extract.action.ts b/src/extract/cli/action/extract.action.ts index c85bb147..093f54b9 100644 --- a/src/extract/cli/action/extract.action.ts +++ b/src/extract/cli/action/extract.action.ts @@ -1,14 +1,14 @@ /* eslint-disable-next-line @typescript-eslint/no-restricted-imports */ import * as fs from 'fs'; -import { ExtractOptions } from '../extract-options'; -import { SchemaStructureDataInclusionWithServices } from '../../../data-inclusion/main'; import axios, { AxiosResponse } from 'axios'; +import { SchemaLieuMediationNumerique } from '@gouvfr-anct/lieux-de-mediation-numerique'; +import { ExtractOptions } from '../extract-options'; const filterDataByDepartement = ( - lieuxMediationNumerique: SchemaStructureDataInclusionWithServices[], + lieuxMediationNumerique: SchemaLieuMediationNumerique[], departements: string -): SchemaStructureDataInclusionWithServices[] => - lieuxMediationNumerique.filter((lieu: SchemaStructureDataInclusionWithServices): boolean => { +): SchemaLieuMediationNumerique[] => + lieuxMediationNumerique.filter((lieu: SchemaLieuMediationNumerique): boolean => { const arrayDepartements: string[] = departements.split(','); const codePostalDepartement: string = lieu.code_postal.slice(0, arrayDepartements[0]?.length); return arrayDepartements.includes(codePostalDepartement); @@ -18,7 +18,7 @@ export const extractAction = async (extractOptions: ExtractOptions): Promise { expect(priseRdv).toBeUndefined(); }); + + it('should ignore empty strings', (): void => { + const matching: LieuxMediationNumeriqueMatching = { + prise_rdv: { + colonne: 'PriseRdv' + } + } as LieuxMediationNumeriqueMatching; + + const source: DataSource = { + PriseRdv: '' + }; + const priseRdv: string | undefined = processPriseRdv(source, matching); + + expect(priseRdv).toBeUndefined(); + }); }); diff --git a/src/transformer/fields/prise-rdv/prise-rdv.field.ts b/src/transformer/fields/prise-rdv/prise-rdv.field.ts index 1268e4b6..34f0e074 100644 --- a/src/transformer/fields/prise-rdv/prise-rdv.field.ts +++ b/src/transformer/fields/prise-rdv/prise-rdv.field.ts @@ -2,7 +2,7 @@ import { Url } from '@gouvfr-anct/lieux-de-mediation-numerique'; import { LieuxMediationNumeriqueMatching, DataSource, Colonne } from '../../input'; const canPorecessPriseRdv = (source: DataSource, priseRdv?: Colonne): priseRdv is Colonne => - priseRdv?.colonne != null && source[priseRdv.colonne] != null; + priseRdv?.colonne != null && source[priseRdv.colonne] != null && source[priseRdv.colonne] !== ''; export const processPriseRdv = (source: DataSource, matching: LieuxMediationNumeriqueMatching): Url | undefined => canPorecessPriseRdv(source, matching.prise_rdv) ? Url(source[matching.prise_rdv.colonne] ?? '') : undefined; diff --git a/yarn.lock b/yarn.lock index 1ad671e3..ba42f546 100644 --- a/yarn.lock +++ b/yarn.lock @@ -474,10 +474,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@gouvfr-anct/lieux-de-mediation-numerique@^1.12.3": - version "1.12.3" - resolved "https://registry.yarnpkg.com/@gouvfr-anct/lieux-de-mediation-numerique/-/lieux-de-mediation-numerique-1.12.3.tgz#99c1d1bfc9668cacb231b220506f9b5a1f162af5" - integrity sha512-Zf6f2ekcvrIlo24Tz/TiotJO2H9uvQcyfoYWT4EPhBSHzLUBZ5y9Rp+X52CkCPkPXF7VTgtyBlmB+XTurnR50g== +"@gouvfr-anct/lieux-de-mediation-numerique@^1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@gouvfr-anct/lieux-de-mediation-numerique/-/lieux-de-mediation-numerique-1.15.0.tgz#71f521791394ad87d413b9ba62141969ea92df3f" + integrity sha512-BfWtCCSCtVODklQUU74ae6ynsJn9XRw2PeuYY/3jfb+9BYLMe9B5CjdImVGuZ7sAyFkZuT+ReVLpWdcs45Rb1Q== "@gouvfr-anct/timetable-to-osm-opening-hours@^1.0.1": version "1.1.0"