Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4015 - FT program year total contribution limits #4154

Merged
merged 27 commits into from
Dec 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
DisbursementValueType,
OfferingIntensity,
StudentAssessmentStatus,
WorkflowData,
} from "@sims/sims-db";
import { addDays, getISODateOnlyString } from "@sims/utilities";
import { createFakeVerifyAssessmentCalculationOrderPayload } from "./verify-assessment-calculation-order-factory";
Expand Down Expand Up @@ -972,6 +973,134 @@ describe("AssessmentController(e2e)-verifyAssessmentCalculationOrder", () => {
// The result will not have data from SFAS or SFAS part time application data.
expect(FakeWorkerJobResult.getOutputVariables(result)).toStrictEqual({
isReadyForCalculation: true,
// Student contribution total.
programYearTotalFederalFSC: 0,
programYearTotalProvincialFSC: 0,
programYearTotalScholarshipBursaries: 0,
programYearTotalSpouseContributionWeeks: 0,
});
});

it("Should sum the contributions from past applications with full-time offering intensities when the applications are for the same student and program year and the student submits a full-time application.", async () => {
// Arrange

// Create the student and program year 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 } });

// Past full-time application with federal and provincial loans and grants.
// Loans will be ignored.
await saveFakeApplicationDisbursements(
db.dataSource,
{
student,
firstDisbursementValues: [
createFakeDisbursementValue(
DisbursementValueType.CanadaLoan,
"CSLF",
1,
),
],
},
{
offeringIntensity: OfferingIntensity.fullTime,
applicationStatus: ApplicationStatus.Completed,
currentAssessmentInitialValues: {
assessmentWorkflowId: "some fake id",
andrepestana-aot marked this conversation as resolved.
Show resolved Hide resolved
studentAssessmentStatus: StudentAssessmentStatus.Completed,
assessmentDate: addDays(10, programYear.startDate),
workflowData: {
calculatedData: {
studentSpouseContribution: 1000,
studentScholarshipsBursaries: 2000,
totalFederalFSC: 3000,
totalProvincialFSC: 4000,
},
} as WorkflowData,
},
},
);
// Past full-time application with federal and provincial loans and grants.
// Loans will be ignored.
await saveFakeApplicationDisbursements(
db.dataSource,
{
student,
firstDisbursementValues: [
createFakeDisbursementValue(
DisbursementValueType.CanadaLoan,
"CSLF",
8,
),
],
},
{
offeringIntensity: OfferingIntensity.fullTime,
applicationStatus: ApplicationStatus.Completed,
currentAssessmentInitialValues: {
assessmentWorkflowId: "some fake id",
studentAssessmentStatus: StudentAssessmentStatus.Completed,
assessmentDate: addDays(20, programYear.startDate),
workflowData: {
calculatedData: {
studentSpouseContribution: 1000,
studentScholarshipsBursaries: 2000,
totalFederalFSC: 3000,
totalProvincialFSC: 4000,
},
} as WorkflowData,
},
},
);
// Current application having the first assessment already processed.
const currentApplication = await saveFakeApplicationDisbursements(
db.dataSource,
{ student },
{
offeringIntensity: OfferingIntensity.fullTime,
applicationStatus: ApplicationStatus.InProgress,
currentAssessmentInitialValues: {
assessmentWorkflowId: "some fake id",
studentAssessmentStatus: StudentAssessmentStatus.Completed,
assessmentDate: addDays(30, programYear.startDate),
},
},
);
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);

// Act
const result = await assessmentController.verifyAssessmentCalculationOrder(
createFakeVerifyAssessmentCalculationOrderPayload(
currentApplication.currentAssessment.id,
),
);
// Assert
expect(FakeWorkerJobResult.getResultType(result)).toBe(
MockedZeebeJobResult.Complete,
);
// The calculation will only take SFAS and SFAS part time application data where the start date is the date before the first assessment date of the current application.
expect(FakeWorkerJobResult.getOutputVariables(result)).toStrictEqual({
isReadyForCalculation: true,
// Student contribution total.
programYearTotalFederalFSC: 6000,
programYearTotalProvincialFSC: 8000,
programYearTotalScholarshipBursaries: 4000,
programYearTotalSpouseContributionWeeks: 2000,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { MaxJobsToActivate } from "../../types";
import {
AssessmentSequentialProcessingService,
AwardTotal,
FTProgramYearContributionTotal,
ProgramYearTotal,
SystemUsersService,
WorkflowClientService,
} from "@sims/services";
Expand Down Expand Up @@ -372,6 +374,7 @@ export class AssessmentController {
const assessmentId = job.variables.assessmentId;
const studentId = assessment.application.student.id;
const programYearId = assessment.application.programYear.id;
const offeringIntensity = assessment.offering.offeringIntensity;
jobLogger.log(
`Verifying the assessment calculation order for processing assessment id ${assessmentId} student id ${studentId} ` +
`program year id ${programYearId}.`,
Expand Down Expand Up @@ -420,16 +423,21 @@ export class AssessmentController {
this.studentAssessmentService.saveAssessmentCalculationStartDate(
assessmentId,
);
const getProgramYearTotalAwards =
this.assessmentSequentialProcessingService.getProgramYearPreviousAwardsTotals(
assessmentId,
{ alternativeReferenceDate: new Date() },
);
// Updates the calculation start date and get the program year totals in parallel.
const [, programYearTotalAwards] = await Promise.all([
saveAssessmentCalculationStartDate,
getProgramYearTotalAwards,
]);
const programYearTotals = await this.getProgramYearTotals(
assessmentId,
{
alternativeReferenceDate: new Date(),
offeringIntensity: offeringIntensity,
},
);
// Updates the calculation start date and get the program year totals and for
// full time the contribution program year totals in parallel.
andrepestana-aot marked this conversation as resolved.
Show resolved Hide resolved
const [, programYearTotalAwards, ftProgramYearContributionTotal] =
await Promise.all([
saveAssessmentCalculationStartDate,
programYearTotals.awardTotal,
programYearTotals.ftProgramYearContributionTotal,
]);
if (
assessment.offering.offeringIntensity === OfferingIntensity.partTime
) {
Expand All @@ -442,7 +450,11 @@ export class AssessmentController {
jobLogger.log(
`The assessment calculation order has been verified and the assessment id ${assessmentId} is ready to be processed.`,
);
this.createOutputForProgramYearTotals(programYearTotalAwards, result);
this.createOutputForProgramYearTotals(
programYearTotalAwards,
ftProgramYearContributionTotal,
result,
);
result.isReadyForCalculation = true;
return job.complete(result);
}
Expand All @@ -469,6 +481,7 @@ export class AssessmentController {
*/
private createOutputForProgramYearTotals(
programYearTotalAwards: AwardTotal[],
ftProgramYearContributionTotal: FTProgramYearContributionTotal[],
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
output: VerifyAssessmentCalculationOrderJobOutDTO,
): void {
// Create the dynamic variables to be outputted.
Expand All @@ -482,6 +495,65 @@ export class AssessmentController {
? output[outputName] + award.total
: award.total;
});
ftProgramYearContributionTotal?.forEach((ftContributionTotal) => {
const outputName = `programYearTotal${ftContributionTotal.contribution}`;
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
output[outputName] = output[outputName]
? output[outputName] + ftContributionTotal.total
: ftContributionTotal.total;
});
}

/**
* Get the promises of the program year awards totals for part time and
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
* full time and contribution totals for full time for the assessment.
andrepestana-aot marked this conversation as resolved.
Show resolved Hide resolved
* @param assessmentId assessment id.
* @param options method options.
* - `offeringIntensity` the offering intensity to be used.
* - `alternativeReferenceDate` the reference date to be used.
* @returns the promise to get the program year totals.
*/
private async getProgramYearTotals(
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
assessmentId: number,
options?: {
offeringIntensity?: OfferingIntensity;
alternativeReferenceDate?: Date;
},
): Promise<ProgramYearTotal> {
// Get the current assessment from the assessment id.
const currentAssessment =
await this.assessmentSequentialProcessingService.getCurrentAssessment(
assessmentId,
);
// The chronology of the applications is defined by the method {@link getSequencedApplications}.
// Only the current assessment awards are considered since it must reflect the most updated
// workflow calculated values.
const sequencedApplications =
await this.assessmentSequentialProcessingService.getSequencedApplications(
currentAssessment.application.applicationNumber,
currentAssessment.application.student.id,
currentAssessment.application.programYear.id,
{ alternativeReferenceDate: options?.alternativeReferenceDate },
);
// Get the application numbers of the previous applications.
const applicationNumbers = sequencedApplications.previous.map(
(application) => application.applicationNumber,
);
return {
// Get the program year awards totals for part time and full time.
andrepestana-aot marked this conversation as resolved.
Show resolved Hide resolved
awardTotal:
this.assessmentSequentialProcessingService.getProgramYearPreviousAwardsTotals(
sequencedApplications,
currentAssessment,
options,
),
// Only get the full time contribution totals if the offering intensity is full time.
ftProgramYearContributionTotal:
OfferingIntensity.fullTime === options.offeringIntensity
? this.assessmentSequentialProcessingService.getFTProgramYearContributionTotals(
applicationNumbers,
)
: null,
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OfferingIntensity } from "@sims/sims-db";
import { OfferingIntensity, StudentAssessment } from "@sims/sims-db";

/**
* Application information part of a sequence of ordered applications.
Expand Down Expand Up @@ -124,3 +124,28 @@ export interface AwardTotal {
valueCode: string;
total: number;
}

/**
* Program year totals with award code (e.g. CSPT, CSGD, CSGP, SBSD, BCAG) and its totals promise
* and for full time program year contribution (e.g. ScholarshipBursaries, SpouseContributionWeeks, FederalFSC, ProvincialFSC) and its totals promise.
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
*/
export interface ProgramYearTotal {
awardTotal: Promise<AwardTotal[]>;
ftProgramYearContributionTotal?: Promise<FTProgramYearContributionTotal[]>;
}

/**
* Full time program year contribution and its totals.
*/
export interface FTProgramYearContributionTotal {
contribution: string;
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
total: number;
}

/**
* Sequenced applications and current assessment.
*/
export interface SequencedApplicationsWithAssessment {
sequencedApplications: SequencedApplications;
andrewsignori-aot marked this conversation as resolved.
Show resolved Hide resolved
currentAssessment: StudentAssessment;
}
Loading
Loading