diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.submitApplication.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.submitApplication.e2e-spec.ts index 13646683fd..453684097e 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.submitApplication.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.submitApplication.e2e-spec.ts @@ -533,6 +533,104 @@ describe("ApplicationStudentsController(e2e)-submitApplication", () => { }, ); + it( + "Should submit an application for a student when there is a cancelled part-time application in SFAS with overlapping study dates" + + " ignoring the cancelled SFAS application.", + async () => { + // Arrange + const student = await saveFakeStudent(db.dataSource); + const sfasIndividual = await saveFakeSFASIndividual(db.dataSource, { + initialValues: { + lastName: student.user.lastName, + birthDate: student.birthDate, + sin: student.sinValidation.sin, + }, + }); + // Cancelled SFAS full time application with overlapping study dates. + const sfasPartTimeApplication = createFakeSFASPartTimeApplication( + { individual: sfasIndividual }, + { + initialValues: { + startDate: getISODateOnlyString(new Date()), + endDate: getISODateOnlyString(addDays(50)), + // The SFAS application is cancelled. + applicationCancelDate: getISODateOnlyString(new Date()), + }, + }, + ); + await db.sfasPartTimeApplications.save(sfasPartTimeApplication); + + // SIMS Offering having overlapping study period with SFAS. + const simsApplicationOfferingInitialValues = { + studyStartDate: getISODateOnlyString(addDays(30)), + studyEndDate: getISODateOnlyString(addDays(90)), + offeringIntensity: OfferingIntensity.partTime, + }; + const simsApplication = createFakeApplication( + { + student, + }, + { + initialValue: { + data: {}, + applicationStatus: ApplicationStatus.Draft, + applicationStatusUpdatedOn: new Date(), + creator: systemUsersService.systemUser, + createdAt: new Date(), + } as Application, + }, + ); + const simsDraftApplication = await db.application.save(simsApplication); + const auditUser = await db.user.save(createFakeUser()); + const simsApplicationOffering = await db.educationProgramOffering.save( + createFakeEducationProgramOffering( + { + auditUser, + institutionLocation: simsApplication.location, + }, + { + initialValues: simsApplicationOfferingInitialValues, + }, + ), + ); + const secondApplicationProgram = simsApplicationOffering.educationProgram; + const applicationData = { + selectedOfferingDate: + simsApplicationOfferingInitialValues.studyStartDate, + selectedOfferingEndDate: + simsApplicationOfferingInitialValues.studyEndDate, + howWillYouBeAttendingTheProgram: + simsApplicationOfferingInitialValues.offeringIntensity, + selectedProgram: secondApplicationProgram.id, + selectedOffering: simsApplicationOffering.id, + selectedLocation: simsApplication.location.id, + }; + const payload = { + associatedFiles: [], + data: applicationData, + programYearId: simsApplication.programYear.id, + } as SaveApplicationAPIInDTO; + const endpoint = `/students/application/${simsDraftApplication.id}/submit`; + const token = await getStudentToken( + FakeStudentUsersTypes.FakeStudentUserType1, + ); + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.Application, + data: { data: applicationData }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + await mockUserLoginInfo(appModule, student); + // Act/Assert + await request(app.getHttpServer()) + .patch(endpoint) + .send(payload) + .auth(token, BEARER_AUTH_TYPE) + .expect(HttpStatus.OK) + .expect({}); + }, + ); + afterAll(async () => { await app?.close(); }); diff --git a/sources/packages/backend/apps/workers/src/controllers/assessment/_tests_/e2e/assessment.controller-verifyAssessmentCalculationOrder.e2e-spec.ts b/sources/packages/backend/apps/workers/src/controllers/assessment/_tests_/e2e/assessment.controller-verifyAssessmentCalculationOrder.e2e-spec.ts index 4186a74336..662d3ce806 100644 --- a/sources/packages/backend/apps/workers/src/controllers/assessment/_tests_/e2e/assessment.controller-verifyAssessmentCalculationOrder.e2e-spec.ts +++ b/sources/packages/backend/apps/workers/src/controllers/assessment/_tests_/e2e/assessment.controller-verifyAssessmentCalculationOrder.e2e-spec.ts @@ -825,6 +825,112 @@ describe("AssessmentController(e2e)-verifyAssessmentCalculationOrder", () => { }); }); + it("Should not sum the awards from SFAS and SFAS part-time applications data when the SFAS applications for the same student and program year are cancelled.", async () => { + // Arrange + + // Create the student to be shared across the applications. + const student = await saveFakeStudent(db.dataSource); + // Get the program year for the start date. + const programYear = await db.programYear.findOne({ where: { id: 2 } }); + + // Current application having the first assessment already processed. + const currentApplication = await saveFakeApplicationDisbursements( + db.dataSource, + { student }, + { + offeringIntensity: OfferingIntensity.partTime, + applicationStatus: ApplicationStatus.InProgress, + currentAssessmentInitialValues: { + assessmentWorkflowId: "some fake id", + studentAssessmentStatus: StudentAssessmentStatus.Completed, + assessmentDate: addDays(30, programYear.startDate), + }, + }, + ); + const firstAssessmentDate = + currentApplication.currentAssessment.assessmentDate; + // The start date for the first SFAS and SFAS part-time application record is set to the date before the first assessment date of the current application. + const legacyApplicationStartDate = faker.date.between( + programYear.startDate, + addDays(-1, firstAssessmentDate), + ); + const legacyApplicationEndDate = addDays(30, firstAssessmentDate); + + // Create the second assessment for the current application with a different assessment date. + const secondAssessment = createFakeStudentAssessment( + { + auditUser: currentApplication.student.user, + application: currentApplication, + offering: currentApplication.currentAssessment.offering, + }, + { + initialValue: { + assessmentWorkflowId: "some fake id", + studentAssessmentStatus: StudentAssessmentStatus.InProgress, + }, + }, + ); + currentApplication.currentAssessment = secondAssessment; + await db.application.save(currentApplication); + + // SFAS Individual. + const sfasIndividual = await saveFakeSFASIndividual(db.dataSource, { + initialValues: { + lastName: student.user.lastName, + birthDate: student.birthDate, + sin: student.sinValidation.sin, + }, + }); + + const fakeSFASApplication = createFakeSFASApplication( + { individual: sfasIndividual }, + { + initialValues: { + startDate: getISODateOnlyString(legacyApplicationStartDate), + endDate: getISODateOnlyString(legacyApplicationEndDate), + csgdAward: 9, + csgpAward: 10, + sbsdAward: 12, + bcagAward: 13, + applicationCancelDate: getISODateOnlyString(new Date()), + }, + }, + ); + await db.sfasApplication.save(fakeSFASApplication); + + const fakeSFASPartTimeApplication = createFakeSFASPartTimeApplication( + { individual: sfasIndividual }, + { + initialValues: { + startDate: getISODateOnlyString(legacyApplicationStartDate), + endDate: getISODateOnlyString(legacyApplicationEndDate), + csptAward: 2, + csgdAward: 3, + csgpAward: 4, + sbsdAward: 6, + bcagAward: 7, + applicationCancelDate: getISODateOnlyString(new Date()), + }, + }, + ); + await db.sfasPartTimeApplications.save(fakeSFASPartTimeApplication); + // Act + const result = await assessmentController.verifyAssessmentCalculationOrder( + createFakeVerifyAssessmentCalculationOrderPayload( + currentApplication.currentAssessment.id, + ), + ); + // Assert + expect(FakeWorkerJobResult.getResultType(result)).toBe( + MockedZeebeJobResult.Complete, + ); + // The calculation will not take cancelled SFAS and SFAS part-time application data. + expect(FakeWorkerJobResult.getOutputVariables(result)).toStrictEqual({ + isReadyForCalculation: true, + latestCSLPBalance: 0, + }); + }); + it("Should not return any program year total awards or grants from awards from SFAS and SFAS part-time applications when there are no SIMS applications in the past and SFAS and SFAS part-time applications for the same student and program year.", async () => { // Arrange diff --git a/sources/packages/backend/apps/workers/src/controllers/disbursement/_tests_/e2e/disbursement.controller.associateMSFAA.e2e-spec.ts b/sources/packages/backend/apps/workers/src/controllers/disbursement/_tests_/e2e/disbursement.controller.associateMSFAA.e2e-spec.ts index 60446c0578..bfc6e99cb8 100644 --- a/sources/packages/backend/apps/workers/src/controllers/disbursement/_tests_/e2e/disbursement.controller.associateMSFAA.e2e-spec.ts +++ b/sources/packages/backend/apps/workers/src/controllers/disbursement/_tests_/e2e/disbursement.controller.associateMSFAA.e2e-spec.ts @@ -449,6 +449,69 @@ describe("DisbursementController(e2e)-associateMSFAA", () => { expect(cancelledMSFAANumber.cancelledDate).not.toBe(null); }); + it("Should not reuse the MSFAA number from SFAS full-time cancelled applications in SIMS when there is a MSFAA for a cancelled SFAS application.", async () => { + // Arrange + const legacyApplicationStartDate = addDays(-100); + const legacyApplicationEndDate = addDays(-10); + + // Create student and save fake application with disbursements. + const student = await saveFakeStudent(db.dataSource); + const application = await saveFakeApplicationDisbursements( + db.dataSource, + { student }, + { + offeringIntensity: OfferingIntensity.fullTime, + applicationStatus: ApplicationStatus.InProgress, + }, + ); + + const savedSFASIndividual = await saveFakeSFASIndividual(db.dataSource, { + initialValues: { student: student }, + }); + await db.sfasApplication.save( + createFakeSFASApplication( + { individual: savedSFASIndividual }, + { + initialValues: { + startDate: getISODateOnlyString(legacyApplicationStartDate), + endDate: getISODateOnlyString(legacyApplicationEndDate), + applicationCancelDate: getISODateOnlyString(new Date()), + }, + }, + ), + ); + + // Act + const associateMSFAAPayload = createFakeAssociateMSFAAPayload({ + assessmentId: application.currentAssessment.id, + }); + const saveResult = await disbursementController.associateMSFAA( + associateMSFAAPayload, + ); + + // Asserts + expect(FakeWorkerJobResult.getResultType(saveResult)).toBe( + MockedZeebeJobResult.Complete, + ); + + // Fetch MSFAA Number for the student in SIMS. + const createdMSFAANumber = await db.msfaaNumber.findOne({ + select: { + id: true, + msfaaNumber: true, + }, + where: { + student: { id: student.id }, + }, + order: { + id: "DESC", + }, + }); + expect(createdMSFAANumber.msfaaNumber).not.toEqual( + savedSFASIndividual.msfaaNumber, + ); + }); + it("Should create new MSFAA Number for full-time applications by creating and activating in SIMS when MSFAA is not found or invalid SFAS application offering end date.", async () => { // Arrange const firstLegacyApplicationStartDate = addDays(-MAX_MSFAA_VALID_DAYS + 10); @@ -617,6 +680,69 @@ describe("DisbursementController(e2e)-associateMSFAA", () => { expect(secondDisbursementSchedule.msfaaNumber.id).toBe(savedMSFAANumber.id); }); + it("Should not reuse the MSFAA number from SFAS part-time cancelled applications in SIMS when there is a MSFAA for a cancelled SFAS application.", async () => { + // Arrange + const legacyApplicationStartDate = addDays(-100); + const legacyApplicationEndDate = addDays(-10); + + // Create student and save fake application with disbursements. + const student = await saveFakeStudent(db.dataSource); + const application = await saveFakeApplicationDisbursements( + db.dataSource, + { student }, + { + offeringIntensity: OfferingIntensity.partTime, + applicationStatus: ApplicationStatus.InProgress, + }, + ); + + const savedSFASIndividual = await saveFakeSFASIndividual(db.dataSource, { + initialValues: { student: student }, + }); + await db.sfasPartTimeApplications.save( + createFakeSFASPartTimeApplication( + { individual: savedSFASIndividual }, + { + initialValues: { + startDate: getISODateOnlyString(legacyApplicationStartDate), + endDate: getISODateOnlyString(legacyApplicationEndDate), + applicationCancelDate: getISODateOnlyString(new Date()), + }, + }, + ), + ); + + // Act + const associateMSFAAPayload = createFakeAssociateMSFAAPayload({ + assessmentId: application.currentAssessment.id, + }); + const saveResult = await disbursementController.associateMSFAA( + associateMSFAAPayload, + ); + + // Asserts + expect(FakeWorkerJobResult.getResultType(saveResult)).toBe( + MockedZeebeJobResult.Complete, + ); + + // Fetch MSFAA Number for the student in SIMS. + const createdMSFAANumber = await db.msfaaNumber.findOne({ + select: { + id: true, + msfaaNumber: true, + }, + where: { + student: { id: student.id }, + }, + order: { + id: "DESC", + }, + }); + expect(createdMSFAANumber.msfaaNumber).not.toEqual( + savedSFASIndividual.msfaaNumber, + ); + }); + it("Should create new MSFAA number for part-time application when MSFAA is found for previously signed disbursement and its offering end date is not between the valid date.", async () => { // Arrange const student = await saveFakeStudent(db.dataSource); diff --git a/sources/packages/backend/libs/services/src/sfas/sfas-application.service.ts b/sources/packages/backend/libs/services/src/sfas/sfas-application.service.ts index 5a0ae1c197..c491d91cba 100644 --- a/sources/packages/backend/libs/services/src/sfas/sfas-application.service.ts +++ b/sources/packages/backend/libs/services/src/sfas/sfas-application.service.ts @@ -109,6 +109,7 @@ export class SFASApplicationService extends DataModelService { individual: true, }, where: { + applicationCancelDate: IsNull(), individual: { student: { id: studentId }, msfaaNumber: Not(IsNull()) }, endDate: MoreThanOrEqual(getISODateOnlyString(minMSFAAValidDate)), }, diff --git a/sources/packages/backend/libs/services/src/sfas/sfas-part-time-application.service.ts b/sources/packages/backend/libs/services/src/sfas/sfas-part-time-application.service.ts index 5e9366b903..070e1cbc0e 100644 --- a/sources/packages/backend/libs/services/src/sfas/sfas-part-time-application.service.ts +++ b/sources/packages/backend/libs/services/src/sfas/sfas-part-time-application.service.ts @@ -38,7 +38,10 @@ export class SFASPartTimeApplicationsService extends DataModelService= :startDate", { diff --git a/sources/packages/backend/libs/test-utils/src/factories/sfas-part-time-application.ts b/sources/packages/backend/libs/test-utils/src/factories/sfas-part-time-application.ts index 48e8e7c5d4..5f3eac51b4 100644 --- a/sources/packages/backend/libs/test-utils/src/factories/sfas-part-time-application.ts +++ b/sources/packages/backend/libs/test-utils/src/factories/sfas-part-time-application.ts @@ -42,5 +42,7 @@ export function createFakeSFASPartTimeApplication( sfasPartTimeApplication.createdAt = faker.date.past(18); sfasPartTimeApplication.updatedAt = faker.date.past(18); sfasPartTimeApplication.extractedAt = faker.date.past(18); + sfasPartTimeApplication.applicationCancelDate = + options?.initialValues?.applicationCancelDate; return sfasPartTimeApplication; }