From 3ff7b466af46c863a0c4c86918f3ffe29047db37 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Thu, 12 Mar 2026 17:26:55 -0700 Subject: [PATCH 01/16] Form Submissions, Appeals, Change requests notifications initial changes --- .../form-submission-approval.service.ts | 21 ++- .../form-submission-submit.service.ts | 80 ++++++++- .../form-submission/form-submission.models.ts | 1 + .../apps/api/src/services/form/constants.ts | 20 +++ .../student-appeal-assessment.service.ts | 53 +++++- .../student-appeal/student-appeal.service.ts | 72 +++++++- ...AppealChangeRequestNotificationMessages.ts | 22 +++ ...l-change-request-notification-messages.sql | 23 +++ ...l-change-request-notification-messages.sql | 4 + .../notification-actions.service.ts | 159 ++++++++++++++++++ .../notification/notification.model.ts | 15 ++ .../src/entities/notification.model.ts | 16 ++ 12 files changed, 466 insertions(+), 20 deletions(-) create mode 100644 sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts create mode 100644 sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql create mode 100644 sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Rollback-insert-appeal-change-request-notification-messages.sql diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-approval.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-approval.service.ts index d9f3a46ea0..cb463cbe0d 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-approval.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-approval.service.ts @@ -32,6 +32,7 @@ import { } from "./form-submission.models"; import { NoteSharedService } from "@sims/services"; import { FormSubmissionActionProcessor } from "./form-submission-actions/form-submission-action-processor"; +import { NotificationActionsService } from "@sims/services/notifications"; @Injectable() export class FormSubmissionApprovalService { @@ -39,6 +40,7 @@ export class FormSubmissionApprovalService { private readonly dataSource: DataSource, private readonly noteSharedService: NoteSharedService, private readonly formSubmissionActionProcessor: FormSubmissionActionProcessor, + private readonly notificationActionsService: NotificationActionsService, @InjectRepository(FormSubmission) private readonly formSubmissionRepo: Repository, ) {} @@ -248,7 +250,10 @@ export class FormSubmissionApprovalService { const formSubmission = await formSubmissionRepo.findOne({ select: { id: true, - student: { id: true }, + student: { + id: true, + user: { id: true, firstName: true, lastName: true, email: true }, + }, submissionStatus: true, formCategory: true, application: { id: true, applicationStatus: true }, @@ -263,7 +268,7 @@ export class FormSubmissionApprovalService { }, }, relations: { - student: true, + student: { user: true }, application: true, formSubmissionItems: { currentDecision: { decisionNote: true } }, }, @@ -328,6 +333,18 @@ export class FormSubmissionApprovalService { now, entityManager, ); + // Send student notification that the form or appeal adjudication is complete. + const studentUser = formSubmission.student.user; + await this.notificationActionsService.saveStudentFormCompletedNotification( + { + givenNames: studentUser.firstName, + lastName: studentUser.lastName, + toAddress: studentUser.email, + userId: studentUser.id, + }, + auditUserId, + entityManager, + ); return formSubmission; }); } diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts index 04c1a5331b..72cc387e57 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@nestjs/common"; -import { DataSource } from "typeorm"; +import { DataSource, EntityManager } from "typeorm"; import { Application, User, @@ -26,6 +26,8 @@ import { CustomNamedError, processInParallel } from "@sims/utilities"; import { DryRunSubmissionResult } from "../../types"; import { FormSubmissionValidator } from "./form-submission-validator"; import { SupplementaryDataLoader } from "./form-supplementary-data"; +import { NotificationActionsService } from "@sims/services/notifications"; +import { NOTIFICATION_FORM_TYPE } from "../form/constants"; /** * Manages how the form submissions are submitted, including the validations, @@ -41,6 +43,7 @@ export class FormSubmissionSubmitService { private readonly formService: FormService, private readonly formSubmissionValidator: FormSubmissionValidator, private readonly supplementaryDataLoader: SupplementaryDataLoader, + private readonly notificationActionsService: NotificationActionsService, ) {} /** @@ -116,11 +119,84 @@ export class FormSubmissionSubmitService { { entityManager: entityManager }, ); } - // TODO: send notification. + // Send a notification to the ministry that a new form or appeal was submitted. + await this.saveFormSubmissionNotification( + studentId, + applicationId, + submissionConfigs, + referenceSubmissionConfig.formCategory, + entityManager, + ); return entityManager.getRepository(FormSubmission).save(formSubmission); }); } + /** + * Sends a notification to the ministry when a new form or appeal has been submitted. + * Loads the required student and application data within the provided + * transaction to ensure data consistency. + * @param studentId ID of the student who submitted the form. + * @param applicationId ID of the application linked to the submission, if applicable. + * @param submissionConfigs form submission configurations used to determine + * the form names included in the notification. + * @param formCategory category of the submitted form, used to derive the notification form type. + * @param entityManager entity manager for the current transaction. + */ + private async saveFormSubmissionNotification( + studentId: number, + applicationId: number | undefined, + submissionConfigs: FormSubmissionConfig[], + formCategory: FormCategory, + entityManager: EntityManager, + ): Promise { + // Load student info required for the notification. + const studentForNotification = await entityManager + .getRepository(Student) + .findOne({ + select: { + id: true, + birthDate: true, + user: { id: true, firstName: true, lastName: true, email: true }, + }, + relations: { user: true }, + where: { id: studentId }, + }); + // Load application number if an application is linked to this submission. + let applicationNumber = "N/A"; + if (applicationId) { + const application = await entityManager + .getRepository(Application) + .findOne({ + select: { id: true, applicationNumber: true }, + where: { id: applicationId }, + }); + applicationNumber = application?.applicationNumber ?? applicationNumber; + } + // Determine the form type category based on the form category and application presence. + let notificationFormType: string = NOTIFICATION_FORM_TYPE.StandardForm; + if (formCategory === FormCategory.StudentAppeal) { + notificationFormType = applicationId + ? NOTIFICATION_FORM_TYPE.ApplicationAppeal + : NOTIFICATION_FORM_TYPE.OtherAppeal; + } + // Collect all form friendly names from the submission configs, comma-separated. + const formName = submissionConfigs + .map((config) => config.formType) + .join(", "); + await this.notificationActionsService.saveMinistryFormSubmittedNotification( + { + givenNames: studentForNotification.user.firstName, + lastName: studentForNotification.user.lastName, + email: studentForNotification.user.email, + birthDate: studentForNotification.birthDate, + formType: notificationFormType, + formName, + applicationNumber, + }, + entityManager, + ); + } + /** * Converts the form submission models to form submission configurations, * making the association of the form submission items with the related form diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission.models.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission.models.ts index 79481426a3..3ef9f255c4 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission.models.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission.models.ts @@ -40,6 +40,7 @@ export type FormSubmissionConfig = FormSubmissionModel & Pick< DynamicFormConfiguration, | "formDefinitionName" + | "formType" | "formCategory" | "hasApplicationScope" | "allowBundledSubmission" diff --git a/sources/packages/backend/apps/api/src/services/form/constants.ts b/sources/packages/backend/apps/api/src/services/form/constants.ts index 5fba2f1717..6afd16e686 100644 --- a/sources/packages/backend/apps/api/src/services/form/constants.ts +++ b/sources/packages/backend/apps/api/src/services/form/constants.ts @@ -49,3 +49,23 @@ export const CHANGE_REQUEST_APPEAL_FORMS = [ FormNames.StudentFinancialInformationAppeal, FormNames.PartnerInformationAndIncomeAppeal, ]; + +/** + * Maps form definition names to their human-readable (friendly) form names + * for use in ministry notifications. + */ +export const APPEAL_FORM_FRIENDLY_NAMES: Record = { + [FormNames.ModifiedIndependentAppeal]: "Modified independent", + [FormNames.RoomAndBoardCostsAppeal]: "Room and board costs", + [FormNames.StepParentWaiverAppeal]: "Step-parent waiver", +}; + +/** + * Notification form type category labels used in ministry form submitted notifications + * to classify the type of form or appeal being submitted. + */ +export const NOTIFICATION_FORM_TYPE = { + ApplicationAppeal: "Application appeal", + OtherAppeal: "Other appeal", + StandardForm: "Standard form", +} as const; diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts index 94c8ce4b01..efb3263dcb 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts @@ -17,6 +17,7 @@ import { import { NotificationActionsService } from "@sims/services/notifications"; import { NoteSharedService } from "@sims/services"; import { StudentAppealActionsProcessor } from "."; +import { allowApplicationChangeRequest } from "../../../utilities"; /** * Service layer for Student appeals. @@ -93,20 +94,53 @@ export class StudentAppealAssessmentService { entityManager, ); // Create student notification when ministry completes student appeal. - const studentUser = appealToUpdate.student.user; - await this.notificationActionsService.saveChangeRequestCompleteNotification( - { - givenNames: studentUser.firstName, - lastName: studentUser.lastName, - toAddress: studentUser.email, - userId: studentUser.id, - }, + await this.saveAssessmentCompletedNotification( + appealToUpdate, auditUserId, entityManager, ); }); } + /** + * Creates a student notification when the ministry completes a student appeal assessment. + * Determines whether to send a change request review completed notification (for legacy + * change requests) or a form completed notification (for new appeals), based on the + * associated application program year. + * @param appeal student appeal that was assessed, including student user and application data. + * @param auditUserId ID of the user performing the operation, used for auditing purposes. + * @param entityManager entity manager for the current transaction. + */ + private async saveAssessmentCompletedNotification( + appeal: StudentAppeal, + auditUserId: number, + entityManager: EntityManager, + ): Promise { + const studentUser = appeal.student.user; + const isLegacyChangeRequest = + appeal.application !== null && + !allowApplicationChangeRequest(appeal.application.programYear); + const studentNotification = { + givenNames: studentUser.firstName, + lastName: studentUser.lastName, + toAddress: studentUser.email, + userId: studentUser.id, + }; + if (isLegacyChangeRequest) { + await this.notificationActionsService.saveStudentChangeRequestReviewCompletedNotification( + studentNotification, + auditUserId, + entityManager, + ); + } else { + await this.notificationActionsService.saveStudentFormCompletedNotification( + studentNotification, + auditUserId, + entityManager, + ); + } + } + /** * Get the student appeal information required to process their approval or decline. * @param appealId appeal ID to be retrieved. @@ -134,6 +168,8 @@ export class StudentAppealAssessmentService { "appealRequest.submittedData", "application.id", "application.applicationStatus", + "programYear.id", + "programYear.programYear", "student.id", "user.id", "user.firstName", @@ -144,6 +180,7 @@ export class StudentAppealAssessmentService { .innerJoin("studentAppeal.student", "student") .innerJoin("student.user", "user") .leftJoin("studentAppeal.application", "application") + .leftJoin("application.programYear", "programYear") .leftJoin("application.currentAssessment", "currentAssessment") .leftJoin("currentAssessment.offering", "offering") .leftJoin("studentAppeal.studentAssessment", "studentAssessment") diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index 158effc23e..f70bd83089 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -20,6 +20,7 @@ import { FileOriginType, ApplicationStatus, Student, + ProgramYear, } from "@sims/sims-db"; import { AppealType, @@ -33,13 +34,19 @@ import { SortPriority, OrderByCondition, StudentAppealPaginationOptions, + allowApplicationChangeRequest, } from "../../utilities"; import { FieldSortOrder } from "@sims/utilities"; import { PROGRAM_YEAR_2025_26_START_DATE } from "./constants"; import { NotificationActionsService, StudentSubmittedChangeRequestNotification, + MinistryFormSubmittedNotification, } from "@sims/services/notifications"; +import { + APPEAL_FORM_FRIENDLY_NAMES, + NOTIFICATION_FORM_TYPE, +} from "../form/constants"; import { StudentFileService } from "../student-file/student-file.service"; import { InjectRepository } from "@nestjs/typeorm"; @@ -115,7 +122,10 @@ export class StudentAppealService extends RecordDataModelService } /** - * Create a notification for the student appeal. + * Creates a ministry notification for the student appeal submission. + * Determines whether to send a change request submitted notification (for legacy change requests) + * or a form submitted notification (for new appeals), based on the associated application + * program year. * @param appealId appeal ID to send the notification. * @param entityManager entity manager to keep DB operations in the same transaction. */ @@ -138,22 +148,68 @@ export class StudentAppealService extends RecordDataModelService }, birthDate: true, }, - application: { id: true, applicationNumber: true }, + application: { + id: true, + applicationNumber: true, + programYear: { id: true, programYear: true }, + }, + appealRequests: { id: true, submittedFormName: true }, + }, + relations: { + student: { user: true }, + application: { programYear: true }, + appealRequests: true, }, - relations: { student: { user: true }, application: true }, where: { id: appealId }, loadEagerRelations: false, }); - const ministryNotification: StudentSubmittedChangeRequestNotification = { + const isLegacyChangeRequest = + studentAppeal.application !== null && + !allowApplicationChangeRequest( + studentAppeal.application.programYear as ProgramYear, + ); + if (isLegacyChangeRequest) { + // Legacy change request: send Ministry - Change Request Submitted notification. + const ministryNotification: StudentSubmittedChangeRequestNotification = { + givenNames: studentAppeal.student.user.firstName, + lastName: studentAppeal.student.user.lastName, + email: studentAppeal.student.user.email, + birthDate: studentAppeal.student.birthDate, + applicationNumber: studentAppeal.application.applicationNumber, + }; + return this.notificationActionsService.saveMinistryChangeRequestSubmittedNotification( + ministryNotification, + entityManager, + ); + } + // New appeal type: send Ministry - Form Submitted notification. + const formTypeCategory = + studentAppeal.application !== null + ? NOTIFICATION_FORM_TYPE.ApplicationAppeal + : NOTIFICATION_FORM_TYPE.OtherAppeal; + // Map technical form names to human-readable friendly names. + const formNames = studentAppeal.appealRequests.map( + (request) => + APPEAL_FORM_FRIENDLY_NAMES[request.submittedFormName] ?? + request.submittedFormName, + ); + // For application appeals, all form names are comma-separated. + // For other appeals, only the first form name is used. + const formName = + formTypeCategory === NOTIFICATION_FORM_TYPE.ApplicationAppeal + ? formNames.join(", ") + : formNames[0]; + const ministryFormNotification: MinistryFormSubmittedNotification = { givenNames: studentAppeal.student.user.firstName, lastName: studentAppeal.student.user.lastName, email: studentAppeal.student.user.email, birthDate: studentAppeal.student.birthDate, - applicationNumber: - studentAppeal.application?.applicationNumber ?? "not applicable", + formType: formTypeCategory, + formName, + applicationNumber: studentAppeal.application?.applicationNumber ?? "N/A", }; - return this.notificationActionsService.saveStudentSubmittedChangeRequestNotification( - ministryNotification, + return this.notificationActionsService.saveMinistryFormSubmittedNotification( + ministryFormNotification, entityManager, ); } diff --git a/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts b/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts new file mode 100644 index 0000000000..12bf4db84e --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { getSQLFileData } from "../utilities/sqlLoader"; + +export class InsertAppealChangeRequestNotificationMessages1773355579843 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Insert-appeal-change-request-notification-messages.sql", + "NotificationMessages", + ), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Rollback-insert-appeal-change-request-notification-messages.sql", + "NotificationMessages", + ), + ); + } +} diff --git a/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql b/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql new file mode 100644 index 0000000000..8f983b88ff --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql @@ -0,0 +1,23 @@ +INSERT INTO + sims.notification_messages(id, description, template_id) +VALUES + ( + 36, + 'Ministry notification for student submits change request.', + 'fad81016-0bed-4d4e-ad48-f70cc943399c' + ), + ( + 37, + 'Student notification for change request review completed.', + '9a4855d1-4f9a-4293-9868-cd853a8e4061' + ), + ( + 38, + 'Ministry notification for student submits form or appeal.', + '296aa2ea-dfa7-4285-9d5b-315b2a4911d6' + ), + ( + 39, + 'Student notification for form or appeal adjudication complete.', + 'fed6b26e-d1f2-4a8c-bfe5-5cb66c00458b' + ); \ No newline at end of file diff --git a/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Rollback-insert-appeal-change-request-notification-messages.sql b/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Rollback-insert-appeal-change-request-notification-messages.sql new file mode 100644 index 0000000000..2173ad62f4 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Rollback-insert-appeal-change-request-notification-messages.sql @@ -0,0 +1,4 @@ +DELETE FROM + sims.notification_messages +WHERE + id IN (36, 37, 38, 39); \ No newline at end of file diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index dd9c6e8436..3c0bc061cd 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -37,6 +37,7 @@ import { ParentInformationRequiredFromStudentNotification, ScholasticStandingReversalNotification, StudentCOERequiredNearEndDateNotification, + MinistryFormSubmittedNotification, } from ".."; import { NotificationService } from "./notification.service"; import { LoggerService } from "@sims/utilities/logger"; @@ -1486,4 +1487,162 @@ export class NotificationActionsService { { entityManager }, ); } + + /** + * Creates a ministry notification when a student submits a change request, + * using the updated change request submitted template. + * @param notification notification details. + * @param entityManager entity manager to execute in transaction. + */ + async saveMinistryChangeRequestSubmittedNotification( + notification: StudentSubmittedChangeRequestNotification, + entityManager: EntityManager, + ): Promise { + const auditUser = this.systemUsersService.systemUser; + const { templateId, emailContacts } = + await this.assertNotificationMessageDetails( + NotificationMessageType.MinistryChangeRequestSubmitted, + ); + if (!emailContacts?.length) { + return; + } + const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ + userId: auditUser.id, + messageType: NotificationMessageType.MinistryChangeRequestSubmitted, + messagePayload: { + email_address: emailContact, + template_id: templateId, + personalisation: { + givenNames: notification.givenNames ?? "", + lastName: notification.lastName, + birthDate: getDateOnlyFormat(notification.birthDate), + studentEmail: notification.email, + applicationNumber: notification.applicationNumber, + dateTime: this.getDateTimeOnPSTTimeZone(), + }, + }, + })); + // Save notifications to be sent to the ministry into the notification table. + await this.notificationService.saveNotifications( + ministryNotificationsToSend, + auditUser.id, + { entityManager }, + ); + } + + /** + * Creates a student notification when a change request review is completed by the ministry, + * using the updated change request review completed template. + * @param notification notification details. + * @param auditUserId user who completes the change request review. + * @param entityManager entity manager to execute in transaction. + */ + async saveStudentChangeRequestReviewCompletedNotification( + notification: StudentNotification, + auditUserId: number, + entityManager: EntityManager, + ): Promise { + const { templateId } = + await this.notificationMessageService.getNotificationMessageDetails( + NotificationMessageType.StudentChangeRequestReviewCompleted, + ); + const changeRequestReviewCompletedNotification = { + userId: notification.userId, + messageType: NotificationMessageType.StudentChangeRequestReviewCompleted, + messagePayload: { + email_address: notification.toAddress, + template_id: templateId, + personalisation: { + givenNames: notification.givenNames ?? "", + lastName: notification.lastName, + date: this.getDateTimeOnPSTTimeZone(), + }, + }, + }; + await this.notificationService.saveNotifications( + [changeRequestReviewCompletedNotification], + auditUserId, + { entityManager }, + ); + } + + /** + * Creates a ministry notification when a student submits a form or appeal, + * including form type categorization (application appeal, other appeal, or standard form), + * a human-readable form name, and the related application number. + * @param notification notification details. + * @param entityManager entity manager to execute in transaction. + */ + async saveMinistryFormSubmittedNotification( + notification: MinistryFormSubmittedNotification, + entityManager: EntityManager, + ): Promise { + const auditUser = this.systemUsersService.systemUser; + const { templateId, emailContacts } = + await this.assertNotificationMessageDetails( + NotificationMessageType.MinistryFormSubmitted, + ); + if (!emailContacts?.length) { + return; + } + const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ + userId: auditUser.id, + messageType: NotificationMessageType.MinistryFormSubmitted, + messagePayload: { + email_address: emailContact, + template_id: templateId, + personalisation: { + givenNames: notification.givenNames ?? "", + lastName: notification.lastName, + birthDate: getDateOnlyFormat(notification.birthDate), + studentEmail: notification.email, + formType: notification.formType, + formName: notification.formName, + applicationNumber: notification.applicationNumber, + dateTime: this.getDateTimeOnPSTTimeZone(), + }, + }, + })); + // Save notifications to be sent to the ministry into the notification table. + await this.notificationService.saveNotifications( + ministryNotificationsToSend, + auditUser.id, + { entityManager }, + ); + } + + /** + * Creates a student notification when a form or appeal is completed. + * @param notification notification details. + * @param auditUserId user who completed the form or appeal. + * @param entityManager entity manager to execute in transaction. + */ + async saveStudentFormCompletedNotification( + notification: StudentNotification, + auditUserId: number, + entityManager: EntityManager, + ): Promise { + const { templateId } = + await this.notificationMessageService.getNotificationMessageDetails( + NotificationMessageType.StudentFormCompleted, + ); + const formCompletedNotification = { + userId: notification.userId, + messageType: NotificationMessageType.StudentFormCompleted, + messagePayload: { + email_address: notification.toAddress, + template_id: templateId, + personalisation: { + givenNames: notification.givenNames ?? "", + lastName: notification.lastName, + date: this.getDateTimeOnPSTTimeZone(), + }, + }, + }; + await this.notificationService.saveNotifications( + [formCompletedNotification], + auditUserId, + { entityManager }, + ); + } } diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts index c87df72950..0e8272c670 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts @@ -245,3 +245,18 @@ export interface StudentCOERequiredNearEndDateNotification { email: string; applicationNumber: string; } + +/** + * Ministry notification data when a student submits a form or appeal, + * with form type categorization (application appeal, other appeal, standard form), + * a human-readable form name, and the related application number. + */ +export interface MinistryFormSubmittedNotification { + givenNames: string; + lastName: string; + email: string; + birthDate: string; + formType: string; + formName: string; + applicationNumber: string; +} diff --git a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts index 33164d921d..7bdd954129 100644 --- a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts +++ b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts @@ -237,4 +237,20 @@ export enum NotificationMessageType { * Student COE required near study end date. */ StudentCOERequiredNearEndDateNotification = 35, + /** + * Ministry notification for student submits change request. + */ + MinistryChangeRequestSubmitted = 36, + /** + * Student notification for change request review completed. + */ + StudentChangeRequestReviewCompleted = 37, + /** + * Ministry notification for student submits form or appeal. + */ + MinistryFormSubmitted = 38, + /** + * Student notification for form or appeal adjudication complete. + */ + StudentFormCompleted = 39, } From 2d18ee24ed90ecf80d82a35fa81a1bfdcee5d32e Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sat, 14 Mar 2026 14:53:58 -0700 Subject: [PATCH 02/16] Send change request reviewed student notification --- .../application-change-request.service.ts | 52 ++++++++++++++- .../student-appeal/student-appeal.service.ts | 66 +++++++++++++------ 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts b/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts index 3915d7b6c3..93442f7443 100644 --- a/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts +++ b/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts @@ -7,11 +7,19 @@ import { FormSubmission, getUserFullNameLikeSearch, NoteType, + Student, StudentAppeal, User, } from "@sims/sims-db"; -import { Brackets, DataSource, Repository, SelectQueryBuilder } from "typeorm"; +import { + Brackets, + DataSource, + EntityManager, + Repository, + SelectQueryBuilder, +} from "typeorm"; import { NoteSharedService, WorkflowClientService } from "@sims/services"; +import { NotificationActionsService } from "@sims/services/notifications"; import { ApplicationService } from "../application/application.service"; import { APPLICATION_NOT_FOUND, @@ -31,6 +39,7 @@ export class ApplicationChangeRequestService { private readonly noteSharedService: NoteSharedService, private readonly workflowClientService: WorkflowClientService, private readonly applicationService: ApplicationService, + private readonly notificationActionsService: NotificationActionsService, @InjectRepository(Application) private readonly applicationRepo: Repository, ) {} @@ -99,6 +108,11 @@ export class ApplicationChangeRequestService { changeRequestApplication.applicationEditStatusUpdatedBy = auditUser; changeRequestApplication.applicationEditStatusUpdatedOn = currentDate; await applicationRepo.save(changeRequestApplication); + await this.saveChangeRequestReviewCompletedNotification( + changeRequestApplication.student.id, + auditUserId, + transactionalEntityManager, + ); return; } // Previously completed application that will be replaced by the newly approved application change request. @@ -137,6 +151,11 @@ export class ApplicationChangeRequestService { } as FormSubmission; } await applicationRepo.save(changeRequestApplication); + await this.saveChangeRequestReviewCompletedNotification( + changeRequestApplication.student.id, + auditUserId, + transactionalEntityManager, + ); }); // Send a message to the workflow to proceed. await this.workflowClientService.sendApplicationChangeRequestStatusMessage( @@ -145,6 +164,37 @@ export class ApplicationChangeRequestService { ); } + /** + * Creates a student notification when a change request review is completed by the ministry. + * @param studentId student ID to load the notification data. + * @param auditUserId user who completed the change request review. + * @param entityManager entity manager for the current transaction. + */ + private async saveChangeRequestReviewCompletedNotification( + studentId: number, + auditUserId: number, + entityManager: EntityManager, + ): Promise { + const student = await entityManager.getRepository(Student).findOne({ + select: { + id: true, + user: { id: true, firstName: true, lastName: true, email: true }, + }, + relations: { user: true }, + where: { id: studentId }, + }); + await this.notificationActionsService.saveStudentChangeRequestReviewCompletedNotification( + { + givenNames: student.user.firstName, + lastName: student.user.lastName, + toAddress: student.user.email, + userId: student.user.id, + }, + auditUserId, + entityManager, + ); + } + /** * Gets applications based purely on their edit status. * @param applicationEditStatus The application edit status to filter. diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index f70bd83089..87703bdf4c 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -20,7 +20,6 @@ import { FileOriginType, ApplicationStatus, Student, - ProgramYear, } from "@sims/sims-db"; import { AppealType, @@ -163,30 +162,57 @@ export class StudentAppealService extends RecordDataModelService where: { id: appealId }, loadEagerRelations: false, }); + + // Check if the submission is for new appeal process(appeal process is for submissions from 2025-26 program year). const isLegacyChangeRequest = studentAppeal.application !== null && - !allowApplicationChangeRequest( - studentAppeal.application.programYear as ProgramYear, - ); + !allowApplicationChangeRequest(studentAppeal.application.programYear); if (isLegacyChangeRequest) { - // Legacy change request: send Ministry - Change Request Submitted notification. - const ministryNotification: StudentSubmittedChangeRequestNotification = { - givenNames: studentAppeal.student.user.firstName, - lastName: studentAppeal.student.user.lastName, - email: studentAppeal.student.user.email, - birthDate: studentAppeal.student.birthDate, - applicationNumber: studentAppeal.application.applicationNumber, - }; - return this.notificationActionsService.saveMinistryChangeRequestSubmittedNotification( - ministryNotification, + // For legacy change requests, send a change request submitted notification. + return this.saveMinistryChangeRequestNotification( + studentAppeal, entityManager, ); } - // New appeal type: send Ministry - Form Submitted notification. - const formTypeCategory = - studentAppeal.application !== null - ? NOTIFICATION_FORM_TYPE.ApplicationAppeal - : NOTIFICATION_FORM_TYPE.OtherAppeal; + // Not a legacy change request, so save as a new appeal submission. + return this.saveMinistryAppealNotification(studentAppeal, entityManager); + } + + /** + * Sends a ministry notification for a legacy change request submission. + * @param studentAppeal student appeal with student, user, and application data. + * @param entityManager entity manager for the current transaction. + */ + private async saveMinistryChangeRequestNotification( + studentAppeal: StudentAppeal, + entityManager: EntityManager, + ): Promise { + const ministryNotification: StudentSubmittedChangeRequestNotification = { + givenNames: studentAppeal.student.user.firstName, + lastName: studentAppeal.student.user.lastName, + email: studentAppeal.student.user.email, + birthDate: studentAppeal.student.birthDate, + applicationNumber: studentAppeal.application.applicationNumber, + }; + await this.notificationActionsService.saveMinistryChangeRequestSubmittedNotification( + ministryNotification, + entityManager, + ); + } + + /** + * Sends a ministry notification for a new appeal (application appeal or other appeal) submission. + * Classifies the appeal type and maps technical form names to human-readable friendly names. + * @param studentAppeal student appeal with student, user, application, and appeal request data. + * @param entityManager entity manager for the current transaction. + */ + private async saveMinistryAppealNotification( + studentAppeal: StudentAppeal, + entityManager: EntityManager, + ): Promise { + const formTypeCategory = studentAppeal.application + ? NOTIFICATION_FORM_TYPE.ApplicationAppeal + : NOTIFICATION_FORM_TYPE.OtherAppeal; // Map technical form names to human-readable friendly names. const formNames = studentAppeal.appealRequests.map( (request) => @@ -208,7 +234,7 @@ export class StudentAppealService extends RecordDataModelService formName, applicationNumber: studentAppeal.application?.applicationNumber ?? "N/A", }; - return this.notificationActionsService.saveMinistryFormSubmittedNotification( + await this.notificationActionsService.saveMinistryFormSubmittedNotification( ministryFormNotification, entityManager, ); From 8b0d819088f4120fcb566a6f7c9b5dc1ba54f38d Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sat, 14 Mar 2026 15:36:41 -0700 Subject: [PATCH 03/16] Add change request ministry notification --- .../application/application.service.ts | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/sources/packages/backend/apps/api/src/services/application/application.service.ts b/sources/packages/backend/apps/api/src/services/application/application.service.ts index 0a706cb999..3a2b8ea551 100644 --- a/sources/packages/backend/apps/api/src/services/application/application.service.ts +++ b/sources/packages/backend/apps/api/src/services/application/application.service.ts @@ -34,7 +34,7 @@ import { } from "@sims/sims-db"; import { StudentFileService } from "../student-file/student-file.service"; import { - ApplicationScholasticStandingStatus as ApplicationScholasticStandingStatus, + ApplicationScholasticStandingStatus, ApplicationSubmissionResult, } from "./application.models"; import { @@ -77,6 +77,7 @@ import { ApplicationEditedTooManyTimesNotification, NotificationActionsService, NotificationService, + StudentSubmittedChangeRequestNotification, } from "@sims/services/notifications"; import { InstitutionLocationService } from "../institution-location/institution-location.service"; import { StudentService } from ".."; @@ -521,6 +522,12 @@ export class ApplicationService extends RecordDataModelService { newApplication.studentAssessments = [originalAssessment]; newApplication.currentAssessment = originalAssessment; await applicationRepository.save(newApplication); + // Notify the ministry that a new change request was submitted. + await this.saveMinistryChangeRequestSubmittedNotification( + studentId, + application.applicationNumber, + transactionalEntityManager, + ); }); return { application: newApplication, @@ -586,6 +593,41 @@ export class ApplicationService extends RecordDataModelService { }); } + /** + * Sends a ministry notification when a student submits a new program year change request. + * Loads the required student and application number within the provided + * transaction to ensure data consistency. + * @param studentId ID of the student who submitted the change request. + * @param applicationNumber application number of the application being changed. + * @param entityManager entity manager for the current transaction. + */ + private async saveMinistryChangeRequestSubmittedNotification( + studentId: number, + applicationNumber: string, + entityManager: EntityManager, + ): Promise { + 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: studentId }, + }); + const ministryNotification: StudentSubmittedChangeRequestNotification = { + givenNames: student.user.firstName, + lastName: student.user.lastName, + email: student.user.email, + birthDate: student.birthDate, + applicationNumber, + }; + await this.notificationActionsService.saveMinistryChangeRequestSubmittedNotification( + ministryNotification, + entityManager, + ); + } + /** * Saves a notification when the application is edited too many times * governed by {@link APPLICATION_EDIT_COUNT_TO_SEND_NOTIFICATION}. @@ -642,7 +684,7 @@ export class ApplicationService extends RecordDataModelService { const sequenceNumberSize = MAX_APPLICATION_NUMBER_LENGTH - sequenceName.length; - let nextApplicationSequence = NaN; + let nextApplicationSequence = Number.NaN; await this.sequenceService.consumeNextSequence( sequenceName, async (nextSequenceNumber: number) => { From 9e9c41a34f39a50fac702a716e97b1dbcce39762 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sat, 14 Mar 2026 16:59:17 -0700 Subject: [PATCH 04/16] Load form friendly names --- .../dynamic-form-configuration.service.ts | 14 +++++++++++++ .../apps/api/src/services/form/constants.ts | 10 --------- .../student-appeal/student-appeal.service.ts | 21 ++++++++++--------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts b/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts index 7a384c6dcb..ce72dd0c0a 100644 --- a/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts +++ b/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts @@ -110,4 +110,18 @@ export class DynamicFormConfigurationService { (dynamicFormConfiguration) => dynamicFormConfiguration.id === formId, ); } + + /** + * Get a form configuration by its form definition name. + * @param formDefinitionName form definition name. + * @returns dynamic form configuration for the requested form definition name. + */ + getFormByDefinitionName( + formDefinitionName: string, + ): DynamicFormConfiguration | undefined { + return this.dynamicFormConfigurations.find( + (dynamicFormConfiguration) => + dynamicFormConfiguration.formDefinitionName === formDefinitionName, + ); + } } diff --git a/sources/packages/backend/apps/api/src/services/form/constants.ts b/sources/packages/backend/apps/api/src/services/form/constants.ts index 6afd16e686..dbd3819f72 100644 --- a/sources/packages/backend/apps/api/src/services/form/constants.ts +++ b/sources/packages/backend/apps/api/src/services/form/constants.ts @@ -50,16 +50,6 @@ export const CHANGE_REQUEST_APPEAL_FORMS = [ FormNames.PartnerInformationAndIncomeAppeal, ]; -/** - * Maps form definition names to their human-readable (friendly) form names - * for use in ministry notifications. - */ -export const APPEAL_FORM_FRIENDLY_NAMES: Record = { - [FormNames.ModifiedIndependentAppeal]: "Modified independent", - [FormNames.RoomAndBoardCostsAppeal]: "Room and board costs", - [FormNames.StepParentWaiverAppeal]: "Step-parent waiver", -}; - /** * Notification form type category labels used in ministry form submitted notifications * to classify the type of form or appeal being submitted. diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index 87703bdf4c..82f06edef1 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -42,11 +42,9 @@ import { StudentSubmittedChangeRequestNotification, MinistryFormSubmittedNotification, } from "@sims/services/notifications"; -import { - APPEAL_FORM_FRIENDLY_NAMES, - NOTIFICATION_FORM_TYPE, -} from "../form/constants"; +import { NOTIFICATION_FORM_TYPE } from "../form/constants"; import { StudentFileService } from "../student-file/student-file.service"; +import { DynamicFormConfigurationService } from "../dynamic-form-configuration/dynamic-form-configuration.service"; import { InjectRepository } from "@nestjs/typeorm"; /** @@ -59,6 +57,7 @@ export class StudentAppealService extends RecordDataModelService private readonly studentAppealRequestsService: StudentAppealRequestsService, private readonly notificationActionsService: NotificationActionsService, private readonly studentFileService: StudentFileService, + private readonly dynamicFormConfigurationService: DynamicFormConfigurationService, @InjectRepository(Application) private readonly applicationRepo: Repository, ) { @@ -213,12 +212,14 @@ export class StudentAppealService extends RecordDataModelService const formTypeCategory = studentAppeal.application ? NOTIFICATION_FORM_TYPE.ApplicationAppeal : NOTIFICATION_FORM_TYPE.OtherAppeal; - // Map technical form names to human-readable friendly names. - const formNames = studentAppeal.appealRequests.map( - (request) => - APPEAL_FORM_FRIENDLY_NAMES[request.submittedFormName] ?? - request.submittedFormName, - ); + // Map technical form names to human-readable friendly names using the dynamic form configuration. + const formNames = studentAppeal.appealRequests.map((request) => { + const config = + this.dynamicFormConfigurationService.getFormByDefinitionName( + request.submittedFormName, + ); + return config?.formType ?? request.submittedFormName; + }); // For application appeals, all form names are comma-separated. // For other appeals, only the first form name is used. const formName = From f5e7f4a0968a6ba412b93516ce4e4a37b09840ce Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sat, 14 Mar 2026 23:26:22 -0700 Subject: [PATCH 05/16] Update notification form category --- .../form-submission-submit.service.ts | 11 ++-------- .../student-appeal/student-appeal.service.ts | 15 +++----------- .../notification-actions.service.ts | 20 ++++++++++++++++--- .../notification/notification.model.ts | 4 ++-- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts index 72cc387e57..d340a467da 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -27,7 +27,6 @@ import { DryRunSubmissionResult } from "../../types"; import { FormSubmissionValidator } from "./form-submission-validator"; import { SupplementaryDataLoader } from "./form-supplementary-data"; import { NotificationActionsService } from "@sims/services/notifications"; -import { NOTIFICATION_FORM_TYPE } from "../form/constants"; /** * Manages how the form submissions are submitted, including the validations, @@ -172,13 +171,7 @@ export class FormSubmissionSubmitService { }); applicationNumber = application?.applicationNumber ?? applicationNumber; } - // Determine the form type category based on the form category and application presence. - let notificationFormType: string = NOTIFICATION_FORM_TYPE.StandardForm; - if (formCategory === FormCategory.StudentAppeal) { - notificationFormType = applicationId - ? NOTIFICATION_FORM_TYPE.ApplicationAppeal - : NOTIFICATION_FORM_TYPE.OtherAppeal; - } + // Collect all form friendly names from the submission configs, comma-separated. const formName = submissionConfigs .map((config) => config.formType) @@ -189,7 +182,7 @@ export class FormSubmissionSubmitService { lastName: studentForNotification.user.lastName, email: studentForNotification.user.email, birthDate: studentForNotification.birthDate, - formType: notificationFormType, + formCategory: formCategory, formName, applicationNumber, }, diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index 82f06edef1..7bd62b614d 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -20,6 +20,7 @@ import { FileOriginType, ApplicationStatus, Student, + FormCategory, } from "@sims/sims-db"; import { AppealType, @@ -42,7 +43,6 @@ import { StudentSubmittedChangeRequestNotification, MinistryFormSubmittedNotification, } from "@sims/services/notifications"; -import { NOTIFICATION_FORM_TYPE } from "../form/constants"; import { StudentFileService } from "../student-file/student-file.service"; import { DynamicFormConfigurationService } from "../dynamic-form-configuration/dynamic-form-configuration.service"; import { InjectRepository } from "@nestjs/typeorm"; @@ -209,9 +209,6 @@ export class StudentAppealService extends RecordDataModelService studentAppeal: StudentAppeal, entityManager: EntityManager, ): Promise { - const formTypeCategory = studentAppeal.application - ? NOTIFICATION_FORM_TYPE.ApplicationAppeal - : NOTIFICATION_FORM_TYPE.OtherAppeal; // Map technical form names to human-readable friendly names using the dynamic form configuration. const formNames = studentAppeal.appealRequests.map((request) => { const config = @@ -220,19 +217,13 @@ export class StudentAppealService extends RecordDataModelService ); return config?.formType ?? request.submittedFormName; }); - // For application appeals, all form names are comma-separated. - // For other appeals, only the first form name is used. - const formName = - formTypeCategory === NOTIFICATION_FORM_TYPE.ApplicationAppeal - ? formNames.join(", ") - : formNames[0]; const ministryFormNotification: MinistryFormSubmittedNotification = { givenNames: studentAppeal.student.user.firstName, lastName: studentAppeal.student.user.lastName, email: studentAppeal.student.user.email, birthDate: studentAppeal.student.birthDate, - formType: formTypeCategory, - formName, + formCategory: FormCategory.StudentAppeal, + formName: formNames.join(", "), applicationNumber: studentAppeal.application?.applicationNumber ?? "N/A", }; await this.notificationActionsService.saveMinistryFormSubmittedNotification( diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index 3c0bc061cd..b5640c9952 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1,5 +1,9 @@ import { Injectable } from "@nestjs/common"; -import { NotificationMessage, NotificationMessageType } from "@sims/sims-db"; +import { + FormCategory, + NotificationMessage, + NotificationMessageType, +} from "@sims/sims-db"; import { base64Encode, getDateOnlyFormat, @@ -44,6 +48,7 @@ import { LoggerService } from "@sims/utilities/logger"; import { ECE_RESPONSE_ATTACHMENT_FILE_NAME } from "@sims/integrations/constants"; import { SystemUsersService } from "@sims/services/system-users"; import { NotificationMetadata } from "@sims/sims-db/entities/notification-metadata.type"; +import { NOTIFICATION_FORM_TYPE } from "apps/api/src/services/form/constants"; @Injectable() export class NotificationActionsService { @@ -1585,6 +1590,15 @@ export class NotificationActionsService { if (!emailContacts?.length) { return; } + + let formCategory: string = NOTIFICATION_FORM_TYPE.StandardForm; + if (notification.formCategory == FormCategory.StudentAppeal) { + if (notification.applicationNumber) { + formCategory = NOTIFICATION_FORM_TYPE.ApplicationAppeal; + } else { + formCategory = NOTIFICATION_FORM_TYPE.OtherAppeal; + } + } const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ userId: auditUser.id, messageType: NotificationMessageType.MinistryFormSubmitted, @@ -1596,9 +1610,9 @@ export class NotificationActionsService { lastName: notification.lastName, birthDate: getDateOnlyFormat(notification.birthDate), studentEmail: notification.email, - formType: notification.formType, + formCategory: formCategory, formName: notification.formName, - applicationNumber: notification.applicationNumber, + applicationNumber: notification.applicationNumber ?? "N/A", dateTime: this.getDateTimeOnPSTTimeZone(), }, }, diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts index 0e8272c670..84fe719603 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts @@ -1,4 +1,4 @@ -import { NotificationMessageType } from "@sims/sims-db"; +import { FormCategory, NotificationMessageType } from "@sims/sims-db"; import { NotificationEmailMessage } from "./gc-notify.model"; import { NotificationMetadata } from "@sims/sims-db/entities/notification-metadata.type"; @@ -256,7 +256,7 @@ export interface MinistryFormSubmittedNotification { lastName: string; email: string; birthDate: string; - formType: string; + formCategory: FormCategory; formName: string; applicationNumber: string; } From 2d97c8d0070414141ef37fcccc0f28c80bfed3dd Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sun, 15 Mar 2026 15:36:02 -0700 Subject: [PATCH 06/16] Added notifications e2e tests --- ...assessApplicationChangeRequest.e2e-spec.ts | 81 ++++- ...r.approveStudentAppealRequests.e2e-spec.ts | 59 ++++ ...roller.submitApplicationAppeal.e2e-spec.ts | 305 +++++++++++------- ...controller.submitStudentAppeal.e2e-spec.ts | 16 +- .../form-submission-submit.service.ts | 4 +- .../apps/api/src/services/form/constants.ts | 10 - .../student-appeal/student-appeal.service.ts | 2 +- .../libs/services/src/constants/index.ts | 1 + .../src/constants/notification.constants.ts | 20 ++ .../notification-actions.service.ts | 2 +- .../notification/notification.model.ts | 2 +- .../src/entities/notification.model.ts | 8 +- 12 files changed, 371 insertions(+), 139 deletions(-) create mode 100644 sources/packages/backend/libs/services/src/constants/notification.constants.ts diff --git a/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts index 75459de953..f44a28eaba 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts @@ -1,6 +1,6 @@ import { HttpStatus, INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { DataSource } from "typeorm"; +import { DataSource, IsNull } from "typeorm"; import { AESTGroups, BEARER_AUTH_TYPE, @@ -8,7 +8,12 @@ import { getAESTToken, getAESTUser, } from "../../../../testHelpers"; -import { ApplicationEditStatus, ApplicationStatus, User } from "@sims/sims-db"; +import { + ApplicationEditStatus, + ApplicationStatus, + NotificationMessageType, + User, +} from "@sims/sims-db"; import { faker } from "@faker-js/faker"; import { createE2EDataSources, @@ -18,7 +23,11 @@ import { } from "@sims/test-utils"; import { ZeebeGrpcClient } from "@camunda8/sdk/dist/zeebe"; import MockDate from "mockdate"; -import { INVALID_APPLICATION_EDIT_STATUS } from "@sims/services/constants"; +import { + INVALID_APPLICATION_EDIT_STATUS, + GC_NOTIFY_TEMPLATE_IDS, +} from "@sims/services/constants"; +import { getPSTPDTDateTime } from "@sims/utilities"; describe("ApplicationChangeRequestAESTController(e2e)-assessApplicationChangeRequest", () => { let app: INestApplication; @@ -42,6 +51,15 @@ describe("ApplicationChangeRequestAESTController(e2e)-assessApplicationChangeReq beforeEach(async () => { MockDate.reset(); + // Mark all existing change request review completed notifications as sent to isolate test assertions. + await db.notification.update( + { + notificationMessage: { + id: NotificationMessageType.StudentChangeRequestReviewCompleted, + }, + }, + { dateSent: new Date() }, + ); }); it("Should approve a change request and copy the offering and appeal when the application change request is waiting for approval.", async () => { @@ -218,6 +236,25 @@ describe("ApplicationChangeRequestAESTController(e2e)-assessApplicationChangeReq creator: ministryUser, }, ]); + // Validate notification. + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.StudentChangeRequestReviewCompleted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentChangeRequestReviewCompleted, + email_address: changeRequest.student.user.email, + personalisation: { + givenNames: changeRequest.student.user.firstName ?? "", + lastName: changeRequest.student.user.lastName, + date: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); }); it("Should approve a change request and copy the offering and no appeals when the application change request is waiting for approval, and no appeals are present.", async () => { @@ -320,6 +357,25 @@ describe("ApplicationChangeRequestAESTController(e2e)-assessApplicationChangeReq }, }, }); + // Validate notification. + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.StudentChangeRequestReviewCompleted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentChangeRequestReviewCompleted, + email_address: changeRequest.student.user.email, + personalisation: { + givenNames: changeRequest.student.user.firstName ?? "", + lastName: changeRequest.student.user.lastName, + date: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); }); it("Should be able to decline a change request and create a student note when the application change request is waiting for approval.", async () => { @@ -402,6 +458,25 @@ describe("ApplicationChangeRequestAESTController(e2e)-assessApplicationChangeReq creator: ministryUser, }, ]); + // Validate notification. + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.StudentChangeRequestReviewCompleted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentChangeRequestReviewCompleted, + email_address: changeRequest.student.user.email, + personalisation: { + givenNames: changeRequest.student.user.firstName ?? "", + lastName: changeRequest.student.user.lastName, + date: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); }); it("Should throw a BadRequestException when the application change request approval has an invalid status.", async () => { diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts index ac8c5e1d85..d914289d0d 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts @@ -1,3 +1,4 @@ +import { IsNull } from "typeorm"; import { HttpStatus, INestApplication } from "@nestjs/common"; import * as request from "supertest"; import { @@ -19,12 +20,15 @@ import { ApplicationStatus, AssessmentTriggerType, ModifiedIndependentStatus, + NotificationMessageType, NoteType, StudentAppealActionType, StudentAppealStatus, } from "@sims/sims-db"; import { StudentAppealApprovalAPIInDTO } from "../../../../route-controllers"; import MockDate from "mockdate"; +import { getPSTPDTDateTime } from "@sims/utilities"; +import { GC_NOTIFY_TEMPLATE_IDS } from "@sims/services/constants"; describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => { let app: INestApplication; @@ -39,6 +43,23 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => beforeEach(async () => { MockDate.reset(); + // Mark all existing student appeal notifications as sent to isolate test assertions. + await db.notification.update( + { + notificationMessage: { + id: NotificationMessageType.StudentChangeRequestReviewCompleted, + }, + }, + { dateSent: new Date() }, + ); + await db.notification.update( + { + notificationMessage: { + id: NotificationMessageType.StudentFormCompleted, + }, + }, + { dateSent: new Date() }, + ); }); it("Should approve student appeal requests and add note when the appeal with appeal requests submitted for approval are in pending status.", async () => { @@ -121,6 +142,25 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => }, ], }); + // Validate notification. + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.StudentChangeRequestReviewCompleted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentChangeRequestReviewCompleted, + email_address: application.student.user.email, + personalisation: { + givenNames: application.student.user.firstName ?? "", + lastName: application.student.user.lastName, + date: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); }); it("Should throw an unprocessable entity error when the application associated with the appeal is not in completed status.", async () => { @@ -291,6 +331,25 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => }, ], }); + // Validate notification. + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.StudentFormCompleted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentFormCompleted, + email_address: student.user.email, + personalisation: { + givenNames: student.user.firstName ?? "", + lastName: student.user.lastName, + date: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); }); } }); diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts index 7484908126..450c542ba9 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts @@ -1,6 +1,6 @@ import { HttpStatus, INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { DataSource, Repository } from "typeorm"; +import { DataSource, IsNull, Repository } from "typeorm"; import { BEARER_AUTH_TYPE, createTestingAppModule, @@ -27,6 +27,7 @@ import { Application, ApplicationStatus, FileOriginType, + NotificationMessageType, OfferingIntensity, ProgramYear, StudentAppealRequest, @@ -37,11 +38,17 @@ import { import { StudentApplicationAppealAPIInDTO } from "../../models/student-appeal.dto"; import { AppStudentsModule } from "../../../../app.students.module"; import { FormNames, FormService } from "../../../../services"; +import MockDate from "mockdate"; +import { getDateOnlyFormat, getPSTPDTDateTime } from "@sims/utilities"; import { APPLICATION_CHANGE_NOT_ELIGIBLE, APPLICATION_HAS_PENDING_APPEAL, APPLICATION_IS_NOT_ELIGIBLE_FOR_AN_APPEAL, } from "../../../../constants"; +import { + GC_NOTIFY_TEMPLATE_IDS, + NOTIFICATION_FORM_TYPE, +} from "@sims/services/constants"; describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { let app: INestApplication; @@ -55,6 +62,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { const DEPENDANT_INFORMATION_FORM_NAME = "studentdependantsappeal"; const PARTNER_INFORMATION_FORM_NAME = "partnerinformationandincomeappeal"; const ROOM_AND_BOARD_COSTS_FORM_NAME = "roomandboardcostsappeal"; + const MINISTRY_EMAIL_ADDRESS = "dummy@some.domain"; let recentActiveProgramYear: ProgramYear; beforeAll(async () => { @@ -68,10 +76,37 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { studentAppealRequestRepo = dataSource.getRepository(StudentAppealRequest); studentFileRepo = dataSource.getRepository(StudentFile); recentActiveProgramYear = await getRecentActiveProgramYear(db); + // Update fake email contacts to send ministry notifications. + await db.notificationMessage.update( + { id: NotificationMessageType.MinistryChangeRequestSubmitted }, + { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, + ); + await db.notificationMessage.update( + { id: NotificationMessageType.MinistryFormSubmitted }, + { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, + ); }); beforeEach(async () => { + MockDate.reset(); await resetMockJWTUserInfo(appModule); + // Mark all existing ministry notifications as sent to isolate test assertions. + await db.notification.update( + { + notificationMessage: { + id: NotificationMessageType.MinistryChangeRequestSubmitted, + }, + }, + { dateSent: new Date() }, + ); + await db.notification.update( + { + notificationMessage: { + id: NotificationMessageType.MinistryFormSubmitted, + }, + }, + { dateSent: new Date() }, + ); }); it( @@ -128,6 +163,8 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { const endpoint = `/students/appeal/application/${application.id}`; await mockJWTUserInfo(appModule, student.user); + const now = new Date(); + MockDate.set(now); // Act/Assert let createdAppealId: number; @@ -168,6 +205,28 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { programYear: application.programYear.programYear, }, ); + // Validate notification for legacy change request (pre-2025-26 program year). + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.MinistryChangeRequestSubmitted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryChangeRequestSubmitted, + email_address: MINISTRY_EMAIL_ADDRESS, + personalisation: { + givenNames: student.user.firstName, + lastName: student.user.lastName, + birthDate: getDateOnlyFormat(student.birthDate), + studentEmail: student.user.email, + applicationNumber: application.applicationNumber, + dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); }, ); @@ -617,125 +676,147 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { ); }); - it( - "Should create room and board costs appeal " + - "when student submit an appeal for a program year which is eligible for appeal process.", - async () => { - // Arrange - // Create student to submit application. - const student = await saveFakeStudent(appDataSource); - // Create application submit appeal with eligible program year. - const application = await saveFakeApplicationDisbursements( - db.dataSource, - { - student, - programYear: recentActiveProgramYear, + it("Should create room and board costs appeal when student submit an appeal for a program year which is eligible for appeal process.", async () => { + // Arrange + // Create student to submit application. + const student = await saveFakeStudent(appDataSource); + // Create application submit appeal with eligible program year. + const application = await saveFakeApplicationDisbursements( + db.dataSource, + { + student, + programYear: recentActiveProgramYear, + }, + { + offeringIntensity: OfferingIntensity.fullTime, + applicationStatus: ApplicationStatus.Completed, + currentAssessmentInitialValues: { + eligibleApplicationAppeals: [FormNames.RoomAndBoardCostsAppeal], }, + }, + ); + // Create a temporary file for room and board costs appeal. + const roomAndBoardFile = await saveFakeStudentFileUpload( + appDataSource, + { + student, + creator: student.user, + }, + { fileOrigin: FileOriginType.Temporary }, + ); + // Prepare the data to request a change of financial information. + const roomAndBoardAppealData = { + roomAndBoardAmount: 561, + roomAndBoardSituations: { + parentUnEmployed: false, + parentEarnLowIncome: false, + parentReceiveIncomeAssistance: false, + livingAtHomePayingRoomAndBoard: true, + parentReceiveCanadaPensionOrOldAgeSupplement: false, + }, + roomAndBoardSupportingDocuments: [ { - offeringIntensity: OfferingIntensity.fullTime, - applicationStatus: ApplicationStatus.Completed, - currentAssessmentInitialValues: { - eligibleApplicationAppeals: [FormNames.RoomAndBoardCostsAppeal], - }, + url: `student/files/${roomAndBoardFile.uniqueFileName}`, + hash: "", + name: roomAndBoardFile.uniqueFileName, + size: 4, + type: "text/plain", + storage: "url", + originalName: roomAndBoardFile.fileName, }, - ); - // Create a temporary file for room and board costs appeal. - const roomAndBoardFile = await saveFakeStudentFileUpload( - appDataSource, + ], + }; + const payload: StudentApplicationAppealAPIInDTO = { + studentAppealRequests: [ { - student, - creator: student.user, - }, - { fileOrigin: FileOriginType.Temporary }, - ); - // Prepare the data to request a change of financial information. - const roomAndBoardAppealData = { - roomAndBoardAmount: 561, - roomAndBoardSituations: { - parentUnEmployed: false, - parentEarnLowIncome: false, - parentReceiveIncomeAssistance: false, - livingAtHomePayingRoomAndBoard: true, - parentReceiveCanadaPensionOrOldAgeSupplement: false, + formName: ROOM_AND_BOARD_COSTS_FORM_NAME, + formData: roomAndBoardAppealData, + files: [roomAndBoardFile.uniqueFileName], }, - roomAndBoardSupportingDocuments: [ - { - url: `student/files/${roomAndBoardFile.uniqueFileName}`, - hash: "", - name: roomAndBoardFile.uniqueFileName, - size: 4, - type: "text/plain", - storage: "url", - originalName: roomAndBoardFile.fileName, - }, - ], - }; - const payload: StudentApplicationAppealAPIInDTO = { - studentAppealRequests: [ - { - formName: ROOM_AND_BOARD_COSTS_FORM_NAME, - formData: roomAndBoardAppealData, - files: [roomAndBoardFile.uniqueFileName], - }, - ], - }; - // Mock JWT user to return the saved student from token. - await mockJWTUserInfo(appModule, student.user); - // Get any student user token. - const studentToken = await getStudentToken( - FakeStudentUsersTypes.FakeStudentUserType1, - ); - // Mock the form service to validate the dry-run submission result. - // and this mock must be removed. - const formService = await getProviderInstanceForModule( - appModule, - AppStudentsModule, - FormService, - ); - const dryRunSubmissionMock = jest.fn().mockResolvedValue({ - valid: true, - formName: ROOM_AND_BOARD_COSTS_FORM_NAME, - data: { data: roomAndBoardAppealData }, - }); - formService.dryRunSubmission = dryRunSubmissionMock; - const endpoint = `/students/appeal/application/${application.id}`; + ], + }; + // Mock JWT user to return the saved student from token. + await mockJWTUserInfo(appModule, student.user); + // Get any student user token. + const studentToken = await getStudentToken( + FakeStudentUsersTypes.FakeStudentUserType1, + ); + // Mock the form service to validate the dry-run submission result. + // and this mock must be removed. + const formService = await getProviderInstanceForModule( + appModule, + AppStudentsModule, + FormService, + ); + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: ROOM_AND_BOARD_COSTS_FORM_NAME, + data: { data: roomAndBoardAppealData }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + const endpoint = `/students/appeal/application/${application.id}`; + const now = new Date(); + MockDate.set(now); - // Act/Assert - let createdAppealId: number; - await request(app.getHttpServer()) - .post(endpoint) - .send(payload) - .auth(studentToken, BEARER_AUTH_TYPE) - .expect(HttpStatus.CREATED) - .then((response) => { - expect(response.body.id).toBeGreaterThan(0); - createdAppealId = +response.body.id; - }); - const studentAppeal = await db.studentAppeal.findOne({ - select: { + // Act/Assert + let createdAppealId: number; + await request(app.getHttpServer()) + .post(endpoint) + .send(payload) + .auth(studentToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + expect(response.body.id).toBeGreaterThan(0); + createdAppealId = +response.body.id; + }); + const studentAppeal = await db.studentAppeal.findOne({ + select: { + id: true, + appealRequests: { id: true, - appealRequests: { - id: true, - submittedFormName: true, - submittedData: true, - }, + submittedFormName: true, + submittedData: true, }, - relations: { appealRequests: true }, - where: { application: { id: application.id } }, - }); - const [appealRequest] = studentAppeal.appealRequests; - expect(studentAppeal.id).toBe(createdAppealId); - expect(appealRequest.submittedFormName).toBe( - ROOM_AND_BOARD_COSTS_FORM_NAME, - ); - expect(appealRequest.submittedData).toStrictEqual(roomAndBoardAppealData); - // Expect to call the dry run submission. - expect(dryRunSubmissionMock).toHaveBeenCalledWith( - ROOM_AND_BOARD_COSTS_FORM_NAME, - roomAndBoardAppealData, - ); - }, - ); + }, + relations: { appealRequests: true }, + where: { application: { id: application.id } }, + }); + const [appealRequest] = studentAppeal.appealRequests; + expect(studentAppeal.id).toBe(createdAppealId); + expect(appealRequest.submittedFormName).toBe( + ROOM_AND_BOARD_COSTS_FORM_NAME, + ); + expect(appealRequest.submittedData).toStrictEqual(roomAndBoardAppealData); + // Expect to call the dry run submission. + expect(dryRunSubmissionMock).toHaveBeenCalledWith( + ROOM_AND_BOARD_COSTS_FORM_NAME, + roomAndBoardAppealData, + ); + // Validate notification for new appeal (2025-26+ program year). + const createdNotification = await db.notification.findOne({ + select: { id: true, messagePayload: true }, + where: { + notificationMessage: { + id: NotificationMessageType.MinistryFormSubmitted, + }, + dateSent: IsNull(), + }, + }); + expect(createdNotification.messagePayload).toStrictEqual({ + template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryFormSubmitted, + email_address: MINISTRY_EMAIL_ADDRESS, + personalisation: { + givenNames: student.user.firstName, + lastName: student.user.lastName, + birthDate: getDateOnlyFormat(student.birthDate), + studentEmail: student.user.email, + formCategory: NOTIFICATION_FORM_TYPE.ApplicationAppeal, + formName: "Room and board costs", + applicationNumber: application.applicationNumber, + dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, + }, + }); + }); it( "Should create step-parent waiver appeal for an application" + diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts index a3a4861e08..12a246ec41 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts @@ -31,6 +31,10 @@ import { getPSTPDTDateTime, } from "@sims/utilities/date-utils"; import { STUDENT_HAS_PENDING_APPEAL } from "../../../../constants"; +import { + GC_NOTIFY_TEMPLATE_IDS, + NOTIFICATION_FORM_TYPE, +} from "@sims/services/constants"; describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { let app: INestApplication; @@ -59,7 +63,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { // Update fake email contact to send ministry email. await db.notificationMessage.update( { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.MinistryFormSubmitted, }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); @@ -78,7 +82,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.MinistryFormSubmitted, }, }, { dateSent: new Date() }, @@ -171,20 +175,22 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.MinistryFormSubmitted, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: "241a360a-07d6-486f-9aa4-fae6903e1cff", + template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryFormSubmitted, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, lastName: student.user.lastName, birthDate: getDateOnlyFormat(student.birthDate), studentEmail: student.user.email, - applicationNumber: "not applicable", + formCategory: NOTIFICATION_FORM_TYPE.OtherAppeal, + formName: "Modified independent", + applicationNumber: "N/A", dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, }, }); diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts index d340a467da..4eafd418d0 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -161,7 +161,7 @@ export class FormSubmissionSubmitService { where: { id: studentId }, }); // Load application number if an application is linked to this submission. - let applicationNumber = "N/A"; + let applicationNumber: string | undefined; if (applicationId) { const application = await entityManager .getRepository(Application) @@ -169,7 +169,7 @@ export class FormSubmissionSubmitService { select: { id: true, applicationNumber: true }, where: { id: applicationId }, }); - applicationNumber = application?.applicationNumber ?? applicationNumber; + applicationNumber = application?.applicationNumber; } // Collect all form friendly names from the submission configs, comma-separated. diff --git a/sources/packages/backend/apps/api/src/services/form/constants.ts b/sources/packages/backend/apps/api/src/services/form/constants.ts index dbd3819f72..5fba2f1717 100644 --- a/sources/packages/backend/apps/api/src/services/form/constants.ts +++ b/sources/packages/backend/apps/api/src/services/form/constants.ts @@ -49,13 +49,3 @@ export const CHANGE_REQUEST_APPEAL_FORMS = [ FormNames.StudentFinancialInformationAppeal, FormNames.PartnerInformationAndIncomeAppeal, ]; - -/** - * Notification form type category labels used in ministry form submitted notifications - * to classify the type of form or appeal being submitted. - */ -export const NOTIFICATION_FORM_TYPE = { - ApplicationAppeal: "Application appeal", - OtherAppeal: "Other appeal", - StandardForm: "Standard form", -} as const; diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index 7bd62b614d..ce6c955628 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -224,7 +224,7 @@ export class StudentAppealService extends RecordDataModelService birthDate: studentAppeal.student.birthDate, formCategory: FormCategory.StudentAppeal, formName: formNames.join(", "), - applicationNumber: studentAppeal.application?.applicationNumber ?? "N/A", + applicationNumber: studentAppeal.application?.applicationNumber, }; await this.notificationActionsService.saveMinistryFormSubmittedNotification( ministryFormNotification, diff --git a/sources/packages/backend/libs/services/src/constants/index.ts b/sources/packages/backend/libs/services/src/constants/index.ts index b3767805ba..c45353097a 100644 --- a/sources/packages/backend/libs/services/src/constants/index.ts +++ b/sources/packages/backend/libs/services/src/constants/index.ts @@ -3,3 +3,4 @@ export * from "./error-code.constants"; export * from "./disbursements.constants"; export * from "./worker-constants"; export * from "./restriction.constants"; +export * from "./notification.constants"; diff --git a/sources/packages/backend/libs/services/src/constants/notification.constants.ts b/sources/packages/backend/libs/services/src/constants/notification.constants.ts new file mode 100644 index 0000000000..82ceaacc34 --- /dev/null +++ b/sources/packages/backend/libs/services/src/constants/notification.constants.ts @@ -0,0 +1,20 @@ +/** + * Notification form type category labels used in ministry form submitted notifications + * to classify the type of form or appeal being submitted. + */ +export const NOTIFICATION_FORM_TYPE = { + ApplicationAppeal: "Application appeal", + OtherAppeal: "Other appeal", + StandardForm: "Standard form", +} as const; + +/** + * GC Notify template IDs for the notification message types related to + * appeals and change requests, seeded in the database during migrations. + */ +export const GC_NOTIFY_TEMPLATE_IDS = { + MinistryChangeRequestSubmitted: "fad81016-0bed-4d4e-ad48-f70cc943399c", + StudentChangeRequestReviewCompleted: "9a4855d1-4f9a-4293-9868-cd853a8e4061", + MinistryFormSubmitted: "296aa2ea-dfa7-4285-9d5b-315b2a4911d6", + StudentFormCompleted: "fed6b26e-d1f2-4a8c-bfe5-5cb66c00458b", +} as const; diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index b5640c9952..850c9e9f3e 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -48,7 +48,7 @@ import { LoggerService } from "@sims/utilities/logger"; import { ECE_RESPONSE_ATTACHMENT_FILE_NAME } from "@sims/integrations/constants"; import { SystemUsersService } from "@sims/services/system-users"; import { NotificationMetadata } from "@sims/sims-db/entities/notification-metadata.type"; -import { NOTIFICATION_FORM_TYPE } from "apps/api/src/services/form/constants"; +import { NOTIFICATION_FORM_TYPE } from "../../constants"; @Injectable() export class NotificationActionsService { diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts index 84fe719603..d9cb87b49b 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts @@ -258,5 +258,5 @@ export interface MinistryFormSubmittedNotification { birthDate: string; formCategory: FormCategory; formName: string; - applicationNumber: string; + applicationNumber?: string; } diff --git a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts index 7bdd954129..54326d7d69 100644 --- a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts +++ b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts @@ -238,19 +238,19 @@ export enum NotificationMessageType { */ StudentCOERequiredNearEndDateNotification = 35, /** - * Ministry notification for student submits change request. + * Ministry notification when a student submits a change request. */ MinistryChangeRequestSubmitted = 36, /** - * Student notification for change request review completed. + * Student notification when a change request review is completed. */ StudentChangeRequestReviewCompleted = 37, /** - * Ministry notification for student submits form or appeal. + * Ministry notification when a student submits a form or appeal. */ MinistryFormSubmitted = 38, /** - * Student notification for form or appeal adjudication complete. + * Student notification when a form or appeal is completed. */ StudentFormCompleted = 39, } From 2a705a1d2910a524544a24735ea02983afd6c577 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sun, 15 Mar 2026 16:06:31 -0700 Subject: [PATCH 07/16] Remove unused code --- .../notification-actions.service.ts | 80 ------------------- 1 file changed, 80 deletions(-) diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index 850c9e9f3e..e56f7dc5ac 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -425,44 +425,6 @@ export class NotificationActionsService { ); } - /** - * Create change request complete notification to notify student - * when a change request is completed by ministry. - * @param notification notification details. - * @param auditUserId user who completes the change request. - * @param entityManager entity manager to execute in transaction. - */ - async saveChangeRequestCompleteNotification( - notification: StudentNotification, - auditUserId: number, - entityManager: EntityManager, - ): Promise { - const { templateId } = - await this.notificationMessageService.getNotificationMessageDetails( - NotificationMessageType.MinistryCompletesChange, - ); - - const changeRequestCompleteNotification = { - userId: notification.userId, - messageType: NotificationMessageType.MinistryCompletesChange, - messagePayload: { - email_address: notification.toAddress, - template_id: templateId, - personalisation: { - givenNames: notification.givenNames ?? "", - lastName: notification.lastName, - date: this.getDateTimeOnPSTTimeZone(), - }, - }, - }; - - await this.notificationService.saveNotifications( - [changeRequestCompleteNotification], - auditUserId, - { entityManager }, - ); - } - /** * Create institution report change notification to notify student * when institution reports a change to their application. @@ -884,48 +846,6 @@ export class NotificationActionsService { ); } - /** - * Creates student submitted change request after COE notification for ministry. - * @param notification notification details. - * @param entityManager entity manager to execute in transaction. - */ - async saveStudentSubmittedChangeRequestNotification( - notification: StudentSubmittedChangeRequestNotification, - entityManager: EntityManager, - ): Promise { - const auditUser = this.systemUsersService.systemUser; - const { templateId, emailContacts } = - await this.assertNotificationMessageDetails( - NotificationMessageType.StudentSubmittedChangeRequestNotification, - ); - if (!emailContacts?.length) { - return; - } - const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ - userId: auditUser.id, - messageType: - NotificationMessageType.StudentSubmittedChangeRequestNotification, - messagePayload: { - email_address: emailContact, - template_id: templateId, - personalisation: { - givenNames: notification.givenNames ?? "", - lastName: notification.lastName, - birthDate: getDateOnlyFormat(notification.birthDate), - studentEmail: notification.email, - applicationNumber: notification.applicationNumber, - dateTime: this.getDateTimeOnPSTTimeZone(), - }, - }, - })); - // Save notifications to be sent to the ministry into the notification table. - await this.notificationService.saveNotifications( - ministryNotificationsToSend, - auditUser.id, - { entityManager }, - ); - } - /** * Creates student requests basic bceid account notification for ministry. * @param notification notification details. From e2481ebbdc79ae67946a592930aa9ff59893a464 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sun, 15 Mar 2026 16:34:08 -0700 Subject: [PATCH 08/16] Copilot review --- .../application-change-request.service.ts | 2 +- .../apps/api/src/services/application/application.service.ts | 2 +- .../form-submission/form-submission-submit.service.ts | 2 +- .../api/src/services/student-appeal/student-appeal.service.ts | 2 +- ...355579843-InsertAppealChangeRequestNotificationMessages.ts | 4 +++- .../notification/notification-actions.service.ts | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts b/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts index 93442f7443..e66df4b1ba 100644 --- a/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts +++ b/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts @@ -175,7 +175,7 @@ export class ApplicationChangeRequestService { auditUserId: number, entityManager: EntityManager, ): Promise { - const student = await entityManager.getRepository(Student).findOne({ + const student = await entityManager.getRepository(Student).findOneOrFail({ select: { id: true, user: { id: true, firstName: true, lastName: true, email: true }, diff --git a/sources/packages/backend/apps/api/src/services/application/application.service.ts b/sources/packages/backend/apps/api/src/services/application/application.service.ts index 3a2b8ea551..73b14f9dbb 100644 --- a/sources/packages/backend/apps/api/src/services/application/application.service.ts +++ b/sources/packages/backend/apps/api/src/services/application/application.service.ts @@ -606,7 +606,7 @@ export class ApplicationService extends RecordDataModelService { applicationNumber: string, entityManager: EntityManager, ): Promise { - const student = await entityManager.getRepository(Student).findOne({ + const student = await entityManager.getRepository(Student).findOneOrFail({ select: { id: true, birthDate: true, diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts index 4eafd418d0..d057d57e70 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -151,7 +151,7 @@ export class FormSubmissionSubmitService { // Load student info required for the notification. const studentForNotification = await entityManager .getRepository(Student) - .findOne({ + .findOneOrFail({ select: { id: true, birthDate: true, diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index ce6c955628..927f4605bc 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -162,7 +162,7 @@ export class StudentAppealService extends RecordDataModelService loadEagerRelations: false, }); - // Check if the submission is for new appeal process(appeal process is for submissions from 2025-26 program year). + // Check if the submission is for new appeal process (appeal process is for submissions from 2025-26 program year). const isLegacyChangeRequest = studentAppeal.application !== null && !allowApplicationChangeRequest(studentAppeal.application.programYear); diff --git a/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts b/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts index 12bf4db84e..7a66293d05 100644 --- a/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts +++ b/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts @@ -1,7 +1,9 @@ import { MigrationInterface, QueryRunner } from "typeorm"; import { getSQLFileData } from "../utilities/sqlLoader"; -export class InsertAppealChangeRequestNotificationMessages1773355579843 implements MigrationInterface { +export class InsertAppealChangeRequestNotificationMessages1773355579843 + implements MigrationInterface +{ public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( getSQLFileData( diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index e56f7dc5ac..ce4e527ea6 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1512,7 +1512,7 @@ export class NotificationActionsService { } let formCategory: string = NOTIFICATION_FORM_TYPE.StandardForm; - if (notification.formCategory == FormCategory.StudentAppeal) { + if (notification.formCategory === FormCategory.StudentAppeal) { if (notification.applicationNumber) { formCategory = NOTIFICATION_FORM_TYPE.ApplicationAppeal; } else { From f0d334b75565aed417587949d43cca38e81faded Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Sun, 15 Mar 2026 16:34:32 -0700 Subject: [PATCH 09/16] Update 1773355579843-InsertAppealChangeRequestNotificationMessages.ts --- ...355579843-InsertAppealChangeRequestNotificationMessages.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts b/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts index 7a66293d05..12bf4db84e 100644 --- a/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts +++ b/sources/packages/backend/apps/db-migrations/src/migrations/1773355579843-InsertAppealChangeRequestNotificationMessages.ts @@ -1,9 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; import { getSQLFileData } from "../utilities/sqlLoader"; -export class InsertAppealChangeRequestNotificationMessages1773355579843 - implements MigrationInterface -{ +export class InsertAppealChangeRequestNotificationMessages1773355579843 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( getSQLFileData( From 65b04250853a91dfa33e4aa5bdddb7a16118e600 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 09:26:00 -0700 Subject: [PATCH 10/16] PR Comments --- ...assessApplicationChangeRequest.e2e-spec.ts | 6 ++-- ...roller.submitApplicationAppeal.e2e-spec.ts | 8 ++--- ...controller.submitStudentAppeal.e2e-spec.ts | 8 ++--- .../application-change-request.service.ts | 28 +++++++++------- .../application/application.service.ts | 33 ++++++++++--------- .../form-submission-submit.service.ts | 22 ++++++++----- .../student-appeal/student-appeal.service.ts | 2 +- ...l-change-request-notification-messages.sql | 4 +-- .../libs/services/src/constants/index.ts | 1 - .../notification-actions.service.ts | 22 +++---------- .../notification/notification.model.ts | 4 +-- .../libs/test-utils/src/constants/index.ts | 1 + .../src/constants/notification.constants.ts | 11 +------ 13 files changed, 66 insertions(+), 84 deletions(-) rename sources/packages/backend/libs/{services => test-utils}/src/constants/notification.constants.ts (60%) diff --git a/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts index f44a28eaba..7fd49ee45f 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application-change-request/_tests_/e2e/application-change-request.aest.controller.assessApplicationChangeRequest.e2e-spec.ts @@ -23,10 +23,8 @@ import { } from "@sims/test-utils"; import { ZeebeGrpcClient } from "@camunda8/sdk/dist/zeebe"; import MockDate from "mockdate"; -import { - INVALID_APPLICATION_EDIT_STATUS, - GC_NOTIFY_TEMPLATE_IDS, -} from "@sims/services/constants"; +import { INVALID_APPLICATION_EDIT_STATUS } from "@sims/services/constants"; +import { GC_NOTIFY_TEMPLATE_IDS } from "@sims/test-utils/constants"; import { getPSTPDTDateTime } from "@sims/utilities"; describe("ApplicationChangeRequestAESTController(e2e)-assessApplicationChangeRequest", () => { diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts index 450c542ba9..8afc128084 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts @@ -27,6 +27,7 @@ import { Application, ApplicationStatus, FileOriginType, + FormCategory, NotificationMessageType, OfferingIntensity, ProgramYear, @@ -45,10 +46,7 @@ import { APPLICATION_HAS_PENDING_APPEAL, APPLICATION_IS_NOT_ELIGIBLE_FOR_AN_APPEAL, } from "../../../../constants"; -import { - GC_NOTIFY_TEMPLATE_IDS, - NOTIFICATION_FORM_TYPE, -} from "@sims/services/constants"; +import { GC_NOTIFY_TEMPLATE_IDS } from "@sims/test-utils/constants"; describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { let app: INestApplication; @@ -810,7 +808,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { lastName: student.user.lastName, birthDate: getDateOnlyFormat(student.birthDate), studentEmail: student.user.email, - formCategory: NOTIFICATION_FORM_TYPE.ApplicationAppeal, + formCategory: FormCategory.StudentAppeal, formName: "Room and board costs", applicationNumber: application.applicationNumber, dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts index 12a246ec41..3d92bb6f55 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts @@ -21,6 +21,7 @@ import { TestingModule } from "@nestjs/testing"; import { AppStudentsModule } from "../../../../app.students.module"; import { FormService } from "../../../../services"; import { + FormCategory, NotificationMessageType, StudentAppealActionType, StudentAppealStatus, @@ -31,10 +32,7 @@ import { getPSTPDTDateTime, } from "@sims/utilities/date-utils"; import { STUDENT_HAS_PENDING_APPEAL } from "../../../../constants"; -import { - GC_NOTIFY_TEMPLATE_IDS, - NOTIFICATION_FORM_TYPE, -} from "@sims/services/constants"; +import { GC_NOTIFY_TEMPLATE_IDS } from "@sims/test-utils/constants"; describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { let app: INestApplication; @@ -188,7 +186,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { lastName: student.user.lastName, birthDate: getDateOnlyFormat(student.birthDate), studentEmail: student.user.email, - formCategory: NOTIFICATION_FORM_TYPE.OtherAppeal, + formCategory: FormCategory.StudentAppeal, formName: "Modified independent", applicationNumber: "N/A", dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, diff --git a/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts b/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts index e66df4b1ba..97a2031f78 100644 --- a/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts +++ b/sources/packages/backend/apps/api/src/services/application-change-request/application-change-request.service.ts @@ -107,12 +107,14 @@ export class ApplicationChangeRequestService { changeRequestApplication.updatedAt = currentDate; changeRequestApplication.applicationEditStatusUpdatedBy = auditUser; changeRequestApplication.applicationEditStatusUpdatedOn = currentDate; - await applicationRepo.save(changeRequestApplication); - await this.saveChangeRequestReviewCompletedNotification( - changeRequestApplication.student.id, - auditUserId, - transactionalEntityManager, - ); + await Promise.all([ + applicationRepo.save(changeRequestApplication), + this.saveChangeRequestReviewCompletedNotification( + changeRequestApplication.student.id, + auditUserId, + transactionalEntityManager, + ), + ]); return; } // Previously completed application that will be replaced by the newly approved application change request. @@ -150,12 +152,14 @@ export class ApplicationChangeRequestService { id: copyFromAssessment.formSubmission.id, } as FormSubmission; } - await applicationRepo.save(changeRequestApplication); - await this.saveChangeRequestReviewCompletedNotification( - changeRequestApplication.student.id, - auditUserId, - transactionalEntityManager, - ); + await Promise.all([ + applicationRepo.save(changeRequestApplication), + this.saveChangeRequestReviewCompletedNotification( + changeRequestApplication.student.id, + auditUserId, + transactionalEntityManager, + ), + ]); }); // Send a message to the workflow to proceed. await this.workflowClientService.sendApplicationChangeRequestStatusMessage( diff --git a/sources/packages/backend/apps/api/src/services/application/application.service.ts b/sources/packages/backend/apps/api/src/services/application/application.service.ts index 73b14f9dbb..318cf91ff8 100644 --- a/sources/packages/backend/apps/api/src/services/application/application.service.ts +++ b/sources/packages/backend/apps/api/src/services/application/application.service.ts @@ -508,26 +508,27 @@ export class ApplicationService extends RecordDataModelService { transactionalEntityManager.getRepository(Application); await applicationRepository.save(newApplication); - // Check if the application requires E2 restriction check. - await this.saveApplicationRestrictions( - newApplication.data, - studentId, - newApplication.id, - auditUserId, - transactionalEntityManager, - ); - newApplication.modifier = auditUser; newApplication.updatedAt = now; newApplication.studentAssessments = [originalAssessment]; newApplication.currentAssessment = originalAssessment; - await applicationRepository.save(newApplication); - // Notify the ministry that a new change request was submitted. - await this.saveMinistryChangeRequestSubmittedNotification( - studentId, - application.applicationNumber, - transactionalEntityManager, - ); + // Check if the application requires E2 restriction check, save the updated + // application, and notify the ministry, all in parallel. + await Promise.all([ + this.saveApplicationRestrictions( + newApplication.data, + studentId, + newApplication.id, + auditUserId, + transactionalEntityManager, + ), + applicationRepository.save(newApplication), + this.saveMinistryChangeRequestSubmittedNotification( + studentId, + application.applicationNumber, + transactionalEntityManager, + ), + ]); }); return { application: newApplication, diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts index d057d57e70..cf746506cf 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -151,7 +151,7 @@ export class FormSubmissionSubmitService { // Load student info required for the notification. const studentForNotification = await entityManager .getRepository(Student) - .findOneOrFail({ + .findOne({ select: { id: true, birthDate: true, @@ -160,7 +160,12 @@ export class FormSubmissionSubmitService { relations: { user: true }, where: { id: studentId }, }); - // Load application number if an application is linked to this submission. + if (!studentForNotification) { + throw new Error( + `Student ${studentId} not found while sending form submission notification.`, + ); + } + // Load the application number if a specific application is linked to the submission. let applicationNumber: string | undefined; if (applicationId) { const application = await entityManager @@ -169,13 +174,14 @@ export class FormSubmissionSubmitService { select: { id: true, applicationNumber: true }, where: { id: applicationId }, }); - applicationNumber = application?.applicationNumber; + if (!application) { + throw new Error( + `Application ${applicationId} not found while sending form submission notification.`, + ); + } + applicationNumber = application.applicationNumber; } - // Collect all form friendly names from the submission configs, comma-separated. - const formName = submissionConfigs - .map((config) => config.formType) - .join(", "); await this.notificationActionsService.saveMinistryFormSubmittedNotification( { givenNames: studentForNotification.user.firstName, @@ -183,7 +189,7 @@ export class FormSubmissionSubmitService { email: studentForNotification.user.email, birthDate: studentForNotification.birthDate, formCategory: formCategory, - formName, + formNames: submissionConfigs.map((config) => config.formType), applicationNumber, }, entityManager, diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index 927f4605bc..64ae9a8bd8 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -223,7 +223,7 @@ export class StudentAppealService extends RecordDataModelService email: studentAppeal.student.user.email, birthDate: studentAppeal.student.birthDate, formCategory: FormCategory.StudentAppeal, - formName: formNames.join(", "), + formNames, applicationNumber: studentAppeal.application?.applicationNumber, }; await this.notificationActionsService.saveMinistryFormSubmittedNotification( diff --git a/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql b/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql index 8f983b88ff..200a294892 100644 --- a/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql +++ b/sources/packages/backend/apps/db-migrations/src/sql/NotificationMessages/Insert-appeal-change-request-notification-messages.sql @@ -13,11 +13,11 @@ VALUES ), ( 38, - 'Ministry notification for student submits form or appeal.', + 'Ministry notification for student form submission.', '296aa2ea-dfa7-4285-9d5b-315b2a4911d6' ), ( 39, - 'Student notification for form or appeal adjudication complete.', + 'Student notification for form submission completed.', 'fed6b26e-d1f2-4a8c-bfe5-5cb66c00458b' ); \ No newline at end of file diff --git a/sources/packages/backend/libs/services/src/constants/index.ts b/sources/packages/backend/libs/services/src/constants/index.ts index c45353097a..b3767805ba 100644 --- a/sources/packages/backend/libs/services/src/constants/index.ts +++ b/sources/packages/backend/libs/services/src/constants/index.ts @@ -3,4 +3,3 @@ export * from "./error-code.constants"; export * from "./disbursements.constants"; export * from "./worker-constants"; export * from "./restriction.constants"; -export * from "./notification.constants"; diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index ce4e527ea6..b7b45f10bf 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1,9 +1,5 @@ import { Injectable } from "@nestjs/common"; -import { - FormCategory, - NotificationMessage, - NotificationMessageType, -} from "@sims/sims-db"; +import { NotificationMessage, NotificationMessageType } from "@sims/sims-db"; import { base64Encode, getDateOnlyFormat, @@ -48,7 +44,6 @@ import { LoggerService } from "@sims/utilities/logger"; import { ECE_RESPONSE_ATTACHMENT_FILE_NAME } from "@sims/integrations/constants"; import { SystemUsersService } from "@sims/services/system-users"; import { NotificationMetadata } from "@sims/sims-db/entities/notification-metadata.type"; -import { NOTIFICATION_FORM_TYPE } from "../../constants"; @Injectable() export class NotificationActionsService { @@ -1493,8 +1488,7 @@ export class NotificationActionsService { /** * Creates a ministry notification when a student submits a form or appeal, - * including form type categorization (application appeal, other appeal, or standard form), - * a human-readable form name, and the related application number. + * using the form category directly from the dynamic form configuration. * @param notification notification details. * @param entityManager entity manager to execute in transaction. */ @@ -1511,14 +1505,6 @@ export class NotificationActionsService { return; } - let formCategory: string = NOTIFICATION_FORM_TYPE.StandardForm; - if (notification.formCategory === FormCategory.StudentAppeal) { - if (notification.applicationNumber) { - formCategory = NOTIFICATION_FORM_TYPE.ApplicationAppeal; - } else { - formCategory = NOTIFICATION_FORM_TYPE.OtherAppeal; - } - } const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ userId: auditUser.id, messageType: NotificationMessageType.MinistryFormSubmitted, @@ -1530,8 +1516,8 @@ export class NotificationActionsService { lastName: notification.lastName, birthDate: getDateOnlyFormat(notification.birthDate), studentEmail: notification.email, - formCategory: formCategory, - formName: notification.formName, + formCategory: notification.formCategory, + formName: notification.formNames.join(", "), applicationNumber: notification.applicationNumber ?? "N/A", dateTime: this.getDateTimeOnPSTTimeZone(), }, diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts index d9cb87b49b..2b265eaade 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts @@ -249,7 +249,7 @@ export interface StudentCOERequiredNearEndDateNotification { /** * Ministry notification data when a student submits a form or appeal, * with form type categorization (application appeal, other appeal, standard form), - * a human-readable form name, and the related application number. + * a comma-separated list of human-readable form names, and the related application number. */ export interface MinistryFormSubmittedNotification { givenNames: string; @@ -257,6 +257,6 @@ export interface MinistryFormSubmittedNotification { email: string; birthDate: string; formCategory: FormCategory; - formName: string; + formNames: string[]; applicationNumber?: string; } diff --git a/sources/packages/backend/libs/test-utils/src/constants/index.ts b/sources/packages/backend/libs/test-utils/src/constants/index.ts index 2ffc896660..3e09b8b37c 100644 --- a/sources/packages/backend/libs/test-utils/src/constants/index.ts +++ b/sources/packages/backend/libs/test-utils/src/constants/index.ts @@ -1 +1,2 @@ export * from "./institution.constants"; +export * from "./notification.constants"; diff --git a/sources/packages/backend/libs/services/src/constants/notification.constants.ts b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts similarity index 60% rename from sources/packages/backend/libs/services/src/constants/notification.constants.ts rename to sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts index 82ceaacc34..74891e551a 100644 --- a/sources/packages/backend/libs/services/src/constants/notification.constants.ts +++ b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts @@ -1,16 +1,7 @@ -/** - * Notification form type category labels used in ministry form submitted notifications - * to classify the type of form or appeal being submitted. - */ -export const NOTIFICATION_FORM_TYPE = { - ApplicationAppeal: "Application appeal", - OtherAppeal: "Other appeal", - StandardForm: "Standard form", -} as const; - /** * GC Notify template IDs for the notification message types related to * appeals and change requests, seeded in the database during migrations. + * These constants are intended for use in E2E tests only. */ export const GC_NOTIFY_TEMPLATE_IDS = { MinistryChangeRequestSubmitted: "fad81016-0bed-4d4e-ad48-f70cc943399c", From 6bec4c72723c1fe4c789e066b2d7a88ceef58da0 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 09:51:05 -0700 Subject: [PATCH 11/16] Updated logic for appeals / change requests --- ...r.approveStudentAppealRequests.e2e-spec.ts | 8 +- ...roller.submitApplicationAppeal.e2e-spec.ts | 12 ++- ...controller.submitStudentAppeal.e2e-spec.ts | 12 ++- .../student-appeal-assessment.service.ts | 2 +- .../student-appeal/student-appeal.service.ts | 31 ++----- .../notification-actions.service.ts | 80 +++++++++++++++++++ .../notification/notification.model.ts | 12 +++ .../src/constants/notification.constants.ts | 3 + 8 files changed, 118 insertions(+), 42 deletions(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts index d914289d0d..05faa29c85 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts @@ -28,7 +28,7 @@ import { import { StudentAppealApprovalAPIInDTO } from "../../../../route-controllers"; import MockDate from "mockdate"; import { getPSTPDTDateTime } from "@sims/utilities"; -import { GC_NOTIFY_TEMPLATE_IDS } from "@sims/services/constants"; +import { GC_NOTIFY_TEMPLATE_IDS } from "@sims/test-utils/constants"; describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => { let app: INestApplication; @@ -55,7 +55,7 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentFormCompleted, + id: NotificationMessageType.MinistryCompletesChange, }, }, { dateSent: new Date() }, @@ -336,13 +336,13 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.StudentFormCompleted, + id: NotificationMessageType.MinistryCompletesChange, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.StudentFormCompleted, + template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryCompletesChange, email_address: student.user.email, personalisation: { givenNames: student.user.firstName ?? "", diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts index 8afc128084..311d56f86b 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts @@ -27,7 +27,6 @@ import { Application, ApplicationStatus, FileOriginType, - FormCategory, NotificationMessageType, OfferingIntensity, ProgramYear, @@ -80,7 +79,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); await db.notificationMessage.update( - { id: NotificationMessageType.MinistryFormSubmitted }, + { id: NotificationMessageType.StudentSubmittedChangeRequestNotification }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); }); @@ -100,7 +99,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.MinistryFormSubmitted, + id: NotificationMessageType.StudentSubmittedChangeRequestNotification, }, }, { dateSent: new Date() }, @@ -795,21 +794,20 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.MinistryFormSubmitted, + id: NotificationMessageType.StudentSubmittedChangeRequestNotification, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryFormSubmitted, + template_id: + GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedChangeRequestNotification, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, lastName: student.user.lastName, birthDate: getDateOnlyFormat(student.birthDate), studentEmail: student.user.email, - formCategory: FormCategory.StudentAppeal, - formName: "Room and board costs", applicationNumber: application.applicationNumber, dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, }, diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts index 3d92bb6f55..fcaf61acfb 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts @@ -21,7 +21,6 @@ import { TestingModule } from "@nestjs/testing"; import { AppStudentsModule } from "../../../../app.students.module"; import { FormService } from "../../../../services"; import { - FormCategory, NotificationMessageType, StudentAppealActionType, StudentAppealStatus, @@ -61,7 +60,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { // Update fake email contact to send ministry email. await db.notificationMessage.update( { - id: NotificationMessageType.MinistryFormSubmitted, + id: NotificationMessageType.StudentSubmittedChangeRequestNotification, }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); @@ -80,7 +79,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.MinistryFormSubmitted, + id: NotificationMessageType.StudentSubmittedChangeRequestNotification, }, }, { dateSent: new Date() }, @@ -173,21 +172,20 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.MinistryFormSubmitted, + id: NotificationMessageType.StudentSubmittedChangeRequestNotification, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryFormSubmitted, + template_id: + GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedChangeRequestNotification, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, lastName: student.user.lastName, birthDate: getDateOnlyFormat(student.birthDate), studentEmail: student.user.email, - formCategory: FormCategory.StudentAppeal, - formName: "Modified independent", applicationNumber: "N/A", dateTime: `${getPSTPDTDateTime(now)} PST/PDT`, }, diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts index efb3263dcb..c0cf5416f2 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal-assessment/student-appeal-assessment.service.ts @@ -133,7 +133,7 @@ export class StudentAppealAssessmentService { entityManager, ); } else { - await this.notificationActionsService.saveStudentFormCompletedNotification( + await this.notificationActionsService.saveStudentAppealCompletedNotification( studentNotification, auditUserId, entityManager, diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index 64ae9a8bd8..f4609ecc50 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -20,7 +20,6 @@ import { FileOriginType, ApplicationStatus, Student, - FormCategory, } from "@sims/sims-db"; import { AppealType, @@ -41,10 +40,8 @@ import { PROGRAM_YEAR_2025_26_START_DATE } from "./constants"; import { NotificationActionsService, StudentSubmittedChangeRequestNotification, - MinistryFormSubmittedNotification, } from "@sims/services/notifications"; import { StudentFileService } from "../student-file/student-file.service"; -import { DynamicFormConfigurationService } from "../dynamic-form-configuration/dynamic-form-configuration.service"; import { InjectRepository } from "@nestjs/typeorm"; /** @@ -57,7 +54,6 @@ export class StudentAppealService extends RecordDataModelService private readonly studentAppealRequestsService: StudentAppealRequestsService, private readonly notificationActionsService: NotificationActionsService, private readonly studentFileService: StudentFileService, - private readonly dynamicFormConfigurationService: DynamicFormConfigurationService, @InjectRepository(Application) private readonly applicationRepo: Repository, ) { @@ -209,25 +205,14 @@ export class StudentAppealService extends RecordDataModelService studentAppeal: StudentAppeal, entityManager: EntityManager, ): Promise { - // Map technical form names to human-readable friendly names using the dynamic form configuration. - const formNames = studentAppeal.appealRequests.map((request) => { - const config = - this.dynamicFormConfigurationService.getFormByDefinitionName( - request.submittedFormName, - ); - return config?.formType ?? request.submittedFormName; - }); - const ministryFormNotification: MinistryFormSubmittedNotification = { - givenNames: studentAppeal.student.user.firstName, - lastName: studentAppeal.student.user.lastName, - email: studentAppeal.student.user.email, - birthDate: studentAppeal.student.birthDate, - formCategory: FormCategory.StudentAppeal, - formNames, - applicationNumber: studentAppeal.application?.applicationNumber, - }; - await this.notificationActionsService.saveMinistryFormSubmittedNotification( - ministryFormNotification, + await this.notificationActionsService.saveMinistryStudentSubmittedAppealNotification( + { + givenNames: studentAppeal.student.user.firstName, + lastName: studentAppeal.student.user.lastName, + email: studentAppeal.student.user.email, + birthDate: studentAppeal.student.birthDate, + applicationNumber: studentAppeal.application?.applicationNumber, + }, entityManager, ); } diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index b7b45f10bf..2ea3de768a 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -38,6 +38,7 @@ import { ScholasticStandingReversalNotification, StudentCOERequiredNearEndDateNotification, MinistryFormSubmittedNotification, + MinistryStudentAppealNotification, } from ".."; import { NotificationService } from "./notification.service"; import { LoggerService } from "@sims/utilities/logger"; @@ -1408,6 +1409,85 @@ export class NotificationActionsService { ); } + /** + * Creates a ministry notification when a student submits an appeal, + * using the existing production appeal submitted template. + * @param notification notification details. + * @param entityManager entity manager to execute in transaction. + */ + async saveMinistryStudentSubmittedAppealNotification( + notification: MinistryStudentAppealNotification, + entityManager: EntityManager, + ): Promise { + const auditUser = this.systemUsersService.systemUser; + const { templateId, emailContacts } = + await this.assertNotificationMessageDetails( + NotificationMessageType.StudentSubmittedChangeRequestNotification, + ); + if (!emailContacts?.length) { + return; + } + const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ + userId: auditUser.id, + messageType: + NotificationMessageType.StudentSubmittedChangeRequestNotification, + messagePayload: { + email_address: emailContact, + template_id: templateId, + personalisation: { + givenNames: notification.givenNames ?? "", + lastName: notification.lastName, + birthDate: getDateOnlyFormat(notification.birthDate), + studentEmail: notification.email, + applicationNumber: notification.applicationNumber ?? "N/A", + dateTime: this.getDateTimeOnPSTTimeZone(), + }, + }, + })); + // Save notifications to be sent to the ministry into the notification table. + await this.notificationService.saveNotifications( + ministryNotificationsToSend, + auditUser.id, + { entityManager }, + ); + } + + /** + * Creates a student notification when the ministry completes reviewing an appeal, + * using the existing production ministry-completes-change template. + * @param notification notification details. + * @param auditUserId user who completed the appeal review. + * @param entityManager entity manager to execute in transaction. + */ + async saveStudentAppealCompletedNotification( + notification: StudentNotification, + auditUserId: number, + entityManager: EntityManager, + ): Promise { + const { templateId } = + await this.notificationMessageService.getNotificationMessageDetails( + NotificationMessageType.MinistryCompletesChange, + ); + const appealCompletedNotification = { + userId: notification.userId, + messageType: NotificationMessageType.MinistryCompletesChange, + messagePayload: { + email_address: notification.toAddress, + template_id: templateId, + personalisation: { + givenNames: notification.givenNames ?? "", + lastName: notification.lastName, + date: this.getDateTimeOnPSTTimeZone(), + }, + }, + }; + await this.notificationService.saveNotifications( + [appealCompletedNotification], + auditUserId, + { entityManager }, + ); + } + /** * Creates a ministry notification when a student submits a change request, * using the updated change request submitted template. diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts index 2b265eaade..26111a33d8 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts @@ -111,6 +111,18 @@ export interface StudentSubmittedChangeRequestNotification { applicationNumber: string; } +/** + * Ministry notification data when a student submits an appeal using the + * existing (pre-form-submissions-framework) appeal notification template. + */ +export interface MinistryStudentAppealNotification { + givenNames: string; + lastName: string; + email: string; + birthDate: string; + applicationNumber?: string; +} + export interface StudentRequestsBasicBCeIDAccountNotification { givenNames: string; lastName: string; diff --git a/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts index 74891e551a..9d37ba33a4 100644 --- a/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts +++ b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts @@ -4,6 +4,9 @@ * These constants are intended for use in E2E tests only. */ export const GC_NOTIFY_TEMPLATE_IDS = { + StudentSubmittedChangeRequestNotification: + "241a360a-07d6-486f-9aa4-fae6903e1cff", + MinistryCompletesChange: "d78624da-c0f3-4bf7-8508-e311a50cfead", MinistryChangeRequestSubmitted: "fad81016-0bed-4d4e-ad48-f70cc943399c", StudentChangeRequestReviewCompleted: "9a4855d1-4f9a-4293-9868-cd853a8e4061", MinistryFormSubmitted: "296aa2ea-dfa7-4285-9d5b-315b2a4911d6", From 8d8d336d427721e5b3641398c9fcb06005261f64 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 10:08:30 -0700 Subject: [PATCH 12/16] Rename NotificationMessageType --- ...ontroller.approveStudentAppealRequests.e2e-spec.ts | 6 +++--- ...nts.controller.submitApplicationAppeal.e2e-spec.ts | 9 ++++----- ...tudents.controller.submitStudentAppeal.e2e-spec.ts | 11 +++++------ .../notification/notification-actions.service.ts | 11 +++++------ .../libs/sims-db/src/entities/notification.model.ts | 8 ++++---- .../src/constants/notification.constants.ts | 5 ++--- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts index 05faa29c85..43f5d1d6f3 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts @@ -55,7 +55,7 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => await db.notification.update( { notificationMessage: { - id: NotificationMessageType.MinistryCompletesChange, + id: NotificationMessageType.MinistryCompletesAppeal, }, }, { dateSent: new Date() }, @@ -336,13 +336,13 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.MinistryCompletesChange, + id: NotificationMessageType.MinistryCompletesAppeal, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryCompletesChange, + template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryCompletesAppeal, email_address: student.user.email, personalisation: { givenNames: student.user.firstName ?? "", diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts index 311d56f86b..584c2964c4 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts @@ -79,7 +79,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); await db.notificationMessage.update( - { id: NotificationMessageType.StudentSubmittedChangeRequestNotification }, + { id: NotificationMessageType.StudentSubmittedAppealNotification }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); }); @@ -99,7 +99,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.StudentSubmittedAppealNotification, }, }, { dateSent: new Date() }, @@ -794,14 +794,13 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.StudentSubmittedAppealNotification, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: - GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedChangeRequestNotification, + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedAppealNotification, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts index fcaf61acfb..ab9ff00f9a 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts @@ -60,7 +60,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { // Update fake email contact to send ministry email. await db.notificationMessage.update( { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.StudentSubmittedAppealNotification, }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); @@ -74,12 +74,12 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { beforeEach(async () => { MockDate.reset(); await resetMockJWTUserInfo(appModule); - // Mark all existing appeals(change request) notifications as sent + // Mark all existing appeals notifications as sent // to allow it to asserted when a new appeal is submitted. await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.StudentSubmittedAppealNotification, }, }, { dateSent: new Date() }, @@ -172,14 +172,13 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.StudentSubmittedChangeRequestNotification, + id: NotificationMessageType.StudentSubmittedAppealNotification, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: - GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedChangeRequestNotification, + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedAppealNotification, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index 2ea3de768a..0ea5361e21 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1422,15 +1422,14 @@ export class NotificationActionsService { const auditUser = this.systemUsersService.systemUser; const { templateId, emailContacts } = await this.assertNotificationMessageDetails( - NotificationMessageType.StudentSubmittedChangeRequestNotification, + NotificationMessageType.StudentSubmittedAppealNotification, ); if (!emailContacts?.length) { return; } const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ userId: auditUser.id, - messageType: - NotificationMessageType.StudentSubmittedChangeRequestNotification, + messageType: NotificationMessageType.StudentSubmittedAppealNotification, messagePayload: { email_address: emailContact, template_id: templateId, @@ -1454,7 +1453,7 @@ export class NotificationActionsService { /** * Creates a student notification when the ministry completes reviewing an appeal, - * using the existing production ministry-completes-change template. + * using the existing production ministry-completes-appeal template. * @param notification notification details. * @param auditUserId user who completed the appeal review. * @param entityManager entity manager to execute in transaction. @@ -1466,11 +1465,11 @@ export class NotificationActionsService { ): Promise { const { templateId } = await this.notificationMessageService.getNotificationMessageDetails( - NotificationMessageType.MinistryCompletesChange, + NotificationMessageType.MinistryCompletesAppeal, ); const appealCompletedNotification = { userId: notification.userId, - messageType: NotificationMessageType.MinistryCompletesChange, + messageType: NotificationMessageType.MinistryCompletesAppeal, messagePayload: { email_address: notification.toAddress, template_id: templateId, diff --git a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts index 54326d7d69..77e73a71f5 100644 --- a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts +++ b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts @@ -113,9 +113,9 @@ export enum NotificationMessageType { */ MinistryCompletesException = 4, /** - * Ministry completes updating a change requested by student. + * Ministry completes updating an appeal requested by student. */ - MinistryCompletesChange = 5, + MinistryCompletesAppeal = 5, /** * Institution reporting a change on application. */ @@ -166,9 +166,9 @@ export enum NotificationMessageType { */ MinistryNotificationDisbursementBlocked = 17, /** - * Student submitted change request after COE. + * Student submitted appeal. */ - StudentSubmittedChangeRequestNotification = 18, + StudentSubmittedAppealNotification = 18, /** * Student submits application with exception request. */ diff --git a/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts index 9d37ba33a4..6c5fd84cc9 100644 --- a/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts +++ b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts @@ -4,9 +4,8 @@ * These constants are intended for use in E2E tests only. */ export const GC_NOTIFY_TEMPLATE_IDS = { - StudentSubmittedChangeRequestNotification: - "241a360a-07d6-486f-9aa4-fae6903e1cff", - MinistryCompletesChange: "d78624da-c0f3-4bf7-8508-e311a50cfead", + StudentSubmittedAppealNotification: "241a360a-07d6-486f-9aa4-fae6903e1cff", + MinistryCompletesAppeal: "d78624da-c0f3-4bf7-8508-e311a50cfead", MinistryChangeRequestSubmitted: "fad81016-0bed-4d4e-ad48-f70cc943399c", StudentChangeRequestReviewCompleted: "9a4855d1-4f9a-4293-9868-cd853a8e4061", MinistryFormSubmitted: "296aa2ea-dfa7-4285-9d5b-315b2a4911d6", From 32e84981dfc86e8c0e5d4944987e358ce41491df Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 10:14:23 -0700 Subject: [PATCH 13/16] Enum updated --- ...st.controller.approveStudentAppealRequests.e2e-spec.ts | 4 ++-- ...tudents.controller.submitApplicationAppeal.e2e-spec.ts | 8 ++++---- ...al.students.controller.submitStudentAppeal.e2e-spec.ts | 8 ++++---- .../notification/notification-actions.service.ts | 8 ++++---- .../libs/sims-db/src/entities/notification.model.ts | 4 ++-- .../test-utils/src/constants/notification.constants.ts | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts index 43f5d1d6f3..5f65cc45b5 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts @@ -55,7 +55,7 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => await db.notification.update( { notificationMessage: { - id: NotificationMessageType.MinistryCompletesAppeal, + id: NotificationMessageType.MinistryAppealCompleted, }, }, { dateSent: new Date() }, @@ -336,7 +336,7 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.MinistryCompletesAppeal, + id: NotificationMessageType.MinistryAppealCompleted, }, dateSent: IsNull(), }, diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts index 584c2964c4..ccc1fa1320 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts @@ -79,7 +79,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); await db.notificationMessage.update( - { id: NotificationMessageType.StudentSubmittedAppealNotification }, + { id: NotificationMessageType.StudentAppealSubmitted }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); }); @@ -99,7 +99,7 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentSubmittedAppealNotification, + id: NotificationMessageType.StudentAppealSubmitted, }, }, { dateSent: new Date() }, @@ -794,13 +794,13 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.StudentSubmittedAppealNotification, + id: NotificationMessageType.StudentAppealSubmitted, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedAppealNotification, + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentAppealSubmitted, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts index ab9ff00f9a..65f855773b 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts @@ -60,7 +60,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { // Update fake email contact to send ministry email. await db.notificationMessage.update( { - id: NotificationMessageType.StudentSubmittedAppealNotification, + id: NotificationMessageType.StudentAppealSubmitted, }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); @@ -79,7 +79,7 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentSubmittedAppealNotification, + id: NotificationMessageType.StudentAppealSubmitted, }, }, { dateSent: new Date() }, @@ -172,13 +172,13 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => { select: { id: true, messagePayload: true }, where: { notificationMessage: { - id: NotificationMessageType.StudentSubmittedAppealNotification, + id: NotificationMessageType.StudentAppealSubmitted, }, dateSent: IsNull(), }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.StudentSubmittedAppealNotification, + template_id: GC_NOTIFY_TEMPLATE_IDS.StudentAppealSubmitted, email_address: MINISTRY_EMAIL_ADDRESS, personalisation: { givenNames: student.user.firstName, diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index 0ea5361e21..223c544f26 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1422,14 +1422,14 @@ export class NotificationActionsService { const auditUser = this.systemUsersService.systemUser; const { templateId, emailContacts } = await this.assertNotificationMessageDetails( - NotificationMessageType.StudentSubmittedAppealNotification, + NotificationMessageType.StudentAppealSubmitted, ); if (!emailContacts?.length) { return; } const ministryNotificationsToSend = emailContacts.map((emailContact) => ({ userId: auditUser.id, - messageType: NotificationMessageType.StudentSubmittedAppealNotification, + messageType: NotificationMessageType.StudentAppealSubmitted, messagePayload: { email_address: emailContact, template_id: templateId, @@ -1465,11 +1465,11 @@ export class NotificationActionsService { ): Promise { const { templateId } = await this.notificationMessageService.getNotificationMessageDetails( - NotificationMessageType.MinistryCompletesAppeal, + NotificationMessageType.MinistryAppealCompleted, ); const appealCompletedNotification = { userId: notification.userId, - messageType: NotificationMessageType.MinistryCompletesAppeal, + messageType: NotificationMessageType.MinistryAppealCompleted, messagePayload: { email_address: notification.toAddress, template_id: templateId, diff --git a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts index 77e73a71f5..b15851bd65 100644 --- a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts +++ b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts @@ -115,7 +115,7 @@ export enum NotificationMessageType { /** * Ministry completes updating an appeal requested by student. */ - MinistryCompletesAppeal = 5, + MinistryAppealCompleted = 5, /** * Institution reporting a change on application. */ @@ -168,7 +168,7 @@ export enum NotificationMessageType { /** * Student submitted appeal. */ - StudentSubmittedAppealNotification = 18, + StudentAppealSubmitted = 18, /** * Student submits application with exception request. */ diff --git a/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts index 6c5fd84cc9..2d7ac443b8 100644 --- a/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts +++ b/sources/packages/backend/libs/test-utils/src/constants/notification.constants.ts @@ -4,8 +4,8 @@ * These constants are intended for use in E2E tests only. */ export const GC_NOTIFY_TEMPLATE_IDS = { - StudentSubmittedAppealNotification: "241a360a-07d6-486f-9aa4-fae6903e1cff", - MinistryCompletesAppeal: "d78624da-c0f3-4bf7-8508-e311a50cfead", + StudentAppealSubmitted: "241a360a-07d6-486f-9aa4-fae6903e1cff", + MinistryAppealCompleted: "d78624da-c0f3-4bf7-8508-e311a50cfead", MinistryChangeRequestSubmitted: "fad81016-0bed-4d4e-ad48-f70cc943399c", StudentChangeRequestReviewCompleted: "9a4855d1-4f9a-4293-9868-cd853a8e4061", MinistryFormSubmitted: "296aa2ea-dfa7-4285-9d5b-315b2a4911d6", From 27c6e643fb2b7ec9ca49f07d864811d29580b01c Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 10:22:11 -0700 Subject: [PATCH 14/16] Update formName to be a list --- .../notification/notification-actions.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index 223c544f26..fe72c127ab 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1596,7 +1596,9 @@ export class NotificationActionsService { birthDate: getDateOnlyFormat(notification.birthDate), studentEmail: notification.email, formCategory: notification.formCategory, - formName: notification.formNames.join(", "), + formName: notification.formNames + .map((name) => `
  • ${name}
  • `) + .join(""), applicationNumber: notification.applicationNumber ?? "N/A", dateTime: this.getDateTimeOnPSTTimeZone(), }, From 7b72f22b66c7596597b27188abe375e295ac048e Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 11:08:36 -0700 Subject: [PATCH 15/16] Update student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts --- ...eal.aest.controller.approveStudentAppealRequests.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts index 5f65cc45b5..2e4c524b64 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts @@ -342,7 +342,7 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => }, }); expect(createdNotification.messagePayload).toStrictEqual({ - template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryCompletesAppeal, + template_id: GC_NOTIFY_TEMPLATE_IDS.MinistryAppealCompleted, email_address: student.user.email, personalisation: { givenNames: student.user.firstName ?? "", From da3215b96394f52c43ed46d77c845674a201dd66 Mon Sep 17 00:00:00 2001 From: Tiago Graf Date: Tue, 17 Mar 2026 15:27:01 -0700 Subject: [PATCH 16/16] Updated PR comments --- ...r.approveStudentAppealRequests.e2e-spec.ts | 15 ++--- ...roller.submitApplicationAppeal.e2e-spec.ts | 26 ++++---- .../application/application.service.ts | 2 +- .../dynamic-form-configuration.service.ts | 14 ---- .../form-submission-submit.service.ts | 64 ++++++++----------- .../student-appeal/student-appeal.service.ts | 2 - .../notification-actions.service.ts | 6 +- .../notification/notification.model.ts | 5 +- .../src/entities/notification.model.ts | 4 +- 9 files changed, 50 insertions(+), 88 deletions(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts index 2e4c524b64..5809470092 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.aest.controller.approveStudentAppealRequests.e2e-spec.ts @@ -1,4 +1,4 @@ -import { IsNull } from "typeorm"; +import { In, IsNull } from "typeorm"; import { HttpStatus, INestApplication } from "@nestjs/common"; import * as request from "supertest"; import { @@ -47,15 +47,10 @@ describe("StudentAppealAESTController(e2e)-approveStudentAppealRequests", () => await db.notification.update( { notificationMessage: { - id: NotificationMessageType.StudentChangeRequestReviewCompleted, - }, - }, - { dateSent: new Date() }, - ); - await db.notification.update( - { - notificationMessage: { - id: NotificationMessageType.MinistryAppealCompleted, + id: In([ + NotificationMessageType.StudentChangeRequestReviewCompleted, + NotificationMessageType.MinistryAppealCompleted, + ]), }, }, { dateSent: new Date() }, diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts index ccc1fa1320..ef6fe0e9be 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitApplicationAppeal.e2e-spec.ts @@ -1,6 +1,6 @@ import { HttpStatus, INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { DataSource, IsNull, Repository } from "typeorm"; +import { DataSource, In, IsNull, Repository } from "typeorm"; import { BEARER_AUTH_TYPE, createTestingAppModule, @@ -75,11 +75,12 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { recentActiveProgramYear = await getRecentActiveProgramYear(db); // Update fake email contacts to send ministry notifications. await db.notificationMessage.update( - { id: NotificationMessageType.MinistryChangeRequestSubmitted }, - { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, - ); - await db.notificationMessage.update( - { id: NotificationMessageType.StudentAppealSubmitted }, + { + id: In([ + NotificationMessageType.MinistryChangeRequestSubmitted, + NotificationMessageType.StudentAppealSubmitted, + ]), + }, { emailContacts: [MINISTRY_EMAIL_ADDRESS] }, ); }); @@ -91,15 +92,10 @@ describe("StudentAppealStudentsController(e2e)-submitApplicationAppeal", () => { await db.notification.update( { notificationMessage: { - id: NotificationMessageType.MinistryChangeRequestSubmitted, - }, - }, - { dateSent: new Date() }, - ); - await db.notification.update( - { - notificationMessage: { - id: NotificationMessageType.StudentAppealSubmitted, + id: In([ + NotificationMessageType.MinistryChangeRequestSubmitted, + NotificationMessageType.StudentAppealSubmitted, + ]), }, }, { dateSent: new Date() }, diff --git a/sources/packages/backend/apps/api/src/services/application/application.service.ts b/sources/packages/backend/apps/api/src/services/application/application.service.ts index 318cf91ff8..7fe1598bd3 100644 --- a/sources/packages/backend/apps/api/src/services/application/application.service.ts +++ b/sources/packages/backend/apps/api/src/services/application/application.service.ts @@ -595,7 +595,7 @@ export class ApplicationService extends RecordDataModelService { } /** - * Sends a ministry notification when a student submits a new program year change request. + * Sends a ministry notification when a student submits a change request (edit post-COE). * Loads the required student and application number within the provided * transaction to ensure data consistency. * @param studentId ID of the student who submitted the change request. diff --git a/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts b/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts index ce72dd0c0a..7a384c6dcb 100644 --- a/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts +++ b/sources/packages/backend/apps/api/src/services/dynamic-form-configuration/dynamic-form-configuration.service.ts @@ -110,18 +110,4 @@ export class DynamicFormConfigurationService { (dynamicFormConfiguration) => dynamicFormConfiguration.id === formId, ); } - - /** - * Get a form configuration by its form definition name. - * @param formDefinitionName form definition name. - * @returns dynamic form configuration for the requested form definition name. - */ - getFormByDefinitionName( - formDefinitionName: string, - ): DynamicFormConfiguration | undefined { - return this.dynamicFormConfigurations.find( - (dynamicFormConfiguration) => - dynamicFormConfiguration.formDefinitionName === formDefinitionName, - ); - } } diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts index cf746506cf..ed67ca9f2f 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -118,7 +118,7 @@ export class FormSubmissionSubmitService { { entityManager: entityManager }, ); } - // Send a notification to the ministry that a new form or appeal was submitted. + // Send a ministry notification when a new form submission has been created. await this.saveFormSubmissionNotification( studentId, applicationId, @@ -131,7 +131,7 @@ export class FormSubmissionSubmitService { } /** - * Sends a notification to the ministry when a new form or appeal has been submitted. + * Sends a ministry notification when a new form submission has been created. * Loads the required student and application data within the provided * transaction to ensure data consistency. * @param studentId ID of the student who submitted the form. @@ -148,49 +148,37 @@ export class FormSubmissionSubmitService { formCategory: FormCategory, entityManager: EntityManager, ): Promise { - // Load student info required for the notification. - const studentForNotification = await entityManager - .getRepository(Student) - .findOne({ - select: { - id: true, - birthDate: true, - user: { id: true, firstName: true, lastName: true, email: true }, - }, - relations: { user: true }, - where: { id: studentId }, - }); - if (!studentForNotification) { + // Load student and application data in a single query. + const student = await entityManager.getRepository(Student).findOneOrFail({ + select: { + id: true, + birthDate: true, + user: { id: true, firstName: true, lastName: true, email: true }, + applications: { id: true, applicationNumber: true }, + }, + relations: { user: true, applications: true }, + where: { + id: studentId, + applications: { id: applicationId }, + }, + }); + if (!student) { + throw new Error("Student not found while sending notification."); + } + if (applicationId && !student.applications.length) { throw new Error( - `Student ${studentId} not found while sending form submission notification.`, + "Application not found found while sending notification.", ); } - // Load the application number if a specific application is linked to the submission. - let applicationNumber: string | undefined; - if (applicationId) { - const application = await entityManager - .getRepository(Application) - .findOne({ - select: { id: true, applicationNumber: true }, - where: { id: applicationId }, - }); - if (!application) { - throw new Error( - `Application ${applicationId} not found while sending form submission notification.`, - ); - } - applicationNumber = application.applicationNumber; - } - await this.notificationActionsService.saveMinistryFormSubmittedNotification( { - givenNames: studentForNotification.user.firstName, - lastName: studentForNotification.user.lastName, - email: studentForNotification.user.email, - birthDate: studentForNotification.birthDate, + givenNames: student.user.firstName, + lastName: student.user.lastName, + email: student.user.email, + birthDate: student.birthDate, formCategory: formCategory, formNames: submissionConfigs.map((config) => config.formType), - applicationNumber, + applicationNumber: student.applications?.[0]?.applicationNumber, }, entityManager, ); diff --git a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts index f4609ecc50..06c3611718 100644 --- a/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-appeal/student-appeal.service.ts @@ -147,12 +147,10 @@ export class StudentAppealService extends RecordDataModelService applicationNumber: true, programYear: { id: true, programYear: true }, }, - appealRequests: { id: true, submittedFormName: true }, }, relations: { student: { user: true }, application: { programYear: true }, - appealRequests: true, }, where: { id: appealId }, loadEagerRelations: false, diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts index fe72c127ab..339a59a548 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification-actions.service.ts @@ -1566,7 +1566,7 @@ export class NotificationActionsService { } /** - * Creates a ministry notification when a student submits a form or appeal, + * Creates a ministry notification when a student submits a form submission, * using the form category directly from the dynamic form configuration. * @param notification notification details. * @param entityManager entity manager to execute in transaction. @@ -1613,9 +1613,9 @@ export class NotificationActionsService { } /** - * Creates a student notification when a form or appeal is completed. + * Creates a student notification when a form submission is completed. * @param notification notification details. - * @param auditUserId user who completed the form or appeal. + * @param auditUserId user who completed the form submission. * @param entityManager entity manager to execute in transaction. */ async saveStudentFormCompletedNotification( diff --git a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts index 26111a33d8..91bd43e128 100644 --- a/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts +++ b/sources/packages/backend/libs/services/src/notifications/notification/notification.model.ts @@ -259,9 +259,8 @@ export interface StudentCOERequiredNearEndDateNotification { } /** - * Ministry notification data when a student submits a form or appeal, - * with form type categorization (application appeal, other appeal, standard form), - * a comma-separated list of human-readable form names, and the related application number. + * Ministry notification data when a student submits a form submission, + * including the form category, human-readable form names, and the related application number if available. */ export interface MinistryFormSubmittedNotification { givenNames: string; diff --git a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts index b15851bd65..da102b890f 100644 --- a/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts +++ b/sources/packages/backend/libs/sims-db/src/entities/notification.model.ts @@ -246,11 +246,11 @@ export enum NotificationMessageType { */ StudentChangeRequestReviewCompleted = 37, /** - * Ministry notification when a student submits a form or appeal. + * Ministry notification when a student submits a form submission. */ MinistryFormSubmitted = 38, /** - * Student notification when a form or appeal is completed. + * Student notification when a form submission is completed. */ StudentFormCompleted = 39, }