diff --git a/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.ts b/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.ts index 23815add88..c229f17cf3 100644 --- a/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.ts +++ b/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.ts @@ -4,6 +4,7 @@ import { CreateConventionMagicLinkPayloadProperties, frontRoutes, Role, + TemplatedEmail, } from "shared"; import { AppConfig } from "../../../../adapters/primary/config/appConfig"; import { GenerateConventionMagicLinkUrl } from "../../../../adapters/primary/config/magicLinkUrl"; @@ -47,94 +48,114 @@ export class NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification exte } for (const role of roles) { - const email = emailByRoleForConventionNeedsModification( - role, - convention, - agency, - ); - if (email instanceof Error) throw email; - - const conventionMagicLinkPayload: CreateConventionMagicLinkPayloadProperties = - { - id: convention.id, - role, - email, - now: this.timeGateway.now(), - // UGLY : need to rework, handling of JWT payloads - ...(role === "backOffice" - ? { sub: this.config.backofficeUsername } - : {}), - }; - - const makeShortMagicLink = prepareMagicShortLinkMaker({ - config: this.config, - conventionMagicLinkPayload, - generateConventionMagicLinkUrl: this.generateConventionMagicLinkUrl, - shortLinkIdGeneratorGateway: this.shortLinkIdGeneratorGateway, - uow, - }); - - await this.saveNotificationAndRelatedEvent(uow, { - kind: "email", - templatedContent: { - kind: "CONVENTION_MODIFICATION_REQUEST_NOTIFICATION", - recipients: [email], - params: { - conventionId: convention.id, - internshipKind: convention.internshipKind, - beneficiaryFirstName: convention.signatories.beneficiary.firstName, - beneficiaryLastName: convention.signatories.beneficiary.lastName, - businessName: convention.businessName, + const recipientsOrError = recipientsByRole(role, convention, agency); + if (recipientsOrError instanceof Error) throw recipientsOrError; + for (const recipient of recipientsOrError) { + await this.saveNotificationAndRelatedEvent(uow, { + kind: "email", + templatedContent: await this.prepareEmail( + convention, + role, + recipient, + uow, justification, - signature: agency.signature, - magicLink: await makeShortMagicLink( - frontRoutes.conventionImmersionRoute, - ), - conventionStatusLink: await makeShortMagicLink( - frontRoutes.conventionStatusDashboard, - ), - agencyLogoUrl: agency.logoUrl, + agency, + ), + followedIds: { + conventionId: convention.id, + agencyId: convention.agencyId, + establishmentSiret: convention.siret, }, - }, - followedIds: { - conventionId: convention.id, - agencyId: convention.agencyId, - establishmentSiret: convention.siret, - }, - }); + }); + } } } + + private async prepareEmail( + convention: ConventionDto, + role: Role, + recipient: string, + uow: UnitOfWork, + justification: string, + agency: AgencyDto, + ): Promise { + const conventionMagicLinkPayload: CreateConventionMagicLinkPayloadProperties = + { + id: convention.id, + role, + email: recipient, + now: this.timeGateway.now(), + // UGLY : need to rework, handling of JWT payloads + ...(role === "backOffice" + ? { sub: this.config.backofficeUsername } + : {}), + }; + + const makeShortMagicLink = prepareMagicShortLinkMaker({ + config: this.config, + conventionMagicLinkPayload, + generateConventionMagicLinkUrl: this.generateConventionMagicLinkUrl, + shortLinkIdGeneratorGateway: this.shortLinkIdGeneratorGateway, + uow, + }); + + return { + kind: "CONVENTION_MODIFICATION_REQUEST_NOTIFICATION", + recipients: [recipient], + params: { + conventionId: convention.id, + internshipKind: convention.internshipKind, + beneficiaryFirstName: convention.signatories.beneficiary.firstName, + beneficiaryLastName: convention.signatories.beneficiary.lastName, + businessName: convention.businessName, + justification, + signature: agency.signature, + magicLink: await makeShortMagicLink( + frontRoutes.conventionImmersionRoute, + ), + conventionStatusLink: await makeShortMagicLink( + frontRoutes.conventionStatusDashboard, + ), + agencyLogoUrl: agency.logoUrl, + }, + }; + } } -const emailByRoleForConventionNeedsModification = ( +const recipientsByRole = ( role: Role, convention: ConventionDto, agency: AgencyDto, -): string | Error => { - const error = new Error( - `Unsupported role for beneficiary/enterprise modification request notification: ${role}`, - ); - const missingEmailError = new Error( - `No actor with role ${role} for convention ${convention.id}`, - ); - const strategy: Record = { - backOffice: backOfficeEmail, +): string[] | Error => { + const unsupportedErrorMessage = `Unsupported role ${role}`; + const missingActorConventionErrorMessage = `No actor with role ${role} for convention ${convention.id}`; + const missingActorAgencyErrorMessage = `No actor with role ${role} for agency ${agency.id}`; + + const strategy: Record = { "beneficiary-current-employer": convention.signatories.beneficiaryCurrentEmployer?.email ?? - missingEmailError, + new Error(missingActorConventionErrorMessage), "beneficiary-representative": convention.signatories.beneficiaryRepresentative?.email ?? - missingEmailError, - "establishment-tutor": error, + new Error(missingActorConventionErrorMessage), + "establishment-tutor": new Error(unsupportedErrorMessage), "legal-representative": convention.signatories.beneficiaryRepresentative?.email ?? - missingEmailError, - counsellor: agency.counsellorEmails[0], - validator: agency.validatorEmails[0], + new Error(missingActorConventionErrorMessage), "establishment-representative": convention.signatories.establishmentRepresentative.email, establishment: convention.signatories.establishmentRepresentative.email, beneficiary: convention.signatories.beneficiary.email, + counsellor: + agency.counsellorEmails.length > 0 + ? agency.counsellorEmails + : new Error(missingActorAgencyErrorMessage), + validator: + agency.validatorEmails.length > 0 + ? agency.validatorEmails + : new Error(missingActorAgencyErrorMessage), + backOffice: backOfficeEmail, }; - return strategy[role]; + const result = strategy[role]; + return Array.isArray(result) || result instanceof Error ? result : [result]; }; diff --git a/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.unit.test.ts b/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.unit.test.ts index f98e0bfc4b..ad9574c99b 100644 --- a/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.unit.test.ts +++ b/back/src/domain/convention/useCases/notifications/NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification.unit.test.ts @@ -1,4 +1,5 @@ import { + AbsoluteUrl, AgencyDtoBuilder, ConventionDtoBuilder, CreateConventionMagicLinkPayloadProperties, @@ -22,6 +23,7 @@ import { CustomTimeGateway } from "../../../../adapters/secondary/core/TimeGatew import { UuidV4Generator } from "../../../../adapters/secondary/core/UuidGeneratorImplementations"; import { InMemoryUowPerformer } from "../../../../adapters/secondary/InMemoryUowPerformer"; import { DeterministShortLinkIdGeneratorGateway } from "../../../../adapters/secondary/shortLinkIdGeneratorGateway/DeterministShortLinkIdGeneratorGateway"; +import { ShortLinkId } from "../../../core/ports/ShortLinkQuery"; import { TimeGateway } from "../../../core/ports/TimeGateway"; import { makeShortLinkUrl } from "../../../core/ShortLink"; import { makeSaveNotificationAndRelatedEvent } from "../../../generic/notifications/entities/Notification"; @@ -30,18 +32,21 @@ import { NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification, } from "./NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification"; +const beneficiaryCurrentEmployerEmail = "current@employer.com"; +const beneficiaryRepresentativeEmail = "beneficiary@representative.fr"; + const convention = new ConventionDtoBuilder() .withBeneficiaryRepresentative({ firstName: "Tom", lastName: "Cruise", phone: "0665454271", role: "beneficiary-representative", - email: "beneficiary@representative.fr", + email: beneficiaryRepresentativeEmail, }) .withBeneficiaryCurrentEmployer({ businessName: "boss", role: "beneficiary-current-employer", - email: "current@employer.com", + email: beneficiaryCurrentEmployerEmail, phone: "001223344", firstName: "Harry", lastName: "Potter", @@ -51,7 +56,11 @@ const convention = new ConventionDtoBuilder() }) .build(); -const agency = new AgencyDtoBuilder().withId(convention.agencyId).build(); +const agency = new AgencyDtoBuilder() + .withCounsellorEmails(["a@a.com", "b@b.com"]) + .withValidatorEmails(["c@c.com", "d@d.com"]) + .withId(convention.agencyId) + .build(); describe("NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification", () => { let usecase: NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification; @@ -91,44 +100,32 @@ describe("NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification", () => }); describe("Right paths", () => { - it.each<[Role, string | undefined]>([ - ["beneficiary", convention.signatories.beneficiary.email], + it.each<[Role, string[]]>([ + ["beneficiary", [convention.signatories.beneficiary.email]], [ "establishment", - convention.signatories.establishmentRepresentative.email, + [convention.signatories.establishmentRepresentative.email], ], [ "establishment-representative", - convention.signatories.establishmentRepresentative.email, - ], - [ - "beneficiary-current-employer", - convention.signatories.beneficiaryCurrentEmployer?.email, + [convention.signatories.establishmentRepresentative.email], ], - [ - "beneficiary-representative", - convention.signatories.beneficiaryRepresentative?.email, - ], - [ - "legal-representative", - convention.signatories.beneficiaryRepresentative?.email, - ], - ["counsellor", agency.counsellorEmails[0]], - ["validator", agency.validatorEmails[0]], - ["backOffice", backOfficeEmail], + ["beneficiary-current-employer", [beneficiaryCurrentEmployerEmail]], + ["beneficiary-representative", [beneficiaryRepresentativeEmail]], + ["legal-representative", [beneficiaryRepresentativeEmail]], + ["counsellor", agency.counsellorEmails], + ["validator", agency.validatorEmails], + ["backOffice", [backOfficeEmail]], ])( "Notify %s that application needs modification.", - async (role, expectedRecipient) => { - const shortLinkIds = ["shortLinkId1", "shortLinkId2"]; - shortLinkIdGateway.addMoreShortLinkIds(shortLinkIds); + async (role, expectedRecipients) => { + shortLinkIdGateway.addMoreShortLinkIds( + expectedRecipients.flatMap((expectedRecipient) => [ + `shortLinkId_${expectedRecipient}_1`, + `shortLinkId_${expectedRecipient}_2`, + ]), + ); const justification = "Change required."; - const magicLinkCommonFields: CreateConventionMagicLinkPayloadProperties = - { - id: convention.id, - role, - email: expectedRecipient!, - now: timeGateway.now(), - }; await usecase.execute({ convention, @@ -136,38 +133,55 @@ describe("NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification", () => roles: [role], }); - expectToEqual(uow.shortLinkQuery.getShortLinks(), { - [shortLinkIds[0]]: fakeGenerateMagicLinkUrlFn({ - ...magicLinkCommonFields, - targetRoute: frontRoutes.conventionImmersionRoute, - }), - [shortLinkIds[1]]: fakeGenerateMagicLinkUrlFn({ - ...magicLinkCommonFields, - targetRoute: frontRoutes.conventionStatusDashboard, - }), - }); + const shortLinks = expectedRecipients.reduce< + Partial> + >((prev, expectedRecipient, _) => { + const magicLinkCommonFields: CreateConventionMagicLinkPayloadProperties = + { + id: convention.id, + role, + email: expectedRecipient, + now: timeGateway.now(), + }; + return { + ...prev, + [`shortLinkId_${expectedRecipient}_1`]: fakeGenerateMagicLinkUrlFn({ + ...magicLinkCommonFields, + targetRoute: frontRoutes.conventionImmersionRoute, + }), + [`shortLinkId_${expectedRecipient}_2`]: fakeGenerateMagicLinkUrlFn({ + ...magicLinkCommonFields, + targetRoute: frontRoutes.conventionStatusDashboard, + }), + }; + }, {}); + + expectToEqual(uow.shortLinkQuery.getShortLinks(), shortLinks); expectSavedNotificationsAndEvents({ - emails: [ - { - kind: "CONVENTION_MODIFICATION_REQUEST_NOTIFICATION", - recipients: [expectedRecipient!], - params: { - conventionId: convention.id, - internshipKind: convention.internshipKind, - beneficiaryFirstName: - convention.signatories.beneficiary.firstName, - beneficiaryLastName: - convention.signatories.beneficiary.lastName, - businessName: convention.businessName, - justification, - magicLink: makeShortLinkUrl(config, shortLinkIds[0]), - conventionStatusLink: makeShortLinkUrl(config, shortLinkIds[1]), - signature: agency.signature, - agencyLogoUrl: agency.logoUrl, - }, + emails: expectedRecipients.map((expectedRecipient) => ({ + kind: "CONVENTION_MODIFICATION_REQUEST_NOTIFICATION", + recipients: [expectedRecipient], + params: { + conventionId: convention.id, + internshipKind: convention.internshipKind, + beneficiaryFirstName: + convention.signatories.beneficiary.firstName, + beneficiaryLastName: convention.signatories.beneficiary.lastName, + businessName: convention.businessName, + justification, + magicLink: makeShortLinkUrl( + config, + `shortLinkId_${expectedRecipient}_1`, + ), + conventionStatusLink: makeShortLinkUrl( + config, + `shortLinkId_${expectedRecipient}_2`, + ), + signature: agency.signature, + agencyLogoUrl: agency.logoUrl, }, - ], + })), }); }, ); @@ -184,9 +198,42 @@ describe("NotifyBeneficiaryAndEnterpriseThatApplicationNeedsModification", () => justification, roles: [role], }), - `Unsupported role for beneficiary/enterprise modification request notification: ${role}`, + `Unsupported role ${role}`, ); }, ); + it("Agency without counsellors", async () => { + const role: Role = "counsellor"; + const agencyWithoutCounsellors = new AgencyDtoBuilder(agency) + .withCounsellorEmails([]) + .build(); + uow.agencyRepository.setAgencies([agencyWithoutCounsellors]); + + await expectPromiseToFailWith( + usecase.execute({ + convention, + justification: "OSEF", + roles: [role], + }), + `No actor with role ${role} for agency ${agencyWithoutCounsellors.id}`, + ); + }); + + it("Agency without validators", async () => { + const role: Role = "validator"; + const agencyWithoutValidators = new AgencyDtoBuilder(agency) + .withValidatorEmails([]) + .build(); + uow.agencyRepository.setAgencies([agencyWithoutValidators]); + + await expectPromiseToFailWith( + usecase.execute({ + convention, + justification: "OSEF", + roles: [role], + }), + `No actor with role ${role} for agency ${agencyWithoutValidators.id}`, + ); + }); }); });