diff --git a/.talismanrc b/.talismanrc index eefc83f69..cdbe0f8f4 100644 --- a/.talismanrc +++ b/.talismanrc @@ -146,7 +146,7 @@ fileignoreconfig: - filename: shared/constants/recruteur.ts checksum: af4631fe998b78b13691dbc821a8ffa71ffc2f4cb6967361cb5ef19965dc95cb - filename: shared/fixtures/application.fixture.ts - checksum: 4c4590586b8c5683ce7aebb32d2286af3265bb9d3d39581f6d7a1f8ed1a2850d + checksum: 5f77b1127060f10911006a797590a7ff36f81ca61dbc0567049bc4e866e17c89 - filename: shared/fixtures/appointment.fixture.ts checksum: 8b894b4059d8f7531e26f249901b83dbec433d4e660c0eca193c8e61572b3ae0 - filename: shared/fixtures/recruiter.fixture.ts diff --git a/server/src/http/controllers/application.controller.test.ts b/server/src/http/controllers/application.controller.test.ts index 4ebee0d8f..ec2562f17 100644 --- a/server/src/http/controllers/application.controller.test.ts +++ b/server/src/http/controllers/application.controller.test.ts @@ -84,10 +84,12 @@ describe("POST /v1/application", () => { }) const applications = await getDbCollection("applications").find({}).toArray() + const applicant = await getDbCollection("applicants").findOne({ _id: applications[0]?.applicant_id }) expect(applications).toEqual([ { _id: expect.any(ObjectId), + applicant_id: applicant?._id, applicant_attachment_name: body.applicant_file_name, applicant_email: body.applicant_email, applicant_first_name: body.applicant_first_name, diff --git a/server/src/http/controllers/application.controller.ts b/server/src/http/controllers/application.controller.ts index fff38c86b..a7cd32e8f 100644 --- a/server/src/http/controllers/application.controller.ts +++ b/server/src/http/controllers/application.controller.ts @@ -4,6 +4,7 @@ import { oldItemTypeToNewItemType } from "shared/constants/lbaitem" import { zRoutes } from "shared/index" import { getDbCollection } from "../../common/utils/mongodbUtils" +import { getApplicantFromDB } from "../../services/applicant.service" import { getCompanyEmailFromToken, sendApplication, sendMailToApplicant } from "../../services/application.service" import { Server } from "../server" @@ -69,8 +70,15 @@ export default function (server: Server) { throw notFound() } + const applicant = await getApplicantFromDB({ _id: application.applicant_id }) + + if (!applicant) { + throw notFound(`unexpected: applicant not found for application ${application._id}`) + } + await sendMailToApplicant({ application, + applicant, email, phone, company_recruitment_intention, diff --git a/server/src/http/controllers/v2/applications.controller.v2.test.ts b/server/src/http/controllers/v2/applications.controller.v2.test.ts index 6f911a54f..9b6703e4f 100644 --- a/server/src/http/controllers/v2/applications.controller.v2.test.ts +++ b/server/src/http/controllers/v2/applications.controller.v2.test.ts @@ -136,12 +136,14 @@ describe("POST /v2/application", () => { }) const application = await getDbCollection("applications").findOne({ company_siret: recruteur.siret }) + const applicant = await getDbCollection("applicants").findOne({ _id: application?.applicant_id }) expect.soft(response.statusCode).toEqual(202) expect.soft(response.json()).toEqual({ id: application!._id.toString() }) expect(application).toEqual({ _id: expect.any(ObjectId), + applicant_id: applicant?._id, applicant_attachment_name: body.applicant_attachment_name, applicant_email: body.applicant_email, applicant_first_name: body.applicant_first_name, @@ -192,12 +194,14 @@ describe("POST /v2/application", () => { }) const application = await getDbCollection("applications").findOne({ job_id: job._id.toString() }) + const applicant = await getDbCollection("applicants").findOne({ _id: application?.applicant_id }) expect.soft(response.statusCode).toEqual(202) expect.soft(response.json()).toEqual({ id: application!._id.toString() }) expect(application).toEqual({ _id: expect.any(ObjectId), + applicant_id: applicant?._id, applicant_attachment_name: body.applicant_attachment_name, applicant_email: body.applicant_email, applicant_first_name: body.applicant_first_name, diff --git a/server/src/jobs/applications/processApplications.ts b/server/src/jobs/applications/processApplications.ts index fa303a832..bf8bb0eed 100644 --- a/server/src/jobs/applications/processApplications.ts +++ b/server/src/jobs/applications/processApplications.ts @@ -59,6 +59,10 @@ const processApplicationGroup = async (applicationFilter: Filter, return } const applicant = await getApplicantFromDB({ _id: application.applicant_id }) + if (!applicant) { + await getDbCollection("applications").findOneAndUpdate({ _id: application._id }, { $set: { scan_status: ApplicationScanStatus.ERROR_APPLICANT_NOT_FOUND } }) + return + } try { let hasVirus: boolean = false if (application.scan_status !== ApplicationScanStatus.NO_VIRUS_DETECTED) { diff --git a/server/src/jobs/simpleJobDefinitions.ts b/server/src/jobs/simpleJobDefinitions.ts index 78e3f19da..829f379d5 100644 --- a/server/src/jobs/simpleJobDefinitions.ts +++ b/server/src/jobs/simpleJobDefinitions.ts @@ -38,7 +38,6 @@ import { opcoReminderJob } from "./recruiters/opcoReminderJob" import { updateMissingStartDate } from "./recruiters/updateMissingStartDateJob" import { updateSiretInfosInError } from "./recruiters/updateSiretInfosInErrorJob" import { importReferentielRome } from "./referentielRome/referentielRome" -import { migrationTest } from "./test" type SimpleJobDefinition = { fct: () => Promise @@ -216,8 +215,4 @@ export const simpleJobDefinitions: SimpleJobDefinition[] = [ fct: processJobPartners, description: "Chaîne complète de traitement des jobs_partners", }, - { - fct: migrationTest, - description: "test", - }, ] diff --git a/server/src/jobs/test.ts b/server/src/jobs/test.ts deleted file mode 100644 index 7152f6e66..000000000 --- a/server/src/jobs/test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ObjectId } from "bson" -import { IApplicant, IApplication, ZApplicant } from "shared" - -import { getDbCollection } from "@/common/utils/mongodbUtils" - -import { asyncForEach } from "../common/utils/asyncUtils" - -type ApplicationAggregate = Pick - -export const migrationTest = async () => { - // console.log(extensions.telephone.safeParse("0690760552")) - // return - const applications = (await getDbCollection("applications") - .find({}, { projection: { applicant_first_name: 1, applicant_last_name: 1, applicant_email: 1, applicant_phone: 1, _id: 1 } }) - .toArray()) as ApplicationAggregate[] - - const stat = { error: 0, success: 0, total: applications.length } - - await asyncForEach(applications, async (application) => { - const { applicant_first_name, applicant_email, applicant_last_name, applicant_phone, _id } = application - const now = new Date() - const applicant: IApplicant = { - _id: new ObjectId(), - firstname: applicant_first_name, - lastname: applicant_last_name, - phone: applicant_phone, - email: applicant_email, - last_connection: null, - createdAt: now, - updatedAt: now, - } - const validation = ZApplicant.safeParse(applicant) - if (!validation.success) { - stat.error++ - console.log({ applicant, err: validation.error.errors }) - } else { - stat.success++ - } - }) - console.log(stat) -} diff --git a/server/src/services/application.service.ts b/server/src/services/application.service.ts index 1bb7b2813..2b21af1d8 100644 --- a/server/src/services/application.service.ts +++ b/server/src/services/application.service.ts @@ -914,7 +914,7 @@ export const getApplicationByCompanyCount = async (sirets: ILbaCompany["siret"][ * if hardbounce event si related to an application sent to a compay then * warns the applicant and returns true otherwise returns false */ -export const processApplicationHardbounceEvent = async (payload, sendNotificationToApplicant = notifyHardbounceToApplicant) => { +export const processApplicationHardbounceEvent = async (payload, sendNotificationToApplicant: any = notifyHardbounceToApplicant) => { const { email } = payload const messageId = payload["message-id"] @@ -936,7 +936,7 @@ export const processApplicationCandidateHardbounceEvent = async (payload) => { const messageId = payload["message-id"] const applicant = await getDbCollection("applicants").findOne({ email }) - const application = await getDbCollection("applications").findOne({ applicant_id: applicant!._id, to_applicant_message_id: messageId }) + const application = await getDbCollection("applications").findOne({ applicant_id: applicant?._id, to_applicant_message_id: messageId }) if (application) { return true diff --git a/server/src/services/emails.service.test.ts b/server/src/services/emails.service.test.ts index 105436eee..f7a9fdec1 100644 --- a/server/src/services/emails.service.test.ts +++ b/server/src/services/emails.service.test.ts @@ -1,5 +1,5 @@ import { EApplicantRole } from "shared/constants/rdva" -import { generateApplicationFixture } from "shared/fixtures/application.fixture" +import { generateApplicantFixture, generateApplicationFixture } from "shared/fixtures/application.fixture" import { generateAppointmentFixture, generateEligibleTrainingEstablishmentFixture, generateEligibleTrainingFixture } from "shared/fixtures/appointment.fixture" import { generateUserFixture } from "shared/fixtures/user.fixture" import { generateUserWithAccountFixture } from "shared/fixtures/userWithAccount.fixture" @@ -16,6 +16,7 @@ import { IBrevoWebhookEvent, processHardBounceWebhookEvent } from "./emails.serv async function cleanTest() { await getDbCollection("emailblacklists").deleteMany({}) await getDbCollection("applications").deleteMany({}) + await getDbCollection("applicants").deleteMany({}) await getDbCollection("recruteurslba").deleteMany({}) await getDbCollection("etablissements").deleteMany({}) await getDbCollection("users").deleteMany({}) @@ -25,15 +26,7 @@ async function cleanTest() { describe("email blaklist events", () => { useMongo() - - beforeEach(async () => { - return async () => { - await cleanTest() - } - }) - const blacklistedEmail = "blacklisted@email.com" - const fakeMessageId_1 = "" const baseWebHookPayload: IBrevoWebhookEvent = { @@ -59,6 +52,12 @@ describe("email blaklist events", () => { }, ] + beforeEach(async () => { + return async () => { + await cleanTest() + } + }) + it("Non 'blocked' event shoud throw an error", async () => { baseWebHookPayload.event = BrevoEventStatus.DELIVRE await expect.soft(processHardBounceWebhookEvent(baseWebHookPayload)).rejects.toThrow("Non hardbounce event received on hardbounce webhook route") @@ -66,6 +65,8 @@ describe("email blaklist events", () => { it("Unidentified hardbounce should register campaign origin", async () => { baseWebHookPayload.event = BrevoEventStatus.HARD_BOUNCE + const applicant = generateApplicantFixture({ email: blacklistedEmail }) + await getDbCollection("applicants").insertOne(applicant) await processHardBounceWebhookEvent(baseWebHookPayload) const blEvent = await getDbCollection("emailblacklists").findOne({ email: blacklistedEmail }) @@ -90,7 +91,11 @@ describe("email blaklist events", () => { }) it("Applicant blocked should register candidature_spontanee_candidat (blocked)", async () => { - await getDbCollection("applications").insertOne(generateApplicationFixture({ applicant_email: blacklistedEmail, to_applicant_message_id: fakeMessageId_1 })) + const applicant = generateApplicantFixture({ email: blacklistedEmail }) + await getDbCollection("applicants").insertOne(applicant) + await getDbCollection("applications").insertOne( + generateApplicationFixture({ applicant_id: applicant._id, applicant_email: blacklistedEmail, to_applicant_message_id: fakeMessageId_1 }) + ) baseWebHookPayload.event = BrevoEventStatus.BLOCKED baseWebHookPayload["message-id"] = fakeMessageId_1 @@ -107,6 +112,8 @@ describe("email blaklist events", () => { it("Unsubscribed variation events should register correct origin with (unsubscribed) reason", async () => { baseBlockedAddress[0].reason.code = BrevoBlockedReasons.UNSUBSCRIBED_VIA_API + const applicant = generateApplicantFixture({ email: blacklistedEmail }) + await getDbCollection("applicants").insertOne(applicant) await getDbCollection("applications").insertOne(generateApplicationFixture({ applicant_email: blacklistedEmail })) await saveBlacklistEmails(baseBlockedAddress) @@ -179,6 +186,8 @@ describe("email blaklist events", () => { }) it("Recruteur LBA with SPAM (plainte) should register candidature_spontanee_recruteur (spam)", async () => { + const applicant = generateApplicantFixture({ email: blacklistedEmail }) + await getDbCollection("applicants").insertOne(applicant) await getDbCollection("applications").insertOne(generateApplicationFixture({ company_email: blacklistedEmail, to_company_message_id: fakeMessageId_1 })) baseWebHookPayload.event = BrevoEventStatus.SPAM baseWebHookPayload["message-id"] = fakeMessageId_1 diff --git a/shared/fixtures/application.fixture.ts b/shared/fixtures/application.fixture.ts index 99e0a0a24..35f6f4ba0 100644 --- a/shared/fixtures/application.fixture.ts +++ b/shared/fixtures/application.fixture.ts @@ -1,12 +1,14 @@ import { ObjectId } from "bson" import { LBA_ITEM_TYPE } from "../constants/lbaitem" -import { ApplicationScanStatus, IApplication } from "../models" +import { ApplicationScanStatus, IApplicant, IApplication } from "../models" export function generateApplicationFixture(data: Partial): IApplication { + const applicant = generateApplicantFixture() return { _id: new ObjectId(), - applicant_email: "test@test.fr", + applicant_id: applicant._id, + applicant_email: "applicant@mail.fr", applicant_first_name: "a", applicant_last_name: "a", applicant_phone: "0125252525", @@ -33,6 +35,20 @@ export function generateApplicationFixture(data: Partial): IApplic } } +export function generateApplicantFixture(data: Partial = {}): IApplicant { + return { + _id: new ObjectId(), + firstname: "applicant_firstname", + lastname: "applicant_lastname", + phone: "0123456789", + email: "applicant@mail.fr", + last_connection: new Date("2024-07-28T03:05:34.187Z"), + createdAt: new Date("2024-07-28T03:05:34.187Z"), + updatedAt: new Date("2024-07-28T03:05:34.187Z"), + ...data, + } +} + export const applicationTestFile = "data:application/pdf;base64,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c3Rm9t/N8Lv1XNOe3Xo/pDQJ7x+aYvPPx/vnZPrE6/7BtewcJfbOcXdC9g/KNPLXPPNwjB8kdHrWHraLFZvPT/ZPyXQ1r5syNBydK9a4z6FTKHcvubtXM3r6iB25B/lmJ9ef3s/ZIfNPGV1cTo+np8w/O3T9fj49+3R5ZevZB37+A1jMOUYKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgNTk1IDg0Ml0vUmVzb3VyY2VzPDwvRm9udDw8L0YxIDEgMCBSL0YyIDIgMCBSL0YzIDMgMCBSPj4+Pi9Db250ZW50cyA0IDAgUi9QYXJlbnQgNSAwIFI+PgplbmRvYmoKMSAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UeXBlMS9CYXNlRm9udC9IZWx2ZXRpY2EtQm9sZC9FbmNvZGluZy9XaW5BbnNpRW5jb2Rpbmc+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UeXBlMS9CYXNlRm9udC9IZWx2ZXRpY2EvRW5jb2RpbmcvV2luQW5zaUVuY29kaW5nPj4KZW5kb2JqCjMgMCBvYmoKPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTEvQmFzZUZvbnQvVGltZXMtUm9tYW4vRW5jb2RpbmcvV2luQW5zaUVuY29kaW5nPj4KZW5kb2JqCjUgMCBvYmoKPDwvVHlwZS9QYWdlcy9Db3VudCAxL0tpZHNbNiAwIFJdPj4KZW5kb2JqCjcgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDUgMCBSPj4KZW5kb2JqCjggMCBvYmoKPDwvUHJvZHVjZXIoaVRleHRTaGFycJIgNS41LjEwIKkyMDAwLTIwMTYgaVRleHQgR3JvdXAgTlYgXChBR1BMLXZlcnNpb25cKSkvQ3JlYXRpb25EYXRlKEQ6MjAyMzA1MTEwOTMwMjUrMDInMDAnKS9Nb2REYXRlKEQ6MjAyMzA1MTEwOTMwMjUrMDInMDAnKT4+CmVuZG9iagp4cmVmCjAgOQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDIwODIgMDAwMDAgbiAKMDAwMDAwMjE3NSAwMDAwMCBuIAowMDAwMDAyMjYzIDAwMDAwIG4gCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMjM1MyAwMDAwMCBuIAowMDAwMDAxOTUyIDAwMDAwIG4gCjAwMDAwMDI0MDQgMDAwMDAgbiAKMDAwMDAwMjQ0OSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgOS9Sb290IDcgMCBSL0luZm8gOCAwIFIvSUQgWzwwYTE1N2MxMDBiMDJiMzBjYzZmNTRmMGE4ZWJhYWMzYT48MGExNTdjMTAwYjAyYjMwY2M2ZjU0ZjBhOGViYWFjM2E+XT4+CiVpVGV4dC01LjUuMTAKc3RhcnR4cmVmCjI2MTIKJSVFT0YK" diff --git a/shared/models/applications.model.ts b/shared/models/applications.model.ts index c82af3d4f..004150d3c 100644 --- a/shared/models/applications.model.ts +++ b/shared/models/applications.model.ts @@ -17,6 +17,7 @@ export enum ApplicationScanStatus { ERROR_CLAMAV = "ERROR_CLAMAV", NO_VIRUS_DETECTED = "NO_VIRUS_DETECTED", DO_NOT_SEND = "DO_NOT_SEND", + ERROR_APPLICANT_NOT_FOUND = "ERROR_APPLICANT_NOT_FOUND", } export const ZApplication = z @@ -112,6 +113,7 @@ export const ZNewApplication = ZApplication.extend({ }) .omit({ _id: true, + applicant_id: true, applicant_message_to_company: true, applicant_attachment_name: true, job_origin: true, @@ -157,6 +159,7 @@ const ZNewApplicationTransitionToV2 = ZApplication.extend({ }) .omit({ _id: true, + applicant_id: true, applicant_message_to_company: true, applicant_attachment_name: true, job_origin: true,