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

#1790 - Enable Applications Tab for Public Institution User #1903

Merged
merged 12 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
@@ -1,6 +1,6 @@
import { IsEnum, IsIn, IsOptional, Max, MaxLength, Min } from "class-validator";
import { FieldSortOrder } from "../../utilities";
import { PAGINATION_SEARCH_MAX_LENGTH } from "../../constants";
import { FieldSortOrder } from "@sims/utilities";

/**
* Common parameters used when an API result
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { HttpStatus, INestApplication } from "@nestjs/common";
import * as request from "supertest";
import { DataSource, Repository } from "typeorm";
import {
authorizeUserTokenForLocation,
BEARER_AUTH_TYPE,
createTestingAppModule,
getAuthRelatedEntities,
getInstitutionToken,
InstitutionTokenTypes,
} from "../../../../testHelpers";
import {
createFakeInstitutionLocation,
saveFakeStudent,
saveFakeApplication,
} from "@sims/test-utils";
import {
Application,
ApplicationStatus,
Institution,
InstitutionLocation,
} from "@sims/sims-db";
import { FieldSortOrder } from "@sims/utilities";

describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () => {
let app: INestApplication;
let appDataSource: DataSource;
let collegeF: Institution;
let collegeFLocation: InstitutionLocation;
let applicationRepo: Repository<Application>;

beforeAll(async () => {
const { nestApplication, dataSource } = await createTestingAppModule();
app = nestApplication;
appDataSource = dataSource;
// College F.
const { institution } = await getAuthRelatedEntities(
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: if you do:

    const { institution: collegeF } = await getAuthRelatedEntities(
      appDataSource,
      InstitutionTokenTypes.CollegeFUser,
    );

then you won't need line 41.

appDataSource,
InstitutionTokenTypes.CollegeFUser,
);
collegeF = institution;
collegeFLocation = createFakeInstitutionLocation(collegeF);
await authorizeUserTokenForLocation(
appDataSource,
InstitutionTokenTypes.CollegeFUser,
collegeFLocation,
);
applicationRepo = appDataSource.getRepository(Application);
});

it(
"Should get the student application details as summary when student has a submitted application" +
"for the institution (application with location id saved).",
async () => {
// Arrange

// Student has a submitted application to the institution.
const student = await saveFakeStudent(appDataSource);

const savedApplication = await saveFakeApplication(appDataSource, {
institution: collegeF,
institutionLocation: collegeFLocation,
student,
});

const endpoint = `/institutions/student/${student.id}/application-summary?page=0&pageLimit=10`;
const institutionUserToken = await getInstitutionToken(
InstitutionTokenTypes.CollegeFUser,
);

// Act/Assert
await request(app.getHttpServer())
.get(endpoint)
.auth(institutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect({
results: [
{
id: savedApplication.id,
applicationNumber: savedApplication.applicationNumber,
studyStartPeriod:
savedApplication.currentAssessment.offering.studyStartDate,
studyEndPeriod:
savedApplication.currentAssessment.offering.studyEndDate,
applicationName: "Financial Aid Application",
status: savedApplication.applicationStatus,
},
],
count: 1,
});
},
);

it(
"Should get the first submitted student application details as summary when student has two submitted/Inprogress " +
"application for the institution (application with location id saved) when pagination is 1.",
async () => {
// Arrange

// Student has one submitted and one inprogress application to the institution.
const student = await saveFakeStudent(appDataSource);
// Application 1.
const savedApplication1 = await saveFakeApplication(appDataSource, {
institution: collegeF,
institutionLocation: collegeFLocation,
student,
});

// Application 2.
await saveFakeApplication(
appDataSource,
{
institution: collegeF,
institutionLocation: collegeFLocation,
student,
},
{ applicationStatus: ApplicationStatus.InProgress },
);

// By default the application is sorted by status.
const endpoint = `/institutions/student/${student.id}/application-summary?page=0&pageLimit=1`;
const institutionUserToken = await getInstitutionToken(
InstitutionTokenTypes.CollegeFUser,
);

// Act/Assert
await request(app.getHttpServer())
.get(endpoint)
.auth(institutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect({
results: [
{
id: savedApplication1.id,
applicationNumber: savedApplication1.applicationNumber,
studyStartPeriod:
savedApplication1.currentAssessment.offering.studyStartDate,
studyEndPeriod:
savedApplication1.currentAssessment.offering.studyEndDate,
applicationName: "Financial Aid Application",
status: savedApplication1.applicationStatus,
},
],
count: 2,
});
},
);

it(
`Should get all the two student application details in ${FieldSortOrder.DESC} order of application number ` +
"as summary when student has two submitted/Inprogress application for the institution (application with location id saved) " +
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a suggestion, I would not mind omitting the explanation about "(application with location id saved)", that said I do support the long text description 😉

`and when sortField= is application number and sortOrder is ${FieldSortOrder.DESC}. `,
async () => {
// Arrange

// Student has a submitted application to the institution.
const student = await saveFakeStudent(appDataSource);
// Application 1.
const savedApplication1 = await saveFakeApplication(appDataSource, {
institution: collegeF,
institutionLocation: collegeFLocation,
student,
});

// Application 2.
const savedApplication2 = await saveFakeApplication(
appDataSource,
{
institution: collegeF,
institutionLocation: collegeFLocation,
student,
},
{ applicationStatus: ApplicationStatus.InProgress },
);

savedApplication1.applicationNumber = "1000000000";
savedApplication2.applicationNumber = "1000000001";
await applicationRepo.save([savedApplication1, savedApplication2]);

const endpoint = `/institutions/student/${student.id}/application-summary?page=0&pageLimit=10&sortField=applicationNumber&sortOrder=${FieldSortOrder.DESC}`;
const institutionUserToken = await getInstitutionToken(
InstitutionTokenTypes.CollegeFUser,
);

// Act/Assert
await request(app.getHttpServer())
.get(endpoint)
.auth(institutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect({
results: [
{
id: savedApplication2.id,
applicationNumber: savedApplication2.applicationNumber,
studyStartPeriod:
savedApplication2.currentAssessment.offering.studyStartDate,
studyEndPeriod:
savedApplication2.currentAssessment.offering.studyEndDate,
applicationName: "Financial Aid Application",
status: savedApplication2.applicationStatus,
},
{
id: savedApplication1.id,
applicationNumber: savedApplication1.applicationNumber,
studyStartPeriod:
savedApplication1.currentAssessment.offering.studyStartDate,
studyEndPeriod:
savedApplication1.currentAssessment.offering.studyEndDate,
applicationName: "Financial Aid Application",
status: savedApplication1.applicationStatus,
},
],
count: 2,
});
},
);

afterAll(async () => {
await app?.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,19 @@ export class StudentControllerService {
* This API will be used by students.
* @param studentId student id to retrieve the application summary.
* @param pagination options to execute the pagination.
* @param institutionId id of the institution that the student applied to.
* @returns student application list with total count.
*/
async getStudentApplicationSummary(
studentId: number,
pagination: ApplicationPaginationOptionsAPIInDTO,
institutionId?: number,
): Promise<PaginatedResultsAPIOutDTO<ApplicationSummaryAPIOutDTO>> {
const [applications, count] =
await this.applicationService.getAllStudentApplications(
studentId,
pagination,
institutionId,
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Param,
ParseIntPipe,
Post,
Query,
} from "@nestjs/common";
import { ApiNotFoundResponse, ApiTags } from "@nestjs/swagger";
import { StudentService, StudentFileService } from "../../services";
Expand All @@ -18,10 +19,15 @@ import {
StudentSearchAPIInDTO,
SearchStudentAPIOutDTO,
StudentProfileAPIOutDTO,
ApplicationSummaryAPIOutDTO,
StudentFileDetailsAPIOutDTO,
} from "./models/student.dto";
import { IInstitutionUserToken } from "../../auth";
import { StudentControllerService } from "./student.controller.service";
import {
ApplicationPaginationOptionsAPIInDTO,
PaginatedResultsAPIOutDTO,
} from "../models/pagination.dto";

/**
* Student controller for institutions.
Expand Down Expand Up @@ -76,6 +82,27 @@ export class StudentInstitutionsController extends BaseController {
return this.studentControllerService.getStudentProfile(studentId);
}

/**
* Get the list of applications that belongs to a student for the institution.
* TODO: Authorization must be enabled to validate if the student has submitted
* at least one application for the institution of the user.
* @param studentId student.
* @returns list of applications that belongs to a student for the institution.
*/
@Get(":studentId/application-summary")
@ApiNotFoundResponse({ description: "Student not found." })
async getStudentApplicationSummary(
@UserToken() userToken: IInstitutionUserToken,
@Param("studentId", ParseIntPipe) studentId: number,
@Query() paginationOptions: ApplicationPaginationOptionsAPIInDTO,
): Promise<PaginatedResultsAPIOutDTO<ApplicationSummaryAPIOutDTO>> {
return this.studentControllerService.getStudentApplicationSummary(
studentId,
paginationOptions,
userToken.authorizations.institutionId,
);
}

/**
* Get all the documents uploaded by student.
* @param studentId student id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import {
getUserFullNameLikeSearch,
} from "@sims/sims-db";
import {
FieldSortOrder,
OrderByCondition,
PaginatedResults,
PaginationOptions,
} from "../../utilities";
import { CustomNamedError } from "@sims/utilities";
import { CustomNamedError, FieldSortOrder } from "@sims/utilities";
import {
STUDENT_APPLICATION_EXCEPTION_INVALID_STATE,
STUDENT_APPLICATION_EXCEPTION_NOT_FOUND,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ import {
PIR_OR_DATE_OVERLAP_ERROR,
PaginationOptions,
PaginatedResults,
FieldSortOrder,
OrderByCondition,
} from "../../utilities";
import { CustomNamedError, QueueNames } from "@sims/utilities";
import { CustomNamedError, FieldSortOrder, QueueNames } from "@sims/utilities";
import {
SFASApplicationService,
SFASPartTimeApplicationsService,
Expand Down Expand Up @@ -585,12 +584,15 @@ export class ApplicationService extends RecordDataModelService<Application> {

/**
* Get all student applications.
* @param studentId student id .
* @param studentId student id.
* @param pagination options to execute the pagination.
* @param institutionId id of the institution that the student applied to.
* @returns student Application list.
*/
async getAllStudentApplications(
studentId: number,
pagination: PaginationOptions,
institutionId?: number,
): Promise<[Application[], number]> {
const applicationQuery = this.repo
.createQueryBuilder("application")
Expand All @@ -608,6 +610,17 @@ export class ApplicationService extends RecordDataModelService<Application> {
.andWhere("application.applicationStatus != :overwrittenStatus", {
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is not part of your PR, but can we change the leftJoin on the currentAssessment and offering to innerJoin?

Copy link
Contributor

Choose a reason for hiding this comment

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

As the services is used across client types, the left join actually make sense

Copy link
Collaborator

Choose a reason for hiding this comment

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

To be clear, the method is supposed to return all applications, even draft ones, unless I am missing something. It is not about bing used across different clients it is about what it needs to return.

overwrittenStatus: ApplicationStatus.Overwritten,
});
// If institution id is present, get only the applications
// linked with the institution.

if (institutionId) {
applicationQuery
.innerJoin("application.location", "institutionLocation")
.innerJoin("institutionLocation.institution", "institution")
.andWhere("institution.id = :institutionId", {
institutionId,
});
}

// sorting
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
COE_WINDOW,
COE_DENIED_REASON_OTHER_ID,
PaginationOptions,
FieldSortOrder,
PaginatedResults,
OrderByCondition,
} from "../../utilities";
Expand All @@ -12,6 +11,7 @@ import {
getISODateOnlyString,
isBetweenPeriod,
isBeforeDate,
FieldSortOrder,
} from "@sims/utilities";
import { DataSource, UpdateResult, Brackets, EntityManager } from "typeorm";
import { SequenceControlService } from "@sims/services";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
EducationProgramOfferingBasicData,
} from "./education-program-offering.service.models";
import {
FieldSortOrder,
sortOfferingsColumnMap,
PaginationOptions,
PaginatedResults,
Expand All @@ -45,6 +44,7 @@ import {
CustomNamedError,
dateDifference,
decimalRound,
FieldSortOrder,
QueueNames,
} from "@sims/utilities";
import {
Expand Down
Loading