diff --git a/server/migrations/20241219125144-salaire.js b/server/migrations/20241219125144-salaire.js new file mode 100644 index 00000000..594bbe6a --- /dev/null +++ b/server/migrations/20241219125144-salaire.js @@ -0,0 +1,50 @@ +import { omit, without, uniq } from "lodash-es"; +import { integer } from "#src/common/db/collections/jsonSchema/jsonSchemaTypes.js"; +import * as MongoDB from "#src/common/db/mongodb.js"; + +const schema = { + properties: { + salaire_12_mois_q1: integer(), + salaire_12_mois_q2: integer(), + salaire_12_mois_q3: integer(), + }, + required: [], +}; + +export const up = async (db, client) => { + MongoDB.setMongoDBClient(client); + await Promise.all([ + MongoDB.mergeSchema("certificationsStats", schema), + MongoDB.mergeSchema("formationsStats", schema), + MongoDB.mergeSchema("regionalesStats", schema), + ]); +}; + +export const down = async (db, client) => { + return Promise.all( + ["certificationsStats", "formationsStats", "regionalesStats"].map(async (collection) => { + const collectionInfos = await db.listCollections({ name: collection }).toArray(); + const validator = collectionInfos[0].options.validator; + + if (!validator) { + return; + } + + const oldSchema = validator.$jsonSchema; + const newSchema = { + ...oldSchema, + properties: omit(oldSchema.properties, Object.keys(schema.properties)), + required: uniq(without(oldSchema.required || [], ...schema.required)), + }; + + return db.command({ + collMod: collection, + validationLevel: "strict", + validationAction: "error", + validator: { + $jsonSchema: newSchema, + }, + }); + }) + ); +}; diff --git a/server/src/common/stats.js b/server/src/common/stats.js index d5f456c5..82b0326e 100644 --- a/server/src/common/stats.js +++ b/server/src/common/stats.js @@ -13,6 +13,9 @@ export const INSERJEUNES_STATS_NAMES = [ "nb_poursuite_etudes", "nb_sortant", "taux_rupture_contrats", + "salaire_12_mois_q1", + "salaire_12_mois_q2", + "salaire_12_mois_q3", ]; export const INSERJEUNES_IGNORED_STATS_NAMES = [ "taux_poursuite_etudes", @@ -63,6 +66,7 @@ export const CUSTOM_STATS_NAMES = [ ]; export const ALL = /.*/; +export const ALL_WITHOUT_INCOME = /^(?!salaire_).*/; export const TAUX = /^taux_.*$/; export const VALEURS = /^nb_.*$/; diff --git a/server/src/common/utils/csvUtils.js b/server/src/common/utils/csvUtils.js index 94e7ef7b..fbf96acf 100644 --- a/server/src/common/utils/csvUtils.js +++ b/server/src/common/utils/csvUtils.js @@ -16,6 +16,6 @@ export function parseCsv(options = {}) { }); } -export function getStatsAsColumns() { - return getStats(ALL, (statName) => (f) => f[statName]); +export function getStatsAsColumns(keyRegex = ALL) { + return getStats(keyRegex, (statName) => (f) => f[statName]); } diff --git a/server/src/http/routes/formationsRoutes.js b/server/src/http/routes/formationsRoutes.js index b7523dd5..7d9fa7cb 100644 --- a/server/src/http/routes/formationsRoutes.js +++ b/server/src/http/routes/formationsRoutes.js @@ -21,6 +21,7 @@ import { getMillesimeFormationsYearFrom, transformDisplayStat, isMillesimesYearSingle, + ALL_WITHOUT_INCOME, } from "#src/common/stats.js"; import BCNRepository from "#src/common/repositories/bcn.js"; import BCNSiseRepository from "#src/common/repositories/bcnSise.js"; @@ -161,7 +162,7 @@ export default () => { millesime: (f) => f.millesime, donnee_source_type: (f) => f.donnee_source.type, donnee_source_code_certification: (f) => f.donnee_source.code_certification, - ...getStatsAsColumns(), + ...getStatsAsColumns(ALL_WITHOUT_INCOME), }, mapper: (v) => (v === null ? "null" : v), }); diff --git a/server/src/http/routes/regionalesRoutes.js b/server/src/http/routes/regionalesRoutes.js index 48adf592..6ae8a6d9 100644 --- a/server/src/http/routes/regionalesRoutes.js +++ b/server/src/http/routes/regionalesRoutes.js @@ -15,7 +15,7 @@ import { sendImageOnError, } from "#src/http/utils/responseUtils.js"; import BCNRepository from "#src/common/repositories/bcn.js"; -import { getLastMillesimesRegionales, transformDisplayStat } from "#src/common/stats.js"; +import { ALL_WITHOUT_INCOME, getLastMillesimesRegionales, transformDisplayStat } from "#src/common/stats.js"; import { getStatsAsColumns } from "#src/common/utils/csvUtils.js"; import RegionaleStatsRepository from "#src/common/repositories/regionaleStats.js"; import { ErrorRegionaleNotFound, ErrorNoDataForMillesime, ErrorFormationNotExist } from "#src/http/errors.js"; @@ -84,7 +84,7 @@ export default () => { millesime: (f) => f.millesime, donnee_source_type: (f) => f.donnee_source.type, donnee_source_code_certification: (f) => f.donnee_source.code_certification, - ...getStatsAsColumns(), + ...getStatsAsColumns(ALL_WITHOUT_INCOME), }, mapper: (v) => (v === null ? "null" : v), }); diff --git a/server/src/http/routes/swagger.yml b/server/src/http/routes/swagger.yml index e5ee4344..0a7bd986 100644 --- a/server/src/http/routes/swagger.yml +++ b/server/src/http/routes/swagger.yml @@ -1056,6 +1056,18 @@ components: taux_autres_24_mois: type: number nullable: true + salaire_12_mois_q1: + type: number + nullable: true + description: "en euros, 1er quartile du salaire net mensuel à 12 mois des sortants en emploi retrouvés dans la Base Tous Salariés (BTS)" + salaire_12_mois_q2: + type: number + description: "en euros, 2ème quartile du salaire net mensuel à 12 mois des sortants en emploi retrouvés dans la Base Tous Salariés (BTS)" + nullable: true + salaire_12_mois_q3: + type: number + nullable: true + description: "en euros, 3ème quartile du salaire net mensuel à 12 mois des sortants en emploi retrouvés dans la Base Tous Salariés (BTS)" _meta: $ref: "#/components/schemas/Meta" diff --git a/server/src/services/inserjeunes/InserJeunes.js b/server/src/services/inserjeunes/InserJeunes.js index ea6a4f84..f9afb202 100644 --- a/server/src/services/inserjeunes/InserJeunes.js +++ b/server/src/services/inserjeunes/InserJeunes.js @@ -85,6 +85,25 @@ function computeMissingStats() { }); } +function renameStats() { + return transformData((data) => { + const statsToRename = { + salaire_TS_Q1_12_mois: "salaire_12_mois_q1", + salaire_TS_Q2_12_mois: "salaire_12_mois_q2", + salaire_TS_Q3_12_mois: "salaire_12_mois_q3", + }; + + return { + ...data, + ...Object.fromEntries( + Object.entries(data) + .filter(([k]) => statsToRename[k]) + .map(([k, d]) => [statsToRename[k], d]) + ), + }; + }); +} + async function transformApiStats(statsFromApi, groupBy) { let stats = []; await oleoduc( @@ -94,6 +113,7 @@ async function transformApiStats(statsFromApi, groupBy) { groupBy, flattenArray(), computeMissingStats(), + renameStats(), writeData((data) => { stats.push(data); }) diff --git a/server/tests/http/certificationsRoutes-test.js b/server/tests/http/certificationsRoutes-test.js index 53b22fa0..a0ffe8c7 100644 --- a/server/tests/http/certificationsRoutes-test.js +++ b/server/tests/http/certificationsRoutes-test.js @@ -46,6 +46,9 @@ describe("certificationsRoutes", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, }); const response = await httpClient.get(`/api/inserjeunes/certifications`, { @@ -82,6 +85,9 @@ describe("certificationsRoutes", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, formation_fermee: false, donnee_source: { code_certification: "12345678", @@ -263,6 +269,9 @@ describe("certificationsRoutes", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, }); const response = await httpClient.get(`/api/inserjeunes/certifications.csv`, { @@ -275,9 +284,12 @@ describe("certificationsRoutes", () => { assert.strictEqual(response.headers["content-type"], "text/csv; charset=UTF-8"); assert.deepStrictEqual( response.data, - `code_certification;code_formation_diplome;filiere;millesime;donnee_source_type;donnee_source_code_certification;nb_annee_term;nb_en_emploi_12_mois;nb_en_emploi_18_mois;nb_en_emploi_24_mois;nb_en_emploi_6_mois;nb_poursuite_etudes;nb_sortant;taux_autres_12_mois;taux_autres_18_mois;taux_autres_24_mois;taux_autres_6_mois;taux_en_emploi_12_mois;taux_en_emploi_18_mois;taux_en_emploi_24_mois;taux_en_emploi_6_mois;taux_en_formation;taux_rupture_contrats -12345678;12345678;apprentissage;2020;self;12345678;19;4;3;2;5;1;6;null;null;null;null;null;null;null;null;null;null -` + `code_certification;code_formation_diplome;filiere;millesime;donnee_source_type;donnee_source_code_certification;` + + `nb_annee_term;nb_en_emploi_12_mois;nb_en_emploi_18_mois;nb_en_emploi_24_mois;nb_en_emploi_6_mois;nb_poursuite_etudes;` + + `nb_sortant;salaire_12_mois_q1;salaire_12_mois_q2;salaire_12_mois_q3;taux_autres_12_mois;taux_autres_18_mois;` + + `taux_autres_24_mois;taux_autres_6_mois;taux_en_emploi_12_mois;taux_en_emploi_18_mois;taux_en_emploi_24_mois;` + + `taux_en_emploi_6_mois;taux_en_formation;taux_rupture_contrats\n` + + `12345678;12345678;apprentissage;2020;self;12345678;19;4;3;2;5;1;6;18;19;20;null;null;null;null;null;null;null;null;null;null\n` ); }); @@ -305,6 +317,9 @@ describe("certificationsRoutes", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, }); const response = await httpClient.get(`/api/inserjeunes/certifications.csv`, { @@ -317,8 +332,12 @@ describe("certificationsRoutes", () => { assert.strictEqual(response.headers["content-type"], "text/csv; charset=UTF-8"); assert.deepStrictEqual( response.data, - `code_certification;code_formation_diplome;filiere;millesime;donnee_source_type;donnee_source_code_certification;nb_annee_term;nb_en_emploi_12_mois;nb_en_emploi_18_mois;nb_en_emploi_24_mois;nb_en_emploi_6_mois;nb_poursuite_etudes;nb_sortant;taux_autres_12_mois;taux_autres_18_mois;taux_autres_24_mois;taux_autres_6_mois;taux_en_emploi_12_mois;taux_en_emploi_18_mois;taux_en_emploi_24_mois;taux_en_emploi_6_mois;taux_en_formation;taux_rupture_contrats -12345678;12345678;apprentissage;2020;self;12345678;100;4;3;2;5;1;6;14;15;16;13;11;10;9;12;8;7 + `code_certification;code_formation_diplome;filiere;millesime;donnee_source_type;donnee_source_code_certification;` + + `nb_annee_term;nb_en_emploi_12_mois;nb_en_emploi_18_mois;nb_en_emploi_24_mois;nb_en_emploi_6_mois;` + + `nb_poursuite_etudes;nb_sortant;salaire_12_mois_q1;salaire_12_mois_q2;salaire_12_mois_q3;taux_autres_12_mois;` + + `taux_autres_18_mois;taux_autres_24_mois;taux_autres_6_mois;taux_en_emploi_12_mois;taux_en_emploi_18_mois;` + + `taux_en_emploi_24_mois;taux_en_emploi_6_mois;taux_en_formation;taux_rupture_contrats +12345678;12345678;apprentissage;2020;self;12345678;100;4;3;2;5;1;6;18;19;20;14;15;16;13;11;10;9;12;8;7 ` ); }); @@ -439,6 +458,9 @@ describe("certificationsRoutes", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, }); await insertCertificationsStats({ code_certification: "12345678", millesime: "2019" }); @@ -470,6 +492,9 @@ describe("certificationsRoutes", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, formation_fermee: false, donnee_source: { code_certification: "12345678", diff --git a/server/tests/jobs/stats/computeContinuumStats-test.js b/server/tests/jobs/stats/computeContinuumStats-test.js index a1db62a1..989d13b5 100644 --- a/server/tests/jobs/stats/computeContinuumStats-test.js +++ b/server/tests/jobs/stats/computeContinuumStats-test.js @@ -39,6 +39,9 @@ describe("computeContinuumStats", () => { taux_en_emploi_6_mois: 90, taux_en_formation: 10, taux_rupture_contrats: 10, + salaire_12_mois_q1: 19, + salaire_12_mois_q2: 20, + salaire_12_mois_q3: 21, }; const DEFAULT_STATS_VARIANT = { @@ -59,6 +62,9 @@ describe("computeContinuumStats", () => { taux_en_emploi_6_mois: 50, taux_en_formation: 25, taux_rupture_contrats: 10, + salaire_12_mois_q1: 19, + salaire_12_mois_q2: 20, + salaire_12_mois_q3: 21, }; describe("Certifications", () => { diff --git a/server/tests/jobs/stats/computeUAI-test.js b/server/tests/jobs/stats/computeUAI-test.js index c1655ddc..81f75035 100644 --- a/server/tests/jobs/stats/computeUAI-test.js +++ b/server/tests/jobs/stats/computeUAI-test.js @@ -471,6 +471,9 @@ describe("computeUAI", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 17, + salaire_12_mois_q2: 18, + salaire_12_mois_q3: 19, }); await insertFormationsStats({ @@ -495,6 +498,9 @@ describe("computeUAI", () => { taux_autres_12_mois: 15, taux_autres_18_mois: 16, taux_autres_24_mois: 17, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, }); const result = await computeUAI(); @@ -549,6 +555,9 @@ describe("computeUAI", () => { taux_autres_12_mois: 14, taux_autres_18_mois: 15, taux_autres_24_mois: 16, + salaire_12_mois_q1: 17, + salaire_12_mois_q2: 18, + salaire_12_mois_q3: 19, }, { uai: "ABCD1234", @@ -578,6 +587,9 @@ describe("computeUAI", () => { taux_autres_12_mois: 15, taux_autres_18_mois: 16, taux_autres_24_mois: 17, + salaire_12_mois_q1: 18, + salaire_12_mois_q2: 19, + salaire_12_mois_q3: 20, }, ] ); diff --git a/server/tests/utils/fakeData.js b/server/tests/utils/fakeData.js index e8c8ae96..d7017778 100644 --- a/server/tests/utils/fakeData.js +++ b/server/tests/utils/fakeData.js @@ -19,7 +19,7 @@ import { users, CAFormations, } from "#src/common/db/collections/collections.js"; -import { ALL, getStatsCompute } from "#src/common/stats.js"; +import { ALL, ALL_WITHOUT_INCOME, getStatsCompute } from "#src/common/stats.js"; import { hashPassword } from "#src/services/auth/auth.js"; import { ObjectId } from "mongodb"; @@ -80,7 +80,7 @@ export function insertRegionalesStats(custom = {}, withStat = true) { code_formation_diplome: createCodeFormationDiplome(), libelle: "LIBELLE", diplome: { code: "4", libelle: "BAC" }, - ...(withStat ? getStatsCompute(ALL, () => generateStatValue()) : {}), + ...(withStat ? getStatsCompute(ALL_WITHOUT_INCOME, () => generateStatValue()) : {}), donnee_source: { code_certification, type: "self", @@ -115,7 +115,7 @@ export function insertFormationsStats(custom = {}, withStat = true) { code_formation_diplome: createCodeFormationDiplome(), libelle: "LIBELLE", diplome: { code: "4", libelle: "BAC" }, - ...(withStat ? getStatsCompute(ALL, () => generateStatValue()) : {}), + ...(withStat ? getStatsCompute(ALL_WITHOUT_INCOME, () => generateStatValue()) : {}), region: { code: "11", nom: "Île-de-France" }, academie: { code: "01",