Skip to content

Commit

Permalink
#2180 - Queued Assessments - Assessment Workflow Enqueuer Scheduler -…
Browse files Browse the repository at this point in the history
… Start Assessment (Part 1) (#2285)

- Created the new queue configuration for the scheduler, executed every
30 seconds, and cleaned up every 24 hours.
- Removed from the code the points where the queue to start the workflow
was invoked. From now on only the scheduler will invoke the workflow
start queue.
    - Method `startAssessment` removed since it is no longer needed.
- Method `hasIncompleteAssessment` and `assertAllAssessmentsCompleted`
removed since there is no purpose anymore.
- Multiple workflows can now be saved and the scheduler will coordinate
their execution.
- `StartApplicationAssessment` queue can now be started without
providing the `workflowName`.
- Created a migration to update the assessment status based on
`assessment_workflow_id`, and `assessment_data`. The rollback is less
meaningful and it was created to try to bring the column back to its
original state where the column was set with its default value
`Submitted`.
  • Loading branch information
andrewsignori-aot authored Sep 12, 2023
1 parent 28fe367 commit 672f0a4
Show file tree
Hide file tree
Showing 25 changed files with 422 additions and 178 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
APPLICATION_NOT_FOUND,
APPLICATION_NOT_VALID,
EducationProgramOfferingService,
StudentAssessmentService,
INVALID_OPERATION_IN_THE_CURRENT_STATUS,
ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE,
CRAIncomeVerificationService,
Expand Down Expand Up @@ -89,7 +88,6 @@ export class ApplicationStudentsController extends BaseController {
private readonly programYearService: ProgramYearService,
private readonly offeringService: EducationProgramOfferingService,
private readonly confirmationOfEnrollmentService: ConfirmationOfEnrollmentService,
private readonly assessmentService: StudentAssessmentService,
private readonly applicationControllerService: ApplicationControllerService,
private readonly craIncomeVerificationService: CRAIncomeVerificationService,
private readonly supportingUserService: SupportingUserService,
Expand Down Expand Up @@ -236,16 +234,14 @@ export class ApplicationStudentsController extends BaseController {
studyStartDate,
studyEndDate,
);
const { createdAssessment } =
await this.applicationService.submitApplication(
applicationId,
studentToken.userId,
student.id,
programYear.id,
submissionResult.data.data,
payload.associatedFiles,
);
await this.assessmentService.startAssessment(createdAssessment.id);
await this.applicationService.submitApplication(
applicationId,
studentToken.userId,
student.id,
programYear.id,
submissionResult.data.data,
payload.associatedFiles,
);
} catch (error) {
switch (error.name) {
case APPLICATION_NOT_FOUND:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
import {
ASSESSMENT_ALREADY_IN_PROGRESS,
StudentAppealService,
StudentAssessmentService,
} from "../../services";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import {
Expand Down Expand Up @@ -57,7 +56,6 @@ import { StudentAppealControllerService } from "./student-appeal.controller.serv
export class StudentAppealAESTController extends BaseController {
constructor(
private readonly studentAppealService: StudentAppealService,
private readonly studentAssessmentService: StudentAssessmentService,
private readonly studentAppealControllerService: StudentAppealControllerService,
) {
super();
Expand Down Expand Up @@ -107,20 +105,11 @@ export class StudentAppealAESTController extends BaseController {
@UserToken() userToken: IUserToken,
): Promise<void> {
try {
const savedAppeal = await this.studentAppealService.approveRequests(
await this.studentAppealService.approveRequests(
appealId,
payload.requests,
userToken.userId,
);

// The appeal approval will create a student assessment to be processed only
// if at least one request was approved, hence sometimes an appeal will not result
// is an assessment creation if all requests are declined.
if (savedAppeal.studentAssessment) {
await this.studentAssessmentService.startAssessment(
savedAppeal.studentAssessment.id,
);
}
} catch (error: unknown) {
if (error instanceof CustomNamedError) {
switch (error.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
ApplicationWithdrawalImportTextService,
FormService,
INVALID_OPERATION_IN_THE_CURRENT_STATUS,
StudentAssessmentService,
StudentScholasticStandingsService,
} from "../../services";
import { ApiProcessError, ClientTypeBaseRoute } from "../../types";
Expand Down Expand Up @@ -77,7 +76,6 @@ export class ScholasticStandingInstitutionsController extends BaseController {
constructor(
private readonly formService: FormService,
private readonly studentScholasticStandingsService: StudentScholasticStandingsService,
private readonly studentAssessmentService: StudentAssessmentService,
private readonly scholasticStandingControllerService: ScholasticStandingControllerService,
private readonly applicationWithdrawalImportTextService: ApplicationWithdrawalImportTextService,
) {
Expand Down Expand Up @@ -115,20 +113,12 @@ export class ScholasticStandingInstitutionsController extends BaseController {
if (!submissionResult.valid) {
throw new BadRequestException("Invalid submission.");
}
const scholasticStanding =
await this.studentScholasticStandingsService.processScholasticStanding(
locationId,
applicationId,
userToken.userId,
submissionResult.data.data,
);

// Start assessment.
if (scholasticStanding.studentAssessment) {
await this.studentAssessmentService.startAssessment(
scholasticStanding.studentAssessment.id,
);
}
await this.studentScholasticStandingsService.processScholasticStanding(
locationId,
applicationId,
userToken.userId,
submissionResult.data.data,
);
} catch (error: unknown) {
if (error instanceof CustomNamedError) {
switch (error.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1202,14 +1202,6 @@ export class EducationProgramOfferingService extends RecordDataModelService<Educ
});
promises.push(deleteAssessmentPromise);
}
if (application.applicationStatus === ApplicationStatus.Completed) {
const startAssessmentPromise =
this.workflowClientService.startApplicationAssessment(
application.workflowName,
application.currentAssessment.id,
);
promises.push(startAssessmentPromise);
}
if (promises.length >= maxPromisesAllowed) {
// Waits for promises to be process when it reaches maximum allowable parallel
// count.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
STUDENT_APPEAL_INVALID_OPERATION,
STUDENT_APPEAL_NOT_FOUND,
} from "./constants";
import { StudentAssessmentService } from "../student-assessment/student-assessment.service";
import { NotificationActionsService } from "@sims/services/notifications";
import { NoteSharedService } from "@sims/services";

Expand All @@ -43,7 +42,6 @@ export class StudentAppealService extends RecordDataModelService<StudentAppeal>
constructor(
private readonly dataSource: DataSource,
private readonly studentAppealRequestsService: StudentAppealRequestsService,
private readonly studentAssessmentService: StudentAssessmentService,
private readonly notificationActionsService: NotificationActionsService,
private readonly noteSharedService: NoteSharedService,
) {
Expand Down Expand Up @@ -352,10 +350,6 @@ export class StudentAppealService extends RecordDataModelService<StudentAppeal>
);
}

await this.studentAssessmentService.assertAllAssessmentsCompleted(
appealToUpdate.application.id,
);

const auditUser = { id: auditUserId } as User;
const auditDate = new Date();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,19 @@ import {
StudentAssessmentStatus,
} from "@sims/sims-db";
import { Brackets, DataSource } from "typeorm";
import { InjectQueue } from "@nestjs/bull";
import { Queue } from "bull";
import { CustomNamedError, QueueNames } from "@sims/utilities";
import { CustomNamedError } from "@sims/utilities";
import {
ASSESSMENT_ALREADY_IN_PROGRESS,
ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE,
ASSESSMENT_NOT_FOUND,
} from "./student-assessment.constants";
import { AssessmentHistory } from "./student-assessment.models";
import { StartAssessmentQueueInDTO } from "@sims/services/queue";

/**
* Manages the student assessment related operations.
*/
@Injectable()
export class StudentAssessmentService extends RecordDataModelService<StudentAssessment> {
constructor(
dataSource: DataSource,
@InjectQueue(QueueNames.StartApplicationAssessment)
private readonly startAssessmentQueue: Queue<StartAssessmentQueueInDTO>,
) {
constructor(dataSource: DataSource) {
super(dataSource.getRepository(StudentAssessment));
}

Expand Down Expand Up @@ -131,58 +123,6 @@ export class StudentAssessmentService extends RecordDataModelService<StudentAsse
return query.getMany();
}

/**
* Starts the assessment workflow of one Student Application.
* @param assessmentId Student assessment that need to be processed.
*/
async startAssessment(assessmentId: number): Promise<void> {
const assessment = await this.repo
.createQueryBuilder("assessment")
.select([
"assessment.id",
"assessment.assessmentWorkflowId",
"assessment.triggerType",
"application.id",
"application.applicationStatus",
"application.data",
])
.innerJoin("assessment.application", "application")
.where("assessment.id = :assessmentId", { assessmentId })
.getOne();

if (assessment.assessmentWorkflowId) {
throw new CustomNamedError(
`Student assessment was already started and has a workflow associated with. Assessment id ${assessmentId}`,
ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE,
);
}

if (
assessment.triggerType === AssessmentTriggerType.OriginalAssessment &&
assessment.application.applicationStatus !== ApplicationStatus.Submitted
) {
throw new CustomNamedError(
`An assessment with a trigger type '${AssessmentTriggerType.OriginalAssessment}' can only be started with a Student Application in the status '${ApplicationStatus.Submitted}'. Assessment id ${assessmentId}`,
ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE,
);
}

if (
assessment.triggerType !== AssessmentTriggerType.OriginalAssessment &&
assessment.application.applicationStatus !== ApplicationStatus.Completed
) {
throw new CustomNamedError(
`An assessment with a trigger type other than '${AssessmentTriggerType.OriginalAssessment}' can only be started with a Student Application in the status '${ApplicationStatus.Completed}'. Assessment id ${assessmentId}`,
ASSESSMENT_INVALID_OPERATION_IN_THE_CURRENT_STATE,
);
}

await this.startAssessmentQueue.add({
workflowName: assessment.application.data.workflowName,
assessmentId: assessment.id,
});
}

/**
* Updates assessment and application statuses when
* the student is confirming the NOA (Notice of Assessment).
Expand Down Expand Up @@ -331,45 +271,4 @@ export class StudentAssessmentService extends RecordDataModelService<StudentAsse

return mapFromRawAndEntities<AssessmentHistory>(queryResult, "status");
}

/**
* Checks if some student assessment is still being processed.
* Only one student assessment can be processed at a given time because
* any assessment can generate Over Awards and they must be taken into
* the consideration every time.
* * Alongside with the check, the DB has an index to prevent that a new
* * assessment record is created when there is already one with the
* * assessment data not populated (submitted/pending).
* @param application application to have the assessments verified.
* @returns true if there is an assessment that is not finalized yet.
*/
async hasIncompleteAssessment(application: number): Promise<boolean> {
const queryResult = await this.repo
.createQueryBuilder("assessment")
.select("1")
.innerJoin("assessment.application", "application")
.andWhere("application.id = :application", { application })
.andWhere("assessment.assessmentData IS NULL")
.limit(1)
.getRawOne();
return !!queryResult;
}

/**
* Validate if the student has any student assessment that it is not
* finished yet (submitted/pending). If there is a student assessment
* already being processed, throws an exception.
* @param application application to have the assessments verified.
*/
async assertAllAssessmentsCompleted(application: number) {
const hasIncompleteAssessment = await this.hasIncompleteAssessment(
application,
);
if (hasIncompleteAssessment) {
throw new CustomNamedError(
"There is already an assessment waiting to be completed. Another assessment cannot be initiated at this time.",
ASSESSMENT_ALREADY_IN_PROGRESS,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
INVALID_OPERATION_IN_THE_CURRENT_STATUS,
} from "../application/application.service";
import { ScholasticStanding } from "./student-scholastic-standings.model";
import { StudentAssessmentService } from "../student-assessment/student-assessment.service";
import { StudentRestrictionService } from "../restriction/student-restriction.service";
import { APPLICATION_CHANGE_NOT_ELIGIBLE } from "../../constants";
import { SCHOLASTIC_STANDING_MINIMUM_UNSUCCESSFUL_WEEKS } from "../../utilities";
Expand All @@ -40,7 +39,6 @@ export class StudentScholasticStandingsService extends RecordDataModelService<St

constructor(
private readonly dataSource: DataSource,
private readonly studentAssessmentService: StudentAssessmentService,
private readonly studentRestrictionService: StudentRestrictionService,
private readonly notificationActionsService: NotificationActionsService,
private readonly studentRestrictionSharedService: StudentRestrictionSharedService,
Expand Down Expand Up @@ -107,10 +105,6 @@ export class StudentScholasticStandingsService extends RecordDataModelService<St
INVALID_OPERATION_IN_THE_CURRENT_STATUS,
);
}
// Check if any assessment already in progress for this application.
await this.studentAssessmentService.assertAllAssessmentsCompleted(
application.id,
);

// Save scholastic standing and create reassessment.
return this.saveScholasticStandingCreateReassessment(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { getSQLFileData } from "../utilities/sqlLoader";

export class AddAssessmentWorkflowEnqueuerQueue1694116500443
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData("Add-assessment-workflow-enqueuer-queue.sql", "Queue"),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Rollback-add-assessment-workflow-enqueuer-queue.sql",
"Queue",
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { getSQLFileData } from "../utilities/sqlLoader";

export class UpdateStudentAssessmentStatus1694202849264
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Update-student-assessment-status.sql",
"StudentAssessments",
),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
getSQLFileData(
"Rollback-update-student-assessment-status.sql",
"StudentAssessments",
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
INSERT INTO
sims.queue_configurations(queue_name, queue_configuration)
VALUES
(
'assessment-workflow-enqueuer',
'{
"dashboardReadonly": false,
"cron": "*/30 * * * * *",
"cleanUpPeriod": 3600000
}' :: json
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DELETE FROM
sims.queue_configurations
WHERE
queue_name = 'assessment-workflow-enqueuer';
Loading

0 comments on commit 672f0a4

Please sign in to comment.