diff --git a/back/package.json b/back/package.json index 519cacf318..445d25ebd1 100644 --- a/back/package.json +++ b/back/package.json @@ -36,7 +36,7 @@ "trigger-delete-old-discussion-messages": "ts-node src/scripts/triggerDeleteOldDiscussionMessages", "trigger-refresh-materialized-views": "ts-node src/scripts/triggerRefreshMaterializedViews.ts", "trigger-resync-old-conventions-to-pe": "ts-node src/scripts/triggerResyncOldConventionsToPe.ts", - "trigger-assessment-first-reminders": "ts-node src/scripts/triggerAssessmentReminder.ts", + "trigger-assessment-reminders": "ts-node src/scripts/triggerAssessmentReminder.ts", "trigger-sending-emails-with-assessment-creation-link": "ts-node src/scripts/triggerSendingEmailsWithAssessmentCreationLink.ts", "trigger-sending-beneficiary-assessment-emails": "ts-node src/scripts/triggerSendingBeneficiaryPdfAssessmentEmails.ts", "trigger-suggest-edit-form-establishment-every-6-months": "ts-node src/scripts/triggerSuggestEditFormEstablishmentEvery6Months.ts", diff --git a/back/scalingo/cron.json b/back/scalingo/cron.json index 891e02fa6b..42254a067a 100644 --- a/back/scalingo/cron.json +++ b/back/scalingo/cron.json @@ -41,7 +41,7 @@ "size": "M" }, { - "command": "15 21 * * * pnpm back run trigger-assessment-first-reminders", + "command": "15 21 * * * pnpm back run trigger-assessment-reminders", "size": "M" }, { diff --git a/back/src/domains/establishment/use-cases/AssessmentReminder.ts b/back/src/domains/establishment/use-cases/AssessmentReminder.ts index 5af2309138..43c98e2603 100644 --- a/back/src/domains/establishment/use-cases/AssessmentReminder.ts +++ b/back/src/domains/establishment/use-cases/AssessmentReminder.ts @@ -7,6 +7,7 @@ import { ConventionReadDto, Email, errors, + executeInSequence, frontRoutes, immersionFacileNoReplyEmailSender, } from "shared"; @@ -23,10 +24,12 @@ import { TimeGateway } from "../../core/time-gateway/ports/TimeGateway"; import { UnitOfWork } from "../../core/unit-of-work/ports/UnitOfWork"; type AssessmentReminderOutput = { - numberOfFirstReminders: number; + numberOfReminders: number; }; -export type AssessmentReminderMode = "3daysAfterConventionEnd"; +export type AssessmentReminderMode = + | "3daysAfterConventionEnd" + | "10daysAfterConventionEnd"; export type AssessmentReminder = ReturnType; export const makeAssessmentReminder = createTransactionalUseCase< @@ -41,7 +44,9 @@ export const makeAssessmentReminder = createTransactionalUseCase< >( { name: "AssessmentReminder", - inputSchema: z.object({ mode: z.enum(["3daysAfterConventionEnd"]) }), + inputSchema: z.object({ + mode: z.enum(["3daysAfterConventionEnd", "10daysAfterConventionEnd"]), + }), }, async ({ inputParams: params, uow, deps }) => { const now = deps.timeGateway.now(); @@ -52,36 +57,36 @@ export const makeAssessmentReminder = createTransactionalUseCase< conventionRepository: uow.conventionRepository, }); - await Promise.all( - conventionIdsToRemind.map(async (conventionId) => { - const convention = - await uow.conventionQueries.getConventionById(conventionId); - if (!convention) - throw errors.convention.notFound({ conventionId: conventionId }); - await sendAssessmentReminders({ - uow, - recipientEmails: convention.agencyValidatorEmails, - convention, - now, - generateConventionMagicLinkUrl: deps.generateConventionMagicLinkUrl, - saveNotificationAndRelatedEvent: deps.saveNotificationAndRelatedEvent, - role: "validator", - }); - await sendAssessmentReminders({ - uow, - recipientEmails: convention.agencyCounsellorEmails, - convention, - now, - generateConventionMagicLinkUrl: deps.generateConventionMagicLinkUrl, - saveNotificationAndRelatedEvent: deps.saveNotificationAndRelatedEvent, - role: "counsellor", - }); - }), - ); - - return Promise.resolve({ - numberOfFirstReminders: conventionIdsToRemind.length, + await executeInSequence(conventionIdsToRemind, async (conventionId) => { + const convention = + await uow.conventionQueries.getConventionById(conventionId); + if (!convention) + throw errors.convention.notFound({ conventionId: conventionId }); + await sendAssessmentReminders({ + uow, + mode: params.mode, + recipientEmails: convention.agencyValidatorEmails, + convention, + now, + generateConventionMagicLinkUrl: deps.generateConventionMagicLinkUrl, + saveNotificationAndRelatedEvent: deps.saveNotificationAndRelatedEvent, + role: "validator", + }); + await sendAssessmentReminders({ + uow, + mode: params.mode, + recipientEmails: convention.agencyCounsellorEmails, + convention, + now, + generateConventionMagicLinkUrl: deps.generateConventionMagicLinkUrl, + saveNotificationAndRelatedEvent: deps.saveNotificationAndRelatedEvent, + role: "counsellor", + }); }); + + return { + numberOfReminders: conventionIdsToRemind.length, + }; }, ); @@ -111,11 +116,13 @@ const getConventionIdsToRemind = async ({ }; const createNotification = ({ + mode, convention, recipientEmail, establishmentContactEmail, assessmentCreationLink, }: { + mode: AssessmentReminderMode; convention: ConventionReadDto; recipientEmail: Email; establishmentContactEmail: Email; @@ -129,7 +136,10 @@ const createNotification = ({ }, kind: "email", templatedContent: { - kind: "ASSESSMENT_AGENCY_FIRST_REMINDER", + kind: + mode === "3daysAfterConventionEnd" + ? "ASSESSMENT_AGENCY_FIRST_REMINDER" + : "ASSESSMENT_AGENCY_SECOND_REMINDER", params: { beneficiaryFirstName: convention.signatories.beneficiary.firstName, beneficiaryLastName: convention.signatories.beneficiary.lastName, @@ -147,6 +157,7 @@ const createNotification = ({ const sendAssessmentReminders = async ({ uow, + mode, saveNotificationAndRelatedEvent, convention, recipientEmails, @@ -155,6 +166,7 @@ const sendAssessmentReminders = async ({ role, }: { uow: UnitOfWork; + mode: AssessmentReminderMode; saveNotificationAndRelatedEvent: SaveNotificationAndRelatedEvent; convention: ConventionReadDto; recipientEmails: Email[]; @@ -171,6 +183,7 @@ const sendAssessmentReminders = async ({ now, }); const notification = createNotification({ + mode, convention, recipientEmail: recipientEmail, establishmentContactEmail: diff --git a/back/src/domains/establishment/use-cases/AssessmentReminder.unit.test.ts b/back/src/domains/establishment/use-cases/AssessmentReminder.unit.test.ts index cc2969eeef..2ab575c978 100644 --- a/back/src/domains/establishment/use-cases/AssessmentReminder.unit.test.ts +++ b/back/src/domains/establishment/use-cases/AssessmentReminder.unit.test.ts @@ -73,11 +73,11 @@ describe("AssessmentReminder", () => { ]; await uow.conventionRepository.save(convention); - const { numberOfFirstReminders } = await assessmentReminder.execute({ + const { numberOfReminders } = await assessmentReminder.execute({ mode: "3daysAfterConventionEnd", }); - expect(numberOfFirstReminders).toBe(1); + expect(numberOfReminders).toBe(1); expectObjectInArrayToMatch(uow.notificationRepository.notifications, [ { templatedContent: { @@ -110,4 +110,66 @@ describe("AssessmentReminder", () => { { topic: "NotificationAdded" }, ]); }); + + it("send second assessment reminder", async () => { + const now = timeGateway.now(); + const conventionEndDate = subDays(now, 10); + const agency = new AgencyDtoBuilder().build(); + const convention = new ConventionDtoBuilder() + .withStatus("ACCEPTED_BY_VALIDATOR") + .withDateEnd(conventionEndDate.toISOString()) + .withAgencyId(agency.id) + .build(); + const validator = new InclusionConnectedUserBuilder() + .withId("10000000-0000-0000-0000-000000000003") + .withEmail("validator@agency1.fr") + .buildUser(); + await uow.userRepository.save(validator); + uow.agencyRepository.agencies = [ + toAgencyWithRights(agency, { + [validator.id]: { + isNotifiedByEmail: true, + roles: ["validator"], + }, + }), + ]; + await uow.conventionRepository.save(convention); + + const { numberOfReminders } = await assessmentReminder.execute({ + mode: "10daysAfterConventionEnd", + }); + + expect(numberOfReminders).toBe(1); + expectObjectInArrayToMatch(uow.notificationRepository.notifications, [ + { + templatedContent: { + kind: "ASSESSMENT_AGENCY_SECOND_REMINDER", + params: { + conventionId: convention.id, + internshipKind: convention.internshipKind, + businessName: convention.businessName, + establishmentContactEmail: + convention.signatories.establishmentRepresentative.email, + beneficiaryFirstName: convention.signatories.beneficiary.firstName, + beneficiaryLastName: convention.signatories.beneficiary.lastName, + assessmentCreationLink: fakeGenerateMagicLinkUrlFn({ + id: convention.id, + email: validator.email, + role: "validator", + targetRoute: frontRoutes.assessment, + now, + }), + }, + recipients: [validator.email], + sender: { + email: "ne-pas-ecrire-a-cet-email@immersion-facile.beta.gouv.fr", + name: "Immersion Facilitée", + }, + }, + }, + ]); + expectObjectInArrayToMatch(uow.outboxRepository.events, [ + { topic: "NotificationAdded" }, + ]); + }); }); diff --git a/back/src/scripts/triggerAssessmentReminder.ts b/back/src/scripts/triggerAssessmentReminder.ts index 7ca259ecbb..1714154ff8 100644 --- a/back/src/scripts/triggerAssessmentReminder.ts +++ b/back/src/scripts/triggerAssessmentReminder.ts @@ -22,28 +22,54 @@ const triggerAssessmentReminder = async () => { 3600 * 24 * 30, ); - return makeAssessmentReminder({ - uowPerformer: createUowPerformer(config, createGetPgPoolFn(config)) - .uowPerformer, - deps: { - timeGateway, - saveNotificationAndRelatedEvent: makeSaveNotificationAndRelatedEvent( - new UuidV4Generator(), + const { numberOfReminders: numberOfFirstReminders } = + await makeAssessmentReminder({ + uowPerformer: createUowPerformer(config, createGetPgPoolFn(config)) + .uowPerformer, + deps: { timeGateway, - ), - generateConventionMagicLinkUrl: makeGenerateConventionMagicLinkUrl( - config, - generateConventionJwt, - ), - }, - }).execute({ mode: "3daysAfterConventionEnd" }); + saveNotificationAndRelatedEvent: makeSaveNotificationAndRelatedEvent( + new UuidV4Generator(), + timeGateway, + ), + generateConventionMagicLinkUrl: makeGenerateConventionMagicLinkUrl( + config, + generateConventionJwt, + ), + }, + }).execute({ mode: "3daysAfterConventionEnd" }); + + const { numberOfReminders: numberOfSecondReminders } = + await makeAssessmentReminder({ + uowPerformer: createUowPerformer(config, createGetPgPoolFn(config)) + .uowPerformer, + deps: { + timeGateway, + saveNotificationAndRelatedEvent: makeSaveNotificationAndRelatedEvent( + new UuidV4Generator(), + timeGateway, + ), + generateConventionMagicLinkUrl: makeGenerateConventionMagicLinkUrl( + config, + generateConventionJwt, + ), + }, + }).execute({ mode: "10daysAfterConventionEnd" }); + + return { + numberOfFirstReminders, + numberOfSecondReminders, + }; }; handleCRONScript( "assessmentReminder", config, triggerAssessmentReminder, - ({ numberOfFirstReminders }) => - [`Total of first reminders : ${numberOfFirstReminders}`].join("\n"), + ({ numberOfFirstReminders, numberOfSecondReminders }) => + [ + `Total of first reminders : ${numberOfFirstReminders}`, + `Total if second reminders: ${numberOfSecondReminders}`, + ].join("\n"), logger, ); diff --git a/front/src/app/pages/admin/EmailPreviewTab.tsx b/front/src/app/pages/admin/EmailPreviewTab.tsx index 5aa1a49a52..3e3782c3f5 100644 --- a/front/src/app/pages/admin/EmailPreviewTab.tsx +++ b/front/src/app/pages/admin/EmailPreviewTab.tsx @@ -282,6 +282,15 @@ export const defaultEmailValueByEmailKind: { establishmentContactEmail: "ESTABLISHMENT_CONTACT_EMAIL", internshipKind: "immersion", }, + ASSESSMENT_AGENCY_SECOND_REMINDER: { + assessmentCreationLink: "ASSESSMENT_CREATION_LINK", + beneficiaryFirstName: "BENEFICIARY_FIRST_NAME", + beneficiaryLastName: "BENEFICIARY_LAST_NAME", + businessName: "BUSINESS_NAME", + conventionId: "CONVENTION_ID", + establishmentContactEmail: "ESTABLISHMENT_CONTACT_EMAIL", + internshipKind: "immersion", + }, ASSESSMENT_AGENCY_NOTIFICATION: { agencyLogoUrl: defaultEmailPreviewUrl, assessmentCreationLink: "ASSESSMENT_CREATION_LINK", diff --git a/shared/src/email/EmailParamsByEmailType.ts b/shared/src/email/EmailParamsByEmailType.ts index 892a3db0b6..393e2f9a5c 100644 --- a/shared/src/email/EmailParamsByEmailType.ts +++ b/shared/src/email/EmailParamsByEmailType.ts @@ -88,6 +88,15 @@ export type EmailParamsByEmailType = { establishmentContactEmail: Email; internshipKind: InternshipKind; }; + ASSESSMENT_AGENCY_SECOND_REMINDER: { + assessmentCreationLink: string; + beneficiaryFirstName: string; + beneficiaryLastName: string; + businessName: string; + conventionId: ConventionId; + establishmentContactEmail: Email; + internshipKind: InternshipKind; + }; ASSESSMENT_BENEFICIARY_NOTIFICATION: { conventionId: ConventionId; beneficiaryFirstName: string; diff --git a/shared/src/email/emailTemplatesByName.ts b/shared/src/email/emailTemplatesByName.ts index f61dcab38f..9050135e08 100644 --- a/shared/src/email/emailTemplatesByName.ts +++ b/shared/src/email/emailTemplatesByName.ts @@ -38,7 +38,7 @@ export const emailTemplatesByName = ASSESSMENT_AGENCY_FIRST_REMINDER: { niceName: "Bilan - Prescripteurs - Relance à 3 jours après la fin de l’immersion", - tags: ["bilan_prescripteur_formulaireBilan_premierRappel"], + tags: ["bilan_prescripteur_formulaireBilan_J+3"], createEmailVariables: ({ assessmentCreationLink, beneficiaryFirstName, @@ -69,6 +69,38 @@ export const emailTemplatesByName = `, }), }, + ASSESSMENT_AGENCY_SECOND_REMINDER: { + niceName: + "Bilan - Prescripteurs - Relance à 10 jours après la fin de l’immersion", + tags: ["bilan_prescripteur_formulaireBilan_J+10"], + createEmailVariables: ({ + assessmentCreationLink, + beneficiaryFirstName, + beneficiaryLastName, + businessName, + conventionId, + establishmentContactEmail, + internshipKind, + }) => ({ + subject: `Immersion Facilitée - Urgent : Bilan toujours non complété pour l'immersion de ${beneficiaryFirstName}`, + greetings: greetingsWithConventionId(conventionId), + content: ` + Malgré une première relance, le bilan de l’immersion de ${beneficiaryFirstName} ${beneficiaryLastName} chez ${businessName} reste incomplet. + + 1. Relancer l'entreprise (${establishmentContactEmail}) pour qu’elle remplisse le bilan. + 2. Les contacter par téléphone et les accompagner pour le compléter ensemble. + `, + buttons: [ + { + label: "Formulaire de bilan", + url: assessmentCreationLink, + }, + ], + subContent: ` + ${defaultSignature(internshipKind)} + `, + }), + }, ASSESSMENT_AGENCY_NOTIFICATION: { niceName: "Bilan - Prescripteurs - Lien de création du bilan", tags: ["bilan_prescripteur_formulaireBilan"],