Skip to content

Commit

Permalink
#3050 - Build Student Unmet Need: Ministry Report (#3431)
Browse files Browse the repository at this point in the history
### As a part of this PR, the following were completed:

- Added `Student Unmet Needs` dropdown to the dynamic dropdown reports
list for the Ministry.
- Updated the `formio` to dynamically load the filters for this report.
- Added the DB Migrations and the SQL for the **Student Unmet Need
Report for the Ministry** and generated the Report.
- Added the following e2e test for the **Student Unmet Need Report for
the Ministry**:
       `ReportAestController(e2e)-exportReport`
√ `Should generate the Student Unmet Need Report for ministry when a
report generation request is made with the appropriate filters.`

**SQL**

```sql
SELECT
	users.first_name AS "Student First Name",
	users.last_name AS "Student Last Name",
	sin_validations.sin AS "SIN",
	applications.student_number AS "Student Number",
	users.email AS "Student Email Address",
	students.contact_info ->> 'phone' AS "Student Phone Number",
	institution_locations.institution_code AS "Institution Location Code",
	institution_locations.name AS "Institution Location Name",
	applications.application_number AS "Application Number",
	CAST(
        CAST(student_assessments.assessment_date AS date) AS varchar
      ) AS "Assessment Date",
	education_programs_offerings.offering_intensity AS "Study Intensity (PT or FT)",
	students.disability_status AS "Profile Disability Status",
	CASE
		WHEN student_assessments.workflow_data -> 'calculatedData' ->> 'pdppdStatus' = 'false' THEN 'no'
		WHEN student_assessments.workflow_data -> 'calculatedData' ->> 'pdppdStatus' = 'true' THEN 'yes'
	END AS "Application Disability Status",
	CAST(
        education_programs_offerings.study_start_date AS varchar
      ) AS "Study Start Date",
	CAST(
        education_programs_offerings.study_end_date AS varchar
      ) AS "Study End Date",
	education_programs.program_name AS "Program Name",
	education_programs.credential_type AS "Program Credential Type",
	education_programs.cip_code AS "CIP Code",
	education_programs.completion_years AS "Program Length",
	education_programs.sabc_code AS "SABC Program Code",
	education_programs_offerings.offering_name AS "Offering Name",
	education_programs_offerings.year_of_study AS "Year of Study",
	applications.data ->> 'indigenousStatus' AS "Indigenous person status",
	applications.data ->> 'citizenship' AS "Citizenship Status",
	applications.data ->> 'youthInCare' AS "Youth in Care Flag",
	applications.data ->> 'custodyOfChildWelfare' AS "Youth in Care beyond age 19",
	applications.relationship_status AS "Marital Status",
	applications.data ->> 'dependantstatus' AS "Independant/Dependant",
	student_assessments.workflow_data -> 'calculatedData' ->> 'totalEligibleDependents' AS "Number of Eligible Dependants Total",
	CASE
		WHEN education_programs_offerings.offering_intensity = 'Part Time' THEN student_assessments.assessment_data ->> 'totalAssessmentNeed'
		WHEN education_programs_offerings.offering_intensity = 'Full Time' THEN student_assessments.assessment_data ->> 'totalAssessedCost'
	END AS "Federal/Provincial Assessed Costs",
	student_assessments.assessment_data ->> 'totalFederalAssessedResources' AS "Federal Assessed Resources",
	student_assessments.assessment_data ->> 'federalAssessmentNeed' AS "Federal assessed need",
	student_assessments.assessment_data ->> 'totalProvincialAssessedResources' AS "Provincial Assessed Resources",
	student_assessments.assessment_data ->> 'provincialAssessmentNeed' AS "Provincial assessed need",
	student_assessments.assessment_data ->> 'finalAwardTotal' AS "Total assistance",
	student_assessments.assessment_data ->> 'finalFederalAwardNetCSGPAmount' AS "Estimated CSGP",
	student_assessments.assessment_data ->> 'finalFederalAwardNetCSPTAmount' AS "Estimated CSPT",
	student_assessments.assessment_data ->> 'finalFederalAwardNetCSGDAmount' AS "Estimated CSGD",
	student_assessments.assessment_data ->> 'finalProvincialAwardNetBCAGAmount' AS "Estimated BCAG",
	student_assessments.assessment_data ->> 'finalProvincialAwardNetSBSDAmount' AS "Estimated SBSD",
	student_assessments.assessment_data ->> 'finalFederalAwardNetCSLPAmount' AS "Estimated CSLP",
	student_assessments.assessment_data ->> 'finalProvincialAwardNetBCSLAmount' AS "Estimated BCSL",
	student_assessments.assessment_data ->> 'finalFederalAwardNetCSGFAmount' AS "Estimated CSGF",
	student_assessments.assessment_data ->> 'finalProvincialAwardNetBGPDAmount' AS "Estimated BGPD"
FROM
	sims.applications applications
INNER JOIN sims.students students ON
	students.id = applications.student_id
INNER JOIN sims.sin_validations sin_validations ON
	sin_validations.student_id = students.id
INNER JOIN sims.users users ON
	users.id = students.user_id
INNER JOIN sims.student_assessments student_assessments ON
	student_assessments.id = applications.current_assessment_id
INNER JOIN sims.education_programs_offerings education_programs_offerings ON
	education_programs_offerings.id = student_assessments.offering_id
INNER JOIN sims.education_programs education_programs ON
	education_programs.id = education_programs_offerings.program_id
INNER JOIN sims.institution_locations institution_locations ON
	institution_locations.id = applications.location_id
WHERE
	applications.application_status IN ('Assessment', 'Enrolment', 'Completed')
      AND education_programs_offerings.offering_intensity = ANY(:offeringIntensity)
	AND applications.is_archived = FALSE
	AND (:sabcProgramCode = ''
		OR education_programs.sabc_code = :sabcProgramCode)
	AND (:institution = 0
		OR institution_locations.institution_id = :institution)
	AND education_programs_offerings.study_start_date BETWEEN :startDate
      AND :endDate;
```

### Screenshots:

**Dropdown showing the Student Unmet Needs Report in Ministry View:**

<img width="1920" alt="image"
src="https://github.com/bcgov/SIMS/assets/7859295/e1612220-a671-4121-9327-acb240336956">


----------------------------------------------------------------------------

**Dynamically loaded filters on selecting the Student Unmet Needs
Report:**

<img width="1920" alt="image"
src="https://github.com/bcgov/SIMS/assets/7859295/83f2eec3-5a35-403b-a01a-ba91e684c6f2">


-----------------------------------------------------------------------------

**Student Unmet Need Report:**

**1st Half:**

<img width="1913" alt="image"
src="https://github.com/bcgov/SIMS/assets/7859295/b572596d-6e38-41fc-b404-44a8ee694f50">


**2nd Half:**

<img width="1909" alt="image"
src="https://github.com/bcgov/SIMS/assets/7859295/b41ea02e-c8e4-40df-b787-37b4f9a4c762">


------------------------------------------------------------------------------

**E2E Test Screenshot:**

<img width="1262" alt="image"
src="https://github.com/bcgov/SIMS/assets/7859295/b07d1e90-dcdf-4abc-8d2d-27a829149aab">
****
  • Loading branch information
sh16011993 authored Jun 26, 2024
1 parent 352ce8a commit f98a9cb
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 812 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getProviderInstanceForModule,
saveFakeApplicationDisbursements,
saveFakeDesignationAgreementLocation,
saveFakeStudent,
} from "@sims/test-utils";
import {
AESTGroups,
Expand All @@ -22,14 +23,18 @@ import { FormNames, FormService } from "../../../../services";
import { AppAESTModule } from "../../../../app.aest.module";
import { TestingModule } from "@nestjs/testing";
import {
ApplicationData,
ApplicationStatus,
Assessment,
COEStatus,
DisbursementScheduleStatus,
EducationProgram,
EducationProgramOffering,
FullTimeAssessment,
InstitutionLocation,
OfferingIntensity,
ProgramIntensity,
WorkflowData,
} from "@sims/sims-db";
import { addDays, getISODateOnlyString } from "@sims/utilities";
import { DataSource } from "typeorm";
Expand Down Expand Up @@ -1036,6 +1041,162 @@ describe("ReportAestController(e2e)-exportReport", () => {
},
);

it("Should generate the Student Unmet Need Report for ministry when a report generation request is made with the appropriate filters.", async () => {
// Arrange
const student = await saveFakeStudent(db.dataSource);
const now = new Date();
const savedApplication = await saveFakeApplicationDisbursements(
db.dataSource,
{ student },
{
currentAssessmentInitialValues: {
workflowData: {
calculatedData: {
totalEligibleDependents: 2,
pdppdStatus: false,
},
} as WorkflowData,
assessmentDate: now,
assessmentData: {
totalFederalAssessedResources: 10,
federalAssessmentNeed: 3,
totalProvincialAssessedResources: 5,
provincialAssessmentNeed: 11,
finalAwardTotal: 20,
finalFederalAwardNetCSGPAmount: 13,
finalFederalAwardNetCSGDAmount: 14,
finalProvincialAwardNetBCAGAmount: 15,
finalProvincialAwardNetSBSDAmount: 16,
finalProvincialAwardNetBCSLAmount: 17,
finalFederalAwardNetCSGFAmount: 18,
finalProvincialAwardNetBGPDAmount: 19,
totalAssessedCost: 50,
} as Assessment,
},
applicationData: {
indigenousStatus: "no",
citizenship: "canadianCitizen",
youthInCare: "no",
dependantstatus: "independant",
} as ApplicationData,
offeringInitialValues: {
studyStartDate: getISODateOnlyString(now),
studyEndDate: getISODateOnlyString(addDays(30, now)),
},
},
);
const payload = {
reportName: "Ministry_Student_Unmet_Need_Report",
params: {
institution: "",
startDate: now,
endDate: now,
offeringIntensity: {
"Full Time": true,
"Part Time": true,
},
sabcProgramCode: "",
},
};
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,
);
const assessmentData = savedApplication.currentAssessment
.assessmentData as FullTimeAssessment;
const applicationData = savedApplication.currentAssessment.application.data;
const savedOffering = savedApplication.currentAssessment.offering;
const savedEducationProgram = savedOffering.educationProgram;
const savedLocation = savedApplication.location;
const savedStudent = savedApplication.student;
const savedUser = savedStudent.user;
// 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).toEqual(
expect.arrayContaining([
{
"Application Disability Status": "no",
"Application Number": savedApplication.applicationNumber,
"Assessment Date": getISODateOnlyString(
savedApplication.currentAssessment.assessmentDate,
),
"CIP Code": savedEducationProgram.cipCode,
"Citizenship Status": applicationData.citizenship,
"Estimated BCAG":
assessmentData.finalProvincialAwardNetBCAGAmount.toString(),
"Estimated BCSL":
assessmentData.finalProvincialAwardNetBCSLAmount.toString(),
"Estimated BGPD":
assessmentData.finalProvincialAwardNetBGPDAmount.toString(),
"Estimated CSGD":
assessmentData.finalFederalAwardNetCSGDAmount.toString(),
"Estimated CSGF":
assessmentData.finalFederalAwardNetCSGFAmount.toString(),
"Estimated CSGP":
assessmentData.finalFederalAwardNetCSGPAmount.toString(),
"Estimated CSLP": "",
"Estimated CSPT": "",
"Estimated SBSD":
assessmentData.finalProvincialAwardNetSBSDAmount.toString(),
"Federal Assessed Resources":
assessmentData.totalFederalAssessedResources.toString(),
"Federal assessed need":
assessmentData.federalAssessmentNeed.toString(),
"Federal/Provincial Assessed Costs":
assessmentData.totalAssessedCost.toString(),
"Independant/Dependant": applicationData.dependantstatus,
"Indigenous person status": applicationData.indigenousStatus,
"Institution Location Code": savedLocation.institutionCode,
"Institution Location Name": savedLocation.name,
"Marital Status":
savedApplication.currentAssessment.application
.relationshipStatus,
"Number of Eligible Dependants Total":
savedApplication.currentAssessment.workflowData.calculatedData.totalEligibleDependents.toString(),
"Offering Name": savedOffering.name,
"Profile Disability Status": savedStudent.disabilityStatus,
"Program Credential Type": savedEducationProgram.credentialType,
"Program Length": savedEducationProgram.completionYears,
"Program Name": savedEducationProgram.name,
"Provincial Assessed Resources":
assessmentData.totalProvincialAssessedResources.toString(),
"Provincial assessed need":
assessmentData.provincialAssessmentNeed.toString(),
"SABC Program Code": "",
SIN: savedStudent.sinValidation.sin,
"Student Email Address": savedUser.email,
"Student First Name": savedUser.firstName,
"Student Last Name": savedUser.lastName,
"Student Number": "",
"Student Phone Number": savedStudent.contactInfo.phone,
"Study End Date": savedOffering.studyEndDate,
"Study Intensity (PT or FT)": savedOffering.offeringIntensity,
"Study Start Date": savedOffering.studyStartDate,
"Total assistance": assessmentData.finalAwardTotal.toString(),
"Year of Study": savedOffering.yearOfStudy.toString(),
"Youth in Care Flag": applicationData.youthInCare,
"Youth in Care beyond age 19": "",
},
]),
);
});
});

/**
* Converts education program offering object into a key-value pair object matching the result data.
* @param fakeOffering an education program offering record.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum MinistryReportNames {
DataInventory = "Data_Inventory_Report",
ECertErrors = "ECert_Errors_Report",
InstitutionDesignation = "Institution_Designation_Report",
StudentUnmetNeed = "Ministry_Student_Unmet_Need_Report",
ProgramAndOfferingStatus = "Program_And_Offering_Status_Report",
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export class ReportControllerService {
"Not able to export report due to an invalid request.",
);
}
// In case the `institution` is present as optional in the submission it will be sent as an empty string (in case it is not provided)
// or as a number (in case one institution was selected). To ensure the dynamic parameter will always be sent with the same type, the default 0 is used.
if (submissionResult.data.data.params["institution"] === "") {
submissionResult.data.data.params["institution"] = 0;
}
const programYearExists = await this.programYearService.programYearExists(
payload.params.programYear as number,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { getSQLFileData } from "../utilities/sqlLoader";

export class CreateMinistryStudentUnmetNeedReport1718660711659
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Create-ministry-student-unmet-need-report.sql",
"Reports",
),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Rollback-create-ministry-student-unmet-need-report.sql",
"Reports",
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
INSERT INTO
sims.report_configs (report_name, report_sql)
VALUES
(
'Ministry_Student_Unmet_Need_Report',
'SELECT
users.first_name AS "Student First Name",
users.last_name AS "Student Last Name",
sin_validations.sin AS "SIN",
applications.student_number AS "Student Number",
users.email AS "Student Email Address",
students.contact_info ->> ''phone'' AS "Student Phone Number",
institution_locations.institution_code AS "Institution Location Code",
institution_locations.name AS "Institution Location Name",
applications.application_number AS "Application Number",
CAST(
CAST(student_assessments.assessment_date AS date) AS varchar
) AS "Assessment Date",
education_programs_offerings.offering_intensity AS "Study Intensity (PT or FT)",
students.disability_status AS "Profile Disability Status",
CASE
WHEN student_assessments.workflow_data -> ''calculatedData'' ->> ''pdppdStatus'' = ''false'' THEN ''no''
WHEN student_assessments.workflow_data -> ''calculatedData'' ->> ''pdppdStatus'' = ''true'' THEN ''yes''
END AS "Application Disability Status",
CAST(
education_programs_offerings.study_start_date AS varchar
) AS "Study Start Date",
CAST(
education_programs_offerings.study_end_date AS varchar
) AS "Study End Date",
education_programs.program_name AS "Program Name",
education_programs.credential_type AS "Program Credential Type",
education_programs.cip_code AS "CIP Code",
education_programs.completion_years AS "Program Length",
education_programs.sabc_code AS "SABC Program Code",
education_programs_offerings.offering_name AS "Offering Name",
education_programs_offerings.year_of_study AS "Year of Study",
applications.data ->> ''indigenousStatus'' AS "Indigenous person status",
applications.data ->> ''citizenship'' AS "Citizenship Status",
applications.data ->> ''youthInCare'' AS "Youth in Care Flag",
applications.data ->> ''custodyOfChildWelfare'' AS "Youth in Care beyond age 19",
applications.relationship_status AS "Marital Status",
applications.data ->> ''dependantstatus'' AS "Independant/Dependant",
student_assessments.workflow_data -> ''calculatedData'' ->> ''totalEligibleDependents'' AS "Number of Eligible Dependants Total",
CASE
WHEN education_programs_offerings.offering_intensity = ''Part Time'' THEN student_assessments.assessment_data ->> ''totalAssessmentNeed''
WHEN education_programs_offerings.offering_intensity = ''Full Time'' THEN student_assessments.assessment_data ->> ''totalAssessedCost''
END AS "Federal/Provincial Assessed Costs",
student_assessments.assessment_data ->> ''totalFederalAssessedResources'' AS "Federal Assessed Resources",
student_assessments.assessment_data ->> ''federalAssessmentNeed'' AS "Federal assessed need",
student_assessments.assessment_data ->> ''totalProvincialAssessedResources'' AS "Provincial Assessed Resources",
student_assessments.assessment_data ->> ''provincialAssessmentNeed'' AS "Provincial assessed need",
student_assessments.assessment_data ->> ''finalAwardTotal'' AS "Total assistance",
student_assessments.assessment_data ->> ''finalFederalAwardNetCSGPAmount'' AS "Estimated CSGP",
student_assessments.assessment_data ->> ''finalFederalAwardNetCSPTAmount'' AS "Estimated CSPT",
student_assessments.assessment_data ->> ''finalFederalAwardNetCSGDAmount'' AS "Estimated CSGD",
student_assessments.assessment_data ->> ''finalProvincialAwardNetBCAGAmount'' AS "Estimated BCAG",
student_assessments.assessment_data ->> ''finalProvincialAwardNetSBSDAmount'' AS "Estimated SBSD",
student_assessments.assessment_data ->> ''finalFederalAwardNetCSLPAmount'' AS "Estimated CSLP",
student_assessments.assessment_data ->> ''finalProvincialAwardNetBCSLAmount'' AS "Estimated BCSL",
student_assessments.assessment_data ->> ''finalFederalAwardNetCSGFAmount'' AS "Estimated CSGF",
student_assessments.assessment_data ->> ''finalProvincialAwardNetBGPDAmount'' AS "Estimated BGPD"
FROM
sims.applications applications
INNER JOIN sims.students students ON students.id = applications.student_id
INNER JOIN sims.sin_validations sin_validations ON sin_validations.id = students.sin_validation_id
INNER JOIN sims.users users ON users.id = students.user_id
INNER JOIN sims.student_assessments student_assessments ON student_assessments.id = applications.current_assessment_id
INNER JOIN sims.education_programs_offerings education_programs_offerings ON education_programs_offerings.id = student_assessments.offering_id
INNER JOIN sims.education_programs education_programs ON education_programs.id = education_programs_offerings.program_id
INNER JOIN sims.institution_locations institution_locations ON institution_locations.id = applications.location_id
WHERE
applications.application_status IN (''Assessment'', ''Enrolment'', ''Completed'')
AND education_programs_offerings.offering_intensity = ANY(:offeringIntensity)
AND applications.is_archived = FALSE
AND (
:sabcProgramCode = ''''
OR education_programs.sabc_code = :sabcProgramCode
)
AND (
:institution = 0
OR institution_locations.institution_id = :institution
)
AND education_programs_offerings.study_start_date BETWEEN :startDate
AND :endDate;'
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DELETE from
sims.report_configs
WHERE
report_name = 'Ministry_Student_Unmet_Need_Report';
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function createFakeApplication(
* otherwise only one disbursement will be created.
* - `firstDisbursementInitialValues` if provided sets the disbursement schedule status for the first disbursement otherwise sets to pending status by default.
* - `secondDisbursementInitialValues` if provided sets the disbursement schedule status for the second disbursement otherwise sets to pending status by default.
* - `offeringInitialValues` initial values related to the offering for the original assessment.
* @returns the created application and its dependencies including the disbursement
* with the confirmation of enrollment data.
*/
Expand All @@ -113,6 +114,7 @@ export async function saveFakeApplicationDisbursements(
currentAssessmentInitialValues?: Partial<StudentAssessment>;
firstDisbursementInitialValues?: Partial<DisbursementSchedule>;
secondDisbursementInitialValues?: Partial<DisbursementSchedule>;
offeringInitialValues?: Partial<EducationProgramOffering>;
},
): Promise<Application> {
const applicationRepo = dataSource.getRepository(Application);
Expand Down Expand Up @@ -235,6 +237,7 @@ export async function saveFakeApplicationDisbursements(
* - `offeringIntensity` if provided sets the offering intensity for the created fakeApplication, otherwise sets it to fulltime by default.
* - `applicationData` related application data.
* - `currentAssessmentInitialValues` initial values related to the current assessment.
* - `offeringInitialValues` initial values related to the offering for the original assessment.
* @returns the created application.
*/
export async function saveFakeApplication(
Expand All @@ -252,6 +255,7 @@ export async function saveFakeApplication(
offeringIntensity?: OfferingIntensity;
applicationData?: ApplicationData;
currentAssessmentInitialValues?: Partial<StudentAssessment>;
offeringInitialValues?: Partial<EducationProgramOffering>;
},
): Promise<Application> {
const userRepo = dataSource.getRepository(User);
Expand Down Expand Up @@ -289,12 +293,15 @@ export async function saveFakeApplication(
// Offering.
let savedOffering = relations?.offering;
if (!savedOffering) {
const fakeOffering = createFakeEducationProgramOffering({
institution: relations?.institution,
institutionLocation: relations?.institutionLocation,
program: relations?.program,
auditUser: savedUser,
});
const fakeOffering = createFakeEducationProgramOffering(
{
institution: relations?.institution,
institutionLocation: relations?.institutionLocation,
program: relations?.program,
auditUser: savedUser,
},
{ initialValues: options?.offeringInitialValues },
);
fakeOffering.offeringIntensity =
options?.offeringIntensity ?? OfferingIntensity.fullTime;
fakeOffering.parentOffering = fakeOffering;
Expand Down
Loading

0 comments on commit f98a9cb

Please sign in to comment.