diff --git a/sources/packages/backend/apps/api/src/route-controllers/report/_tests_/e2e/report.aest.controller.exportReport.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/report/_tests_/e2e/report.aest.controller.exportReport.e2e-spec.ts index 0a329cfac4..41c4e938b1 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/report/_tests_/e2e/report.aest.controller.exportReport.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/report/_tests_/e2e/report.aest.controller.exportReport.e2e-spec.ts @@ -11,6 +11,7 @@ import { saveFakeApplicationDisbursements, saveFakeCASSupplier, saveFakeDesignationAgreementLocation, + saveFakeDisbursementReceiptsFromDisbursementSchedule, saveFakeStudent, } from "@sims/test-utils"; import { @@ -46,7 +47,10 @@ import { import { DataSource } from "typeorm"; import { createFakeEducationProgram } from "@sims/test-utils/factories/education-program"; import { createFakeSINValidation } from "@sims/test-utils/factories/sin-validation"; -import { MinistryReportsFilterAPIInDTO } from "../../models/report.dto"; +import { + MinistryReportNames, + MinistryReportsFilterAPIInDTO, +} from "../../models/report.dto"; import { buildUnmetNeedReportData, createApplicationsDataSetup, @@ -1340,6 +1344,148 @@ describe("ReportAestController(e2e)-exportReport", () => { }); }); + it("Should generate the Disbursement report when a report generation request is made for full time offering intensity and date range.", async () => { + // Arrange + // Use a unique historical date to avoid conflicts with other tests. + const disburseDate = "2025-01-15"; + + const bcGrantBCAG = createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BCAG", + 300, + ); + + const canadaLoanCSL = createFakeDisbursementValue( + DisbursementValueType.CanadaLoan, + "CSL", + 1000, + ); + + const bcLoanBCSL = createFakeDisbursementValue( + DisbursementValueType.BCLoan, + "BCSL", + 500, + ); + + const student = await saveFakeStudent(appDataSource); + // Full time application with disbursement values. + const application = await saveFakeApplicationDisbursements( + appDataSource, + { + firstDisbursementValues: [bcLoanBCSL, canadaLoanCSL, bcGrantBCAG], + student: student, + }, + { + applicationStatus: ApplicationStatus.Completed, + offeringIntensity: OfferingIntensity.fullTime, + firstDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Sent, + disbursementDate: disburseDate, + }, + }, + ); + + // Part time application with disbursement values. + // This data is not used, its only created to ensure filtering works. + const partTimeApplication = await saveFakeApplicationDisbursements( + appDataSource, + { + firstDisbursementValues: [bcLoanBCSL, canadaLoanCSL, bcGrantBCAG], + }, + { + applicationStatus: ApplicationStatus.Completed, + offeringIntensity: OfferingIntensity.partTime, + firstDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Sent, + disbursementDate: disburseDate, + }, + }, + ); + // Create disbursement receipts for the part-time application using the utility method. + await saveFakeDisbursementReceiptsFromDisbursementSchedule( + db, + partTimeApplication.currentAssessment.disbursementSchedules[0], + ); + + // Create disbursement receipts for the report using the utility method. + const [firstDisbursement] = + application.currentAssessment.disbursementSchedules; + const { federal, provincial } = + await saveFakeDisbursementReceiptsFromDisbursementSchedule( + db, + firstDisbursement, + ); + + // Update receipt dates to match the unique test date to ensure proper filtering. + federal.disburseDate = disburseDate; + federal.batchRunDate = disburseDate; + federal.fileDate = disburseDate; + provincial.disburseDate = disburseDate; + provincial.batchRunDate = disburseDate; + provincial.fileDate = disburseDate; + await db.disbursementReceipt.save([federal, provincial]); + + const payload = { + reportName: MinistryReportNames.Disbursements, + params: { + startDate: disburseDate, + endDate: getISODateOnlyString(addDays(1, disburseDate)), + offeringIntensity: { + "Full Time": true, // Filter to include only full time disbursements. + "Part Time": false, + }, + }, + }; + + // Mock the formio service dry run submission to return the payload. + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.ExportFinancialReports, + data: { data: payload }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + + const endpoint = "/aest/report"; + const ministryUserToken = await getAESTToken( + AESTGroups.BusinessAdministrators, + ); + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(payload) + .auth(ministryUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + const fileContent = response.request.res["text"]; + const parsedResult = parse(fileContent, { + header: true, + }); + expect(parsedResult.data).toStrictEqual([ + { + "Forecast Date": disburseDate, + "Date of Disbursement": disburseDate, + SIN: student.sinValidation.sin, + "Application Number": application.applicationNumber, + "Certificate Number": firstDisbursement.documentNumber.toString(), + "Funding Code": bcLoanBCSL.valueCode, + "Disbursement Amount": bcLoanBCSL.valueAmount.toFixed(2), + }, + { + "Forecast Date": disburseDate, + "Date of Disbursement": disburseDate, + SIN: student.sinValidation.sin, + "Application Number": application.applicationNumber, + "Certificate Number": firstDisbursement.documentNumber.toString(), + "Funding Code": canadaLoanCSL.valueCode, + "Disbursement Amount": canadaLoanCSL.valueAmount.toFixed(2), + }, + ]); + }); + }); + it( "Should generate CAS Supplier maintenance updates report with the student details of the given student" + " when last name of the student is updated after the CAS supplier is set to be valid.", diff --git a/sources/packages/backend/apps/api/src/route-controllers/report/models/report.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/report/models/report.dto.ts index 8a672ddd92..628565e07f 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/report/models/report.dto.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/report/models/report.dto.ts @@ -15,7 +15,7 @@ enum InstitutionReportNames { COERequests = "COE_Requests", } -enum MinistryReportNames { +export enum MinistryReportNames { ForecastDisbursements = "Disbursement_Forecast_Report", Disbursements = "Disbursement_Report", DisbursementsWithoutValidSupplier = "Disbursements_Without_Valid_Supplier_Report", diff --git a/sources/packages/backend/apps/db-migrations/src/migrations/1767653404966-UpdateDisbursementReportForecastDate.ts b/sources/packages/backend/apps/db-migrations/src/migrations/1767653404966-UpdateDisbursementReportForecastDate.ts new file mode 100644 index 0000000000..ac707ec22d --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/migrations/1767653404966-UpdateDisbursementReportForecastDate.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { getSQLFileData } from "../utilities/sqlLoader"; + +/** + * Update the disbursement reports to include the "Forecast Date" column. + */ +export class UpdateDisbursementReportForecastDate1767653404966 implements MigrationInterface { + /** + * Update the disbursement reports to include the "Forecast Date" column. + * @param queryRunner the query runner. + */ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData("Update-disbursement-report-forecast-date.sql", "Reports"), + ); + } + + /** + * Rollback the disbursement reports update that included the "Forecast Date" column. + * @param queryRunner the query runner. + */ + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Rollback-update-disbursement-report-forecast-date.sql", + "Reports", + ), + ); + } +} diff --git a/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-update-disbursement-report-forecast-date.sql b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-update-disbursement-report-forecast-date.sql new file mode 100644 index 0000000000..13dec7fe09 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-update-disbursement-report-forecast-date.sql @@ -0,0 +1,75 @@ +-- Rollback the disbursement report SQL to the previous version before the forecast date update. +UPDATE + sims.report_configs +SET + report_sql = $$ ( + SELECT + to_char(dr.disburse_date, 'YYYY-MM-DD') AS "Date of Disbursement", + dr.student_sin AS "SIN", + app.application_number AS "Application Number", + ds.document_number AS "Certificate Number", + drv.grant_type AS "Funding Code", + drv.grant_amount AS "Disbursement Amount" + FROM + sims.disbursement_receipts dr + INNER JOIN sims.disbursement_receipt_values drv ON drv.disbursement_receipt_id = dr.id + INNER JOIN sims.disbursement_schedules ds ON ds.id = dr.disbursement_schedule_id + INNER JOIN sims.student_assessments sa ON sa.id = ds.student_assessment_id + INNER JOIN sims.applications app ON app.id = sa.application_id + INNER JOIN sims.education_programs_offerings epo ON epo.id = sa.offering_id + WHERE + epo.offering_intensity = ANY(:offeringIntensity) + AND dr.disburse_date BETWEEN :startDate + AND :endDate + UNION + ALL + SELECT + to_char(dr.disburse_date, 'YYYY-MM-DD') AS "Date of Disbursement", + dr.student_sin AS "SIN", + app.application_number AS "Application Number", + ds.document_number AS "Certificate Number", + dv.value_code AS "Funding Code", + dv.effective_amount AS "Disbursement Amount" + FROM + sims.disbursement_receipts dr + INNER JOIN sims.disbursement_receipt_values drv ON drv.disbursement_receipt_id = dr.id + INNER JOIN sims.disbursement_schedules ds ON ds.id = dr.disbursement_schedule_id + INNER JOIN sims.disbursement_values dv ON dv.disbursement_schedule_id = ds.id + INNER JOIN sims.student_assessments sa ON sa.id = ds.student_assessment_id + INNER JOIN sims.applications app ON app.id = sa.application_id + INNER JOIN sims.education_programs_offerings epo ON epo.id = sa.offering_id + WHERE + epo.offering_intensity = ANY(:offeringIntensity) + AND dr.funding_type <> 'FE' + AND drv.grant_type = 'BCSG' + AND dv.value_type = 'BC Grant' + AND dr.disburse_date BETWEEN :startDate + AND :endDate + UNION + ALL + SELECT + to_char(dr.disburse_date, 'YYYY-MM-DD') AS "Date of Disbursement", + dr.student_sin AS "SIN", + app.application_number AS "Application Number", + ds.document_number AS "Certificate Number", + CASE + WHEN dr.funding_type = 'BC' THEN 'BCSL' + WHEN dr.funding_type = 'FE' THEN 'CSL' + END AS "Funding Code", + dr.total_disbursed_amount AS "Disbursement Amount" + FROM + sims.disbursement_receipts dr + INNER JOIN sims.disbursement_schedules ds ON ds.id = dr.disbursement_schedule_id + INNER JOIN sims.student_assessments sa ON sa.id = ds.student_assessment_id + INNER JOIN sims.applications app ON app.id = sa.application_id + INNER JOIN sims.education_programs_offerings epo ON epo.id = sa.offering_id + WHERE + epo.offering_intensity = ANY(:offeringIntensity) + AND dr.disburse_date BETWEEN :startDate + AND :endDate + ) +ORDER BY + "Date of Disbursement", + "Certificate Number" $$ +WHERE + report_name = 'Disbursement_Report'; \ No newline at end of file diff --git a/sources/packages/backend/apps/db-migrations/src/sql/Reports/Update-disbursement-report-forecast-date.sql b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Update-disbursement-report-forecast-date.sql new file mode 100644 index 0000000000..4c022f07de --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Update-disbursement-report-forecast-date.sql @@ -0,0 +1,79 @@ +-- Update the Disbursements Report to include the "Forecast Date" column. +UPDATE + sims.report_configs +SET + report_sql = $$ ( + SELECT + to_char(ds.disbursement_date, 'YYYY-MM-DD') AS "Forecast Date", + to_char(dr.disburse_date, 'YYYY-MM-DD') AS "Date of Disbursement", + dr.student_sin AS "SIN", + app.application_number AS "Application Number", + ds.document_number AS "Certificate Number", + drv.grant_type AS "Funding Code", + drv.grant_amount AS "Disbursement Amount" + FROM + sims.disbursement_receipts dr + INNER JOIN sims.disbursement_receipt_values drv ON drv.disbursement_receipt_id = dr.id + INNER JOIN sims.disbursement_schedules ds ON ds.id = dr.disbursement_schedule_id + INNER JOIN sims.student_assessments sa ON sa.id = ds.student_assessment_id + INNER JOIN sims.applications app ON app.id = sa.application_id + INNER JOIN sims.education_programs_offerings epo ON epo.id = sa.offering_id + WHERE + epo.offering_intensity = ANY(:offeringIntensity) + AND dr.disburse_date BETWEEN :startDate + AND :endDate + UNION + ALL + SELECT + to_char(ds.disbursement_date, 'YYYY-MM-DD') AS "Forecast Date", + to_char(dr.disburse_date, 'YYYY-MM-DD') AS "Date of Disbursement", + dr.student_sin AS "SIN", + app.application_number AS "Application Number", + ds.document_number AS "Certificate Number", + dv.value_code AS "Funding Code", + dv.effective_amount AS "Disbursement Amount" + FROM + sims.disbursement_receipts dr + INNER JOIN sims.disbursement_receipt_values drv ON drv.disbursement_receipt_id = dr.id + INNER JOIN sims.disbursement_schedules ds ON ds.id = dr.disbursement_schedule_id + INNER JOIN sims.disbursement_values dv ON dv.disbursement_schedule_id = ds.id + INNER JOIN sims.student_assessments sa ON sa.id = ds.student_assessment_id + INNER JOIN sims.applications app ON app.id = sa.application_id + INNER JOIN sims.education_programs_offerings epo ON epo.id = sa.offering_id + WHERE + epo.offering_intensity = ANY(:offeringIntensity) + AND dr.funding_type <> 'FE' + AND drv.grant_type = 'BCSG' + AND dv.value_type = 'BC Grant' + AND dr.disburse_date BETWEEN :startDate + AND :endDate + UNION + ALL + SELECT + to_char(ds.disbursement_date, 'YYYY-MM-DD') AS "Forecast Date", + to_char(dr.disburse_date, 'YYYY-MM-DD') AS "Date of Disbursement", + dr.student_sin AS "SIN", + app.application_number AS "Application Number", + ds.document_number AS "Certificate Number", + CASE + WHEN dr.funding_type = 'BC' THEN 'BCSL' + WHEN dr.funding_type = 'FE' THEN 'CSL' + END AS "Funding Code", + dr.total_disbursed_amount AS "Disbursement Amount" + FROM + sims.disbursement_receipts dr + INNER JOIN sims.disbursement_schedules ds ON ds.id = dr.disbursement_schedule_id + INNER JOIN sims.student_assessments sa ON sa.id = ds.student_assessment_id + INNER JOIN sims.applications app ON app.id = sa.application_id + INNER JOIN sims.education_programs_offerings epo ON epo.id = sa.offering_id + WHERE + epo.offering_intensity = ANY(:offeringIntensity) + AND dr.disburse_date BETWEEN :startDate + AND :endDate + ) +ORDER BY + "Date of Disbursement", + "Certificate Number", + "Funding Code" $$ +WHERE + report_name = 'Disbursement_Report'; \ No newline at end of file