-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathstudent.aest.controller.ts
392 lines (377 loc) · 13.2 KB
/
student.aest.controller.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
Query,
Res,
UnprocessableEntityException,
UploadedFile,
UseInterceptors,
} from "@nestjs/common";
import {
ApiNotFoundResponse,
ApiTags,
ApiUnprocessableEntityResponse,
} from "@nestjs/swagger";
import {
SINValidationService,
StudentFileService,
StudentRestrictionService,
StudentService,
} from "../../services";
import { NotificationActionsService } from "@sims/services/notifications";
import { ClientTypeBaseRoute } from "../../types";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import {
AllowAuthorizedParty,
Groups,
Roles,
UserToken,
} from "../../auth/decorators";
import { UserGroups } from "../../auth/user-groups.enum";
import BaseController from "../BaseController";
import {
AESTFileUploadToStudentAPIInDTO,
StudentFileDetailsAPIOutDTO,
AESTStudentProfileAPIOutDTO,
StudentSearchAPIInDTO,
ApplicationSummaryAPIOutDTO,
CreateSINValidationAPIInDTO,
SearchStudentAPIOutDTO,
SINValidationsAPIOutDTO,
UniqueFileNameParamAPIInDTO,
UpdateSINValidationAPIInDTO,
} from "./models/student.dto";
import { Response } from "express";
import { FileInterceptor } from "@nestjs/platform-express";
import {
defaultFileFilter,
MAX_UPLOAD_FILES,
MAX_UPLOAD_PARTS,
MINISTRY_FILE_UPLOAD_GROUP_NAME,
uploadLimits,
} from "../../utilities";
import { CustomNamedError } from "@sims/utilities";
import { IUserToken } from "../../auth/userToken.interface";
import { StudentControllerService } from "..";
import { FileOriginType } from "@sims/sims-db";
import { FileCreateAPIOutDTO } from "../models/common.dto";
import {
ApplicationPaginationOptionsAPIInDTO,
PaginatedResultsAPIOutDTO,
} from "../models/pagination.dto";
import { PrimaryIdentifierAPIOutDTO } from "../models/primary.identifier.dto";
import {
SIN_VALIDATION_RECORD_INVALID_OPERATION,
SIN_VALIDATION_RECORD_NOT_FOUND,
} from "../../constants";
import { Role } from "../../auth/roles.enum";
import { EntityManager } from "typeorm";
/**
* Student controller for AEST Client.
*/
@AllowAuthorizedParty(AuthorizedParties.aest)
@Groups(UserGroups.AESTUser)
@Controller("student")
@ApiTags(`${ClientTypeBaseRoute.AEST}-student`)
export class StudentAESTController extends BaseController {
constructor(
private readonly fileService: StudentFileService,
private readonly studentService: StudentService,
private readonly studentControllerService: StudentControllerService,
private readonly notificationActionsService: NotificationActionsService,
private readonly studentRestrictionService: StudentRestrictionService,
private readonly sinValidationService: SINValidationService,
) {
super();
}
/**
* This controller returns all student documents uploaded
* by student uploader.
* @param studentId student id.
* @returns list of student documents.
*/
@Get(":studentId/documents")
async getAESTStudentFiles(
@Param("studentId", ParseIntPipe) studentId: number,
): Promise<StudentFileDetailsAPIOutDTO[]> {
return this.studentControllerService.getStudentUploadedFiles(studentId, {
extendedDetails: true,
}) as Promise<StudentFileDetailsAPIOutDTO[]>;
}
/**
* Gets a student file and writes it to the HTTP response.
* @param uniqueFileName unique file name (name+guid).
* @param response file content.
*/
@Get("files/:uniqueFileName")
@ApiNotFoundResponse({ description: "Requested file was not found." })
async getUploadedFile(
@Param() uniqueFileNameParam: UniqueFileNameParamAPIInDTO,
@Res() response: Response,
): Promise<void> {
await this.studentControllerService.writeFileToResponse(
response,
uniqueFileNameParam.uniqueFileName,
);
}
/**
* Allow files uploads to a particular student.
* @param userToken authentication token.
* @param studentId student to receive the uploaded file.
* @param file file content.
* @param uniqueFileName unique file name (name+guid).
* @param groupName friendly name to group files. Currently using
* the value from 'Directory' property from form.IO file component.
* @returns created file information.
*/
@Roles(Role.StudentUploadFile)
@Post(":studentId/files")
@ApiNotFoundResponse({ description: "Student was not found." })
@UseInterceptors(
FileInterceptor("file", {
limits: uploadLimits(MAX_UPLOAD_FILES, MAX_UPLOAD_PARTS),
fileFilter: defaultFileFilter,
}),
)
async uploadFile(
@UserToken() userToken: IUserToken,
@Param("studentId", ParseIntPipe) studentId: number,
@UploadedFile() file: Express.Multer.File,
@Body("uniqueFileName") uniqueFileName: string,
@Body("group") groupName: string,
): Promise<FileCreateAPIOutDTO> {
const studentExists = await this.studentService.studentExists(studentId);
if (!studentExists) {
throw new NotFoundException("Student was not found.");
}
return this.studentControllerService.uploadFile(
studentId,
file,
uniqueFileName,
groupName,
userToken.userId,
);
}
/**
* Saves the files submitted by the Ministry to the student.
* All the files uploaded are first saved as temporary file in
* the DB. When this endpoint is called, the temporary
* files (saved during the upload) are updated to its proper
* group and file origin.
* @param userToken user authentication.
* @param studentId student to have the file saved.
* @param payload list of files to be saved.
*/
@Roles(Role.StudentUploadFile)
@Patch(":studentId/save-uploaded-files")
@ApiNotFoundResponse({ description: "Student not found." })
async saveStudentUploadedFiles(
@UserToken() userToken: IUserToken,
@Param("studentId", ParseIntPipe) studentId: number,
@Body() payload: AESTFileUploadToStudentAPIInDTO,
): Promise<void> {
const student = await this.studentService.getStudentById(studentId);
if (!student) {
throw new NotFoundException("Student not found.");
}
// This method will be executed alongside with the transaction during the
// execution of the method updateStudentFiles.
const saveFileUploadNotification = (entityManager: EntityManager) =>
this.notificationActionsService.saveMinistryFileUploadNotification(
{
firstName: student.user.firstName,
lastName: student.user.lastName,
toAddress: student.user.email,
userId: student.user.id,
},
userToken.userId,
entityManager,
);
// Updates the previously temporary uploaded files.
await this.fileService.updateStudentFiles(
studentId,
userToken.userId,
payload.associatedFiles,
FileOriginType.Ministry,
MINISTRY_FILE_UPLOAD_GROUP_NAME,
{ saveFileUploadNotification },
);
}
/**
* Search students based on the search criteria.
* Returns a 200 HTTP status instead of 201 to indicate that the operation
* was completed with success but no resource was created.
* @param searchCriteria criteria to be used in the search.
* @returns searched student details.
*/
@Post("search")
@HttpCode(HttpStatus.OK)
async searchStudents(
@Body() searchCriteria: StudentSearchAPIInDTO,
): Promise<SearchStudentAPIOutDTO[]> {
const students = await this.studentService.searchStudent(searchCriteria);
return this.studentControllerService.transformStudentsToSearchStudentDetails(
students,
);
}
/**
* Get the student information that represents the profile.
* @param studentId student id to retrieve the data.
* @returns student profile details.
*/
@Get(":studentId")
@ApiNotFoundResponse({ description: "Student not found." })
async getStudentProfile(
@Param("studentId", ParseIntPipe) studentId: number,
): Promise<AESTStudentProfileAPIOutDTO> {
const [student, studentRestrictions] = await Promise.all([
this.studentControllerService.getStudentProfile(studentId, {
withSensitiveData: true,
}),
this.studentRestrictionService.getStudentRestrictionsById(studentId, {
onlyActive: true,
}),
]);
return {
...student,
hasRestriction: !!studentRestrictions.length,
};
}
/**
* Get the list of applications that belongs to a student on a summary view format.
* @param studentId student id to retrieve the application summary.
* @param pagination options to execute the pagination.
* @returns student application list with total count.
*/
@Get(":studentId/application-summary")
@ApiNotFoundResponse({ description: "Student does not exists." })
async getStudentApplicationSummary(
@Param("studentId", ParseIntPipe) studentId: number,
@Query() pagination: ApplicationPaginationOptionsAPIInDTO,
): Promise<PaginatedResultsAPIOutDTO<ApplicationSummaryAPIOutDTO>> {
const studentExists = await this.studentService.studentExists(studentId);
if (!studentExists) {
throw new NotFoundException("Student does not exists.");
}
return this.studentControllerService.getStudentApplicationSummary(
studentId,
pagination,
);
}
/**
* Get the SIN validations associated with the student user.
* @param studentId student to retrieve the SIN validations.
* @returns the history of SIN validations associated with the student user.
*/
@Get(":studentId/sin-validations")
@ApiNotFoundResponse({ description: "Student does not exists." })
async getStudentSINValidations(
@Param("studentId", ParseIntPipe) studentId: number,
): Promise<SINValidationsAPIOutDTO[]> {
const student = await this.studentService.getStudentById(studentId);
if (!student) {
throw new NotFoundException("Student does not exists.");
}
const sinValidations =
await this.sinValidationService.getSINValidationsByStudentId(student.id);
return sinValidations?.map((sinValidation) => ({
id: sinValidation.id,
sin: sinValidation.sin,
createdAt: sinValidation.createdAt,
isValidSIN: sinValidation.isValidSIN,
sinStatus: sinValidation.sinStatus,
validSINCheck: sinValidation.validSINCheck,
validBirthdateCheck: sinValidation.validBirthdateCheck,
validFirstNameCheck: sinValidation.validFirstNameCheck,
validLastNameCheck: sinValidation.validLastNameCheck,
validGenderCheck: sinValidation.validGenderCheck,
temporarySIN: sinValidation.temporarySIN,
sinExpiryDate: sinValidation.sinExpiryDate,
}));
}
/**
* Creates a new SIN validation entry associated with the student user.
* This entry will be updated in the student record as the one that represents
* the current state of the SIN validation.
* @param studentId student to have the SIN validation created.
* @returns newly created record id.
*/
@Roles(Role.StudentAddNewSIN)
@Post(":studentId/sin-validations")
@ApiNotFoundResponse({ description: "Student does not exists." })
async createStudentSINValidation(
@Param("studentId", ParseIntPipe) studentId: number,
@Body() payload: CreateSINValidationAPIInDTO,
@UserToken() userToken: IUserToken,
): Promise<PrimaryIdentifierAPIOutDTO> {
const studentExists = await this.studentService.studentExists(studentId);
if (!studentExists) {
throw new NotFoundException("Student does not exists.");
}
const createdSINValidation =
await this.sinValidationService.createSINValidation(
studentId,
payload.sin,
payload.skipValidations,
payload.noteDescription,
userToken.userId,
);
return { id: createdSINValidation.id };
}
/**
* Updates the SIN validation expiry date for temporary SIN.
* @param studentId student to have the SIN validation updated.
* @param sinValidationId SIN validation record to be updated.
* @param payload data to be updated.
*/
@Roles(Role.StudentAddSINExpiry)
@Patch(":studentId/sin-validations/:sinValidationId")
@ApiNotFoundResponse({
description:
"Student does not exists or SIN validation record not found or it does not belong to the student.",
})
@ApiUnprocessableEntityResponse({
description:
"Not a temporary SIN or the expiry date is already set and cannot be updated again.",
})
async updateStudentSINValidation(
@Param("studentId", ParseIntPipe) studentId: number,
@Param("sinValidationId", ParseIntPipe) sinValidationId: number,
@Body() payload: UpdateSINValidationAPIInDTO,
@UserToken() userToken: IUserToken,
): Promise<void> {
const studentExists = await this.studentService.studentExists(studentId);
if (!studentExists) {
throw new NotFoundException("Student does not exists.");
}
try {
await this.sinValidationService.updateSINValidation(
sinValidationId,
studentId,
payload.expiryDate,
payload.noteDescription,
userToken.userId,
);
} catch (error: unknown) {
if (error instanceof CustomNamedError) {
switch (error.name) {
case SIN_VALIDATION_RECORD_NOT_FOUND:
throw new NotFoundException(error.message);
case SIN_VALIDATION_RECORD_INVALID_OPERATION:
throw new UnprocessableEntityException(error.message);
default:
throw error;
}
}
throw error;
}
}
}