Skip to content

Commit

Permalink
#3738 - SIMS to SFAS Part 2 (#3829)
Browse files Browse the repository at this point in the history
# SIMS TO SFAS - INTEGRATION IMPLEMENTATION FOR STUDENT DATA RECORDS

## ENV

- [x] Added a new env variable `SFAS_SEND_FOLDER`. Declared it as github
env, not as secret.

## PROCESS
- [x] File generated with following name structure
`SIMS-TO-SFAS-YYYYMMDD-HHMMSS.TXT`

![image](https://github.com/user-attachments/assets/39411500-a070-4bf4-a0dc-d312a3e450e0)

- [x] Student data is retrieved to build the file for all students who
has one or more of the following updates since last run date.
    - Student or User data
    - Sin validation data
    - Cas supplier data
    - Overawards data

- [x] Students with at least one submitted application are considered to
extract the data. As submitted applications can be cancelled and also
the Draft applications can be cancelled, application status NOT Draft
could be tricky. So used the condition of application with current
assessment NOT NULL to identify a submitted application.
- [x] When one or more students with updates are present, then a file is
produced and sent to SFAS SFTP location.
**Summary:**

![image](https://github.com/user-attachments/assets/11b9ff5d-3dbf-44ca-9989-c7899ba104c9)

**Process Logs:**

![image](https://github.com/user-attachments/assets/7ec7ef8a-0ddf-41e9-a767-5003637864c3)

- [x] When no students with updates are present, then no file is
produced.

**Summary:**

![image](https://github.com/user-attachments/assets/4a43e594-a262-4841-ae4e-5dbd7d46f75d)

**Process Logs:**
 

![image](https://github.com/user-attachments/assets/326c2561-e17d-484c-a3cc-c61a608fe73c)

## TECHNICAL CONTEXT

- STEPS involved in creation of the bridge data file

```
STEP 1 [Implemented]: Get the reference date of last bridge file sent. If there is no bridge file sent, it is null.

STEP 2 [Implemented]: Get all the student ids of students who has one or more student data related change since last bridge file date.

STEP 3 [TODO]: Get all the student ids and application details of students who has one or more application data related change since last bridge file date.

STEP 4 [TODO]: Get all the student ids and restriction details of students who has one or more restriction data related change since last bridge file date.

STEP 5 [Partially Implemented]: Consolidate all the studentIds produced in STEPS 2,3 and 4. 

STEP 6 [Partially Implemented]: Get student details of students who has one or more student data related change using consolidated studentIds.

STEP 7 [Implemented]: Build header, footer and student records for file lines.

STEP 8 [TODO]: Build application records and restriction records for file lines.

STEP 9[Implemented]: Upload the file to SFTP and create bridge file log.

```

- For future implementations and areas, search for `TODO: SIMS to SFAS`
in the code.
  • Loading branch information
dheepak-aot authored Oct 28, 2024
1 parent 4a2ac40 commit 7eedbb5
Show file tree
Hide file tree
Showing 30 changed files with 803 additions and 15 deletions.
1 change: 1 addition & 0 deletions .github/workflows/env-setup-deploy-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
ESDC_RESPONSE_FOLDER: ${{ secrets.ESDC_RESPONSE_FOLDER }}
ESDC_ENVIRONMENT_CODE: ${{ secrets.ESDC_ENVIRONMENT_CODE }}
SFAS_RECEIVE_FOLDER: ${{ secrets.SFAS_RECEIVE_FOLDER }}
SFAS_SEND_FOLDER: ${{ vars.SFAS_SEND_FOLDER }}
SIMS_DB_NAME: ${{ secrets.SIMS_DB_NAME }}
INSTITUTION_REQUEST_FOLDER: ${{ secrets.INSTITUTION_REQUEST_FOLDER }}
INSTITUTION_RESPONSE_FOLDER: ${{ vars.INSTITUTION_RESPONSE_FOLDER }}
Expand Down
1 change: 1 addition & 0 deletions configs/env-example
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ FILE_UPLOAD_ALLOWED_EXTENSIONS=.pdf,.doc,.docx,.jpg,.png

#SFAS Integration
SFAS_RECEIVE_FOLDER=
SFAS_SEND_FOLDER=

#Institution Integration
INSTITUTION_REQUEST_FOLDER=
Expand Down
1 change: 1 addition & 0 deletions devops/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ init-secrets:
-p ESDC_RESPONSE_FOLDER=$(ESDC_RESPONSE_FOLDER) \
-p ESDC_ENVIRONMENT_CODE=$(ESDC_ENVIRONMENT_CODE) \
-p SFAS_RECEIVE_FOLDER=$(SFAS_RECEIVE_FOLDER) \
-p SFAS_SEND_FOLDER=$(SFAS_SEND_FOLDER) \
-p SIMS_DB_NAME=$(SIMS_DB_NAME) \
-p INSTITUTION_REQUEST_FOLDER=$(INSTITUTION_REQUEST_FOLDER) \
-p INSTITUTION_RESPONSE_FOLDER=$(INSTITUTION_RESPONSE_FOLDER) \
Expand Down
6 changes: 6 additions & 0 deletions devops/openshift/init-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ objects:
esdc-response-folder: ${ESDC_RESPONSE_FOLDER}
esdc-environment-code: ${ESDC_ENVIRONMENT_CODE}
sfas-receive-folder: ${SFAS_RECEIVE_FOLDER}
sfas-send-folder: ${SFAS_SEND_FOLDER}
sims-db-name: ${SIMS_DB_NAME}
institution-request-folder: ${INSTITUTION_REQUEST_FOLDER}
institution-response-folder: ${INSTITUTION_RESPONSE_FOLDER}
Expand Down Expand Up @@ -142,6 +143,11 @@ parameters:
required: true
description: |
Folder on the SFTP where SFAS integration files will be placed.
- name: SFAS_SEND_FOLDER
displayName: SFAS send folder
required: true
description: |
Folder on the SFTP where files sent to SFAS will be placed.
- name: SIMS_DB_NAME
displayName: SIMS database name
required: true
Expand Down
7 changes: 7 additions & 0 deletions devops/openshift/queue-consumers-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ objects:
secretKeyRef:
key: ${SFAS_RECEIVE_FOLDER_NAME_KEY}
name: ${QUEUE_CONSUMERS_SECRET_NAME}
- name: SFAS_SEND_FOLDER
valueFrom:
secretKeyRef:
key: ${SFAS_SEND_FOLDER_NAME_KEY}
name: ${QUEUE_CONSUMERS_SECRET_NAME}
- name: CRA_REQUEST_FOLDER
valueFrom:
secretKeyRef:
Expand Down Expand Up @@ -448,6 +453,8 @@ parameters:
value: gc-notify-api-key
- name: SFAS_RECEIVE_FOLDER_NAME_KEY
value: sfas-receive-folder
- name: SFAS_SEND_FOLDER_NAME_KEY
value: sfas-send-folder
- name: ATBC_LOGIN_ENDPOINT
required: true
- name: ATBC_ENDPOINT
Expand Down
1 change: 1 addition & 0 deletions sources/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export FILE_UPLOAD_MAX_FILE_SIZE := $(or $(FILE_UPLOAD_MAX_FILE_SIZE), 15728640)
export FILE_UPLOAD_ALLOWED_EXTENSIONS := $(or $(FILE_UPLOAD_ALLOWED_EXTENSIONS), .pdf,.doc,.docx,.jpg,.png)
#SFAS Integration
export SFAS_RECEIVE_FOLDER := $(or $(SFAS_RECEIVE_FOLDER), SFAS-Receive)
export SFAS_SEND_FOLDER := $(or $(SFAS_SEND_FOLDER), OUT)
# Fulltime Allowed
export IS_FULLTIME_ALLOWED := $(or $(IS_FULLTIME_ALLOWED), true)
#Institution Integration
Expand Down
1 change: 1 addition & 0 deletions sources/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ services:
- ZONE_B_SFTP_PRIVATE_KEY_PASSPHRASE=${ZONE_B_SFTP_PRIVATE_KEY_PASSPHRASE}
- ZONE_B_SFTP_PRIVATE_KEY=${ZONE_B_SFTP_PRIVATE_KEY}
- SFAS_RECEIVE_FOLDER=${SFAS_RECEIVE_FOLDER}
- SFAS_SEND_FOLDER=${SFAS_SEND_FOLDER}
- ATBC_LOGIN_ENDPOINT=${ATBC_LOGIN_ENDPOINT}
- ATBC_USERNAME=${ATBC_USERNAME}
- ATBC_PASSWORD=${ATBC_PASSWORD}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,25 @@ import {
logProcessSummaryToJobLogger,
} from "../../../utilities";
import { QueueNames } from "@sims/utilities";
import { SIMSToSFASProcessingService } from "@sims/integrations/sfas-integration";
import { SIMSToSFASService } from "@sims/integrations/services/sfas";
import { SIMS_TO_SFAS_BRIDGE_FILE_INITIAL_DATE } from "@sims/integrations/constants";

@Processor(QueueNames.SIMSToSFASIntegration)
export class SIMSToSFASIntegrationScheduler extends BaseScheduler<void> {
constructor(
@InjectQueue(QueueNames.SIMSToSFASIntegration)
schedulerQueue: Queue<void>,
queueService: QueueService,
private readonly simsToSFASService: SIMSToSFASService,
private readonly simsToSFASIntegrationProcessingService: SIMSToSFASProcessingService,
) {
super(schedulerQueue, queueService);
}

/**
* Generate data file consisting of all student and application updates in SIMS since the previous file generation
* and send the data file to SFAS.
* Generate data file consisting of all student, application and restriction updates in SIMS since the previous file generation
* until the start of the current job and send the data file to SFAS.
* @param job job.
* @returns process summary.
*/
Expand All @@ -37,13 +42,38 @@ export class SIMSToSFASIntegrationScheduler extends BaseScheduler<void> {
processSummary.info(
`Processing SIMS to SFAS integration job. Job id: ${job.id} and Job name: ${job.name}.`,
);
// TODO: Processing implementation of SIMS to SFAS integration.
// Set the bridge data extracted date as current date-time
// before staring to process the bridge data.
const bridgeDataExtractedDate = new Date();
const latestBridgeFileReferenceDate =
await this.simsToSFASService.getLatestBridgeFileLogDate();
// If the bridge is being executed for the first time, set the modified since date to
// a safe initial date.
const modifiedSince =
latestBridgeFileReferenceDate ?? SIMS_TO_SFAS_BRIDGE_FILE_INITIAL_DATE;
processSummary.info(
`Processing data since ${modifiedSince} until ${bridgeDataExtractedDate}.`,
);
const integrationProcessSummary = new ProcessSummary();
processSummary.children(integrationProcessSummary);
const { studentRecordsSent, uploadedFileName } =
await this.simsToSFASIntegrationProcessingService.processSIMSUpdates(
integrationProcessSummary,
modifiedSince,
bridgeDataExtractedDate,
);
processSummary.info("Processing SIMS to SFAS integration job completed.");
return getSuccessMessageWithAttentionCheck(
["Process finalized with success."],
[
"Process finalized with success.",
`Student records sent: ${studentRecordsSent}.`,
`Uploaded file name: ${uploadedFileName}.`,
],
processSummary,
);
} catch (error: unknown) {
const errorMessage = "Unexpected error while executing the job.";
const errorMessage =
"Unexpected error while executing the SIMS to SFAS integration job.";
processSummary.error(errorMessage, error);
throw new Error(errorMessage, { cause: error });
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ export const APPLICATION_CHANGES_REPORT_PREFIX = "PBC.EDU.APPCHANGES";
* SFTP directory name used to archive files.
*/
export const SFTP_ARCHIVE_DIRECTORY = "Archive";

/**
* Initial date for the SIMS to SFAS bridge first ever execution.
*/
export const SIMS_TO_SFAS_BRIDGE_FILE_INITIAL_DATE = new Date("2024-01-01");
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import {
NUMBER_FILLER,
SPACE_FILLER,
ScholasticStandingCode,
YNFlag,
} from "./models/ier12-integration.model";
import { FullTimeAwardTypes } from "@sims/integrations/models";
import { FullTimeAwardTypes, YNFlag } from "@sims/integrations/models";

/**
* Record of a IER12 file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
IER12Record,
IERAward,
ScholasticStandingCode,
YNFlag,
} from "./models/ier12-integration.model";
import {
ApplicationStatus,
Expand All @@ -22,6 +21,7 @@ import {
getTotalYearsOfStudy,
replaceLineBreaks,
} from "@sims/utilities";
import { YNFlag } from "@sims/integrations/models";

/**
* Manages the creation of the content files that needs to be sent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ export enum ScholasticStandingCode {
ST = "ST",
}

export enum YNFlag {
Y = "Y",
N = "N",
}

export type IERAddressInfo = Omit<AddressInfo, "country" | "selectedCountry">;

export type IERAward = Pick<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ export enum FullTimeAwardTypes {
BGPD = "BGPD",
SBSD = "SBSD",
}

export enum YNFlag {
Y = "Y",
N = "N",
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from "./sfas-data-importer";
export * from "./sfas-individual.service";
export * from "./sfas-part-time-application.service";
export * from "./sfas-restriction.service";
export * from "./sims-to-sfas.model";
export * from "./sims-to-sfas.service";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Student } from "@sims/sims-db";

export type StudentDetail = Student & {
cslfOverawardTotal?: string;
bcslOverawardTotal?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { StudentDetail } from "./sims-to-sfas.model";
import {
BC_STUDENT_LOAN_AWARD_CODE,
CANADA_STUDENT_LOAN_FULL_TIME_AWARD_CODE,
} from "@sims/services/constants";
import {
Application,
ApplicationStatus,
mapFromRawAndEntities,
SFASBridgeLog,
Student,
} from "@sims/sims-db";
import { Brackets, Repository } from "typeorm";

/**
* SIMS to SFAS services.
*/
@Injectable()
export class SIMSToSFASService {
constructor(
@InjectRepository(Student)
private readonly studentRepo: Repository<Student>,
@InjectRepository(Application)
private readonly applicationRepo: Repository<Application>,
@InjectRepository(SFASBridgeLog)
private readonly sfasBridgeLogRepo: Repository<SFASBridgeLog>,
) {}

/**
* Get the latest bridge file log date.
* When there is no log, null will be returned.
* @returns latest bridge file log date.
*/
async getLatestBridgeFileLogDate(): Promise<Date | null> {
const [latestBridgeFileLog] = await this.sfasBridgeLogRepo.find({
select: { id: true, referenceDate: true },
order: { referenceDate: "DESC" },
take: 1,
});
return latestBridgeFileLog ? latestBridgeFileLog.referenceDate : null;
}

/**
* Log the details of bridge file that was sent to SFAS.
* @param referenceDate date when the bridge file data was extracted.
* @param fileName bridge file name.
*/
async logBridgeFileDetails(
referenceDate: Date,
fileName: string,
): Promise<void> {
await this.sfasBridgeLogRepo.insert({
referenceDate,
generatedFileName: fileName,
});
}

/**
* Get all student ids of students who have one or more updates
* between the given period.
* The updates can be one or more of the following:
* - Student or User data
* - SIN validation data
* - CAS supplier data
* - Overawards data
* @param modifiedSince the date after which the student data was updated.
* @param modifiedUntil the date until which the student data was updated.
*/
async getAllStudentsWithUpdates(
modifiedSince: Date,
modifiedUntil: Date,
): Promise<number[]> {
const studentIdAlias = "studentId";
const applicationsWithStudentUpdates = await this.applicationRepo
.createQueryBuilder("application")
.select("student.id", studentIdAlias)
.distinctOn([`"${studentIdAlias}"`])
.innerJoin("application.student", "student")
.innerJoin("student.user", "user")
.innerJoin("student.sinValidation", "sinValidation")
.innerJoin("student.casSupplier", "casSupplier")
.leftJoin("student.overawards", "overaward")
.where("application.applicationStatus != :overwritten")
.andWhere("application.currentAssessment is not null")
// Check if the student data was updated in the given period.
.andWhere(
new Brackets((qb) => {
qb.where(
"student.updatedAt > :modifiedSince AND student.updatedAt <= :modifiedUntil",
)
.orWhere(
"user.updatedAt > :modifiedSince AND user.updatedAt <= :modifiedUntil",
)
.orWhere(
"sinValidation.updatedAt > :modifiedSince AND sinValidation.updatedAt <= :modifiedUntil",
)
.orWhere(
"casSupplier.updatedAt > :modifiedSince AND casSupplier.updatedAt <= :modifiedUntil",
)
.orWhere(
"overaward.updatedAt > :modifiedSince AND overaward.updatedAt <= :modifiedUntil",
);
}),
)
.setParameters({
overwritten: ApplicationStatus.Overwritten,
modifiedSince,
modifiedUntil,
})
.getRawMany<{ [studentIdAlias]: number }>();
// Extract the student ids from the applications.
const modifiedStudentIds = applicationsWithStudentUpdates.map(
(applicationWithStudentUpdate) =>
applicationWithStudentUpdate[studentIdAlias],
);

return modifiedStudentIds;
}

/**
* Get student details of students who have one or more updates.
* @param studentIds student ids.
* @returns student details.
*/
async getStudentRecordsByStudentIds(
studentIds: number[],
): Promise<StudentDetail[]> {
const queryResult = await this.studentRepo
.createQueryBuilder("student")
.select([
"student.id",
"student.birthDate",
"student.disabilityStatus",
"student.disabilityStatusEffectiveDate",
"user.firstName",
"user.lastName",
"sinValidation.sin",
"casSupplier.supplierNumber",
"casSupplier.supplierAddress",
])
.addSelect("SUM(cslfOveraward.overawardValue)", "cslfOverawardTotal")
.addSelect("SUM(bcslOveraward.overawardValue)", "bcslOverawardTotal")
.innerJoin("student.user", "user")
.innerJoin("student.sinValidation", "sinValidation")
.innerJoin("student.casSupplier", "casSupplier")
.leftJoin(
"student.overawards",
"cslfOveraward",
"cslfOveraward.disbursementValueCode = :cslfAwardCode",
)
.leftJoin(
"student.overawards",
"bcslOveraward",
"bcslOveraward.disbursementValueCode = :bcslAwardCode",
)
.groupBy("student.id")
.addGroupBy("user.id")
.addGroupBy("sinValidation.id")
.addGroupBy("casSupplier.id")
.where("student.id IN (:...studentIds)")
.setParameters({
cslfAwardCode: CANADA_STUDENT_LOAN_FULL_TIME_AWARD_CODE,
bcslAwardCode: BC_STUDENT_LOAN_AWARD_CODE,
studentIds,
})
.getRawAndEntities();

return mapFromRawAndEntities<StudentDetail>(
queryResult,
"cslfOverawardTotal",
"bcslOverawardTotal",
);
}
// TODO: SIMS to SFAS - Add methods to extract application and restriction data.
}
Loading

0 comments on commit 7eedbb5

Please sign in to comment.