diff --git a/sources/packages/backend/apps/api/src/constants/error-code.constants.ts b/sources/packages/backend/apps/api/src/constants/error-code.constants.ts index 48b7dad85d..b8ac697319 100644 --- a/sources/packages/backend/apps/api/src/constants/error-code.constants.ts +++ b/sources/packages/backend/apps/api/src/constants/error-code.constants.ts @@ -239,6 +239,7 @@ export const APPLICATION_RESTRICTION_BYPASS_NOT_FOUND = */ export const APPLICATION_RESTRICTION_BYPASS_IS_NOT_ACTIVE = "APPLICATION_RESTRICTION_BYPASS_IS_NOT_ACTIVE"; + /** * Student not found error code. */ 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 763200e55c..49f1932cf5 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 @@ -38,12 +38,16 @@ import { InstitutionLocation, OfferingIntensity, ProgramIntensity, + Student, + SupplierStatus, WorkflowData, } from "@sims/sims-db"; 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"; +import { getPSTPDTDateFormatted } from "@sims/test-utils/utils"; +import { MinistryReportsFilterAPIInDTO } from "apps/api/src/route-controllers/report/models/report.dto"; describe("ReportAestController(e2e)-exportReport", () => { let app: INestApplication; @@ -51,6 +55,8 @@ describe("ReportAestController(e2e)-exportReport", () => { let db: E2EDataSources; let appDataSource: DataSource; let formService: FormService; + let sharedCASSupplierUpdatedStudent: Student; + let casSupplierMaintenanceUpdatesPayload: MinistryReportsFilterAPIInDTO; beforeAll(async () => { const { nestApplication, module, dataSource } = @@ -65,6 +71,13 @@ describe("ReportAestController(e2e)-exportReport", () => { AppAESTModule, FormService, ); + // Shared student used for CAS Supplier maintenance updates report. + sharedCASSupplierUpdatedStudent = await saveFakeStudent(db.dataSource); + // Build payload for CAS Supplier maintenance updates report to use across tests. + casSupplierMaintenanceUpdatesPayload = { + reportName: "CAS_Supplier_Maintenance_Updates_Report", + params: {}, + }; }); it("Should generate the eCert Feedback Errors report when a report generation request is made with the appropriate offering intensity and date range.", async () => { @@ -1426,6 +1439,285 @@ describe("ReportAestController(e2e)-exportReport", () => { }); }); + 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.", + async () => { + // Arrange + // Save a CASSupplier for the student. + await saveFakeCASSupplier( + db, + { student: sharedCASSupplierUpdatedStudent }, + { + initialValues: { + supplierStatus: SupplierStatus.Verified, + isValid: true, + }, + }, + ); + // Update the student's last name. + sharedCASSupplierUpdatedStudent.user.lastName = "Updated Last Name"; + await db.student.save(sharedCASSupplierUpdatedStudent); + + const endpoint = "/aest/report"; + const ministryUserToken = await getAESTToken( + AESTGroups.BusinessAdministrators, + ); + + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.ExportFinancialReports, + data: { data: casSupplierMaintenanceUpdatesPayload }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(casSupplierMaintenanceUpdatesPayload) + .auth(ministryUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + const fileContent = response.request.res["text"]; + const parsedResult = parse(fileContent, { + header: true, + }); + const expectedReportData = buildCASSupplierMaintenanceUpdatesReport( + sharedCASSupplierUpdatedStudent, + { lastNameUpdated: true }, + ); + const [actualReportData] = parsedResult.data as Record< + string, + string + >[]; + expect(parsedResult.data.length).toBe(1); + expect(actualReportData).toEqual(expectedReportData); + }); + }, + ); + + it( + "Should generate CAS Supplier maintenance updates report with the student details of the given student" + + " when SIN number of the student is updated after the CAS supplier is set to be valid.", + async () => { + // Arrange + // Save a CASSupplier for the student. + await saveFakeCASSupplier( + db, + { student: sharedCASSupplierUpdatedStudent }, + { + initialValues: { + supplierStatus: SupplierStatus.Verified, + isValid: true, + }, + }, + ); + // Update the student's SIN. + const newSINValidation = createFakeSINValidation({ + student: sharedCASSupplierUpdatedStudent, + }); + sharedCASSupplierUpdatedStudent.sinValidation = newSINValidation; + await db.student.save(sharedCASSupplierUpdatedStudent); + + const endpoint = "/aest/report"; + const ministryUserToken = await getAESTToken( + AESTGroups.BusinessAdministrators, + ); + + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.ExportFinancialReports, + data: { data: casSupplierMaintenanceUpdatesPayload }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(casSupplierMaintenanceUpdatesPayload) + .auth(ministryUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + const fileContent = response.request.res["text"]; + const parsedResult = parse(fileContent, { + header: true, + }); + const expectedReportData = buildCASSupplierMaintenanceUpdatesReport( + sharedCASSupplierUpdatedStudent, + { sinUpdated: true }, + ); + const [actualReportData] = parsedResult.data as Record< + string, + string + >[]; + expect(parsedResult.data.length).toBe(1); + expect(actualReportData).toEqual(expectedReportData); + }); + }, + ); + + it( + "Should generate CAS Supplier maintenance updates report with the student details of the given student" + + " when 'address line 1' of the student is updated after the CAS supplier is set to be valid.", + async () => { + // Arrange + // Save a CASSupplier for the student. + await saveFakeCASSupplier( + db, + { student: sharedCASSupplierUpdatedStudent }, + { + initialValues: { + supplierStatus: SupplierStatus.Verified, + isValid: true, + }, + }, + ); + // Update the student's address line 1. + sharedCASSupplierUpdatedStudent.contactInfo.address.addressLine1 = + "Updated Address Line 1"; + await db.student.save(sharedCASSupplierUpdatedStudent); + + const endpoint = "/aest/report"; + const ministryUserToken = await getAESTToken( + AESTGroups.BusinessAdministrators, + ); + + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.ExportFinancialReports, + data: { data: casSupplierMaintenanceUpdatesPayload }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(casSupplierMaintenanceUpdatesPayload) + .auth(ministryUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + const fileContent = response.request.res["text"]; + const parsedResult = parse(fileContent, { + header: true, + }); + const expectedReportData = buildCASSupplierMaintenanceUpdatesReport( + sharedCASSupplierUpdatedStudent, + { addressLine1Updated: true }, + ); + const [actualReportData] = parsedResult.data as Record< + string, + string + >[]; + expect(parsedResult.data.length).toBe(1); + expect(actualReportData).toEqual(expectedReportData); + }); + }, + ); + + it( + "Should generate CAS Supplier maintenance updates report with the student details of the given student" + + " when postal code of the student is updated after the CAS supplier is set to be valid.", + async () => { + // Arrange + // Save a CASSupplier for the student. + await saveFakeCASSupplier( + db, + { student: sharedCASSupplierUpdatedStudent }, + { + initialValues: { + supplierStatus: SupplierStatus.Verified, + isValid: true, + }, + }, + ); + // Update the student's postal code. + sharedCASSupplierUpdatedStudent.contactInfo.address.postalCode = + "Updated postal code"; + await db.student.save(sharedCASSupplierUpdatedStudent); + + const endpoint = "/aest/report"; + const ministryUserToken = await getAESTToken( + AESTGroups.BusinessAdministrators, + ); + + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.ExportFinancialReports, + data: { data: casSupplierMaintenanceUpdatesPayload }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(casSupplierMaintenanceUpdatesPayload) + .auth(ministryUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + const fileContent = response.request.res["text"]; + const parsedResult = parse(fileContent, { + header: true, + }); + const expectedReportData = buildCASSupplierMaintenanceUpdatesReport( + sharedCASSupplierUpdatedStudent, + { postalCodeUpdated: true }, + ); + const [actualReportData] = parsedResult.data as Record< + string, + string + >[]; + expect(parsedResult.data.length).toBe(1); + expect(actualReportData).toEqual(expectedReportData); + }); + }, + ); + + it( + "Should generate CAS Supplier maintenance updates report without the student details of the given student" + + " when last name of the student is updated just to change the case to upper case characters.", + async () => { + // Arrange + // Save a CASSupplier for the student. + await saveFakeCASSupplier( + db, + { student: sharedCASSupplierUpdatedStudent }, + { + initialValues: { + supplierStatus: SupplierStatus.Verified, + isValid: true, + }, + }, + ); + // Update the student's last name to upper case. + sharedCASSupplierUpdatedStudent.user.lastName = + sharedCASSupplierUpdatedStudent.user.lastName.toUpperCase(); + await db.student.save(sharedCASSupplierUpdatedStudent); + + const endpoint = "/aest/report"; + const ministryUserToken = await getAESTToken( + AESTGroups.BusinessAdministrators, + ); + + const dryRunSubmissionMock = jest.fn().mockResolvedValue({ + valid: true, + formName: FormNames.ExportFinancialReports, + data: { data: casSupplierMaintenanceUpdatesPayload }, + }); + formService.dryRunSubmission = dryRunSubmissionMock; + + // Act/Assert + await request(app.getHttpServer()) + .post(endpoint) + .send(casSupplierMaintenanceUpdatesPayload) + .auth(ministryUserToken, BEARER_AUTH_TYPE) + .expect(HttpStatus.CREATED) + .then((response) => { + const fileContent = response.request.res["text"] as string; + expect(fileContent).toBe(""); + }); + }, + ); + /** * Converts education program offering object into a key-value pair object matching the result data. * @param fakeOffering an education program offering record. @@ -1492,4 +1784,61 @@ describe("ReportAestController(e2e)-exportReport", () => { "Offering Intensity": "", }; } + + /** + * Build CAS Supplier maintenance updates report data. + * @param student student. + * @param options expected report data options. + * - `firstNameUpdated` indicates if the first name of the student is updated. + * - `lastNameUpdated` indicates if the last name of the student is updated. + * - `sinUpdated` indicates if the SIN number of the student is updated. + * - `addressLine1Updated` indicates if the address line 1 of the student is updated. + * - `cityUpdated` indicates if the city of the student is updated. + * - `provinceUpdated` indicates if the province of the student is updated. + * - `postalCodeUpdated` indicates if the postal code of the student is updated. + * - `countryUpdated` indicates if the country of the student is updated. + * @returns report data. + */ + function buildCASSupplierMaintenanceUpdatesReport( + student: Student, + options?: { + firstNameUpdated?: boolean; + lastNameUpdated?: boolean; + sinUpdated?: boolean; + addressLine1Updated?: boolean; + cityUpdated?: boolean; + provinceUpdated?: boolean; + postalCodeUpdated?: boolean; + countryUpdated?: boolean; + }, + ): Record { + return { + "Given Names": student.user.firstName, + "Last Name": student.user.lastName, + SIN: student.sinValidation.sin, + "Address Line 1": student.contactInfo.address.addressLine1, + City: student.contactInfo.address.city, + Province: student.contactInfo.address.provinceState, + "Postal Code": student.contactInfo.address.postalCode, + Country: student.contactInfo.address.country, + "Disability Status": student.disabilityStatus, + "Profile Type": student.user.identityProviderType ?? "", + "Student Updated Date": getPSTPDTDateFormatted(student.updatedAt), + Supplier: student.casSupplier.supplierNumber, + Site: student.casSupplier.supplierAddress.supplierSiteCode, + "Protected Supplier": student.casSupplier.supplierProtected?.toString(), + "Protected Site": student.casSupplier.supplierAddress.siteProtected, + "Supplier Verified Date": getPSTPDTDateFormatted( + student.casSupplier.supplierStatusUpdatedOn, + ), + "First Name Updated": options?.firstNameUpdated ? "true" : "false", + "Last Name Updated": options?.lastNameUpdated ? "true" : "false", + "SIN Updated": options?.sinUpdated ? "true" : "false", + "Address Line 1 Updated": options?.addressLine1Updated ? "true" : "false", + "City Updated": options?.cityUpdated ? "true" : "false", + "Province Updated": options?.provinceUpdated ? "true" : "false", + "Postal Code Updated": options?.postalCodeUpdated ? "true" : "false", + "Country Updated": options?.countryUpdated ? "true" : "false", + }; + } }); 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 679034231f..8a672ddd92 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 @@ -24,6 +24,7 @@ enum MinistryReportNames { InstitutionDesignation = "Institution_Designation_Report", StudentUnmetNeed = "Ministry_Student_Unmet_Need_Report", ProgramAndOfferingStatus = "Program_And_Offering_Status_Report", + CASSupplierMaintenanceUpdates = "CAS_Supplier_Maintenance_Updates_Report", } /** diff --git a/sources/packages/backend/apps/api/src/route-controllers/report/report.controller.service.ts b/sources/packages/backend/apps/api/src/route-controllers/report/report.controller.service.ts index d380770da6..cec3371f76 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/report/report.controller.service.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/report/report.controller.service.ts @@ -11,7 +11,7 @@ import { import { getFileNameAsCurrentTimestamp, CustomNamedError, - UTF8_BYTE_ORDER_MARK, + appendByteOrderMark, } from "@sims/utilities"; import { Response } from "express"; import { Readable } from "stream"; @@ -99,12 +99,10 @@ export class ReportControllerService { const filename = `${reportName}_${timestamp}.csv`; // Adding byte order mark characters to the original file content as applications // like excel would look for BOM characters to view the file as UTF8 encoded. - const byteOrderMarkBuffer = Buffer.from(UTF8_BYTE_ORDER_MARK); - const fileContentBuffer = Buffer.from(fileContent); - const responseBuffer = Buffer.concat([ - byteOrderMarkBuffer, - fileContentBuffer, - ]); + // Append byte order mark characters only if the file content is not empty. + const responseBuffer = fileContent + ? appendByteOrderMark(fileContent) + : Buffer.from(""); response.setHeader( "Content-Disposition", `attachment; filename=${filename}`, diff --git a/sources/packages/backend/apps/db-migrations/src/migrations/1733340941443-AddCASSupplierMaintenanceReport.ts b/sources/packages/backend/apps/db-migrations/src/migrations/1733340941443-AddCASSupplierMaintenanceReport.ts new file mode 100644 index 0000000000..2b66249151 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/migrations/1733340941443-AddCASSupplierMaintenanceReport.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { getSQLFileData } from "../utilities/sqlLoader"; + +export class AddCASSupplierMaintenanceReport1733340941443 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Create-cas-supplier-maintenance-updates-report.sql", + "Reports", + ), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + getSQLFileData( + "Rollback-create-cas-supplier-maintenance-updates-report.sql", + "Reports", + ), + ); + } +} diff --git a/sources/packages/backend/apps/db-migrations/src/sql/Reports/Create-cas-supplier-maintenance-updates-report.sql b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Create-cas-supplier-maintenance-updates-report.sql new file mode 100644 index 0000000000..776f2dbaf0 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Create-cas-supplier-maintenance-updates-report.sql @@ -0,0 +1,139 @@ +INSERT INTO + sims.report_configs (report_name, report_sql) +VALUES + ( + 'CAS_Supplier_Maintenance_Updates_Report', + 'WITH cas_supplier_report AS ( + SELECT + COALESCE(student_user.first_name, '''') AS student_first_name, + student_user.last_name AS student_last_name, + student_user.identity_provider_type AS student_profile_type, + student.updated_at AS student_updated_date, + student.disability_status AS student_disability_status, + sin_validation.sin AS student_sin, + student.contact_info -> ''address'' ->> ''addressLine1'' AS student_address_line_1, + student.contact_info -> ''address'' ->> ''city'' AS student_city, + COALESCE( + student.contact_info -> ''address'' ->> ''provinceState'', + '''' + ) AS student_province, + student.contact_info -> ''address'' ->> ''postalCode'' AS student_postal_code, + student.contact_info -> ''address'' ->> ''country'' AS student_country, + cas_supplier.supplier_number AS cas_supplier, + cas_supplier.supplier_protected AS cas_supplier_protected, + cas_supplier.supplier_address ->> ''supplierSiteCode'' AS cas_site, + cas_supplier.supplier_address ->> ''siteProtected'' AS cas_site_protected, + cas_supplier.supplier_status_updated_on AS cas_supplier_verified_date, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''firstName'', + '''' + ) AS cas_snapshot_first_name, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''lastName'', + '''' + ) AS cas_snapshot_last_name, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''sin'', + '''' + ) AS cas_snapshot_sin, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''addressLine1'', + '''' + ) AS cas_snapshot_address_line_1, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''city'', + '''' + ) AS cas_snapshot_city, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''province'', + '''' + ) AS cas_snapshot_province, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''postalCode'', + '''' + ) AS cas_snapshot_postal_code, + COALESCE( + cas_supplier.student_profile_snapshot ->> ''country'', + '''' + ) AS cas_snapshot_country + FROM + sims.students student + INNER JOIN sims.cas_suppliers cas_supplier on student.cas_supplier_id = cas_supplier.id + INNER JOIN sims.users student_user on student.user_id = student_user.id + INNER JOIN sims.sin_validations sin_validation on student.sin_validation_id = sin_validation.id + WHERE + cas_supplier.is_valid = true + AND ( + jsonb_build_object( + ''firstName'', + student_user.first_name, + ''lastName'', + student_user.last_name, + ''sin'', + sin_validation.sin, + ''addressLine1'', + student.contact_info -> ''address'' ->> ''addressLine1'', + ''city'', + student.contact_info -> ''address'' ->> ''city'', + ''province'', + student.contact_info -> ''address'' ->> ''provinceState'', + ''postalCode'', + student.contact_info -> ''address'' ->> ''postalCode'', + ''country'', + student.contact_info -> ''address'' ->> ''country'' + ) :: text NOT ILIKE jsonb_build_object( + ''firstName'', + cas_supplier.student_profile_snapshot ->> ''firstName'', + ''lastName'', + cas_supplier.student_profile_snapshot ->> ''lastName'', + ''sin'', + cas_supplier.student_profile_snapshot ->> ''sin'', + ''addressLine1'', + cas_supplier.student_profile_snapshot ->> ''addressLine1'', + ''city'', + cas_supplier.student_profile_snapshot ->> ''city'', + ''province'', + cas_supplier.student_profile_snapshot ->> ''province'', + ''postalCode'', + cas_supplier.student_profile_snapshot ->> ''postalCode'', + ''country'', + cas_supplier.student_profile_snapshot ->> ''country'' + ) :: text + ) + ) + SELECT + cas_supplier_report.student_first_name AS "Given Names", + cas_supplier_report.student_last_name AS "Last Name", + cas_supplier_report.student_sin AS "SIN", + cas_supplier_report.student_address_line_1 AS "Address Line 1", + cas_supplier_report.student_city AS "City", + cas_supplier_report.student_province AS "Province", + cas_supplier_report.student_postal_code AS "Postal Code", + cas_supplier_report.student_country AS "Country", + cas_supplier_report.student_disability_status AS "Disability Status", + cas_supplier_report.student_profile_type AS "Profile Type", + TO_CHAR( + (cas_supplier_report.student_updated_date AT TIME ZONE ''America/Vancouver''), + ''YYYY-MM-DD'' + ) AS "Student Updated Date", + cas_supplier_report.cas_supplier AS "Supplier", + cas_supplier_report.cas_site AS "Site", + cas_supplier_report.cas_supplier_protected AS "Protected Supplier", + cas_supplier_report.cas_site_protected AS "Protected Site", + TO_CHAR( + (cas_supplier_report.cas_supplier_verified_date AT TIME ZONE ''America/Vancouver''), + ''YYYY-MM-DD'' + ) AS "Supplier Verified Date", + cas_supplier_report.student_first_name NOT ILIKE cas_supplier_report.cas_snapshot_first_name AS "First Name Updated", + cas_supplier_report.student_last_name NOT ILIKE cas_supplier_report.cas_snapshot_last_name AS "Last Name Updated", + cas_supplier_report.student_sin NOT ILIKE cas_supplier_report.cas_snapshot_sin AS "SIN Updated", + cas_supplier_report.student_address_line_1 NOT ILIKE cas_supplier_report.cas_snapshot_address_line_1 AS "Address Line 1 Updated", + cas_supplier_report.student_city NOT ILIKE cas_supplier_report.cas_snapshot_city AS "City Updated", + cas_supplier_report.student_province NOT ILIKE cas_supplier_report.cas_snapshot_province AS "Province Updated", + cas_supplier_report.student_postal_code NOT ILIKE cas_supplier_report.cas_snapshot_postal_code AS "Postal Code Updated", + cas_supplier_report.student_country NOT ILIKE cas_supplier_report.cas_snapshot_country AS "Country Updated" + FROM + cas_supplier_report + ORDER BY + student_updated_date;' + ); \ No newline at end of file diff --git a/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-create-cas-supplier-maintenance-updates-report.sql b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-create-cas-supplier-maintenance-updates-report.sql new file mode 100644 index 0000000000..64d5f5bb01 --- /dev/null +++ b/sources/packages/backend/apps/db-migrations/src/sql/Reports/Rollback-create-cas-supplier-maintenance-updates-report.sql @@ -0,0 +1,4 @@ +DELETE from + sims.report_configs +WHERE + report_name = 'CAS_Supplier_Maintenance_Updates_Report'; \ No newline at end of file diff --git a/sources/packages/backend/libs/sims-db/src/entities/cas-supplier.model.ts b/sources/packages/backend/libs/sims-db/src/entities/cas-supplier.model.ts index 714b2e684d..bd3e72e316 100644 --- a/sources/packages/backend/libs/sims-db/src/entities/cas-supplier.model.ts +++ b/sources/packages/backend/libs/sims-db/src/entities/cas-supplier.model.ts @@ -164,12 +164,12 @@ export interface SupplierAddress { * Student profile snapshot information. */ export interface StudentProfileSnapshot { - firstName: string; + firstName?: string; lastName: string; sin: string; addressLine1: string; city: string; - province: string; + province?: string; postalCode: string; country: string; } 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 e874eb7819..3c0d91906e 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 @@ -1,6 +1,7 @@ import { CASSupplier, Student, + StudentProfileSnapshot, SupplierAddress, SupplierStatus, User, @@ -38,7 +39,9 @@ export async function saveFakeCASSupplier( student = await saveFakeStudent(db.dataSource); } const casSupplier = createFakeCASSupplier({ student, auditUser }, options); - return db.casSupplier.save(casSupplier); + student.casSupplier = casSupplier; + await db.student.save(student); + return student.casSupplier; } /** @@ -86,7 +89,7 @@ export function createFakeCASSupplier( lastUpdated: new Date(), }; casSupplier.supplierProtected = true; - casSupplier.isValid = false; + casSupplier.isValid = options?.initialValues.isValid ?? false; } casSupplier.supplierName = `${faker.name.lastName()}, ${faker.name.firstName()}`; casSupplier.supplierNumber = faker.datatype @@ -98,6 +101,21 @@ export function createFakeCASSupplier( casSupplier.supplierStatusUpdatedOn = new Date(); casSupplier.creator = relations.auditUser; casSupplier.student = relations.student; + // Build the student profile snapshot based on valid status. + const studentProfileSnapshot: StudentProfileSnapshot = casSupplier.isValid + ? { + firstName: relations.student.user.firstName, + lastName: relations.student.user.lastName, + sin: relations.student.sinValidation.sin, + addressLine1: relations.student.contactInfo.address.addressLine1, + city: relations.student.contactInfo.address.city, + province: relations.student.contactInfo.address.provinceState, + postalCode: relations.student.contactInfo.address.postalCode, + country: relations.student.contactInfo.address.country, + } + : null; + casSupplier.studentProfileSnapshot = + options?.initialValues?.studentProfileSnapshot ?? studentProfileSnapshot; return casSupplier; } diff --git a/sources/packages/backend/libs/test-utils/src/utils/date-utils.ts b/sources/packages/backend/libs/test-utils/src/utils/date-utils.ts index d447e413ec..2ddb5395a9 100644 --- a/sources/packages/backend/libs/test-utils/src/utils/date-utils.ts +++ b/sources/packages/backend/libs/test-utils/src/utils/date-utils.ts @@ -1,3 +1,4 @@ +import { DATE_ONLY_ISO_FORMAT, PST_TIMEZONE } from "@sims/utilities"; import * as dayjs from "dayjs"; /** @@ -35,3 +36,16 @@ export const addMilliSeconds = ( .add(milliSecondsToAdd, "millisecond") .toDate(); }; + +/** + * Convert the date to PST/PDT(PST: UTC−08:00, PDT: UTC−07:00) and format. + * @param date date. + * @param format date format. + * @returns converted date in the given format. + */ +export function getPSTPDTDateFormatted( + date: Date | string, + format = DATE_ONLY_ISO_FORMAT, +): string { + return dayjs(new Date(date)).tz(PST_TIMEZONE, false).format(format); +} diff --git a/sources/packages/backend/libs/utilities/src/string-utils.ts b/sources/packages/backend/libs/utilities/src/string-utils.ts index 34b4fbea48..185bb5236e 100644 --- a/sources/packages/backend/libs/utilities/src/string-utils.ts +++ b/sources/packages/backend/libs/utilities/src/string-utils.ts @@ -1,4 +1,4 @@ -import { FILE_DEFAULT_ENCODING } from "@sims/utilities"; +import { FILE_DEFAULT_ENCODING, UTF8_BYTE_ORDER_MARK } from "@sims/utilities"; const REPLACE_LINE_BREAK_REGEX = /\r?\n|\r/g; @@ -154,3 +154,14 @@ export function convertToASCII(rawContent?: string): Buffer | null { export function convertToASCIIString(rawContent?: string): string | null { return convertToASCII(rawContent)?.toString() ?? null; } + +/** + * Append UTF-8 BOM to the content. + * @param content content to append the BOM. + * @returns content with the BOM appended. + */ +export function appendByteOrderMark(content: string): Buffer { + const byteOrderMarkBuffer = Buffer.from(UTF8_BYTE_ORDER_MARK); + const fileContentBuffer = Buffer.from(content); + return Buffer.concat([byteOrderMarkBuffer, fileContentBuffer]); +} diff --git a/sources/packages/web/src/constants/report-constants.ts b/sources/packages/web/src/constants/report-constants.ts index 19f9bbcff5..4f36195e75 100644 --- a/sources/packages/web/src/constants/report-constants.ts +++ b/sources/packages/web/src/constants/report-constants.ts @@ -10,6 +10,10 @@ export const INSTITUTION_REPORTS: OptionItemAPIOutDTO[] = [ ]; export const MINISTRY_REPORTS: OptionItemAPIOutDTO[] = [ + { + description: "CAS Supplier Maintenance Updates", + id: "CAS_Supplier_Maintenance_Updates_Report", + }, { description: "Data Inventory", id: "Data_Inventory_Report" }, { description: "Disbursements", id: "Disbursement_Report" }, {