mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 21:59:40 +00:00
Compare commits
9 Commits
feat/forge
...
feat/tasks
Author | SHA1 | Date | |
---|---|---|---|
35b434bc3d | |||
749ee5457f | |||
d539073f29 | |||
66e1bb0f28 | |||
577f91b796 | |||
7ed37c30e1 | |||
c2f63ccc72 | |||
970a41c895 | |||
3fd29b3905 |
96
package-lock.json
generated
96
package-lock.json
generated
@ -45,9 +45,10 @@
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20"
|
||||
"typeorm": "^0.3.20",
|
||||
"typeorm-transactional": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@golevelup/ts-jest": "^0.6.0",
|
||||
@ -64,6 +65,7 @@
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"eslint": "^8.39.0",
|
||||
@ -2743,6 +2745,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cls-hooked": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/cls-hooked/-/cls-hooked-4.3.9.tgz",
|
||||
"integrity": "sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
@ -3111,6 +3122,13 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
|
||||
@ -3966,6 +3984,18 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-hook-jl": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz",
|
||||
"integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stack-chain": "^1.3.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^4.7 || >=6.9 || >=7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@ -5011,6 +5041,29 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cls-hooked": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz",
|
||||
"integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"async-hook-jl": "^1.7.6",
|
||||
"emitter-listener": "^1.0.1",
|
||||
"semver": "^5.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cls-hooked/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
@ -5796,6 +5849,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emitter-listener": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
|
||||
"integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"shimmer": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
||||
@ -14467,6 +14529,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shimmer": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
|
||||
"integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
@ -14648,6 +14716,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-chain": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz",
|
||||
"integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -15601,6 +15675,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm-transactional": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/typeorm-transactional/-/typeorm-transactional-0.5.0.tgz",
|
||||
"integrity": "sha512-53/CwnXpOIJnWU3oVCNbhHB95FwciKSGbY+m/Hw4e2dBM2c4toiOHwf4pmk83Ne7guznmDgVr/5IUfbp+JTPCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cls-hooked": "^4.3.3",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"semver": "^7.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"reflect-metadata": ">= 0.1.12",
|
||||
"typeorm": ">= 0.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
@ -62,9 +62,10 @@
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20"
|
||||
"typeorm": "^0.3.20",
|
||||
"typeorm-transactional": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@golevelup/ts-jest": "^0.6.0",
|
||||
@ -81,6 +82,7 @@
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"eslint": "^8.39.0",
|
||||
|
@ -4,7 +4,10 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { addTransactionalDataSource } from 'typeorm-transactional';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { LookupModule } from './common/modules/lookup/lookup.module';
|
||||
import { OtpModule } from './common/modules/otp/otp.module';
|
||||
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
||||
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
||||
@ -13,7 +16,10 @@ import { buildValidationPipe } from './core/pipes';
|
||||
import { CustomerModule } from './customer/customer.module';
|
||||
import { migrations } from './db';
|
||||
import { DocumentModule } from './document/document.module';
|
||||
import { GuardianModule } from './guardian/guardian.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { JuniorModule } from './junior/junior.module';
|
||||
import { TaskModule } from './task/task.module';
|
||||
@Module({
|
||||
controllers: [],
|
||||
imports: [
|
||||
@ -21,7 +27,18 @@ import { HealthModule } from './health/health.module';
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => buildTypeormOptions(config, migrations),
|
||||
useFactory: (config: ConfigService) => {
|
||||
return buildTypeormOptions(config, migrations);
|
||||
},
|
||||
/* eslint-disable require-await */
|
||||
async dataSourceFactory(options) {
|
||||
if (!options) {
|
||||
throw new Error('Invalid options passed');
|
||||
}
|
||||
|
||||
return addTransactionalDataSource(new DataSource(options));
|
||||
},
|
||||
/* eslint-enable require-await */
|
||||
}),
|
||||
LoggerModule.forRootAsync({
|
||||
useFactory: (config: ConfigService) => buildLoggerOptions(config),
|
||||
@ -31,9 +48,14 @@ import { HealthModule } from './health/health.module';
|
||||
// App modules
|
||||
AuthModule,
|
||||
CustomerModule,
|
||||
DocumentModule,
|
||||
HealthModule,
|
||||
JuniorModule,
|
||||
TaskModule,
|
||||
GuardianModule,
|
||||
OtpModule,
|
||||
DocumentModule,
|
||||
LookupModule,
|
||||
|
||||
HealthModule,
|
||||
],
|
||||
providers: [
|
||||
// Global Pipes
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { AuthController } from './controllers';
|
||||
import { Device, User, UserNotificationSettings } from './entities';
|
||||
import { DeviceRepository, UserRepository } from './repositories';
|
||||
@ -9,7 +10,11 @@ import { UserService } from './services/user.service';
|
||||
import { AccessTokenStrategy } from './strategies';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, UserNotificationSettings, Device]), JwtModule.register({})],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, UserNotificationSettings, Device]),
|
||||
JwtModule.register({}),
|
||||
forwardRef(() => CustomerModule),
|
||||
],
|
||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [UserService],
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Roles } from '../enums';
|
||||
|
||||
export interface IJwtPayload {
|
||||
sub: string;
|
||||
roles: string[];
|
||||
roles: Roles[];
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { User, UserNotificationSettings } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
@ -20,7 +19,7 @@ export class UserRepository {
|
||||
);
|
||||
}
|
||||
|
||||
findOne(where: FindOptionsWhere<User>) {
|
||||
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
return this.userRepository.findOne({ where });
|
||||
}
|
||||
|
||||
@ -29,13 +28,16 @@ export class UserRepository {
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
verifyUserAndCreateCustomer(user: User) {
|
||||
user.customer = Customer.create({ ...user.customer, id: user.id, isGuardian: true });
|
||||
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
update(userId: string, data: Partial<User>) {
|
||||
return this.userRepository.update(userId, data);
|
||||
}
|
||||
|
||||
createUser(data: Partial<User>) {
|
||||
const user = this.userRepository.create({
|
||||
...data,
|
||||
notificationSettings: UserNotificationSettings.create(),
|
||||
});
|
||||
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
|
||||
import { User } from '../entities';
|
||||
import { Roles } from '../enums';
|
||||
@ -8,7 +10,10 @@ import { UserRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
|
||||
) {}
|
||||
|
||||
async updateNotificationSettings(userId: string, body: UpdateNotificationsSettingsRequestDto) {
|
||||
const user = await this.findUserOrThrow({ id: userId });
|
||||
@ -19,7 +24,7 @@ export class UserService {
|
||||
return notificationSettings;
|
||||
}
|
||||
|
||||
findUser(where: FindOptionsWhere<User>) {
|
||||
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
return this.userRepository.findOne(where);
|
||||
}
|
||||
|
||||
@ -44,12 +49,19 @@ export class UserService {
|
||||
}
|
||||
|
||||
if (user && user.roles.includes(Roles.JUNIOR)) {
|
||||
throw new BadRequestException('USERS.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
|
||||
//TODO add role Guardian to the existing user and send OTP
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(data: Partial<User>) {
|
||||
const user = await this.userRepository.createUser(data);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
setEmail(userId: string, email: string) {
|
||||
return this.userRepository.update(userId, { email });
|
||||
}
|
||||
@ -58,7 +70,14 @@ export class UserService {
|
||||
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
|
||||
}
|
||||
|
||||
verifyUserAndCreateCustomer(user: User) {
|
||||
return this.userRepository.verifyUserAndCreateCustomer(user);
|
||||
async verifyUserAndCreateCustomer(user: User) {
|
||||
await this.customerService.createCustomer(
|
||||
{
|
||||
guardian: Guardian.create({ id: user.id }),
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return this.findUserOrThrow({ id: user.id });
|
||||
}
|
||||
}
|
||||
|
5
src/common/decorators/allowed-roles.decorator.ts
Normal file
5
src/common/decorators/allowed-roles.decorator.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Roles } from '~/auth/enums';
|
||||
|
||||
export const ROLE_METADATA_KEY = 'roles';
|
||||
export const AllowedRoles = (...roles: Roles[]) => SetMetadata(ROLE_METADATA_KEY, roles);
|
@ -1,2 +1,3 @@
|
||||
export * from './allowed-roles.decorator';
|
||||
export * from './public.decorator';
|
||||
export * from './user.decorator';
|
||||
|
@ -6,7 +6,7 @@ import { IS_PUBLIC_KEY } from '../decorators';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenGuard extends AuthGuard('access-token') {
|
||||
constructor(private reflector: Reflector) {
|
||||
constructor(protected reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './access-token.guard';
|
||||
export * from './roles-guard';
|
||||
|
28
src/common/guards/roles-guard.ts
Normal file
28
src/common/guards/roles-guard.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { ROLE_METADATA_KEY } from '../decorators';
|
||||
import { AccessTokenGuard } from './access-token.guard';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard extends AccessTokenGuard {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
await super.canActivate(context);
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
const allowedRoles = this.reflector.getAllAndOverride<Roles[]>(ROLE_METADATA_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!allowedRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return allowedRoles.some((role) => user.roles.includes(role));
|
||||
}
|
||||
}
|
1
src/common/modules/lookup/controllers/index.ts
Normal file
1
src/common/modules/lookup/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lookup.controller';
|
23
src/common/modules/lookup/controllers/lookup.controller.ts
Normal file
23
src/common/modules/lookup/controllers/lookup.controller.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ApiDataArrayResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { LookupService } from '../services';
|
||||
|
||||
@Controller('lookup')
|
||||
@ApiTags('Lookups')
|
||||
@ApiBearerAuth()
|
||||
export class LookupController {
|
||||
constructor(private readonly lookupService: LookupService) {}
|
||||
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@Get('default-avatars')
|
||||
@ApiDataArrayResponse(DocumentMetaResponseDto)
|
||||
async findDefaultAvatars() {
|
||||
const avatars = await this.lookupService.findDefaultAvatar();
|
||||
|
||||
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
|
||||
}
|
||||
}
|
11
src/common/modules/lookup/lookup.module.ts
Normal file
11
src/common/modules/lookup/lookup.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DocumentModule } from '~/document/document.module';
|
||||
import { LookupController } from './controllers';
|
||||
import { LookupService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [LookupController],
|
||||
providers: [LookupService],
|
||||
imports: [DocumentModule],
|
||||
})
|
||||
export class LookupModule {}
|
1
src/common/modules/lookup/services/index.ts
Normal file
1
src/common/modules/lookup/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lookup.service';
|
11
src/common/modules/lookup/services/lookup.service.ts
Normal file
11
src/common/modules/lookup/services/lookup.service.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DocumentType } from '~/document/enums';
|
||||
import { DocumentService } from '~/document/services';
|
||||
|
||||
@Injectable()
|
||||
export class LookupService {
|
||||
constructor(private readonly documentService: DocumentService) {}
|
||||
findDefaultAvatar() {
|
||||
return this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Patch, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
|
||||
@ -11,11 +12,12 @@ import { CustomerService } from '../services';
|
||||
@Controller('customers')
|
||||
@ApiTags('Customers')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AccessTokenGuard)
|
||||
export class CustomerController {
|
||||
constructor(private readonly customerService: CustomerService) {}
|
||||
|
||||
@Patch('')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
|
||||
const customer = await this.customerService.updateCustomer(sub, body);
|
||||
|
||||
@ -23,6 +25,7 @@ export class CustomerController {
|
||||
}
|
||||
|
||||
@Patch('settings/notifications')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async updateNotificationSettings(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Body() body: UpdateNotificationsSettingsRequestDto,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '~/auth/auth.module';
|
||||
import { CustomerController } from './controllers';
|
||||
@ -7,8 +7,9 @@ import { CustomerRepository } from './repositories/customer.repository';
|
||||
import { CustomerService } from './services';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Customer]), AuthModule],
|
||||
imports: [TypeOrmModule.forFeature([Customer]), forwardRef(() => AuthModule)],
|
||||
controllers: [CustomerController],
|
||||
providers: [CustomerService, CustomerRepository],
|
||||
exports: [CustomerService],
|
||||
})
|
||||
export class CustomerModule {}
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
|
||||
@Entity()
|
||||
export class Customer extends BaseEntity {
|
||||
@ -67,6 +69,12 @@ export class Customer extends BaseEntity {
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
|
||||
@OneToOne(() => Junior, (junior) => junior.customer, { cascade: true })
|
||||
junior!: Junior;
|
||||
|
||||
@OneToOne(() => Guardian, (guardian) => guardian.customer, { cascade: true })
|
||||
guardian!: Guardian;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { UpdateCustomerRequestDto } from '../dtos/request';
|
||||
import { Customer } from '../entities';
|
||||
|
||||
@ -15,4 +17,16 @@ export class CustomerRepository {
|
||||
findOne(where: FindOptionsWhere<Customer>) {
|
||||
return this.customerRepository.findOne({ where });
|
||||
}
|
||||
|
||||
createCustomer(customerData: Partial<Customer>, user: User) {
|
||||
return this.customerRepository.save(
|
||||
this.customerRepository.create({
|
||||
...customerData,
|
||||
id: user.id,
|
||||
user,
|
||||
isGuardian: user.roles.includes(Roles.GUARDIAN),
|
||||
isJunior: user.roles.includes(Roles.JUNIOR),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { User } from '~/auth/entities';
|
||||
import { UserService } from '~/auth/services/user.service';
|
||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { Customer } from '../entities';
|
||||
@ -6,7 +7,10 @@ import { CustomerRepository } from '../repositories/customer.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerService {
|
||||
constructor(private readonly userService: UserService, private readonly customerRepository: CustomerRepository) {}
|
||||
constructor(
|
||||
@Inject(forwardRef(() => UserService)) private readonly userService: UserService,
|
||||
private readonly customerRepository: CustomerRepository,
|
||||
) {}
|
||||
updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
|
||||
return this.userService.updateNotificationSettings(userId, data);
|
||||
}
|
||||
@ -16,6 +20,10 @@ export class CustomerService {
|
||||
return this.findCustomerById(userId);
|
||||
}
|
||||
|
||||
createCustomer(customerData: Partial<Customer>, user: User) {
|
||||
return this.customerRepository.createCustomer(customerData, user);
|
||||
}
|
||||
|
||||
async findCustomerById(id: string) {
|
||||
const customer = await this.customerRepository.findOne({ id });
|
||||
if (!customer) {
|
||||
|
37
src/db/migrations/1733731507261-create-junior-entity.ts
Normal file
37
src/db/migrations/1733731507261-create-junior-entity.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateJuniorEntity1733731507261 implements MigrationInterface {
|
||||
name = 'CreateJuniorEntity1733731507261';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "juniors"
|
||||
("id" uuid NOT NULL,
|
||||
"relationship" character varying(255) NOT NULL,
|
||||
"civil_id_front_id" uuid NOT NULL,
|
||||
"civil_id_back_id" uuid NOT NULL,
|
||||
"customer_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "REL_6a72e1a5758643737cc563b96c" UNIQUE ("civil_id_front_id"),
|
||||
CONSTRAINT "REL_4662c4433223c01fe69fc1382f" UNIQUE ("civil_id_back_id"),
|
||||
CONSTRAINT "REL_dfbf64ede1ff823a489902448a" UNIQUE ("customer_id"), CONSTRAINT "PK_2d273092322c1f8bf26296fa608" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_6a72e1a5758643737cc563b96c7" FOREIGN KEY ("civil_id_front_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_4662c4433223c01fe69fc1382f5" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_dfbf64ede1ff823a489902448a2"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_4662c4433223c01fe69fc1382f5"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_6a72e1a5758643737cc563b96c7"`);
|
||||
await queryRunner.query(`DROP TABLE "juniors"`);
|
||||
}
|
||||
}
|
30
src/db/migrations/1733732021622-create-guardian-entity.ts
Normal file
30
src/db/migrations/1733732021622-create-guardian-entity.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateGuardianEntity1733732021622 implements MigrationInterface {
|
||||
name = 'CreateGuardianEntity1733732021622';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "guardians"
|
||||
("id" uuid NOT NULL,
|
||||
"customer_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "REL_6c46a1b6af00e6457cb1b70f7e" UNIQUE ("customer_id"), CONSTRAINT "PK_3dcf02f3dc96a2c017106f280be" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" ADD "guardian_id" uuid NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "guardians" DROP CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_0b11aa56264184690e2220da4a0"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP COLUMN "guardian_id"`);
|
||||
await queryRunner.query(`DROP TABLE "guardians"`);
|
||||
}
|
||||
}
|
28
src/db/migrations/1733748083604-create-theme-entity.ts
Normal file
28
src/db/migrations/1733748083604-create-theme-entity.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateThemeEntity1733748083604 implements MigrationInterface {
|
||||
name = 'CreateThemeEntity1733748083604';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "themes"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"color" character varying(255) NOT NULL,
|
||||
"avatar_id" uuid, "junior_id" uuid NOT NULL,
|
||||
CONSTRAINT "REL_73fcb76399a308cdd2d431a8f2" UNIQUE ("junior_id"),
|
||||
CONSTRAINT "PK_ddbeaab913c18682e5c88155592" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "themes" ADD CONSTRAINT "FK_169b672cc28cc757e1f4464864d" FOREIGN KEY ("avatar_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "themes" ADD CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e"`);
|
||||
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_169b672cc28cc757e1f4464864d"`);
|
||||
await queryRunner.query(`DROP TABLE "themes"`);
|
||||
}
|
||||
}
|
73
src/db/migrations/1733750228289-seed-default-avatar.ts
Normal file
73
src/db/migrations/1733750228289-seed-default-avatar.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Document } from '../../document/entities';
|
||||
import { DocumentType } from '../../document/enums';
|
||||
const DEFAULT_AVATARS = [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'vacation',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'colors',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'astronaut',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'pet',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'disney',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'clothes',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'playstation',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'football',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'cars',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_AVATAR,
|
||||
},
|
||||
];
|
||||
export class SeedDefaultAvatar1733750228289 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.manager.getRepository(Document).save(DEFAULT_AVATARS);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await DEFAULT_AVATARS.forEach(async (avatar) => {
|
||||
await queryRunner.manager
|
||||
.getRepository(Document)
|
||||
.delete({ name: avatar.name, documentType: avatar.documentType });
|
||||
});
|
||||
}
|
||||
}
|
39
src/db/migrations/1733904556416-create-task-entities.ts
Normal file
39
src/db/migrations/1733904556416-create-task-entities.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateTaskEntities1733904556416 implements MigrationInterface {
|
||||
name = 'CreateTaskEntities1733904556416';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "task_submissions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "status" character varying NOT NULL, "submitted_at" TIMESTAMP WITH TIME ZONE NOT NULL, "task_id" uuid NOT NULL, "proof_of_completion_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_d6cfaee118a0300d652e28ee16" UNIQUE ("task_id"), CONSTRAINT "PK_8d19d6b5dd776e373113de50018" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "tasks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(255) NOT NULL, "description" character varying(255) NOT NULL, "reward_amount" numeric(12,3) NOT NULL, "image_id" uuid NOT NULL, "task_frequency" character varying NOT NULL, "start_date" date NOT NULL, "due_date" date NOT NULL, "is_proof_required" boolean NOT NULL, "assigned_to_id" uuid NOT NULL, "assigned_by_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8d12ff38fcc62aaba2cab748772" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_submissions" ADD CONSTRAINT "FK_d6cfaee118a0300d652e28ee166" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_submissions" ADD CONSTRAINT "FK_87876dfe440de7aafce216e9f58" FOREIGN KEY ("proof_of_completion_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_f1f00d41b1e95d0bbda2710a62b" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_9430f12c5a1604833f64595a57f" FOREIGN KEY ("assigned_to_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_3e08a7ca125a175cf899b09f71a" FOREIGN KEY ("assigned_by_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_3e08a7ca125a175cf899b09f71a"`);
|
||||
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_9430f12c5a1604833f64595a57f"`);
|
||||
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_f1f00d41b1e95d0bbda2710a62b"`);
|
||||
await queryRunner.query(`ALTER TABLE "task_submissions" DROP CONSTRAINT "FK_87876dfe440de7aafce216e9f58"`);
|
||||
await queryRunner.query(`ALTER TABLE "task_submissions" DROP CONSTRAINT "FK_d6cfaee118a0300d652e28ee166"`);
|
||||
await queryRunner.query(`DROP TABLE "tasks"`);
|
||||
await queryRunner.query(`DROP TABLE "task_submissions"`);
|
||||
}
|
||||
}
|
@ -4,3 +4,7 @@ export * from './1733209041336-create-otp-entity';
|
||||
export * from './1733231692252-create-notification-settings-table';
|
||||
export * from './1733298524771-create-customer-entity';
|
||||
export * from './1733314952318-create-device-entity';
|
||||
export * from './1733731507261-create-junior-entity';
|
||||
export * from './1733732021622-create-guardian-entity';
|
||||
export * from './1733748083604-create-theme-entity';
|
||||
export * from './1733750228289-seed-default-avatar';
|
||||
|
@ -3,4 +3,5 @@ import { DocumentType } from '../enums';
|
||||
export const BUCKETS: Record<DocumentType, string> = {
|
||||
[DocumentType.PROFILE_PICTURE]: 'profile-pictures',
|
||||
[DocumentType.PASSPORT]: 'passports',
|
||||
[DocumentType.DEFAULT_AVATAR]: 'avatars',
|
||||
};
|
||||
|
@ -9,5 +9,6 @@ import { DocumentService, OciService } from './services';
|
||||
imports: [TypeOrmModule.forFeature([Document])],
|
||||
controllers: [DocumentController],
|
||||
providers: [DocumentService, OciService, DocumentRepository],
|
||||
exports: [DocumentService],
|
||||
})
|
||||
export class DocumentModule {}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Column, Entity, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Junior, Theme } from '~/junior/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
|
||||
import { DocumentType } from '../enums';
|
||||
|
||||
@Entity('documents')
|
||||
@ -17,7 +20,22 @@ export class Document {
|
||||
documentType!: DocumentType;
|
||||
|
||||
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'CASCADE' })
|
||||
user!: User;
|
||||
user?: User;
|
||||
|
||||
@OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'CASCADE' })
|
||||
juniorCivilIdFront?: User;
|
||||
|
||||
@OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'CASCADE' })
|
||||
juniorCivilIdBack?: User;
|
||||
|
||||
@OneToMany(() => Theme, (theme) => theme.avatar)
|
||||
themes?: Theme[];
|
||||
|
||||
@OneToMany(() => Task, (task) => task.image)
|
||||
tasks?: Task[];
|
||||
|
||||
@OneToMany(() => TaskSubmission, (taskSubmission) => taskSubmission.proofOfCompletion)
|
||||
submissions?: TaskSubmission[];
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
@ -1,4 +1,5 @@
|
||||
export enum DocumentType {
|
||||
PROFILE_PICTURE = 'PROFILE_PICTURE',
|
||||
PASSPORT = 'PASSPORT',
|
||||
DEFAULT_AVATAR = 'DEFAULT_AVATAR',
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { UploadResponseDto } from '../dtos/response';
|
||||
import { Document } from '../entities';
|
||||
|
||||
@ -17,4 +17,8 @@ export class DocumentRepository {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findDocuments(where: FindOptionsWhere<Document>) {
|
||||
return this.documentRepository.find({ where });
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { UploadDocumentRequestDto } from '../dtos/request';
|
||||
import { Document } from '../entities';
|
||||
import { DocumentRepository } from '../repositories';
|
||||
import { OciService } from './oci.service';
|
||||
|
||||
@ -10,4 +12,8 @@ export class DocumentService {
|
||||
const uploadedFile = await this.ociService.uploadFile(file, uploadedDocumentRequest);
|
||||
return this.documentRepository.createDocument(uploadedFile);
|
||||
}
|
||||
|
||||
findDocuments(where: FindOptionsWhere<Document>) {
|
||||
return this.documentRepository.findDocuments(where);
|
||||
}
|
||||
}
|
||||
|
39
src/guardian/entities/guradian.entity.ts
Normal file
39
src/guardian/entities/guradian.entity.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
|
||||
@Entity('guardians')
|
||||
export class Guardian extends BaseEntity {
|
||||
@PrimaryColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column('uuid', { name: 'customer_id' })
|
||||
customerId!: string;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.guardian, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'customer_id' })
|
||||
customer!: Customer;
|
||||
|
||||
@OneToMany(() => Junior, (junior) => junior.guardian)
|
||||
juniors!: Junior[];
|
||||
|
||||
@OneToMany(() => Task, (task) => task.assignedBy)
|
||||
tasks?: Task[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
8
src/guardian/guardian.module.ts
Normal file
8
src/guardian/guardian.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Guardian } from './entities/guradian.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Guardian])],
|
||||
})
|
||||
export class GuardianModule {}
|
1
src/junior/controllers/index.ts
Normal file
1
src/junior/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './junior.controller';
|
69
src/junior/controllers/junior.controller.ts
Normal file
69
src/junior/controllers/junior.controller.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||
import { JuniorResponseDto, ThemeResponseDto } from '../dtos/response';
|
||||
import { JuniorService } from '../services';
|
||||
|
||||
@Controller('juniors')
|
||||
@ApiTags('Juniors')
|
||||
@ApiBearerAuth()
|
||||
export class JuniorController {
|
||||
constructor(private readonly juniorService: JuniorService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(JuniorResponseDto)
|
||||
async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) {
|
||||
const junior = await this.juniorService.createJuniors(body, user.sub);
|
||||
|
||||
return ResponseFactory.data(new JuniorResponseDto(junior));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataPageResponse(JuniorResponseDto)
|
||||
async findJuniors(@AuthenticatedUser() user: IJwtPayload, @Query() pageOptions: PageOptionsRequestDto) {
|
||||
const [juniors, count] = await this.juniorService.findJuniorsByGuardianId(user.sub, pageOptions);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
juniors.map((juniors) => new JuniorResponseDto(juniors)),
|
||||
{
|
||||
page: pageOptions.page,
|
||||
size: pageOptions.size,
|
||||
itemCount: count,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':juniorId')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(JuniorResponseDto)
|
||||
async findJuniorById(
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
||||
) {
|
||||
const junior = await this.juniorService.findJuniorById(juniorId, user.sub);
|
||||
|
||||
return ResponseFactory.data(new JuniorResponseDto(junior));
|
||||
}
|
||||
|
||||
@Post('set-theme')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@ApiDataResponse(JuniorResponseDto)
|
||||
async setTheme(@Body() body: SetThemeRequestDto, @AuthenticatedUser() user: IJwtPayload) {
|
||||
const theme = await this.juniorService.setTheme(body, user.sub);
|
||||
return ResponseFactory.data(new ThemeResponseDto(theme));
|
||||
}
|
||||
}
|
37
src/junior/dtos/request/create-junior-user.request.dto.ts
Normal file
37
src/junior/dtos/request/create-junior-user.request.dto.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDateString, IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
|
||||
import { IsValidPhoneNumber } from '~/core/decorators/validations';
|
||||
|
||||
export class CreateJuniorUserRequestDto {
|
||||
@ApiProperty({ example: '+962' })
|
||||
@Matches(COUNTRY_CODE_REGEX, {
|
||||
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
|
||||
})
|
||||
countryCode: string = '+966';
|
||||
|
||||
@ApiProperty({ example: '787259134' })
|
||||
@IsValidPhoneNumber({
|
||||
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
|
||||
})
|
||||
phoneNumber!: string;
|
||||
|
||||
@ApiProperty({ example: 'John' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.firstName' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.firstName' }) })
|
||||
firstName!: string;
|
||||
|
||||
@ApiProperty({ example: 'Doe' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.lastName' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.lastName' }) })
|
||||
lastName!: string;
|
||||
|
||||
@ApiProperty({ example: '2020-01-01' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||
dateOfBirth!: Date;
|
||||
|
||||
@ApiProperty({ example: 'test@test.com' })
|
||||
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||
email!: string;
|
||||
}
|
18
src/junior/dtos/request/create-junior.request.dto.ts
Normal file
18
src/junior/dtos/request/create-junior.request.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { Relationship } from '~/junior/enums';
|
||||
import { CreateJuniorUserRequestDto } from './create-junior-user.request.dto';
|
||||
export class CreateJuniorRequestDto extends CreateJuniorUserRequestDto {
|
||||
@ApiProperty({ example: Relationship.PARENT })
|
||||
@IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) })
|
||||
relationship!: Relationship;
|
||||
|
||||
@ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdFrontId' }) })
|
||||
civilIdFrontId!: string;
|
||||
|
||||
@ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdBackId' }) })
|
||||
civilIdBackId!: string;
|
||||
}
|
3
src/junior/dtos/request/index.ts
Normal file
3
src/junior/dtos/request/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './create-junior-user.request.dto';
|
||||
export * from './create-junior.request.dto';
|
||||
export * from './set-theme.request.dto';
|
13
src/junior/dtos/request/set-theme.request.dto.ts
Normal file
13
src/junior/dtos/request/set-theme.request.dto.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsUUID } from 'class-validator';
|
||||
import { ThemeColor } from '~/junior/enums';
|
||||
|
||||
export class SetThemeRequestDto {
|
||||
@ApiProperty({ example: ThemeColor.VIOLET })
|
||||
@IsEnum(ThemeColor)
|
||||
color!: ThemeColor;
|
||||
|
||||
@ApiProperty({ example: 'fbfre-4f4f-4f4f-4f4f' })
|
||||
@IsUUID()
|
||||
avatarId!: string;
|
||||
}
|
2
src/junior/dtos/response/index.ts
Normal file
2
src/junior/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './junior.response.dto';
|
||||
export * from './theme.response.dto';
|
24
src/junior/dtos/response/junior.response.dto.ts
Normal file
24
src/junior/dtos/response/junior.response.dto.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { Relationship } from '~/junior/enums';
|
||||
|
||||
export class JuniorResponseDto {
|
||||
@ApiProperty({ example: 'id' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: 'fullName' })
|
||||
fullName!: string;
|
||||
|
||||
@ApiProperty({ example: 'relationship' })
|
||||
relationship!: Relationship;
|
||||
|
||||
@ApiProperty({ example: 'profilePictureId' })
|
||||
profilePictureId: string | null;
|
||||
|
||||
constructor(junior: Junior) {
|
||||
this.id = junior.id;
|
||||
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
|
||||
this.relationship = junior.relationship;
|
||||
this.profilePictureId = junior.customer.user.profilePictureId;
|
||||
}
|
||||
}
|
26
src/junior/dtos/response/theme.response.dto.ts
Normal file
26
src/junior/dtos/response/theme.response.dto.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Theme } from '~/junior/entities';
|
||||
import { ThemeColor } from '~/junior/enums';
|
||||
|
||||
export class ThemeResponseDto {
|
||||
@ApiProperty({ example: '645fds-4f5s4f-4f5s4f-4f5s4f' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: ThemeColor.BLUE })
|
||||
color!: string;
|
||||
|
||||
@ApiProperty({ example: '645fds-4f5s4f-4f5s4f-4f5s4f' })
|
||||
avatarId!: string;
|
||||
|
||||
@ApiProperty({ example: '645fds-4f5s4f-4f5s4f-4f5s4f' })
|
||||
juniorId!: string;
|
||||
|
||||
constructor(theme: Theme | null) {
|
||||
if (theme) {
|
||||
this.id = theme.id;
|
||||
this.color = theme.color;
|
||||
this.avatarId = theme.avatarId;
|
||||
this.juniorId = theme.juniorId;
|
||||
}
|
||||
}
|
||||
}
|
2
src/junior/entities/index.ts
Normal file
2
src/junior/entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './junior.entity';
|
||||
export * from './theme.entity';
|
67
src/junior/entities/junior.entity.ts
Normal file
67
src/junior/entities/junior.entity.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Task } from '~/task/entities';
|
||||
import { Relationship } from '../enums';
|
||||
import { Theme } from './theme.entity';
|
||||
|
||||
@Entity('juniors')
|
||||
export class Junior extends BaseEntity {
|
||||
@PrimaryColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
relationship!: Relationship;
|
||||
|
||||
@Column('uuid', { name: 'civil_id_front_id' })
|
||||
civilIdFrontId!: string;
|
||||
|
||||
@Column('uuid', { name: 'civil_id_back_id' })
|
||||
civilIdBackId!: string;
|
||||
|
||||
@Column('uuid', { name: 'customer_id' })
|
||||
customerId!: string;
|
||||
|
||||
@Column('uuid', { name: 'guardian_id' })
|
||||
guardianId!: string;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.juniorCivilIdFront)
|
||||
@JoinColumn({ name: 'civil_id_front_id' })
|
||||
civilIdFront!: Document;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.juniorCivilIdBack)
|
||||
@JoinColumn({ name: 'civil_id_back_id' })
|
||||
civilIdBack!: Document;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.junior, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'customer_id' })
|
||||
customer!: Customer;
|
||||
|
||||
@OneToOne(() => Theme, (theme) => theme.junior, { cascade: true, nullable: true })
|
||||
theme!: Theme;
|
||||
|
||||
@ManyToOne(() => Guardian, (guardian) => guardian.juniors)
|
||||
@JoinColumn({ name: 'guardian_id' })
|
||||
guardian!: Guardian;
|
||||
|
||||
@OneToMany(() => Task, (task) => task.assignedTo)
|
||||
tasks?: Task[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
26
src/junior/entities/theme.entity.ts
Normal file
26
src/junior/entities/theme.entity.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Junior } from './junior.entity';
|
||||
|
||||
@Entity('themes')
|
||||
export class Theme extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'color' })
|
||||
color!: string;
|
||||
|
||||
@Column('uuid', { name: 'avatar_id', nullable: true })
|
||||
avatarId!: string;
|
||||
|
||||
@ManyToOne(() => Document, (document) => document.themes, { cascade: true, nullable: true })
|
||||
@JoinColumn({ name: 'avatar_id' })
|
||||
avatar!: Document;
|
||||
|
||||
@Column('uuid', { name: 'junior_id' })
|
||||
juniorId!: string;
|
||||
|
||||
@OneToOne(() => Junior, (junior) => junior.theme, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
}
|
2
src/junior/enums/index.ts
Normal file
2
src/junior/enums/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './relationship.enum';
|
||||
export * from './theme-color.enum';
|
4
src/junior/enums/relationship.enum.ts
Normal file
4
src/junior/enums/relationship.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum Relationship {
|
||||
PARENT = 'PARENT',
|
||||
GUARDIAN = 'GUARDIAN',
|
||||
}
|
5
src/junior/enums/theme-color.enum.ts
Normal file
5
src/junior/enums/theme-color.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum ThemeColor {
|
||||
BLUE = 'BLUE',
|
||||
GREEN = 'GREEN',
|
||||
VIOLET = 'VIOLET',
|
||||
}
|
15
src/junior/junior.module.ts
Normal file
15
src/junior/junior.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '~/auth/auth.module';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { JuniorController } from './controllers';
|
||||
import { Junior, Theme } from './entities';
|
||||
import { JuniorRepository } from './repositories';
|
||||
import { JuniorService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [JuniorController],
|
||||
providers: [JuniorService, JuniorRepository],
|
||||
imports: [TypeOrmModule.forFeature([Junior, Theme]), AuthModule, CustomerModule],
|
||||
})
|
||||
export class JuniorModule {}
|
1
src/junior/repositories/index.ts
Normal file
1
src/junior/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './junior.repository';
|
41
src/junior/repositories/junior.repository.ts
Normal file
41
src/junior/repositories/junior.repository.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { SetThemeRequestDto } from '../dtos/request';
|
||||
import { Theme } from '../entities';
|
||||
import { Junior } from '../entities/junior.entity';
|
||||
const FIRST_PAGE = 1;
|
||||
@Injectable()
|
||||
export class JuniorRepository {
|
||||
constructor(@InjectRepository(Junior) private juniorRepository: Repository<Junior>) {}
|
||||
|
||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||
return this.juniorRepository.findAndCount({
|
||||
where: { guardianId },
|
||||
relations: ['customer', 'customer.user'],
|
||||
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
||||
take: pageOptions.size,
|
||||
});
|
||||
}
|
||||
|
||||
findJuniorById(juniorId: string, guardianId?: string) {
|
||||
return this.juniorRepository.findOne({
|
||||
where: { id: juniorId, guardianId },
|
||||
relations: ['customer', 'customer.user', 'theme'],
|
||||
});
|
||||
}
|
||||
|
||||
setTheme(body: SetThemeRequestDto, junior: Junior) {
|
||||
junior.theme = Theme.create({ ...body, junior });
|
||||
return this.juniorRepository.save(junior);
|
||||
}
|
||||
|
||||
removeTheme(theme: Theme) {
|
||||
return this.juniorRepository.manager.remove(theme);
|
||||
}
|
||||
|
||||
findThemeForJunior(juniorId: string) {
|
||||
return this.juniorRepository.manager.findOne(Theme, { where: { juniorId }, relations: ['avatar'] });
|
||||
}
|
||||
}
|
1
src/junior/services/index.ts
Normal file
1
src/junior/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './junior.service';
|
75
src/junior/services/junior.service.ts
Normal file
75
src/junior/services/junior.service.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { UserService } from '~/auth/services';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||
import { Junior } from '../entities';
|
||||
import { JuniorRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class JuniorService {
|
||||
constructor(
|
||||
private readonly juniorRepository: JuniorRepository,
|
||||
private readonly userService: UserService,
|
||||
private readonly customerService: CustomerService,
|
||||
) {}
|
||||
|
||||
@Transactional()
|
||||
async createJuniors(body: CreateJuniorRequestDto, guardianId: string) {
|
||||
const existingUser = await this.userService.findUser([{ email: body.email }, { phoneNumber: body.phoneNumber }]);
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('USER.ALREADY_EXISTS');
|
||||
}
|
||||
|
||||
const user = await this.userService.createUser({
|
||||
email: body.email,
|
||||
countryCode: body.countryCode,
|
||||
phoneNumber: body.phoneNumber,
|
||||
roles: [Roles.JUNIOR],
|
||||
});
|
||||
|
||||
await this.customerService.createCustomer(
|
||||
{
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
dateOfBirth: body.dateOfBirth,
|
||||
junior: Junior.create({
|
||||
id: user.id,
|
||||
guardianId,
|
||||
relationship: body.relationship,
|
||||
civilIdFrontId: body.civilIdFrontId,
|
||||
civilIdBackId: body.civilIdBackId,
|
||||
}),
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return this.findJuniorById(user.id, guardianId);
|
||||
}
|
||||
|
||||
async findJuniorById(juniorId: string, guardianId?: string) {
|
||||
const junior = await this.juniorRepository.findJuniorById(juniorId, guardianId);
|
||||
|
||||
if (!junior) {
|
||||
throw new BadRequestException('JUNIOR.NOT_FOUND');
|
||||
}
|
||||
return junior;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async setTheme(body: SetThemeRequestDto, juniorId: string) {
|
||||
const junior = await this.findJuniorById(juniorId);
|
||||
if (junior.theme) {
|
||||
await this.juniorRepository.removeTheme(junior.theme);
|
||||
}
|
||||
await this.juniorRepository.setTheme(body, junior);
|
||||
return this.juniorRepository.findThemeForJunior(juniorId);
|
||||
}
|
||||
|
||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||
return this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions);
|
||||
}
|
||||
}
|
@ -3,12 +3,20 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { Logger } from 'nestjs-pino';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
import { AppModule } from './app.module';
|
||||
const DEFAULT_PORT = 3000;
|
||||
async function bootstrap() {
|
||||
initializeTransactionalContext();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.useLogger(app.get(Logger));
|
||||
app.enableCors({
|
||||
origin: '*',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204,
|
||||
});
|
||||
const config = app.get(ConfigService);
|
||||
const swaggerDocument = await createSwagger(app);
|
||||
|
||||
|
1
src/task/controllers/index.ts
Normal file
1
src/task/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './task.controller';
|
75
src/task/controllers/task.controller.ts
Normal file
75
src/task/controllers/task.controller.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateTaskRequestDto, TaskSubmissionRequestDto } from '../dtos/request';
|
||||
import { TasksFilterOptions } from '../dtos/request/tasks-filter-options.request.dto';
|
||||
import { TaskResponseDto } from '../dtos/response';
|
||||
import { TaskService } from '../services';
|
||||
|
||||
@Controller('tasks')
|
||||
@ApiTags('Tasks')
|
||||
@ApiBearerAuth()
|
||||
export class TaskController {
|
||||
constructor(private readonly taskService: TaskService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
async createTask(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateTaskRequestDto) {
|
||||
const task = await this.taskService.createTask(sub, body);
|
||||
return ResponseFactory.data(new TaskResponseDto(task));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async findTasks(@Query() query: TasksFilterOptions, @AuthenticatedUser() user: IJwtPayload) {
|
||||
const [tasks, itemCount] = await this.taskService.findTasks(user, query);
|
||||
return ResponseFactory.dataPage(
|
||||
tasks.map((task) => new TaskResponseDto(task)),
|
||||
{
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
itemCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':taskId/submit')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async submitTask(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('taskId', CustomParseUUIDPipe) taskId: string,
|
||||
@Body() body: TaskSubmissionRequestDto,
|
||||
) {
|
||||
await this.taskService.submitTask(sub, taskId, body);
|
||||
}
|
||||
|
||||
@Patch(':taskId/approve')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async approveTaskSubmission(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('taskId', CustomParseUUIDPipe) taskId: string,
|
||||
) {
|
||||
await this.taskService.approveTaskSubmission(sub, taskId);
|
||||
}
|
||||
|
||||
@Patch(':taskId/reject')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async rejectTaskSubmission(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('taskId', CustomParseUUIDPipe) taskId: string,
|
||||
) {
|
||||
await this.taskService.rejectTaskSubmission(sub, taskId);
|
||||
}
|
||||
}
|
67
src/task/dtos/request/create-task.request.dto.ts
Normal file
67
src/task/dtos/request/create-task.request.dto.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { TaskFrequency } from '~/task/enums/task.frequency.enum';
|
||||
const TEXT_LENGTH = 255;
|
||||
const MAX_DECIMAL_PLACES = 3;
|
||||
export class CreateTaskRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'task.title' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'task.title' }) })
|
||||
@MaxLength(TEXT_LENGTH, {
|
||||
message: i18n('validation.MaxLength', { path: 'general', property: 'task.title', length: TEXT_LENGTH }),
|
||||
})
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'task.description' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'task.description' }) })
|
||||
@MaxLength(TEXT_LENGTH, {
|
||||
message: i18n('validation.MaxLength', { path: 'general', property: 'task.description', length: TEXT_LENGTH }),
|
||||
})
|
||||
@IsOptional()
|
||||
description!: string;
|
||||
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'task.startDate' }) })
|
||||
@IsOptional()
|
||||
startDate: string = new Date().toISOString();
|
||||
|
||||
@ApiProperty({ example: '2024-01-15' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'task.dueDate' }) })
|
||||
dueDate!: string;
|
||||
|
||||
@ApiProperty({ example: 100 })
|
||||
@IsNumber(
|
||||
{ maxDecimalPlaces: MAX_DECIMAL_PLACES },
|
||||
{ message: i18n('validation.IsNumber', { path: 'general', property: 'task.rewardAmount' }) },
|
||||
)
|
||||
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'task.rewardAmount' }) })
|
||||
rewardAmount!: number;
|
||||
|
||||
@IsEnum(TaskFrequency, { message: i18n('validation.IsEnum', { path: 'general', property: 'task.frequency' }) })
|
||||
@IsOptional()
|
||||
frequency: TaskFrequency = TaskFrequency.ONE_TIME;
|
||||
|
||||
@ApiProperty({ example: false })
|
||||
@IsBoolean({ message: i18n('validation.IsBoolean', { path: 'general', property: 'task.isProofRequired' }) })
|
||||
isProofRequired!: boolean;
|
||||
|
||||
@ApiProperty({ example: 'e7b1b3b1-4b3b-4b3b-4b3b-4b3b4b3b4b3b' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'task.imageId' }) })
|
||||
imageId!: string;
|
||||
|
||||
@ApiProperty({ example: 'e7b1b3b1-4b3b-4b3b-4b3b-4b3b4b3b4b3b' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'task.juniorId' }) })
|
||||
juniorId!: string;
|
||||
}
|
3
src/task/dtos/request/index.ts
Normal file
3
src/task/dtos/request/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './create-task.request.dto';
|
||||
export * from './task-submission.request.dto';
|
||||
export * from './tasks-filter-options.request.dto';
|
9
src/task/dtos/request/task-submission.request.dto.ts
Normal file
9
src/task/dtos/request/task-submission.request.dto.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class TaskSubmissionRequestDto {
|
||||
@ApiPropertyOptional({ example: '4dsf3-4dsf3-4dsf3-4dsf3', description: 'The Proof id of the task submission' })
|
||||
@IsUUID('4', { message: i18n('validation.isUUID', { path: 'general', property: 'task.imageId' }) })
|
||||
@IsOptional()
|
||||
imageId!: string;
|
||||
}
|
17
src/task/dtos/request/tasks-filter-options.request.dto.ts
Normal file
17
src/task/dtos/request/tasks-filter-options.request.dto.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { TaskStatus } from '~/task/enums';
|
||||
export class TasksFilterOptions extends PageOptionsRequestDto {
|
||||
@ApiProperty({ enum: TaskStatus, required: true })
|
||||
@IsEnum(TaskStatus, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'task.status' }),
|
||||
})
|
||||
status?: TaskStatus;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'task.juniorId' }) })
|
||||
@IsOptional()
|
||||
juniorId?: string;
|
||||
}
|
1
src/task/dtos/response/index.ts
Normal file
1
src/task/dtos/response/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './task.response.dto';
|
49
src/task/dtos/response/task.response.dto.ts
Normal file
49
src/task/dtos/response/task.response.dto.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||
import { Task } from '~/task/entities';
|
||||
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
|
||||
|
||||
export class TaskResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@ApiProperty()
|
||||
status!: string;
|
||||
|
||||
@ApiProperty()
|
||||
dueDate!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
rewardAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
submission?: TaskSubmission;
|
||||
|
||||
@ApiProperty()
|
||||
junior!: JuniorResponseDto;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(task: Task) {
|
||||
this.id = task.id;
|
||||
this.title = task.title;
|
||||
this.description = task.description;
|
||||
this.status = task.status;
|
||||
this.dueDate = task.dueDate;
|
||||
this.rewardAmount = task.rewardAmount;
|
||||
this.submission = task.submission;
|
||||
this.junior = new JuniorResponseDto(task.assignedTo);
|
||||
this.createdAt = task.createdAt;
|
||||
this.updatedAt = task.updatedAt;
|
||||
}
|
||||
}
|
1
src/task/entities/index.ts
Normal file
1
src/task/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './task.entity';
|
46
src/task/entities/task-submissions.entity.ts
Normal file
46
src/task/entities/task-submissions.entity.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Document } from '~/document/entities';
|
||||
import { SubmissionStatus } from '../enums';
|
||||
import { Task } from './task.entity';
|
||||
|
||||
@Entity('task_submissions')
|
||||
export class TaskSubmission extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', name: 'status' })
|
||||
status!: SubmissionStatus;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'submitted_at' })
|
||||
submittedAt!: Date;
|
||||
|
||||
@Column({ type: 'uuid', name: 'task_id' })
|
||||
taskId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'proof_of_completion_id', nullable: true })
|
||||
proofOfCompletionId!: string;
|
||||
|
||||
@OneToOne(() => Task, (task) => task.submission, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'task_id' })
|
||||
task!: Task;
|
||||
|
||||
@ManyToOne(() => Document, (document) => document.submissions, { nullable: true })
|
||||
@JoinColumn({ name: 'proof_of_completion_id' })
|
||||
proofOfCompletion!: Document;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
90
src/task/entities/task.entity.ts
Normal file
90
src/task/entities/task.entity.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { SubmissionStatus, TaskStatus } from '../enums';
|
||||
import { TaskFrequency } from '../enums/task.frequency.enum';
|
||||
import { TaskSubmission } from './task-submissions.entity';
|
||||
|
||||
@Entity('tasks')
|
||||
export class Task extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'title' })
|
||||
title!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'description' })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'decimal', name: 'reward_amount', precision: 12, scale: 3 })
|
||||
rewardAmount!: number;
|
||||
|
||||
@Column({ type: 'uuid', name: 'image_id' })
|
||||
imageId!: string;
|
||||
|
||||
@Column({ type: 'varchar', name: 'task_frequency' })
|
||||
taskFrequency!: TaskFrequency;
|
||||
|
||||
@Column({ type: 'date', name: 'start_date' })
|
||||
startDate!: Date;
|
||||
|
||||
@Column({ type: 'date', name: 'due_date' })
|
||||
dueDate!: Date;
|
||||
|
||||
@Column({ type: 'boolean', name: 'is_proof_required' })
|
||||
isProofRequired!: boolean;
|
||||
|
||||
@Column({ type: 'uuid', name: 'assigned_to_id' })
|
||||
assignedToId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'assigned_by_id' })
|
||||
assignedById!: string;
|
||||
|
||||
@ManyToOne(() => Document, (document) => document.tasks)
|
||||
@JoinColumn({ name: 'image_id' })
|
||||
image!: Document;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.tasks, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'assigned_to_id' })
|
||||
assignedTo!: Junior;
|
||||
|
||||
@ManyToOne(() => Guardian, (guardian) => guardian.tasks)
|
||||
@JoinColumn({ name: 'assigned_by_id' })
|
||||
assignedBy!: Guardian;
|
||||
|
||||
@OneToOne(() => TaskSubmission, (submission) => submission.task, { cascade: true })
|
||||
submission?: TaskSubmission;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
||||
get status(): string {
|
||||
if (new Date(this.dueDate) < new Date()) {
|
||||
return TaskStatus.COMPLETED;
|
||||
}
|
||||
|
||||
if (this.submission && this.submission.status === SubmissionStatus.APPROVED) {
|
||||
return TaskStatus.COMPLETED;
|
||||
}
|
||||
|
||||
if (this.submission && this.submission.status !== SubmissionStatus.APPROVED) {
|
||||
return TaskStatus.IN_PROGRESS;
|
||||
}
|
||||
|
||||
return TaskStatus.PENDING;
|
||||
}
|
||||
}
|
2
src/task/enums/index.ts
Normal file
2
src/task/enums/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './submission-status.enum';
|
||||
export * from './task-status.enum';
|
5
src/task/enums/submission-status.enum.ts
Normal file
5
src/task/enums/submission-status.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum SubmissionStatus {
|
||||
PENDING = 'PENDING',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
5
src/task/enums/task-status.enum.ts
Normal file
5
src/task/enums/task-status.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum TaskStatus {
|
||||
PENDING = 'PENDING',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
COMPLETED = 'COMPLETED',
|
||||
}
|
6
src/task/enums/task.frequency.enum.ts
Normal file
6
src/task/enums/task.frequency.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum TaskFrequency {
|
||||
ONE_TIME = 'ONE_TIME',
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
1
src/task/repositories/index.ts
Normal file
1
src/task/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './task.repository';
|
102
src/task/repositories/task.repository.ts
Normal file
102
src/task/repositories/task.repository.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { CreateTaskRequestDto, TasksFilterOptions, TaskSubmissionRequestDto } from '../dtos/request';
|
||||
import { Task } from '../entities';
|
||||
import { TaskSubmission } from '../entities/task-submissions.entity';
|
||||
import { SubmissionStatus, TaskStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class TaskRepository {
|
||||
constructor(@InjectRepository(Task) private readonly taskRepository: Repository<Task>) {}
|
||||
|
||||
createTask(userId: string, body: CreateTaskRequestDto) {
|
||||
return this.taskRepository.save(
|
||||
this.taskRepository.create({
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
rewardAmount: body.rewardAmount,
|
||||
taskFrequency: body.frequency,
|
||||
startDate: body.startDate,
|
||||
dueDate: body.dueDate,
|
||||
assignedById: userId,
|
||||
assignedToId: body.juniorId,
|
||||
imageId: body.imageId,
|
||||
isProofRequired: body.isProofRequired,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findTask(where: FindOptionsWhere<Task>) {
|
||||
return this.taskRepository.findOne({
|
||||
where,
|
||||
relations: ['image', 'assignedTo', 'assignedTo.customer', 'assignedTo.customer.user', 'submission'],
|
||||
});
|
||||
}
|
||||
|
||||
findTasks({ roles, sub: userId }: IJwtPayload, query: TasksFilterOptions) {
|
||||
const queryBuilder = this.taskRepository.createQueryBuilder('task');
|
||||
|
||||
queryBuilder
|
||||
.leftJoinAndSelect('task.image', 'image')
|
||||
.leftJoinAndSelect('task.assignedTo', 'assignedTo')
|
||||
.leftJoinAndSelect('assignedTo.customer', 'customer')
|
||||
.leftJoinAndSelect('customer.user', 'user')
|
||||
.leftJoinAndSelect('task.submission', 'submission');
|
||||
|
||||
if (roles.includes(Roles.GUARDIAN)) {
|
||||
queryBuilder.where('task.assignedById = :userId', { userId });
|
||||
|
||||
// Add a condition for juniorId if it exists
|
||||
if (query.juniorId) {
|
||||
queryBuilder.andWhere('task.assignedToId = :juniorId', { juniorId: query.juniorId });
|
||||
}
|
||||
} else {
|
||||
queryBuilder.where('task.assignedToId = :userId', { userId });
|
||||
}
|
||||
|
||||
if (query.status === TaskStatus.PENDING) {
|
||||
queryBuilder.andWhere('task.dueDate >= :today', { today: new Date() });
|
||||
queryBuilder.andWhere('submission IS NULL');
|
||||
}
|
||||
|
||||
if (query.status === TaskStatus.IN_PROGRESS) {
|
||||
queryBuilder.andWhere('submission IS NOT NULL');
|
||||
queryBuilder.andWhere('submission.status != :status', { status: SubmissionStatus.APPROVED });
|
||||
queryBuilder.andWhere('task.dueDate >= :today', { today: new Date() });
|
||||
}
|
||||
|
||||
if (query.status === TaskStatus.COMPLETED) {
|
||||
queryBuilder.andWhere('task.dueDate < :today', { today: new Date() });
|
||||
queryBuilder.orWhere('submission.status = :status', { status: SubmissionStatus.APPROVED });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('task.createdAt', 'DESC');
|
||||
queryBuilder.skip((query.page - ONE) * query.size);
|
||||
queryBuilder.take(query.size);
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
|
||||
createSubmission(task: Task, body: TaskSubmissionRequestDto) {
|
||||
const submission = task.submission || new TaskSubmission();
|
||||
submission.status = SubmissionStatus.PENDING;
|
||||
submission.submittedAt = new Date();
|
||||
submission.taskId = task.id;
|
||||
submission.proofOfCompletionId = body.imageId;
|
||||
|
||||
return task.save();
|
||||
}
|
||||
|
||||
approveSubmission(submission: TaskSubmission) {
|
||||
submission.status = SubmissionStatus.APPROVED;
|
||||
return submission.save();
|
||||
}
|
||||
|
||||
rejectSubmission(submission: TaskSubmission) {
|
||||
submission.status = SubmissionStatus.REJECTED;
|
||||
return submission.save();
|
||||
}
|
||||
}
|
1
src/task/services/index.ts
Normal file
1
src/task/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './task.service';
|
71
src/task/services/task.service.ts
Normal file
71
src/task/services/task.service.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { CreateTaskRequestDto, TasksFilterOptions, TaskSubmissionRequestDto } from '../dtos/request';
|
||||
import { Task } from '../entities';
|
||||
import { SubmissionStatus, TaskStatus } from '../enums';
|
||||
import { TaskRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class TaskService {
|
||||
constructor(private readonly taskRepository: TaskRepository) {}
|
||||
async createTask(userId: string, body: CreateTaskRequestDto) {
|
||||
const task = await this.taskRepository.createTask(userId, body);
|
||||
return this.findTask({ id: task.id });
|
||||
}
|
||||
|
||||
async findTask(where: FindOptionsWhere<Task>) {
|
||||
const task = await this.taskRepository.findTask(where);
|
||||
|
||||
if (!task) {
|
||||
throw new BadRequestException('TASK.NOT_FOUND');
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
findTasks(user: IJwtPayload, query: TasksFilterOptions) {
|
||||
return this.taskRepository.findTasks(user, query);
|
||||
}
|
||||
async submitTask(userId: string, taskId: string, body: TaskSubmissionRequestDto) {
|
||||
const task = await this.findTask({ id: taskId, assignedToId: userId });
|
||||
|
||||
if (task.status == TaskStatus.COMPLETED) {
|
||||
throw new BadRequestException('TASK.ALREADY_COMPLETED');
|
||||
}
|
||||
|
||||
if (task.isProofRequired && !body.imageId) {
|
||||
throw new BadRequestException('TASK.PROOF_REQUIRED');
|
||||
}
|
||||
|
||||
await this.taskRepository.createSubmission(task, body);
|
||||
}
|
||||
|
||||
async approveTaskSubmission(userId: string, taskId: string) {
|
||||
const task = await this.findTask({ id: taskId, assignedById: userId });
|
||||
|
||||
if (!task.submission) {
|
||||
throw new BadRequestException('TASK.NO_SUBMISSION');
|
||||
}
|
||||
|
||||
if (task.submission.status !== SubmissionStatus.PENDING) {
|
||||
throw new BadRequestException('TASK.SUBMISSION_ALREADY_REVIEWED');
|
||||
}
|
||||
|
||||
await this.taskRepository.approveSubmission(task.submission);
|
||||
}
|
||||
|
||||
async rejectTaskSubmission(userId: string, taskId: string) {
|
||||
const task = await this.findTask({ id: taskId, assignedById: userId });
|
||||
|
||||
if (!task.submission) {
|
||||
throw new BadRequestException('TASK.NO_SUBMISSION');
|
||||
}
|
||||
|
||||
if (task.submission.status !== SubmissionStatus.PENDING) {
|
||||
throw new BadRequestException('TASK.SUBMISSION_ALREADY_REVIEWED');
|
||||
}
|
||||
|
||||
await this.taskRepository.rejectSubmission(task.submission);
|
||||
}
|
||||
}
|
14
src/task/task.module.ts
Normal file
14
src/task/task.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TaskController } from './controllers';
|
||||
import { Task } from './entities';
|
||||
import { TaskSubmission } from './entities/task-submissions.entity';
|
||||
import { TaskRepository } from './repositories';
|
||||
import { TaskService } from './services';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Task, TaskSubmission])],
|
||||
controllers: [TaskController],
|
||||
providers: [TaskService, TaskRepository],
|
||||
})
|
||||
export class TaskModule {}
|
@ -1,6 +1,7 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './src/app.module';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||
import { AppModule } from './src/app.module';
|
||||
|
||||
/**
|
||||
* Getting data source through NestJS app helps in getting entities dynamically with "autoLoadEntities" NestJS feature
|
||||
@ -8,6 +9,7 @@ import { DataSource } from 'typeorm';
|
||||
*/
|
||||
async function getTypeOrmDataSource() {
|
||||
process.env.MIGRATIONS_RUN = 'false';
|
||||
initializeTransactionalContext();
|
||||
|
||||
const app = await NestFactory.createApplicationContext(AppModule);
|
||||
|
||||
|
Reference in New Issue
Block a user