diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-scholastic-standings/_tests_/e2e/student-scholastic-standings.institutions.controller.saveScholasticStanding.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-scholastic-standings/_tests_/e2e/student-scholastic-standings.institutions.controller.saveScholasticStanding.e2e-spec.ts index bb7854b007..2851273cc5 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/student-scholastic-standings/_tests_/e2e/student-scholastic-standings.institutions.controller.saveScholasticStanding.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/student-scholastic-standings/_tests_/e2e/student-scholastic-standings.institutions.controller.saveScholasticStanding.e2e-spec.ts @@ -1,6 +1,7 @@ import { HttpStatus, INestApplication } from "@nestjs/common"; import { E2EDataSources, + RestrictionCode, createE2EDataSources, createFakeInstitutionLocation, createFakeStudentAppeal, @@ -20,6 +21,8 @@ import { ApplicationStatus, AssessmentTriggerType, InstitutionLocation, + NotificationMessageType, + OfferingIntensity, StudentScholasticStandingChangeType, } from "@sims/sims-db"; import { @@ -250,18 +253,112 @@ describe("StudentScholasticStandingsInstitutionsController(e2e)-saveScholasticSt ).toBe(createdScholasticStandingId); }); + it("Should create a new scholastic standing for a part-time application when the institution user requests.", async () => { + // Arrange + mockFormioDryRun({ + studentScholasticStandingChangeType: + StudentScholasticStandingChangeType.StudentDidNotCompleteProgram, + }); + const application = await saveFakeApplication( + db.dataSource, + { + institutionLocation: collegeFLocation, + }, + { + offeringIntensity: OfferingIntensity.partTime, + applicationStatus: ApplicationStatus.Completed, + }, + ); + // Institution token. + const institutionUserToken = await getInstitutionToken( + InstitutionTokenTypes.CollegeFUser, + ); + const endpoint = `/institutions/scholastic-standing/location/${collegeFLocation.id}/application/${application.id}`; + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(payload) + .auth(institutionUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .expect((response) => { + expect(response.body.id).toBeGreaterThan(0); + }); + const restriction = await db.studentRestriction.find({ + select: { + id: true, + restriction: { + id: true, + restrictionCode: true, + }, + }, + relations: { + restriction: true, + }, + where: { application: { id: application.id } }, + }); + expect(restriction).toEqual([ + { + id: expect.any(Number), + restriction: { + id: expect.any(Number), + restrictionCode: RestrictionCode.PTSSR, + }, + }, + { + id: expect.any(Number), + restriction: { + id: expect.any(Number), + restrictionCode: RestrictionCode.PTWTHD, + }, + }, + ]); + const notifications = await db.notification.find({ + select: { + id: true, + user: { id: true }, + notificationMessage: { id: true }, + }, + relations: { user: true, notificationMessage: true }, + where: { user: { id: application.student.user.id } }, + order: { notificationMessage: { id: "ASC" } }, + }); + expect(notifications).toEqual([ + { + id: expect.any(Number), + notificationMessage: { + id: NotificationMessageType.StudentRestrictionAdded, + }, + user: { id: application.student.user.id }, + }, + { + id: expect.any(Number), + notificationMessage: { + id: NotificationMessageType.InstitutionReportsChange, + }, + user: { id: application.student.user.id }, + }, + ]); + }); + /** * Centralized method to handle the form.io mock. * @param options method options: * - `validDryRun`: boolean false indicates that the form mock resolved value is invalid. Default value is true. + * - `studentScholasticStandingChangeType`: Student scholastic standing change type to be added in combination with SchoolTransfer scholastic standing type . */ - function mockFormioDryRun(options?: { validDryRun?: boolean }): void { + function mockFormioDryRun(options?: { + validDryRun?: boolean; + studentScholasticStandingChangeType?: StudentScholasticStandingChangeType; + }): void { const validDryRun = options?.validDryRun ?? true; payload = { data: { dateOfChange: getISODateOnlyString(new Date()), scholasticStandingChangeType: - StudentScholasticStandingChangeType.SchoolTransfer, + options?.studentScholasticStandingChangeType + ? options.studentScholasticStandingChangeType + : StudentScholasticStandingChangeType.SchoolTransfer, }, }; formService.dryRunSubmission = jest.fn().mockResolvedValue({ diff --git a/sources/packages/backend/apps/api/src/services/student-scholastic-standings/student-scholastic-standings.service.ts b/sources/packages/backend/apps/api/src/services/student-scholastic-standings/student-scholastic-standings.service.ts index 0448a903eb..1f5ffa3d2f 100644 --- a/sources/packages/backend/apps/api/src/services/student-scholastic-standings/student-scholastic-standings.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-scholastic-standings/student-scholastic-standings.service.ts @@ -26,7 +26,10 @@ import { } from "./student-scholastic-standings.models"; import { StudentRestrictionService } from "../restriction/student-restriction.service"; import { APPLICATION_CHANGE_NOT_ELIGIBLE } from "../../constants"; -import { SCHOLASTIC_STANDING_MINIMUM_UNSUCCESSFUL_WEEKS } from "../../utilities"; +import { + PART_TIME_SCHOLASTIC_STANDING_RESTRICTIONS, + SCHOLASTIC_STANDING_MINIMUM_UNSUCCESSFUL_WEEKS, +} from "../../utilities"; import { NotificationActionsService, RestrictionCode, @@ -157,19 +160,19 @@ export class StudentScholasticStandingsService extends RecordDataModelService createdRestriction.id, + ); await this.studentRestrictionSharedService.createNotifications( - [createdRestriction.id], + restrictionIds, auditUserId, transactionalEntityManager, ); @@ -324,7 +330,7 @@ export class StudentScholasticStandingsService extends RecordDataModelService { + ): Promise { if (offeringIntensity === OfferingIntensity.fullTime) { - return this.getFullTimeStudentRestrictions( + const fullTimeRestriction = await this.getFullTimeStudentRestrictions( scholasticStandingData, studentId, auditUserId, applicationId, ); + return fullTimeRestriction ? [fullTimeRestriction] : []; } if (offeringIntensity === OfferingIntensity.partTime) { return this.getPartTimeStudentRestrictions( @@ -442,27 +449,35 @@ export class StudentScholasticStandingsService extends RecordDataModelService { + ): Promise { + const studentRestriction: StudentRestriction[] = []; if ( [ StudentScholasticStandingChangeType.StudentDidNotCompleteProgram, StudentScholasticStandingChangeType.StudentWithdrewFromProgram, ].includes(scholasticStandingData.scholasticStandingChangeType) ) { - return this.studentRestrictionSharedService.createRestrictionToSave( - studentId, - RestrictionCode.PTSSR, - auditUserId, - applicationId, - ); + // Create an array to hold the restriction promises. + const restrictionPromises = + PART_TIME_SCHOLASTIC_STANDING_RESTRICTIONS.map((restrictionCode) => + this.studentRestrictionSharedService.createRestrictionToSave( + studentId, + restrictionCode, + auditUserId, + applicationId, + ), + ); + const restrictions = await Promise.all(restrictionPromises); + studentRestriction.push(...restrictions); } + return studentRestriction; } /** diff --git a/sources/packages/backend/apps/api/src/utilities/system-configurations-constants.ts b/sources/packages/backend/apps/api/src/utilities/system-configurations-constants.ts index 3e8afe3c17..182118b04e 100644 --- a/sources/packages/backend/apps/api/src/utilities/system-configurations-constants.ts +++ b/sources/packages/backend/apps/api/src/utilities/system-configurations-constants.ts @@ -1,3 +1,5 @@ +import { RestrictionCode } from "@sims/services"; + export const PIR_DENIED_REASON_OTHER_ID = 1; // Timeout to handle the worst-case scenario where the commit/rollback // was not executed due to a possible catastrophic failure. @@ -110,3 +112,11 @@ export const OFFERING_STUDY_PERIOD_MIN_FUNDED_WEEKS_FULL_TIME = 12; * Minimum amount of funded weeks required for a part time offering study period. */ export const OFFERING_STUDY_PERIOD_MIN_FUNDED_WEEKS_PART_TIME = 6; + +/** + * Part time scholastic standing restrictions. + */ +export const PART_TIME_SCHOLASTIC_STANDING_RESTRICTIONS: RestrictionCode[] = [ + RestrictionCode.PTSSR, + RestrictionCode.PTWTHD, +]; diff --git a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts index 7343696a53..e28bfcf476 100644 --- a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts +++ b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts @@ -22,6 +22,11 @@ export enum RestrictionCode { * for a PT course application, "PTSSR" restriction is added to the student account. */ PTSSR = "PTSSR", + /** + * When an institution report withdrawal or unsuccessful weeks + * for a PT course application, "PTWTHD" restriction is added to the student account. + */ + PTWTHD = "PTWTHD", /** * When a student has a temporary SIN and applies for a full-time/part-time application * this restriction is applied case the SIN expiry date is before the offering end date. diff --git a/sources/packages/backend/libs/test-utils/src/models/common.model.ts b/sources/packages/backend/libs/test-utils/src/models/common.model.ts index 4dc90ca6a7..677dcacf44 100644 --- a/sources/packages/backend/libs/test-utils/src/models/common.model.ts +++ b/sources/packages/backend/libs/test-utils/src/models/common.model.ts @@ -77,6 +77,11 @@ export enum RestrictionCode { */ AF = "AF", /** - * Verification restriction on file. Set up call back for Student case review verification team. + * Part-time scholastic standing restrictions - Student withdrew or was unsuccesful from Part Time studies. */ + PTWTHD = "PTWTHD", + /** + * Part-time scholastic standing restrictions - Not eligible for part time funding due to scholastic standing must self fund or appeal. + */ + PTSSR = "PTSSR", }