diff --git a/sources/packages/backend/apps/api/src/route-controllers/cas-supplier/_tests_/cas-supplier.aest.controller.getCASSupplier.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/cas-supplier/_tests_/cas-supplier.aest.controller.getCASSupplier.e2e-spec.ts index ac40c2f91e..fe846fc86c 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/cas-supplier/_tests_/cas-supplier.aest.controller.getCASSupplier.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/cas-supplier/_tests_/cas-supplier.aest.controller.getCASSupplier.e2e-spec.ts @@ -30,7 +30,7 @@ describe("CASSupplierAESTController(e2e)-getCASSuppliers", () => { const savedCASSupplier2 = await saveFakeCASSupplier( db, { student }, - { supplierStatus: SupplierStatus.VerifiedManually }, + { initialValues: { supplierStatus: SupplierStatus.VerifiedManually } }, ); const endpoint = `/aest/cas-supplier/student/${student.id}`; 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 93be58bf8b..763200e55c 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 @@ -3,11 +3,13 @@ import { E2EDataSources, createE2EDataSources, createFakeDisbursementFeedbackError, + createFakeDisbursementValue, createFakeEducationProgramOffering, createFakeInstitutionLocation, createFakeUser, getProviderInstanceForModule, saveFakeApplicationDisbursements, + saveFakeCASSupplier, saveFakeDesignationAgreementLocation, saveFakeStudent, } from "@sims/test-utils"; @@ -28,9 +30,11 @@ import { Assessment, COEStatus, DisbursementScheduleStatus, + DisbursementValueType, EducationProgram, EducationProgramOffering, FullTimeAssessment, + IdentityProviders, InstitutionLocation, OfferingIntensity, ProgramIntensity, @@ -39,6 +43,7 @@ import { import { addDays, getISODateOnlyString } from "@sims/utilities"; import { DataSource } from "typeorm"; import { createFakeEducationProgram } from "@sims/test-utils/factories/education-program"; +import { createFakeSINValidation } from "@sims/test-utils/factories/sin-validation"; describe("ReportAestController(e2e)-exportReport", () => { let app: INestApplication; @@ -1197,6 +1202,230 @@ describe("ReportAestController(e2e)-exportReport", () => { }); }); + it("Should generate the Disbursements Without Valid Supplier report when a report generation request is made with the appropriate date range.", async () => { + // Arrange + const application1 = await saveFakeApplicationDisbursements( + appDataSource, + { + firstDisbursementValues: [ + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BCAG", + 500, + { + effectiveAmount: 500, + }, + ), + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "SBSD", + 300, + { + effectiveAmount: 298, + }, + ), + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BGPD", + 200, + { + effectiveAmount: 198, + }, + ), + ], + secondDisbursementValues: [ + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BCAG", + 500, + { + effectiveAmount: 499, + }, + ), + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "SBSD", + 300, + { + effectiveAmount: 299, + }, + ), + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BGPD", + 200, + { + effectiveAmount: 199, + }, + ), + ], + }, + { + createSecondDisbursement: true, + applicationStatus: ApplicationStatus.Completed, + offeringIntensity: OfferingIntensity.partTime, + firstDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Sent, + dateSent: new Date("2020-01-01"), + }, + secondDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Sent, + dateSent: new Date("2020-02-01"), + }, + }, + ); + const student1 = application1.student; + student1.user.identityProviderType = IdentityProviders.BCSC; + await db.user.save(student1.user); + const sinValidation1 = createFakeSINValidation({ + student: student1, + }); + student1.sinValidation = sinValidation1; + await db.student.save(student1); + await db.sinValidation.save(sinValidation1); + const casSupplier1 = await saveFakeCASSupplier( + db, + { + student: student1, + }, + { + initialValues: { + isValid: false, + }, + }, + ); + student1.casSupplier = casSupplier1; + await db.student.save(student1); + + const application2 = await saveFakeApplicationDisbursements( + appDataSource, + { + firstDisbursementValues: [ + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BCAG", + 500, + { + effectiveAmount: 100, + }, + ), + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "SBSD", + 300, + { + effectiveAmount: 150, + }, + ), + createFakeDisbursementValue( + DisbursementValueType.BCGrant, + "BGPD", + 200, + { + effectiveAmount: 275, + }, + ), + ], + }, + { + applicationStatus: ApplicationStatus.Completed, + offeringIntensity: OfferingIntensity.fullTime, + firstDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Sent, + dateSent: new Date("2020-01-15"), + }, + }, + ); + const student2 = application2.student; + student2.user.identityProviderType = IdentityProviders.BCeIDBasic; + await db.user.save(student2.user); + const sinValidation2 = createFakeSINValidation({ + student: student2, + }); + student2.sinValidation = sinValidation2; + await db.student.save(student2); + await db.sinValidation.save(sinValidation2); + const casSupplier2 = await saveFakeCASSupplier( + db, + { + student: student2, + }, + { + initialValues: { + isValid: false, + }, + }, + ); + student2.casSupplier = casSupplier2; + await db.student.save(student2); + + const payload = { + reportName: "Disbursements_Without_Valid_Supplier_Report", + params: { + startDate: "2020-01-01", + endDate: "2020-02-01", + }, + }; + 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([ + { + "Address Line 1": student1.contactInfo.address.addressLine1, + BCAG: "999.00", + BGPD: "397.00", + City: student1.contactInfo.address.city, + Country: student1.contactInfo.address.country, + "Disability Status": student1.disabilityStatus, + "Given Name": student1.user.firstName, + "Last Name": student1.user.lastName, + "Postal Code": student1.contactInfo.address.postalCode, + "Profile Type": student1.user.identityProviderType, + Province: student1.contactInfo.address.provinceState, + SBSD: "597.00", + SIN: student1.sinValidation.sin, + }, + { + "Address Line 1": student2.contactInfo.address.addressLine1, + BCAG: "100.00", + BGPD: "275.00", + City: student2.contactInfo.address.city, + Country: student2.contactInfo.address.country, + "Disability Status": student2.disabilityStatus, + "Given Name": student2.user.firstName, + "Last Name": student2.user.lastName, + "Postal Code": student2.contactInfo.address.postalCode, + "Profile Type": student2.user.identityProviderType, + Province: student2.contactInfo.address.provinceState, + SBSD: "150.00", + SIN: student2.sinValidation.sin, + }, + ]); + }); + }); + /** * Converts education program offering object into a key-value pair object matching the result data. * @param fakeOffering an education program offering record. 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 5d95728814..679034231f 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 @@ -18,6 +18,7 @@ enum InstitutionReportNames { enum MinistryReportNames { ForecastDisbursements = "Disbursement_Forecast_Report", Disbursements = "Disbursement_Report", + DisbursementsWithoutValidSupplier = "Disbursements_Without_Valid_Supplier_Report", DataInventory = "Data_Inventory_Report", ECertErrors = "ECert_Errors_Report", InstitutionDesignation = "Institution_Designation_Report", diff --git a/sources/packages/backend/apps/db-migrations/src/migrations/1732567769085-CreateDisbursementsWithoutValidSupplierReport.ts b/sources/packages/backend/apps/db-migrations/src/migrations/1732567769085-CreateDisbursementsWithoutValidSupplierReport.ts new file mode 100644 index 0000000000..e322781377 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/migrations/1732567769085-CreateDisbursementsWithoutValidSupplierReport.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { getSQLFileData } from "../utilities/sqlLoader"; + +export class CreateDisbursementsWithoutValidSupplierReport1732567769085 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Create-disbursements-without-valid-supplier-report.sql", + "Reports", + ), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Rollback-create-disbursements-without-valid-supplier-report.sql", + "Reports", + ), + ); + } +} diff --git a/sources/packages/backend/apps/db-migrations/src/sql/Reports/Create-disbursements-without-valid-supplier-report.sql b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Create-disbursements-without-valid-supplier-report.sql new file mode 100644 index 0000000000..170c14781c --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Create-disbursements-without-valid-supplier-report.sql @@ -0,0 +1,54 @@ +INSERT INTO + sims.report_configs (report_name, report_sql) +VALUES + ( + 'Disbursements_Without_Valid_Supplier_Report', + 'SELECT + users.first_name AS "Given Name", + users.last_name AS "Last Name", + sin_validations.sin AS "SIN", + users.identity_provider_type AS "Profile Type", + students.contact_info -> ''address'' ->> ''addressLine1'' AS "Address Line 1", + students.contact_info -> ''address'' ->> ''city'' AS "City", + students.contact_info -> ''address'' ->> ''provinceState'' AS "Province", + students.contact_info -> ''address'' ->> ''country'' AS "Country", + students.contact_info -> ''address'' ->> ''postalCode'' AS "Postal Code", + students.disability_status AS "Disability Status", + SUM( + CASE + WHEN disbursement_values.value_code = ''BCAG'' THEN disbursement_values.effective_amount + ELSE 0 + END + ) AS "BCAG", + SUM( + CASE + WHEN disbursement_values.value_code = ''SBSD'' THEN disbursement_values.effective_amount + ELSE 0 + END + ) AS "SBSD", + SUM( + CASE + WHEN disbursement_values.value_code = ''BGPD'' THEN disbursement_values.effective_amount + ELSE 0 + END + ) AS "BGPD" + FROM + sims.students students + INNER JOIN sims.users users ON students.user_id = users.id + INNER JOIN sims.sin_validations sin_validations ON students.id = sin_validations.student_id + INNER JOIN sims.cas_suppliers cas_suppliers ON cas_suppliers.student_id = students.id + INNER JOIN sims.applications applications ON applications.student_id = students.id + INNER JOIN sims.student_assessments student_assessments ON student_assessments.application_id = applications.id + INNER JOIN sims.disbursement_schedules disbursement_schedules ON disbursement_schedules.student_assessment_id = student_assessments.id + INNER JOIN sims.disbursement_values disbursement_values ON disbursement_values.disbursement_schedule_id = disbursement_schedules.id + WHERE + disbursement_schedules.disbursement_schedule_status = ''Sent'' + AND disbursement_schedules.date_sent BETWEEN :startDate + AND :endDate + AND cas_suppliers.is_valid = false + GROUP BY + users.id, + sin_validations.sin, + students.contact_info, + students.disability_status;' + ); \ No newline at end of file diff --git a/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-create-disbursements-without-valid-supplier-report.sql b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-create-disbursements-without-valid-supplier-report.sql new file mode 100644 index 0000000000..e674a56c46 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-create-disbursements-without-valid-supplier-report.sql @@ -0,0 +1,4 @@ +DELETE FROM + sims.report_configs +WHERE + report_name = 'Disbursements_Without_Valid_Supplier_Report'; \ No newline at end of file diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/sfas-integration/_tests_/sims-to-sfas-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/sfas-integration/_tests_/sims-to-sfas-integration.scheduler.e2e-spec.ts index bd21f67689..f4d20f6893 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/sfas-integration/_tests_/sims-to-sfas-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/sfas-integration/_tests_/sims-to-sfas-integration.scheduler.e2e-spec.ts @@ -710,7 +710,9 @@ describe(describeProcessorRootTest(QueueNames.SIMSToSFASIntegration), () => { auditUser: student.user, }, { - supplierStatus: SupplierStatus.Verified, + initialValues: { + supplierStatus: SupplierStatus.Verified, + }, }, ); // Update CAS details as expected. diff --git a/sources/packages/backend/libs/test-utils/src/factories/cas-supplier.ts b/sources/packages/backend/libs/test-utils/src/factories/cas-supplier.ts index f43f2abd38..e38f17352a 100644 --- a/sources/packages/backend/libs/test-utils/src/factories/cas-supplier.ts +++ b/sources/packages/backend/libs/test-utils/src/factories/cas-supplier.ts @@ -18,7 +18,7 @@ import * as faker from "faker"; * @param relations dependencies: * - `student` student for the CAS supplier record. * @param options optional params. - * - `supplierStatus` supplier status. + * - `initialValues` CAS supplier initial values. * @returns a saved fake CAS supplier. */ export async function saveFakeCASSupplier( @@ -27,7 +27,7 @@ export async function saveFakeCASSupplier( student?: Student; }, options?: { - supplierStatus: SupplierStatus; + initialValues?: Partial; }, ): Promise { const auditUser = await db.user.save(createFakeUser()); @@ -47,7 +47,7 @@ export async function saveFakeCASSupplier( * - `student` related student. * - `auditUser` audit user for the record. * @param options optional params. - * - `supplierStatus` supplier status. + * - `initialValues` CAS supplier initial values. * @returns a fake CAS supplier. */ export function createFakeCASSupplier( @@ -56,17 +56,20 @@ export function createFakeCASSupplier( auditUser: User; }, options?: { - supplierStatus: SupplierStatus; + initialValues?: Partial; }, ): CASSupplier { const casSupplier = new CASSupplier(); casSupplier.supplierStatus = - options?.supplierStatus ?? SupplierStatus.PendingSupplierVerification; + options?.initialValues?.supplierStatus ?? + SupplierStatus.PendingSupplierVerification; // Verified manually has a minimum of values populated. - if (options?.supplierStatus === SupplierStatus.VerifiedManually) { + if ( + options?.initialValues?.supplierStatus === SupplierStatus.VerifiedManually + ) { casSupplier.supplierAddress = {} as SupplierAddress; - casSupplier.isValid = true; + casSupplier.isValid = options?.initialValues.isValid ?? true; } else { casSupplier.supplierAddress = { supplierSiteCode: faker.datatype.number(999).toString(), diff --git a/sources/packages/forms/src/form-definitions/exportfinancialreports.json b/sources/packages/forms/src/form-definitions/exportfinancialreports.json index 943b0bb697..d59e4b7b80 100644 --- a/sources/packages/forms/src/form-definitions/exportfinancialreports.json +++ b/sources/packages/forms/src/form-definitions/exportfinancialreports.json @@ -292,7 +292,7 @@ } ], "key": "columns", - "customConditional": "show = \n data.reportName === 'Disbursement_Forecast_Report' \n || data.reportName === 'Disbursement_Report' \n || data.reportName === 'Data_Inventory_Report'\n || data.reportName === 'Institution_Designation_Report'\n || data.reportName === 'ECert_Errors_Report'\n || data.reportName === 'Program_And_Offering_Status_Report'\n || data.reportName === 'Ministry_Student_Unmet_Need_Report'", + "customConditional": "show = \n data.reportName === 'Disbursement_Forecast_Report' \n || data.reportName === 'Disbursement_Report' \n || data.reportName === 'Data_Inventory_Report'\n || data.reportName === 'Institution_Designation_Report'\n || data.reportName === 'ECert_Errors_Report'\n || data.reportName === 'Program_And_Offering_Status_Report'\n || data.reportName === 'Ministry_Student_Unmet_Need_Report'\n || data.reportName === 'Disbursements_Without_Valid_Supplier_Report'", "type": "columns", "input": false, "tableView": false, diff --git a/sources/packages/web/src/constants/report-constants.ts b/sources/packages/web/src/constants/report-constants.ts index 9d62e017ce..19f9bbcff5 100644 --- a/sources/packages/web/src/constants/report-constants.ts +++ b/sources/packages/web/src/constants/report-constants.ts @@ -12,6 +12,10 @@ export const INSTITUTION_REPORTS: OptionItemAPIOutDTO[] = [ export const MINISTRY_REPORTS: OptionItemAPIOutDTO[] = [ { description: "Data Inventory", id: "Data_Inventory_Report" }, { description: "Disbursements", id: "Disbursement_Report" }, + { + description: "Disbursements Without Valid Supplier", + id: "Disbursements_Without_Valid_Supplier_Report", + }, { description: "eCert Errors", id: "ECert_Errors_Report" }, { description: "Forecast disbursements",