Skip to content

Commit fafd502

Browse files
#4013 - Legacy Restriction Mapping - Student Changes (#4049)
- Prevent API from retuning 'No effect' notifications since they are not supposed to be notified to the student. - Consolidated `LGCY_*` restrictions into a single `LGCY` restriction code to be returned to the student. The error code returned will be the highest one present in the list of the `LGCY_*` being returned. - Create few `LGCY_*` in the DB seed to allow the E2E to use them. - Fixed the `db:seed:test:clean` that was not removing the `create_history_entry` function leading to an error when the DB was populated again. - Close Nestjs app (`app.close()`) to avoid the `db:seed:test:clean` and `db:seed:test` to be hanging and waiting. ### Ministry visualization for the new legacy map ![image](https://github.com/user-attachments/assets/e50659f4-4cc1-4b9a-b6a6-de27e99c79b9) ### SQL to be manually added during release ```sql INSERT INTO sims.sfas_restriction_maps (legacy_code, code, is_legacy_only) VALUES ('TD', 'LGCY_TD', TRUE), ('B5', 'LGCY_B5', TRUE), ('B5A', 'LGCY_B5A', TRUE), ('B7', 'LGCY_B7', TRUE), ('SSD', 'LGCY_SSD', TRUE), ('Z2', 'LGCY_Z2', TRUE), ('M1', 'LGCY_M1', TRUE), ('SSRN', 'LGCY_SSRN', TRUE), ('B4', 'LGCY_B4', TRUE), ('Z1', 'LGCY_Z1', TRUE) ```
1 parent 39031e4 commit fafd502

File tree

15 files changed

+263
-7
lines changed

15 files changed

+263
-7
lines changed

sims.code-workspace

+5-3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@
113113
"golevelup",
114114
"Ilike",
115115
"lastupdated",
116+
"LGCY",
117+
"LGCYAAAA",
118+
"LGCYBBBB",
119+
"LGCYCCCC",
116120
"MBAL",
117121
"MSFAA",
118122
"NOAAPI",
@@ -134,9 +138,7 @@
134138
"timestamptz",
135139
"typeorm",
136140
"unparse",
137-
"Zeebe",
138-
"LGCY",
139-
"SFAS"
141+
"Zeebe"
140142
],
141143
"[json]": {
142144
"editor.defaultFormatter": "vscode.json-language-features"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { HttpStatus, INestApplication } from "@nestjs/common";
2+
import * as request from "supertest";
3+
import {
4+
BEARER_AUTH_TYPE,
5+
createTestingAppModule,
6+
FakeStudentUsersTypes,
7+
getStudentToken,
8+
mockUserLoginInfo,
9+
} from "../../../../testHelpers";
10+
import {
11+
createE2EDataSources,
12+
E2EDataSources,
13+
RestrictionCode,
14+
saveFakeStudent,
15+
saveFakeStudentRestriction,
16+
} from "@sims/test-utils";
17+
import { TestingModule } from "@nestjs/testing";
18+
import { In } from "typeorm";
19+
import { RestrictionNotificationType } from "@sims/sims-db";
20+
21+
describe("RestrictionStudentsController(e2e)-getStudentRestrictions", () => {
22+
let app: INestApplication;
23+
let db: E2EDataSources;
24+
let appModule: TestingModule;
25+
const endpoint = "/students/restriction";
26+
27+
beforeAll(async () => {
28+
const { nestApplication, module, dataSource } =
29+
await createTestingAppModule();
30+
app = nestApplication;
31+
db = createE2EDataSources(dataSource);
32+
appModule = module;
33+
});
34+
35+
it(`Should get the active student restrictions including legacy restrictions and skipping '${RestrictionNotificationType.NoEffect}' when the student has active student restrictions.`, async () => {
36+
// Arrange
37+
const student = await saveFakeStudent(db.dataSource);
38+
// Restrictions to be associated with the student.
39+
const restrictions = await db.restriction.find({
40+
select: {
41+
id: true,
42+
},
43+
where: {
44+
restrictionCode: In([
45+
// 'No effect' restrictions.
46+
RestrictionCode.LGCYAAAA,
47+
RestrictionCode.AF4,
48+
// Notification others than 'No effect' that should be returned.
49+
RestrictionCode.B6A,
50+
RestrictionCode.LGCYBBBB,
51+
RestrictionCode.LGCYCCCC,
52+
]),
53+
},
54+
});
55+
// Associate all restriction with the student.
56+
const savePromises = restrictions.map((restriction) =>
57+
saveFakeStudentRestriction(db.dataSource, {
58+
restriction,
59+
student,
60+
}),
61+
);
62+
await Promise.all(savePromises);
63+
64+
// Mock user service to return the saved student.
65+
await mockUserLoginInfo(appModule, student);
66+
67+
// Get any student user token.
68+
const studentToken = await getStudentToken(
69+
FakeStudentUsersTypes.FakeStudentUserType1,
70+
);
71+
72+
// Act/Assert
73+
await request(app.getHttpServer())
74+
.get(endpoint)
75+
.auth(studentToken, BEARER_AUTH_TYPE)
76+
.expect(HttpStatus.OK)
77+
.expect([
78+
{ code: RestrictionCode.B6A, type: RestrictionNotificationType.Error },
79+
{ code: RestrictionCode.LGCY, type: RestrictionNotificationType.Error },
80+
]);
81+
});
82+
83+
afterAll(async () => {
84+
await app?.close();
85+
});
86+
});

sources/packages/backend/apps/api/src/route-controllers/restriction/restriction.students.controller.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ import { StudentUserToken } from "../../auth/userToken.interface";
77
import { StudentRestrictionService } from "../../services";
88
import BaseController from "../BaseController";
99
import { StudentRestrictionAPIOutDTO } from "./models/restriction.dto";
10+
import { RestrictionNotificationType, StudentRestriction } from "@sims/sims-db";
11+
import { DEFAULT_LEGACY_RESTRICTION_CODE } from "@sims/services/constants";
12+
13+
/**
14+
* Restriction notifications priority order.
15+
* Priority 1 indicates the most important notification.
16+
*/
17+
const NOTIFICATION_PRIORITY_ORDER_MAP = {
18+
[RestrictionNotificationType.Error]: 1,
19+
[RestrictionNotificationType.Warning]: 2,
20+
};
1021

1122
/**
1223
* Controller for Student Restrictions.
@@ -34,12 +45,37 @@ export class RestrictionStudentsController extends BaseController {
3445
studentToken.studentId,
3546
{
3647
onlyActive: true,
48+
filterNoEffectRestrictions: true,
3749
},
3850
);
39-
40-
return studentRestrictions.map((studentRestriction) => ({
51+
// Separate the results between non-legacy and legacy restrictions.
52+
const nonLegacyRestrictions: StudentRestriction[] = [];
53+
const legacyRestrictions: StudentRestriction[] = [];
54+
studentRestrictions.forEach((studentRestriction) => {
55+
const resultList = studentRestriction.restriction.isLegacy
56+
? legacyRestrictions
57+
: nonLegacyRestrictions;
58+
resultList.push(studentRestriction);
59+
});
60+
// Create the output result for non-legacy restrictions.
61+
const results = nonLegacyRestrictions.map((studentRestriction) => ({
4162
code: studentRestriction.restriction.restrictionCode,
4263
type: studentRestriction.restriction.notificationType,
4364
}));
65+
if (legacyRestrictions.length) {
66+
// If any legacy restriction is present, create a generic LGCY restriction
67+
// with the highest notification type.
68+
studentRestrictions.sort(
69+
(a, b) =>
70+
NOTIFICATION_PRIORITY_ORDER_MAP[a.restriction.notificationType] -
71+
NOTIFICATION_PRIORITY_ORDER_MAP[b.restriction.notificationType],
72+
);
73+
const [legacyRestriction] = studentRestrictions;
74+
results.push({
75+
code: DEFAULT_LEGACY_RESTRICTION_CODE,
76+
type: legacyRestriction.restriction.notificationType,
77+
});
78+
}
79+
return results;
4480
}
4581
}

sources/packages/backend/apps/api/src/services/restriction/student-restriction.service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class StudentRestrictionService extends RecordDataModelService<StudentRes
6262
"restriction.restrictionCode",
6363
"restriction.description",
6464
"restriction.notificationType",
65+
"restriction.isLegacy",
6566
])
6667
.innerJoin("studentRestrictions.restriction", "restriction")
6768
.innerJoin("studentRestrictions.student", "student")

sources/packages/backend/apps/test-db-seeding/src/clean-db/clean-db.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export class CleanDatabase {
99
// Drops the database and all its data.It will erase all your database tables and their data.
1010
await this.dataSource.dropDatabase();
1111
await this.dataSource.query("DROP EXTENSION pg_trgm");
12+
await this.dataSource.query("DROP FUNCTION sims.create_history_entry");
1213
}
1314
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { RestrictionNotificationType, RestrictionType } from "@sims/sims-db";
2+
3+
export interface FakeRestriction {
4+
restrictionType: RestrictionType;
5+
isLegacy: boolean;
6+
restrictionCode: string;
7+
notificationType: RestrictionNotificationType;
8+
description: string;
9+
}
10+
11+
/**
12+
* Additional data for restrictions created besides the ones
13+
* already created by the regular DB migrations.
14+
*/
15+
export const RESTRICTIONS_ADDITIONAL_DATA: FakeRestriction[] = [
16+
{
17+
restrictionType: RestrictionType.Provincial,
18+
isLegacy: true,
19+
restrictionCode: "LGCY_AAAA",
20+
notificationType: RestrictionNotificationType.NoEffect,
21+
description: "Description for LGCY_AAAA",
22+
},
23+
{
24+
restrictionType: RestrictionType.Provincial,
25+
isLegacy: true,
26+
restrictionCode: "LGCY_BBBB",
27+
notificationType: RestrictionNotificationType.Warning,
28+
description: "Description for LGCY_BBBB",
29+
},
30+
{
31+
restrictionType: RestrictionType.Provincial,
32+
isLegacy: true,
33+
restrictionCode: "LGCY_CCCC",
34+
notificationType: RestrictionNotificationType.Error,
35+
description: "Description for LGCY_CCCC",
36+
},
37+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { InjectRepository } from "@nestjs/typeorm";
3+
import { Restriction } from "@sims/sims-db";
4+
import {
5+
DataSeed,
6+
DataSeedMethod,
7+
SeedPriorityOrder,
8+
} from "../../../seed-executors";
9+
import { Repository } from "typeorm";
10+
import { RESTRICTIONS_ADDITIONAL_DATA } from "./create-restrictions.model";
11+
import { createFakeRestriction } from "@sims/test-utils";
12+
13+
@Injectable()
14+
@DataSeed({ order: SeedPriorityOrder.Priority1 })
15+
export class CreateRestrictions {
16+
constructor(
17+
@InjectRepository(Restriction)
18+
private readonly restrictionRepo: Repository<Restriction>,
19+
) {}
20+
21+
/**
22+
* Seeds the database with additional fake restrictions.
23+
*/
24+
@DataSeedMethod()
25+
async createRestrictions(): Promise<void> {
26+
const additionalRestrictions = RESTRICTIONS_ADDITIONAL_DATA.map(
27+
(restrictionData) =>
28+
createFakeRestriction({ initialValues: { ...restrictionData } }),
29+
);
30+
await this.restrictionRepo.insert(additionalRestrictions);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./basic-seeding/create-restrictions";

sources/packages/backend/apps/test-db-seeding/src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const TEST_DB_NAME = "_TESTS";
3232
await app.get(CleanDatabase).cleanDatabase();
3333
console.info("Database cleaned.");
3434
}
35+
await app.close();
3536
return;
3637
}
3738

@@ -55,4 +56,5 @@ const TEST_DB_NAME = "_TESTS";
5556
.split(",");
5657
}
5758
await app.get(SeedExecutor).executeSeed(testClassList);
59+
await app.close();
5860
})();

sources/packages/backend/apps/test-db-seeding/src/test-db-seeding.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { ConfigModule } from "@sims/utilities/config";
1717
import { CreateAESTUsers } from "./db-seeding/aest";
1818
import { CreateStudentUsers } from "./db-seeding/student";
19+
import { CreateRestrictions } from "./db-seeding/restriction";
1920

2021
@Module({
2122
imports: [DatabaseModule, ConfigModule],
@@ -31,6 +32,7 @@ import { CreateStudentUsers } from "./db-seeding/student";
3132
CreateInstitutionsAndAuthenticationUsers,
3233
CreateAESTUsers,
3334
CreateStudentUsers,
35+
CreateRestrictions,
3436
],
3537
})
3638
export class TestDbSeedingModule {}

sources/packages/backend/libs/services/src/constants/restriction.constants.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
* Restriction code of provincial default restriction.
33
*/
44
export const PROVINCIAL_DEFAULT_RESTRICTION_CODE = "B2";
5+
/**
6+
* Default code for a legacy restriction.
7+
*/
8+
export const DEFAULT_LEGACY_RESTRICTION_CODE = "LGCY";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
Restriction,
3+
RestrictionActionType,
4+
RestrictionNotificationType,
5+
RestrictionType,
6+
} from "@sims/sims-db";
7+
import * as faker from "faker";
8+
9+
/**
10+
* Creates a fake restriction to be persisted.
11+
* @param options - options for creating the fake restriction.
12+
* - `initialValues`: initial values to override the default ones for the restriction fields.
13+
* @returns fake restriction to be persisted.
14+
*/
15+
export function createFakeRestriction(options?: {
16+
initialValues: Partial<Restriction>;
17+
}): Restriction {
18+
const restriction = new Restriction();
19+
restriction.restrictionType =
20+
options?.initialValues.restrictionType ?? RestrictionType.Provincial;
21+
restriction.restrictionCategory =
22+
options?.initialValues.restrictionCategory ?? "Other";
23+
restriction.restrictionCode =
24+
options?.initialValues.restrictionCode ??
25+
faker.random.alpha({ count: 10, upcase: true });
26+
restriction.description =
27+
options?.initialValues.description ?? faker.random.words(2);
28+
restriction.actionType = options?.initialValues.actionType ?? [
29+
RestrictionActionType.NoEffect,
30+
];
31+
restriction.notificationType =
32+
options?.initialValues.notificationType ??
33+
RestrictionNotificationType.NoEffect;
34+
restriction.isLegacy = options?.initialValues.isLegacy ?? false;
35+
return restriction;
36+
}

sources/packages/backend/libs/test-utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ export * from "./factories/supporting-user";
3737
export * from "./factories/application-restriction-bypass";
3838
export * from "./factories/cas-supplier";
3939
export * from "./factories/sfas-restriction-maps";
40+
export * from "./factories/restriction";

sources/packages/backend/libs/test-utils/src/models/common.model.ts

+15
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,19 @@ export enum RestrictionCode {
100100
* B6A restriction.
101101
*/
102102
B6A = "B6A",
103+
/**
104+
* Legacy restriction added during DB seeding.
105+
* Notification type as no effect.
106+
*/
107+
LGCYAAAA = "LGCY_AAAA",
108+
/**
109+
* Legacy restriction added during DB seeding.
110+
* Notification type as warning.
111+
*/
112+
LGCYBBBB = "LGCY_BBBB",
113+
/**
114+
* Legacy restriction added during DB seeding.
115+
* Notification type as error.
116+
*/
117+
LGCYCCCC = "LGCY_CCCC",
103118
}

sources/packages/backend/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"test:watch": "jest --watch ",
2626
"test:cov": "cross-env ENVIRONMENT=test jest --coverage --forceExit",
2727
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
28-
"test:e2e:api": "npm run db:seed:test filter=CreateInstitutionsAndAuthenticationUsers,CreateAESTUsers,CreateStudentUsers && cross-env ENVIRONMENT=test jest --collect-coverage --verbose --config ./apps/api/test/jest-e2e.json --forceExit",
28+
"test:e2e:api": "npm run db:seed:test filter=CreateInstitutionsAndAuthenticationUsers,CreateAESTUsers,CreateStudentUsers,CreateRestrictions && cross-env ENVIRONMENT=test jest --collect-coverage --verbose --config ./apps/api/test/jest-e2e.json --forceExit",
2929
"test:e2e:api:local": "cross-env ENVIRONMENT=test TZ=UTC jest --config ./apps/api/test/jest-e2e.json --forceExit",
3030
"test:e2e:workers": "npm run migration:run && cross-env ENVIRONMENT=test jest --collect-coverage --verbose --config ./apps/workers/test/jest-e2e.json --forceExit",
3131
"test:e2e:workers:local": "cross-env ENVIRONMENT=test jest --collect-coverage --verbose --config ./apps/workers/test/jest-e2e.json --forceExit",
@@ -176,4 +176,4 @@
176176
"^@sims/auth(|/.*)$": "<rootDir>/libs/auth/src/$1"
177177
}
178178
}
179-
}
179+
}

0 commit comments

Comments
 (0)