Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3054 - Email notification for blocked disbursement to ministry only once #4245

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DisbursementScheduleStatus,
DisbursementValueType,
FormYesNoOptions,
NotificationMessage,
NotificationMessageType,
OfferingIntensity,
RelationshipStatus,
Expand All @@ -22,6 +23,7 @@ import {
createFakeDisbursementOveraward,
createFakeDisbursementValue,
createFakeMSFAANumber,
createFakeNotification,
createFakeUser,
saveFakeApplicationDisbursements,
saveFakeApplicationRestrictionBypass,
Expand Down Expand Up @@ -1361,8 +1363,8 @@ describe(
expect(
mockedJob.containLogMessages([
"Disbursement estimated awards do not contain any amount to be disbursed.",
`Creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Completed creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Ministry Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
`Student Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
]),
).toBe(true);
const notifications = await db.notification.find({
Expand Down Expand Up @@ -1399,6 +1401,77 @@ describe(
},
);

it("Should create a notification for the student and avoid creating a notification for the Ministry when the Ministry was already notified about the same blocked disbursement.", async () => {
// Arrange
const { student, disbursement } = await createBlockedDisbursementTestData(
db,
{
offeringIntensity: OfferingIntensity.fullTime,
isValidSIN: true,
disbursementValues: [],
},
);
// Create a sent notification for Ministry.
const ministryNotification = createFakeNotification(
{
user: systemUsersService.systemUser,
auditUser: systemUsersService.systemUser,
notificationMessage: {
id: NotificationMessageType.MinistryNotificationDisbursementBlocked,
} as NotificationMessage,
},
{
initialValue: {
metadata: {
disbursementId: disbursement.id,
},
dateSent: new Date(),
},
},
);
await db.notification.save(ministryNotification);

// Queued job.
const mockedJob = mockBullJob<void>();

// Act
await processor.processQueue(mockedJob.job);

// Assert
// Assert disbursement was blocked and expected student notification was created and ministry notification was not created.
expect(
mockedJob.containLogMessages([
"The step determined that the calculation should be interrupted. This disbursement will not be part of the next e-Cert generation.",
`Ministry Blocked Disbursement notification should not be created at this moment for disbursement ID ${disbursement.id}.`,
`Student Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
]),
).toBe(true);
// Assert only student notification was created on DB.
const notifications = await db.notification.find({
select: {
id: true,
user: { id: true },
notificationMessage: { id: true },
},
relations: { user: true, notificationMessage: true },
where: {
metadata: {
disbursementId: disbursement.id,
},
dateSent: IsNull(),
},
});
expect(notifications).toEqual([
{
id: expect.any(Number),
notificationMessage: {
id: NotificationMessageType.StudentNotificationDisbursementBlocked,
},
user: { id: student.user.id },
},
]);
});

/**
* Helper function to get the uploaded file name.
* @returns The uploaded file name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ describe(
]);
expect(
mockedJob.containLogMessages([
`Creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Completed creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Ministry Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
`Student Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
]),
).toBe(true);
const notifications = await db.notification.find({
Expand Down Expand Up @@ -191,8 +191,8 @@ describe(
expect(
mockedJob.containLogMessages([
"Disbursement estimated awards do not contain any amount to be disbursed.",
`Creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Completed creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Ministry Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
`Student Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
]),
).toBe(true);
const notifications = await db.notification.find({
Expand Down Expand Up @@ -287,6 +287,13 @@ describe(
`Generated file: ${uploadedFileName}`,
"Uploaded records: 0",
]);
expect(
mockedJob.containLogMessages([
"The step determined that the calculation should be interrupted. This disbursement will not be part of the next e-Cert generation.",
`Ministry Blocked Disbursement notification should not be created at this moment for disbursement ID ${disbursement.id}.`,
`Student Blocked Disbursement notification should not be created at this moment for disbursement ID ${disbursement.id}.`,
]),
).toBe(true);
const notificationsCount = await db.notification.count({
where: {
notificationMessage: {
Expand Down Expand Up @@ -326,8 +333,8 @@ describe(
]);
expect(
mockedJob.containLogMessages([
`Creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Completed creating notifications for disbursement id: ${disbursement.id} for student and ministry.`,
`Ministry Blocked Disbursement notification should not be created at this moment for disbursement ID ${disbursement.id}.`,
`Student Blocked Disbursement notification created for disbursement ID ${disbursement.id}.`,
]),
).toBe(true);
const notificationsCount = await db.notification.count({
Expand All @@ -338,8 +345,8 @@ describe(
dateSent: IsNull(),
},
});
// Checking 1 created notification for the student and 1 notification for the ministry.
expect(notificationsCount).toBe(2);
// Checking 1 created notification for the student only.
expect(notificationsCount).toBe(1);
});

it("Should create an e-Cert with one disbursement record for one student with one eligible schedule.", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ import {
} from "@sims/integrations/services/disbursement-schedule/e-cert-calculation";
import { FullTimeECertFileHandler } from "./full-time-e-cert-file-handler";
import { PartTimeECertFileHandler } from "./part-time-e-cert-file-handler";
import {
MinistryBlockedDisbursementNotification,
StudentBlockedDisbursementNotification,
} from "@sims/integrations/services/disbursement-schedule/e-cert-notification";

@Module({
imports: [ConfigModule, SystemUserModule],
Expand Down Expand Up @@ -88,6 +92,8 @@ import { PartTimeECertFileHandler } from "./part-time-e-cert-file-handler";
StudentLoanBalanceSharedService,
ECertFeedbackErrorService,
ECertPreValidationService,
MinistryBlockedDisbursementNotification,
StudentBlockedDisbursementNotification,
],
exports: [
FullTimeECertFileHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ export abstract class ECertCalculationProcess {
disbursementLog,
);
if (!shouldProceed) {
await this.eCertNotificationService.createDisbursementBlockedNotification(
eCertDisbursement.disbursement.id,
await this.eCertNotificationService.notifyBlockedDisbursement(
eCertDisbursement,
entityManager,
parentLog,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Injectable } from "@nestjs/common";
import {
DisbursementBlockedNotification,
NotificationActionsService,
} from "@sims/services";
import { Notification, NotificationMessageType, Student } from "@sims/sims-db";
import { EntityManager } from "typeorm";
import { ECertNotification } from "../e-cert-notification";
import { EligibleECertDisbursement } from "../../disbursement-schedule.models";

/**
* Creates a notification for a blocked disbursement for the Ministry, if required.
*/
@Injectable()
export class MinistryBlockedDisbursementNotification extends ECertNotification {
constructor(
private readonly notificationActionsService: NotificationActionsService,
) {
super("Ministry Blocked Disbursement");
}

/**
* Determines whether a notification should be created or not.
* @param eCertDisbursement eligible disbursement to be potentially added to an e-Cert.
* @param entityManager entity manager to execute in transaction.
* @returns true if a notification should be created, false otherwise.
*/
protected async shouldCreateNotification(
eCertDisbursement: EligibleECertDisbursement,
entityManager: EntityManager,
): Promise<boolean> {
const hasNotification = await entityManager
.getRepository(Notification)
.exists({
where: {
notificationMessage: {
id: NotificationMessageType.MinistryNotificationDisbursementBlocked,
},
metadata: {
disbursementId: eCertDisbursement.disbursement.id,
},
},
});
return !hasNotification;
}

/**
* Creates a notification for the given e-Cert disbursement.
* @param eCertDisbursement eligible disbursement to be potentially added to an e-Cert.
* @param entityManager entity manager to execute in transaction.
*/
protected async createNotification(
eCertDisbursement: EligibleECertDisbursement,
entityManager: EntityManager,
): Promise<void> {
const student = await entityManager.getRepository(Student).findOne({
select: {
id: true,
birthDate: true,
user: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
relations: {
user: true,
},
where: { id: eCertDisbursement.studentId },
});
const ministryNotification: DisbursementBlockedNotification = {
givenNames: student.user.firstName,
lastName: student.user.lastName,
email: student.user.email,
birthDate: student.birthDate,
applicationNumber: eCertDisbursement.applicationNumber,
};
await this.notificationActionsService.saveDisbursementBlockedNotificationForMinistry(
ministryNotification,
eCertDisbursement.disbursement.id,
entityManager,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Injectable } from "@nestjs/common";
import {
NotificationActionsService,
StudentNotification,
} from "@sims/services";
import {
BLOCKED_DISBURSEMENT_MAXIMUM_NOTIFICATIONS_TO_SEND,
BLOCKED_DISBURSEMENT_NOTIFICATION_MIN_DAYS_INTERVAL,
} from "@sims/services/constants";
import { Notification, NotificationMessageType, Student } from "@sims/sims-db";
import { dateDifference } from "@sims/utilities";
import { EntityManager } from "typeorm";
import { ECertNotification } from "../e-cert-notification";
import { EligibleECertDisbursement } from "../../disbursement-schedule.models";

interface NotificationData {
maxCreatedAt: Date;
notificationCount: number;
}

/**
* Creates a notification for a blocked disbursement for the Student, if required.
*/
@Injectable()
export class StudentBlockedDisbursementNotification extends ECertNotification {
constructor(
private readonly notificationActionsService: NotificationActionsService,
) {
super("Student Blocked Disbursement");
}

/**
* Determines whether a notification should be created or not.
* @param eCertDisbursement eligible disbursement to be potentially added to an e-Cert.
* @param entityManager entity manager to execute in transaction.
* @returns true if a notification should be created, false otherwise.
*/
protected async shouldCreateNotification(
eCertDisbursement: EligibleECertDisbursement,
entityManager: EntityManager,
): Promise<boolean> {
const notification = await entityManager
.getRepository(Notification)
.createQueryBuilder("notification")
.select("count(*)::int", "notificationCount")
.addSelect("max(notification.createdAt)", "maxCreatedAt")
.innerJoin("notification.notificationMessage", "notificationMessage")
.where("notificationMessage.id = :notificationMessageId", {
notificationMessageId:
NotificationMessageType.StudentNotificationDisbursementBlocked,
})
.andWhere("notification.metadata->>'disbursementId' = :disbursementId", {
disbursementId: eCertDisbursement.disbursement.id,
})
.getRawOne<NotificationData>();
const canSendNewNotification =
dateDifference(new Date(), notification.maxCreatedAt) >
BLOCKED_DISBURSEMENT_NOTIFICATION_MIN_DAYS_INTERVAL;
// Condition check to create notifications: Less than BLOCKED_DISBURSEMENT_MAXIMUM_NOTIFICATIONS_TO_SEND notifications are created previously and there are no failures in sending those created notifications.
// Plus, it has been at least BLOCKED_DISBURSEMENT_NOTIFICATION_MIN_DAYS_INTERVAL days from the last created notification for this disbursement.
return (
!notification.notificationCount ||
(notification.notificationCount <
BLOCKED_DISBURSEMENT_MAXIMUM_NOTIFICATIONS_TO_SEND &&
canSendNewNotification)
);
}

/**
* Creates a notification for the given e-Cert disbursement.
* @param eCertDisbursement eligible disbursement to be potentially added to an e-Cert.
* @param entityManager entity manager to execute in transaction.
*/
protected async createNotification(
eCertDisbursement: EligibleECertDisbursement,
entityManager: EntityManager,
): Promise<void> {
const student = await entityManager.getRepository(Student).findOne({
select: {
id: true,
user: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
relations: {
user: true,
},
where: { id: eCertDisbursement.studentId },
});
const studentNotification: StudentNotification = {
givenNames: student.user.firstName,
lastName: student.user.lastName,
toAddress: student.user.email,
userId: student.user.id,
};
await this.notificationActionsService.saveDisbursementBlockedNotificationForStudent(
studentNotification,
eCertDisbursement.disbursement.id,
entityManager,
);
}
}
Loading
Loading