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

#3258 - CAS Supplier Retry operation #4053

Merged
merged 25 commits into from
Dec 11, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { HttpStatus, INestApplication } from "@nestjs/common";
import * as request from "supertest";
import {
AESTGroups,
BEARER_AUTH_TYPE,
createTestingAppModule,
getAESTToken,
} from "../../../testHelpers";
import {
E2EDataSources,
createE2EDataSources,
saveFakeCASSupplier,
saveFakeStudent,
} from "@sims/test-utils";
import { SupplierStatus } from "@sims/sims-db";

describe("CASSupplierAESTController(e2e)-retryCASSupplier", () => {
let app: INestApplication;
let db: E2EDataSources;

beforeAll(async () => {
const { nestApplication, dataSource } = await createTestingAppModule();
app = nestApplication;
db = createE2EDataSources(dataSource);
});

it("Should throw Unprocessable Entity Error when retry CAS supplier is requested for a student who already has a CAS supplier in Pending supplier verification status.", async () => {
// Arrange
// Create a CAS Supplier for the student with Pending supplier verification status.
const casSupplier = await saveFakeCASSupplier(db, undefined, {
initialValues: {
supplierStatus: SupplierStatus.PendingSupplierVerification,
},
});
const student = await saveFakeStudent(db.dataSource, { casSupplier });
const endpoint = `/aest/cas-supplier/student/${student.id}/retry`;
const token = await getAESTToken(AESTGroups.BusinessAdministrators);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.auth(token, BEARER_AUTH_TYPE)
.expect(HttpStatus.UNPROCESSABLE_ENTITY)
.expect({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message:
"There is already a CAS Supplier for this student in Pending supplier verification status.",
error: "Unprocessable Entity",
});
});

it("Should create CAS supplier when retry CAS supplier is requested for a student who does not have a latest CAS supplier in Pending supplier verification status.", async () => {
// Arrange
// Create a CAS Supplier for the student with Manual Intervention status.
const fakeCASSupplier = await saveFakeCASSupplier(db, undefined, {
initialValues: {
supplierStatus: SupplierStatus.ManualIntervention,
},
});
const student = await saveFakeStudent(db.dataSource, undefined, {
initialValue: {
casSupplier: fakeCASSupplier,
},
});
const endpoint = `/aest/cas-supplier/student/${student.id}/retry`;
const token = await getAESTToken(AESTGroups.BusinessAdministrators);

// Act/Assert
let responseCasSupplierId: number;
await request(app.getHttpServer())
.post(endpoint)
.auth(token, BEARER_AUTH_TYPE)
.expect(HttpStatus.CREATED)
.then((response) => {
responseCasSupplierId = +response.body.id;
expect(responseCasSupplierId).toEqual(expect.any(Number));
});

const updatedStudent = await db.student.findOne({
select: {
id: true,
casSupplier: {
id: true,
supplierStatus: true,
supplierStatusUpdatedOn: true,
isValid: true,
},
},
relations: {
casSupplier: true,
},
where: {
id: student.id,
},
loadEagerRelations: false,
});

expect(updatedStudent).toEqual({
id: student.id,
casSupplier: {
id: responseCasSupplierId,
supplierStatus: SupplierStatus.PendingSupplierVerification,
supplierStatusUpdatedOn: expect.any(Date),
isValid: false,
},
});
});

it("Should throw Not Found Error when a student is not found.", async () => {
// Arrange
const endpoint = `/aest/cas-supplier/student/99999/retry`;
const token = await getAESTToken(AESTGroups.BusinessAdministrators);

// Act/Assert
await request(app.getHttpServer())
.post(endpoint)
.auth(token, BEARER_AUTH_TYPE)
.expect(HttpStatus.NOT_FOUND)
.expect({
statusCode: HttpStatus.NOT_FOUND,
message: "Student not found.",
error: "Not Found",
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
Param,
ParseIntPipe,
Post,
UnprocessableEntityException,
} from "@nestjs/common";
import { ApiNotFoundResponse, ApiTags } from "@nestjs/swagger";
import {
ApiNotFoundResponse,
ApiTags,
ApiUnprocessableEntityResponse,
} from "@nestjs/swagger";
import { AuthorizedParties, IUserToken, Role, UserGroups } from "../../auth";
import {
AllowAuthorizedParty,
Expand All @@ -20,7 +25,11 @@ import {
AddCASSupplierAPIInDTO,
CASSupplierInfoAPIOutDTO,
} from "./models/cas-supplier.dto";
import { CASSupplierService, StudentService } from "../../services";
import {
CASSupplierService,
CAS_SUPPLIER_ALREADY_IN_PENDING_SUPPLIER_VERIFICATION,
StudentService,
} from "../../services";
import { ClientTypeBaseRoute } from "../../types";
import { PrimaryIdentifierAPIOutDTO } from "../models/primary.identifier.dto";
import { STUDENT_NOT_FOUND } from "../../constants";
Expand Down Expand Up @@ -110,4 +119,41 @@ export class CASSupplierAESTController extends BaseController {
throw error;
}
}

/**
* Retries CAS Supplier for a student.
* @param studentId student id.
* @param userToken user token.
*/
@Roles(Role.AESTEditCASSupplierInfo)
@Post("student/:studentId/retry")
@ApiNotFoundResponse({
description: "Student not found.",
})
@ApiUnprocessableEntityResponse({
description:
"There is already a CAS Supplier for this student in Pending supplier verification status.",
})
async retryCASSupplier(
@Param("studentId", ParseIntPipe) studentId: number,
@UserToken() userToken: IUserToken,
): Promise<PrimaryIdentifierAPIOutDTO> {
try {
const casSupplier = await this.casSupplierService.retryCASSupplier(
studentId,
userToken.userId,
);
return { id: casSupplier.id };
} catch (error: unknown) {
if (error instanceof CustomNamedError) {
switch (error.name) {
case STUDENT_NOT_FOUND:
throw new NotFoundException(error.message);
case CAS_SUPPLIER_ALREADY_IN_PENDING_SUPPLIER_VERIFICATION:
throw new UnprocessableEntityException(error.message);
}
}
throw error;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { STUDENT_NOT_FOUND } from "../../constants";
import { Repository } from "typeorm";
import { CASSupplierSharedService } from "@sims/services";

export const CAS_SUPPLIER_ALREADY_IN_PENDING_SUPPLIER_VERIFICATION =
"CAS_SUPPLIER_ALREADY_IN_PENDING_SUPPLIER_VERIFICATION";

Injectable();
export class CASSupplierService {
constructor(
Expand Down Expand Up @@ -103,4 +106,49 @@ export class CASSupplierService {

return student.casSupplier;
}

/**
* Retries CAS supplier info for a student.
* Inserts a new CAS Supplier record for the student with {@link SupplierStatus.PendingSupplierVerification} status.
* @param studentId student id.
* @param auditUserId user id for the record creation.
* @returns the created CAS Supplier.
*/
async retryCASSupplier(
studentId: number,
auditUserId: number,
): Promise<CASSupplier> {
const student = await this.studentRepo.findOne({
select: {
id: true,
casSupplier: {
id: true,
supplierStatus: true,
},
},
relations: {
casSupplier: true,
},
where: {
id: studentId,
},
});
if (!student) {
throw new CustomNamedError("Student not found.", STUDENT_NOT_FOUND);
}
if (
student.casSupplier?.supplierStatus ===
SupplierStatus.PendingSupplierVerification
) {
throw new CustomNamedError(
`There is already a CAS Supplier for this student in ${SupplierStatus.PendingSupplierVerification} status.`,
CAS_SUPPLIER_ALREADY_IN_PENDING_SUPPLIER_VERIFICATION,
);
}
const savedStudent = await this.studentService.createPendingCASSupplier(
studentId,
auditUserId,
);
return savedStudent.casSupplier;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,9 @@ export class StudentService extends RecordDataModelService<Student> {
sinValidation.sin = studentSIN;
sinValidation.student = student;
student.sinValidation = sinValidation;

const casSupplier = new CASSupplier();
casSupplier.supplierStatus = SupplierStatus.PendingSupplierVerification;
casSupplier.supplierStatusUpdatedOn = new Date();
casSupplier.isValid = false;
casSupplier.creator = auditUser;
casSupplier.student = savedStudent;
const savedCASSupplier = await entityManager
.getRepository(CASSupplier)
.save(casSupplier);

savedStudent.casSupplier = savedCASSupplier;
await entityManager.getRepository(Student).save(student);
await this.createPendingCASSupplier(student.id, auditUserId, {
entityManager,
});
}

if (sfasIndividual) {
Expand Down Expand Up @@ -917,4 +907,36 @@ export class StudentService extends RecordDataModelService<Student> {
loadEagerRelations: false,
});
}

/**
* Creates a new CAS Supplier for a student.
* @param studentId student id.
* @param auditUserId audit user id.
* @param options options.
* - `entityManager`: entity manager for a given transaction.
* @returns saved student.
**/
async createPendingCASSupplier(
studentId: number,
auditUserId: number,
options?: { entityManager?: EntityManager },
): Promise<Student> {
const auditUser = { id: auditUserId } as User;
const now = new Date();
const studentRepo = options?.entityManager
? options?.entityManager.getRepository(Student)
: this.repo;
const casSupplier = new CASSupplier();
casSupplier.supplierStatus = SupplierStatus.PendingSupplierVerification;
casSupplier.supplierStatusUpdatedOn = now;
casSupplier.isValid = false;
casSupplier.creator = auditUser;
casSupplier.student = { id: studentId } as Student;
const student = new Student();
student.id = studentId;
student.casSupplier = casSupplier;
student.modifier = auditUser;
student.updatedAt = now;
return studentRepo.save(student);
}
}
22 changes: 18 additions & 4 deletions sources/packages/backend/libs/test-utils/src/factories/student.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import * as faker from "faker";
import { DisabilityStatus, SINValidation, Student, User } from "@sims/sims-db";
import {
CASSupplier,
DisabilityStatus,
SINValidation,
Student,
User,
} from "@sims/sims-db";
import { createFakeUser } from "@sims/test-utils";
import { DataSource } from "typeorm";
import { createFakeSINValidation } from "./sin-validation";
Expand All @@ -9,10 +15,12 @@ import { COUNTRY_CANADA, getISODateOnlyString } from "@sims/utilities";
// updated.
export function createFakeStudent(
user?: User,
relations?: { casSupplier?: CASSupplier },
options?: { initialValue: Partial<Student> },
): Student {
const student = new Student();
student.user = user ?? createFakeUser();
student.casSupplier = relations?.casSupplier;
student.birthDate =
options?.initialValue?.birthDate ??
getISODateOnlyString(faker.date.past(18));
Expand Down Expand Up @@ -41,6 +49,7 @@ export function createFakeStudent(
* - `user` related user.
* - `student` student to be created an associated with other relations.
* - `sinValidation` related SIN validation.
* - `casSupplier` related CAS supplier.
* @param options student options.
* - `initialValue` student initial values.
* - `sinValidationInitialValue` sinValidation initial value.
Expand All @@ -52,6 +61,7 @@ export async function saveFakeStudent(
student?: Student;
user?: User;
sinValidation?: SINValidation;
casSupplier?: CASSupplier;
},
options?: {
initialValue?: Partial<Student>;
Expand All @@ -61,9 +71,13 @@ export async function saveFakeStudent(
const studentRepo = dataSource.getRepository(Student);
const student = await studentRepo.save(
relations?.student ??
createFakeStudent(relations?.user, {
initialValue: options?.initialValue,
}),
createFakeStudent(
relations?.user,
{ casSupplier: relations?.casSupplier },
{
initialValue: options?.initialValue,
},
),
);
// Saving SIN validation after student is saved due to cyclic dependency error.
student.sinValidation =
Expand Down
Loading
Loading