From 21794811a7010ad6645b31a9df1624a7ac5f3439 Mon Sep 17 00:00:00 2001 From: Andrew Boni Signori Date: Tue, 3 Mar 2026 20:23:18 -0800 Subject: [PATCH 01/11] Navigating and loading student view --- .../backend/apps/api/src/app.aest.module.ts | 2 + .../apps/api/src/app.institutions.module.ts | 2 + .../apps/api/src/app.students.module.ts | 2 + .../assessment.controller.service.ts | 27 ++ .../assessment/models/assessment.dto.ts | 3 + .../form-submission.students.controller.ts | 73 +++++- .../models/form-submission.dto.ts | 8 +- .../form-submission-submit.service.ts | 187 ++++++++++++++ .../form-submission.service.ts | 243 +++++------------- .../backend/apps/api/src/services/index.ts | 3 +- .../student-assessment.service.ts | 2 + .../common/students/assessment/History.vue | 9 +- .../FormSubmissionApproval.vue | 4 +- .../form-submissions/FormSubmissionItems.vue | 3 +- .../students/StudentAssessmentDetails.vue | 18 +- .../packages/web/src/router/StudentRoutes.ts | 3 + .../src/services/http/dto/Assessment.dto.ts | 1 + .../contracts/FormSubmissionContracts.ts | 6 + .../applicationDetails/AssessmentsSummary.vue | 15 +- .../AssessmentsSummaryVersion.vue | 15 +- .../form-submissions/FormSubmissionView.vue | 52 ++-- 21 files changed, 469 insertions(+), 209 deletions(-) create mode 100644 sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts diff --git a/sources/packages/backend/apps/api/src/app.aest.module.ts b/sources/packages/backend/apps/api/src/app.aest.module.ts index 29af7a5877..d390dbdace 100644 --- a/sources/packages/backend/apps/api/src/app.aest.module.ts +++ b/sources/packages/backend/apps/api/src/app.aest.module.ts @@ -46,6 +46,7 @@ import { FormSubmissionActionProcessor, FormSubmissionCreateAppealAssessmentAction, FormSubmissionUpdateModifiedIndependentAction, + FormSubmissionService, } from "./services"; import { SupportingUserAESTController, @@ -237,6 +238,7 @@ import { ECertIntegrationModule } from "@sims/integrations/esdc-integration"; FormSubmissionUpdateModifiedIndependentAction, FormSubmissionActionProcessor, FormSubmissionApprovalService, + FormSubmissionService, ], }) export class AppAESTModule {} diff --git a/sources/packages/backend/apps/api/src/app.institutions.module.ts b/sources/packages/backend/apps/api/src/app.institutions.module.ts index d29a5d8b99..4d828dec5f 100644 --- a/sources/packages/backend/apps/api/src/app.institutions.module.ts +++ b/sources/packages/backend/apps/api/src/app.institutions.module.ts @@ -35,6 +35,7 @@ import { SupportingUserService, ApplicationRestrictionBypassService, InstitutionRestrictionService, + FormSubmissionService, } from "./services"; import { ApplicationControllerService, @@ -196,6 +197,7 @@ import { ECertIntegrationModule } from "@sims/integrations/esdc-integration"; SupportingUserService, DisbursementScheduleSharedService, InstitutionRestrictionService, + FormSubmissionService, ], }) export class AppInstitutionsModule {} diff --git a/sources/packages/backend/apps/api/src/app.students.module.ts b/sources/packages/backend/apps/api/src/app.students.module.ts index 457389035c..c6e0be8cb2 100644 --- a/sources/packages/backend/apps/api/src/app.students.module.ts +++ b/sources/packages/backend/apps/api/src/app.students.module.ts @@ -29,6 +29,7 @@ import { AnnouncementService, ApplicationRestrictionBypassService, InstitutionRestrictionService, + FormSubmissionSubmitService, FormSubmissionService, } from "./services"; import { @@ -183,6 +184,7 @@ import { SupplementaryDataParents, SupplementaryDataLoader, // Form Submission Service. + FormSubmissionSubmitService, FormSubmissionService, ], }) diff --git a/sources/packages/backend/apps/api/src/route-controllers/assessment/assessment.controller.service.ts b/sources/packages/backend/apps/api/src/route-controllers/assessment/assessment.controller.service.ts index 612afa583a..defe8cdaa1 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/assessment/assessment.controller.service.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/assessment/assessment.controller.service.ts @@ -21,6 +21,7 @@ import { MASKED_MSFAA_NUMBER, ApplicationOfferingChangeRequestService, MASKED_MONEY_AMOUNT, + FormSubmissionService, } from "../../services"; import { AssessmentNOAAPIOutDTO, @@ -55,6 +56,7 @@ export class AssessmentControllerService { constructor( private readonly assessmentService: StudentAssessmentService, private readonly studentAppealService: StudentAppealService, + private readonly formSubmissionService: FormSubmissionService, private readonly studentScholasticStandingsService: StudentScholasticStandingsService, private readonly educationProgramOfferingService: EducationProgramOfferingService, private readonly applicationExceptionService: ApplicationExceptionService, @@ -488,6 +490,25 @@ export class AssessmentControllerService { return studentAppealArray; } + async getPendingAndDeniedFormSubmissions( + applicationId: number, + studentId?: number, + ): Promise { + const formSubmissions = + await this.formSubmissionService.getNonCompletedFormSubmissions( + applicationId, + studentId, + ); + const submissions: RequestAssessmentSummaryAPIOutDTO[] = + formSubmissions.map((submission) => ({ + id: submission.id, + submittedDate: submission.submittedDate, + status: submission.submissionStatus, + requestType: RequestAssessmentTypeAPIOutDTO.StudentAppeal, + })); + return submissions; + } + /** * Get history of approved assessment requests and * unsuccessful scholastic standings change requests(which will not create new assessment) @@ -517,6 +538,7 @@ export class AssessmentControllerService { offeringId: assessment.offering.id, programId: assessment.offering.educationProgram.id, studentAppealId: assessment.studentAppeal?.id, + formSubmissionId: assessment.formSubmission?.id, applicationOfferingChangeRequestId: assessment.applicationOfferingChangeRequest?.id, applicationExceptionId: assessment.application.applicationException?.id, @@ -600,6 +622,10 @@ export class AssessmentControllerService { applicationId, options?.studentId, ); + const formSubmissionAppeals = await this.getPendingAndDeniedFormSubmissions( + applicationId, + options?.studentId, + ); const applicationOfferingChangeRequests = await this.getApplicationOfferingChangeRequestsByStatus( applicationId, @@ -613,6 +639,7 @@ export class AssessmentControllerService { ); return requestAssessmentSummary .concat(appeals) + .concat(formSubmissionAppeals) .concat(applicationOfferingChangeRequests) .sort(this.sortAssessmentHistory); } diff --git a/sources/packages/backend/apps/api/src/route-controllers/assessment/models/assessment.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/assessment/models/assessment.dto.ts index fcaafd4b1b..c625a34f24 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/assessment/models/assessment.dto.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/assessment/models/assessment.dto.ts @@ -7,6 +7,7 @@ import { AssessmentTriggerType, COEStatus, DisbursementScheduleStatus, + FormSubmissionStatus, NOTE_DESCRIPTION_MAX_LENGTH, OfferingIntensity, OfferingStatus, @@ -49,6 +50,7 @@ export enum RequestAssessmentTypeAPIOutDTO { type RequestAssessmentSummaryStatus = | StudentAppealStatus + | FormSubmissionStatus | ApplicationExceptionStatus | OfferingStatus | ApplicationOfferingChangeRequestStatus; @@ -70,6 +72,7 @@ export class AssessmentHistorySummaryAPIOutDTO { offeringId?: number; programId?: number; studentAppealId?: number; + formSubmissionId?: number; applicationOfferingChangeRequestId?: number; applicationExceptionId?: number; studentScholasticStandingId?: number; diff --git a/sources/packages/backend/apps/api/src/route-controllers/form-submission/form-submission.students.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/form-submission/form-submission.students.controller.ts index 2d40cd19d0..02db094141 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/form-submission/form-submission.students.controller.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/form-submission/form-submission.students.controller.ts @@ -3,6 +3,9 @@ import { Body, Controller, Get, + NotFoundException, + Param, + ParseIntPipe, Post, Query, UnprocessableEntityException, @@ -12,6 +15,7 @@ import { FORM_SUBMISSION_INVALID_DYNAMIC_DATA, FORM_SUBMISSION_PENDING_DECISION, FormSubmissionService, + FormSubmissionSubmitService, } from "../../services"; import { AuthorizedParties, StudentUserToken } from "../../auth"; import { @@ -21,15 +25,21 @@ import { } from "../../auth/decorators"; import { ApiBadRequestResponse, + ApiNotFoundResponse, ApiTags, ApiUnprocessableEntityResponse, } from "@nestjs/swagger"; import BaseController from "../BaseController"; import { ApiProcessError, ClientTypeBaseRoute } from "../../types"; -import { FormCategory } from "@sims/sims-db"; +import { + FormCategory, + FormSubmissionDecisionStatus, + FormSubmissionStatus, +} from "@sims/sims-db"; import { FormSubmissionAPIInDTO, FormSubmissionConfigurationsAPIOutDTO, + FormSubmissionStudentAPIOutDTO, FormSupplementaryDataAPIInDTO, FormSupplementaryDataAPIOutDTO, } from "./models/form-submission.dto"; @@ -44,8 +54,9 @@ import { SupplementaryDataLoader } from "../../services/form-submission/form-sup export class FormSubmissionStudentsController extends BaseController { constructor( private readonly dynamicFormConfigurationService: DynamicFormConfigurationService, - private readonly formSubmissionService: FormSubmissionService, + private readonly formSubmissionSubmitService: FormSubmissionSubmitService, private readonly supplementaryDataLoader: SupplementaryDataLoader, + private readonly formSubmissionService: FormSubmissionService, ) { super(); } @@ -94,6 +105,51 @@ export class FormSubmissionStudentsController extends BaseController { }; } + /** + * Get the details of a form submission, including the individual form items and their details. + * @param formSubmissionId ID of the form submission to retrieve the details for. + * @param itemId optional ID of the form submission item to filter the details for. + * Useful only when a single item detail is required. + * @returns form submission details including individual form items and their details. + */ + @ApiNotFoundResponse({ description: "Form submission not found" }) + @Get(":formSubmissionId") + async getFormSubmission( + @UserToken() userToken: StudentUserToken, + @Param("formSubmissionId", ParseIntPipe) formSubmissionId: number, + ): Promise { + const submission = await this.formSubmissionService.getFormSubmissionsById( + formSubmissionId, + { studentId: userToken.studentId }, + ); + if (!submission) { + throw new NotFoundException( + `Form submission with ID ${formSubmissionId} not found.`, + ); + } + return { + id: submission.id, + formCategory: submission.formCategory, + status: submission.submissionStatus, + applicationId: submission.application?.id, + applicationNumber: submission.application?.applicationNumber, + submittedDate: submission.submittedDate, + submissionItems: submission.formSubmissionItems.map((item) => ({ + id: item.id, + formType: item.dynamicFormConfiguration.formType, + formCategory: item.dynamicFormConfiguration.formCategory, + dynamicFormConfigurationId: item.dynamicFormConfiguration.id, + submissionData: item.submittedData, + formDefinitionName: item.dynamicFormConfiguration.formDefinitionName, + decisionStatus: + submission.submissionStatus === FormSubmissionStatus.Pending + ? FormSubmissionDecisionStatus.Pending + : (item.currentDecision?.decisionStatus ?? + FormSubmissionDecisionStatus.Pending), + })), + }; + } + /** * Executes a dynamic form submission for the Ministry decision. * Each form will have an individual decision associated with and upon its @@ -121,12 +177,13 @@ export class FormSubmissionStudentsController extends BaseController { @UserToken() userToken: StudentUserToken, ): Promise { try { - const submission = await this.formSubmissionService.saveFormSubmission( - userToken.studentId, - payload.applicationId, - payload.items, - userToken.userId, - ); + const submission = + await this.formSubmissionSubmitService.saveFormSubmission( + userToken.studentId, + payload.applicationId, + payload.items, + userToken.userId, + ); return { id: submission.id, }; diff --git a/sources/packages/backend/apps/api/src/route-controllers/form-submission/models/form-submission.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/form-submission/models/form-submission.dto.ts index 3efb063508..bdce7805f0 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/form-submission/models/form-submission.dto.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/form-submission/models/form-submission.dto.ts @@ -72,7 +72,7 @@ abstract class FormSubmissionAPIOutDTO { * This is a basic representation of a form submission item properties to be extended * for Ministry, Student, and Institutions. */ -abstract class FormSubmissionItemAPIOutDTO { +export class FormSubmissionItemAPIOutDTO { id: number; formType: string; formCategory: FormCategory; @@ -89,7 +89,7 @@ abstract class FormSubmissionItemAPIOutDTO { /** * Current decision associated with a form submission item. */ -class FormSubmissionItemDecisionAPIOutDTO { +export class FormSubmissionItemDecisionAPIOutDTO { id: number; decisionStatus: FormSubmissionDecisionStatus; decisionDate?: Date; @@ -130,6 +130,10 @@ export class FormSubmissionMinistryAPIOutDTO extends FormSubmissionAPIOutDTO { submissionItems: FormSubmissionItemMinistryAPIOutDTO[]; } +export class FormSubmissionStudentAPIOutDTO extends FormSubmissionAPIOutDTO { + submissionItems: FormSubmissionItemAPIOutDTO[]; +} + /** * Forms supplementary data necessary for a dynamic form * submissions (e.g., program year, parents). diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts new file mode 100644 index 0000000000..04c1a5331b --- /dev/null +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission-submit.service.ts @@ -0,0 +1,187 @@ +import { Injectable } from "@nestjs/common"; +import { DataSource } from "typeorm"; +import { + Application, + User, + FileOriginType, + Student, + FormSubmission, + FormSubmissionStatus, + FormSubmissionItem, + DynamicFormConfiguration, + FormCategory, +} from "@sims/sims-db"; +import { StudentFileService } from "../student-file/student-file.service"; +import { + FormSubmissionConfig, + FormSubmissionModel, +} from "./form-submission.models"; +import { + DynamicFormConfigurationService, + FORM_SUBMISSION_INVALID_DYNAMIC_DATA, + FORM_SUBMISSION_UNKNOWN_FORM_CONFIGURATION, + FormService, +} from "../../services"; +import { CustomNamedError, processInParallel } from "@sims/utilities"; +import { DryRunSubmissionResult } from "../../types"; +import { FormSubmissionValidator } from "./form-submission-validator"; +import { SupplementaryDataLoader } from "./form-supplementary-data"; + +/** + * Manages how the form submissions are submitted, including the validations, + * ensuring the necessary supplementary data is loaded, and how the form + * submission and related items are saved in the database. + */ +@Injectable() +export class FormSubmissionSubmitService { + constructor( + private readonly dataSource: DataSource, + private readonly studentFileService: StudentFileService, + private readonly dynamicFormConfigurationService: DynamicFormConfigurationService, + private readonly formService: FormService, + private readonly formSubmissionValidator: FormSubmissionValidator, + private readonly supplementaryDataLoader: SupplementaryDataLoader, + ) {} + + /** + * Saves a form submission, including the related form submission items, executing validations + * and ensuring the necessary supplementary data is loaded for the submission. + * @param studentId ID of the student associated with the form submission. + * @param applicationId ID of the application associated with the form submission, if applicable. + * @param submissionItems form submission items to be saved as part of the form submission. + * @param auditUserId ID of the user performing the operation, used for auditing purposes. + * @returns the saved form submission with the related form submission items. + */ + async saveFormSubmission( + studentId: number, + applicationId: number | undefined, + submissionItems: FormSubmissionModel[], + auditUserId: number, + ): Promise { + const submissionConfigs = this.convertToFormSubmissionConfigs( + submissionItems, + applicationId, + ); + // Validate the form submission based on different validators. + await this.formSubmissionValidator.validate(submissionConfigs, studentId); + // Ensure all form submissions have the necessary supplementary data loaded to be processed + // in the dry run submission, such as parents and program year for application-scoped forms. + await this.supplementaryDataLoader.loadSupplementaryData( + submissionConfigs, + studentId, + ); + // Process all the dry run submissions to validate the requests. + const validatedItems = await processInParallel( + (submissionConfig) => this.getValidatedFormSubmission(submissionConfig), + submissionConfigs, + ); + const [referenceSubmissionConfig] = submissionConfigs; + // Save the form submission and the related items. + return this.dataSource.transaction(async (entityManager) => { + const now = new Date(); + const creator = { id: auditUserId } as User; + const formSubmission = new FormSubmission(); + formSubmission.student = { id: studentId } as Student; + formSubmission.application = { id: applicationId } as Application; + formSubmission.submittedDate = now; + formSubmission.submissionStatus = FormSubmissionStatus.Pending; + formSubmission.formCategory = referenceSubmissionConfig.formCategory; + formSubmission.creator = creator; + formSubmission.createdAt = now; + formSubmission.formSubmissionItems = validatedItems.map( + (submissionItem) => + ({ + dynamicFormConfiguration: { + id: submissionItem.dynamicConfigurationId, + } as DynamicFormConfiguration, + submittedData: submissionItem.formData, + creator: creator, + createdAt: now, + }) as FormSubmissionItem, + ); + const uniqueFileNames: string[] = validatedItems.flatMap( + (submissionItem) => submissionItem.files, + ); + if (uniqueFileNames.length) { + const fileOrigin = + referenceSubmissionConfig.formCategory === FormCategory.StudentAppeal + ? FileOriginType.Appeal + : FileOriginType.Student; + await this.studentFileService.updateStudentFiles( + studentId, + auditUserId, + uniqueFileNames, + fileOrigin, + + { entityManager: entityManager }, + ); + } + // TODO: send notification. + return entityManager.getRepository(FormSubmission).save(formSubmission); + }); + } + + /** + * Converts the form submission models to form submission configurations, + * making the association of the form submission items with the related form + * configurations and preparing the data for validation and saving. + * @param submissionItems form submission models to be converted. + * @param applicationId application ID to be associated with the form submissions. + * Only required for forms that have application scope. + * @returns an array of form submission configurations. + */ + private convertToFormSubmissionConfigs( + submissionItems: FormSubmissionModel[], + applicationId: number | undefined = undefined, + ): FormSubmissionConfig[] { + return submissionItems.map((submissionItem) => { + const config = this.dynamicFormConfigurationService.getFormById( + submissionItem.dynamicConfigurationId, + ); + if (!config) { + throw new CustomNamedError( + "One or more forms configurations in the submission are not recognized.", + FORM_SUBMISSION_UNKNOWN_FORM_CONFIGURATION, + ); + } + return { + applicationId, + ...submissionItem, + ...config, + }; + }); + } + + /** + * Executes a dry run submission for the given form + * submission configuration to validate the request. + * @param submissionItem form submission configuration to be validated. + * @returns validated form submission model. + */ + private async getValidatedFormSubmission( + submissionItem: FormSubmissionConfig, + ): Promise { + let submissionResult: DryRunSubmissionResult; + try { + submissionResult = await this.formService.dryRunSubmission( + submissionItem.formDefinitionName, + submissionItem.formData, + ); + } catch (error: unknown) { + throw new Error("Dry run submission failed due to unknown reason.", { + cause: error, + }); + } + if (!submissionResult.valid) { + throw new CustomNamedError( + "Not able to complete the submission due to an invalid request.", + FORM_SUBMISSION_INVALID_DYNAMIC_DATA, + ); + } + return { + dynamicConfigurationId: submissionItem.dynamicConfigurationId, + formData: submissionResult.data.data, + files: submissionItem.files, + }; + } +} diff --git a/sources/packages/backend/apps/api/src/services/form-submission/form-submission.service.ts b/sources/packages/backend/apps/api/src/services/form-submission/form-submission.service.ts index 816e0d1334..69da34401d 100644 --- a/sources/packages/backend/apps/api/src/services/form-submission/form-submission.service.ts +++ b/sources/packages/backend/apps/api/src/services/form-submission/form-submission.service.ts @@ -1,187 +1,86 @@ import { Injectable } from "@nestjs/common"; -import { DataSource } from "typeorm"; -import { - Application, - User, - FileOriginType, - Student, - FormSubmission, - FormSubmissionStatus, - FormSubmissionItem, - DynamicFormConfiguration, - FormCategory, -} from "@sims/sims-db"; -import { StudentFileService } from "../student-file/student-file.service"; -import { - FormSubmissionConfig, - FormSubmissionModel, -} from "./form-submission.models"; -import { - DynamicFormConfigurationService, - FORM_SUBMISSION_INVALID_DYNAMIC_DATA, - FORM_SUBMISSION_UNKNOWN_FORM_CONFIGURATION, - FormService, -} from "../../services"; -import { CustomNamedError, processInParallel } from "@sims/utilities"; -import { DryRunSubmissionResult } from "../../types"; -import { FormSubmissionValidator } from "./form-submission-validator"; -import { SupplementaryDataLoader } from "./form-supplementary-data"; +import { In, Repository } from "typeorm"; +import { FormSubmission, FormSubmissionStatus } from "@sims/sims-db"; +import { InjectRepository } from "@nestjs/typeorm"; -/** - * Manages how the form submissions are processed, including the validations, - * ensuring the necessary supplementary data is loaded, and how the form - * submission and related items are saved in the database. - */ @Injectable() export class FormSubmissionService { constructor( - private readonly dataSource: DataSource, - private readonly studentFileService: StudentFileService, - private readonly dynamicFormConfigurationService: DynamicFormConfigurationService, - private readonly formService: FormService, - private readonly formSubmissionValidator: FormSubmissionValidator, - private readonly supplementaryDataLoader: SupplementaryDataLoader, + @InjectRepository(FormSubmission) + private readonly formSubmissionRepo: Repository, ) {} - /** - * Saves a form submission, including the related form submission items, executing validations - * and ensuring the necessary supplementary data is loaded for the submission. - * @param studentId ID of the student associated with the form submission. - * @param applicationId ID of the application associated with the form submission, if applicable. - * @param submissionItems form submission items to be saved as part of the form submission. - * @param auditUserId ID of the user performing the operation, used for auditing purposes. - * @returns the saved form submission with the related form submission items. - */ - async saveFormSubmission( - studentId: number, - applicationId: number | undefined, - submissionItems: FormSubmissionModel[], - auditUserId: number, - ): Promise { - const submissionConfigs = this.convertToFormSubmissionConfigs( - submissionItems, - applicationId, - ); - // Validate the form submission based on different validators. - await this.formSubmissionValidator.validate(submissionConfigs, studentId); - // Ensure all form submissions have the necessary supplementary data loaded to be processed - // in the dry run submission, such as parents and program year for application-scoped forms. - await this.supplementaryDataLoader.loadSupplementaryData( - submissionConfigs, - studentId, - ); - // Process all the dry run submissions to validate the requests. - const validatedItems = await processInParallel( - (submissionConfig) => this.getValidatedFormSubmission(submissionConfig), - submissionConfigs, - ); - const [referenceSubmissionConfig] = submissionConfigs; - // Save the form submission and the related items. - return this.dataSource.transaction(async (entityManager) => { - const now = new Date(); - const creator = { id: auditUserId } as User; - const formSubmission = new FormSubmission(); - formSubmission.student = { id: studentId } as Student; - formSubmission.application = { id: applicationId } as Application; - formSubmission.submittedDate = now; - formSubmission.submissionStatus = FormSubmissionStatus.Pending; - formSubmission.formCategory = referenceSubmissionConfig.formCategory; - formSubmission.creator = creator; - formSubmission.createdAt = now; - formSubmission.formSubmissionItems = validatedItems.map( - (submissionItem) => - ({ - dynamicFormConfiguration: { - id: submissionItem.dynamicConfigurationId, - } as DynamicFormConfiguration, - submittedData: submissionItem.formData, - creator: creator, - createdAt: now, - }) as FormSubmissionItem, - ); - const uniqueFileNames: string[] = validatedItems.flatMap( - (submissionItem) => submissionItem.files, - ); - if (uniqueFileNames.length) { - const fileOrigin = - referenceSubmissionConfig.formCategory === FormCategory.StudentAppeal - ? FileOriginType.Appeal - : FileOriginType.Student; - await this.studentFileService.updateStudentFiles( - studentId, - auditUserId, - uniqueFileNames, - fileOrigin, - - { entityManager: entityManager }, - ); - } - // TODO: send notification. - return entityManager.getRepository(FormSubmission).save(formSubmission); + async getNonCompletedFormSubmissions( + applicationId: number, + studentId?: number, + ): Promise { + return this.formSubmissionRepo.find({ + select: { + id: true, + submissionStatus: true, + submittedDate: true, + }, + where: { + application: { id: applicationId }, + student: studentId ? { id: studentId } : undefined, + submissionStatus: In([ + FormSubmissionStatus.Pending, + FormSubmissionStatus.Declined, + ]), + }, }); } /** - * Converts the form submission models to form submission configurations, - * making the association of the form submission items with the related form - * configurations and preparing the data for validation and saving. - * @param submissionItems form submission models to be converted. - * @param applicationId application ID to be associated with the form submissions. - * Only required for forms that have application scope. - * @returns an array of form submission configurations. + * Gets a form submission by its ID. + * @param formSubmissionId The ID of the form submission to retrieve. + * @param options optional filters. + * - `itemId`: if provided, returns only the form submission item with the specified ID in the + * form submission items array. + * @returns The form submission if found, otherwise null. */ - private convertToFormSubmissionConfigs( - submissionItems: FormSubmissionModel[], - applicationId: number | undefined = undefined, - ): FormSubmissionConfig[] { - return submissionItems.map((submissionItem) => { - const config = this.dynamicFormConfigurationService.getFormById( - submissionItem.dynamicConfigurationId, - ); - if (!config) { - throw new CustomNamedError( - "One or more forms configurations in the submission are not recognized.", - FORM_SUBMISSION_UNKNOWN_FORM_CONFIGURATION, - ); - } - return { - applicationId, - ...submissionItem, - ...config, - }; + async getFormSubmissionsById( + formSubmissionId: number, + options?: { studentId?: number }, + ): Promise { + return this.formSubmissionRepo.findOne({ + select: { + id: true, + submissionStatus: true, + submittedDate: true, + formCategory: true, + application: { + id: true, + applicationNumber: true, + }, + formSubmissionItems: { + id: true, + dynamicFormConfiguration: { + id: true, + formType: true, + formCategory: true, + formDefinitionName: true, + }, + submittedData: true, + updatedAt: true, + currentDecision: { + id: true, + decisionStatus: true, + }, + }, + }, + relations: { + application: true, + formSubmissionItems: { + dynamicFormConfiguration: true, + currentDecision: true, + }, + }, + where: { + id: formSubmissionId, + student: { id: options.studentId }, + }, + order: { formSubmissionItems: { id: "ASC" } }, }); } - - /** - * Executes a dry run submission for the given form - * submission configuration to validate the request. - * @param submissionItem form submission configuration to be validated. - * @returns validated form submission model. - */ - private async getValidatedFormSubmission( - submissionItem: FormSubmissionConfig, - ): Promise { - let submissionResult: DryRunSubmissionResult; - try { - submissionResult = await this.formService.dryRunSubmission( - submissionItem.formDefinitionName, - submissionItem.formData, - ); - } catch (error: unknown) { - throw new Error("Dry run submission failed due to unknown reason.", { - cause: error, - }); - } - if (!submissionResult.valid) { - throw new CustomNamedError( - "Not able to complete the submission due to an invalid request.", - FORM_SUBMISSION_INVALID_DYNAMIC_DATA, - ); - } - return { - dynamicConfigurationId: submissionItem.dynamicConfigurationId, - formData: submissionResult.data.data, - files: submissionItem.files, - }; - } } diff --git a/sources/packages/backend/apps/api/src/services/index.ts b/sources/packages/backend/apps/api/src/services/index.ts index a7b5c6e2fb..54eba7d21a 100644 --- a/sources/packages/backend/apps/api/src/services/index.ts +++ b/sources/packages/backend/apps/api/src/services/index.ts @@ -62,8 +62,9 @@ export * from "./student-appeal/student-appeal.model"; export * from "./student-appeal/student-appeal-assessment"; export * from "./form-submission/constants"; export * from "./form-submission/form-submission.models"; -export * from "./form-submission/form-submission.service"; +export * from "./form-submission/form-submission-submit.service"; export * from "./form-submission/form-submission-approval.service"; +export * from "./form-submission/form-submission.service"; export * from "./form-submission/form-submission-actions/form-submission-action-processor"; export * from "./form-submission/form-submission-actions/form-submission-create-appeal-assessment-action"; export * from "./form-submission/form-submission-actions/form-submission-update-modified-independent-action"; diff --git a/sources/packages/backend/apps/api/src/services/student-assessment/student-assessment.service.ts b/sources/packages/backend/apps/api/src/services/student-assessment/student-assessment.service.ts index 64e196884d..e350190375 100644 --- a/sources/packages/backend/apps/api/src/services/student-assessment/student-assessment.service.ts +++ b/sources/packages/backend/apps/api/src/services/student-assessment/student-assessment.service.ts @@ -250,6 +250,7 @@ export class StudentAssessmentService extends RecordDataModelService { switch (data.triggerType) { case AssessmentTriggerType.StudentAppeal: - context.emit("viewStudentAppeal", data.studentAppealId); + if (isFormSubmissionEnabled.value) { + context.emit("viewStudentAppeal", data.formSubmissionId); + } else { + // TODO: removed once the toggle is no longer required. + context.emit("viewStudentAppeal", data.studentAppealId); + } break; case AssessmentTriggerType.ApplicationOfferingChange: context.emit( diff --git a/sources/packages/web/src/components/form-submissions/FormSubmissionApproval.vue b/sources/packages/web/src/components/form-submissions/FormSubmissionApproval.vue index a83c0238e4..6103a6a1e9 100644 --- a/sources/packages/web/src/components/form-submissions/FormSubmissionApproval.vue +++ b/sources/packages/web/src/components/form-submissions/FormSubmissionApproval.vue @@ -284,7 +284,8 @@ export default defineComponent({ submissionItem, formSubmission.value.status, ), - files: undefined, + decisionStatus: submissionItem.decisionStatus, + files: [], }), ); } catch { @@ -355,6 +356,7 @@ export default defineComponent({ throw new Error("Expected item to be updated was not found."); } const [reloadedSubmissionItem] = submission.submissionItems; + itemToUpdate.decisionStatus = reloadedSubmissionItem.decisionStatus; assignItemDecisionProperties( reloadedSubmissionItem, submission.status, diff --git a/sources/packages/web/src/components/form-submissions/FormSubmissionItems.vue b/sources/packages/web/src/components/form-submissions/FormSubmissionItems.vue index fbefd23718..1df21ec1ed 100644 --- a/sources/packages/web/src/components/form-submissions/FormSubmissionItems.vue +++ b/sources/packages/web/src/components/form-submissions/FormSubmissionItems.vue @@ -28,9 +28,8 @@ diff --git a/sources/packages/web/src/components/students/StudentAssessmentDetails.vue b/sources/packages/web/src/components/students/StudentAssessmentDetails.vue index 7b5292d332..053c9eec27 100644 --- a/sources/packages/web/src/components/students/StudentAssessmentDetails.vue +++ b/sources/packages/web/src/components/students/StudentAssessmentDetails.vue @@ -27,6 +27,7 @@ import { AssessmentTriggerType } from "@/types"; import { StudentRoutesConst } from "@/constants/routes/RouteConstants"; import HistoryAssessment from "@/components/common/students/assessment/History.vue"; import RequestAssessment from "@/components/common/students/assessment/Request.vue"; +import { useFeatureToggles } from "@/composables"; export default defineComponent({ components: { @@ -41,6 +42,7 @@ export default defineComponent({ }, setup(props) { const router = useRouter(); + const { isFormSubmissionEnabled } = useFeatureToggles(); // The assessment trigger types for which the request form is visible to student. const studentAssessmentRequestTypes = [ AssessmentTriggerType.StudentAppeal, @@ -58,12 +60,24 @@ export default defineComponent({ }); }; - const goToStudentAppeal = (appealId: number) => { + const goToStudentAppeal = (id: number) => { + if (isFormSubmissionEnabled.value) { + router.push({ + name: StudentRoutesConst.STUDENT_FORMS_SUBMISSION_VIEW, + params: { + formSubmissionId: id, + }, + query: { + applicationId: props.applicationId, + }, + }); + return; + } router.push({ name: StudentRoutesConst.STUDENT_APPLICATION_APPEAL_REQUEST, params: { applicationId: props.applicationId, - appealId, + appealId: id, }, }); }; diff --git a/sources/packages/web/src/router/StudentRoutes.ts b/sources/packages/web/src/router/StudentRoutes.ts index 017008e888..aa37c9cf27 100644 --- a/sources/packages/web/src/router/StudentRoutes.ts +++ b/sources/packages/web/src/router/StudentRoutes.ts @@ -323,6 +323,9 @@ export const studentRoutes: Array = [ formSubmissionId: Number.parseInt( route.params.formSubmissionId as string, ), + applicationId: route.query.applicationId + ? Number.parseInt(route.query.applicationId as string) + : undefined, }), meta: { clientType: ClientIdType.Student, diff --git a/sources/packages/web/src/services/http/dto/Assessment.dto.ts b/sources/packages/web/src/services/http/dto/Assessment.dto.ts index 66e253a818..6ed3712018 100644 --- a/sources/packages/web/src/services/http/dto/Assessment.dto.ts +++ b/sources/packages/web/src/services/http/dto/Assessment.dto.ts @@ -42,6 +42,7 @@ export interface AssessmentHistorySummaryAPIOutDTO { offeringId?: number; programId?: number; studentAppealId?: number; + formSubmissionId?: number; applicationOfferingChangeRequestId?: number; applicationExceptionId?: number; studentScholasticStandingId?: number; diff --git a/sources/packages/web/src/types/contracts/FormSubmissionContracts.ts b/sources/packages/web/src/types/contracts/FormSubmissionContracts.ts index 0b8171f168..b5ae9129f7 100644 --- a/sources/packages/web/src/types/contracts/FormSubmissionContracts.ts +++ b/sources/packages/web/src/types/contracts/FormSubmissionContracts.ts @@ -128,6 +128,12 @@ export interface FormSubmissionItem extends FormSubmissionItemSubmitted { * Form definition name. */ formName: string; + /** + * Current decision status for this form submission item. + * This is the mos recent status returned from the system and may differ of the current + * Ministry decision if currently being assessed or updated. + */ + decisionStatus: FormSubmissionDecisionStatus; /** * Current decision status for this form submission item, available for certain * user roles for Ministry, allowing a decision to be made and later auditing. diff --git a/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummary.vue b/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummary.vue index b23969640e..162f130051 100644 --- a/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummary.vue +++ b/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummary.vue @@ -52,6 +52,7 @@ import RequestAssessment from "@/components/common/students/assessment/Request.v import HistoryAssessment from "@/components/common/students/assessment/History.vue"; import ManualReassessment from "@/components/aest/students/assessment/ManualReassessment.vue"; import ApplicationHeaderTitle from "@/components/aest/students/ApplicationHeaderTitle.vue"; +import { useFeatureToggles } from "@/composables"; export default defineComponent({ components: { @@ -72,6 +73,7 @@ export default defineComponent({ }, setup(props) { const router = useRouter(); + const { isFormSubmissionEnabled } = useFeatureToggles(); const historyKey = ref(0); // The assessment trigger types for which the request form must be visible by default. @@ -83,13 +85,22 @@ export default defineComponent({ AssessmentTriggerType.ApplicationOfferingChange, ]; - const goToStudentAppeal = (appealId: number) => { + const goToStudentAppeal = (id: number) => { + if (isFormSubmissionEnabled.value) { + router.push({ + name: AESTRoutesConst.STUDENT_FORM_SUBMISSION_APPROVAL, + params: { + formSubmissionId: id, + }, + }); + return; + } router.push({ name: AESTRoutesConst.STUDENT_APPLICATION_APPEAL_REQUESTS_APPROVAL, params: { studentId: props.studentId, applicationId: props.applicationId, - appealId, + appealId: id, }, }); }; diff --git a/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummaryVersion.vue b/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummaryVersion.vue index 8e37774c39..a79fd69f3c 100644 --- a/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummaryVersion.vue +++ b/sources/packages/web/src/views/aest/student/applicationDetails/AssessmentsSummaryVersion.vue @@ -45,6 +45,7 @@ import { AssessmentTriggerType } from "@/types"; import RequestAssessment from "@/components/common/students/assessment/Request.vue"; import HistoryAssessment from "@/components/common/students/assessment/History.vue"; import ApplicationHeaderTitle from "@/components/aest/students/ApplicationHeaderTitle.vue"; +import { useFeatureToggles } from "@/composables"; export default defineComponent({ components: { @@ -68,6 +69,7 @@ export default defineComponent({ }, setup(props) { const router = useRouter(); + const { isFormSubmissionEnabled } = useFeatureToggles(); // The assessment trigger types for which the request form must be visible by default. const assessmentRequestTypes = [ @@ -84,12 +86,21 @@ export default defineComponent({ versionApplicationId: props.versionApplicationId, }); - const goToStudentAppeal = (appealId: number) => { + const goToStudentAppeal = (id: number) => { + if (isFormSubmissionEnabled.value) { + router.push({ + name: AESTRoutesConst.STUDENT_FORM_SUBMISSION_APPROVAL, + params: { + formSubmissionId: id, + }, + }); + return; + } router.push({ name: AESTRoutesConst.STUDENT_APPLICATION_APPEAL_REQUESTS_APPROVAL_VERSION, params: { ...getDefaultVersionParameters(), - appealId, + appealId: id, }, }); }; diff --git a/sources/packages/web/src/views/student/form-submissions/FormSubmissionView.vue b/sources/packages/web/src/views/student/form-submissions/FormSubmissionView.vue index a00f8df10d..ab30a0afc4 100644 --- a/sources/packages/web/src/views/student/form-submissions/FormSubmissionView.vue +++ b/sources/packages/web/src/views/student/form-submissions/FormSubmissionView.vue @@ -13,6 +13,12 @@