From 439a32c5da5ea7be5c658007eb87a05927ed98ff Mon Sep 17 00:00:00 2001 From: Eric Plaquevent Date: Wed, 2 Mar 2022 16:00:48 +0100 Subject: [PATCH] feat: compute rncp data for france competence study (#546) --- server/jsconfig.json | 8 + server/scripts/preventSensibleFilesCommit.sh | 2 +- .../model/schema/formation/formation.js | 17 +++ .../src/logic/mappers/etablissementsMapper.js | 141 ++++++++++++++++-- .../mappers/etablissementsMapper.test.js | 14 +- 5 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 server/jsconfig.json diff --git a/server/jsconfig.json b/server/jsconfig.json new file mode 100644 index 000000000..29518049d --- /dev/null +++ b/server/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es6" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*test.js"] +} diff --git a/server/scripts/preventSensibleFilesCommit.sh b/server/scripts/preventSensibleFilesCommit.sh index 2789a7cee..c30a2945c 100755 --- a/server/scripts/preventSensibleFilesCommit.sh +++ b/server/scripts/preventSensibleFilesCommit.sh @@ -3,7 +3,7 @@ # A hook script to verify that we don't commit files that could contain sensible data or credentials like json, csv, xls(x) or .env sensible_files_pattern="\.(csv|xls|xls(x?)|json|env)$" -exception="(package.json|custom-environment-variables.json|manifest.json|locales/.*.json|latest_public_ofs.csv|catalogue-formations-apprentissage.*.json|sample.json|uai-edited-2021-12-28.csv" +exception="(package.json|custom-environment-variables.json|manifest.json|locales/.*.json|latest_public_ofs.csv|catalogue-formations-apprentissage.*.json|sample.json|uai-edited-2021-12-28.csv|jsconfig.json" exception="$exception)$" files=$(git diff --cached --name-only | grep -v -E "$exception" | grep -E "$sensible_files_pattern") diff --git a/server/src/common/model/schema/formation/formation.js b/server/src/common/model/schema/formation/formation.js index 48349a796..7b2efd2eb 100644 --- a/server/src/common/model/schema/formation/formation.js +++ b/server/src/common/model/schema/formation/formation.js @@ -565,6 +565,23 @@ const formationSchema = { default: false, description: "Renseigné si la formation peut être suivie entièrement à distance", }, + + france_competence_infos: { + type: new mongoose.Schema( + { + fc_is_catalog_general: Boolean, + fc_is_habilite_rncp: Boolean, + fc_is_certificateur: Boolean, + fc_is_certificateur_siren: Boolean, + fc_is_partenaire: Boolean, + fc_has_partenaire: Boolean, + }, + { _id: false } + ), + default: null, + description: "Données pour étude France Compétence", + }, + ...etablissementGestionnaireInfo, ...etablissementFormateurInfo, ...etablissementReferenceInfo, diff --git a/server/src/logic/mappers/etablissementsMapper.js b/server/src/logic/mappers/etablissementsMapper.js index 8455a6018..ae4e4ee93 100644 --- a/server/src/logic/mappers/etablissementsMapper.js +++ b/server/src/logic/mappers/etablissementsMapper.js @@ -1,7 +1,22 @@ +// @ts-check + +/** @typedef {{Habilitation_Partenaire:string, Siret_Partenaire:string}} Partenaire */ +/** @typedef {{certificateur:string, siret_certificateur:string}} Certificateur */ +/** @typedef {{_id:string, siret: string, siren: string, nda:string, catalogue_published: boolean, published: boolean, ferme: boolean, uai: string, enseigne: string, numero_voie: string, type_voie: string, nom_voie: string, complement_adresse: string, cedex: string, geo_coordonnees: string, entreprise_raison_sociale: string, code_postal: string, code_insee_localite: string, num_departement: string, nom_departement: string, region_implantation_nom: string, nom_academie: string, num_academie: string, localite: string, date_creation: Date}} Etablissement */ +/** @typedef {{code_type_certif:string, rncp_eligible_apprentissage:boolean, partenaires: Partenaire[], certificateurs: Certificateur[]}} RNCPInfo */ + const logger = require("../../common/logger"); const { habiliteList } = require("../../constants/certificateurs"); const { Etablissement } = require("../../common/model"); +/** + * Retrieve establishments data from couple of sirets + * + * @param {string} etablissement_gestionnaire_siret + * @param {string} etablissement_formateur_siret + * + * @returns {Promise<{gestionnaire: Etablissement, formateur: Etablissement}>} + */ const getAttachedEstablishments = async (etablissement_gestionnaire_siret, etablissement_formateur_siret) => { // Get establishment Gestionnaire const gestionnaire = await Etablissement.findOne({ @@ -24,6 +39,12 @@ const getAttachedEstablishments = async (etablissement_gestionnaire_siret, etabl }; }; +/** + * Get address of an establishment on one line + * + * @param {Etablissement} establishment + * @returns {null|string} + */ const getEstablishmentAddress = (establishment) => { if (!establishment) { return null; @@ -33,6 +54,47 @@ const getEstablishmentAddress = (establishment) => { return [numero_voie, type_voie, nom_voie].filter((val) => val).join(" "); }; +/** + * Check if given siret is in partenaires + * + * @param {Partenaire[]} partenaires + * @param {string} siret + * @returns {boolean} + */ +const isPartenaire = (partenaires, siret) => { + return (partenaires ?? []).some( + ({ Siret_Partenaire, Habilitation_Partenaire }) => + Siret_Partenaire === siret && ["HABILITATION_ORGA_FORM", "HABILITATION_FORMER"].includes(Habilitation_Partenaire) + ); +}; + +/** + * Check if given siret is in certificateurs + * + * @param {Certificateur[]} certificateurs + * @param {string} siret + * @returns {boolean} + */ +const isCertificateur = (certificateurs, siret) => { + return siret && (certificateurs ?? []).some(({ siret_certificateur }) => siret_certificateur === siret); +}; + +/** + * Check if given siret is in certificateurs sirens + * + * @param {Certificateur[]} certificateurs + * @param {string} siret + * @returns {boolean} + */ +const isSirenCertificateur = (certificateurs, siret) => { + return ( + siret && + (certificateurs ?? []).some( + ({ siret_certificateur = "" }) => siret_certificateur?.substring(0, 9) === siret.substring(0, 9) + ) + ); +}; + /** * Si la formation est un titre (code_type_certif = Titre ou TP) * On regarde pour gestionnaire ou le formateur si l'un des deux est habilité RNCP @@ -41,18 +103,21 @@ const getEstablishmentAddress = (establishment) => { * - soit habilité par défaut (certificateur = ministère du travail) * - soit siret dans la liste des partenaires avec habilitation 'organiser et former' ou 'former' * - soit siret est dans la liste des certificateurs + * + * @param {RNCPInfo} rncpInfo + * @param {string} siret + * @param {boolean} [checkDefaultHabilitation=true] + * @returns {boolean} */ -const isHabiliteRncp = ({ partenaires = [], certificateurs = [] }, siret) => { - if ((certificateurs ?? []).some(({ certificateur }) => habiliteList.includes(certificateur))) { +const isHabiliteRncp = ({ partenaires = [], certificateurs = [] }, siret, checkDefaultHabilitation = true) => { + if ( + checkDefaultHabilitation && + (certificateurs ?? []).some(({ certificateur }) => habiliteList.includes(certificateur)) + ) { return true; } - const isPartenaire = (partenaires ?? []).some( - ({ Siret_Partenaire, Habilitation_Partenaire }) => - Siret_Partenaire === siret && ["HABILITATION_ORGA_FORM", "HABILITATION_FORMER"].includes(Habilitation_Partenaire) - ); - const isCertificateur = (certificateurs ?? []).some(({ siret_certificateur }) => siret_certificateur === siret); - return isPartenaire || isCertificateur; + return isPartenaire(partenaires, siret) || isCertificateur(certificateurs, siret); }; const getEtablissementReference = ({ gestionnaire, formateur }, rncpInfo) => { @@ -92,6 +157,9 @@ const getEtablissementReference = ({ gestionnaire, formateur }, rncpInfo) => { }; }; +/** + * @param {{gestionnaire: Etablissement, formateur: Etablissement}} attachedEstablishments + */ const getGeoloc = ({ gestionnaire, formateur }) => { const geo_coordonnees_etablissement_formateur = formateur?.geo_coordonnees ?? null; const geo_coordonnees_etablissement_gestionnaire = gestionnaire?.geo_coordonnees ?? null; @@ -102,6 +170,12 @@ const getGeoloc = ({ gestionnaire, formateur }) => { }; }; +/** + * Map etablissement keys for formation + * + * @param {Etablissement} etablissement + * @param {string} prefix + */ const mapEtablissementKeys = (etablissement, prefix = "etablissement_gestionnaire") => { return { [`${prefix}_siren`]: etablissement.siren || null, @@ -129,7 +203,16 @@ const mapEtablissementKeys = (etablissement, prefix = "etablissement_gestionnair }; }; -const isInCatalogEligible = (gestionnaire, referenceEstablishment, rncpInfo) => { +/** + * Check if formation should be in "catalogue général" or in "catalogue non-éligible" + * + * @param {Etablissement} gestionnaire + * @param {Etablissement} referenceEstablishment + * @param {RNCPInfo} rncpInfo + * + * @returns {boolean} true if formation should be in "catalogue général" + */ +const isInCatalogGeneral = (gestionnaire, referenceEstablishment, rncpInfo) => { // Put non-qualiopi in catalogue général (law change https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000044792191) // ensure gestionnaire is published = is qualiopi certified // if (!gestionnaire.catalogue_published) { @@ -146,6 +229,38 @@ const isInCatalogEligible = (gestionnaire, referenceEstablishment, rncpInfo) => return true; }; +/** + * Compute data for France Competence study + * + * @param {{gestionnaire: Etablissement, formateur: Etablissement}} attachedEstablishments + * @param {RNCPInfo} rncpInfo + */ +const getFranceCompetenceInfos = ({ gestionnaire, formateur }, rncpInfo) => { + const fc_is_habilite_rncp = + isHabiliteRncp(rncpInfo, gestionnaire?.siret, false) ?? isHabiliteRncp(rncpInfo, formateur?.siret, false); + + const result = { + fc_is_catalog_general: fc_is_habilite_rncp && rncpInfo.rncp_eligible_apprentissage, + fc_is_habilite_rncp, + fc_is_certificateur: + isCertificateur(rncpInfo.certificateurs, gestionnaire?.siret) ?? + isCertificateur(rncpInfo.certificateurs, formateur?.siret), + fc_is_certificateur_siren: + isSirenCertificateur(rncpInfo.certificateurs, gestionnaire?.siret) ?? + isSirenCertificateur(rncpInfo.certificateurs, formateur?.siret), + fc_is_partenaire: + isPartenaire(rncpInfo.partenaires, gestionnaire?.siret) ?? isPartenaire(rncpInfo.partenaires, formateur?.siret), + fc_has_partenaire: rncpInfo.partenaires?.length > 0, + }; + + return result; +}; + +/** + * @param {string} etablissement_gestionnaire_siret + * @param {string} etablissement_formateur_siret + * @param {RNCPInfo} rncpInfo + */ const etablissementsMapper = async (etablissement_gestionnaire_siret, etablissement_formateur_siret, rncpInfo) => { try { if (!etablissement_gestionnaire_siret && !etablissement_formateur_siret) { @@ -199,13 +314,15 @@ const etablissementsMapper = async (etablissement_gestionnaire_siret, etablissem const geolocInfo = getGeoloc(attachedEstablishments); + const france_competence_infos = getFranceCompetenceInfos(attachedEstablishments, rncpInfo); + return { result: { ...etablissementGestionnaire, ...etablissementFormateur, etablissement_reference, - etablissement_reference_catalogue_published: isInCatalogEligible( + etablissement_reference_catalogue_published: isInCatalogGeneral( attachedEstablishments?.gestionnaire, referenceEstablishment, rncpInfo @@ -216,6 +333,8 @@ const etablissementsMapper = async (etablissement_gestionnaire_siret, etablissem rncp_etablissement_formateur_habilite: isHabiliteRncp(rncpInfo, etablissement_formateur_siret), ...geolocInfo, + + france_competence_infos, }, }; } catch (e) { @@ -231,6 +350,6 @@ module.exports = { getEtablissementReference, getGeoloc, mapEtablissementKeys, - isInCatalogEligible, + isInCatalogGeneral, etablissementsMapper, }; diff --git a/server/tests/unit/logic/mappers/etablissementsMapper.test.js b/server/tests/unit/logic/mappers/etablissementsMapper.test.js index a9953b0b6..54043aa2d 100644 --- a/server/tests/unit/logic/mappers/etablissementsMapper.test.js +++ b/server/tests/unit/logic/mappers/etablissementsMapper.test.js @@ -6,7 +6,7 @@ const { getEtablissementReference, getGeoloc, mapEtablissementKeys, - isInCatalogEligible, + isInCatalogGeneral, etablissementsMapper, } = require("../../../../src/logic/mappers/etablissementsMapper"); const { connectToMongoForTests, cleanAll } = require("../../../../tests/utils/testUtils.js"); @@ -283,15 +283,15 @@ describe(__filename, () => { }); }); - describe("isInCatalogEligible", () => { + describe("isInCatalogGeneral", () => { // Test disabled since currently we show non-qualiopi formations in catalogue général // it("should return false if etablissement not published", () => { - // const result = isInCatalogEligible({ siret: "1234" }, { siret: "1234" }, {}); + // const result = isInCatalogGeneral({ siret: "1234" }, { siret: "1234" }, {}); // assert.deepStrictEqual(result, false); // }); it("should return false if etablissement is not habilite and formation is a Titre or TP", () => { - const result = isInCatalogEligible( + const result = isInCatalogGeneral( { siret: "1234", catalogue_published: true }, { siret: "1234", catalogue_published: true }, { code_type_certif: "TP", rncp_eligible_apprentissage: true } @@ -300,7 +300,7 @@ describe(__filename, () => { }); it("should return false if etablissement is not rncp_eligible_apprentissage and formation is a Titre or TP", () => { - const result = isInCatalogEligible( + const result = isInCatalogGeneral( { siret: "1234", catalogue_published: true }, { siret: "1234", catalogue_published: true }, { @@ -314,7 +314,7 @@ describe(__filename, () => { }); it("should return true if etablissement is habilite rncp and formation is a Titre or TP", () => { - const result = isInCatalogEligible( + const result = isInCatalogGeneral( { siret: "1234", catalogue_published: true }, { siret: "1234", catalogue_published: true }, { @@ -328,7 +328,7 @@ describe(__filename, () => { }); it("should return true if etablissement is published and formation is not Titre or TP", () => { - const result = isInCatalogEligible( + const result = isInCatalogGeneral( { siret: "1234", catalogue_published: true }, { siret: "1234", catalogue_published: true }, {}