some progress
This commit is contained in:
parent
c4d2cba4cb
commit
b8ff56847a
@ -3,7 +3,7 @@ services:
|
||||
mongodb:
|
||||
image: mongo:6
|
||||
container_name: mongo
|
||||
restart: unless-stopped
|
||||
restart: 'no'
|
||||
ports:
|
||||
- 27017:27017
|
||||
environment:
|
||||
|
8
src/decorators/token.decorator.ts
Normal file
8
src/decorators/token.decorator.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const Token = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const response = ctx.switchToHttp().getResponse();
|
||||
return response.locals.token as string;
|
||||
},
|
||||
);
|
20
src/guards/auth.guard.ts
Normal file
20
src/guards/auth.guard.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import type { Response, Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const response = context.switchToHttp().getResponse<Response>();
|
||||
const authHeader = request.header('Authorization');
|
||||
if (!authHeader) return true; // false;
|
||||
const [, token] = authHeader.split(' ');
|
||||
if (!token) return true; // false
|
||||
// Validate `token` JWT here
|
||||
response.locals.token = token;
|
||||
return true;
|
||||
}
|
||||
}
|
14
src/industry-change-application/dtos/list-query.dto.ts
Normal file
14
src/industry-change-application/dtos/list-query.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { ApplicationStatus } from 'src/enums/status.enum';
|
||||
|
||||
export class ListQueryDto {
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value.split(','))
|
||||
@IsEnum(ApplicationStatus, { each: true })
|
||||
readonly statuses?: ApplicationStatus[];
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
readonly residentSub: string;
|
||||
}
|
@ -1,43 +1,30 @@
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { Industry } from 'src/enums/industry.enum';
|
||||
import { RegulatoryElection } from 'src/enums/regulatory-election.enums';
|
||||
import { ApplicationStatus, ObjectStatus } from 'src/enums/status.enum';
|
||||
import { Resident } from 'src/resident/schemas/Resident.schema';
|
||||
|
||||
export class ApplicationInformationDto {
|
||||
@Exclude()
|
||||
_id: string;
|
||||
export class RegisterIndustryChangeApplicationDto {
|
||||
@IsString()
|
||||
residentSub: string;
|
||||
|
||||
willWorkInPhysicalJuristiction: string;
|
||||
@IsBoolean()
|
||||
willWorkInPhysicalJurisdiction: boolean;
|
||||
|
||||
@IsEnum(Industry)
|
||||
@ValidateIf((o) => o.willWorkInPhysicalJurisdiction === true)
|
||||
industry: Industry;
|
||||
|
||||
@IsEnum(RegulatoryElection)
|
||||
@ValidateIf((o) => o.willWorkInPhysicalJurisdiction === true)
|
||||
regulatoryElection: RegulatoryElection;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ValidateIf((o) => o.willWorkInPhysicalJurisdiction === true)
|
||||
regulatoryElectionSub: string;
|
||||
}
|
||||
|
||||
export class DecisionDto {
|
||||
@Exclude()
|
||||
_id: string;
|
||||
|
||||
decidedAt: Date;
|
||||
rejctionReason: string;
|
||||
}
|
||||
|
||||
export class GetRegisterIndustryChangeApplicationDto {
|
||||
@Expose({ name: 'id' })
|
||||
_id: string;
|
||||
|
||||
@Exclude()
|
||||
resident: Resident;
|
||||
|
||||
@Expose({ name: 'residentSub' })
|
||||
getResidentSub() {
|
||||
return this.resident.sub;
|
||||
}
|
||||
|
||||
current: ApplicationInformationDto;
|
||||
requested: ApplicationInformationDto;
|
||||
status: ApplicationStatus;
|
||||
submittedAt: Date;
|
||||
decision: DecisionDto;
|
||||
objectStatus: ObjectStatus;
|
||||
}
|
||||
|
@ -1,24 +1,61 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { GetRegisterIndustryChangeApplicationDto } from './dtos/register-industry-change-application.dto';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { Token } from 'src/decorators/token.decorator';
|
||||
import { AuthGuard } from 'src/guards/auth.guard';
|
||||
import { ListQueryDto } from './dtos/list-query.dto';
|
||||
import { RegisterIndustryChangeApplicationDto } from './dtos/register-industry-change-application.dto';
|
||||
import { IndustryChangeApplicationService } from './industry-change-application.service';
|
||||
|
||||
@Controller({
|
||||
path: '/resident-register/industry-change-applications',
|
||||
})
|
||||
@UseGuards(AuthGuard)
|
||||
export class IndustryChangeApplicationController {
|
||||
constructor(
|
||||
private readonly applicationsSerivce: IndustryChangeApplicationService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async getList() {
|
||||
const allApplications = await this.applicationsSerivce.getAll();
|
||||
const transformed = plainToInstance(
|
||||
GetRegisterIndustryChangeApplicationDto,
|
||||
allApplications,
|
||||
{ excludeExtraneousValues: true },
|
||||
async getList(
|
||||
@Query(new ValidationPipe({ transform: true })) queryOpts: ListQueryDto,
|
||||
) {
|
||||
const allApplications = await this.applicationsSerivce.getAll(queryOpts);
|
||||
return this.applicationsSerivce.makeReadable(allApplications);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getSingle(@Param('id') id: string) {
|
||||
const findInstance = await this.applicationsSerivce.getById(id);
|
||||
if (!findInstance) return null;
|
||||
return this.applicationsSerivce.makeReadable(findInstance);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: string, @Token() authToken: string) {
|
||||
return this.applicationsSerivce.makeReadable(
|
||||
await this.applicationsSerivce.markDeleted(id, authToken),
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() application: RegisterIndustryChangeApplicationDto,
|
||||
@Token() authToken: string,
|
||||
) {
|
||||
const newApplication = await this.applicationsSerivce.create(
|
||||
application,
|
||||
authToken,
|
||||
);
|
||||
|
||||
return this.applicationsSerivce.makeReadable(newApplication);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { ResidentModule } from 'src/resident/resident.module';
|
||||
import { IndustryChangeApplicationController } from './industry-change-application.controller';
|
||||
import { IndustryChangeApplicationService } from './industry-change-application.service';
|
||||
import { Decision, DecisionSchema } from './schemas/Decision.schema';
|
||||
@ -28,6 +29,7 @@ import {
|
||||
schema: IndustryChangeApplicationSchema,
|
||||
},
|
||||
]),
|
||||
ResidentModule,
|
||||
],
|
||||
controllers: [IndustryChangeApplicationController],
|
||||
providers: [IndustryChangeApplicationService],
|
||||
|
@ -1,19 +1,156 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import {
|
||||
ApplicationStatus,
|
||||
ObjectStatus,
|
||||
ResidentStatus,
|
||||
} from 'src/enums/status.enum';
|
||||
import { TypeOfRegistration } from 'src/enums/type-of-registration.enums';
|
||||
import { ResidentService } from 'src/resident/resident.service';
|
||||
import { takeMongoObject, equate, take } from 'src/utility';
|
||||
import { ListQueryDto } from './dtos/list-query.dto';
|
||||
import { RegisterIndustryChangeApplicationDto } from './dtos/register-industry-change-application.dto';
|
||||
import {
|
||||
IndustryChangeApplication,
|
||||
IndustryChangeApplicationDocument,
|
||||
} from './schemas/IndustryChangeApplication.schema';
|
||||
|
||||
const requestedFields = [
|
||||
'industry',
|
||||
'willWorkInPhysicalJurisdiction',
|
||||
'regulatoryElection',
|
||||
'regulatoryElectionSub',
|
||||
];
|
||||
|
||||
const fieldsToExpose = [
|
||||
'id',
|
||||
'residentSub',
|
||||
'current',
|
||||
'requested',
|
||||
'status',
|
||||
'submittedAt',
|
||||
'decision',
|
||||
'objectStatus',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class IndustryChangeApplicationService {
|
||||
constructor(
|
||||
@InjectModel(IndustryChangeApplication.name)
|
||||
private applicationModel: Model<IndustryChangeApplicationDocument>,
|
||||
private resident: ResidentService,
|
||||
) {}
|
||||
|
||||
async getAll() {
|
||||
return this.applicationModel.find();
|
||||
async getAll(options: ListQueryDto) {
|
||||
const getResident = await this.resident.getResidentBySub(
|
||||
options.residentSub,
|
||||
);
|
||||
|
||||
if (!getResident) {
|
||||
throw new NotFoundException('Resident not found');
|
||||
}
|
||||
|
||||
return this.applicationModel.find({
|
||||
residentSub: options.residentSub,
|
||||
status: {
|
||||
$in: options.statuses || Object.values(ApplicationStatus),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return this.applicationModel.findById(id);
|
||||
}
|
||||
|
||||
async create(data: RegisterIndustryChangeApplicationDto, token: string) {
|
||||
// This might be possible to turn into a custom validator for class-validator
|
||||
if (
|
||||
data.willWorkInPhysicalJurisdiction === false &&
|
||||
(!!data.industry ||
|
||||
!!data.regulatoryElection ||
|
||||
!!data.regulatoryElectionSub)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'industry, regulatoryElection and regulatoryElectionSub are not allowed when willWorkInPhysicalJurisdiction is false',
|
||||
);
|
||||
}
|
||||
|
||||
const getResident = await this.resident.getResidentBySub(data.residentSub);
|
||||
if (!getResident) {
|
||||
throw new BadRequestException('Resident not found');
|
||||
}
|
||||
|
||||
if (getResident.status !== ResidentStatus.ACTIVE) {
|
||||
throw new BadRequestException('Resident must be active!');
|
||||
}
|
||||
|
||||
if (
|
||||
![TypeOfRegistration.E_RESIDENCY, TypeOfRegistration.RESIDENCY].includes(
|
||||
getResident.typeOfRegistration,
|
||||
)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Resident must be either an E-resident or a resident',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
equate(data, getResident, requestedFields).length ===
|
||||
requestedFields.length
|
||||
) {
|
||||
throw new BadRequestException('Cannot request what is already the case.');
|
||||
}
|
||||
|
||||
const status =
|
||||
data.willWorkInPhysicalJurisdiction === true
|
||||
? ApplicationStatus.IN_REVIEW
|
||||
: ApplicationStatus.APPROVED;
|
||||
|
||||
const newApplication = new this.applicationModel({
|
||||
residentSub: getResident.sub,
|
||||
current: take(takeMongoObject(getResident), requestedFields),
|
||||
requested: take(data, requestedFields),
|
||||
status,
|
||||
submittedAt: new Date(),
|
||||
createdBy: token ?? 'no token provided for testing',
|
||||
});
|
||||
|
||||
return newApplication.save();
|
||||
}
|
||||
|
||||
async markDeleted(id: string, token: string) {
|
||||
const findApplication = await this.applicationModel.findById(id);
|
||||
|
||||
if (!findApplication) {
|
||||
throw new NotFoundException('Application was not found');
|
||||
}
|
||||
|
||||
if (findApplication.status !== ApplicationStatus.IN_REVIEW) {
|
||||
throw new BadRequestException(
|
||||
'Only applications which are currently in review can be deleted.',
|
||||
);
|
||||
}
|
||||
|
||||
if (findApplication.objectStatus === ObjectStatus.DELETED) {
|
||||
throw new BadRequestException('The object has already been deleted.');
|
||||
}
|
||||
|
||||
findApplication.objectStatus = ObjectStatus.DELETED;
|
||||
findApplication.updatedBy = token || 'no token provided for testing';
|
||||
return findApplication.save();
|
||||
}
|
||||
|
||||
// I wrote this because I could not for the life of me get class-transformer to
|
||||
// play along with mongo documents. I do not have experience with either, so this
|
||||
// was a last ditch effort.
|
||||
makeReadable(input: IndustryChangeApplication | IndustryChangeApplication[]) {
|
||||
return Array.isArray(input)
|
||||
? input.map((object) => take(takeMongoObject(object), fieldsToExpose))
|
||||
: take(takeMongoObject(input), fieldsToExpose);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import mongoose, { HydratedDocument } from 'mongoose';
|
||||
import { Resident } from 'src/resident/schemas/Resident.schema';
|
||||
import { HydratedDocument } from 'mongoose';
|
||||
import { Decision } from './Decision.schema';
|
||||
import { ApplicationStatus, ObjectStatus } from 'src/enums/status.enum';
|
||||
import { ICAInformation } from './ICAInformation.schema';
|
||||
@ -14,10 +13,8 @@ export type IndustryChangeApplicationDocument =
|
||||
export class IndustryChangeApplication {
|
||||
@Prop({
|
||||
required: true,
|
||||
ref: 'Resident',
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
})
|
||||
resident: Resident;
|
||||
residentSub: string;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
@ -58,6 +55,10 @@ export class IndustryChangeApplication {
|
||||
required: true,
|
||||
})
|
||||
objectStatus: ObjectStatus;
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const IndustryChangeApplicationSchema = SchemaFactory.createForClass(
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Resident, ResidentDocument } from './schemas/Resident.schema';
|
||||
import { ResidentAddress } from './schemas/ResidentAddress.schema';
|
||||
|
||||
@Injectable()
|
||||
export class ResidentService {
|
||||
@ -14,4 +13,8 @@ export class ResidentService {
|
||||
const createdResident = new this.residentModel(resident);
|
||||
return createdResident.save();
|
||||
}
|
||||
|
||||
public async getResidentBySub(sub: string): Promise<Resident> {
|
||||
return this.residentModel.findOne({ sub });
|
||||
}
|
||||
}
|
||||
|
17
src/utility/equate.ts
Normal file
17
src/utility/equate.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Equate keys of two objects
|
||||
* @param object1 First object
|
||||
* @param object2 Second object
|
||||
* @param keys Keys to equate
|
||||
* @returns Keys which are equal in both objects
|
||||
*/
|
||||
export default function equate<T, K>(
|
||||
object1: T,
|
||||
object2: K,
|
||||
keys: string[],
|
||||
): string[] {
|
||||
return keys.reduce<string[]>((list, current) => {
|
||||
if (object1[current] === object2[current]) return [...list, current];
|
||||
return list;
|
||||
}, []);
|
||||
}
|
3
src/utility/index.ts
Normal file
3
src/utility/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as take } from './take';
|
||||
export { default as equate } from './equate';
|
||||
export { default as takeMongoObject } from './take-mongo-object';
|
17
src/utility/take-mongo-object.ts
Normal file
17
src/utility/take-mongo-object.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Take the plain javascript object from a mongo document.
|
||||
* I wrote this because I could not for the life of me get class-transformer to
|
||||
* play along with mongo documents.
|
||||
* @param mongoObj Mongo database response
|
||||
* @returns Plain javascript object
|
||||
*/
|
||||
export default function takeMongoObject<T>(mongoObj: T): T {
|
||||
const dirty = (mongoObj as Record<string, unknown>)._doc as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
dirty.id = dirty._id.toString();
|
||||
delete dirty._id;
|
||||
delete dirty.__v;
|
||||
return dirty as T;
|
||||
}
|
9
src/utility/take.ts
Normal file
9
src/utility/take.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default function take<T>(object: T, keys: string[]): Partial<T> {
|
||||
if (!object) return null;
|
||||
return Object.keys(object).reduce<Partial<T>>((obj, field) => {
|
||||
if (keys.includes(field)) {
|
||||
obj[field] = object[field];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
0
src/validators/registration.validator.ts
Normal file
0
src/validators/registration.validator.ts
Normal file
Loading…
Reference in New Issue
Block a user