From 87aeda01ed0944cecf55bf1f68d42e12ad8a4318 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 12 Sep 2024 09:31:32 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20LBA-2475=20correction=20de=20la=20ma?= =?UTF-8?q?uvaise=20g=C3=A9olocalisation=20des=20formations=20(#1503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: récupération du bon champ dans la projection et inversion des lat lon * feat: cleaning --- server/src/services/formation.service.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/server/src/services/formation.service.ts b/server/src/services/formation.service.ts index 79f1743c3b..25ddae8d0f 100644 --- a/server/src/services/formation.service.ts +++ b/server/src/services/formation.service.ts @@ -59,6 +59,7 @@ const minimalDataMongoFields = { lieu_formation_geo_coordonnees: 1, localite: 1, distance: 1, + lieu_formation_geopoint: 1, } /** @@ -332,8 +333,8 @@ const transformFormations = (rawFormations: IFormationCatalogue[], isMinimalData * Adaptation au modèle LBAC et conservation des seules infos utilisées des formations */ const transformFormation = (rawFormation: IFormationCatalogue): ILbaItemFormation => { - const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null - const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null const sessions = setSessions(rawFormation) const duration = getDurationFromSessions(sessions) @@ -417,8 +418,8 @@ const transformFormation = (rawFormation: IFormationCatalogue): ILbaItemFormatio * Adaptation au modèle LBAC et conservation des seules infos utilisées des formations */ const transformFormationV2 = (rawFormation: IFormationCatalogue): ILbaItemFormation2 => { - const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null - const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null const sessions = setSessions(rawFormation) const duration = getDurationFromSessions(sessions) @@ -493,8 +494,8 @@ const transformFormationV2 = (rawFormation: IFormationCatalogue): ILbaItemFormat } const transformFormationWithMinimalDataV2 = (rawFormation: IFormationCatalogue): ILbaItemFormation2 => { - const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null - const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null const resultFormation: ILbaItemFormation2 = { type: LBA_ITEM_TYPE.FORMATION, @@ -526,8 +527,8 @@ const transformFormationWithMinimalDataV2 = (rawFormation: IFormationCatalogue): * Adaptation au modèle LBAC et conservation des seules infos utilisées des formations */ const transformFormationWithMinimalData = (rawFormation: IFormationCatalogue): ILbaItemFormation => { - const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null - const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const latOpt = rawFormation.lieu_formation_geopoint?.coordinates[1] ?? null + const longOpt = rawFormation.lieu_formation_geopoint?.coordinates[0] ?? null const resultFormation: ILbaItemFormation = { ideaType: LBA_ITEM_TYPE_OLD.FORMATION, From 0ec835d38547ca5d8bb9e32713fd428922f561ce Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 12 Sep 2024 09:50:09 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20LBA-2414=20LBA-2432=20LBA-2458=20sy?= =?UTF-8?q?nth=C3=A8se=20jobs=20import=20hello=20work=20+=20raw=5Fjobs=5Fp?= =?UTF-8?q?artners=20to=20computed=5Fjobs=5Fpartners=20+=20computed=5Fjobs?= =?UTF-8?q?=5Fpartners=20to=20jobs=5Fpartners=20=20(#1501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .talismanrc | 4 +- server/src/commands.ts | 24 +++++- .../controllers/v2/jobs.controller.v2.test.ts | 10 +-- server/src/jobs/jobs.ts | 13 +++- .../cancelRemovedJobsPartners.test.ts | 56 ++++++++++++++ .../cancelRemovedJobsPartners.ts | 45 +++++++++++ .../offrePartenaire/helloWorkMapper.test.ts | 2 +- .../jobs/offrePartenaire/helloWorkMapper.ts | 5 +- .../importFromComputedToJobsPartners.test.ts | 52 +++++++++++++ .../importFromComputedToJobsPartners.ts | 32 ++++++++ .../jobs/offrePartenaire/importHelloWork.ts | 3 + .../rawToComputedJobsPartners.ts | 2 +- server/src/security/authorisationService.ts | 2 +- server/src/services/etablissement.service.ts | 2 +- .../jobOpportunity.service.test.ts.snap | 16 ++-- .../jobOpportunity.service.test.ts | 75 +++++++++++-------- .../jobOpportunity/jobOpportunity.service.ts | 8 +- .../src/services/recruteurLba.service.test.ts | 7 +- server/tests/utils/jobsPartners.test.utils.ts | 29 +++++++ server/tests/utils/user.test.utils.ts | 9 +-- shared/fixtures/jobPartners.fixture.ts | 2 +- shared/fixtures/recruteurLba.fixture.ts | 2 +- shared/models/jobsPartners.model.ts | 7 +- shared/models/jobsPartnersComputed.model .ts | 9 ++- 24 files changed, 338 insertions(+), 78 deletions(-) create mode 100644 server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.test.ts create mode 100644 server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.ts create mode 100644 server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.test.ts create mode 100644 server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.ts create mode 100644 server/tests/utils/jobsPartners.test.utils.ts diff --git a/.talismanrc b/.talismanrc index a3cac26f06..c1712e50c7 100644 --- a/.talismanrc +++ b/.talismanrc @@ -58,7 +58,7 @@ fileignoreconfig: - filename: server/src/http/controllers/updateRecruteurLba.controller.test.ts checksum: 71dd55af4f1f44fd2b69dee91f0fc8006f3b9e0bd80d40271c73697d4eea9ea2 - filename: server/src/http/controllers/v2/jobs.controller.v2.test.ts - checksum: 21b3574a8d82bba190ecdf1ace1b5db58db0fa45e6890506de530bc0781380af + checksum: 06366000e6615ce90b5dedbd4792eac0a93ce5a0cf1459273e3cc9bbdf867e4f - filename: server/src/http/routes/appointmentRequest.controller.ts checksum: d2770daa97ae332eec0b66497fdb717229895583ac3bfd48af1a830b36504968 - filename: server/src/http/routes/auth/password.controller.ts @@ -90,7 +90,7 @@ fileignoreconfig: - filename: server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts checksum: 7c2a7f5d8ec1c82652296c3271bd435f5998e9c61f9ead2d528a5f2a7195a07d - filename: server/src/services/recruteurLba.service.test.ts - checksum: 98a1f338a0721200ace9b9a62f755996ad395035621c5830681e721ca58d67c2 + checksum: 38c947c87cfb508fec88cbce878f89725cbb6679e833a66bdb7fca60e3685a0a - filename: server/src/services/referrers.service.ts checksum: 966b0ece2b18b5a6066df531524c97ba0ad38266e782a334004e8c127b41ade6 - filename: server/src/services/userRecruteur.service.ts diff --git a/server/src/commands.ts b/server/src/commands.ts index 14ef925242..3af3221809 100644 --- a/server/src/commands.ts +++ b/server/src/commands.ts @@ -538,10 +538,32 @@ program .option("-parallelism, [parallelism]", "Number of threads", "10") .action(createJobAction("referentiel-opco:constructys:import")) -program.command("import-hellowork").description("Importe les offres hellowork").option("-q, --queued", "Run job asynchronously", false).action(createJobAction("import-hellowork")) +program + .command("import-hellowork") + .description("Importe les offres hellowork dans la collection raw") + .option("-q, --queued", "Run job asynchronously", false) + .action(createJobAction("import-hellowork")) + +program + .command("import-hellowork-to-computed") + .description("Importe les offres hellowork depuis raw vers computed") + .option("-q, --queued", "Run job asynchronously", false) + .action(createJobAction("import-hellowork-to-computed")) program.command("import-kelio").description("Importe les offres kelio").option("-q, --queued", "Run job asynchronously", false).action(createJobAction("import-kelio")) +program + .command("import-computed-to-jobs-partners") + .description("Met à jour la collection jobs_partners à partir de computed_jobs_partners") + .option("-q, --queued", "Run job asynchronously", false) + .action(createJobAction("import-computed-to-jobs-partners")) + +program + .command("cancel-removed-jobs-partners") + .description("Met à jour la collection jobs_partners en mettant à 'Annulé' les offres qui ne sont plus dans computed_jobs_partners") + .option("-q, --queued", "Run job asynchronously", false) + .action(createJobAction("cancel-removed-jobs-partners")) + program .command("send-applications") .description("Scanne les virus des pièces jointes et envoie les candidatures. Timeout à 8 minutes.") diff --git a/server/src/http/controllers/v2/jobs.controller.v2.test.ts b/server/src/http/controllers/v2/jobs.controller.v2.test.ts index 059b13e095..703ea34cb5 100644 --- a/server/src/http/controllers/v2/jobs.controller.v2.test.ts +++ b/server/src/http/controllers/v2/jobs.controller.v2.test.ts @@ -4,7 +4,7 @@ import { useServer } from "@tests/utils/server.test.utils" import { ObjectId } from "mongodb" import nock from "nock" import { generateJobsPartnersOfferPrivate } from "shared/fixtures/jobPartners.fixture" -import { generateLbaConpanyFixture } from "shared/fixtures/recruteurLba.fixture" +import { generateLbaCompanyFixture } from "shared/fixtures/recruteurLba.fixture" import { clichyFixture, generateReferentielCommuneFixtures, levalloisFixture, marseilleFixture, parisFixture } from "shared/fixtures/referentiel/commune.fixture" import { IGeoPoint } from "shared/models" import { IJobsPartnersOfferPrivate, IJobsPartnersWritableApiInput } from "shared/models/jobsPartners.model" @@ -39,7 +39,7 @@ const porteDeClichy: IGeoPoint = { } const romesQuery = rome.join(",") const [longitude, latitude] = porteDeClichy.coordinates -const recruteurLba = generateLbaConpanyFixture({ rome_codes: rome, geopoint: clichyFixture.centre, siret: "58006820882692", email: "email@mail.com", website: "http://site.fr" }) +const recruteurLba = generateLbaCompanyFixture({ rome_codes: rome, geopoint: clichyFixture.centre, siret: "58006820882692", email: "email@mail.com", website: "http://site.fr" }) const jobPartnerOffer: IJobsPartnersOfferPrivate = generateJobsPartnersOfferPrivate({ offer_rome_codes: ["D1214"], workplace_geopoint: parisFixture.centre, @@ -164,8 +164,8 @@ describe("GET /jobs/search", () => { "offer_target_diploma", "offer_title", "offer_to_be_acquired_skills", - "partner", "partner_job_id", + "partner_label", "workplace_address", "workplace_brand", "workplace_description", @@ -385,7 +385,7 @@ describe("POST /jobs", async () => { const doc = await getDbCollection("jobs_partners").findOne({ _id: new ObjectId(responseJson.id as string) }) // Ensure that the job offer is associated to the correct permission - expect(doc?.partner).toBe("Un super Partenaire") + expect(doc?.partner_label).toBe("Un super Partenaire") }) it("should apply method be defined", async () => { @@ -431,7 +431,7 @@ describe("PUT /jobs/:id", async () => { const now = new Date("2024-06-18T00:00:00.000Z") const inSept = new Date("2024-09-01T00:00:00.000Z") - const originalJob = generateJobsPartnersOfferPrivate({ _id: id, offer_title: "Old title", partner: "Un super Partenaire" }) + const originalJob = generateJobsPartnersOfferPrivate({ _id: id, offer_title: "Old title", partner_label: "Un super Partenaire" }) const data: IJobsPartnersWritableApiInput = { partner_job_id: null, diff --git a/server/src/jobs/jobs.ts b/server/src/jobs/jobs.ts index a6cdca00ff..d7c4a9880d 100644 --- a/server/src/jobs/jobs.ts +++ b/server/src/jobs/jobs.ts @@ -28,7 +28,9 @@ import { pocRomeo } from "./franceTravail/pocRomeo" import { createJobsCollectionForMetabase } from "./metabase/metabaseJobsCollection" import { createRoleManagement360 } from "./metabase/metabaseRoleManagement360" import { runGarbageCollector } from "./misc/runGarbageCollector" -import { importHelloWork } from "./offrePartenaire/importHelloWork" +import { cancelRemovedJobsPartners } from "./offrePartenaire/cancelRemovedJobsPartners" +import { importFromComputedToJobsPartners } from "./offrePartenaire/importFromComputedToJobsPartners" +import { importHelloWork, importRawHelloWorkIntoComputedJobPartners } from "./offrePartenaire/importHelloWork" import { importKelio } from "./offrePartenaire/importKelio" import { exportLbaJobsToS3 } from "./partenaireExport/exportJobsToS3" import { exportToFranceTravail } from "./partenaireExport/exportToFranceTravail" @@ -414,9 +416,18 @@ export async function setupJobProcessor() { "import-hellowork": { handler: async () => importHelloWork(), }, + "import-hellowork-to-computed": { + handler: async () => importRawHelloWorkIntoComputedJobPartners(), + }, "import-kelio": { handler: async () => importKelio(), }, + "import-computed-to-jobs-partners": { + handler: async () => importFromComputedToJobsPartners(), + }, + "cancel-removed-jobs-partners": { + handler: async () => cancelRemovedJobsPartners(), + }, "send-applications": { handler: async () => processApplications(), }, diff --git a/server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.test.ts b/server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.test.ts new file mode 100644 index 0000000000..f098523bf5 --- /dev/null +++ b/server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.test.ts @@ -0,0 +1,56 @@ +import { createComputedJobPartner, createJobPartner } from "@tests/utils/jobsPartners.test.utils" +import { useMongo } from "@tests/utils/mongo.test.utils" +import { JOB_STATUS_ENGLISH } from "shared/models" +import { beforeEach, describe, expect, it } from "vitest" + +import { getDbCollection } from "@/common/utils/mongodbUtils" + +import { cancelRemovedJobsPartners } from "./cancelRemovedJobsPartners" + +useMongo() + +describe("Canceling jobs_partners that have been removed from computed_jobs_partners", () => { + beforeEach(async () => { + // créations de plusieurs éléments existants dans jobs partners + // création de plusieurs éléments dans computed jobs partners . certains avec validated true, d'autres false + // certains éléments validated de computed sont déjà présents dans jobs partners + // certains éléments dans jobs partners ne sont pas dans computed + await createJobPartner({ partner_job_id: "existing_1", partner_label: "ft", offer_status: JOB_STATUS_ENGLISH.ACTIVE }) + await createJobPartner({ partner_job_id: "existing_2", partner_label: "ft", offer_status: JOB_STATUS_ENGLISH.ACTIVE }) + await createJobPartner({ partner_job_id: "existing_3", partner_label: "hw", offer_status: JOB_STATUS_ENGLISH.ACTIVE }) + await createJobPartner({ partner_job_id: "existing_4", partner_label: "hw", offer_status: JOB_STATUS_ENGLISH.ACTIVE }) + await createJobPartner({ partner_job_id: "existing_5", partner_label: "hw", offer_status: JOB_STATUS_ENGLISH.ACTIVE }) + await createComputedJobPartner({ partner_job_id: "existing_1", partner_label: "notft", validated: true }) + await createComputedJobPartner({ partner_job_id: "computed_1", partner_label: "ft", validated: true }) + await createComputedJobPartner({ partner_job_id: "computed_2", partner_label: "ft", validated: false }) + await createComputedJobPartner({ partner_job_id: "existing_3", partner_label: "hw", validated: true }) + await createComputedJobPartner({ partner_job_id: "existing_4", partner_label: "hw", validated: true }) + await createComputedJobPartner({ partner_job_id: "existing_5", partner_label: "hw", validated: false }) + + return async () => { + await getDbCollection("computed_jobs_partners").deleteMany({}) + await getDbCollection("jobs_partners").deleteMany({}) + } + }) + + it("L'annulation dans jobs_partners fonctionne comme attendue : \n- les éléments de jobs_partners qui ne sont plus dans computed doivent être taggés Annulé\n- les éléments de jobs_partners qui sont également dans computed sont toujours présents\n- aucun éléments de jobs_partners n'a été retiré de la collection", async () => { + await cancelRemovedJobsPartners() + + // les éléments de jobs_partners qui ne sont plus dans computed doivent être taggés Annulé + const countCanceledJobsPartners = await getDbCollection("jobs_partners").countDocuments({ + partner_job_id: { $in: ["existing_1", "existing_2"] }, + offer_status: JOB_STATUS_ENGLISH.ANNULEE, + }) + expect.soft(countCanceledJobsPartners).toEqual(2) + + // les éléments de jobs_partners qui sont également dans computed sont toujours présents + const countRemainingJobsPartners = await getDbCollection("jobs_partners").countDocuments({ + partner_job_id: { $in: ["existing_3", "existing_4", "existing_5"] }, + }) + expect.soft(countRemainingJobsPartners).toEqual(3) + + // aucun éléments de jobs_partners n'a été retiré de la collection + const countJobsPartners = await getDbCollection("jobs_partners").countDocuments({}) + expect.soft(countJobsPartners).toEqual(5) + }) +}) diff --git a/server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.ts b/server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.ts new file mode 100644 index 0000000000..f821fe1aae --- /dev/null +++ b/server/src/jobs/offrePartenaire/cancelRemovedJobsPartners.ts @@ -0,0 +1,45 @@ +import { JOB_STATUS_ENGLISH } from "shared/models" + +import { getDbCollection } from "@/common/utils/mongodbUtils" + +export const cancelRemovedJobsPartners = async () => { + const pipeline = [ + { + $lookup: { + from: "computed_jobs_partners", + let: { partnerLabel: "$partner_label", partnerJobId: "$partner_job_id" }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ["$partner_label", "$$partnerLabel"] }, { $eq: ["$partner_job_id", "$$partnerJobId"] }], + }, + }, + }, + ], + as: "matched", + }, + }, + { + $match: { + matched: { $size: 0 }, + }, + }, + { + $set: { offer_status: JOB_STATUS_ENGLISH.ANNULEE }, + }, + { + $unset: "matched", + }, + { + $merge: { + into: "jobs_partners", + on: ["partner_job_id", "partner_label"], + whenMatched: "merge", + whenNotMatched: "discard", + }, + }, + ] + + await getDbCollection("jobs_partners").aggregate(pipeline).toArray() +} diff --git a/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts b/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts index 63ed60c4c8..ae8642e47a 100644 --- a/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts +++ b/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts @@ -62,7 +62,7 @@ describe("helloWorkJobToJobsPartners", () => { ).toEqual({ _id: expect.any(ObjectId), created_at: now, - partner: "Hellowork", + partner_label: "Hello work", partner_job_id: "73228597", contract_start: new Date("2024-12-01T00:00:00.000+01:00"), contract_type: ["Apprentissage", "Professionnalisation"], diff --git a/server/src/jobs/offrePartenaire/helloWorkMapper.ts b/server/src/jobs/offrePartenaire/helloWorkMapper.ts index a455ffdfe0..8595b28bb3 100644 --- a/server/src/jobs/offrePartenaire/helloWorkMapper.ts +++ b/server/src/jobs/offrePartenaire/helloWorkMapper.ts @@ -85,11 +85,12 @@ export const helloWorkJobToJobsPartners = (job: IHelloWorkJob): IComputedJobsPar const { latitude, longitude } = geolocToLatLon(geoloc) const siretParsing = extensions.siret.safeParse(siret) const codeRomeParsing = extensions.romeCode().safeParse(code_rome) + const urlParsing = extensions.url().safeParse(url) const partnerJob: IComputedJobsPartners = { _id: new ObjectId(), created_at: new Date(), - partner: JOBPARTNERS_LABEL.HELLOWORK, + partner_label: JOBPARTNERS_LABEL.HELLOWORK, partner_job_id: job_id, contract_start: parseDate(contract_start_date), contract_type: contract.toLowerCase() === "alternance" ? [TRAINING_CONTRACT_TYPE.APPRENTISSAGE, TRAINING_CONTRACT_TYPE.PROFESSIONNALISATION] : undefined, @@ -122,7 +123,7 @@ export const helloWorkJobToJobsPartners = (job: IHelloWorkJob): IComputedJobsPar coordinates: [longitude, latitude], } : undefined, - apply_url: url, + apply_url: urlParsing.success ? urlParsing.data : null, errors: [], validated: false, } diff --git a/server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.test.ts b/server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.test.ts new file mode 100644 index 0000000000..c8ca93a551 --- /dev/null +++ b/server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.test.ts @@ -0,0 +1,52 @@ +import { createComputedJobPartner, createJobPartner } from "@tests/utils/jobsPartners.test.utils" +import { useMongo } from "@tests/utils/mongo.test.utils" +import { beforeEach, describe, expect, it } from "vitest" + +import { getDbCollection } from "@/common/utils/mongodbUtils" + +import { importFromComputedToJobsPartners } from "./importFromComputedToJobsPartners" + +useMongo() + +describe("Importing computed_jobs_partners into jobs_partners", () => { + const oldDesc = "description existante dans la table jobs_partners" + const newDesc = "nouvelle description dans la table computed_jobs_partners" + + beforeEach(async () => { + // créations de plusieurs éléments existants dans jobs partners + // création de plusieurs éléments dans computed jobs partners . certains avec validated true, d'autres false + // certains éléments validated de computed sont déjà présents dans jobs partners + await createJobPartner({ partner_job_id: "existing_1" }) + await createJobPartner({ partner_job_id: "existing_2" }) + await createJobPartner({ partner_job_id: "existing_3", offer_description: oldDesc }) + await createComputedJobPartner({ partner_job_id: "computed_1", validated: true }) + await createComputedJobPartner({ partner_job_id: "computed_2", validated: false }) + await createComputedJobPartner({ partner_job_id: "existing_3", offer_description: newDesc, validated: true }) + await createComputedJobPartner({ partner_job_id: "computed_4", validated: false }) + + return async () => { + await getDbCollection("computed_jobs_partners").deleteMany({}) + await getDbCollection("jobs_partners").deleteMany({}) + } + }) + + it("La transition de computed_jobs_partners vers jobs_partners fonctionne comme attendue : \n- les éléments non validated ne doivent pas se retrouver dans jobs partners\n- les éléments validated et absents initialement de jobs partners doivent se rerouver dans jobs partners\n- les éléments validated et déjà dans jobs partners doivent toujours y être avec les data modifiées à jour", async () => { + await importFromComputedToJobsPartners() + + // les éléments non validated ne doivent pas se retrouver dans jobs partners + const countNonValidatedInJobsPartners = await getDbCollection("jobs_partners").countDocuments({ partner_job_id: { $in: ["computed_2", "computed_4"] } }) + expect.soft(countNonValidatedInJobsPartners).toEqual(0) + + // les éléments validated et absents initialement de jobs partners doivent se rerouver dans jobs partners + const countNewValidatedInJobsPartners = await getDbCollection("jobs_partners").countDocuments({ partner_job_id: { $in: ["computed_1"] } }) + expect.soft(countNewValidatedInJobsPartners).toEqual(1) + + // les éléments qui existaient avant l'import sont toujours là + const countExistingStillHere = await getDbCollection("jobs_partners").countDocuments({ partner_job_id: { $in: ["existing_1", "existing_2", "existing_3"] } }) + expect.soft(countExistingStillHere).toEqual(3) + + // les éléments validated et déjà dans jobs partners doivent toujours y être avec les data modifiées à jour + const existing_3 = await getDbCollection("jobs_partners").findOne({ partner_job_id: "existing_3" }) + expect.soft(existing_3?.offer_description === newDesc) + }) +}) diff --git a/server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.ts b/server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.ts new file mode 100644 index 0000000000..0dc24079f6 --- /dev/null +++ b/server/src/jobs/offrePartenaire/importFromComputedToJobsPartners.ts @@ -0,0 +1,32 @@ +import { Transform } from "stream" +import { pipeline } from "stream/promises" + +import { ObjectId } from "mongodb" +import { IJobsPartnersOfferPrivate } from "shared/models/jobsPartners.model" + +import { getDbCollection } from "@/common/utils/mongodbUtils" + +export const importFromComputedToJobsPartners = async () => { + const stream = await getDbCollection("computed_jobs_partners").find({ validated: true }).project({ _id: 0, validated: 0, errors: 0 }).stream() + + const transform = new Transform({ + objectMode: true, + async transform(computedJobPartner: Omit, encoding, callback: (error?: Error | null, data?: any) => void) { + try { + await getDbCollection("jobs_partners").updateOne( + { partner_job_id: computedJobPartner.partner_job_id, partner_label: computedJobPartner.partner_label }, + { + $set: { ...computedJobPartner }, + $setOnInsert: { _id: new ObjectId() }, + }, + { upsert: true } + ) + callback(null) + } catch (err: unknown) { + err instanceof Error ? callback(err) : callback(new Error(String(err))) + } + }, + }) + + await pipeline(stream, transform) +} diff --git a/server/src/jobs/offrePartenaire/importHelloWork.ts b/server/src/jobs/offrePartenaire/importHelloWork.ts index 51d4cf96bf..c56133d450 100644 --- a/server/src/jobs/offrePartenaire/importHelloWork.ts +++ b/server/src/jobs/offrePartenaire/importHelloWork.ts @@ -6,6 +6,9 @@ import { rawToComputedJobsPartners } from "./rawToComputedJobsPartners" export const importHelloWork = async () => { await importFromUrlInXml({ destinationCollection: "raw_hellowork", url: "plop", offerXmlTag: "job" }) +} + +export const importRawHelloWorkIntoComputedJobPartners = async () => { await rawToComputedJobsPartners({ collectionSource: "raw_hellowork", partnerLabel: JOBPARTNERS_LABEL.HELLOWORK, diff --git a/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts b/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts index 8bad5c5316..36bdfb19f2 100644 --- a/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts +++ b/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts @@ -39,7 +39,7 @@ export const rawToComputedJobsPartners = async ({ const computedJobPartner = mapper(parsedDocument) await getDbCollection("computed_jobs_partners").insertOne({ ...computedJobPartner, - partner: partnerLabel, + partner_label: partnerLabel, validated: ZJobsPartnersOfferPrivate.safeParse(computedJobPartner).success, }) counters.success++ diff --git a/server/src/security/authorisationService.ts b/server/src/security/authorisationService.ts index 7cfe9d3f54..f565b3cebf 100644 --- a/server/src/security/authorisationService.ts +++ b/server/src/security/authorisationService.ts @@ -305,7 +305,7 @@ function canAccessEntreprise(userAccess: ComputedUserAccess, resource: Entrepris function canAccessJobPartner(userAccess: ComputedUserAccess, resource: JobPartnerResource): boolean { const { job } = resource - return userAccess.partner.includes(job.partner) + return userAccess.partner.includes(job.partner_label) } function isAuthorized(access: AccessPermission, userAccess: ComputedUserAccess, resources: Resources): boolean { diff --git a/server/src/services/etablissement.service.ts b/server/src/services/etablissement.service.ts index 18f9cd51a9..61a7b169fe 100644 --- a/server/src/services/etablissement.service.ts +++ b/server/src/services/etablissement.service.ts @@ -317,7 +317,7 @@ export const formatReferentielData = (d: IReferentiel): ICfaReferentielData => { type: "Point", coordinates: coords, }, - } + } as ICfaReferentielData const validation = ZCfaReferentielData.safeParse(referentielData) if (!validation.success) { sentryCaptureException(internal(`erreur de validation sur les données du référentiel CFA pour le siret=${d.siret}.`, { validationError: validation.error })) diff --git a/server/src/services/jobs/jobOpportunity/__snapshots__/jobOpportunity.service.test.ts.snap b/server/src/services/jobs/jobOpportunity/__snapshots__/jobOpportunity.service.test.ts.snap index a87cdef473..69e19b2054 100644 --- a/server/src/services/jobs/jobOpportunity/__snapshots__/jobOpportunity.service.test.ts.snap +++ b/server/src/services/jobs/jobOpportunity/__snapshots__/jobOpportunity.service.test.ts.snap @@ -26,8 +26,8 @@ exports[`createJobOffer > should create a job offer with the minimal data 1`] = "offer_target_diploma": null, "offer_title": "Apprentis en développement web", "offer_to_be_acquired_skills": [], - "partner": "Some organisation", - "partner_job_id": null, + "partner_job_id": "job-id-b", + "partner_label": "Some organisation", "updated_at": 2024-06-18T00:00:00.000Z, "workplace_address": { "label": "20 AVENUE DE SEGUR 75007 PARIS", @@ -84,8 +84,8 @@ exports[`findJobsOpportunities > should execute query 1`] = ` "Production, Fabrication: Réaliser des travaux de reprographie", "Organisation: Contrôler la conformité des données ou des documents", ], - "partner": "La bonne alternance", "partner_job_id": null, + "partner_label": "La bonne alternance", "workplace_address": { "label": "Paris", }, @@ -127,8 +127,8 @@ exports[`findJobsOpportunities > should execute query 1`] = ` "offer_target_diploma": null, "offer_title": "Super offre d'apprentissage", "offer_to_be_acquired_skills": [], - "partner": "France Travail", "partner_job_id": "1", + "partner_label": "France Travail", "workplace_address": { "label": "Paris", }, @@ -175,8 +175,8 @@ exports[`findJobsOpportunities > should execute query 1`] = ` "offer_target_diploma": null, "offer_title": "Une super offre d'alternance", "offer_to_be_acquired_skills": [], - "partner": "Hellowork", - "partner_job_id": null, + "partner_job_id": "job-id-1", + "partner_label": "Hello work", "updated_at": 2021-01-28T15:00:00.000Z, "workplace_address": { "label": "126 RUE DE L'UNIVERSITE 75007 PARIS", @@ -259,8 +259,8 @@ exports[`updateJobOffer > should update a job offer with the minimal data 1`] = "offer_target_diploma": null, "offer_title": "Apprentis en développement web", "offer_to_be_acquired_skills": [], - "partner": "Some organisation", - "partner_job_id": null, + "partner_job_id": "job-id-9", + "partner_label": "Some organisation", "updated_at": 2024-06-18T00:00:00.000Z, "workplace_address": { "label": "20 AVENUE DE SEGUR 75007 PARIS", diff --git a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts index 294d046d02..5e8d9a0a25 100644 --- a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts +++ b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts @@ -6,7 +6,7 @@ import { NIVEAUX_POUR_LBA, NIVEAUX_POUR_OFFRES_PE, RECRUITER_STATUS } from "shar import { generateCfaFixture } from "shared/fixtures/cfa.fixture" import { generateJobsPartnersOfferPrivate } from "shared/fixtures/jobPartners.fixture" import { generateRecruiterFixture } from "shared/fixtures/recruiter.fixture" -import { generateLbaConpanyFixture } from "shared/fixtures/recruteurLba.fixture" +import { generateLbaCompanyFixture } from "shared/fixtures/recruteurLba.fixture" import { clichyFixture, generateReferentielCommuneFixtures, levalloisFixture, marseilleFixture, parisFixture } from "shared/fixtures/referentiel/commune.fixture" import { generateReferentielRome } from "shared/fixtures/rome.fixture" import { generateUserWithAccountFixture } from "shared/fixtures/userWithAccount.fixture" @@ -47,7 +47,7 @@ afterEach(() => { describe("findJobsOpportunities", () => { const recruiters: ILbaCompany[] = [ - generateLbaConpanyFixture({ + generateLbaCompanyFixture({ siret: "11000001500013", raison_sociale: "ASSEMBLEE NATIONALE", enseigne: "ASSEMBLEE NATIONALE - La vraie", @@ -57,7 +57,7 @@ describe("findJobsOpportunities", () => { phone: "0100000000", last_update_at: new Date("2021-01-01"), }), - generateLbaConpanyFixture({ + generateLbaCompanyFixture({ siret: "77555848900073", raison_sociale: "GRAND PORT MARITIME DE MARSEILLE (GPMM)", rome_codes: ["M1602", "D1212"], @@ -66,7 +66,7 @@ describe("findJobsOpportunities", () => { phone: "0200000000", last_update_at: new Date("2022-01-01"), }), - generateLbaConpanyFixture({ + generateLbaCompanyFixture({ siret: "52951974600034", raison_sociale: "SOCIETE PARISIENNE DE LA PISCINE PONTOISE (S3P)", enseigne: "SOCIETE PARISIENNE DE LA PISCINE PONTOISE (S3P)", @@ -148,16 +148,19 @@ describe("findJobsOpportunities", () => { offer_rome_codes: ["M1602"], workplace_geopoint: parisFixture.centre, offer_creation: new Date("2021-01-01"), + partner_job_id: "job-id-1", }), generateJobsPartnersOfferPrivate({ offer_rome_codes: ["M1602", "D1214"], workplace_geopoint: marseilleFixture.centre, offer_creation: new Date("2022-01-01"), + partner_job_id: "job-id-2", }), generateJobsPartnersOfferPrivate({ offer_rome_codes: ["D1212"], workplace_geopoint: levalloisFixture.centre, offer_creation: new Date("2023-01-01"), + partner_job_id: "job-id-3", }), ] const ftJobs: FTJob[] = [ @@ -215,7 +218,7 @@ describe("findJobsOpportunities", () => { expect.objectContaining({ _id: null, partner_job_id: ftJobs[0].id, - partner: "France Travail", + partner_label: "France Travail", }), expect.objectContaining({ _id: partnerJobs[0]._id, @@ -288,7 +291,7 @@ describe("findJobsOpportunities", () => { expect.objectContaining({ _id: null, partner_job_id: ftJobs[0].id, - partner: "France Travail", + partner_label: "France Travail", }), expect.objectContaining({ _id: partnerJobs[0]._id, @@ -357,7 +360,7 @@ describe("findJobsOpportunities", () => { expect.objectContaining({ _id: null, partner_job_id: ftJobs[0].id, - partner: "France Travail", + partner_label: "France Travail", }), expect.objectContaining({ _id: partnerJobs[1]._id, @@ -615,7 +618,7 @@ describe("findJobsOpportunities", () => { expect.objectContaining({ _id: null, partner_job_id: ftJobs[0].id, - partner: "France Travail", + partner_label: "France Travail", }), expect.objectContaining({ _id: partnerJobs[0]._id, @@ -666,7 +669,7 @@ describe("findJobsOpportunities", () => { it("should limit companies to 150", async () => { const extraLbaCompanies: ILbaCompany[] = Array.from({ length: 200 }, () => - generateLbaConpanyFixture({ + generateLbaCompanyFixture({ geopoint: parisFixture.centre, rome_codes: ["M1602"], }) @@ -1249,10 +1252,11 @@ describe("findJobsOpportunities", () => { vi.mocked(searchForFtJobs).mockResolvedValue({ resultats: [] }) }) it("should limit jobs to 150", async () => { - const extraOffers: IJobsPartnersOfferPrivate[] = Array.from({ length: 300 }, () => + const extraOffers: IJobsPartnersOfferPrivate[] = Array.from({ length: 300 }, (e, idx) => generateJobsPartnersOfferPrivate({ workplace_geopoint: parisFixture.centre, offer_rome_codes: ["M1602"], + partner_job_id: `job-id-a-${idx}`, }) ) await getDbCollection("jobs_partners").insertMany(extraOffers) @@ -1301,6 +1305,7 @@ describe("findJobsOpportunities", () => { offer_rome_codes: ["M1602"], workplace_geopoint: parisFixture.centre, offer_multicast: false, + partner_job_id: "job-id-4", }) ) await getDbCollection("recruiters").deleteMany({}) @@ -1329,11 +1334,13 @@ describe("findJobsOpportunities", () => { offer_rome_codes: ["M1602"], workplace_geopoint: parisFixture.centre, offer_target_diploma: { european: "4", label: "BP, Bac, autres formations niveau (Bac)" }, + partner_job_id: "job-id-5", }), generateJobsPartnersOfferPrivate({ offer_rome_codes: ["M1602"], workplace_geopoint: parisFixture.centre, offer_target_diploma: { european: "3", label: "CAP, BEP, autres formations niveau (CAP)" }, + partner_job_id: "job-id-6", }), ]) await getDbCollection("recruiters").deleteMany({}) @@ -1569,6 +1576,7 @@ describe("findJobsOpportunities", () => { offer_rome_codes: ["D1212"], workplace_geopoint: levalloisFixture.centre, offer_creation: new Date("2024-01-01"), + partner_job_id: "job-id-7", // created_at reference the creation date of the job in LBA, not the offer so we don't sort by it created_at: new Date("2021-01-01"), }), @@ -1576,6 +1584,7 @@ describe("findJobsOpportunities", () => { offer_rome_codes: ["D1212"], workplace_geopoint: levalloisFixture.centre, offer_creation: new Date("2021-01-01"), + partner_job_id: "job-id-8", // created_at reference the creation date of the job in LBA, not the offer so we don't sort by it created_at: new Date("2024-01-01"), }), @@ -1584,7 +1593,7 @@ describe("findJobsOpportunities", () => { await getDbCollection("jobs_partners").insertMany(extraOffers) const extraLbaCompanies = [ - generateLbaConpanyFixture({ + generateLbaCompanyFixture({ siret: "52951974600034", raison_sociale: "EXTRA LBA COMPANY 1", rome_codes: ["D1211"], @@ -1592,7 +1601,7 @@ describe("findJobsOpportunities", () => { insee_city_code: levalloisFixture.code, last_update_at: new Date("2024-01-01"), }), - generateLbaConpanyFixture({ + generateLbaCompanyFixture({ siret: "52951974600034", raison_sociale: "EXTRA LBA COMPANY 2", rome_codes: ["D1211"], @@ -1619,70 +1628,70 @@ describe("findJobsOpportunities", () => { expect.soft(parseResult.success).toBeTruthy() expect(parseResult.error).toBeUndefined() expect({ - jobs: results.jobs.map((j) => ({ _id: j._id, partner_job_id: j.partner_job_id, partner: j.partner, workplace_legal_name: j.workplace_legal_name })), + jobs: results.jobs.map((j) => ({ _id: j._id, partner_job_id: j.partner_job_id, partner_label: j.partner_label, workplace_legal_name: j.workplace_legal_name })), recruiters: results.recruiters.map((j) => ({ _id: j._id, workplace_legal_name: j.workplace_legal_name })), }).toEqual({ jobs: [ { // Paris _id: lbaJobs[0].jobs[0]._id.toString(), - partner: "La bonne alternance", + partner_label: "La bonne alternance", partner_job_id: null, workplace_legal_name: lbaJobs[0].establishment_raison_sociale, }, { // Levallois - 2024-01-01 _id: extraLbaJob.jobs[1]._id.toString(), - partner: "La bonne alternance", + partner_label: "La bonne alternance", partner_job_id: null, workplace_legal_name: extraLbaJob.establishment_raison_sociale, }, { // Levallois - 2023-01-01 _id: lbaJobs[2].jobs[0]._id.toString(), - partner: "La bonne alternance", + partner_label: "La bonne alternance", partner_job_id: null, workplace_legal_name: lbaJobs[2].establishment_raison_sociale, }, { // Levallois - 2021-01-01 _id: extraLbaJob.jobs[0]._id.toString(), - partner: "La bonne alternance", + partner_label: "La bonne alternance", partner_job_id: null, workplace_legal_name: extraLbaJob.establishment_raison_sociale, }, { _id: null, partner_job_id: ftJobs[0].id, - partner: "France Travail", + partner_label: "France Travail", workplace_legal_name: null, }, // Paris { _id: partnerJobs[0]._id, - partner: "Hellowork", - partner_job_id: null, + partner_label: "Hello work", + partner_job_id: expect.any(String), workplace_legal_name: partnerJobs[0].workplace_legal_name, }, // Levallois - 2024-01-01 { _id: extraOffers[0]._id, - partner: "Hellowork", - partner_job_id: null, + partner_label: "Hello work", + partner_job_id: expect.any(String), workplace_legal_name: extraOffers[0].workplace_legal_name, }, // Levallois - 2023-01-01 { _id: partnerJobs[2]._id, - partner: "Hellowork", - partner_job_id: null, + partner_label: "Hello work", + partner_job_id: expect.any(String), workplace_legal_name: partnerJobs[2].workplace_legal_name, }, // Levallois - 2021-01-01 { _id: extraOffers[1]._id, - partner: "Hellowork", - partner_job_id: null, + partner_label: "Hello work", + partner_job_id: expect.any(String), workplace_legal_name: extraOffers[1].workplace_legal_name, }, ], @@ -1799,12 +1808,12 @@ describe("createJobOffer", () => { }) it("should create a job offer with the minimal data", async () => { - const result = await createJobOffer(identity, minimalData) + const result = await createJobOffer(identity, { ...minimalData, partner_job_id: "job-id-b" }) expect(result).toBeInstanceOf(ObjectId) const job = await getDbCollection("jobs_partners").findOne({ _id: result }) expect(job?.created_at).toEqual(now) - expect(job?.partner).toEqual(identity.organisation) + expect(job?.partner_label).toEqual(identity.organisation) expect(job?.offer_rome_codes).toEqual(["M1602"]) expect(job?.offer_status).toEqual(JOB_STATUS.ACTIVE) expect(job?.offer_creation).toEqual(now) @@ -1866,7 +1875,7 @@ describe("updateJobOffer", () => { const originalJob = generateJobsPartnersOfferPrivate({ _id, - partner: identity.organisation, + partner_label: identity.organisation, created_at: originalCreatedAt, offer_creation: originalCreatedAt, offer_expiration: originalCreatedAtPlus2Months, @@ -1934,11 +1943,11 @@ describe("updateJobOffer", () => { }) it("should update a job offer with the minimal data", async () => { - await updateJobOffer(_id, identity, minimalData) + await updateJobOffer(_id, identity, { ...minimalData, partner_job_id: "job-id-9" }) const job = await getDbCollection("jobs_partners").findOne({ _id }) expect(job?.created_at).toEqual(originalCreatedAt) - expect(job?.partner).toEqual(identity.organisation) + expect(job?.partner_label).toEqual(identity.organisation) expect(job?.offer_rome_codes).toEqual(["M1602"]) expect(job?.offer_status).toEqual(JOB_STATUS.ACTIVE) expect(job?.offer_creation).toEqual(originalCreatedAt) @@ -1958,7 +1967,7 @@ describe("updateJobOffer", () => { it("should get default rome from ROMEO", async () => { vi.mocked(getRomeoPredictions).mockResolvedValue(franceTravailRomeoFixture["Software Engineer"]) - await updateJobOffer(_id, identity, { ...minimalData, offer_rome_codes: [] }) + await updateJobOffer(_id, identity, { ...minimalData, partner_job_id: "job-id-10", offer_rome_codes: [] }) const job = await getDbCollection("jobs_partners").findOne({ _id }) expect(job?.offer_rome_codes).toEqual(["E1206"]) @@ -1973,7 +1982,7 @@ describe("updateJobOffer", () => { features: [{ geometry: clichyFixture.centre }], }) - await updateJobOffer(_id, identity, { ...minimalData, workplace_address_label: "1T impasse Passoir Clichy" }) + await updateJobOffer(_id, identity, { ...minimalData, partner_job_id: "job-id-11", workplace_address_label: "1T impasse Passoir Clichy" }) const job = await getDbCollection("jobs_partners").findOne({ _id }) expect(job?.workplace_address).toEqual({ label: "1T impasse Passoir Clichy" }) diff --git a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts index 5b0c5a14fa..3fed79f8f5 100644 --- a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts +++ b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts @@ -319,7 +319,7 @@ export const convertLbaRecruiterToJobPartnerOfferApi = (offresEmploiLba: IJobRes .map( ({ recruiter, job }: IJobResult): IJobsPartnersOfferApi => ({ _id: job._id.toString(), - partner: JOBPARTNERS_LABEL.OFFRES_EMPLOI_LBA, + partner_label: JOBPARTNERS_LABEL.OFFRES_EMPLOI_LBA, partner_job_id: null, contract_start: job.job_start_date, contract_duration: job.job_duration ?? null, @@ -372,7 +372,7 @@ export const convertFranceTravailJobToJobPartnerOfferApi = (offresEmploiFranceTr return { _id: null, partner_job_id: offreFT.id, - partner: JOBPARTNERS_LABEL.OFFRES_EMPLOI_FRANCE_TRAVAIL, + partner_label: JOBPARTNERS_LABEL.OFFRES_EMPLOI_FRANCE_TRAVAIL, contract_start: null, contract_duration: isNaN(contractDuration) ? null : contractDuration, @@ -601,7 +601,7 @@ async function resolveRomeCodes(data: IJobsPartnersWritableApi, siretData: Workp return [romeoResponse] } -type InvariantFields = "_id" | "created_at" | "partner" +type InvariantFields = "_id" | "created_at" | "partner_label" async function upsertJobOffer(data: IJobsPartnersWritableApi, identity: IApiApprentissageTokenData, current: IJobsPartnersOfferPrivate | null): Promise { const zodError = new ZodError([]) @@ -627,7 +627,7 @@ async function upsertJobOffer(data: IJobsPartnersWritableApi, identity: IApiAppr const invariantData: Pick = { _id: current?._id ?? new ObjectId(), created_at: current?.created_at ?? now, - partner: identity.organisation, + partner_label: identity.organisation, } const defaultOfferExpiration = current?.offer_expiration diff --git a/server/src/services/recruteurLba.service.test.ts b/server/src/services/recruteurLba.service.test.ts index cf84ff162f..3686561e4b 100644 --- a/server/src/services/recruteurLba.service.test.ts +++ b/server/src/services/recruteurLba.service.test.ts @@ -12,7 +12,12 @@ useMongo() describe("/lbacompany/:siret/contactInfo", () => { beforeEach(async () => { - await createApplicationTest({ company_siret: "34843069553553", company_email: "application_company_email@test.com", company_name: "fake_company_name" }) + await createApplicationTest({ + company_siret: "34843069553553", + company_email: "application_company_email@test.com", + company_name: "fake_company_name", + applicant_attachment_name: "cv.pdf", + }) await createRecruteurLbaTest({ email: "recruteur_lba@test.com", phone: "0610101010", siret: "58006820882692", enseigne: "fake_company_name" }) return async () => { diff --git a/server/tests/utils/jobsPartners.test.utils.ts b/server/tests/utils/jobsPartners.test.utils.ts new file mode 100644 index 0000000000..dffdd109d8 --- /dev/null +++ b/server/tests/utils/jobsPartners.test.utils.ts @@ -0,0 +1,29 @@ +import { generateJobsPartnersOfferPrivate } from "shared/fixtures/jobPartners.fixture" +import { IJobsPartnersOfferPrivate, ZJobsPartnersOfferPrivate } from "shared/models/jobsPartners.model" +import { IComputedJobsPartners } from "shared/models/jobsPartnersComputed.model " + +import { getDbCollection } from "@/common/utils/mongodbUtils" + +import { saveDbEntity } from "./user.test.utils" + +export async function createComputedJobPartner(data: Partial) { + const cjp = { + errors: [], + validated: true, + ...generateJobsPartnersOfferPrivate(data), + } + await getDbCollection("computed_jobs_partners").insertOne(cjp) + return cjp +} + +export async function createJobPartner(data: Partial) { + const jp = { + ...generateJobsPartnersOfferPrivate(data), + } + await getDbCollection("jobs_partners").insertOne(jp) + return jp +} + +export async function saveJobPartnerTest(data: Partial = {}): Promise { + return await saveDbEntity(ZJobsPartnersOfferPrivate, (item) => getDbCollection("jobs_partners").insertOne(item), data) +} diff --git a/server/tests/utils/user.test.utils.ts b/server/tests/utils/user.test.utils.ts index 4274551a46..4b4282e121 100644 --- a/server/tests/utils/user.test.utils.ts +++ b/server/tests/utils/user.test.utils.ts @@ -20,7 +20,6 @@ import { import { ICFA, zCFA } from "shared/models/cfa.model" import { zObjectId } from "shared/models/common" import { EntrepriseStatus, IEntreprise, IEntrepriseStatusEvent, ZEntreprise } from "shared/models/entreprise.model" -import { IJobsPartnersOfferPrivate, ZJobsPartnersOfferPrivate } from "shared/models/jobsPartners.model" import { AccessEntityType, AccessStatus, IRoleManagement, IRoleManagementEvent } from "shared/models/roleManagement.model" import { IUserWithAccount, UserEventType, ZUserWithAccount } from "shared/models/userWithAccount.model" import { ZodArray, ZodObject, ZodString, ZodTypeAny, z } from "zod" @@ -41,12 +40,12 @@ function generateRandomRomeCode() { } let seed = 0 -function getFixture() { +export function getFixture() { seed++ return new Fixture({ seed }).extend([ Generator({ schema: ZodArray, - filter: ({ context }) => context.path.at(-1) === "offer_rome_code", + filter: ({ context }) => context.path.at(-1) === "offer_rome_codes", output: () => [generateRandomRomeCode()], }), Generator({ @@ -262,10 +261,6 @@ export async function createEmailBlacklistTest(data: Partial) { return u } -export async function saveJobPartnerTest(data: Partial = {}): Promise { - return await saveDbEntity(ZJobsPartnersOfferPrivate, (item) => getDbCollection("jobs_partners").insertOne(item), data) -} - export async function createRecruteurLbaTest(data: Partial): Promise { return await saveDbEntity(ZLbaCompany, (item) => getDbCollection("recruteurslba").insertOne(item), data) } diff --git a/shared/fixtures/jobPartners.fixture.ts b/shared/fixtures/jobPartners.fixture.ts index c18abf7b27..650694689b 100644 --- a/shared/fixtures/jobPartners.fixture.ts +++ b/shared/fixtures/jobPartners.fixture.ts @@ -25,7 +25,7 @@ export function generateJobsPartnersOfferPrivate(data: Partial): ILbaCompany { +export function generateLbaCompanyFixture(data: Partial): ILbaCompany { return { _id: new ObjectId(), siret: "11000001500013", diff --git a/shared/models/jobsPartners.model.ts b/shared/models/jobsPartners.model.ts index cb4be2758f..0cca2cdf7a 100644 --- a/shared/models/jobsPartners.model.ts +++ b/shared/models/jobsPartners.model.ts @@ -11,7 +11,7 @@ import { zOpcoLabel } from "./opco.model" const collectionName = "jobs_partners" as const export enum JOBPARTNERS_LABEL { - HELLOWORK = "Hellowork", + HELLOWORK = "Hello work", OFFRES_EMPLOI_LBA = "La bonne alternance", OFFRES_EMPLOI_FRANCE_TRAVAIL = "France Travail", } @@ -48,7 +48,7 @@ export const ZJobsPartnersOfferApi = ZJobsPartnersRecruiterApi.omit({ }).extend({ _id: z.union([zObjectId, z.string()]).nullable().describe("Identifiant de l'offre"), - partner: z.string().describe("Référence du partenaire"), + partner_label: z.string().describe("Référence du partenaire"), partner_job_id: z.string().nullable().describe("Identifiant d'origine l'offre provenant du partenaire").default(null), contract_start: z.date().nullable().describe("Date de début de contrat"), @@ -178,8 +178,7 @@ export default { [{ offer_multicast: 1, offer_rome_codes: 1, offer_creation: -1 }, {}], [{ offer_multicast: 1, "offer_target_diploma.european": 1, offer_creation: -1 }, {}], [{ offer_multicast: 1, offer_rome_codes: 1, "offer_target_diploma.european": 1, offer_creation: -1 }, {}], - - [{ partner: 1, partner_job_id: 1 }, {}], + [{ partner_label: 1, partner_job_id: 1 }, { unique: true }], ], collectionName, } as const satisfies IModelDescriptor diff --git a/shared/models/jobsPartnersComputed.model .ts b/shared/models/jobsPartnersComputed.model .ts index f2ee47f74e..0df46c39cb 100644 --- a/shared/models/jobsPartnersComputed.model .ts +++ b/shared/models/jobsPartnersComputed.model .ts @@ -14,7 +14,7 @@ export enum COMPUTED_ERROR_SOURCE { API_ROMEO = "api_romeo", } -export const ZComputedJobsPatners = ZJobsPartnersOfferPrivate.partial().extend({ +export const ZComputedJobsPartners = ZJobsPartnersOfferPrivate.partial().extend({ errors: z.array( z .object({ @@ -26,15 +26,16 @@ export const ZComputedJobsPatners = ZJobsPartnersOfferPrivate.partial().extend({ ), validated: z.boolean().default(false).describe("Toutes les données nécessaires au passage vers jobs_partners sont présentes et valides"), }) -export type IComputedJobsPartners = z.output +export type IComputedJobsPartners = z.output export default { - zod: ZComputedJobsPatners, + zod: ZComputedJobsPartners, indexes: [ - [{ partner_id: 1 }, {}], + [{ partner_job_id: 1 }, {}], [{ partner_label: 1 }, {}], [{ validated: 1 }, {}], [{ errors: 1 }, {}], + [{ partner_label: 1, partner_job_id: 1 }, { unique: true }], ], collectionName, } as const satisfies IModelDescriptor From dc48f495377f2b9379d11541a79920c7325293f8 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 12 Sep 2024 10:09:32 +0200 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20LBA-2354=20activation=20de=20la=20b?= =?UTF-8?q?anni=C3=A8re=20info=20bleue=20sur=20les=20espaces=20pro=20(#135?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: activation de la bannière info bleue sur les espaces pro * feat: activation de la bannière info bleue sur les espaces pro * Update InfoBanner.tsx * Update InfoBanner.tsx * fix: linting --------- Co-authored-by: guilletmarion <57708683+guilletmarion@users.noreply.github.com> Co-authored-by: Kevin Barnoin --- .talismanrc | 2 ++ ui/components/InfoBanner/InfoBanner.tsx | 8 +++++--- ui/components/espace_pro/AuthentificationLayout.tsx | 2 +- ui/components/espace_pro/Layout/Header.tsx | 2 +- ui/components/espace_pro/common/components/Layout.tsx | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.talismanrc b/.talismanrc index c1712e50c7..6b9d30925d 100644 --- a/.talismanrc +++ b/.talismanrc @@ -157,6 +157,8 @@ fileignoreconfig: checksum: 477c08911f43c273ad8b257c3885ae98b92e6a74c8ad096c7023201762907414 - filename: ui/components/HomeComponents/FacilitezRDVA.tsx checksum: cdeae2ef94431bbe8fd1bb0f1bc0c007c938c1a1d4513316cc44cb966b7f4268 +- filename: ui/components/InfoBanner/InfoBanner.tsx + checksum: e5f8bd78ea46335fbb8784eabc01391be68922960f7d04ce9e8755484d10ce32 - filename: ui/components/ItemDetail/ItemDetail.tsx checksum: 1fcc0442306f83b5e45bf7da67304527598d7749b9e2642c6d4628d3b4f15a9c - filename: ui/components/ItemDetail/LbaJobComponents/LbaJobTechniques.tsx diff --git a/ui/components/InfoBanner/InfoBanner.tsx b/ui/components/InfoBanner/InfoBanner.tsx index 5572b9d46c..3ecff7b156 100644 --- a/ui/components/InfoBanner/InfoBanner.tsx +++ b/ui/components/InfoBanner/InfoBanner.tsx @@ -8,10 +8,12 @@ import { DisplayContext } from "@/context/DisplayContextProvider" const blueBannerText = ( - La bonne alternance évolue ! + La bonne alternance évolue pour les recruteurs ! {" "} - Vous pouvez désormais être plusieurs utilisateurs au sein de votre organisation à gérer les offres d’emploi de votre entreprise.{" "} - En savoir plus + Vous pouvez désormais personnaliser le contenu de vos offres en décochant certains éléments du descriptif généré automatiquement{" "} + + En savoir plus + ) diff --git a/ui/components/espace_pro/AuthentificationLayout.tsx b/ui/components/espace_pro/AuthentificationLayout.tsx index 718bfea894..74a5f12511 100644 --- a/ui/components/espace_pro/AuthentificationLayout.tsx +++ b/ui/components/espace_pro/AuthentificationLayout.tsx @@ -28,7 +28,7 @@ export default function AuthentificationLayout(props) { return ( - + diff --git a/ui/components/espace_pro/Layout/Header.tsx b/ui/components/espace_pro/Layout/Header.tsx index f263eff6da..536dfc441f 100644 --- a/ui/components/espace_pro/Layout/Header.tsx +++ b/ui/components/espace_pro/Layout/Header.tsx @@ -28,7 +28,7 @@ const Header = () => { return ( - + diff --git a/ui/components/espace_pro/common/components/Layout.tsx b/ui/components/espace_pro/common/components/Layout.tsx index 481ff95d14..2a259b6138 100644 --- a/ui/components/espace_pro/common/components/Layout.tsx +++ b/ui/components/espace_pro/common/components/Layout.tsx @@ -15,7 +15,7 @@ import NavigationMenu from "./NavigationMenu" const Layout = ({ children }) => { return ( - +
{children} From b4a05cb45e1cfdd8a55260011f4eabea459d2807 Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Thu, 12 Sep 2024 11:22:05 +0200 Subject: [PATCH 4/4] fix: ajustement du model job-partner (#1506) * fix: ajustement du model job-partner * fix: merge conflicts --------- Co-authored-by: Alan Le Ruyet --- .../controllers/v2/jobs.controller.v2.test.ts | 42 +---- .../offrePartenaire/helloWorkMapper.test.ts | 2 +- .../rawToComputedJobsPartners.ts | 8 +- server/src/security/authorisationService.ts | 10 +- .../jobOpportunity.service.test.ts.snap | 15 +- .../jobOpportunity.service.test.ts | 8 +- .../jobOpportunity/jobOpportunity.service.ts | 18 ++- server/src/services/roleManagement.service.ts | 2 +- shared/fixtures/jobPartners.fixture.ts | 4 +- shared/models/computedUserAccess.model.ts | 2 +- shared/models/jobPartners.model.test.ts | 153 ++++++++++++++++++ .../jobPartners/jobPartners.model.test.ts | 34 +--- shared/models/jobsPartners.model.ts | 29 +++- 13 files changed, 220 insertions(+), 107 deletions(-) create mode 100644 shared/models/jobPartners.model.test.ts diff --git a/server/src/http/controllers/v2/jobs.controller.v2.test.ts b/server/src/http/controllers/v2/jobs.controller.v2.test.ts index 703ea34cb5..989d4a748e 100644 --- a/server/src/http/controllers/v2/jobs.controller.v2.test.ts +++ b/server/src/http/controllers/v2/jobs.controller.v2.test.ts @@ -284,35 +284,15 @@ describe("POST /jobs", async () => { const inSept = new Date("2024-09-01T00:00:00.000Z") const data: IJobsPartnersWritableApiInput = { - partner_job_id: null, - contract_start: inSept.toJSON(), - contract_duration: null, - contract_type: null, - contract_remote: null, offer_title: "Apprentis en développement web", offer_rome_codes: ["M1602"], - offer_desired_skills: [], - offer_to_be_acquired_skills: [], - offer_access_conditions: [], - offer_creation: null, - offer_expiration: null, - offer_opening_count: 1, - offer_origin: null, - offer_multicast: true, offer_description: "Envie de devenir développeur web ? Rejoignez-nous !", - offer_diploma_level_european: null, - apply_url: null, apply_email: "mail@mail.com", - apply_phone: null, workplace_siret: apiEntrepriseEtablissementFixture.dinum.data.siret, - workplace_address_label: null, - workplace_description: null, - workplace_website: null, - workplace_name: null, } beforeEach(async () => { @@ -434,35 +414,15 @@ describe("PUT /jobs/:id", async () => { const originalJob = generateJobsPartnersOfferPrivate({ _id: id, offer_title: "Old title", partner_label: "Un super Partenaire" }) const data: IJobsPartnersWritableApiInput = { - partner_job_id: null, - contract_start: inSept.toJSON(), - contract_duration: null, - contract_type: null, - contract_remote: null, offer_title: "Apprentis en développement web", offer_rome_codes: ["M1602"], - offer_desired_skills: [], - offer_to_be_acquired_skills: [], - offer_access_conditions: [], - offer_creation: null, - offer_expiration: null, - offer_opening_count: 1, - offer_origin: null, - offer_multicast: true, offer_description: "Envie de devenir développeur web ? Rejoignez-nous !", - offer_diploma_level_european: null, - apply_url: null, apply_email: "mail@mail.com", - apply_phone: null, workplace_siret: apiEntrepriseEtablissementFixture.dinum.data.siret, - workplace_address_label: null, - workplace_description: null, - workplace_website: null, - workplace_name: null, } beforeEach(async () => { @@ -551,7 +511,7 @@ describe("PUT /jobs/:id", async () => { expect(response.json()).toEqual({ error: "Forbidden", message: "Unauthorized", statusCode: 403 }) }) - it("should return 403 if user is trying to edit other partner job", async () => { + it("should return 403 if user is trying to edit other partner_label job", async () => { const restrictedToken = getApiApprentissageTestingToken({ email: "mail@mail.com", organisation: "Un autre", habilitations: { "jobs:write": true } }) const response = await httpClient().inject({ diff --git a/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts b/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts index ae8642e47a..54e500a52f 100644 --- a/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts +++ b/server/src/jobs/offrePartenaire/helloWorkMapper.test.ts @@ -15,7 +15,7 @@ describe("helloWorkJobToJobsPartners", () => { } }) - it("should convert a hellowork job to a partner job", () => { + it("should convert a hellowork job to a partner_label job", () => { expect( helloWorkJobToJobsPartners({ job_id: "73228597", diff --git a/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts b/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts index 36bdfb19f2..e11cdceb3b 100644 --- a/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts +++ b/server/src/jobs/offrePartenaire/rawToComputedJobsPartners.ts @@ -23,9 +23,9 @@ export const rawToComputedJobsPartners = async ({ partnerLabel: JOBPARTNERS_LABEL documentJobRoot?: string }) => { - logger.info(`début d'import dans computed_jobs_partners pour partner=${partnerLabel}`) + logger.info(`début d'import dans computed_jobs_partners pour partner_label=${partnerLabel}`) const deletedCount = await getDbCollection("computed_jobs_partners").countDocuments({ partner_label: partnerLabel }) - logger.info(`suppression de ${deletedCount} documents dans computed_jobs_partners pour partner=${partnerLabel}`) + logger.info(`suppression de ${deletedCount} documents dans computed_jobs_partners pour partner_label=${partnerLabel}`) await getDbCollection("computed_jobs_partners").deleteMany({ partner_label: partnerLabel }) const counters = { total: 0, success: 0, error: 0 } await oleoduc( @@ -45,7 +45,7 @@ export const rawToComputedJobsPartners = async ({ counters.success++ } catch (err) { counters.error++ - const newError = internal(`error converting raw job to partner job for partner=${partnerLabel}, id=${document._id}`) + const newError = internal(`error converting raw job to partner_label job for partner_label=${partnerLabel}, id=${document._id}`) logger.error(newError.message, err) newError.cause = err sentryCaptureException(newError) @@ -54,5 +54,5 @@ export const rawToComputedJobsPartners = async ({ { parallel: 10 } ) ) - logger.info(`import dans computed_jobs_partners pour partner=${partnerLabel} terminé`, counters) + logger.info(`import dans computed_jobs_partners pour partner_label=${partnerLabel} terminé`, counters) } diff --git a/server/src/security/authorisationService.ts b/server/src/security/authorisationService.ts index f565b3cebf..4e5859d034 100644 --- a/server/src/security/authorisationService.ts +++ b/server/src/security/authorisationService.ts @@ -5,7 +5,7 @@ import { ADMIN, CFA, ENTREPRISE, OPCOS_LABEL } from "shared/constants/recruteur" import { ComputedUserAccess, IApplication, IJob, IRecruiter } from "shared/models" import { ICFA } from "shared/models/cfa.model" import { IEntreprise } from "shared/models/entreprise.model" -import { IJobsPartnersOfferApi } from "shared/models/jobsPartners.model" +import { IJobsPartnersOfferPrivate } from "shared/models/jobsPartners.model" import { AccessEntityType, IRoleManagement } from "shared/models/roleManagement.model" import { IRouteSchema, WithSecurityScheme } from "shared/routes/common.routes" import { AccessPermission, AccessResourcePath } from "shared/security/permissions" @@ -22,7 +22,7 @@ type RecruiterResource = { recruiter: IRecruiter } & ({ type: "ENTREPRISE"; entr type JobResource = { job: IJob; recruiterResource: RecruiterResource } type ApplicationResource = { application: IApplication; jobResource?: JobResource; applicantId?: string } type EntrepriseResource = { entreprise: IEntreprise } -type JobPartnerResource = { job: IJobsPartnersOfferApi } +type JobPartnerResource = { job: IJobsPartnersOfferPrivate } type Resources = { users: Array<{ _id: string }> @@ -305,7 +305,7 @@ function canAccessEntreprise(userAccess: ComputedUserAccess, resource: Entrepris function canAccessJobPartner(userAccess: ComputedUserAccess, resource: JobPartnerResource): boolean { const { job } = resource - return userAccess.partner.includes(job.partner_label) + return userAccess.partner_label.includes(job.partner_label) } function isAuthorized(access: AccessPermission, userAccess: ComputedUserAccess, resources: Resources): boolean { @@ -384,7 +384,7 @@ export async function authorizationMiddleware should create a job offer with the minimal data 1`] = "contract_duration": null, "contract_remote": null, "contract_start": 2024-09-01T00:00:00.000Z, - "contract_type": null, + "contract_type": [ + "Apprentissage", + "Professionnalisation", + ], "created_at": 2024-06-18T00:00:00.000Z, "offer_access_conditions": [], "offer_creation": 2024-06-18T00:00:00.000Z, @@ -157,7 +160,10 @@ exports[`findJobsOpportunities > should execute query 1`] = ` "contract_duration": null, "contract_remote": null, "contract_start": null, - "contract_type": null, + "contract_type": [ + "Apprentissage", + "Professionnalisation", + ], "created_at": 2021-01-28T15:00:00.000Z, "distance": 0, "offer_access_conditions": [], @@ -242,7 +248,10 @@ exports[`updateJobOffer > should update a job offer with the minimal data 1`] = "contract_duration": null, "contract_remote": null, "contract_start": 2024-09-01T00:00:00.000Z, - "contract_type": null, + "contract_type": [ + "Apprentissage", + "Professionnalisation", + ], "created_at": 2023-09-05T22:00:00.000Z, "offer_access_conditions": [], "offer_creation": 2023-09-05T22:00:00.000Z, diff --git a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts index 5e8d9a0a25..5803b148e9 100644 --- a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts +++ b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.test.ts @@ -1753,7 +1753,7 @@ describe("createJobOffer", () => { contract_start: inSept, contract_duration: null, - contract_type: null, + contract_type: ["Apprentissage", "Professionnalisation"], contract_remote: null, offer_title: "Apprentis en développement web", @@ -1767,7 +1767,7 @@ describe("createJobOffer", () => { offer_origin: null, offer_multicast: true, offer_description: "Envie de devenir développeur web ? Rejoignez-nous !", - offer_diploma_level_european: null, + offer_target_diploma_european: null, apply_url: null, apply_email: null, @@ -1886,7 +1886,7 @@ describe("updateJobOffer", () => { contract_start: inSept, contract_duration: null, - contract_type: null, + contract_type: ["Apprentissage", "Professionnalisation"], contract_remote: null, offer_title: "Apprentis en développement web", @@ -1900,7 +1900,7 @@ describe("updateJobOffer", () => { offer_origin: null, offer_multicast: true, offer_description: "Envie de devenir développeur web ? Rejoignez-nous !", - offer_diploma_level_european: null, + offer_target_diploma_european: null, apply_url: null, apply_email: null, diff --git a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts index 3fed79f8f5..9a4ede8254 100644 --- a/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts +++ b/server/src/services/jobs/jobOpportunity/jobOpportunity.service.ts @@ -197,7 +197,7 @@ export const getJobsQuery = async ( return result } -export const getJobsPartnersFromDB = async ({ romes, geo, target_diploma_level }: IJobOpportunityGetQueryResolved): Promise => { +export const getJobsPartnersFromDB = async ({ romes, geo, target_diploma_level }: IJobOpportunityGetQueryResolved): Promise => { const query: Filter = { offer_multicast: true, } @@ -225,7 +225,7 @@ export const getJobsPartnersFromDB = async ({ romes, geo, target_diploma_level } { $sort: { distance: 1, offer_creation: -1 } }, ] - return await getDbCollection("jobs_partners") + const jobsPartners = await getDbCollection("jobs_partners") .aggregate([ ...filterStages, { @@ -233,6 +233,12 @@ export const getJobsPartnersFromDB = async ({ romes, geo, target_diploma_level } }, ]) .toArray() + + return jobsPartners.map((j) => ({ + ...j, + // TODO: set LBA url + apply_url: j.apply_url ?? `${config.publicUrl}/recherche-apprentissage`, + })) } const convertToGeopoint = ({ longitude, latitude }: { longitude: number; latitude: number }): IGeoPoint => ({ type: "Point", coordinates: [longitude, latitude] }) @@ -621,7 +627,7 @@ async function upsertJobOffer(data: IJobsPartnersWritableApi, identity: IApiAppr throw internal("unexpected: cannot resolve all required data for the job offer") } - const { offer_creation, offer_expiration, offer_rome_codes, offer_diploma_level_european, workplace_address_label, ...rest } = data + const { offer_creation, offer_expiration, offer_rome_codes, offer_target_diploma_european, workplace_address_label, ...rest } = data const now = new Date() const invariantData: Pick = { @@ -640,11 +646,11 @@ async function upsertJobOffer(data: IJobsPartnersWritableApi, identity: IApiAppr offer_creation: offer_creation ?? invariantData.created_at, offer_expiration: offer_expiration || defaultOfferExpiration, offer_target_diploma: - offer_diploma_level_european == null + offer_target_diploma_european == null ? null : { - european: offer_diploma_level_european, - label: NIVEAU_DIPLOME_LABEL[offer_diploma_level_european], + european: offer_target_diploma_european, + label: NIVEAU_DIPLOME_LABEL[offer_target_diploma_european], }, updated_at: now, ...rest, diff --git a/server/src/services/roleManagement.service.ts b/server/src/services/roleManagement.service.ts index d05df11184..24a5dc66d7 100644 --- a/server/src/services/roleManagement.service.ts +++ b/server/src/services/roleManagement.service.ts @@ -173,7 +173,7 @@ export const getComputedUserAccess = (userId: string, grantedRoles: IRoleManagem } return [] }), - partner: [], + partner_label: [], } return userAccess } diff --git a/shared/fixtures/jobPartners.fixture.ts b/shared/fixtures/jobPartners.fixture.ts index 650694689b..de0c8ea563 100644 --- a/shared/fixtures/jobPartners.fixture.ts +++ b/shared/fixtures/jobPartners.fixture.ts @@ -23,14 +23,14 @@ export function generateJobsPartnersOfferPrivate(data: Partial + contract_remote: TRAINING_REMOTE_TYPE | null + + offer_title: string + offer_rome_codes: string[] + offer_description: string + offer_target_diploma: { + european: "3" | "4" | "5" | "6" | "7" + label: string + } | null + offer_desired_skills: string[] + offer_to_be_acquired_skills: string[] + offer_access_conditions: string[] + offer_creation: Date | null + offer_expiration: Date | null + offer_opening_count: number + offer_status: JOB_STATUS_ENGLISH + } + +type IJobOfferWritableExpected = { + partner_job_id: IJobOfferExpected["partner_job_id"] + + contract_duration: IJobOfferExpected["contract_duration"] + contract_type: IJobOfferExpected["contract_type"] + contract_remote: IJobOfferExpected["contract_remote"] | undefined + contract_start: Date | null + + offer_title: string + offer_rome_codes: string[] | null + offer_description: string + offer_target_diploma_european: "3" | "4" | "5" | "6" | "7" | null + offer_desired_skills: IJobOfferExpected["offer_desired_skills"] + offer_to_be_acquired_skills: IJobOfferExpected["offer_to_be_acquired_skills"] + offer_access_conditions: IJobOfferExpected["offer_access_conditions"] + offer_creation: IJobOfferExpected["offer_creation"] + offer_expiration: IJobOfferExpected["offer_expiration"] + offer_opening_count: IJobOfferExpected["offer_opening_count"] + offer_origin: string | null + offer_multicast: boolean + + apply_url: string | null + apply_phone: string | null + apply_email: string | null + + workplace_siret: string + workplace_name: string | null + workplace_website: string | null + workplace_description: string | null + workplace_address_label: string | null +} + +type IJobOfferWritableInputExpected = { + partner_job_id?: string | null | undefined + + contract_duration?: number | null | undefined + contract_type?: Array<"Apprentissage" | "Professionnalisation"> | undefined + contract_remote?: IJobOfferExpected["contract_remote"] | undefined + contract_start?: string | null | undefined + + offer_title: string + offer_rome_codes?: string[] | null | undefined + offer_description: string + offer_target_diploma_european?: "3" | "4" | "5" | "6" | "7" | null | undefined + offer_desired_skills?: string[] | undefined + offer_to_be_acquired_skills?: string[] | undefined + offer_access_conditions?: string[] | undefined + offer_creation?: string | null | undefined + offer_expiration?: string | null | undefined + offer_opening_count?: number | undefined + offer_origin?: string | null | undefined + offer_multicast?: boolean | undefined + + apply_url?: string | null | undefined + apply_phone?: string | null | undefined + apply_email?: string | null | undefined + + workplace_siret: string + workplace_name?: string | null | undefined + workplace_website?: string | null | undefined + workplace_description?: string | null | undefined + workplace_address_label?: string | null | undefined +} + +describe("IJobRecruiterExpected", () => { + it("should have proper typing", () => { + expectTypeOf().toMatchTypeOf() + }) +}) + +describe("IJobsPartnersOfferApi", () => { + it("should have proper typing", () => { + expectTypeOf().toMatchTypeOf() + }) +}) + +describe("IJobsPartnersWritableApi", () => { + it("should have proper typing", () => { + expectTypeOf().toMatchTypeOf() + }) +}) + +describe("IJobsPartnersWritableApiInput", () => { + it("should have proper typing", () => { + expectTypeOf>().toEqualTypeOf() + }) +}) diff --git a/shared/models/jobPartners/jobPartners.model.test.ts b/shared/models/jobPartners/jobPartners.model.test.ts index cb3d03b267..4ee58d9ff8 100644 --- a/shared/models/jobPartners/jobPartners.model.test.ts +++ b/shared/models/jobPartners/jobPartners.model.test.ts @@ -8,37 +8,15 @@ describe("ZJobsPartnersWritableApi", () => { const inOneMinute = new Date("2024-06-18T14:31:00.000Z") const oneHourAgo = new Date("2024-06-18T13:30:00.000Z") const inOneHour = new Date("2024-06-18T15:30:00.000Z") - const inSept = new Date("2024-09-01T00:00:00.000Z") const data: IJobsPartnersWritableApiInput = { - partner_job_id: null, - - contract_start: inSept.toJSON(), - contract_duration: null, - contract_type: null, - contract_remote: null, - offer_title: "Apprentis en développement web", offer_rome_codes: ["M1602"], - offer_desired_skills: [], - offer_to_be_acquired_skills: [], - offer_access_conditions: [], - offer_creation: null, - offer_expiration: null, - offer_opening_count: 1, - offer_origin: null, - offer_multicast: true, offer_description: "Envie de devenir développeur web ? Rejoignez-nous !", - offer_diploma_level_european: null, - apply_url: null, apply_email: "mail@mail.com", - apply_phone: null, workplace_siret: "39837261500128", - workplace_address_label: null, - workplace_description: null, - workplace_website: null, } beforeEach(async () => { @@ -52,19 +30,13 @@ describe("ZJobsPartnersWritableApi", () => { }) describe("contract_start", () => { - it("should be required", () => { + it("should be optional", () => { const result = ZJobsPartnersWritableApi.safeParse({ ...data, - contract_start: null, }) - expect(result.success).toBe(false) - expect(result.error?.format()).toEqual({ - _errors: [], - contract_start: { - _errors: ["Expected ISO 8601 date string"], - }, - }) + expect(result.success).toBe(true) + expect(result.data?.contract_start).toBe(null) }) it("should be required ISO 8601 date string", () => { const result = ZJobsPartnersWritableApi.safeParse({ diff --git a/shared/models/jobsPartners.model.ts b/shared/models/jobsPartners.model.ts index 0cca2cdf7a..27fd48338b 100644 --- a/shared/models/jobsPartners.model.ts +++ b/shared/models/jobsPartners.model.ts @@ -35,7 +35,7 @@ export const ZJobsPartnersRecruiterApi = z.object({ workplace_naf_code: z.string().nullable().describe("code NAF"), workplace_naf_label: z.string().nullable().describe("Libelle NAF"), - apply_url: z.string().url().nullable().describe("URL pour candidater").default(null), + apply_url: z.string().url().describe("URL pour candidater"), apply_phone: extensions.telephone.nullable().describe("Téléphone de contact").default(null), }) @@ -52,8 +52,11 @@ export const ZJobsPartnersOfferApi = ZJobsPartnersRecruiterApi.omit({ partner_job_id: z.string().nullable().describe("Identifiant d'origine l'offre provenant du partenaire").default(null), contract_start: z.date().nullable().describe("Date de début de contrat"), - contract_duration: z.number().int().min(0).nullable().describe("Durée du contrat en mois"), - contract_type: z.array(extensions.buildEnum(TRAINING_CONTRACT_TYPE)).nullable().describe("type de contrat, formaté à l'insertion"), + contract_duration: z.number().int().min(0).nullable().describe("Durée du contrat en mois").default(null), + contract_type: z + .array(extensions.buildEnum(TRAINING_CONTRACT_TYPE)) + .describe("type de contrat, formaté à l'insertion") + .default([TRAINING_CONTRACT_TYPE.APPRENTISSAGE, TRAINING_CONTRACT_TYPE.PROFESSIONNALISATION]), contract_remote: extensions.buildEnum(TRAINING_REMOTE_TYPE).nullable().describe("Format de travail de l'offre").default(null), offer_title: z.string().min(3).describe("Titre de l'offre"), @@ -87,10 +90,12 @@ export const ZJobsPartnersRecruiterPrivate = ZJobsPartnersRecruiterApi.merge(ZJo export const ZJobsPartnersOfferPrivate = ZJobsPartnersOfferApi.omit({ _id: true, + apply_url: true, }) .merge(ZJobsPartnersRecruiterPrivateFields) .extend({ _id: zObjectId, + apply_url: ZJobsPartnersOfferApi.shape.apply_url.nullable().default(null), }) export type IJobsPartnersRecruiterApi = z.output @@ -118,7 +123,6 @@ const ZJobsPartnersPostApiBodyBase = ZJobsPartnersOfferPrivate.pick({ offer_origin: true, offer_multicast: true, - apply_url: true, apply_email: true, apply_phone: true, @@ -126,7 +130,12 @@ const ZJobsPartnersPostApiBodyBase = ZJobsPartnersOfferPrivate.pick({ workplace_website: true, workplace_name: true, }).extend({ - contract_start: z.string({ message: "Expected ISO 8601 date string" }).datetime({ offset: true, message: "Expected ISO 8601 date string" }).pipe(z.coerce.date()), + contract_start: z + .string({ message: "Expected ISO 8601 date string" }) + .datetime({ offset: true, message: "Expected ISO 8601 date string" }) + .pipe(z.coerce.date()) + .nullable() + .default(null), offer_creation: z .string({ message: "Expected ISO 8601 date string" }) .datetime({ offset: true, message: "Expected ISO 8601 date string" }) @@ -135,7 +144,8 @@ const ZJobsPartnersPostApiBodyBase = ZJobsPartnersOfferPrivate.pick({ message: "Creation date cannot be in the future", }) ) - .nullable(), + .nullable() + .default(null), offer_expiration: z .string({ message: "Expected ISO 8601 date string" }) .datetime({ offset: true, message: "Expected ISO 8601 date string" }) @@ -144,13 +154,16 @@ const ZJobsPartnersPostApiBodyBase = ZJobsPartnersOfferPrivate.pick({ message: "Expiration date cannot be in the past", }) ) - .nullable(), + .nullable() + .default(null), offer_rome_codes: ZJobsPartnersOfferPrivate.shape.offer_rome_codes.nullable().default(null), offer_description: ZJobsPartnersOfferPrivate.shape.offer_description.min(30, "Job description should be at least 30 characters"), - offer_diploma_level_european: zDiplomaEuropeanLevel.nullable().default(null), + offer_target_diploma_european: zDiplomaEuropeanLevel.nullable().default(null), workplace_siret: extensions.siret, workplace_address_label: z.string().nullable().default(null), + + apply_url: ZJobsPartnersOfferApi.shape.apply_url.nullable().default(null), }) export const ZJobsPartnersWritableApi = ZJobsPartnersPostApiBodyBase.superRefine((data, ctx) => {