Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2116 - Request an Offering Change - Submit/View UI - Validations #2149

Merged
merged 12 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,12 @@ export const INSTITUTION_LOCATION_NOT_VALID = "INSTITUTION_LOCATION_NOT_VALID";
* Request for disability not allowed.
*/
export const DISABILITY_REQUEST_NOT_ALLOWED = "DISABILITY_REQUEST_NOT_ALLOWED";
/**
* Offering program year mismatch.
*/
export const OFFERING_PROGRAM_YEAR_MISMATCH = "OFFERING_PROGRAM_YEAR_MISMATCH";
/**
* Offering does not belong to the location.
*/
export const OFFERING_DOES_NOT_BELONG_TO_LOCATION =
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion: on the same context OFFERING_LOCATION_MISMATCH ?

Copy link
Contributor Author

@ann-aot ann-aot Aug 3, 2023

Choose a reason for hiding this comment

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

IMO @dheepak-aot MISMATCH is when there is a direct comparison between two values. So, keeping as it is

"OFFERING_DOES_NOT_BELONG_TO_LOCATION";
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@ import {
Body,
Controller,
Get,
InternalServerErrorException,
NotFoundException,
Param,
ParseIntPipe,
Post,
Query,
UnauthorizedException,
UnprocessableEntityException,
} from "@nestjs/common";
import { ApiNotFoundResponse, ApiTags } from "@nestjs/swagger";
import {
ApiNotFoundResponse,
ApiTags,
ApiUnauthorizedResponse,
ApiUnprocessableEntityResponse,
} from "@nestjs/swagger";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import {
AllowAuthorizedParty,
HasLocationAccess,
UserToken,
} from "../../auth/decorators";
import { ClientTypeBaseRoute } from "../../types";
import { getUserFullName } from "../../utilities";
import { ApiProcessError, ClientTypeBaseRoute } from "../../types";
import { STUDY_DATE_OVERLAP_ERROR, getUserFullName } from "../../utilities";
import { PaginatedResultsAPIOutDTO } from "../models/pagination.dto";
import BaseController from "../BaseController";
import {
Expand All @@ -30,10 +38,20 @@ import {
ApplicationOfferingChangeSummaryDetailAPIOutDTO,
CreateApplicationOfferingChangeRequestAPIInDTO,
} from "./models/application-offering-change-request.institutions.dto";
import { ApplicationOfferingChangeRequestService } from "../../services";
import {
APPLICATION_NOT_FOUND,
ApplicationOfferingChangeRequestService,
ApplicationService,
} from "../../services";
import { ApplicationOfferingChangeRequestStatus } from "@sims/sims-db";
import { PrimaryIdentifierAPIOutDTO } from "../models/primary.identifier.dto";
import { IInstitutionUserToken } from "../../auth";
import { CustomNamedError } from "@sims/utilities";
import {
OFFERING_DOES_NOT_BELONG_TO_LOCATION,
OFFERING_INTENSITY_MISMATCH,
OFFERING_PROGRAM_YEAR_MISMATCH,
} from "../../constants";

/**
* Application offering change request controller for institutions client.
Expand All @@ -47,6 +65,7 @@ import { IInstitutionUserToken } from "../../auth";
export class ApplicationOfferingChangeRequestInstitutionsController extends BaseController {
constructor(
private readonly applicationOfferingChangeRequestService: ApplicationOfferingChangeRequestService,
private readonly applicationService: ApplicationService,
) {
super();
}
Expand Down Expand Up @@ -258,21 +277,59 @@ export class ApplicationOfferingChangeRequestInstitutionsController extends Base
* @param payload information to create the new request.
* @returns newly change request id created.
*/
@ApiNotFoundResponse({
description: "Application not found.",
})
@ApiUnprocessableEntityResponse({
description:
"Study period overlap or offering program year mismatch or offering intensity mismatch.",
})
@ApiUnauthorizedResponse({
description: "The location does not have access to the offering.",
})
@Post()
async createApplicationOfferingChangeRequest(
@UserToken() userToken: IInstitutionUserToken,
@Param("locationId", ParseIntPipe) locationId: number,
@Body() payload: CreateApplicationOfferingChangeRequestAPIInDTO,
): Promise<PrimaryIdentifierAPIOutDTO> {
// TODO: Apply the same validations from PIR.
const applicationOfferingChangeRequest =
await this.applicationOfferingChangeRequestService.createRequest(
locationId,
try {
await this.applicationService.applicationOfferingValidation(
payload.applicationId,
payload.offeringId,
payload.reason,
userToken.userId,
locationId,
);
return { id: applicationOfferingChangeRequest.id };
const applicationOfferingChangeRequest =
await this.applicationOfferingChangeRequestService.createRequest(
locationId,
payload.applicationId,
payload.offeringId,
payload.reason,
userToken.userId,
);
return { id: applicationOfferingChangeRequest.id };
} catch (error: unknown) {
if (error instanceof CustomNamedError) {
switch (error.name) {
case APPLICATION_NOT_FOUND:
throw new NotFoundException(
new ApiProcessError(error.message, error.name),
);
case STUDY_DATE_OVERLAP_ERROR:
case OFFERING_PROGRAM_YEAR_MISMATCH:
case OFFERING_INTENSITY_MISMATCH:
throw new UnprocessableEntityException(
new ApiProcessError(error.message, error.name),
);
case OFFERING_DOES_NOT_BELONG_TO_LOCATION:
throw new UnauthorizedException(
new ApiProcessError(error.message, error.name),
);
}
}
throw new InternalServerErrorException(
"Error while submitting an application offering change request.",
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
} from "../../auth/decorators";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import { ApiProcessError, ClientTypeBaseRoute } from "../../types";
import { getPIRDeniedReason, PIR_OR_DATE_OVERLAP_ERROR } from "../../utilities";
import { getPIRDeniedReason, STUDY_DATE_OVERLAP_ERROR } from "../../utilities";
import {
INSTITUTION_LOCATION_NOT_VALID,
INVALID_APPLICATION_NUMBER,
Expand Down Expand Up @@ -227,7 +227,7 @@ export class ApplicationStudentsController extends BaseController {
studentToken.studentId,
);
try {
await this.applicationService.validateOverlappingDatesAndPIR(
await this.applicationService.validateOverlappingDates(
applicationId,
student.user.lastName,
studentToken.userId,
Expand All @@ -252,7 +252,7 @@ export class ApplicationStudentsController extends BaseController {
throw new NotFoundException(error.message);
case APPLICATION_NOT_VALID:
case INVALID_OPERATION_IN_THE_CURRENT_STATUS:
case PIR_OR_DATE_OVERLAP_ERROR:
case STUDY_DATE_OVERLAP_ERROR:
case INSTITUTION_LOCATION_NOT_VALID:
case OFFERING_NOT_VALID:
throw new UnprocessableEntityException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,17 @@ import {
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import { IUserToken } from "../../auth/userToken.interface";
import {
APPLICATION_NOT_FOUND,
ApplicationService,
EducationProgramOfferingService,
PIRDeniedReasonService,
} from "../../services";
import { getUserFullName, PIR_OR_DATE_OVERLAP_ERROR } from "../../utilities";
import { getUserFullName, STUDY_DATE_OVERLAP_ERROR } from "../../utilities";
import { CustomNamedError, getISODateOnlyString } from "@sims/utilities";
import { Application, ProgramInfoStatus } from "@sims/sims-db";
import {
Application,
AssessmentTriggerType,
ProgramInfoStatus,
} from "@sims/sims-db";
import {
OFFERING_DOES_NOT_BELONG_TO_LOCATION,
OFFERING_INTENSITY_MISMATCH,
OFFERING_PROGRAM_YEAR_MISMATCH,
PIR_DENIED_REASON_NOT_FOUND_ERROR,
PIR_REQUEST_NOT_FOUND_ERROR,
} from "../../constants";
Expand All @@ -60,7 +58,6 @@ export class ProgramInfoRequestInstitutionsController extends BaseController {
constructor(
private readonly applicationService: ApplicationService,
private readonly workflowClientService: WorkflowClientService,
private readonly offeringService: EducationProgramOfferingService,
private readonly pirDeniedReasonService: PIRDeniedReasonService,
) {
super();
Expand All @@ -87,9 +84,12 @@ export class ProgramInfoRequestInstitutionsController extends BaseController {
@Param("locationId", ParseIntPipe) locationId: number,
@Param("applicationId", ParseIntPipe) applicationId: number,
): Promise<ProgramInfoRequestAPIOutDTO> {
const application = await this.applicationService.getProgramInfoRequest(
const application = await this.applicationService.getApplicationInfo(
locationId,
applicationId,
{
isPir: true,
},
);
if (!application) {
throw new NotFoundException(
Expand Down Expand Up @@ -122,10 +122,7 @@ export class ProgramInfoRequestInstitutionsController extends BaseController {
// assessment always available for submitted student applications).
// PIR process happens only during original assessment.
// Offering available only when PIR is completed.
const originalAssessmentOffering = application.studentAssessments.find(
(assessment) =>
assessment.triggerType === AssessmentTriggerType.OriginalAssessment,
).offering;
const originalAssessmentOffering = application.currentAssessment.offering;
// If an offering is present (PIR is completed), this value will be the
// program id associated with the offering, otherwise the program id
// from PIR (sims.applications table) will be used.
Expand Down Expand Up @@ -206,6 +203,10 @@ export class ProgramInfoRequestInstitutionsController extends BaseController {
description:
"Application not found or not able to find an application that requires a PIR to be completed.",
})
@ApiUnprocessableEntityResponse({
description:
"Study period overlap or offering program year mismatch or offering intensity mismatch.",
})
@ApiUnauthorizedResponse({
description: "The location does not have access to the offering.",
})
Expand All @@ -217,40 +218,18 @@ export class ProgramInfoRequestInstitutionsController extends BaseController {
@Body() payload: CompleteProgramInfoRequestAPIInDTO,
@UserToken() userToken: IUserToken,
): Promise<void> {
// Validate if the application exists and the location has access to it.
const application = await this.applicationService.getProgramInfoRequest(
locationId,
applicationId,
);
if (!application) {
throw new NotFoundException("Application not found.");
}
// Validates if the offering exists and belongs to the location.
const offering = await this.offeringService.getOfferingLocationId(
payload.selectedOffering,
);
if (offering?.institutionLocation.id !== locationId) {
throw new UnauthorizedException(
"The location does not have access to the offering.",
);
}

try {
// Validate possible overlaps with exists applications.
await this.applicationService.validateOverlappingDatesAndPIR(
await this.applicationService.applicationOfferingValidation(
applicationId,
application.student.user.lastName,
application.student.user.id,
application.student.sinValidation.sin,
application.student.birthDate,
offering.studyStartDate,
offering.studyEndDate,
payload.selectedOffering,
locationId,
{ isPir: true },
);
// Complete PIR.
await this.applicationService.setOfferingForProgramInfoRequest(
applicationId,
locationId,
offering.id,
payload.selectedOffering,
userToken.userId,
);
// Send a message to allow the workflow to proceed.
Expand All @@ -262,14 +241,20 @@ export class ProgramInfoRequestInstitutionsController extends BaseController {
if (error instanceof CustomNamedError) {
switch (error.name) {
case PIR_REQUEST_NOT_FOUND_ERROR:
case APPLICATION_NOT_FOUND:
throw new NotFoundException(
new ApiProcessError(error.message, error.name),
);
case PIR_OR_DATE_OVERLAP_ERROR:
case STUDY_DATE_OVERLAP_ERROR:
case OFFERING_PROGRAM_YEAR_MISMATCH:
case OFFERING_INTENSITY_MISMATCH:
throw new UnprocessableEntityException(
new ApiProcessError(error.message, error.name),
);
case OFFERING_DOES_NOT_BELONG_TO_LOCATION:
throw new UnauthorizedException(
new ApiProcessError(error.message, error.name),
);
}
}
throw new InternalServerErrorException(
Expand Down
Loading