Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,7 +1,11 @@
import { HttpStatus, INestApplication } from "@nestjs/common";
import request from "supertest";
import { DataSource, Repository } from "typeorm";
import { saveFakeStudent } from "@sims/test-utils";
import {
saveFakeApplicationDisbursements,
saveFakeStudent,
} from "@sims/test-utils";
import { faker } from "@faker-js/faker";
import { Student } from "@sims/sims-db";
import {
createTestingAppModule,
Expand Down Expand Up @@ -173,6 +177,71 @@ describe("StudentMinistryController(e2e)-searchStudents", () => {
.expect([]);
});

it("Should find the student by certificate number when there is an exact match.", async () => {
// Arrange
const student = await saveFakeStudent(appDataSource);
const documentNumber = faker.number.int({ min: 100000000, max: 999999999 });
await saveFakeApplicationDisbursements(
appDataSource,
{ student },
{ firstDisbursementInitialValues: { documentNumber } },
);
const token = await getAESTToken(AESTGroups.BusinessAdministrators);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.send({ documentNumber: documentNumber.toString() })
.auth(token, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect([
{
id: student.id,
firstName: student.user.firstName,
lastName: student.user.lastName,
birthDate: student.birthDate,
sin: student.sinValidation.sin,
},
]);
});

it("Should not find the student by certificate number when there is only a partial match.", async () => {
// Arrange
const student = await saveFakeStudent(appDataSource);
const documentNumber = faker.number.int({ min: 100000000, max: 999999999 });
await saveFakeApplicationDisbursements(
appDataSource,
{ student },
{ firstDisbursementInitialValues: { documentNumber } },
);
const token = await getAESTToken(AESTGroups.BusinessAdministrators);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.send({ documentNumber: documentNumber.toString().substring(0, 8) })
.auth(token, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect([]);
});

it("Should throw a bad request error when the certificate number is not numeric.", async () => {
// Arrange
const token = await getAESTToken(AESTGroups.BusinessAdministrators);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.send({ documentNumber: "12A45" })
.auth(token, BEARER_AUTH_TYPE)
.expect(HttpStatus.BAD_REQUEST)
.expect({
message: ["documentNumber must be a number string"],
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
});
});

it("Should throw a bad request error when no search parameters are provided.", async () => {
// Arrange
const student = await saveFakeStudent(appDataSource);
Expand All @@ -194,6 +263,9 @@ describe("StudentMinistryController(e2e)-searchStudents", () => {
"appNumber should not be empty",
"sin should not be empty",
"email should not be empty",
"documentNumber must be shorter than or equal to 9 characters",
"documentNumber must be a number string",
"documentNumber should not be empty",
],
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
import {
createFakeInstitutionLocation,
saveFakeApplication,
saveFakeApplicationDisbursements,
saveFakeStudent,
} from "@sims/test-utils";
import { faker } from "@faker-js/faker";
import {
Application,
ApplicationStatus,
Expand Down Expand Up @@ -483,6 +485,58 @@ describe("StudentInstitutionsController(e2e)-searchStudents", () => {
);
});

it("Should find the student by certificate number when the student has an application for the institution and there is an exact match.", async () => {
// Arrange
const student = await saveFakeStudent(appDataSource);
Comment thread
andrewsignori-aot marked this conversation as resolved.
Resolved
const documentNumber = faker.number.int({ min: 100000000, max: 999999999 });
Comment thread
andrewsignori-aot marked this conversation as resolved.
Resolved
await saveFakeApplicationDisbursements(
appDataSource,
{
institution: collegeF,
institutionLocation: collegeFLocation,
student,
},
{ firstDisbursementInitialValues: { documentNumber } },
);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.send({ documentNumber: documentNumber.toString() })
.auth(collegeFInstitutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect(({ body }) =>
expect(body).toEqual([
{
id: student.id,
firstName: student.user.firstName,
lastName: student.user.lastName,
birthDate: student.birthDate,
sin: student.sinValidation.sin,
},
]),
);
});

it("Should not find the student by certificate number when the student has no application for the institution.", async () => {
// Arrange
const student = await saveFakeStudent(appDataSource);
const documentNumber = faker.number.int({ min: 100000000, max: 999999999 });
await saveFakeApplicationDisbursements(
appDataSource,
{ student },
{ firstDisbursementInitialValues: { documentNumber } },
);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.send({ documentNumber: documentNumber.toString() })
.auth(collegeFInstitutionUserToken, BEARER_AUTH_TYPE)
.expect(HttpStatus.OK)
.expect([]);
});

/**
* Saves a student and an application for the student for College F.
* @param applicationStatus application status.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IsEmail,
IsEnum,
IsNotEmpty,
IsNumberString,
IsOptional,
IsPositive,
Length,
Expand Down Expand Up @@ -108,34 +109,70 @@ export class UpdateStudentAPIInDTO
export class StudentSearchAPIInDTO {
@ValidateIf(
(input: StudentSearchAPIInDTO) =>
!input.lastName && !input.appNumber && !input.sin && !input.email,
!input.lastName &&
!input.appNumber &&
!input.sin &&
!input.email &&
!input.documentNumber,

@sh16011993 sh16011993 Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@tiago-graf Thanks for the discussion related to this. Instead of this approach, I would suggest using a class level validator to validate at least one search field must be provided.

)
@IsNotEmpty()
firstName: string;
@ValidateIf(
(input: StudentSearchAPIInDTO) =>
!input.firstName && !input.appNumber && !input.sin && !input.email,
!input.firstName &&
!input.appNumber &&
!input.sin &&
!input.email &&
!input.documentNumber,
)
@IsNotEmpty()
lastName: string;
@ValidateIf(
(input: StudentSearchAPIInDTO) =>
!input.firstName && !input.lastName && !input.sin && !input.email,
!input.firstName &&
!input.lastName &&
!input.sin &&
!input.email &&
!input.documentNumber,
)
@IsNotEmpty()
appNumber: string;
@ValidateIf(
(input: StudentSearchAPIInDTO) =>
!input.firstName && !input.lastName && !input.appNumber && !input.email,
!input.firstName &&
!input.lastName &&
!input.appNumber &&
!input.email &&
!input.documentNumber,
)
@IsNotEmpty()
sin: string;
@ValidateIf(
(input: StudentSearchAPIInDTO) =>
!input.firstName && !input.lastName && !input.appNumber && !input.sin,
!input.firstName &&
!input.lastName &&
!input.appNumber &&
!input.sin &&
!input.documentNumber,
)
@IsNotEmpty()
email: string;
/**
* Certificate (financial document) number. Limited to 9 digits to fit the integer DB column.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

From a DB perspective, it should validate up to a max int4 amount.
From a biz perspective, it can have up to 8 (full-time) or 7 (part-time).
Maybe the comment can be adjusted to

Document number (a.k.a. e-Cert Number). Limited to 9 digits to be safe, since there is no business requirement to have it restricted; currently it can have up to 8 (full-time) or 7 (part-time).

*/
@ValidateIf(
(input: StudentSearchAPIInDTO) =>
!!input.documentNumber ||
(!input.firstName &&
!input.lastName &&
!input.appNumber &&
!input.sin &&
!input.email),
)
@IsNotEmpty()
@IsNumberString({ no_symbols: true })
@MaxLength(9)
documentNumber: string;
Comment thread
andrewsignori-aot marked this conversation as resolved.
Resolved
}

export class SearchStudentAPIOutDTO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ModifiedIndependentStatus,
SFASApplication,
ORMCacheManager,
DisbursementSchedule,
} from "@sims/sims-db";
import { DataSource, EntityManager, Not, UpdateResult } from "typeorm";
import { LoggerService } from "@sims/utilities/logger";
Expand Down Expand Up @@ -635,7 +636,7 @@ export class StudentService extends RecordDataModelService<Student> {
/**
* Search students based on the search criteria and institution id if provided.
* @param searchCriteria options to search by firstName,
* lastName, appNumber or sin.
* lastName, appNumber, sin, email or documentNumber (certificate number).
* @param institutionId id of the institution that the student applied to.
* @returns list of student details.
*/
Expand All @@ -646,6 +647,7 @@ export class StudentService extends RecordDataModelService<Student> {
appNumber?: string;
sin?: string;
email?: string;
documentNumber?: string;
},
institutionId?: number,
): Promise<Student[]> {
Expand Down Expand Up @@ -708,6 +710,25 @@ export class StudentService extends RecordDataModelService<Student> {
email: searchCriteria.email.toLowerCase(),
});
}
if (searchCriteria.documentNumber) {
// Exact match on the certificate number of any disbursement of the student.
searchQuery.andWhere(
(qb) =>
`EXISTS ${qb
.subQuery()
.select("1")
.from(DisbursementSchedule, "disbursementSchedule")
.innerJoin(
"disbursementSchedule.studentAssessment",
"studentAssessment",
)
.innerJoin("studentAssessment.application", "certApplication")
.where("certApplication.student.id = student.id")
.andWhere("disbursementSchedule.documentNumber = :documentNumber")
.getQuery()}`,
{ documentNumber: Number(searchCriteria.documentNumber) },
);
}
return searchQuery.getMany();
}

Expand Down
Loading
Loading