Skip to content

Commit

Permalink
#2979 - Email notification - PD/PPD Student reminder email 8 weeks be…
Browse files Browse the repository at this point in the history
…fore end date Part 2 (#3759)

**AC**
- [X] Select all student applications where
  - Offerings end date within 8 weeks.
  - Applications with current disbursement pending.
  - Application not archived.
  - Remove assessments where a notification was already sent.
- With the PD/PPD mismatch using the same logic from
[validate-disbursement-base.ts](https://github.com/bcgov/SIMS/blob/main/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts#L44).
- [X] Save the assessment ID as metadata and ensure the email will be
sent once.

- [x] E2E Tests suite added, E2E tests will be added in Part 3 PR
  - [] Ensure the email will be sent once for the same assessment.
- [] Ensure the email will be sent again for different assessments for
the same application.


![image](https://github.com/user-attachments/assets/6e2c5ec4-8cd4-4f53-a7a8-60ae60a756dd)


Migrations Revert Demo:

![image](https://github.com/user-attachments/assets/6b1d1239-e818-468d-8352-ebe25e32401e)

Sample Email

![image](https://github.com/user-attachments/assets/ad743e58-3284-4c02-81ff-1184ab862685)

Underlying SQL for TypeOrm

```
SELECT "application"."application_number" AS "application_application_number",
       "application"."id"                 AS "application_id",
       "student_assessments"."id"         AS "student_assessments_id",
       "students"."disability_status"     AS "students_disability_status",
       "students"."id"                    AS "students_id",
       "users"."id"                       AS "users_id",
       "users"."email"                    AS "users_email",
       "users"."first_name"               AS "users_first_name",
       "users"."last_name"                AS "users_last_name"
FROM "sims"."applications" "application"
         INNER JOIN "sims"."student_assessments" "student_assessments"
                    ON "student_assessments"."id" = "application"."current_assessment_id"
         INNER JOIN "sims"."students" "students" ON "students"."id" = "application"."student_id"
         INNER JOIN "sims"."users" "users" ON "users"."id" = "students"."user_id"
         INNER JOIN "sims"."education_programs_offerings" "epo" ON "epo"."id" = "student_assessments"."offering_id"
         INNER JOIN "sims"."disbursement_schedules" "ds" ON "ds"."student_assessment_id" = "student_assessments"."id"
WHERE "epo"."study_end_date" >= '2024-11-28T21:37:26.764Z'
  AND "students"."disability_status" NOT IN ('PD', 'PPD')
  AND json_extract_path_text(student_assessments.workflow_data::json, 'calculatedData', 'pdppdStatus') = 'true'
  AND "application"."is_archived" = false
  AND "ds"."disbursement_schedule_status" = 'Pending'
  AND NOT EXISTS ((SELECT 1
                   FROM "sims"."notifications" "n"
                   WHERE "n"."notification_message_id" = 30
                     AND json_extract_path_text(n.metadata::json, 'assessmentId') =
                         CAST("student_assessments"."id" AS TEXT)))
  AND NOT EXISTS ((SELECT 1
                   FROM "sims"."disbursement_schedules" "ds2"
                   WHERE "ds2"."student_assessment_id" = "student_assessments"."id"
                     AND "ds2"."disbursement_date" < "ds"."disbursement_date"))
```
  • Loading branch information
bidyashish authored Oct 8, 2024
1 parent fbb17a9 commit 0464a84
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { getSQLFileData } from "../utilities/sqlLoader";

export class InsertStudentNotificationPdPpdStudentReminder1727890225038
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Insert-student-notification-pd-ppd-student-reminder.sql",
"NotificationMessages",
),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Rollback-insert-student-notification-pd-ppd-student-reminder.sql",
"NotificationMessages",
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
INSERT INTO
sims.notification_messages(id, description, template_id)
VALUES
(
30,
'Student application notification for PD/PPD reminder email 8 weeks before end date.',
'7faea39f-cf8e-41ee-af02-c4790cac5b26'
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DELETE FROM
sims.notification_messages
WHERE
ID = 30;
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { createMock } from "@golevelup/ts-jest";
import { INestApplication } from "@nestjs/common";
import { QueueNames } from "@sims/utilities";
import {
createTestingAppModule,
describeProcessorRootTest,
} from "../../../../../test/helpers";
import {
E2EDataSources,
createE2EDataSources,
saveFakeApplicationDisbursements,
saveFakeStudent,
} from "@sims/test-utils";
import { Job } from "bull";
import {
ApplicationStatus,
DisabilityStatus,
DisbursementScheduleStatus,
NotificationMessageType,
WorkflowData,
} from "@sims/sims-db";
import { IsNull, Not } from "typeorm";
import { StudentApplicationNotificationsScheduler } from "../../student-application-notifications/student-application-notifications.scheduler";

describe(
describeProcessorRootTest(QueueNames.StudentApplicationNotifications),
() => {
let app: INestApplication;
let processor: StudentApplicationNotificationsScheduler;
let db: E2EDataSources;

beforeAll(async () => {
// Setup the app and data sources.
const { nestApplication, dataSource } = await createTestingAppModule();
app = nestApplication;
db = createE2EDataSources(dataSource);
// Processor under test.
processor = app.get(StudentApplicationNotificationsScheduler);
});

beforeEach(async () => {
// Cancel all applications to ensure tha existing data will not affect these tests.
await db.application.update(
{ applicationStatus: Not(ApplicationStatus.Cancelled) },
{ applicationStatus: ApplicationStatus.Cancelled },
);
});

it(
"Should generate a notification for PD/PPD student mismatch close to the offering end date " +
"when the application is completed and at least one disbursement is pending and there is a PD/PPD mismatch.",
async () => {
// Arrange
// Create a student with a non-approved disability.
const student = await saveFakeStudent(db.dataSource, undefined, {
initialValue: { disabilityStatus: DisabilityStatus.Requested },
});
// Create an application with the disability as true.
const application = await saveFakeApplicationDisbursements(
db.dataSource,
{ student },
{
applicationStatus: ApplicationStatus.Completed,
currentAssessmentInitialValues: {
workflowData: {
calculatedData: {
pdppdStatus: true,
},
} as WorkflowData,
},
createSecondDisbursement: true,
firstDisbursementInitialValues: {
disbursementScheduleStatus: DisbursementScheduleStatus.Sent,
},
secondDisbursementInitialValues: {
disbursementScheduleStatus: DisbursementScheduleStatus.Pending,
},
},
);
// Queued job.
const job = createMock<Job<void>>();

// Act
await processor.studentApplicationNotifications(job);

// Assert
const notification = await db.notification.findOne({
select: {
id: true,
messagePayload: true,
},
relations: { notificationMessage: true },
where: {
notificationMessage: {
id: NotificationMessageType.StudentPDPPDApplicationNotification,
},
dateSent: IsNull(),
user: { id: student.user.id },
},
});
expect(notification).toBeDefined();
expect(notification.messagePayload).toStrictEqual({
email_address: application.student.user.email,
template_id: "7faea39f-cf8e-41ee-af02-c4790cac5b26",
personalisation: {
lastName: application.student.user.lastName,
givenNames: application.student.user.firstName,
applicationNumber: application.applicationNumber,
},
});
},
);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
} from "../../../utilities";
import { QueueNames } from "@sims/utilities";
import { ApplicationService } from "../../../services";
import { NotificationService } from "@sims/services/notifications";
import { StudentPDPPDNotification } from "@sims/services/notifications";
import { NotificationActionsService } from "@sims/services";

@Processor(QueueNames.StudentApplicationNotifications)
export class StudentApplicationNotificationsScheduler extends BaseScheduler<void> {
Expand All @@ -22,7 +23,7 @@ export class StudentApplicationNotificationsScheduler extends BaseScheduler<void
schedulerQueue: Queue<void>,
queueService: QueueService,
private readonly applicationService: ApplicationService,
private readonly notificationService: NotificationService,
private readonly notificationActionsService: NotificationActionsService,
) {
super(schedulerQueue, queueService);
}
Expand All @@ -39,7 +40,35 @@ export class StudentApplicationNotificationsScheduler extends BaseScheduler<void
`Processing student application notifications job. Job id: ${job.id} and Job name: ${job.name}.`,
);

// TODO: Get applications that have a disability status mismatch and check PDPPD status
const eligibleApplications =
await this.applicationService.getApplicationWithPDPPStatusMismatch();

const notifications = eligibleApplications.map<StudentPDPPDNotification>(
(application) => ({
userId: application.student.user.id,
givenNames: application.student.user.firstName,
lastName: application.student.user.lastName,
email: application.student.user.email,
applicationNumber: application.applicationNumber,
assessmentId: application.currentAssessment.id,
}),
);

await this.notificationActionsService.saveStudentApplicationPDPPDNotification(
notifications,
);

if (eligibleApplications.length) {
processSummary.info(
`PD/PPD mismatch assessments that generated notifications: ${eligibleApplications
.map((app) => app.currentAssessment.id)
.join(", ")}`,
);
} else {
processSummary.info(
`No assessments found to generate PD/PPD mismatch notifications.`,
);
}

return getSuccessMessageWithAttentionCheck(
["Process finalized with success."],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import {
User,
StudentAssessment,
StudentAssessmentStatus,
DisabilityStatus,
DisbursementScheduleStatus,
NotificationMessageType,
Notification,
DisbursementSchedule,
} from "@sims/sims-db";
import { ConfigService } from "@sims/utilities/config";
import { InjectRepository } from "@nestjs/typeorm";
import { addDays, DISABILITY_NOTIFICATION_DAYS_LIMIT } from "@sims/utilities";

@Injectable()
export class ApplicationService {
Expand All @@ -18,6 +24,10 @@ export class ApplicationService {
private readonly applicationRepo: Repository<Application>,
@InjectRepository(StudentAssessment)
private readonly studentAssessmentRepo: Repository<StudentAssessment>,
@InjectRepository(Notification)
private readonly notificationRepo: Repository<Notification>,
@InjectRepository(DisbursementSchedule)
private readonly disbursementScheduleRepo: Repository<DisbursementSchedule>,
) {}

/**
Expand Down Expand Up @@ -153,4 +163,76 @@ export class ApplicationService {
.getMany()
);
}

/**
* Retrieves applications eligible for a specific notification (likely related to disability status and PD/PPD status).
* This method applies several criteria to filter eligible applications:
* - Study end date is at least 8 weeks in the future
* - Student's disability status is not 'PD' or 'PPD'
* - The assessment's workflow data indicates a positive PD/PPD status
* - The application is not archived
* - There's a pending disbursement schedule
* - No notification of this type (messageId 30) has been sent for this assessment
* @returns An array of eligible applications with relevant details for notification.
*/
async getApplicationWithPDPPStatusMismatch(): Promise<Application[]> {
const disabilityNotificationDateLimit = addDays(
DISABILITY_NOTIFICATION_DAYS_LIMIT,
);
// Sub query to defined if a notification was already sent to the current assessment.
const notificationExistsQuery = this.notificationRepo
.createQueryBuilder("notification")
.select("1")
.where("notification.notificationMessage.id = :messageId")
.andWhere(
"notification.metadata->>'assessmentId' = currentAssessment.id :: text",
)
.getQuery();
// Sub query to defined if there is at least on disbursement pending to be sent.
const pendingDisbursementExistsQuery = this.disbursementScheduleRepo
.createQueryBuilder("disbursement")
.select("1")
.where("disbursement.studentAssessment.id = currentAssessment.id")
.andWhere(
"disbursement.disbursementScheduleStatus = :disbursementScheduleStatusPending",
)
.getQuery();
return this.applicationRepo
.createQueryBuilder("application")
.select([
"application.id",
"application.applicationNumber",
"student.id",
"user.id",
"user.firstName",
"user.lastName",
"user.email",
"currentAssessment.id",
])
.innerJoin("application.currentAssessment", "currentAssessment")
.innerJoin("application.student", "student")
.innerJoin("student.user", "user")
.innerJoin("currentAssessment.offering", "offering")
.where("application.applicationStatus = :applicationStatus", {
applicationStatus: ApplicationStatus.Completed,
})
.andWhere("offering.studyEndDate <= :disabilityNotificationDateLimit", {
disabilityNotificationDateLimit,
})
.andWhere("student.disabilityStatus NOT IN (:pdStatus, :ppdStatus)", {
pdStatus: DisabilityStatus.PD,
ppdStatus: DisabilityStatus.PPD,
})
.andWhere(
'currentAssessment.workflow_data @> \'{ "calculatedData": { "pdppdStatus": true}}\'',
)
.andWhere("application.isArchived = false")
.andWhere(`EXISTS (${pendingDisbursementExistsQuery})`)
.andWhere(`NOT EXISTS (${notificationExistsQuery})`)
.setParameters({
messageId: NotificationMessageType.StudentPDPPDApplicationNotification,
disbursementScheduleStatusPending: DisbursementScheduleStatus.Pending,
})
.getMany();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
ECertFeedbackFileErrorNotification,
DailyDisbursementReportProcessingNotification,
SupportingUserInformationNotification,
StudentPDPPDNotification,
} from "..";
import { NotificationService } from "./notification.service";
import { InjectLogger, LoggerService } from "@sims/utilities/logger";
Expand Down Expand Up @@ -1258,6 +1259,42 @@ export class NotificationActionsService {
return { templateId, emailContacts };
}

/**
* Creates student application notification for student for PDPPD assessment.
* @param notifications notification details array.
* @param entityManager entity manager to execute in transaction.
*/
async saveStudentApplicationPDPPDNotification(
notifications: StudentPDPPDNotification[],
entityManager?: EntityManager,
): Promise<void> {
const auditUser = this.systemUsersService.systemUser;
const { templateId } =
await this.notificationMessageService.getNotificationMessageDetails(
NotificationMessageType.StudentPDPPDApplicationNotification,
);
const notificationsToSend = notifications.map((notification) => ({
userId: notification.userId,
messageType: NotificationMessageType.StudentPDPPDApplicationNotification,
messagePayload: {
email_address: notification.email,
template_id: templateId,
personalisation: {
givenNames: notification.givenNames ?? "",
lastName: notification.lastName,
applicationNumber: notification.applicationNumber,
},
},
metadata: { assessmentId: notification.assessmentId },
}));
// Save notifications to be sent to the students into the notification table.
await this.notificationService.saveNotifications(
notificationsToSend,
auditUser.id,
{ entityManager },
);
}

@InjectLogger()
logger: LoggerService;
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,12 @@ export interface SupportingUserInformationNotification {
userId: number;
supportingUserType: NotificationSupportingUserType;
}

export interface StudentPDPPDNotification {
userId: number;
assessmentId: number;
givenNames: string;
lastName: string;
email: string;
applicationNumber: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
export interface NotificationMetadata {
disbursementId?: number;
applicationNumber?: string;
assessmentId?: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,8 @@ export enum NotificationMessageType {
* Supporting User Information Notification.
*/
SupportingUserInformationNotification = 29,
/**
* Student PD PPD Application Notification.
*/
StudentPDPPDApplicationNotification = 30,
}
Loading

0 comments on commit 0464a84

Please sign in to comment.