Skip to content

Commit

Permalink
feat(lbac-2210): update referentiel rome validation (#1649)
Browse files Browse the repository at this point in the history
* feat: update referentiel rome validation

* feat: update openapi snapshot

* fix: formulaire.controller.test.ts

* fix: test

* fix: remove comment

* fix: normalize string before comparison

* fix: test
  • Loading branch information
kevbarns authored Nov 22, 2024
1 parent 4f41282 commit d95e5d3
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ fileignoreconfig:
- filename: server/src/services/emails.service.test.ts
checksum: b06fe36c0000e1ab1e45617b6cd402267d0492eb7867fbf8f4b817161fe671d9
- filename: server/src/services/formulaire.service.test.ts
checksum: 0b57b45fbf40bdbee16a3b61ad6d725532fa067a8c7baf3710352fbeb2ab059a
checksum: 16a7d096bc389e6e8a0811a22c18dd6dab3c2875397fb327a85a95fc4a05d044
- filename: server/src/services/formulaire.service.ts
checksum: e93ff7ce146d35e70eedcbe9b66097750e7754650468a5ada6b0ed72f0fdbc49
- filename: server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts
Expand Down
9 changes: 8 additions & 1 deletion server/src/http/controllers/formulaire.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ describe("formulaire.controller", () => {
const recruiter = generateRecruiterFixture({
establishment_siret: entreprise.siret,
opco,
jobs: [generateJobFixture({ job_status: JOB_STATUS.ACTIVE, competences_rome: referentielRome.competences })],
jobs: [
generateJobFixture({
job_status: JOB_STATUS.ACTIVE,
competences_rome: referentielRome.competences,
rome_label: referentielRome.rome.intitule,
rome_appellation_label: referentielRome.appellations[0].libelle,
}),
],
})
await getDbCollection("referentielromes").insertOne(referentielRome)
await getDbCollection("entreprises").insertOne(entreprise)
Expand Down
26 changes: 4 additions & 22 deletions server/src/services/__snapshots__/formulaire.service.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,15 @@
exports[`createJob > should insert a job 1`] = `
{
"_id": "670ce30b57a50d6875c141f9",
"address": "address",
"address_detail": "address_detail",
"cfa_delegated_siret": null,
"createdAt": 2021-01-28T15:00:00.000Z,
"distance": 10,
"email": "[email protected]",
"establishment_creation_date": 2024-10-14T09:23:21.588Z,
"establishment_enseigne": "establishment_enseigne",
"establishment_id": "xxxx-xxxx-xxxx-xxxx",
"establishment_raison_sociale": "establishment_raison_sociale",
"establishment_siret": "wosdnnpetvgxonk",
"establishment_size": "establishment_size",
"first_name": "first_name",
"geo_coordinates": "geo_coordinates",
"geopoint": {
"coordinates": [
41,
10,
],
"type": "Point",
},
"establishment_siret": "42476141900045",
"idcc": null,
"is_delegated": false,
"last_name": "last_name",
"naf_code": "naf_code",
"naf_label": "naf_label",
"opco": "OPCO multiple",
"origin": "origin",
"phone": "phone",
"opco": null,
"status": "Actif",
"updatedAt": 2021-02-03T17:00:00.000Z,
}
Expand Down Expand Up @@ -85,8 +65,10 @@ exports[`createJob > should insert a job 2`] = `
"Apprentissage",
],
"managed_by": "670ce1ded6ce30c3c90a0e1d",
"rome_appellation_label": "Agent administratif / Agente administrative",
"rome_code": [
"M1602",
],
"rome_label": "Opérations administratives",
}
`;
59 changes: 45 additions & 14 deletions server/src/services/formulaire.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useMongo } from "@tests/utils/mongo.test.utils"
import { saveEntrepriseUserTest } from "@tests/utils/user.test.utils"
import omit from "lodash/omit"
import { ObjectId } from "mongodb"
import { generateJobFixture } from "shared/fixtures/recruiter.fixture"
import { removeAccents } from "shared"
import { RECRUITER_STATUS } from "shared/constants"
import { generateEntrepriseFixture } from "shared/fixtures/entreprise.fixture"
import { generateJobFixture, generateRecruiterFixture } from "shared/fixtures/recruiter.fixture"
import { generateRoleManagementFixture } from "shared/fixtures/roleManagement.fixture"
import { generateReferentielRome } from "shared/fixtures/rome.fixture"
import { generateUserWithAccountFixture } from "shared/fixtures/userWithAccount.fixture"
import { IRecruiter, IReferentielRome, IUserWithAccount } from "shared/models"
import { beforeEach, describe, expect, it } from "vitest"

Expand All @@ -20,20 +24,29 @@ describe("createJob", () => {

beforeEach(async () => {
const email = "[email protected]"
const response = await saveEntrepriseUserTest(
{
_id: new ObjectId("670ce1ded6ce30c3c90a0e1d"),
email,
},
{},
{},
{ jobs: [], email, _id: new ObjectId("670ce30b57a50d6875c141f9"), establishment_creation_date: new Date("2024-10-14T09:23:21.588Z") }
)
user = response.user
recruiter = response.recruiter

const entreprise = generateEntrepriseFixture()
const role = generateRoleManagementFixture()
user = generateUserWithAccountFixture({
_id: new ObjectId("670ce1ded6ce30c3c90a0e1d"),
email,
})
recruiter = generateRecruiterFixture({
is_delegated: false,
cfa_delegated_siret: null,
status: RECRUITER_STATUS.ACTIF,
establishment_siret: entreprise.siret,
opco: entreprise.opco,
jobs: [],
email,
_id: new ObjectId("670ce30b57a50d6875c141f9"),
establishment_creation_date: new Date("2024-10-14T09:23:21.588Z"),
})
referentielRome = generateReferentielRome()
await getDbCollection("userswithaccounts").insertOne(user)
await getDbCollection("referentielromes").insertOne(referentielRome)
await getDbCollection("rolemanagements").insertOne(role)
await getDbCollection("entreprises").insertOne(entreprise)
await getDbCollection("recruiters").insertOne(recruiter)

return async () => {
await getDbCollection("userswithaccounts").deleteMany({})
Expand All @@ -48,6 +61,8 @@ describe("createJob", () => {
return generateJobFixture({
managed_by: user._id.toString(),
rome_code: [referentielRome.rome.code_rome],
rome_label: referentielRome.rome.intitule,
rome_appellation_label: referentielRome.appellations[0].libelle,
competences_rome: {
savoir_etre_professionnel: referentielRome.competences.savoir_etre_professionnel?.slice(0, 1),
savoir_faire: referentielRome.competences.savoir_faire?.slice(0, 1),
Expand Down Expand Up @@ -107,4 +122,20 @@ describe("createJob", () => {
]
expect.soft(() => createJob({ user, establishment_id: recruiter.establishment_id, job })).rejects.toThrow("compétences invalides")
})
it("should raise a bad request when rome_label do not match referentiel rome", async () => {
const job = generateValidJobWritable()
job.rome_label = "test"
expect
.soft(() => createJob({ user, establishment_id: recruiter.establishment_id, job }))
.rejects.toThrow(
`L'intitulé du code ROME ne correspond pas au référentiel : ${removeAccents(referentielRome.rome.intitule.toLowerCase())}, reçu ${removeAccents(job.rome_label.toLowerCase())}`
)
})
it("should raise a bad request when rome_appellation_label do not match referentiel rome", async () => {
const job = generateValidJobWritable()
job.rome_appellation_label = "test"
expect
.soft(() => createJob({ user, establishment_id: recruiter.establishment_id, job }))
.rejects.toThrow(`L'appellation du code ROME ne correspond pas au référentiel : reçu ${removeAccents(job.rome_appellation_label.toLowerCase())}`)
})
})
31 changes: 25 additions & 6 deletions server/src/services/formulaire.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"
import { badRequest, internal, notFound } from "@hapi/boom"
import equal from "fast-deep-equal"
import { Filter, ObjectId, UpdateFilter } from "mongodb"
import { IDelegation, IJob, IJobCreate, IJobWithRomeDetail, IRecruiter, IRecruiterWithApplicationCount, IUserRecruteur, JOB_STATUS } from "shared"
import { IDelegation, IJob, IJobCreate, IJobWithRomeDetail, IRecruiter, IRecruiterWithApplicationCount, IUserRecruteur, JOB_STATUS, removeAccents } from "shared"
import { getDirectJobPath } from "shared/constants/lbaitem"
import { RECRUITER_STATUS } from "shared/constants/recruteur"
import { EntrepriseStatus, IEntreprise } from "shared/models/entreprise.model"
Expand Down Expand Up @@ -222,7 +222,7 @@ const isAuthorizedToPublishJob = async ({ userId, entrepriseId }: { userId: Obje
* @description Create job offer for formulaire
*/
export const createJob = async ({ job, establishment_id, user }: { job: IJobCreate; establishment_id: string; user: IUserWithAccount }): Promise<IRecruiter> => {
await controlEditableRomeDetail(job)
await validateFieldsFromReferentielRome(job)

const userId = user._id
const recruiter = await getDbCollection("recruiters").findOne({ establishment_id: establishment_id })
Expand Down Expand Up @@ -572,7 +572,7 @@ export async function updateOffre(id: string | ObjectId, payload: UpdateFilter<I
* @returns {Promise<IRecruiter>}
*/
export const patchOffre = async (id: IJob["_id"], payload: Partial<IJob>): Promise<IRecruiter> => {
await controlEditableRomeDetail(payload)
await validateFieldsFromReferentielRome(payload)
const fields = {}
for (const key in payload) {
fields[`jobs.$.${key}`] = payload[key]
Expand Down Expand Up @@ -885,15 +885,34 @@ const filterRomeDetails = (romeDetails, competencesRome) => {
return filteredRome
}

const controlEditableRomeDetail = async (job) => {
const { competences_rome, rome_code } = job
const validateFieldsFromReferentielRome = async (job) => {
const { competences_rome, rome_code, rome_appellation_label, rome_label } = job
const romeDetails = await getRomeDetailsFromDB(rome_code[0])

if (!romeDetails) {
throw internal("unexpected: rome details not found")
}

const filteredRome = filterRomeDetails(romeDetails.competences, competences_rome)
const {
competences,
rome: { intitule },
appellations,
} = romeDetails

const formatedIntituleFromRome = removeAccents(intitule.toLowerCase())
const formatedRomeLabelFromJob = removeAccents(rome_label.toLowerCase())

if (formatedIntituleFromRome !== formatedRomeLabelFromJob) {
throw badRequest(`L'intitulé du code ROME ne correspond pas au référentiel : ${formatedIntituleFromRome}, reçu ${formatedRomeLabelFromJob}`)
}

const matchingAppellation = appellations.some((appellation) => removeAccents(appellation.libelle.toLowerCase()) === removeAccents(rome_appellation_label.toLowerCase()))

if (!matchingAppellation) {
throw badRequest(`L'appellation du code ROME ne correspond pas au référentiel : reçu ${removeAccents(rome_appellation_label.toLowerCase())}`)
}

const filteredRome = filterRomeDetails(competences, competences_rome)
const isValid = equal(filteredRome, competences_rome)

if (!isValid) {
Expand Down
20 changes: 10 additions & 10 deletions shared/fixtures/rome.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import { IReferentielRome } from "../models"
export function generateReferentielRome(data: Partial<IReferentielRome> = {}): IReferentielRome {
return {
_id: new ObjectId(),
numero: "512",
rome: {
code_rome: "M1602",
intitule: "Opérations administratives",
code_ogr: "475",
},
definition:
"Exécute des travaux administratifs courants (vérification de documents, frappe et mise en forme de courriers pré-établis, suivi de dossier administratifs, ...) selon l'organisation de la structure ou du service. Peut être en charge d'activités de reprographie et d'archivage. Peut réaliser l'accueil de la structure.",
acces_metier:
"Ce métier est accessible avec un diplôme de fin d'études secondaires (brevet des collèges) à Bac (professionnel, Brevet Professionnel, ...) dans le secteur tertiaire. Il est également accessible avec une expérience professionnelle sans diplôme particulier. La maîtrise de l'outil bureautique (traitement de texte, tableur, ...) peut être requise.",
competences: {
savoir_faire: [
{
Expand Down Expand Up @@ -237,16 +247,6 @@ export function generateReferentielRome(data: Partial<IReferentielRome> = {}): I
ordre_mobilite: "10",
},
],
numero: "512",
rome: {
code_rome: "M1602",
intitule: "Opérations administratives",
code_ogr: "475",
},
definition:
"Exécute des travaux administratifs courants (vérification de documents, frappe et mise en forme de courriers pré-établis, suivi de dossier administratifs, ...) selon l'organisation de la structure ou du service. Peut être en charge d'activités de reprographie et d'archivage. Peut réaliser l'accueil de la structure.",
acces_metier:
"Ce métier est accessible avec un diplôme de fin d'études secondaires (brevet des collèges) à Bac (professionnel, Brevet Professionnel, ...) dans le secteur tertiaire. Il est également accessible avec une expérience professionnelle sans diplôme particulier. La maîtrise de l'outil bureautique (traitement de texte, tableur, ...) peut être requise.",
...data,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1044,10 +1044,7 @@ exports[`generateOpenApiSchema > should generate proper schema 1`] = `
],
"type": "object",
},
"type": [
"array",
"null",
],
"type": "array",
},
"competences": {
"additionalProperties": false,
Expand Down Expand Up @@ -1309,6 +1306,7 @@ exports[`generateOpenApiSchema > should generate proper schema 1`] = `
"required": [
"numero",
"rome",
"appellations",
"definition",
"acces_metier",
"competences",
Expand Down Expand Up @@ -2004,10 +2002,7 @@ exports[`generateOpenApiSchema > should generate proper schema 1`] = `
],
"type": "object",
},
"type": [
"array",
"null",
],
"type": "array",
},
"competences": {
"additionalProperties": false,
Expand Down Expand Up @@ -2269,6 +2264,7 @@ exports[`generateOpenApiSchema > should generate proper schema 1`] = `
"required": [
"numero",
"rome",
"appellations",
"definition",
"acces_metier",
"competences",
Expand Down
2 changes: 1 addition & 1 deletion shared/models/rome.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const ZReferentielRomeForJob = z
.object({
numero: z.string(),
rome: ZRome,
appellations: z.array(ZRomeAppellation).nullish(),
appellations: z.array(ZRomeAppellation),
definition: z.string(),
acces_metier: z.string(),
competences: ZRomeCompetence,
Expand Down

0 comments on commit d95e5d3

Please sign in to comment.