From 58950b948ad5585578aaf002f68a770f9ccd3258 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Fri, 2 Jan 2026 10:28:43 -0700 Subject: [PATCH 01/13] initial commit --- .../disbursement-schedule.models.ts | 68 +++++++++++++++++++ .../e-cert-generation.service.ts | 27 ++++++++ 2 files changed, 95 insertions(+) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index 1b9bdcd5b9..e6dfb225fb 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -4,8 +4,10 @@ import { DisabilityStatus, DisbursementSchedule, DisbursementValueType, + EducationProgram, EducationProgramOffering, FormYesNoOptions, + InstitutionLocation, ModifiedIndependentStatus, RestrictionActionType, RestrictionBypassBehaviors, @@ -186,6 +188,36 @@ export interface StudentActiveRestriction { actionEffectiveConditions?: ActionEffectiveCondition[]; } +/** + * Represents an active institution restriction. + */ +export interface InstitutionActiveRestriction { + /** + * Restriction id. + */ + id: number; + /** + * Association between the institution and + * the active restriction on institution account. + */ + institutionRestrictionId: number; + /** + * Specific program the restriction applies to. + */ + program: EducationProgram; + /** + * Specific location the restriction applies to. + */ + location: InstitutionLocation; + /** + * Restriction code. + */ + code: RestrictionCode; + /** + * Actions associated with the restriction. + */ + actions: RestrictionActionType[]; +} /** * Restriction bypass active for the application. */ @@ -219,6 +251,8 @@ export type EligibleECertOffering = Pick< | "programRelatedCosts" | "mandatoryFees" | "aviationCredentialType" + | "educationProgram" + | "institutionLocation" >; /** @@ -268,6 +302,7 @@ export class EligibleECertDisbursement { readonly modifiedIndependentDetails: ModifiedIndependentDetails, private readonly restrictions: StudentActiveRestriction[], private readonly restrictionBypass: ApplicationActiveRestrictionBypass[], + private readonly restrictionsInstitution: InstitutionActiveRestriction[], ) { this.studentRestrictionsBypassedIds = this.restrictionBypass.map( (bypass) => bypass.studentRestrictionId, @@ -286,6 +321,18 @@ export class EligibleECertDisbursement { this.restrictions.push(...activeRestrictions); } + /** + * Refresh the complete list of institution restrictions. + * @param activeRestrictionsInstitution represents the most updated + * snapshot of all institution active restrictions. + */ + refreshActiveInstitutionRestrictions( + activeRestrictionsInstitution: InstitutionActiveRestriction[], + ): void { + this.restrictionsInstitution.length = 0; + this.restrictionsInstitution.push(...activeRestrictionsInstitution); + } + /** * All student active restrictions. */ @@ -293,6 +340,13 @@ export class EligibleECertDisbursement { return this.restrictions; } + /** + * All institution active restrictions. + */ + get activeInstitutionRestrictions(): ReadonlyArray { + return this.restrictionsInstitution; + } + /** * All application active restrictions bypasses. */ @@ -314,6 +368,20 @@ export class EligibleECertDisbursement { ), ); } + + /** + * List the effective institution restrictions for the given disbursement. + * @returns Effective institution restrictions. + */ + getEffectiveInstitutionRestrictions(): ReadonlyArray { + const programId = this.offering.educationProgram.id; + const locationId = this.offering.institutionLocation.id; + return this.restrictionsInstitution.filter( + (restriction) => + restriction.program.id === programId && + restriction.location.id === locationId, + ); + } } /** diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts index 70a6b40c35..4f024a615a 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts @@ -90,6 +90,16 @@ export class ECertGenerationService { "offering.programRelatedCosts", "offering.mandatoryFees", "offering.aviationCredentialType", + "educationProgram.id", + "institutionLocation.id", + "institution.id", + "institutionRestriction.id", + "institutionRestrictionProgram.id", + "institutionRestrictionLocation.id", + // The property restrictionInstitution is the restriction added to the institution account. + "restrictionInstitution.id", + "restrictionInstitution.restrictionCode", + "restrictionInstitution.actionType", "student.id", "student.disabilityStatus", "student.modifiedIndependentStatus", @@ -119,6 +129,9 @@ export class ECertGenerationService { .leftJoin("disbursementSchedule.disbursementValues", "disbursementValue") .leftJoin("disbursementSchedule.msfaaNumber", "msfaaNumber") .innerJoin("currentAssessment.offering", "offering") + .innerJoin("offering.educationProgram", "educationProgram") + .innerJoin("offering.institutionLocation", "institutionLocation") + .innerJoin("institutionLocation.institution", "institution") .innerJoin("application.programYear", "programYear") .innerJoin("application.student", "student") .innerJoin("student.sinValidation", "sinValidation") @@ -141,6 +154,20 @@ export class ECertGenerationService { "restrictionBypassStudentRestriction.restriction", "restrictionBypassStudentRestrictionRestriction", ) + .leftJoin( + "institution.restrictions", + "institutionRestriction", + "institutionRestriction.isActive = true", + ) + .leftJoin("institutionRestriction.restriction", "restrictionInstitution") + .leftJoin( + "institutionRestriction.program", + "institutionRestrictionProgram", + ) + .leftJoin( + "institutionRestriction.location", + "institutionRestrictionLocation", + ) .where( "disbursementSchedule.disbursementScheduleStatus = :disbursementScheduleStatus", { disbursementScheduleStatus: DisbursementScheduleStatus.Pending }, From bed82b0f455db692e3d3dab4a33e17434e2b89ac Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Fri, 2 Jan 2026 13:00:52 -0700 Subject: [PATCH 02/13] initial commit --- .../disbursement-schedule.models.ts | 46 ++++++++++++++++--- .../e-cert-generation.service.ts | 20 ++++++++ .../e-cert-steps-utils.ts | 23 ++++++++++ .../validate-disbursement-part-time-step.ts | 31 +++++++++++-- .../restriction/model/restriction.model.ts | 14 ++++++ 5 files changed, 123 insertions(+), 11 deletions(-) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index e6dfb225fb..3bb9203ce0 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -8,6 +8,7 @@ import { EducationProgramOffering, FormYesNoOptions, InstitutionLocation, + InstitutionRestriction, ModifiedIndependentStatus, RestrictionActionType, RestrictionBypassBehaviors, @@ -217,6 +218,10 @@ export interface InstitutionActiveRestriction { * Actions associated with the restriction. */ actions: RestrictionActionType[]; + /** + * Action effective conditions associated with the restriction. + */ + actionEffectiveConditions?: ActionEffectiveCondition[]; } /** * Restriction bypass active for the application. @@ -266,6 +271,7 @@ export class EligibleECertDisbursement { * Creates a new instance of a eligible e-Cert to be calculated. * @param studentId student id. * @param hasValidSIN indicates if the student has a validated SIN. + * @param institutionId institution id. * @param assessmentId assessment id. * @param applicationId application id. * @param applicationNumber application number. Intended to be used @@ -288,10 +294,12 @@ export class EligibleECertDisbursement { * be updated using the method {@link refreshActiveStudentRestrictions} to allow all * steps to have access to the most updated data. * @param restrictionBypass all active restrictions bypasses applied to the student application. + * @param institutionRestrictions all active institution restrictions for the application institution. */ constructor( readonly studentId: number, readonly hasValidSIN: boolean, + readonly institutionId: number, readonly assessmentId: number, readonly applicationId: number, readonly applicationNumber: string, @@ -302,7 +310,7 @@ export class EligibleECertDisbursement { readonly modifiedIndependentDetails: ModifiedIndependentDetails, private readonly restrictions: StudentActiveRestriction[], private readonly restrictionBypass: ApplicationActiveRestrictionBypass[], - private readonly restrictionsInstitution: InstitutionActiveRestriction[], + private readonly institutionRestrictions: InstitutionActiveRestriction[], ) { this.studentRestrictionsBypassedIds = this.restrictionBypass.map( (bypass) => bypass.studentRestrictionId, @@ -323,14 +331,14 @@ export class EligibleECertDisbursement { /** * Refresh the complete list of institution restrictions. - * @param activeRestrictionsInstitution represents the most updated + * @param activeInstitutionRestrictions represents the most updated * snapshot of all institution active restrictions. */ refreshActiveInstitutionRestrictions( - activeRestrictionsInstitution: InstitutionActiveRestriction[], + activeInstitutionRestrictions: InstitutionActiveRestriction[], ): void { - this.restrictionsInstitution.length = 0; - this.restrictionsInstitution.push(...activeRestrictionsInstitution); + this.institutionRestrictions.length = 0; + this.institutionRestrictions.push(...activeInstitutionRestrictions); } /** @@ -344,7 +352,7 @@ export class EligibleECertDisbursement { * All institution active restrictions. */ get activeInstitutionRestrictions(): ReadonlyArray { - return this.restrictionsInstitution; + return this.institutionRestrictions; } /** @@ -376,7 +384,7 @@ export class EligibleECertDisbursement { getEffectiveInstitutionRestrictions(): ReadonlyArray { const programId = this.offering.educationProgram.id; const locationId = this.offering.institutionLocation.id; - return this.restrictionsInstitution.filter( + return this.institutionRestrictions.filter( (restriction) => restriction.program.id === programId && restriction.location.id === locationId, @@ -405,6 +413,30 @@ export function mapStudentActiveRestrictions( ); } +/** + * Map institution restrictions to the representation of active + * restrictions used along e-Cert calculations. + * @param institutionRestrictions institution active restrictions to be mapped. + * @returns simplified institution active restrictions. + */ +export function mapInstitutionActiveRestrictions( + institutionRestrictions: InstitutionRestriction[], +): InstitutionActiveRestriction[] { + return institutionRestrictions.map( + (institutionRestriction) => ({ + institutionRestrictionId: institutionRestriction.id, + id: institutionRestriction.restriction.id, + code: institutionRestriction.restriction + .restrictionCode as RestrictionCode, + actions: institutionRestriction.restriction.actionType, + program: institutionRestriction.program, + location: institutionRestriction.location, + actionEffectiveConditions: + institutionRestriction.restriction.actionEffectiveConditions, + }), + ); +} + /** * Possible failed results from an e-Cert validation. * An disbursement could be either ready to be disbursed and just waiting diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts index 4f024a615a..ea9ac75903 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-generation.service.ts @@ -17,8 +17,10 @@ import { ApplicationActiveRestrictionBypass, DisabilityDetails, EligibleECertDisbursement, + InstitutionActiveRestriction, ModifiedIndependentDetails, StudentActiveRestriction, + mapInstitutionActiveRestrictions, mapStudentActiveRestrictions, } from "./disbursement-schedule.models"; import { ConfigService } from "@sims/utilities/config"; @@ -100,6 +102,7 @@ export class ECertGenerationService { "restrictionInstitution.id", "restrictionInstitution.restrictionCode", "restrictionInstitution.actionType", + "restrictionInstitution.actionEffectiveConditions", "student.id", "student.disabilityStatus", "student.modifiedIndependentStatus", @@ -224,6 +227,11 @@ export class ECertGenerationService { // across all disbursements. const groupedStudentRestrictions = this.getGroupedStudentRestrictions(eligibleApplications); + // Grouped institution restrictions grouped by institution id. + const groupedInstitutionRestrictions: Record< + number, + InstitutionActiveRestriction[] + > = {}; // Convert the application records to be returned as disbursements to allow // easier processing along the calculation steps. const eligibleDisbursements = @@ -232,6 +240,16 @@ export class ECertGenerationService { return application.currentAssessment.disbursementSchedules.map( (disbursement) => { const student = application.student; + const institutionId = + application.currentAssessment.offering.institutionLocation + .institution.id; + if (!groupedInstitutionRestrictions[institutionId]) { + const institutionRestrictions = + application.currentAssessment.offering.institutionLocation + .institution.restrictions; + groupedInstitutionRestrictions[institutionId] = + mapInstitutionActiveRestrictions(institutionRestrictions); + } const disabilityDetails: DisabilityDetails = { calculatedPDPPDStatus: workflowData.calculatedData.pdppdStatus, studentProfileDisabilityStatus: student.disabilityStatus, @@ -255,6 +273,7 @@ export class ECertGenerationService { return new EligibleECertDisbursement( student.id, !!student.sinValidation.isValidSIN, + institutionId, application.currentAssessment.id, application.id, application.applicationNumber, @@ -265,6 +284,7 @@ export class ECertGenerationService { modifiedIndependentDetails, groupedStudentRestrictions[student.id], restrictionBypasses, + groupedInstitutionRestrictions[institutionId], ); }, ); diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts index 9ad0162212..433e26f6ef 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts @@ -7,6 +7,7 @@ import { import { ApplicationActiveRestrictionBypass, EligibleECertDisbursement, + InstitutionActiveRestriction, StudentActiveRestriction, } from "../disbursement-schedule.models"; import { RestrictionCode } from "@sims/services"; @@ -37,6 +38,28 @@ export function getRestrictionsByActionType( ); } +/** + * Check active institution restrictions by its action type in an eligible disbursement. + * @param eCertDisbursement disbursement to check institution restrictions. + * @param actionType action type. + * @returns the all the effective restrictions of the requested action type. + */ +export function getInstitutionRestrictionsByActionType( + eCertDisbursement: EligibleECertDisbursement, + actionType: RestrictionActionType, +): InstitutionActiveRestriction[] { + return eCertDisbursement + .getEffectiveInstitutionRestrictions() + .filter( + (restriction) => + restriction.actions.includes(actionType) && + isRestrictionActionEffective( + restriction.actionEffectiveConditions, + eCertDisbursement, + ), + ); +} + /** * Adds to the log the lists of all restriction bypasses active. * @param bypasses active bypasses. diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts index 9bf9c77f5a..db05b9c7da 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts @@ -8,6 +8,7 @@ import { EligibleECertDisbursement, } from "../disbursement-schedule.models"; import { + getInstitutionRestrictionsByActionType, getRestrictionsByActionType, logActiveRestrictionsBypasses, } from "./e-cert-steps-utils"; @@ -74,18 +75,18 @@ export class ValidateDisbursementPartTimeStep log.info("Executing part-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); // Validate stop part-time disbursement restrictions. - const stopPartTimeDisbursementRestrictions = getRestrictionsByActionType( + const stopDisbursementRestrictions = getRestrictionsByActionType( eCertDisbursement, RestrictionActionType.StopPartTimeDisbursement, ); - if (stopPartTimeDisbursementRestrictions.length) { + if (stopDisbursementRestrictions.length) { log.info( `Student has an active '${RestrictionActionType.StopPartTimeDisbursement}' restriction and the disbursement calculation will not proceed.`, ); validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementRestriction, additionalInfo: { - restrictionCodes: stopPartTimeDisbursementRestrictions.map( + restrictionCodes: stopDisbursementRestrictions.map( (restriction) => restriction.code, ), }, @@ -95,6 +96,28 @@ export class ValidateDisbursementPartTimeStep eCertDisbursement.activeRestrictionBypasses, log, ); + // Validate stop part-time disbursement restrictions on the institution. + const stopDisbursementInstitutionRestrictions = + getInstitutionRestrictionsByActionType( + eCertDisbursement, + RestrictionActionType.StopPartTimeDisbursement, + ); + if (stopDisbursementInstitutionRestrictions.length) { + const program = eCertDisbursement.offering.educationProgram; + const location = eCertDisbursement.offering.institutionLocation; + log.info( + `Institution ${eCertDisbursement.institutionId} has an effective '${RestrictionActionType.StopPartTimeDisbursement}' restriction` + + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, + ); + validationResults.push({ + resultType: ECertFailedValidation.HasStopDisbursementRestriction, + additionalInfo: { + restrictionCodes: stopDisbursementInstitutionRestrictions.map( + (restriction) => restriction.code, + ), + }, + }); + } // Validate CSLP. const validateLifetimeMaximumCSLP = await this.validateCSLPLifetimeMaximum( eCertDisbursement, @@ -119,7 +142,7 @@ export class ValidateDisbursementPartTimeStep eCertDisbursement: EligibleECertDisbursement, entityManager: EntityManager, log: ProcessSummary, - ) { + ): Promise { log.info("Validate CSLP Lifetime Maximum."); // Get the disbursed value for the CSLP in the current disbursement. const disbursementCSLP = diff --git a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts index ad29d73fec..8f02924d74 100644 --- a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts +++ b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts @@ -74,3 +74,17 @@ export enum RestrictionCode { */ SFAS_AV = "SFAS_AV", } + +/** + * The party that is restricted by an active restriction. + */ +export enum RestrictedParty { + /** + * Student. + */ + Student = "Student", + /** + * Institution. + */ + Institution = "Institution", +} From 344bfc32a12237a45beffa3c1d3ce7ddb3ba8d37 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Fri, 2 Jan 2026 16:51:29 -0700 Subject: [PATCH 03/13] Institution restrictions E-Cert validations --- .../disbursement-schedule.models.ts | 5 +++ .../e-cert-pre-validation-service-models.ts | 1 + .../e-cert-steps-utils.ts | 43 +++++++++++++------ .../validate-disbursement-full-time-step.ts | 27 ++++++++++-- .../validate-disbursement-part-time-step.ts | 16 +++---- .../restriction/model/restriction.model.ts | 14 ------ 6 files changed, 64 insertions(+), 42 deletions(-) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index 3bb9203ce0..b2abb02ce1 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -473,6 +473,11 @@ export enum ECertFailedValidation { * restriction and the disbursement calculation will not proceed. */ HasStopDisbursementRestriction = "HasStopDisbursementRestriction", + /** + * Institution has an active 'StopFullTimeDisbursement' or 'StopPartTimeDisbursement' + * restriction and the disbursement calculation will not proceed. + */ + HasStopDisbursementInstitutionRestriction = "HasStopDisbursementInstitutionRestriction", /** * Lifetime maximum CSLP is reached. * Affects only part-time disbursements. diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-calculation/e-cert-pre-validation-service-models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-calculation/e-cert-pre-validation-service-models.ts index 0240c8ffa4..bb833f00c7 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-calculation/e-cert-pre-validation-service-models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-calculation/e-cert-pre-validation-service-models.ts @@ -11,6 +11,7 @@ const ACCEPT_ASSESSMENT_BLOCKING_VALIDATIONS = [ ECertFailedValidation.MSFAACanceled, ECertFailedValidation.MSFAANotSigned, ECertFailedValidation.HasStopDisbursementRestriction, + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, ECertFailedValidation.NoEstimatedAwardAmounts, ECertFailedValidation.ModifiedIndependentStatusNotApproved, ]; diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts index 433e26f6ef..c1083347c4 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts @@ -1,5 +1,6 @@ import { BC_FUNDING_TYPES } from "@sims/services/constants"; import { + ActionEffectiveCondition, DisbursementValue, OfferingIntensity, RestrictionActionType, @@ -28,13 +29,8 @@ export function getRestrictionsByActionType( ): StudentActiveRestriction[] { return eCertDisbursement .getEffectiveRestrictions() - .filter( - (restriction) => - restriction.actions.includes(actionType) && - isRestrictionActionEffective( - restriction.actionEffectiveConditions, - eCertDisbursement, - ), + .filter((restriction) => + hasEffectiveRestrictionAction(eCertDisbursement, actionType, restriction), ); } @@ -50,16 +46,35 @@ export function getInstitutionRestrictionsByActionType( ): InstitutionActiveRestriction[] { return eCertDisbursement .getEffectiveInstitutionRestrictions() - .filter( - (restriction) => - restriction.actions.includes(actionType) && - isRestrictionActionEffective( - restriction.actionEffectiveConditions, - eCertDisbursement, - ), + .filter((restriction) => + hasEffectiveRestrictionAction(eCertDisbursement, actionType, restriction), ); } +/** + * Check if a restriction has an effective action of the given action type. + * @param eCertDisbursement disbursement to check restriction conditions. + * @param actionType action type. + * @param restriction restriction to be checked. + * @returns true if the restriction has the effective action, otherwise, false. + */ +function hasEffectiveRestrictionAction( + eCertDisbursement: EligibleECertDisbursement, + actionType: RestrictionActionType, + restriction: { + actions: RestrictionActionType[]; + actionEffectiveConditions?: ActionEffectiveCondition[]; + }, +): boolean { + return ( + restriction.actions.includes(actionType) && + isRestrictionActionEffective( + restriction.actionEffectiveConditions, + eCertDisbursement, + ) + ); +} + /** * Adds to the log the lists of all restriction bypasses active. * @param bypasses active bypasses. diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts index 194819855a..556fd47592 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts @@ -12,6 +12,7 @@ import { EligibleECertDisbursement, } from "../disbursement-schedule.models"; import { + getInstitutionRestrictionsByActionType, getRestrictionsByActionType, logActiveRestrictionsBypasses, } from "./e-cert-steps-utils"; @@ -67,19 +68,19 @@ export class ValidateDisbursementFullTimeStep ): Promise { log.info("Executing full-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); - // Validate stop full-time disbursement restrictions. - const stopFullTimeDisbursementRestrictions = getRestrictionsByActionType( + // Validate stop full-time disbursement restrictions on the student. + const stopDisbursementStudentRestrictions = getRestrictionsByActionType( eCertDisbursement, RestrictionActionType.StopFullTimeDisbursement, ); - if (stopFullTimeDisbursementRestrictions.length) { + if (stopDisbursementStudentRestrictions.length) { log.info( `Student has an active '${RestrictionActionType.StopFullTimeDisbursement}' restriction and the disbursement calculation will not proceed.`, ); validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementRestriction, additionalInfo: { - restrictionCodes: stopFullTimeDisbursementRestrictions.map( + restrictionCodes: stopDisbursementStudentRestrictions.map( (restriction) => restriction.code, ), }, @@ -89,6 +90,24 @@ export class ValidateDisbursementFullTimeStep eCertDisbursement.activeRestrictionBypasses, log, ); + // Validate stop full-time disbursement restrictions on the institution. + const stopDisbursementInstitutionRestrictions = + getInstitutionRestrictionsByActionType( + eCertDisbursement, + RestrictionActionType.StopFullTimeDisbursement, + ); + if (stopDisbursementInstitutionRestrictions.length) { + const program = eCertDisbursement.offering.educationProgram; + const location = eCertDisbursement.offering.institutionLocation; + log.info( + `Institution ${eCertDisbursement.institutionId} has an effective '${RestrictionActionType.StopFullTimeDisbursement}' restriction` + + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, + ); + validationResults.push({ + resultType: + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + }); + } // Validate modified independent status when estranged from parents. if ( eCertDisbursement.modifiedIndependentDetails.estrangedFromParents === diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts index db05b9c7da..2f8641ae96 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts @@ -74,19 +74,19 @@ export class ValidateDisbursementPartTimeStep ): Promise { log.info("Executing part-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); - // Validate stop part-time disbursement restrictions. - const stopDisbursementRestrictions = getRestrictionsByActionType( + // Validate stop part-time disbursement restrictions on the student. + const stopDisbursementStudentRestrictions = getRestrictionsByActionType( eCertDisbursement, RestrictionActionType.StopPartTimeDisbursement, ); - if (stopDisbursementRestrictions.length) { + if (stopDisbursementStudentRestrictions.length) { log.info( `Student has an active '${RestrictionActionType.StopPartTimeDisbursement}' restriction and the disbursement calculation will not proceed.`, ); validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementRestriction, additionalInfo: { - restrictionCodes: stopDisbursementRestrictions.map( + restrictionCodes: stopDisbursementStudentRestrictions.map( (restriction) => restriction.code, ), }, @@ -110,12 +110,8 @@ export class ValidateDisbursementPartTimeStep ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, ); validationResults.push({ - resultType: ECertFailedValidation.HasStopDisbursementRestriction, - additionalInfo: { - restrictionCodes: stopDisbursementInstitutionRestrictions.map( - (restriction) => restriction.code, - ), - }, + resultType: + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, }); } // Validate CSLP. diff --git a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts index 8f02924d74..ad29d73fec 100644 --- a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts +++ b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts @@ -74,17 +74,3 @@ export enum RestrictionCode { */ SFAS_AV = "SFAS_AV", } - -/** - * The party that is restricted by an active restriction. - */ -export enum RestrictedParty { - /** - * Student. - */ - Student = "Student", - /** - * Institution. - */ - Institution = "Institution", -} From 759c9dda459b16519bda8efc995530cfc5f41617 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Sun, 4 Jan 2026 16:39:06 -0700 Subject: [PATCH 04/13] Added E2E Tests for the queue consumers. --- ...-process-integration.scheduler.e2e-spec.ts | 119 +++++++- ...-process-integration.scheduler.e2e-spec.ts | 258 +++++++++++++++++- .../validate-disbursement-full-time-step.ts | 2 +- .../validate-disbursement-part-time-step.ts | 2 +- .../types/contracts/ECertFailedValidation.ts | 5 + 5 files changed, 378 insertions(+), 8 deletions(-) diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts index d7883e2c8b..74353be786 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts @@ -16,6 +16,7 @@ import { RelationshipStatus, RestrictionActionType, RestrictionBypassBehaviors, + RestrictionType, User, WorkflowData, } from "@sims/sims-db"; @@ -30,6 +31,7 @@ import { createFakeUser, saveFakeApplicationDisbursements, saveFakeApplicationRestrictionBypass, + saveFakeInstitutionRestriction, saveFakeSFASIndividual, saveFakeStudent, saveFakeStudentAssessment, @@ -1915,6 +1917,116 @@ describe( ]); }); + it( + "Should block the disbursement and log the information when the institution" + + " has an effective institution restriction for the application location and program" + + ` with action type ${RestrictionActionType.StopFullTimeDisbursement}.`, + async () => { + // Arrange + // Eligible COE basic properties. + const eligibleDisbursement: Partial = { + coeStatus: COEStatus.completed, + coeUpdatedAt: new Date(), + }; + // Student with valid SIN. + const student = await saveFakeStudent(db.dataSource); + // Valid MSFAA Number. + const msfaaNumber = await db.msfaaNumber.save( + createFakeMSFAANumber( + { student }, + { msfaaState: MSFAAStates.Signed }, + ), + ); + const application = await saveFakeApplicationDisbursements( + db.dataSource, + { + student, + msfaaNumber, + firstDisbursementValues: [ + createFakeDisbursementValue( + DisbursementValueType.CanadaLoan, + "CSLP", + 100, + ), + createFakeDisbursementValue( + DisbursementValueType.BCLoan, + "CSPT", + 150, + ), + ], + }, + { + offeringIntensity: OfferingIntensity.fullTime, + applicationStatus: ApplicationStatus.Completed, + currentAssessmentInitialValues: { + assessmentData: { weeks: 5 } as Assessment, + assessmentDate: new Date(), + }, + firstDisbursementInitialValues: { + ...eligibleDisbursement, + disbursementDate: getISODateOnlyString(addDays(1)), + }, + }, + ); + // Institution restriction that blocks disbursement. + const restriction = await db.restriction.findOne({ + select: { id: true }, + where: { + restrictionType: RestrictionType.Institution, + actionType: ArrayContains([ + RestrictionActionType.StopFullTimeDisbursement, + ]), + }, + }); + const location = + application.currentAssessment.offering.institutionLocation; + const program = application.currentAssessment.offering.educationProgram; + const institution = location.institution; + // Add institution restriction for the application location and program. + await saveFakeInstitutionRestriction(db, { + restriction, + institution, + program, + location, + }); + // Queued job. + const mockedJob = mockBullJob(); + + // Act + const result = await processor.processQueue(mockedJob.job); + + // Assert + // Assert uploaded file. + const uploadedFile = getUploadedFile(sftpClientMock); + const uploadedFileName = getUploadedFileName(); + expect(uploadedFile.remoteFilePath).toBe(uploadedFileName); + // No records should be sent. + expect(result).toStrictEqual([ + `Generated file: ${uploadedFileName}`, + "Uploaded records: 0", + ]); + // Assert log messages for the blocked disbursement. + expect( + mockedJob.containLogMessages([ + `Institution has an effective '${RestrictionActionType.StopFullTimeDisbursement}' restriction` + + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, + "The step determined that the calculation should be interrupted. This disbursement will not be part of the next e-Cert generation.", + ]), + ).toBe(true); + const [disbursement] = + application.currentAssessment.disbursementSchedules; + // Assert that the disbursement is still in status 'Pending' with date sent null. + const scheduleIsPending = await db.disbursementSchedule.exists({ + where: { + id: disbursement.id, + dateSent: IsNull(), + disbursementScheduleStatus: DisbursementScheduleStatus.Pending, + }, + }); + expect(scheduleIsPending).toBe(true); + }, + ); + describe("Aviation credential full-time applications e-Cert generation", () => { for (const { aviationCredentialType, @@ -1936,7 +2048,12 @@ describe( const msfaaNumber = await db.msfaaNumber.save( createFakeMSFAANumber( { student }, - { msfaaState: MSFAAStates.Signed }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.fullTime, + }, + }, ), ); const aviationCredentialApplication = diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts index 2381398f76..edd845e069 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts @@ -12,6 +12,7 @@ import { RelationshipStatus, RestrictionActionType, RestrictionBypassBehaviors, + RestrictionType, Student, User, } from "@sims/sims-db"; @@ -30,6 +31,7 @@ import { saveFakeStudentRestriction, createFakeDisbursementOveraward, RestrictionCode, + saveFakeInstitutionRestriction, } from "@sims/test-utils"; import { getUploadedFile } from "@sims/test-utils/mocks"; import { ArrayContains, IsNull, Like, Not } from "typeorm"; @@ -1513,6 +1515,232 @@ describe( ).toBe(true); }); + it( + "Should block the disbursement and log the information when the institution" + + " has an effective institution restriction for the application location and program" + + ` with action type ${RestrictionActionType.StopPartTimeDisbursement}.`, + async () => { + // Arrange + // Eligible COE basic properties. + const eligibleDisbursement: Partial = { + coeStatus: COEStatus.completed, + coeUpdatedAt: new Date(), + }; + // Student with valid SIN. + const student = await saveFakeStudent(db.dataSource); + // Valid MSFAA Number. + const msfaaNumber = await db.msfaaNumber.save( + createFakeMSFAANumber( + { student }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, + ), + ); + const application = await saveFakeApplicationDisbursements( + db.dataSource, + { + student, + msfaaNumber, + firstDisbursementValues: [ + createFakeDisbursementValue( + DisbursementValueType.CanadaLoan, + "CSLP", + 100, + ), + createFakeDisbursementValue( + DisbursementValueType.BCLoan, + "CSPT", + 150, + ), + ], + }, + { + offeringIntensity: OfferingIntensity.partTime, + applicationStatus: ApplicationStatus.Completed, + currentAssessmentInitialValues: { + assessmentData: { weeks: 5 } as Assessment, + assessmentDate: new Date(), + }, + firstDisbursementInitialValues: { + ...eligibleDisbursement, + disbursementDate: getISODateOnlyString(addDays(1)), + }, + }, + ); + // Institution restriction that blocks disbursement. + const restriction = await db.restriction.findOne({ + select: { id: true }, + where: { + restrictionType: RestrictionType.Institution, + actionType: ArrayContains([ + RestrictionActionType.StopPartTimeDisbursement, + ]), + }, + }); + const location = + application.currentAssessment.offering.institutionLocation; + const program = application.currentAssessment.offering.educationProgram; + const institution = location.institution; + // Add institution restriction for the application location and program. + await saveFakeInstitutionRestriction(db, { + restriction, + institution, + program, + location, + }); + // Queued job. + const mockedJob = mockBullJob(); + + // Act + const result = await processor.processQueue(mockedJob.job); + + // Assert + // Assert uploaded file. + const uploadedFile = getUploadedFile(sftpClientMock); + const uploadedFileName = getUploadedFileName(); + expect(uploadedFile.remoteFilePath).toBe(uploadedFileName); + // No records should be sent. + expect(result).toStrictEqual([ + `Generated file: ${uploadedFileName}`, + "Uploaded records: 0", + ]); + // Assert log messages for the blocked disbursement. + expect( + mockedJob.containLogMessages([ + `Institution has an effective '${RestrictionActionType.StopPartTimeDisbursement}' restriction` + + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, + "The step determined that the calculation should be interrupted. This disbursement will not be part of the next e-Cert generation.", + ]), + ).toBe(true); + const [disbursement] = + application.currentAssessment.disbursementSchedules; + // Assert that the disbursement is still in status 'Pending' with date sent null. + const scheduleIsPending = await db.disbursementSchedule.exists({ + where: { + id: disbursement.id, + dateSent: IsNull(), + disbursementScheduleStatus: DisbursementScheduleStatus.Pending, + }, + }); + expect(scheduleIsPending).toBe(true); + }, + ); + + it( + "Should create the e-Cert and log the information when the institution" + + " does not have an effective institution restriction for the application location and program" + + ` with action type ${RestrictionActionType.StopPartTimeDisbursement}.`, + async () => { + // Arrange + // Eligible COE basic properties. + const eligibleDisbursement: Partial = { + coeStatus: COEStatus.completed, + coeUpdatedAt: new Date(), + }; + // Student with valid SIN. + const student = await saveFakeStudent(db.dataSource); + // Valid MSFAA Number. + const msfaaNumber = await db.msfaaNumber.save( + createFakeMSFAANumber( + { student }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, + ), + ); + const application = await saveFakeApplicationDisbursements( + db.dataSource, + { + student, + msfaaNumber, + firstDisbursementValues: [ + createFakeDisbursementValue( + DisbursementValueType.CanadaLoan, + "CSLP", + 100, + ), + createFakeDisbursementValue( + DisbursementValueType.BCLoan, + "CSPT", + 150, + ), + ], + }, + { + offeringIntensity: OfferingIntensity.partTime, + applicationStatus: ApplicationStatus.Completed, + currentAssessmentInitialValues: { + assessmentData: { weeks: 5 } as Assessment, + assessmentDate: new Date(), + }, + firstDisbursementInitialValues: { + ...eligibleDisbursement, + disbursementDate: getISODateOnlyString(addDays(1)), + }, + }, + ); + // Institution restriction that blocks disbursement. + const restriction = await db.restriction.findOne({ + select: { id: true }, + where: { + restrictionType: RestrictionType.Institution, + actionType: ArrayContains([ + RestrictionActionType.StopPartTimeDisbursement, + ]), + }, + }); + const location = + application.currentAssessment.offering.institutionLocation; + const institution = location.institution; + // Add institution restriction for the application location but different program. + await saveFakeInstitutionRestriction(db, { + restriction, + institution, + location, + }); + // Queued job. + const mockedJob = mockBullJob(); + + // Act + const result = await processor.processQueue(mockedJob.job); + + // Assert + // Assert uploaded file. + const uploadedFile = getUploadedFile(sftpClientMock); + const uploadedFileName = getUploadedFileName(); + expect(uploadedFile.remoteFilePath).toBe(uploadedFileName); + // No records should be sent. + expect(result).toStrictEqual([ + `Generated file: ${uploadedFileName}`, + "Uploaded records: 1", + ]); + // Assert log messages for the blocked disbursement. + expect( + mockedJob.containLogMessages([ + `All calculations were saved and disbursement was set to '${DisbursementScheduleStatus.ReadyToSend}'.`, + ]), + ).toBe(true); + const [disbursement] = + application.currentAssessment.disbursementSchedules; + // Assert that the disbursement is updated to status 'sent' with the dateSent defined. + const scheduleIsSent = await db.disbursementSchedule.exists({ + where: { + id: disbursement.id, + dateSent: Not(IsNull()), + disbursementScheduleStatus: DisbursementScheduleStatus.Sent, + }, + }); + expect(scheduleIsSent).toBe(true); + }, + ); + describe("Aviation credential part-time applications e-Cert generation", () => { for (const { aviationCredentialType, @@ -1534,7 +1762,12 @@ describe( const msfaaNumber = await db.msfaaNumber.save( createFakeMSFAANumber( { student }, - { msfaaState: MSFAAStates.Signed }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, ), ); const aviationCredentialApplication = @@ -1646,7 +1879,12 @@ describe( const msfaaNumber = await db.msfaaNumber.save( createFakeMSFAANumber( { student }, - { msfaaState: MSFAAStates.Signed }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, ), ); const aviationCredentialApplication = @@ -1775,7 +2013,12 @@ describe( const msfaaNumber = await db.msfaaNumber.save( createFakeMSFAANumber( { student }, - { msfaaState: MSFAAStates.Signed }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, ), ); const aviationCredentialApplication = @@ -1911,7 +2154,12 @@ describe( const msfaaNumber = await db.msfaaNumber.save( createFakeMSFAANumber( { student }, - { msfaaState: MSFAAStates.Signed }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, ), ); const aviationCredentialApplication = @@ -2029,7 +2277,7 @@ describe( * Helper function to get the uploaded file name. * @returns The uploaded file name */ - function getUploadedFileName() { + function getUploadedFileName(): string { const fileDate = dayjs().format("YYYYMMDD"); const uploadedFileName = `MSFT-Request\\DPBC.EDU.NEW.PTCERTS.D${fileDate}.001`; return uploadedFileName; diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts index 556fd47592..61b2d83fce 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts @@ -100,7 +100,7 @@ export class ValidateDisbursementFullTimeStep const program = eCertDisbursement.offering.educationProgram; const location = eCertDisbursement.offering.institutionLocation; log.info( - `Institution ${eCertDisbursement.institutionId} has an effective '${RestrictionActionType.StopFullTimeDisbursement}' restriction` + + `Institution has an effective '${RestrictionActionType.StopFullTimeDisbursement}' restriction` + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, ); validationResults.push({ diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts index 2f8641ae96..1f449022ff 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts @@ -106,7 +106,7 @@ export class ValidateDisbursementPartTimeStep const program = eCertDisbursement.offering.educationProgram; const location = eCertDisbursement.offering.institutionLocation; log.info( - `Institution ${eCertDisbursement.institutionId} has an effective '${RestrictionActionType.StopPartTimeDisbursement}' restriction` + + `Institution has an effective '${RestrictionActionType.StopPartTimeDisbursement}' restriction` + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, ); validationResults.push({ diff --git a/sources/packages/web/src/types/contracts/ECertFailedValidation.ts b/sources/packages/web/src/types/contracts/ECertFailedValidation.ts index 4bfed1a529..8fef2e1db6 100644 --- a/sources/packages/web/src/types/contracts/ECertFailedValidation.ts +++ b/sources/packages/web/src/types/contracts/ECertFailedValidation.ts @@ -34,6 +34,11 @@ export enum ECertFailedValidation { * restriction and the disbursement calculation will not proceed. */ HasStopDisbursementRestriction = "HasStopDisbursementRestriction", + /** + * Institution has an active 'StopFullTimeDisbursement' or 'StopPartTimeDisbursement' + * restriction and the disbursement calculation will not proceed. + */ + HasStopDisbursementInstitutionRestriction = "HasStopDisbursementInstitutionRestriction", /** * Lifetime maximum CSLP is reached. * Affects only part-time disbursements. From 040980daf2584b3d1f0d09a8668c0e3504f9c65e Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Sun, 4 Jan 2026 16:56:42 -0700 Subject: [PATCH 05/13] Added NOA and ECert blocking messages. --- ...ull-time-process-integration.scheduler.e2e-spec.ts | 7 ++++++- .../disbursement-schedule.models.ts | 4 ++-- sources/packages/web/src/constants/ecert-constants.ts | 5 +++++ .../web/src/types/contracts/ECertFailedValidation.ts | 4 ++-- .../web/src/views/student/NoticeOfAssessment.vue | 11 +++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts index 74353be786..5315e0e222 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts @@ -1934,7 +1934,12 @@ describe( const msfaaNumber = await db.msfaaNumber.save( createFakeMSFAANumber( { student }, - { msfaaState: MSFAAStates.Signed }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.fullTime, + }, + }, ), ); const application = await saveFakeApplicationDisbursements( diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index b2abb02ce1..d89a9de897 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -474,8 +474,8 @@ export enum ECertFailedValidation { */ HasStopDisbursementRestriction = "HasStopDisbursementRestriction", /** - * Institution has an active 'StopFullTimeDisbursement' or 'StopPartTimeDisbursement' - * restriction and the disbursement calculation will not proceed. + * Institution has an effective 'StopFullTimeDisbursement' or 'StopPartTimeDisbursement' + * restriction for the application location and program and the disbursement calculation will not proceed. */ HasStopDisbursementInstitutionRestriction = "HasStopDisbursementInstitutionRestriction", /** diff --git a/sources/packages/web/src/constants/ecert-constants.ts b/sources/packages/web/src/constants/ecert-constants.ts index 9f80ca8e65..c745e1582a 100644 --- a/sources/packages/web/src/constants/ecert-constants.ts +++ b/sources/packages/web/src/constants/ecert-constants.ts @@ -41,6 +41,11 @@ export const ECERT_FAILED_MESSAGES: EcertFailedValidationDetail[] = [ failedMessage: "You have a restriction on your account making you ineligible to receive funding. Please contact StudentAid BC if you still require assistance in identifying the cause of this issue and help resolving the issue.", }, + { + failedType: ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + failedMessage: + "A restriction at your institution makes your application ineligible for funding at this time.", + }, { failedType: ECertFailedValidation.LifetimeMaximumCSLP, failedMessage: diff --git a/sources/packages/web/src/types/contracts/ECertFailedValidation.ts b/sources/packages/web/src/types/contracts/ECertFailedValidation.ts index 8fef2e1db6..c00aa4aef3 100644 --- a/sources/packages/web/src/types/contracts/ECertFailedValidation.ts +++ b/sources/packages/web/src/types/contracts/ECertFailedValidation.ts @@ -35,8 +35,8 @@ export enum ECertFailedValidation { */ HasStopDisbursementRestriction = "HasStopDisbursementRestriction", /** - * Institution has an active 'StopFullTimeDisbursement' or 'StopPartTimeDisbursement' - * restriction and the disbursement calculation will not proceed. + * Institution has an effective 'StopFullTimeDisbursement' or 'StopPartTimeDisbursement' + * restriction for the application location and program and the disbursement calculation will not proceed. */ HasStopDisbursementInstitutionRestriction = "HasStopDisbursementInstitutionRestriction", /** diff --git a/sources/packages/web/src/views/student/NoticeOfAssessment.vue b/sources/packages/web/src/views/student/NoticeOfAssessment.vue index 8b29dcb607..8dbd9d03f7 100644 --- a/sources/packages/web/src/views/student/NoticeOfAssessment.vue +++ b/sources/packages/web/src/views/student/NoticeOfAssessment.vue @@ -72,6 +72,12 @@ You have restrictions that block funding on your account. Please resolve them in order to move forward with your application. +
  • + A restriction at your institution makes your application + ineligible for funding at this time. +
  • Your application has been assessed and no funding has been awarded. If you believe this is an error, please review your @@ -154,6 +160,7 @@ export default defineComponent({ modifiedIndependentStatusNotApproved: false, msfaaInvalid: false, hasStopDisbursementRestriction: false, + hasStopDisbursementInstitutionRestriction: false, noEstimatedAwardAmounts: false, hasEffectiveAviationRestriction: false, }); @@ -232,6 +239,10 @@ export default defineComponent({ warnings.eCertFailedValidations.includes( ECertFailedValidation.HasStopDisbursementRestriction, ), + hasStopDisbursementInstitutionRestriction: + warnings.eCertFailedValidations.includes( + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + ), noEstimatedAwardAmounts: warnings.eCertFailedValidations.includes( ECertFailedValidation.NoEstimatedAwardAmounts, ), From 29013e72c97d16ac36501f9894b8e29ff93fdef7 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Sun, 4 Jan 2026 21:15:53 -0700 Subject: [PATCH 06/13] merging changes into new PY file --- .../src/form-definitions/sfaa2026-27-ft.json | 38 +++++++++++++++++++ .../src/form-definitions/sfaa2026-27-pt.json | 38 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/sources/packages/forms/src/form-definitions/sfaa2026-27-ft.json b/sources/packages/forms/src/form-definitions/sfaa2026-27-ft.json index 356468ec20..fc583c15cc 100644 --- a/sources/packages/forms/src/form-definitions/sfaa2026-27-ft.json +++ b/sources/packages/forms/src/form-definitions/sfaa2026-27-ft.json @@ -631,6 +631,28 @@ "input": true, "lockKey": true }, + { + "label": "HTML", + "className": "alert alert-warning fa fa-exclamation-triangle w-100", + "attrs": [ + { + "attr": "", + "value": "" + } + ], + "content": "Your program was restricted by StudentAid BC and is no longer eligible for funding.", + "refreshOnChange": false, + "customClass": "banner-warning", + "key": "html", + "conditional": { + "show": true, + "when": "isSelectedLocationProgramRestricted", + "eq": "true" + }, + "type": "htmlelement", + "input": false, + "tableView": false + }, { "label": "Your program is not listed.", "optionsLabelPosition": "right", @@ -1332,6 +1354,22 @@ "type": "hidden", "input": true, "tableView": false + }, + { + "label": "Selected location program institution restrictions", + "key": "selectedLocationProgramRestrictions", + "type": "hidden", + "input": true, + "tableView": false + }, + { + "label": "Is selected location program restricted (Selected location and program is not eligible for funding)", + "persistent": false, + "calculateValue": "const selectedLocationProgramRestrictions = data.selectedLocationProgramRestrictions || [];\n\nvalue = selectedLocationProgramRestrictions.some((restriction) =>\n restriction.restrictionActions.includes(\"Stop full time disbursement\")\n);", + "key": "isSelectedLocationProgramRestricted", + "type": "hidden", + "input": true, + "tableView": false } ], "input": false, diff --git a/sources/packages/forms/src/form-definitions/sfaa2026-27-pt.json b/sources/packages/forms/src/form-definitions/sfaa2026-27-pt.json index ff4e92d95b..b435ba091a 100644 --- a/sources/packages/forms/src/form-definitions/sfaa2026-27-pt.json +++ b/sources/packages/forms/src/form-definitions/sfaa2026-27-pt.json @@ -610,6 +610,28 @@ "input": true, "lockKey": true }, + { + "label": "HTML", + "className": "alert alert-warning fa fa-exclamation-triangle w-100", + "attrs": [ + { + "attr": "", + "value": "" + } + ], + "content": "Your program was restricted by StudentAid BC and is no longer eligible for funding.", + "refreshOnChange": false, + "customClass": "banner-warning", + "key": "html1", + "conditional": { + "show": true, + "when": "isSelectedLocationProgramRestricted", + "eq": "true" + }, + "type": "htmlelement", + "input": false, + "tableView": false + }, { "label": "Your program is not listed.", "optionsLabelPosition": "right", @@ -1511,6 +1533,22 @@ "type": "hidden", "input": true, "tableView": false + }, + { + "label": "Selected location program institution restrictions", + "key": "selectedLocationProgramRestrictions", + "type": "hidden", + "input": true, + "tableView": false + }, + { + "label": "Is selected location program restricted (Selected location and program is not eligible for funding)", + "persistent": false, + "calculateValue": "const selectedLocationProgramRestrictions = data.selectedLocationProgramRestrictions || [];\n\nvalue = selectedLocationProgramRestrictions.some((restriction) =>\n restriction.restrictionActions.includes(\"Stop part time disbursement\")\n);", + "key": "isSelectedLocationProgramRestricted", + "type": "hidden", + "input": true, + "tableView": false } ], "input": false, From 9017b37b883c055e7f919b46080692864a01c3f0 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Mon, 5 Jan 2026 13:06:57 -0700 Subject: [PATCH 07/13] API E2E tests. --- ...troller.getApplicationWarnings.e2e-spec.ts | 85 +++++++++++++++++- ...getCompletedApplicationDetails.e2e-spec.ts | 89 +++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts index 14f270c702..97873e6d9f 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts @@ -20,6 +20,7 @@ import { createFakeDisbursementValue, saveFakeApplication, RestrictionCode, + saveFakeInstitutionRestriction, } from "@sims/test-utils"; import { ApplicationStatus, @@ -29,6 +30,7 @@ import { DisbursementValueType, OfferingIntensity, RestrictionActionType, + RestrictionType, } from "@sims/sims-db"; import { getISODateOnlyString } from "@sims/utilities"; import { ECertFailedValidation } from "@sims/integrations/services/disbursement-schedule/disbursement-schedule.models"; @@ -111,8 +113,7 @@ describe("ApplicationStudentsController(e2e)-getApplicationWarnings", () => { }, ); - application.currentAssessment.workflowData.calculatedData.pdppdStatus = - true; + application.currentAssessment.workflowData.calculatedData.pdppdStatus = true; await db.studentAssessment.save(application.currentAssessment); const endpoint = `/students/application/${application.id}/warnings`; @@ -264,6 +265,86 @@ describe("ApplicationStudentsController(e2e)-getApplicationWarnings", () => { }, ); + it( + "Should return a failed ecert validations array with stop disbursement institution restriction when" + + " there are is an effective restriction on institution account for the application location and program" + + " and the offering intensity is part-time.", + async () => { + // Arrange + const student = await saveFakeStudent(db.dataSource); + const msfaaNumber = createFakeMSFAANumber( + { + student, + }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { + offeringIntensity: OfferingIntensity.partTime, + }, + }, + ); + await db.msfaaNumber.save(msfaaNumber); + + // Mock user services to return the saved student. + await mockUserLoginInfo(appModule, student); + + const application = await saveFakeApplicationDisbursements( + appDataSource, + { student, msfaaNumber }, + { + applicationStatus: ApplicationStatus.Completed, + offeringIntensity: OfferingIntensity.partTime, + firstDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Pending, + }, + }, + ); + + // Institution restriction. + const restriction = await db.restriction.findOne({ + select: { id: true }, + where: { + restrictionType: RestrictionType.Institution, + actionType: ArrayContains([ + RestrictionActionType.StopPartTimeDisbursement, + ]), + }, + }); + const location = + application.currentAssessment.offering.institutionLocation; + const program = application.currentAssessment.offering.educationProgram; + const institution = + application.currentAssessment.offering.institutionLocation.institution; + await saveFakeInstitutionRestriction(db, { + restriction, + institution, + location, + program, + }); + + const endpoint = `/students/application/${application.id}/warnings`; + const token = await getStudentToken( + FakeStudentUsersTypes.FakeStudentUserType1, + ); + + // Act/Assert + await request(app.getHttpServer()) + .get(endpoint) + .auth(token, BEARER_AUTH_TYPE) + .expect(HttpStatus.OK) + .expect({ + eCertFailedValidations: [ + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + ], + canAcceptAssessment: false, + eCertFailedValidationsInfo: { + hasEffectiveAviationRestriction: false, + }, + }); + }, + ); + it("Should return a failed ecert validations array with no estimated award amounts when no disbursements values are present.", async () => { // Arrange const student = await saveFakeStudent(db.dataSource); diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts index a790094698..d0da298a6c 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts @@ -28,6 +28,7 @@ import { createFakeCRAIncomeVerification, createFakeSupportingUser, RestrictionCode, + saveFakeInstitutionRestriction, } from "@sims/test-utils"; import { Application, @@ -43,6 +44,7 @@ import { MSFAANumber, OfferingIntensity, RestrictionActionType, + RestrictionType, Student, StudentAppeal, StudentAppealStatus, @@ -806,6 +808,93 @@ describe("ApplicationStudentsController(e2e)-getCompletedApplicationDetails", () }, ); + it( + "Should get application details with ecert failed validations array having stop disbursement institution restriction when" + + " there are is an effective restriction on institution account for the application location and program" + + " and the offering intensity is part-time.", + async () => { + // Arrange + const student = await saveFakeStudent(db.dataSource); + const msfaaNumber = createFakeMSFAANumber( + { + student, + }, + { + msfaaState: MSFAAStates.Signed, + msfaaInitialValues: { offeringIntensity: OfferingIntensity.partTime }, + }, + ); + await db.msfaaNumber.save(msfaaNumber); + + // Mock user services to return the saved student. + await mockUserLoginInfo(appModule, student); + + const application = await saveFakeApplicationDisbursements( + appDataSource, + { student, msfaaNumber }, + { + applicationStatus: ApplicationStatus.Completed, + offeringIntensity: OfferingIntensity.partTime, + firstDisbursementInitialValues: { + coeStatus: COEStatus.completed, + disbursementScheduleStatus: DisbursementScheduleStatus.Pending, + }, + }, + ); + const [firstDisbursement] = + application.currentAssessment.disbursementSchedules; + + // Institution restriction. + const restriction = await db.restriction.findOne({ + select: { id: true }, + where: { + restrictionType: RestrictionType.Institution, + actionType: ArrayContains([ + RestrictionActionType.StopPartTimeDisbursement, + ]), + }, + }); + const location = + application.currentAssessment.offering.institutionLocation; + const program = application.currentAssessment.offering.educationProgram; + const institution = + application.currentAssessment.offering.institutionLocation.institution; + await saveFakeInstitutionRestriction(db, { + restriction, + institution, + location, + program, + }); + + const endpoint = `/students/application/${application.id}/completed`; + const token = await getStudentToken( + FakeStudentUsersTypes.FakeStudentUserType1, + ); + + // Act/Assert + await request(app.getHttpServer()) + .get(endpoint) + .auth(token, BEARER_AUTH_TYPE) + .expect(HttpStatus.OK) + .expect({ + firstDisbursement: { + coeStatus: firstDisbursement.coeStatus, + disbursementScheduleStatus: + firstDisbursement.disbursementScheduleStatus, + }, + assessmentTriggerType: application.currentAssessment.triggerType, + hasActiveUnsuccessfulCompletionWeeks: false, + hasBlockFundingFeedbackError: false, + eCertFailedValidations: [ + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + ], + eCertFailedValidationsInfo: { + hasEffectiveAviationRestriction: false, + }, + }); + }, + ); + it( "Should get application details with e-cert failed validation results having stop disbursement restriction and indicating effective aviation restriction" + " in the validations info when the application is for aviation credential type 'commercialPilotTraining'" + From fdc6d9a9dd801c3e327ee6fa71efb7c91c0d42b6 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Mon, 5 Jan 2026 14:06:00 -0700 Subject: [PATCH 08/13] Updated E2E test util. --- .../ecert-full-time-process-integration.scheduler.e2e-spec.ts | 4 +++- .../ecert-part-time-process-integration.scheduler.e2e-spec.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts index 5315e0e222..977aae71a2 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts @@ -65,6 +65,7 @@ import { } from "./e-cert-utils"; import { RestrictionCode, SystemUsersService } from "@sims/services"; import { createFakeSFASApplication } from "@sims/test-utils/factories/sfas-application"; +import { ConfigService } from "@sims/utilities/config"; describe( describeQueueProcessorRootTest(QueueNames.FullTimeECertIntegration), @@ -2579,7 +2580,8 @@ describe( * @returns The uploaded file name */ function getUploadedFileName(): string { + const esdcConfig = new ConfigService().esdcIntegration; const fileDate = dayjs().format("YYYYMMDD"); - const uploadedFileName = `MSFT-Request\\DPBC.EDU.FTECERTS.${fileDate}.001`; + const uploadedFileName = `${esdcConfig.ftpRequestFolder}\\DPBC.EDU.FTECERTS.${fileDate}.001`; return uploadedFileName; } diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts index edd845e069..ec0807bbed 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts @@ -59,6 +59,7 @@ import { import { SystemUsersService } from "@sims/services"; import { faker } from "@faker-js/faker"; import { ECERT_PART_TIME_SENT_FILE_SEQUENCE_GROUP } from "@sims/integrations/esdc-integration"; +import { ConfigService } from "@sims/utilities/config"; describe( describeQueueProcessorRootTest(QueueNames.PartTimeECertIntegration), @@ -2278,8 +2279,9 @@ describe( * @returns The uploaded file name */ function getUploadedFileName(): string { + const esdcConfig = new ConfigService().esdcIntegration; const fileDate = dayjs().format("YYYYMMDD"); - const uploadedFileName = `MSFT-Request\\DPBC.EDU.NEW.PTCERTS.D${fileDate}.001`; + const uploadedFileName = `${esdcConfig.ftpRequestFolder}\\DPBC.EDU.NEW.PTCERTS.D${fileDate}.001`; return uploadedFileName; } }, From 3286bcd9e43449e8180d0db1f7c2aed2595d8dbe Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Mon, 5 Jan 2026 15:16:36 -0700 Subject: [PATCH 09/13] review comments fix --- ...troller.getApplicationWarnings.e2e-spec.ts | 2 +- ...getCompletedApplicationDetails.e2e-spec.ts | 2 +- ...-process-integration.scheduler.e2e-spec.ts | 22 +++--------- ...-process-integration.scheduler.e2e-spec.ts | 35 ++++--------------- .../disbursement-schedule.models.ts | 1 + .../e-cert-steps-utils.ts | 2 +- 6 files changed, 15 insertions(+), 49 deletions(-) diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts index 97873e6d9f..c483acf79d 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getApplicationWarnings.e2e-spec.ts @@ -267,7 +267,7 @@ describe("ApplicationStudentsController(e2e)-getApplicationWarnings", () => { it( "Should return a failed ecert validations array with stop disbursement institution restriction when" + - " there are is an effective restriction on institution account for the application location and program" + + " there is an effective restriction on institution account for the application location and program" + " and the offering intensity is part-time.", async () => { // Arrange diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts index d0da298a6c..f4f15e9fe1 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.students.controller.getCompletedApplicationDetails.e2e-spec.ts @@ -810,7 +810,7 @@ describe("ApplicationStudentsController(e2e)-getCompletedApplicationDetails", () it( "Should get application details with ecert failed validations array having stop disbursement institution restriction when" + - " there are is an effective restriction on institution account for the application location and program" + + " there is an effective restriction on institution account for the application location and program" + " and the offering intensity is part-time.", async () => { // Arrange diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts index 977aae71a2..750e581263 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-full-time-process-integration.scheduler.e2e-spec.ts @@ -1948,18 +1948,6 @@ describe( { student, msfaaNumber, - firstDisbursementValues: [ - createFakeDisbursementValue( - DisbursementValueType.CanadaLoan, - "CSLP", - 100, - ), - createFakeDisbursementValue( - DisbursementValueType.BCLoan, - "CSPT", - 150, - ), - ], }, { offeringIntensity: OfferingIntensity.fullTime, @@ -1984,9 +1972,9 @@ describe( ]), }, }); - const location = - application.currentAssessment.offering.institutionLocation; - const program = application.currentAssessment.offering.educationProgram; + const offering = application.currentAssessment.offering; + const location = offering.institutionLocation; + const program = offering.educationProgram; const institution = location.institution; // Add institution restriction for the application location and program. await saveFakeInstitutionRestriction(db, { @@ -2580,8 +2568,8 @@ describe( * @returns The uploaded file name */ function getUploadedFileName(): string { - const esdcConfig = new ConfigService().esdcIntegration; + const ftpRequestFolder = new ConfigService().esdcIntegration.ftpRequestFolder; const fileDate = dayjs().format("YYYYMMDD"); - const uploadedFileName = `${esdcConfig.ftpRequestFolder}\\DPBC.EDU.FTECERTS.${fileDate}.001`; + const uploadedFileName = `${ftpRequestFolder}\\DPBC.EDU.FTECERTS.${fileDate}.001`; return uploadedFileName; } diff --git a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts index ec0807bbed..bd57d12e35 100644 --- a/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts +++ b/sources/packages/backend/apps/queue-consumers/src/processors/schedulers/esdc-integration/ecert-integration/_tests_/ecert-part-time-process-integration.scheduler.e2e-spec.ts @@ -1546,18 +1546,6 @@ describe( { student, msfaaNumber, - firstDisbursementValues: [ - createFakeDisbursementValue( - DisbursementValueType.CanadaLoan, - "CSLP", - 100, - ), - createFakeDisbursementValue( - DisbursementValueType.BCLoan, - "CSPT", - 150, - ), - ], }, { offeringIntensity: OfferingIntensity.partTime, @@ -1582,9 +1570,9 @@ describe( ]), }, }); - const location = - application.currentAssessment.offering.institutionLocation; - const program = application.currentAssessment.offering.educationProgram; + const offering = application.currentAssessment.offering; + const location = offering.institutionLocation; + const program = offering.educationProgram; const institution = location.institution; // Add institution restriction for the application location and program. await saveFakeInstitutionRestriction(db, { @@ -1661,18 +1649,6 @@ describe( { student, msfaaNumber, - firstDisbursementValues: [ - createFakeDisbursementValue( - DisbursementValueType.CanadaLoan, - "CSLP", - 100, - ), - createFakeDisbursementValue( - DisbursementValueType.BCLoan, - "CSPT", - 150, - ), - ], }, { offeringIntensity: OfferingIntensity.partTime, @@ -2279,9 +2255,10 @@ describe( * @returns The uploaded file name */ function getUploadedFileName(): string { - const esdcConfig = new ConfigService().esdcIntegration; + const ftpRequestFolder = new ConfigService().esdcIntegration + .ftpRequestFolder; const fileDate = dayjs().format("YYYYMMDD"); - const uploadedFileName = `${esdcConfig.ftpRequestFolder}\\DPBC.EDU.NEW.PTCERTS.D${fileDate}.001`; + const uploadedFileName = `${ftpRequestFolder}\\DPBC.EDU.NEW.PTCERTS.D${fileDate}.001`; return uploadedFileName; } }, diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index d89a9de897..bff4bdc267 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -223,6 +223,7 @@ export interface InstitutionActiveRestriction { */ actionEffectiveConditions?: ActionEffectiveCondition[]; } + /** * Restriction bypass active for the application. */ diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts index c1083347c4..c473a72afb 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts @@ -38,7 +38,7 @@ export function getRestrictionsByActionType( * Check active institution restrictions by its action type in an eligible disbursement. * @param eCertDisbursement disbursement to check institution restrictions. * @param actionType action type. - * @returns the all the effective restrictions of the requested action type. + * @returns all the effective restrictions of the requested action type. */ export function getInstitutionRestrictionsByActionType( eCertDisbursement: EligibleECertDisbursement, From 16d7019679a9d007face539477161a84ed74acb8 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Mon, 5 Jan 2026 17:55:42 -0700 Subject: [PATCH 10/13] Updated the restriction validation as per review discussion --- .../disbursement-schedule.models.ts | 66 ++++++++++------ .../e-cert-steps-utils.ts | 55 +++---------- .../validate-disbursement-base.ts | 77 ++++++++++++++++++- .../validate-disbursement-full-time-step.ts | 44 +---------- .../validate-disbursement-part-time-step.ts | 45 +---------- .../restriction/model/restriction.model.ts | 14 ++++ 6 files changed, 147 insertions(+), 154 deletions(-) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index bff4bdc267..a149a63bc6 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -1,4 +1,4 @@ -import { RestrictionCode } from "@sims/services"; +import { RestrictedParty, RestrictionCode } from "@sims/services"; import { ActionEffectiveCondition, DisabilityStatus, @@ -163,18 +163,13 @@ export interface DisabilityDetails { } /** - * Represents an active student restriction. + * Represents an active restriction which can be student or institution restriction. */ -export interface StudentActiveRestriction { +export interface ActiveRestriction { /** * Restriction id. */ id: number; - /** - * Association between the student and - * the active restriction on his account. - */ - studentRestrictionId: number; /** * Restriction code. */ @@ -187,16 +182,31 @@ export interface StudentActiveRestriction { * Action effective conditions associated with the restriction. */ actionEffectiveConditions?: ActionEffectiveCondition[]; + /** + * The party (student or institution) who is restricted. + */ + restrictedParty?: RestrictedParty; } /** - * Represents an active institution restriction. + * Represents an active student restriction. */ -export interface InstitutionActiveRestriction { +export interface StudentActiveRestriction extends ActiveRestriction { /** - * Restriction id. + * Association between the student and + * the active restriction on his account. */ - id: number; + studentRestrictionId: number; + /** + * Restriction is applied to a student. + */ + restrictedParty: RestrictedParty.Student; +} + +/** + * Represents an active institution restriction. + */ +export interface InstitutionActiveRestriction extends ActiveRestriction { /** * Association between the institution and * the active restriction on institution account. @@ -211,17 +221,9 @@ export interface InstitutionActiveRestriction { */ location: InstitutionLocation; /** - * Restriction code. - */ - code: RestrictionCode; - /** - * Actions associated with the restriction. - */ - actions: RestrictionActionType[]; - /** - * Action effective conditions associated with the restriction. + * Restriction is applied to a student. */ - actionEffectiveConditions?: ActionEffectiveCondition[]; + restrictedParty: RestrictedParty.Institution; } /** @@ -364,9 +366,9 @@ export class EligibleECertDisbursement { } /** - * List of restrictions not bypassed that will be applied to the application. + * List of student restrictions not bypassed that will be applied to the application. */ - getEffectiveRestrictions(): ReadonlyArray { + private getEffectiveStudentRestrictions(): ReadonlyArray { // The restrictions list can be updated as the e-Cert is calculated. // That is why the effective list should be calculated using the most // recent values. @@ -382,7 +384,7 @@ export class EligibleECertDisbursement { * List the effective institution restrictions for the given disbursement. * @returns Effective institution restrictions. */ - getEffectiveInstitutionRestrictions(): ReadonlyArray { + private getEffectiveInstitutionRestrictions(): ReadonlyArray { const programId = this.offering.educationProgram.id; const locationId = this.offering.institutionLocation.id; return this.institutionRestrictions.filter( @@ -391,6 +393,18 @@ export class EligibleECertDisbursement { restriction.location.id === locationId, ); } + + /** + * Get all effective restrictions from student and application institution. + * @returns effective restrictions. + */ + getEffectiveRestrictions(): ReadonlyArray { + const allEffectiveRestrictions = [ + ...this.getEffectiveStudentRestrictions(), + ...this.getEffectiveInstitutionRestrictions(), + ]; + return allEffectiveRestrictions; + } } /** @@ -410,6 +424,7 @@ export function mapStudentActiveRestrictions( actions: studentRestriction.restriction.actionType, actionEffectiveConditions: studentRestriction.restriction.actionEffectiveConditions, + restrictedParty: RestrictedParty.Student, }), ); } @@ -434,6 +449,7 @@ export function mapInstitutionActiveRestrictions( location: institutionRestriction.location, actionEffectiveConditions: institutionRestriction.restriction.actionEffectiveConditions, + restrictedParty: RestrictedParty.Institution, }), ); } diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts index c473a72afb..9ecf4706b9 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils.ts @@ -1,14 +1,13 @@ import { BC_FUNDING_TYPES } from "@sims/services/constants"; import { - ActionEffectiveCondition, DisbursementValue, OfferingIntensity, RestrictionActionType, } from "@sims/sims-db"; import { + ActiveRestriction, ApplicationActiveRestrictionBypass, EligibleECertDisbursement, - InstitutionActiveRestriction, StudentActiveRestriction, } from "../disbursement-schedule.models"; import { RestrictionCode } from "@sims/services"; @@ -26,55 +25,19 @@ import { isRestrictionActionEffective } from "./e-cert-steps-restriction-utils"; export function getRestrictionsByActionType( eCertDisbursement: EligibleECertDisbursement, actionType: RestrictionActionType, -): StudentActiveRestriction[] { +): ActiveRestriction[] { return eCertDisbursement .getEffectiveRestrictions() - .filter((restriction) => - hasEffectiveRestrictionAction(eCertDisbursement, actionType, restriction), + .filter( + (restriction) => + restriction.actions.includes(actionType) && + isRestrictionActionEffective( + restriction.actionEffectiveConditions, + eCertDisbursement, + ), ); } -/** - * Check active institution restrictions by its action type in an eligible disbursement. - * @param eCertDisbursement disbursement to check institution restrictions. - * @param actionType action type. - * @returns all the effective restrictions of the requested action type. - */ -export function getInstitutionRestrictionsByActionType( - eCertDisbursement: EligibleECertDisbursement, - actionType: RestrictionActionType, -): InstitutionActiveRestriction[] { - return eCertDisbursement - .getEffectiveInstitutionRestrictions() - .filter((restriction) => - hasEffectiveRestrictionAction(eCertDisbursement, actionType, restriction), - ); -} - -/** - * Check if a restriction has an effective action of the given action type. - * @param eCertDisbursement disbursement to check restriction conditions. - * @param actionType action type. - * @param restriction restriction to be checked. - * @returns true if the restriction has the effective action, otherwise, false. - */ -function hasEffectiveRestrictionAction( - eCertDisbursement: EligibleECertDisbursement, - actionType: RestrictionActionType, - restriction: { - actions: RestrictionActionType[]; - actionEffectiveConditions?: ActionEffectiveCondition[]; - }, -): boolean { - return ( - restriction.actions.includes(actionType) && - isRestrictionActionEffective( - restriction.actionEffectiveConditions, - eCertDisbursement, - ) - ); -} - /** * Adds to the log the lists of all restriction bypasses active. * @param bypasses active bypasses. diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts index d255815e91..3bd381e7d1 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts @@ -1,10 +1,19 @@ -import { COEStatus, DisabilityStatus } from "@sims/sims-db"; +import { + COEStatus, + DisabilityStatus, + RestrictionActionType, +} from "@sims/sims-db"; import { ProcessSummary } from "@sims/utilities/logger"; import { ECertFailedValidation, ECertFailedValidationResult, EligibleECertDisbursement, } from "../disbursement-schedule.models"; +import { + getRestrictionsByActionType, + logActiveRestrictionsBypasses, +} from "@sims/integrations/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils"; +import { RestrictedParty } from "@sims/services"; /** * Common e-Cert validations for full-time and part-time. @@ -74,4 +83,70 @@ export abstract class ValidateDisbursementBase { } return validationResults; } + + /** + * Validate stop disbursement restrictions on student and institution. + * @param eCertDisbursement eligible disbursement. + * @param restrictionActionType restriction action type to be validated. + * @param validationResults list of failed validations to be updated. + * @param log cumulative log summary. + */ + protected validateStopDisbursementRestriction( + eCertDisbursement: EligibleECertDisbursement, + restrictionActionType: + | RestrictionActionType.StopFullTimeDisbursement + | RestrictionActionType.StopPartTimeDisbursement, + validationResults: ECertFailedValidationResult[], + log: ProcessSummary, + ): void { + // Validate stop part-time disbursement restrictions. + const stopDisbursementRestrictions = getRestrictionsByActionType( + eCertDisbursement, + restrictionActionType, + ); + if (stopDisbursementRestrictions.length) { + const isStudentRestricted = stopDisbursementRestrictions.some( + (restriction) => + restriction.restrictedParty === RestrictedParty.Student, + ); + const isInstitutionRestricted = stopDisbursementRestrictions.some( + (restriction) => + restriction.restrictedParty === RestrictedParty.Institution, + ); + if (!isStudentRestricted && !isInstitutionRestricted) { + throw new Error( + "The stop disbursement restricted party is neither student nor institution.", + ); + } + if (isStudentRestricted) { + log.info( + `Student has an active '${restrictionActionType}' restriction and the disbursement calculation will not proceed.`, + ); + validationResults.push({ + resultType: ECertFailedValidation.HasStopDisbursementRestriction, + additionalInfo: { + restrictionCodes: stopDisbursementRestrictions.map( + (restriction) => restriction.code, + ), + }, + }); + } + if (isInstitutionRestricted) { + const program = eCertDisbursement.offering.educationProgram; + const location = eCertDisbursement.offering.institutionLocation; + log.info( + `Institution has an effective '${restrictionActionType}' restriction` + + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, + ); + validationResults.push({ + resultType: + ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + }); + } + } + logActiveRestrictionsBypasses( + eCertDisbursement.activeRestrictionBypasses, + log, + ); + } } diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts index 61b2d83fce..b9b759943a 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts @@ -11,11 +11,6 @@ import { ECertFailedValidation, EligibleECertDisbursement, } from "../disbursement-schedule.models"; -import { - getInstitutionRestrictionsByActionType, - getRestrictionsByActionType, - logActiveRestrictionsBypasses, -} from "./e-cert-steps-utils"; import { ECertPreValidator, ECertPreValidatorResult, @@ -68,46 +63,13 @@ export class ValidateDisbursementFullTimeStep ): Promise { log.info("Executing full-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); - // Validate stop full-time disbursement restrictions on the student. - const stopDisbursementStudentRestrictions = getRestrictionsByActionType( + // Validate stop full-time disbursement restrictions. + this.validateStopDisbursementRestriction( eCertDisbursement, RestrictionActionType.StopFullTimeDisbursement, - ); - if (stopDisbursementStudentRestrictions.length) { - log.info( - `Student has an active '${RestrictionActionType.StopFullTimeDisbursement}' restriction and the disbursement calculation will not proceed.`, - ); - validationResults.push({ - resultType: ECertFailedValidation.HasStopDisbursementRestriction, - additionalInfo: { - restrictionCodes: stopDisbursementStudentRestrictions.map( - (restriction) => restriction.code, - ), - }, - }); - } - logActiveRestrictionsBypasses( - eCertDisbursement.activeRestrictionBypasses, + validationResults, log, ); - // Validate stop full-time disbursement restrictions on the institution. - const stopDisbursementInstitutionRestrictions = - getInstitutionRestrictionsByActionType( - eCertDisbursement, - RestrictionActionType.StopFullTimeDisbursement, - ); - if (stopDisbursementInstitutionRestrictions.length) { - const program = eCertDisbursement.offering.educationProgram; - const location = eCertDisbursement.offering.institutionLocation; - log.info( - `Institution has an effective '${RestrictionActionType.StopFullTimeDisbursement}' restriction` + - ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, - ); - validationResults.push({ - resultType: - ECertFailedValidation.HasStopDisbursementInstitutionRestriction, - }); - } // Validate modified independent status when estranged from parents. if ( eCertDisbursement.modifiedIndependentDetails.estrangedFromParents === diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts index 1f449022ff..cf40521e5e 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts @@ -7,11 +7,6 @@ import { ECertFailedValidation, EligibleECertDisbursement, } from "../disbursement-schedule.models"; -import { - getInstitutionRestrictionsByActionType, - getRestrictionsByActionType, - logActiveRestrictionsBypasses, -} from "./e-cert-steps-utils"; import { CANADA_STUDENT_LOAN_PART_TIME_AWARD_CODE } from "@sims/services/constants"; import { ECertGenerationService } from "../e-cert-generation.service"; import { StudentLoanBalanceSharedService } from "@sims/services"; @@ -74,46 +69,14 @@ export class ValidateDisbursementPartTimeStep ): Promise { log.info("Executing part-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); - // Validate stop part-time disbursement restrictions on the student. - const stopDisbursementStudentRestrictions = getRestrictionsByActionType( + // Validate stop part-time disbursement restrictions. + this.validateStopDisbursementRestriction( eCertDisbursement, RestrictionActionType.StopPartTimeDisbursement, - ); - if (stopDisbursementStudentRestrictions.length) { - log.info( - `Student has an active '${RestrictionActionType.StopPartTimeDisbursement}' restriction and the disbursement calculation will not proceed.`, - ); - validationResults.push({ - resultType: ECertFailedValidation.HasStopDisbursementRestriction, - additionalInfo: { - restrictionCodes: stopDisbursementStudentRestrictions.map( - (restriction) => restriction.code, - ), - }, - }); - } - logActiveRestrictionsBypasses( - eCertDisbursement.activeRestrictionBypasses, + validationResults, log, ); - // Validate stop part-time disbursement restrictions on the institution. - const stopDisbursementInstitutionRestrictions = - getInstitutionRestrictionsByActionType( - eCertDisbursement, - RestrictionActionType.StopPartTimeDisbursement, - ); - if (stopDisbursementInstitutionRestrictions.length) { - const program = eCertDisbursement.offering.educationProgram; - const location = eCertDisbursement.offering.institutionLocation; - log.info( - `Institution has an effective '${RestrictionActionType.StopPartTimeDisbursement}' restriction` + - ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, - ); - validationResults.push({ - resultType: - ECertFailedValidation.HasStopDisbursementInstitutionRestriction, - }); - } + // Validate CSLP. const validateLifetimeMaximumCSLP = await this.validateCSLPLifetimeMaximum( eCertDisbursement, diff --git a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts index ad29d73fec..2fde90516b 100644 --- a/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts +++ b/sources/packages/backend/libs/services/src/restriction/model/restriction.model.ts @@ -74,3 +74,17 @@ export enum RestrictionCode { */ SFAS_AV = "SFAS_AV", } + +/** + * The party (student or institution) who is restricted. + */ +export enum RestrictedParty { + /** + * Restriction is applied to a student. + */ + Student = "Student", + /** + * Restriction is applied to an institution. + */ + Institution = "Institution", +} From 3bbed1feb26c50e426915c49b8f787b26d628ff0 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Tue, 6 Jan 2026 09:46:03 -0700 Subject: [PATCH 11/13] Some improvements --- .../e-cert-processing-steps/validate-disbursement-base.ts | 6 +++--- .../validate-disbursement-full-time-step.ts | 2 +- .../validate-disbursement-part-time-step.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts index 3bd381e7d1..2f03fad519 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts @@ -12,7 +12,7 @@ import { import { getRestrictionsByActionType, logActiveRestrictionsBypasses, -} from "@sims/integrations/services/disbursement-schedule/e-cert-processing-steps/e-cert-steps-utils"; +} from "./e-cert-steps-utils"; import { RestrictedParty } from "@sims/services"; /** @@ -85,7 +85,7 @@ export abstract class ValidateDisbursementBase { } /** - * Validate stop disbursement restrictions on student and institution. + * Validate effective stop disbursement restrictions on student and institution. * @param eCertDisbursement eligible disbursement. * @param restrictionActionType restriction action type to be validated. * @param validationResults list of failed validations to be updated. @@ -99,7 +99,7 @@ export abstract class ValidateDisbursementBase { validationResults: ECertFailedValidationResult[], log: ProcessSummary, ): void { - // Validate stop part-time disbursement restrictions. + // Validate stop disbursement restrictions. const stopDisbursementRestrictions = getRestrictionsByActionType( eCertDisbursement, restrictionActionType, diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts index b9b759943a..2ed35ebf2f 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-full-time-step.ts @@ -64,7 +64,7 @@ export class ValidateDisbursementFullTimeStep log.info("Executing full-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); // Validate stop full-time disbursement restrictions. - this.validateStopDisbursementRestriction( + super.validateStopDisbursementRestriction( eCertDisbursement, RestrictionActionType.StopFullTimeDisbursement, validationResults, diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts index cf40521e5e..e2c81cb859 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-part-time-step.ts @@ -70,7 +70,7 @@ export class ValidateDisbursementPartTimeStep log.info("Executing part-time disbursement validations."); const validationResults = super.validate(eCertDisbursement, log); // Validate stop part-time disbursement restrictions. - this.validateStopDisbursementRestriction( + super.validateStopDisbursementRestriction( eCertDisbursement, RestrictionActionType.StopPartTimeDisbursement, validationResults, From a15859fd1fb252f07333cb28b57262fa7392c9f5 Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Tue, 6 Jan 2026 13:42:39 -0700 Subject: [PATCH 12/13] review comments update --- .../disbursement-schedule.models.ts | 82 ++++++++++--------- .../validate-disbursement-base.ts | 21 ++++- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index a149a63bc6..6feaac8009 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -165,7 +165,13 @@ export interface DisabilityDetails { /** * Represents an active restriction which can be student or institution restriction. */ -export interface ActiveRestriction { +export abstract class ActiveRestriction { + constructor( + /** + * The party (student or institution) who is restricted. + */ + readonly restrictedParty: RestrictedParty, + ) {} /** * Restriction id. */ @@ -182,31 +188,29 @@ export interface ActiveRestriction { * Action effective conditions associated with the restriction. */ actionEffectiveConditions?: ActionEffectiveCondition[]; - /** - * The party (student or institution) who is restricted. - */ - restrictedParty?: RestrictedParty; } /** * Represents an active student restriction. */ -export interface StudentActiveRestriction extends ActiveRestriction { +export class StudentActiveRestriction extends ActiveRestriction { + constructor() { + super(RestrictedParty.Student); + } /** * Association between the student and * the active restriction on his account. */ studentRestrictionId: number; - /** - * Restriction is applied to a student. - */ - restrictedParty: RestrictedParty.Student; } /** * Represents an active institution restriction. */ -export interface InstitutionActiveRestriction extends ActiveRestriction { +export class InstitutionActiveRestriction extends ActiveRestriction { + constructor() { + super(RestrictedParty.Institution); + } /** * Association between the institution and * the active restriction on institution account. @@ -220,10 +224,6 @@ export interface InstitutionActiveRestriction extends ActiveRestriction { * Specific location the restriction applies to. */ location: InstitutionLocation; - /** - * Restriction is applied to a student. - */ - restrictedParty: RestrictedParty.Institution; } /** @@ -417,15 +417,17 @@ export function mapStudentActiveRestrictions( studentRestrictions: StudentRestriction[], ): StudentActiveRestriction[] { return studentRestrictions.map( - (studentRestriction) => ({ - studentRestrictionId: studentRestriction.id, - id: studentRestriction.restriction.id, - code: studentRestriction.restriction.restrictionCode as RestrictionCode, - actions: studentRestriction.restriction.actionType, - actionEffectiveConditions: - studentRestriction.restriction.actionEffectiveConditions, - restrictedParty: RestrictedParty.Student, - }), + (studentRestriction) => { + const activeRestriction = new StudentActiveRestriction(); + activeRestriction.studentRestrictionId = studentRestriction.id; + activeRestriction.id = studentRestriction.restriction.id; + activeRestriction.code = studentRestriction.restriction + .restrictionCode as RestrictionCode; + activeRestriction.actions = studentRestriction.restriction.actionType; + activeRestriction.actionEffectiveConditions = + studentRestriction.restriction.actionEffectiveConditions; + return activeRestriction; + }, ); } @@ -439,18 +441,19 @@ export function mapInstitutionActiveRestrictions( institutionRestrictions: InstitutionRestriction[], ): InstitutionActiveRestriction[] { return institutionRestrictions.map( - (institutionRestriction) => ({ - institutionRestrictionId: institutionRestriction.id, - id: institutionRestriction.restriction.id, - code: institutionRestriction.restriction - .restrictionCode as RestrictionCode, - actions: institutionRestriction.restriction.actionType, - program: institutionRestriction.program, - location: institutionRestriction.location, - actionEffectiveConditions: - institutionRestriction.restriction.actionEffectiveConditions, - restrictedParty: RestrictedParty.Institution, - }), + (institutionRestriction) => { + const activeRestriction = new InstitutionActiveRestriction(); + activeRestriction.institutionRestrictionId = institutionRestriction.id; + activeRestriction.id = institutionRestriction.restriction.id; + activeRestriction.code = institutionRestriction.restriction + .restrictionCode as RestrictionCode; + activeRestriction.actions = institutionRestriction.restriction.actionType; + activeRestriction.program = institutionRestriction.program; + activeRestriction.location = institutionRestriction.location; + activeRestriction.actionEffectiveConditions = + institutionRestriction.restriction.actionEffectiveConditions; + return activeRestriction; + }, ); } @@ -508,14 +511,17 @@ export enum ECertFailedValidation { } interface StopDisbursementRestrictionValidationResult { - resultType: ECertFailedValidation.HasStopDisbursementRestriction; + resultType: + | ECertFailedValidation.HasStopDisbursementRestriction + | ECertFailedValidation.HasStopDisbursementInstitutionRestriction; additionalInfo: { restrictionCodes: RestrictionCode[] }; } interface OtherECertFailedValidationResult { resultType: Exclude< ECertFailedValidation, - ECertFailedValidation.HasStopDisbursementRestriction + | ECertFailedValidation.HasStopDisbursementRestriction + | ECertFailedValidation.HasStopDisbursementInstitutionRestriction >; } diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts index 2f03fad519..9e6f69b23e 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts @@ -122,12 +122,17 @@ export abstract class ValidateDisbursementBase { log.info( `Student has an active '${restrictionActionType}' restriction and the disbursement calculation will not proceed.`, ); + // Gather restriction codes for additional info. + const restrictionCodes = stopDisbursementRestrictions + .filter( + (restriction) => + restriction.restrictedParty === RestrictedParty.Student, + ) + .map((restriction) => restriction.code); validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementRestriction, additionalInfo: { - restrictionCodes: stopDisbursementRestrictions.map( - (restriction) => restriction.code, - ), + restrictionCodes, }, }); } @@ -138,9 +143,19 @@ export abstract class ValidateDisbursementBase { `Institution has an effective '${restrictionActionType}' restriction` + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, ); + // Gather restriction codes for additional info. + const restrictionCodes = stopDisbursementRestrictions + .filter( + (restriction) => + restriction.restrictedParty === RestrictedParty.Institution, + ) + .map((restriction) => restriction.code); validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementInstitutionRestriction, + additionalInfo: { + restrictionCodes, + }, }); } } From e809e91c964a253e3e0687e0fddc0f0b6498a4aa Mon Sep 17 00:00:00 2001 From: Dheepak Ramanathan Date: Tue, 6 Jan 2026 15:14:30 -0700 Subject: [PATCH 13/13] Code review adjustments --- .../disbursement-schedule.models.ts | 27 ++++++++------ .../validate-disbursement-base.ts | 36 ++++++++----------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts index 6feaac8009..098c2beb8e 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/disbursement-schedule.models.ts @@ -165,13 +165,14 @@ export interface DisabilityDetails { /** * Represents an active restriction which can be student or institution restriction. */ -export abstract class ActiveRestriction { - constructor( - /** - * The party (student or institution) who is restricted. - */ - readonly restrictedParty: RestrictedParty, - ) {} +export type ActiveRestriction = + | StudentActiveRestriction + | InstitutionActiveRestriction; + +/** + * Base class for active restrictions. + */ +abstract class BaseActiveRestriction { /** * Restriction id. */ @@ -193,9 +194,11 @@ export abstract class ActiveRestriction { /** * Represents an active student restriction. */ -export class StudentActiveRestriction extends ActiveRestriction { +export class StudentActiveRestriction extends BaseActiveRestriction { + readonly restrictedParty: RestrictedParty.Student; constructor() { - super(RestrictedParty.Student); + super(); + this.restrictedParty = RestrictedParty.Student; } /** * Association between the student and @@ -207,9 +210,11 @@ export class StudentActiveRestriction extends ActiveRestriction { /** * Represents an active institution restriction. */ -export class InstitutionActiveRestriction extends ActiveRestriction { +export class InstitutionActiveRestriction extends BaseActiveRestriction { + readonly restrictedParty: RestrictedParty.Institution; constructor() { - super(RestrictedParty.Institution); + super(); + this.restrictedParty = RestrictedParty.Institution; } /** * Association between the institution and diff --git a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts index 9e6f69b23e..1a0f56b478 100644 --- a/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts +++ b/sources/packages/backend/libs/integrations/src/services/disbursement-schedule/e-cert-processing-steps/validate-disbursement-base.ts @@ -105,56 +105,48 @@ export abstract class ValidateDisbursementBase { restrictionActionType, ); if (stopDisbursementRestrictions.length) { - const isStudentRestricted = stopDisbursementRestrictions.some( + const studentRestrictions = stopDisbursementRestrictions.filter( (restriction) => restriction.restrictedParty === RestrictedParty.Student, ); - const isInstitutionRestricted = stopDisbursementRestrictions.some( + const institutionRestrictions = stopDisbursementRestrictions.filter( (restriction) => restriction.restrictedParty === RestrictedParty.Institution, ); - if (!isStudentRestricted && !isInstitutionRestricted) { + if (!studentRestrictions.length && !institutionRestrictions.length) { throw new Error( - "The stop disbursement restricted party is neither student nor institution.", + "The stop disbursement restrictions are neither student nor institution restrictions.", ); } - if (isStudentRestricted) { + if (studentRestrictions.length) { log.info( `Student has an active '${restrictionActionType}' restriction and the disbursement calculation will not proceed.`, ); - // Gather restriction codes for additional info. - const restrictionCodes = stopDisbursementRestrictions - .filter( - (restriction) => - restriction.restrictedParty === RestrictedParty.Student, - ) - .map((restriction) => restriction.code); + validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementRestriction, additionalInfo: { - restrictionCodes, + restrictionCodes: studentRestrictions.map( + (restriction) => restriction.code, + ), }, }); } - if (isInstitutionRestricted) { + if (institutionRestrictions.length) { const program = eCertDisbursement.offering.educationProgram; const location = eCertDisbursement.offering.institutionLocation; log.info( `Institution has an effective '${restrictionActionType}' restriction` + ` for program ${program.id} and location ${location.id} and the disbursement calculation will not proceed.`, ); - // Gather restriction codes for additional info. - const restrictionCodes = stopDisbursementRestrictions - .filter( - (restriction) => - restriction.restrictedParty === RestrictedParty.Institution, - ) - .map((restriction) => restriction.code); + validationResults.push({ resultType: ECertFailedValidation.HasStopDisbursementInstitutionRestriction, additionalInfo: { - restrictionCodes, + restrictionCodes: institutionRestrictions.map( + (restriction) => restriction.code, + ), }, }); }