Skip to content

#6624 - Search student by certificate number - #6849

Merged
tiago-graf merged 8 commits into
mainfrom
feature/#6624-ecert-search
Nov 10, 2026
Merged

tiago-graf merged 8 commits into
mainfrom
feature/#6624-ecert-search

Conversation

@tiago-graf

Copy link
Copy Markdown
Collaborator

Summary

Adds a new field "Certificate Number" in the student search tab.

@tiago-graf tiago-graf self-assigned this Nov 9, 2026
Copilot AI balanced review requested due to automatic review settings November 9, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Certificate validation allows nonnumeric or overlong values, causing uncaught HTTP 400 errors that leave users without validation feedback and previous search results still displayed.

1 open issue
What changed in this PR

Adds certificate-number lookup to student search, connecting the shared search interface to exact-match disbursement queries in the API.

Changes:

  • Adds a certificate-number field and converts the search component to <script setup>.
  • Extends search contracts, input validation, and backend filtering.
  • Adds API tests for exact matches, invalid input, and institution access restrictions.
File Description
sources/​packages/​web/​src/​services/​http/​dto/​Student.dto.ts Adds the certificate-number search parameter.
sources/​packages/​web/​src/​components/​common/​SearchStudents.vue Adds the search field and adopts <script setup>.
sources/​packages/​backend/​apps/​api/​src/​services/​student/​student.service.ts Filters students by exact disbursement certificate number.
sources/​packages/​backend/​apps/​api/​src/​route-controllers/​student/​models/​student.dto.ts Validates numeric certificate input up to nine digits.
sources/​packages/​backend/​apps/​api/​src/​route-controllers/​student/​_tests_/​e2e/​student.institutions.controller.searchStudents.e2e-spec.ts Tests certificate lookup with institution access restrictions.
sources/​packages/​backend/​apps/​api/​src/​route-controllers/​student/​_tests_/​e2e/​student.aest.controller.searchStudents.e2e-spec.ts Tests exact matching, partial-match rejection, and input validation.

Review effort: Balanced

@sh16011993
sh16011993 self-requested a review November 9, 2026 16:20
Comment on lines +112 to +116
!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.

@andrewsignori-aot
andrewsignori-aot self-requested a review November 9, 2026 17:53
@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).

Comment on lines +5 to +14
<v-col cols="12" lg>
<v-text-field
label="Certificate number"
density="compact"
variant="outlined"
v-model.trim="documentNumber"
@keyup.enter="searchStudents"
hide-details
/>
</v-col>

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.

The UI does not restrict the input, which allows more characters than the DTO allows to be submitted, causing a 400 HTTP error.

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.

This can be restricted to accept only digits, up to the max length allowed by the API, or use validation similar to the one used by SIN, but simpler.

@sh16011993 sh16011993 left a comment •

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.

One suggestion comment: To use the class-level validator

Nice work @tiago-graf 👍

@andrewsignori-aot andrewsignori-aot left a comment

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.

Nice work, minor comments.

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.

👍

ValidateIf,
} from "class-validator";
import { IsValidSIN } from "../../../utilities/class-validation/custom-validators/sin-validator";
import { IsAnyProvided } from "../../../utilities/class-validation/custom-validators/is-any-provided";

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.

Please import from the existing index.

registerDecorator({
name: "IsAnyProvided",
target,
// Class-level validation is not tied to any property.

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.

Does the framework support class-level validation, or should this be considered a workaround?

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.

If the above assumption is correct, I would recommend following the framework standards unless no other way is available.

For this scenario, the validtor can be adapted to be added to every property to act similarly to a "ValidateIf".

DTO

/**
 * Student search filters, of which at least one is required
 * to avoid retrieving all the students.
 */
const STUDENT_SEARCH_FILTERS: Array<keyof StudentSearchAPIInDTO> = [
  "firstName",
  "lastName",
  "appNumber",
  "sin",
  "email",
  "documentNumber",
];
export class StudentSearchAPIInDTO {
  @IsOptionalIfAnyOtherProvided(STUDENT_SEARCH_FILTERS)
  firstName?: string;
  @IsOptionalIfAnyOtherProvided(STUDENT_SEARCH_FILTERS)
  lastName?: string;
  @IsOptionalIfAnyOtherProvided(STUDENT_SEARCH_FILTERS)
  appNumber?: string;
  @IsOptionalIfAnyOtherProvided(STUDENT_SEARCH_FILTERS)
  sin?: string;
  @IsOptionalIfAnyOtherProvided(STUDENT_SEARCH_FILTERS)
  email?: string;
  /**
   * 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).
   */
  @IsOptionalIfAnyOtherProvided(STUDENT_SEARCH_FILTERS)
  @IsInt()
  @IsPositive()
  @Max(999999999)
  documentNumber?: number;
}

Validator DRAFT (not tested)

export function IsOptionalIfAnyOtherProvided<T extends object>(
  properties: Array<keyof T & string>,
  validationOptions?: ValidationOptions,
): PropertyDecorator {
  return (target: object, propertyKey: string | symbol) => {
    const propertyName = propertyKey as string;
    ValidateIf(
      (object: Record<string, unknown>) =>
        !!object[propertyName] ||
        !properties.some(
          (property) => property !== propertyName && !!object[property],
        ),
    )(target, propertyKey);
    registerDecorator({
      name: "IsOptionalIfAnyOtherProvided",
      target: target.constructor,
      propertyName,
      options: validationOptions,
      constraints: [properties],
      validator: {
        validate(value: unknown): boolean {
          return !!value;
        },
        defaultMessage(args: ValidationArguments): string {
          const [requiredProperties] = args.constraints as [string[]];
          return `At least one of the following must be provided: ${requiredProperties.join(", ")}.`;
        },
      },
    });
  };
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This seem to work great, i just made an adjustment to allow for a optional "properties" parameter, if not provided the validator assumes all properties in the object must be checked, this avoids the need of the list of "filter" properties names

@andrewsignori-aot andrewsignori-aot left a comment

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.

Thanks for making the changes. The accepted suggestion to change the DTO decorators required further discussion, please see my comments about it.
Please also check the minor Sonarcloud issues.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Backend Unit Tests Coverage Report

Totals Coverage
Statements: 19.96% ( 5112 / 25609 )
Methods: 9.57% ( 300 / 3134 )
Lines: 24.42% ( 4376 / 17920 )
Branches: 9.57% ( 436 / 4555 )

@github-actions

Copy link
Copy Markdown

E2E Workflow Workers Coverage Report

Totals Coverage
Statements: 48.61% ( 3016 / 6205 )
Methods: 38.49% ( 316 / 821 )
Lines: 55.41% ( 2279 / 4113 )
Branches: 33.12% ( 421 / 1271 )

@github-actions

Copy link
Copy Markdown

E2E Queue Consumers Coverage Report

Totals Coverage
Statements: 82.2% ( 10417 / 12673 )
Methods: 81.44% ( 1356 / 1665 )
Lines: 85.68% ( 7776 / 9076 )
Branches: 66.51% ( 1285 / 1932 )

@github-actions

Copy link
Copy Markdown

E2E SIMS API Coverage Report

Totals Coverage
Statements: 71.64% ( 16062 / 22421 )
Methods: 69.54% ( 1934 / 2781 )
Lines: 74.58% ( 11404 / 15290 )
Branches: 62.62% ( 2724 / 4350 )

@andrewsignori-aot andrewsignori-aot left a comment

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.

Nice work, thanks for the extra effort to refactor and created the IsOptionalIfAnyOtherProvided. Looks good 👍

@tiago-graf
tiago-graf added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 3625777 Nov 10, 2026
14 checks passed
@tiago-graf
tiago-graf deleted the feature/#6624-ecert-search branch November 10, 2026 22:04

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants