Skip to content

Commit

Permalink
do not send assessment reminders if already sent
Browse files Browse the repository at this point in the history
  • Loading branch information
celineung committed Jan 30, 2025
1 parent 4a9db61 commit ac23019
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 2 deletions.
4 changes: 4 additions & 0 deletions back/scalingo/cron.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"command": "0 21 * * * pnpm back run trigger-delete-email-attachements",
"size": "M"
},
{
"command": "15 21 * * * pnpm back run trigger-assessment-reminders",
"size": "M"
},
{
"command": "0 22 * * * pnpm back run trigger-mark-old-convention-as-deprecated",
"size": "M"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { parseISO } from "date-fns";
import { difference } from "ramda";
import {
AgencyDto,
AgencyWithUsersRights,
ConventionDto,
ConventionId,
EmailNotification,
EmailType,
Notification,
NotificationId,
NotificationKind,
ShortLinkId,
Signatory,
SmsNotification,
SmsTemplateByName,
TemplatedEmail,
concatValidatorNames,
displayEmergencyContactInfos,
Expand All @@ -23,6 +27,36 @@ export class InMemoryNotificationRepository implements NotificationRepository {
// for tests purposes
public notifications: Notification[] = [];

public async getConventionIdsWithoutNotifications({
emailType,
smsType,
conventionIds,
}: {
emailType?: EmailType;
smsType?: keyof SmsTemplateByName;
conventionIds: ConventionId[];
}): Promise<ConventionId[]> {
const conventionsWithNotifications = this.notifications
.filter(
(notification) =>
notification.followedIds.conventionId &&
conventionIds.includes(notification.followedIds.conventionId),
)
.filter((notification) => {
if (emailType && notification.kind === "email") {
return notification.templatedContent.kind === emailType;
}
if (smsType && notification.kind === "sms") {
return notification.templatedContent.kind === smsType;
}
return false;
})
.map((notification) => notification.followedIds.conventionId)
.filter((id): id is ConventionId => id !== null);

return difference(conventionIds, conventionsWithNotifications);
}

async getSmsByIds(ids: NotificationId[]): Promise<SmsNotification[]> {
return getNotificationsMatchingKindAndIds("sms", this.notifications, ids);
}
Expand Down
19 changes: 18 additions & 1 deletion back/src/domains/establishment/use-cases/AssessmentReminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
NotificationContentAndFollowedIds,
SaveNotificationAndRelatedEvent,
} from "../../core/notifications/helpers/Notification";
import { NotificationRepository } from "../../core/notifications/ports/NotificationRepository";
import { TimeGateway } from "../../core/time-gateway/ports/TimeGateway";
import { UnitOfWork } from "../../core/unit-of-work/ports/UnitOfWork";

Expand Down Expand Up @@ -55,6 +56,7 @@ export const makeAssessmentReminder = createTransactionalUseCase<
now,
assessmentRepository: uow.assessmentRepository,
conventionRepository: uow.conventionRepository,
notificationRepository: uow.notificationRepository,
});

await executeInSequence(conventionIdsToRemind, async (conventionId) => {
Expand Down Expand Up @@ -95,11 +97,13 @@ const getConventionIdsToRemind = async ({
now,
conventionRepository,
assessmentRepository,
notificationRepository,
}: {
mode: AssessmentReminderMode;
now: Date;
conventionRepository: ConventionRepository;
assessmentRepository: AssessmentRepository;
notificationRepository: NotificationRepository;
}): Promise<ConventionId[]> => {
const daysAfterLastNotifications =
mode === "3daysAfterConventionEnd" ? 3 : 10;
Expand All @@ -112,7 +116,20 @@ const getConventionIdsToRemind = async ({
await assessmentRepository.getByConventionIds(potentialConventionsToRemind)
).map((assessment) => assessment.conventionId);

return difference(potentialConventionsToRemind, conventionsWithAssessments);
const ids = await notificationRepository.getConventionIdsWithoutNotifications(
{
emailType:
mode === "3daysAfterConventionEnd"
? "ASSESSMENT_AGENCY_FIRST_REMINDER"
: "ASSESSMENT_AGENCY_SECOND_REMINDER",
conventionIds: difference(
potentialConventionsToRemind,
conventionsWithAssessments,
),
},
);

return ids;
};

const createNotification = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { addDays } from "date-fns";
import subDays from "date-fns/subDays";
import {
AgencyDtoBuilder,
Expand Down Expand Up @@ -172,4 +173,148 @@ describe("AssessmentReminder", () => {
{ topic: "NotificationAdded" },
]);
});

it("do not send first reminder if it is already sent", async () => {
const now = timeGateway.now();
const conventionEndDate = subDays(now, 3);
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);
uow.notificationRepository.notifications = [
{
kind: "email",
id: "111111111111-1111-4000-1111-111111111111",
createdAt: addDays(conventionEndDate, 3).toISOString(),
followedIds: { conventionId: convention.id },
templatedContent: {
kind: "ASSESSMENT_AGENCY_FIRST_REMINDER",
recipients: [validator.email],
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,
}),
},
},
},
];

const { numberOfReminders } = await assessmentReminder.execute({
mode: "3daysAfterConventionEnd",
});

expect(numberOfReminders).toBe(0);
});

it("do not send second reminder if it is already sent", 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);
uow.notificationRepository.notifications = [
{
kind: "email",
id: "111111111111-1111-4000-1111-111111111111",
createdAt: addDays(conventionEndDate, 3).toISOString(),
followedIds: { conventionId: convention.id },
templatedContent: {
kind: "ASSESSMENT_AGENCY_FIRST_REMINDER",
recipients: [validator.email],
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,
}),
},
},
},
{
kind: "email",
id: "111111111111-1111-4000-1111-111111111112",
createdAt: addDays(conventionEndDate, 3).toISOString(),
followedIds: { conventionId: convention.id },
templatedContent: {
kind: "ASSESSMENT_AGENCY_SECOND_REMINDER",
recipients: [validator.email],
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,
}),
},
},
},
];

const { numberOfReminders } = await assessmentReminder.execute({
mode: "10daysAfterConventionEnd",
});

expect(numberOfReminders).toBe(0);
});
});
2 changes: 1 addition & 1 deletion back/src/scripts/triggerAssessmentReminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ handleCRONScript(
({ numberOfFirstReminders, numberOfSecondReminders }) =>
[
`Total of first reminders : ${numberOfFirstReminders}`,
`Total if second reminders: ${numberOfSecondReminders}`,
`Total of second reminders: ${numberOfSecondReminders}`,
].join("\n"),
logger,
);

0 comments on commit ac23019

Please sign in to comment.