Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ env:
TLS_KEY: ${{ secrets.TLS_KEY }}
TLS_CA_CERTIFICATE: ${{ secrets.TLS_CA_CERTIFICATE }}
ALLOW_BETA_USERS_ONLY: ${{ vars.ALLOW_BETA_USERS_ONLY }}
ALLOW_BETA_INSTITUTIONS_ONLY: ${{ vars.ALLOW_BETA_INSTITUTIONS_ONLY }}
FORMIO_CPU_REQUEST: ${{ vars.FORMIO_CPU_REQUEST }}
FORMIO_MEMORY_REQUEST: ${{ vars.FORMIO_MEMORY_REQUEST }}
FORMIO_MEMORY_LIMIT: ${{ vars.FORMIO_MEMORY_LIMIT }}
Expand Down
3 changes: 3 additions & 0 deletions configs/env-example
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,8 @@ MAXIMUM_IDLE_TIME_FOR_WARNING_AEST =
# Allow beta users only to access the system if true.
ALLOW_BETA_USERS_ONLY=

# Allow full-time application submissions only for beta institution locations if true.
ALLOW_BETA_INSTITUTIONS_ONLY=

# App Environment
APP_ENV=
1 change: 1 addition & 0 deletions devops/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ deploy-api:
-p TLS_KEY=$(TLS_KEY) \
-p TLS_CA_CERTIFICATE=$(TLS_CA_CERTIFICATE) \
-p ALLOW_BETA_USERS_ONLY=$(ALLOW_BETA_USERS_ONLY) \
-p ALLOW_BETA_INSTITUTIONS_ONLY=$(ALLOW_BETA_INSTITUTIONS_ONLY) \
-p EXTERNAL_IP_WHITELIST=$(EXTERNAL_IP_WHITELIST) \
-p QUEUE_DASHBOARD_BASE_URL=https://$(HOST) \
-p CPU_REQUEST=$(API_CPU_REQUEST) \
Expand Down
4 changes: 4 additions & 0 deletions devops/openshift/api-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ objects:
value: "${APP_ENV}"
- name: ALLOW_BETA_USERS_ONLY
value: "${ALLOW_BETA_USERS_ONLY}"
- name: ALLOW_BETA_INSTITUTIONS_ONLY
value: "${ALLOW_BETA_INSTITUTIONS_ONLY}"
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
Expand Down Expand Up @@ -585,6 +587,8 @@ parameters:
required: true
- name: ALLOW_BETA_USERS_ONLY
required: true
- name: ALLOW_BETA_INSTITUTIONS_ONLY
required: true
- name: EXTERNAL_API_PATH
value: /api/external
required: true
Expand Down
3 changes: 3 additions & 0 deletions sources/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export APP_ENV := $(or $(APP_ENV), local)
# Allow beta users only to access the system if true.
export ALLOW_BETA_USERS_ONLY := $(or $(ALLOW_BETA_USERS_ONLY), false)

# Allow full-time application submissions only for beta institution locations if true.
export ALLOW_BETA_INSTITUTIONS_ONLY := $(or $(ALLOW_BETA_INSTITUTIONS_ONLY), false)

# Starts all applications: SIMS application (Web Portals, SIMS Api, Workers, Queue Consumers),
# and also Form.io server, and Camunda related services.
# Use make deploy-camunda-definitions to deploy all workflow definitions.
Expand Down
1 change: 1 addition & 0 deletions sources/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ services:
- MAXIMUM_IDLE_TIME_FOR_WARNING_AEST=${MAXIMUM_IDLE_TIME_FOR_WARNING_AEST}
- APP_ENV=${APP_ENV}
- ALLOW_BETA_USERS_ONLY=${ALLOW_BETA_USERS_ONLY}
- ALLOW_BETA_INSTITUTIONS_ONLY=${ALLOW_BETA_INSTITUTIONS_ONLY}
ports:
- ${API_PORT}:${API_PORT}
volumes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ParseIntPipe,
DefaultValuePipe,
ParseBoolPipe,
ForbiddenException,
} from "@nestjs/common";
import {
ApplicationService,
Expand Down Expand Up @@ -67,6 +68,7 @@ import { ApplicationStatus, OfferingIntensity } from "@sims/sims-db";
import { ConfirmationOfEnrollmentService } from "@sims/services";
import { ConfigService } from "@sims/utilities/config";
import { ECertPreValidationService } from "@sims/integrations/services/disbursement-schedule/e-cert-calculation";
import { INVALID_OPERATION_IN_THE_CURRENT_STATE } from "@sims/services/constants";

@AllowAuthorizedParty(AuthorizedParties.student)
@RequiresStudentAccount()
Expand Down Expand Up @@ -198,7 +200,8 @@ export class ApplicationStudentsController extends BaseController {
"or INVALID_OPERATION_IN_THE_CURRENT_STATUS or ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE " +
"or INSTITUTION_LOCATION_NOT_VALID or OFFERING_NOT_VALID " +
"or Invalid offering intensity " +
"or dynamic form configuration not found.",
"or dynamic form configuration not found " +
"or application submission for a non-beta institution location is not allowed.",
})
@ApiBadRequestResponse({
description: "Form validation failed or Offering intensity type is invalid",
Expand Down Expand Up @@ -256,6 +259,8 @@ export class ApplicationStudentsController extends BaseController {
);
case ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE:
throw new UnprocessableEntityException(error.message);
case INVALID_OPERATION_IN_THE_CURRENT_STATE:
throw new ForbiddenException(error.message);
}
}
throw error;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get } from "@nestjs/common";
import { Controller, Get, Query } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import {
Expand All @@ -9,6 +9,9 @@ import { InstitutionLocationService } from "../../services";
import { ClientTypeBaseRoute } from "../../types";
import BaseController from "../BaseController";
import { OptionItemAPIOutDTO } from "../models/common.dto";
import { OfferingIntensity } from "@sims/sims-db";
import { ParseEnumQueryPipe } from "../utils/custom-validation-pipe";
import { ConfigService } from "@sims/utilities/config";

/**
* Institution location controller for Students client.
Expand All @@ -18,18 +21,33 @@ import { OptionItemAPIOutDTO } from "../models/common.dto";
@Controller("location")
@ApiTags(`${ClientTypeBaseRoute.Student}-location`)
export class InstitutionLocationStudentsController extends BaseController {
constructor(private readonly locationService: InstitutionLocationService) {
constructor(
private readonly locationService: InstitutionLocationService,
private readonly configService: ConfigService,
) {
super();
}

/**
* Get a key/value pair list of all locations
* from all institution available.
* @param offeringIntensity offering intensity to evaluate if only beta institution locations
* should be returned.
* @returns key/value pair list of all locations.
*/
@Get("options-list")
async getOptionsList(): Promise<OptionItemAPIOutDTO[]> {
const locations = await this.locationService.getDesignatedLocations();
async getOptionsList(
@Query("offeringIntensity", new ParseEnumQueryPipe(OfferingIntensity))
offeringIntensity?: OfferingIntensity,
Comment on lines +40 to +41
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new query parameter offeringIntensity lacks API documentation. Consider adding OpenAPI decorators like @ApiQuery() to document this optional parameter, its purpose, and possible values for API consumers.

Copilot uses AI. Check for mistakes.
): Promise<OptionItemAPIOutDTO[]> {
// For the context of full-time applications, allow only beta institution locations
// if the beta flag is enabled.
const allowBetaInstitutionsOnly =
offeringIntensity === OfferingIntensity.fullTime &&
this.configService.allowBetaInstitutionsOnly;
const locations = await this.locationService.getDesignatedLocations(
allowBetaInstitutionsOnly,
);
return locations.map((location) => ({
id: location.id,
description: location.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
} from "@sims/services/notifications";
import { InstitutionLocationService } from "../institution-location/institution-location.service";
import { StudentService } from "..";
import { INVALID_OPERATION_IN_THE_CURRENT_STATE } from "@sims/services/constants";

export const APPLICATION_DRAFT_NOT_FOUND = "APPLICATION_DRAFT_NOT_FOUND";
export const MORE_THAN_ONE_APPLICATION_DRAFT_ERROR =
Expand Down Expand Up @@ -179,6 +180,10 @@ export class ApplicationService extends RecordDataModelService<Application> {
INSTITUTION_LOCATION_NOT_VALID,
);
}
// Validate beta institution location only if the offering intensity is full-time.
if (application.offeringIntensity === OfferingIntensity.fullTime) {
this.validateBetaInstitutionLocation(institutionLocation.isBeta);
}
// Offering is assigned to the original assessment if the application is not
// required for PIR.
if (applicationData.selectedProgram && applicationData.selectedOffering) {
Expand Down Expand Up @@ -2415,6 +2420,25 @@ export class ApplicationService extends RecordDataModelService<Application> {
}
}

/**
* Check if the beta institution mode is enabled and if enabled
* allow application submission only for beta institution locations.
* @param isBetaInstitutionLocation is beta institution.
* @throws {ForbiddenException} application submission for a non-beta institution location is not allowed.
*/
private validateBetaInstitutionLocation(
isBetaInstitutionLocation: boolean,
): void {
const allowBetaInstitutionsOnly =
this.configService.allowBetaInstitutionsOnly;
if (allowBetaInstitutionsOnly && !isBetaInstitutionLocation) {
throw new CustomNamedError(
"Application submission for a non-beta institution location is not allowed.",
INVALID_OPERATION_IN_THE_CURRENT_STATE,
);
}
}

@InjectLogger()
logger: LoggerService;
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,26 @@ export class InstitutionLocationService extends RecordDataModelService<Instituti
/**
* Gets all locations available and return just
* a subset of available data.
* @param onlyBetaInstitutions if true then return only beta institution locations.
* @returns all locations.
*/
async getDesignatedLocations(): Promise<Partial<InstitutionLocation>[]> {
return this.repo
async getDesignatedLocations(
onlyBetaInstitutions: boolean,
): Promise<Partial<InstitutionLocation>[]> {
const designatedLocationsQuery = this.repo
.createQueryBuilder("location")
.select("location.id")
.addSelect("location.name")
.orderBy("location.name")
.andWhere(
.where(
`EXISTS(${this.designationAgreementLocationService
.getExistsDesignatedLocation()
.getSql()})`,
)
.getMany();
);
if (onlyBetaInstitutions) {
designatedLocationsQuery.andWhere("location.isBeta = true");
}
return designatedLocationsQuery.getMany();
}

/**
Expand All @@ -177,6 +183,7 @@ export class InstitutionLocationService extends RecordDataModelService<Instituti
"institutionLocation.id",
"institutionLocation.institutionCode",
"institutionLocation.primaryContact",
"institutionLocation.isBeta",
"institution.id",
"institution.operatingName",
"institution.legalOperatingName",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { getSQLFileData } from "../utilities/sqlLoader";

export class InstitutionLocationsAddColIsBeta1758903872348
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData("Add-col-is-beta.sql", "InstitutionLocations"),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData("Rollback-add-col-is-beta.sql", "InstitutionLocations"),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ALTER TABLE
sims.institution_locations
ADD
COLUMN is_beta BOOLEAN NOT NULL DEFAULT FALSE;

COMMENT ON COLUMN sims.institution_locations.is_beta IS 'Identifies if the institution location is a beta.';

-- Add column to history table.
ALTER TABLE
sims.institution_locations_history
ADD
COLUMN is_beta BOOLEAN;

COMMENT ON COLUMN sims.institution_locations_history.is_beta IS 'Historical data from the original table. See original table comments for details.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE
sims.institution_locations DROP COLUMN is_beta;

ALTER TABLE
sims.institution_locations_history DROP COLUMN is_beta;
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,12 @@ export class InstitutionLocation extends RecordDataModel {
type: "varchar",
})
integrationContacts?: string[];

/**
* Identifies if the institution location is a beta.
*/
@Column({
name: "is_beta",
})
isBeta: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,17 @@ export class ConfigService {
);
}

/**
* When enabled, allow full-time application submission
* only for programs from beta institution locations.
*/
get allowBetaInstitutionsOnly(): boolean {
return this.getCachedConfig(
"allowBetaInstitutionsOnlyConfig",
process.env.ALLOW_BETA_INSTITUTIONS_ONLY === "true",
);
}

/**
* When defined as true, allows the simulation of a complete cycle of the
* CRA send/response process that allows the workflow to proceed without
Expand Down
1 change: 1 addition & 0 deletions sources/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export MAXIMUM_IDLE_TIME_FOR_WARNING_AEST := $(or $(MAXIMUM_IDLE_TIME_FOR_WARNIN
export APP_ENV := $(or $(APP_ENV), local)

export ALLOW_BETA_USERS_ONLY :=$(or $(ALLOW_BETA_USERS_ONLY), false)
export ALLOW_BETA_INSTITUTIONS_ONLY :=$(or $(ALLOW_BETA_INSTITUTIONS_ONLY), false)

backend-unit-tests:
@cd ../packages/backend && npm i && npm run test:cov
Expand Down
1 change: 1 addition & 0 deletions sources/tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ services:
- MAXIMUM_IDLE_TIME_FOR_WARNING_AEST=${MAXIMUM_IDLE_TIME_FOR_WARNING_AEST}
- APP_ENV=${APP_ENV}
- ALLOW_BETA_USERS_ONLY=${ALLOW_BETA_USERS_ONLY}
- ALLOW_BETA_INSTITUTIONS_ONLY=${ALLOW_BETA_INSTITUTIONS_ONLY}
networks:
- local-network-tests
restart: always
Expand Down
Loading