mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-26 06:09:41 +00:00
Compare commits
46 Commits
feat/forge
...
feat/mobil
Author | SHA1 | Date | |
---|---|---|---|
a7028fa64c | |||
0750509a85 | |||
4d9ebe729e | |||
bb8cc33d53 | |||
e933cacdcf | |||
3719498c2f | |||
c7470302bd | |||
5e9e83cb74 | |||
4cef58580e | |||
0ba09cbf8b | |||
28a2cb5d75 | |||
4961a192ea | |||
8ab47f3835 | |||
8112fb81a2 | |||
2c3c862c4a | |||
93f5d83825 | |||
ea60ac3d7b | |||
0748695f23 | |||
a201692c0c | |||
fd6c1d1442 | |||
ed57ce6e91 | |||
33453b193f | |||
b0972f1a0a | |||
7437403756 | |||
4d2f6f57f4 | |||
24d990592d | |||
5b7b7ff689 | |||
6fccacd085 | |||
51fa61dbc6 | |||
4867a5f858 | |||
687b6a5c6d | |||
e6ed1772f7 | |||
1f0a14fee4 | |||
eb70828ae0 | |||
220a03cc46 | |||
39b1e76bb5 | |||
83fc634d25 | |||
35b434bc3d | |||
749ee5457f | |||
d539073f29 | |||
66e1bb0f28 | |||
577f91b796 | |||
7ed37c30e1 | |||
c2f63ccc72 | |||
970a41c895 | |||
3fd29b3905 |
7225
package-lock.json
generated
7225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -28,6 +28,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abdalhamid/hello": "^2.0.0",
|
"@abdalhamid/hello": "^2.0.0",
|
||||||
"@hamid/hello": "file:../libraries/test-package",
|
"@hamid/hello": "file:../libraries/test-package",
|
||||||
|
"@keyv/redis": "^4.0.2",
|
||||||
"@nestjs-modules/mailer": "^2.0.2",
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
"@nestjs/axios": "^3.1.2",
|
"@nestjs/axios": "^3.1.2",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
@ -45,8 +46,10 @@
|
|||||||
"amqp-connection-manager": "^4.1.14",
|
"amqp-connection-manager": "^4.1.14",
|
||||||
"amqplib": "^0.10.4",
|
"amqplib": "^0.10.4",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cacheable": "^1.8.5",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"firebase-admin": "^13.0.2",
|
||||||
"google-libphonenumber": "^3.2.39",
|
"google-libphonenumber": "^3.2.39",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
@ -54,6 +57,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nestjs-i18n": "^10.4.9",
|
"nestjs-i18n": "^10.4.9",
|
||||||
"nestjs-pino": "^4.1.0",
|
"nestjs-pino": "^4.1.0",
|
||||||
|
"nestjs-twilio": "^4.4.0",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"oci-common": "^2.99.0",
|
"oci-common": "^2.99.0",
|
||||||
"oci-sdk": "^2.99.0",
|
"oci-sdk": "^2.99.0",
|
||||||
@ -62,9 +66,11 @@
|
|||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"pino-http": "^10.3.0",
|
"pino-http": "^10.3.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"reflect-metadata": "^0.2.0",
|
"qrcode": "^1.5.4",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.20"
|
"typeorm": "^0.3.20",
|
||||||
|
"typeorm-transactional": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@golevelup/ts-jest": "^0.6.0",
|
"@golevelup/ts-jest": "^0.6.0",
|
||||||
@ -80,15 +86,19 @@
|
|||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/nodemailer": "^6.4.16",
|
"@types/nodemailer": "^6.4.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||||
"@typescript-eslint/parser": "^5.59.2",
|
"@typescript-eslint/parser": "^5.59.2",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-security": "^1.7.1",
|
"eslint-plugin-security": "^1.7.1",
|
||||||
|
"i": "^0.3.7",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
|
"npm": "^10.9.2",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
|
20
src/allowance/allowance.module.ts
Normal file
20
src/allowance/allowance.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { JuniorModule } from '~/junior/junior.module';
|
||||||
|
import { AllowanceChangeRequestController, AllowancesController } from './controllers';
|
||||||
|
import { Allowance, AllowanceChangeRequest } from './entities';
|
||||||
|
import { AllowanceChangeRequestsRepository, AllowancesRepository } from './repositories';
|
||||||
|
import { AllowanceChangeRequestsService, AllowancesService } from './services';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AllowancesController, AllowanceChangeRequestController],
|
||||||
|
imports: [TypeOrmModule.forFeature([Allowance, AllowanceChangeRequest]), JuniorModule],
|
||||||
|
providers: [
|
||||||
|
AllowancesService,
|
||||||
|
AllowancesRepository,
|
||||||
|
AllowanceChangeRequestsService,
|
||||||
|
AllowanceChangeRequestsRepository,
|
||||||
|
],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class AllowanceModule {}
|
@ -0,0 +1,80 @@
|
|||||||
|
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 { 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 { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceChangeRequestResponseDto } from '../dtos/response';
|
||||||
|
import { AllowanceChangeRequestsService } from '../services';
|
||||||
|
|
||||||
|
@Controller('allowance-change-requests')
|
||||||
|
@ApiTags('Allowance Change Requests')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AllowanceChangeRequestController {
|
||||||
|
constructor(private readonly allowanceChangeRequestsService: AllowanceChangeRequestsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
requestAllowanceChange(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceChangeRequestDto) {
|
||||||
|
return this.allowanceChangeRequestsService.createAllowanceChangeRequest(sub, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataPageResponse(AllowanceChangeRequestResponseDto)
|
||||||
|
async findAllowanceChangeRequests(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
|
||||||
|
const [requests, itemCount] = await this.allowanceChangeRequestsService.findAllowanceChangeRequests(sub, query);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
requests.map((request) => new AllowanceChangeRequestResponseDto(request)),
|
||||||
|
{
|
||||||
|
itemCount,
|
||||||
|
page: query.page,
|
||||||
|
size: query.size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:changeRequestId')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceChangeRequestResponseDto)
|
||||||
|
async findAllowanceChangeRequestById(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||||
|
) {
|
||||||
|
const request = await this.allowanceChangeRequestsService.findAllowanceChangeRequestById(sub, changeRequestId);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new AllowanceChangeRequestResponseDto(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':changeRequestId/approve')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
approveAllowanceChangeRequest(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||||
|
) {
|
||||||
|
return this.allowanceChangeRequestsService.approveAllowanceChangeRequest(sub, changeRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':changeRequestId/reject')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
rejectAllowanceChangeRequest(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||||
|
) {
|
||||||
|
return this.allowanceChangeRequestsService.rejectAllowanceChangeRequest(sub, changeRequestId);
|
||||||
|
}
|
||||||
|
}
|
72
src/allowance/controllers/allowances.controller.ts
Normal file
72
src/allowance/controllers/allowances.controller.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, 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 { CreateAllowanceRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceResponseDto } from '../dtos/response';
|
||||||
|
import { AllowancesService } from '../services';
|
||||||
|
|
||||||
|
@Controller('allowances')
|
||||||
|
@ApiTags('Allowances')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AllowancesController {
|
||||||
|
constructor(private readonly allowancesService: AllowancesService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceResponseDto)
|
||||||
|
async createAllowance(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceRequestDto) {
|
||||||
|
const allowance = await this.allowancesService.createAllowance(sub, body);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new AllowanceResponseDto(allowance));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataPageResponse(AllowanceResponseDto)
|
||||||
|
async findAllowances(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
|
||||||
|
const [allowances, itemCount] = await this.allowancesService.findAllowances(sub, query);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
allowances.map((allowance) => new AllowanceResponseDto(allowance)),
|
||||||
|
{
|
||||||
|
itemCount,
|
||||||
|
page: query.page,
|
||||||
|
size: query.size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':allowanceId')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceResponseDto)
|
||||||
|
async findAllowanceById(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
|
||||||
|
) {
|
||||||
|
const allowance = await this.allowancesService.findAllowanceById(allowanceId, sub);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new AllowanceResponseDto(allowance));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':allowanceId')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceResponseDto)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
deleteAllowance(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
|
||||||
|
) {
|
||||||
|
return this.allowancesService.deleteAllowance(sub, allowanceId);
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/controllers/index.ts
Normal file
2
src/allowance/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.controller';
|
||||||
|
export * from './allowances.controller';
|
@ -0,0 +1,28 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
export class CreateAllowanceChangeRequestDto {
|
||||||
|
@ApiProperty({ example: 'I want to change the amount of the allowance' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowanceChangeRequest.reason' }) })
|
||||||
|
@IsNotEmpty({
|
||||||
|
message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowanceChangeRequest.reason' }),
|
||||||
|
})
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
@IsNumber(
|
||||||
|
{},
|
||||||
|
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowanceChangeRequest.amount' }) },
|
||||||
|
)
|
||||||
|
@IsPositive({
|
||||||
|
message: i18n('validation.IsPositive', { path: 'general', property: 'allowanceChangeRequest.amount' }),
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
@IsUUID('4', {
|
||||||
|
message: i18n('validation.IsUUID', { path: 'general', property: 'allowanceChangeRequest.allowanceId' }),
|
||||||
|
})
|
||||||
|
allowanceId!: string;
|
||||||
|
}
|
52
src/allowance/dtos/request/create-allowance.request.dto.ts
Normal file
52
src/allowance/dtos/request/create-allowance.request.dto.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { IsDate, IsEnum, IsInt, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID, ValidateIf } from 'class-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
|
||||||
|
export class CreateAllowanceRequestDto {
|
||||||
|
@ApiProperty({ example: 'Allowance name' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowance.name' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowance.name' }) })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
|
||||||
|
@IsEnum(AllowanceFrequency, {
|
||||||
|
message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.frequency' }),
|
||||||
|
})
|
||||||
|
frequency!: AllowanceFrequency;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceType.BY_END_DATE })
|
||||||
|
@IsEnum(AllowanceType, { message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.type' }) })
|
||||||
|
type!: AllowanceType;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.startDate' }) })
|
||||||
|
@Transform(({ value }) => moment(value).startOf('day').toDate())
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.endDate' }) })
|
||||||
|
@Transform(({ value }) => moment(value).endOf('day').toDate())
|
||||||
|
@ValidateIf((o) => o.type === AllowanceType.BY_END_DATE)
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10 })
|
||||||
|
@IsNumber(
|
||||||
|
{},
|
||||||
|
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.numberOfTransactions' }) },
|
||||||
|
)
|
||||||
|
@IsInt({ message: i18n('validation.IsInt', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
@ValidateIf((o) => o.type === AllowanceType.BY_COUNT)
|
||||||
|
numberOfTransactions?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'e7b1b3b4-4b3b-4b3b-4b3b-4b3b4b3b4b3b' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'allowance.juniorId' }) })
|
||||||
|
juniorId!: string;
|
||||||
|
}
|
2
src/allowance/dtos/request/index.ts
Normal file
2
src/allowance/dtos/request/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './create-allowance-change.request.dto';
|
||||||
|
export * from './create-allowance.request.dto';
|
@ -0,0 +1,45 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { AllowanceChangeRequest } from '~/allowance/entities';
|
||||||
|
import { AllowanceChangeRequestStatus } from '~/allowance/enums';
|
||||||
|
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||||
|
|
||||||
|
export class AllowanceChangeRequestResponseDto {
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceChangeRequestStatus.APPROVED })
|
||||||
|
status!: AllowanceChangeRequestStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Allowance name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '100' })
|
||||||
|
oldAmount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '200' })
|
||||||
|
newAmount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Some reason' })
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
allowanceId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JuniorResponseDto })
|
||||||
|
junior!: JuniorResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
constructor(allowanceChangeRequest: AllowanceChangeRequest) {
|
||||||
|
this.id = allowanceChangeRequest.id;
|
||||||
|
this.status = allowanceChangeRequest.status;
|
||||||
|
this.name = allowanceChangeRequest.allowance.name;
|
||||||
|
this.oldAmount = allowanceChangeRequest.allowance.amount;
|
||||||
|
this.newAmount = allowanceChangeRequest.amount;
|
||||||
|
this.reason = allowanceChangeRequest.reason;
|
||||||
|
this.allowanceId = allowanceChangeRequest.allowanceId;
|
||||||
|
this.junior = new JuniorResponseDto(allowanceChangeRequest.allowance.junior);
|
||||||
|
this.createdAt = allowanceChangeRequest.createdAt;
|
||||||
|
}
|
||||||
|
}
|
53
src/allowance/dtos/response/allowance.response.dto.ts
Normal file
53
src/allowance/dtos/response/allowance.response.dto.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Allowance } from '~/allowance/entities';
|
||||||
|
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
|
||||||
|
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||||
|
|
||||||
|
export class AllowanceResponseDto {
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Allowance name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
|
||||||
|
frequency!: AllowanceFrequency;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceType.BY_END_DATE })
|
||||||
|
type!: AllowanceType;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10 })
|
||||||
|
numberOfTransactions?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JuniorResponseDto })
|
||||||
|
junior!: JuniorResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
constructor(allowance: Allowance) {
|
||||||
|
this.id = allowance.id;
|
||||||
|
this.name = allowance.name;
|
||||||
|
this.amount = allowance.amount;
|
||||||
|
this.frequency = allowance.frequency;
|
||||||
|
this.type = allowance.type;
|
||||||
|
this.startDate = allowance.startDate;
|
||||||
|
this.endDate = allowance.endDate;
|
||||||
|
this.numberOfTransactions = allowance.numberOfTransactions;
|
||||||
|
this.junior = new JuniorResponseDto(allowance.junior);
|
||||||
|
this.createdAt = allowance.createdAt;
|
||||||
|
this.updatedAt = allowance.updatedAt;
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/dtos/response/index.ts
Normal file
2
src/allowance/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.response.dto';
|
||||||
|
export * from './allowance.response.dto';
|
45
src/allowance/entities/allowance-change-request.entity.ts
Normal file
45
src/allowance/entities/allowance-change-request.entity.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { AllowanceChangeRequestStatus } from '../enums';
|
||||||
|
import { Allowance } from './allowance.entity';
|
||||||
|
|
||||||
|
@Entity('allowance_change_requests')
|
||||||
|
export class AllowanceChangeRequest {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', name: 'reason' })
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
name: 'amount',
|
||||||
|
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'status', default: AllowanceChangeRequestStatus.PENDING })
|
||||||
|
status!: AllowanceChangeRequestStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'allowance_id' })
|
||||||
|
allowanceId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Allowance, (allowance) => allowance.changeRequests)
|
||||||
|
@JoinColumn({ name: 'allowance_id' })
|
||||||
|
allowance!: Allowance;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
74
src/allowance/entities/allowance.entity.ts
Normal file
74
src/allowance/entities/allowance.entity.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { AllowanceFrequency, AllowanceType } from '../enums';
|
||||||
|
import { AllowanceChangeRequest } from './allowance-change-request.entity';
|
||||||
|
|
||||||
|
@Entity('allowances')
|
||||||
|
export class Allowance {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
name: 'amount',
|
||||||
|
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'frequency' })
|
||||||
|
frequency!: AllowanceFrequency;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'type' })
|
||||||
|
type!: AllowanceType;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', name: 'start_date' })
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', name: 'end_date', nullable: true })
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'int', name: 'number_of_transactions', nullable: true })
|
||||||
|
numberOfTransactions?: number;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'guardian_id' })
|
||||||
|
guardianId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'junior_id' })
|
||||||
|
juniorId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Guardian, (guardian) => guardian.allowances)
|
||||||
|
@JoinColumn({ name: 'guardian_id' })
|
||||||
|
guardian!: Guardian;
|
||||||
|
|
||||||
|
@ManyToOne(() => Junior, (junior) => junior.allowances)
|
||||||
|
@JoinColumn({ name: 'junior_id' })
|
||||||
|
junior!: Junior;
|
||||||
|
|
||||||
|
@OneToMany(() => AllowanceChangeRequest, (changeRequest) => changeRequest.allowance)
|
||||||
|
changeRequests!: AllowanceChangeRequest[];
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true })
|
||||||
|
deletedAt?: Date;
|
||||||
|
}
|
2
src/allowance/entities/index.ts
Normal file
2
src/allowance/entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.entity';
|
||||||
|
export * from './allowance.entity';
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum AllowanceChangeRequestStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
APPROVED = 'APPROVED',
|
||||||
|
REJECTED = 'REJECTED',
|
||||||
|
}
|
5
src/allowance/enums/allowance-frequency.enum.ts
Normal file
5
src/allowance/enums/allowance-frequency.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum AllowanceFrequency {
|
||||||
|
DAILY = 'DAILY',
|
||||||
|
WEEKLY = 'WEEKLY',
|
||||||
|
MONTHLY = 'MONTHLY',
|
||||||
|
}
|
4
src/allowance/enums/allowance-type.enum.ts
Normal file
4
src/allowance/enums/allowance-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum AllowanceType {
|
||||||
|
BY_END_DATE = 'BY_END_DATE',
|
||||||
|
BY_COUNT = 'BY_COUNT',
|
||||||
|
}
|
3
src/allowance/enums/index.ts
Normal file
3
src/allowance/enums/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './allowance-change-request-status.enum';
|
||||||
|
export * from './allowance-frequency.enum';
|
||||||
|
export * from './allowance-type.enum';
|
@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceChangeRequest } from '../entities';
|
||||||
|
import { AllowanceChangeRequestStatus } from '../enums';
|
||||||
|
const ONE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class AllowanceChangeRequestsRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AllowanceChangeRequest)
|
||||||
|
private readonly allowanceChangeRequestsRepository: Repository<AllowanceChangeRequest>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createAllowanceChangeRequest(allowanceId: string, body: CreateAllowanceChangeRequestDto) {
|
||||||
|
return this.allowanceChangeRequestsRepository.save(
|
||||||
|
this.allowanceChangeRequestsRepository.create({
|
||||||
|
allowanceId,
|
||||||
|
amount: body.amount,
|
||||||
|
reason: body.reason,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>, withRelations = false) {
|
||||||
|
const relations = withRelations
|
||||||
|
? ['allowance', 'allowance.junior', 'allowance.junior.customer', 'allowance.junior.customer.profilePicture']
|
||||||
|
: [];
|
||||||
|
return this.allowanceChangeRequestsRepository.findOne({ where, relations });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllowanceChangeRequestStatus(requestId: string, status: AllowanceChangeRequestStatus) {
|
||||||
|
return this.allowanceChangeRequestsRepository.update({ id: requestId }, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceChangeRequests(guardianId: string, query: PageOptionsRequestDto) {
|
||||||
|
return this.allowanceChangeRequestsRepository.findAndCount({
|
||||||
|
where: { allowance: { guardianId } },
|
||||||
|
take: query.size,
|
||||||
|
skip: query.size * (query.page - ONE),
|
||||||
|
relations: [
|
||||||
|
'allowance',
|
||||||
|
'allowance.junior',
|
||||||
|
'allowance.junior.customer',
|
||||||
|
'allowance.junior.customer.profilePicture',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
47
src/allowance/repositories/allowances.repository.ts
Normal file
47
src/allowance/repositories/allowances.repository.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||||
|
import { Allowance } from '../entities';
|
||||||
|
const ONE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class AllowancesRepository {
|
||||||
|
constructor(@InjectRepository(Allowance) private readonly allowancesRepository: Repository<Allowance>) {}
|
||||||
|
|
||||||
|
createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||||
|
return this.allowancesRepository.save(
|
||||||
|
this.allowancesRepository.create({
|
||||||
|
guardianId,
|
||||||
|
name: body.name,
|
||||||
|
amount: body.amount,
|
||||||
|
frequency: body.frequency,
|
||||||
|
type: body.type,
|
||||||
|
startDate: body.startDate,
|
||||||
|
endDate: body.endDate,
|
||||||
|
numberOfTransactions: body.numberOfTransactions,
|
||||||
|
juniorId: body.juniorId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||||
|
return this.allowancesRepository.findOne({
|
||||||
|
where: { id: allowanceId, guardianId },
|
||||||
|
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
|
||||||
|
return this.allowancesRepository.findAndCount({
|
||||||
|
where: { guardianId },
|
||||||
|
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||||
|
take: query.size,
|
||||||
|
skip: query.size * (query.page - ONE),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllowance(guardianId: string, allowanceId: string) {
|
||||||
|
return this.allowancesRepository.softDelete({ id: allowanceId, guardianId });
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/repositories/index.ts
Normal file
2
src/allowance/repositories/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.repository';
|
||||||
|
export * from './allowances.repository';
|
115
src/allowance/services/allowance-change-requests.service.ts
Normal file
115
src/allowance/services/allowance-change-requests.service.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceChangeRequest } from '../entities';
|
||||||
|
import { AllowanceChangeRequestStatus } from '../enums';
|
||||||
|
import { AllowanceChangeRequestsRepository } from '../repositories';
|
||||||
|
import { AllowancesService } from './allowances.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AllowanceChangeRequestsService {
|
||||||
|
constructor(
|
||||||
|
private readonly allowanceChangeRequestsRepository: AllowanceChangeRequestsRepository,
|
||||||
|
private readonly ociService: OciService,
|
||||||
|
private readonly allowanceService: AllowancesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createAllowanceChangeRequest(juniorId: string, body: CreateAllowanceChangeRequestDto) {
|
||||||
|
const allowance = await this.allowanceService.validateAllowanceForJunior(juniorId, body.allowanceId);
|
||||||
|
|
||||||
|
if (allowance.amount === body.amount) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestWithTheSameAmount = await this.findAllowanceChangeRequestBy({
|
||||||
|
allowanceId: body.allowanceId,
|
||||||
|
amount: body.amount,
|
||||||
|
status: AllowanceChangeRequestStatus.PENDING,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestWithTheSameAmount) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT_PENDING');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allowanceChangeRequestsRepository.createAllowanceChangeRequest(body.allowanceId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>) {
|
||||||
|
return this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(where);
|
||||||
|
}
|
||||||
|
|
||||||
|
async approveAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||||
|
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
if (request.status === AllowanceChangeRequestStatus.APPROVED) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_APPROVED');
|
||||||
|
}
|
||||||
|
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||||
|
requestId,
|
||||||
|
AllowanceChangeRequestStatus.APPROVED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||||
|
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
if (request.status === AllowanceChangeRequestStatus.REJECTED) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_REJECTED');
|
||||||
|
}
|
||||||
|
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||||
|
requestId,
|
||||||
|
AllowanceChangeRequestStatus.REJECTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowanceChangeRequests(
|
||||||
|
guardianId: string,
|
||||||
|
query: PageOptionsRequestDto,
|
||||||
|
): Promise<[AllowanceChangeRequest[], number]> {
|
||||||
|
const [requests, itemCount] = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequests(
|
||||||
|
guardianId,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.prepareAllowanceChangeRequestsImages(requests);
|
||||||
|
|
||||||
|
return [requests, itemCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowanceChangeRequestById(guardianId: string, requestId: string) {
|
||||||
|
const request = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(
|
||||||
|
{
|
||||||
|
id: requestId,
|
||||||
|
allowance: { guardianId },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prepareAllowanceChangeRequestsImages([request]);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareAllowanceChangeRequestsImages(requests: AllowanceChangeRequest[]) {
|
||||||
|
return Promise.all(
|
||||||
|
requests.map(async (request) => {
|
||||||
|
const profilePicture = request.allowance.junior.customer.profilePicture;
|
||||||
|
if (profilePicture) {
|
||||||
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
85
src/allowance/services/allowances.service.ts
Normal file
85
src/allowance/services/allowances.service.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { JuniorService } from '~/junior/services';
|
||||||
|
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||||
|
import { Allowance } from '../entities';
|
||||||
|
import { AllowancesRepository } from '../repositories';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AllowancesService {
|
||||||
|
constructor(
|
||||||
|
private readonly allowancesRepository: AllowancesRepository,
|
||||||
|
private readonly juniorService: JuniorService,
|
||||||
|
private readonly ociService: OciService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||||
|
if (moment(body.startDate).isBefore(moment().startOf('day'))) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.START_DATE_BEFORE_TODAY');
|
||||||
|
}
|
||||||
|
if (moment(body.startDate).isAfter(body.endDate)) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.START_DATE_AFTER_END_DATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(guardianId, body.juniorId);
|
||||||
|
|
||||||
|
if (!doesJuniorBelongToGuardian) {
|
||||||
|
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowance = await this.allowancesRepository.createAllowance(guardianId, body);
|
||||||
|
|
||||||
|
return this.findAllowanceById(allowance.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||||
|
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId, guardianId);
|
||||||
|
|
||||||
|
if (!allowance) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||||
|
}
|
||||||
|
await this.prepareAllowanceDocuments([allowance]);
|
||||||
|
return allowance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowances(guardianId: string, query: PageOptionsRequestDto): Promise<[Allowance[], number]> {
|
||||||
|
const [allowances, itemCount] = await this.allowancesRepository.findAllowances(guardianId, query);
|
||||||
|
await this.prepareAllowanceDocuments(allowances);
|
||||||
|
return [allowances, itemCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllowance(guardianId: string, allowanceId: string) {
|
||||||
|
const { affected } = await this.allowancesRepository.deleteAllowance(guardianId, allowanceId);
|
||||||
|
|
||||||
|
if (!affected) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAllowanceForJunior(juniorId: string, allowanceId: string) {
|
||||||
|
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId);
|
||||||
|
|
||||||
|
if (!allowance) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowance.juniorId !== juniorId) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.DOES_NOT_BELONG_TO_JUNIOR');
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareAllowanceDocuments(allowance: Allowance[]) {
|
||||||
|
await Promise.all(
|
||||||
|
allowance.map(async (allowance) => {
|
||||||
|
const profilePicture = allowance.junior.customer.profilePicture;
|
||||||
|
if (profilePicture) {
|
||||||
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/services/index.ts
Normal file
2
src/allowance/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-requests.service';
|
||||||
|
export * from './allowances.service';
|
@ -4,7 +4,13 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
||||||
import { LoggerModule } from 'nestjs-pino';
|
import { LoggerModule } from 'nestjs-pino';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { addTransactionalDataSource } from 'typeorm-transactional';
|
||||||
|
import { AllowanceModule } from './allowance/allowance.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { CacheModule } from './common/modules/cache/cache.module';
|
||||||
|
import { LookupModule } from './common/modules/lookup/lookup.module';
|
||||||
|
import { NotificationModule } from './common/modules/notification/notification.module';
|
||||||
import { OtpModule } from './common/modules/otp/otp.module';
|
import { OtpModule } from './common/modules/otp/otp.module';
|
||||||
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
||||||
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
||||||
@ -13,7 +19,14 @@ import { buildValidationPipe } from './core/pipes';
|
|||||||
import { CustomerModule } from './customer/customer.module';
|
import { CustomerModule } from './customer/customer.module';
|
||||||
import { migrations } from './db';
|
import { migrations } from './db';
|
||||||
import { DocumentModule } from './document/document.module';
|
import { DocumentModule } from './document/document.module';
|
||||||
|
import { GiftModule } from './gift/gift.module';
|
||||||
|
import { GuardianModule } from './guardian/guardian.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { JuniorModule } from './junior/junior.module';
|
||||||
|
import { MoneyRequestModule } from './money-request/money-request.module';
|
||||||
|
import { SavingGoalsModule } from './saving-goals/saving-goals.module';
|
||||||
|
import { TaskModule } from './task/task.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [],
|
controllers: [],
|
||||||
imports: [
|
imports: [
|
||||||
@ -21,19 +34,46 @@ import { HealthModule } from './health/health.module';
|
|||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [],
|
imports: [],
|
||||||
inject: [ConfigService],
|
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({
|
LoggerModule.forRootAsync({
|
||||||
useFactory: (config: ConfigService) => buildLoggerOptions(config),
|
useFactory: (config: ConfigService) => buildLoggerOptions(config),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
I18nModule.forRoot(buildI18nOptions()),
|
I18nModule.forRoot(buildI18nOptions()),
|
||||||
|
|
||||||
|
CacheModule,
|
||||||
|
|
||||||
// App modules
|
// App modules
|
||||||
AuthModule,
|
AuthModule,
|
||||||
CustomerModule,
|
CustomerModule,
|
||||||
DocumentModule,
|
JuniorModule,
|
||||||
HealthModule,
|
|
||||||
|
TaskModule,
|
||||||
|
GuardianModule,
|
||||||
|
SavingGoalsModule,
|
||||||
|
AllowanceModule,
|
||||||
|
MoneyRequestModule,
|
||||||
|
GiftModule,
|
||||||
|
|
||||||
OtpModule,
|
OtpModule,
|
||||||
|
DocumentModule,
|
||||||
|
LookupModule,
|
||||||
|
|
||||||
|
HealthModule,
|
||||||
|
|
||||||
|
NotificationModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Global Pipes
|
// Global Pipes
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { CustomerModule } from '~/customer/customer.module';
|
||||||
|
import { JuniorModule } from '~/junior/junior.module';
|
||||||
import { AuthController } from './controllers';
|
import { AuthController } from './controllers';
|
||||||
import { Device, User, UserNotificationSettings } from './entities';
|
import { Device, User } from './entities';
|
||||||
import { DeviceRepository, UserRepository } from './repositories';
|
import { DeviceRepository, UserRepository } from './repositories';
|
||||||
import { AuthService, DeviceService } from './services';
|
import { AuthService, DeviceService } from './services';
|
||||||
import { UserService } from './services/user.service';
|
import { UserService } from './services/user.service';
|
||||||
import { AccessTokenStrategy } from './strategies';
|
import { AccessTokenStrategy } from './strategies';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, UserNotificationSettings, Device]), JwtModule.register({})],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([User, Device]),
|
||||||
|
JwtModule.register({}),
|
||||||
|
forwardRef(() => CustomerModule),
|
||||||
|
forwardRef(() => JuniorModule),
|
||||||
|
],
|
||||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [UserService],
|
exports: [UserService, DeviceService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Request } from 'express';
|
||||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||||
import { AuthenticatedUser } from '~/common/decorators';
|
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||||
import { AccessTokenGuard } from '~/common/guards';
|
import { AccessTokenGuard } from '~/common/guards';
|
||||||
import { ResponseFactory } from '~/core/utils';
|
import { ResponseFactory } from '~/core/utils';
|
||||||
import {
|
import {
|
||||||
@ -10,8 +11,10 @@ import {
|
|||||||
EnableBiometricRequestDto,
|
EnableBiometricRequestDto,
|
||||||
ForgetPasswordRequestDto,
|
ForgetPasswordRequestDto,
|
||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
|
RefreshTokenRequestDto,
|
||||||
SendForgetPasswordOtpRequestDto,
|
SendForgetPasswordOtpRequestDto,
|
||||||
SetEmailRequestDto,
|
SetEmailRequestDto,
|
||||||
|
setJuniorPasswordRequestDto,
|
||||||
SetPasscodeRequestDto,
|
SetPasscodeRequestDto,
|
||||||
VerifyUserRequestDto,
|
VerifyUserRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
@ -77,9 +80,30 @@ export class AuthController {
|
|||||||
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('junior/set-passcode')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@Public()
|
||||||
|
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
|
||||||
|
return this.authService.setJuniorPasscode(setPasscodeDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('refresh-token')
|
||||||
|
@Public()
|
||||||
|
async refreshToken(@Body() { refreshToken }: RefreshTokenRequestDto) {
|
||||||
|
const [res, user] = await this.authService.refreshToken(refreshToken);
|
||||||
|
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||||
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
||||||
const [res, user] = await this.authService.login(loginDto, deviceId);
|
const [res, user] = await this.authService.login(loginDto, deviceId);
|
||||||
return ResponseFactory.data(new LoginResponseDto(res, user));
|
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('logout')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
async logout(@Req() request: Request) {
|
||||||
|
await this.authService.logout(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@ export * from './disable-biometric.request.dto';
|
|||||||
export * from './enable-biometric.request.dto';
|
export * from './enable-biometric.request.dto';
|
||||||
export * from './forget-password.request.dto';
|
export * from './forget-password.request.dto';
|
||||||
export * from './login.request.dto';
|
export * from './login.request.dto';
|
||||||
|
export * from './refresh-token.request.dto';
|
||||||
export * from './send-forget-password-otp.request.dto';
|
export * from './send-forget-password-otp.request.dto';
|
||||||
export * from './set-email.request.dto';
|
export * from './set-email.request.dto';
|
||||||
|
export * from './set-junior-password.request.dto';
|
||||||
export * from './set-passcode.request.dto';
|
export * from './set-passcode.request.dto';
|
||||||
export * from './verify-user.request.dto';
|
export * from './verify-user.request.dto';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEmail, IsEnum, IsString, ValidateIf } from 'class-validator';
|
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
import { GrantType } from '~/auth/enums';
|
import { GrantType } from '~/auth/enums';
|
||||||
export class LoginRequestDto {
|
export class LoginRequestDto {
|
||||||
@ -17,8 +17,14 @@ export class LoginRequestDto {
|
|||||||
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
|
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
|
||||||
password!: string;
|
password!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'device-token' })
|
@ApiProperty({ example: 'fcm-device-token' })
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceToken' }) })
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.fcmToken' }) })
|
||||||
|
@IsOptional()
|
||||||
|
fcmToken?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Login signature' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.signature' }) })
|
||||||
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
|
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
|
||||||
deviceToken!: string;
|
signature!: string;
|
||||||
}
|
}
|
||||||
|
9
src/auth/dtos/request/refresh-token.request.dto.ts
Normal file
9
src/auth/dtos/request/refresh-token.request.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
export class RefreshTokenRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString({ message: i18n('validation.isString', { path: 'general', property: 'auth.refreshToken' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.required', { path: 'general', property: 'auth.refreshToken' }) })
|
||||||
|
refreshToken!: string;
|
||||||
|
}
|
10
src/auth/dtos/request/set-junior-password.request.dto.ts
Normal file
10
src/auth/dtos/request/set-junior-password.request.dto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { SetPasscodeRequestDto } from './set-passcode.request.dto';
|
||||||
|
export class setJuniorPasswordRequestDto extends SetPasscodeRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.qrToken' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.qrToken' }) })
|
||||||
|
qrToken!: string;
|
||||||
|
}
|
@ -15,6 +15,9 @@ export class Device {
|
|||||||
@Column('varchar', { name: 'public_key', nullable: true })
|
@Column('varchar', { name: 'public_key', nullable: true })
|
||||||
publicKey?: string | null;
|
publicKey?: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'fcm_token', nullable: true })
|
||||||
|
fcmToken?: string | null;
|
||||||
|
|
||||||
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
|
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
lastAccessOn!: Date;
|
lastAccessOn!: Date;
|
||||||
|
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './device.entity';
|
export * from './device.entity';
|
||||||
export * from './user-notification-settings.entity';
|
|
||||||
export * from './user.entity';
|
export * from './user.entity';
|
||||||
|
@ -3,18 +3,16 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
JoinColumn,
|
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { Notification } from '~/common/modules/notification/entities';
|
||||||
import { Otp } from '~/common/modules/otp/entities';
|
import { Otp } from '~/common/modules/otp/entities';
|
||||||
import { Customer } from '~/customer/entities/customer.entity';
|
import { Customer } from '~/customer/entities/customer.entity';
|
||||||
import { Document } from '~/document/entities';
|
|
||||||
import { Roles } from '../enums';
|
import { Roles } from '../enums';
|
||||||
import { Device } from './device.entity';
|
import { Device } from './device.entity';
|
||||||
import { UserNotificationSettings } from './user-notification-settings.entity';
|
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
@ -48,28 +46,18 @@ export class User extends BaseEntity {
|
|||||||
@Column('text', { nullable: true, array: true, name: 'roles' })
|
@Column('text', { nullable: true, array: true, name: 'roles' })
|
||||||
roles!: Roles[];
|
roles!: Roles[];
|
||||||
|
|
||||||
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
|
||||||
profilePictureId!: string;
|
|
||||||
|
|
||||||
@OneToOne(() => Document, (document) => document.user, { cascade: true, nullable: true })
|
|
||||||
@JoinColumn({ name: 'profile_picture_id' })
|
|
||||||
profilePicture!: Document;
|
|
||||||
|
|
||||||
@OneToMany(() => Otp, (otp) => otp.user)
|
@OneToMany(() => Otp, (otp) => otp.user)
|
||||||
otp!: Otp[];
|
otp!: Otp[];
|
||||||
|
|
||||||
@OneToOne(() => UserNotificationSettings, (notificationSettings) => notificationSettings.user, {
|
|
||||||
cascade: true,
|
|
||||||
eager: true,
|
|
||||||
})
|
|
||||||
notificationSettings!: UserNotificationSettings;
|
|
||||||
|
|
||||||
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
|
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
|
||||||
customer!: Customer;
|
customer!: Customer;
|
||||||
|
|
||||||
@OneToMany(() => Device, (device) => device.user)
|
@OneToMany(() => Device, (device) => device.user)
|
||||||
devices!: Device[];
|
devices!: Device[];
|
||||||
|
|
||||||
|
@OneToMany(() => Notification, (notification) => notification.user)
|
||||||
|
notifications!: Notification[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { Roles } from '../enums';
|
||||||
|
|
||||||
export interface IJwtPayload {
|
export interface IJwtPayload {
|
||||||
sub: string;
|
sub: string;
|
||||||
roles: string[];
|
roles: Roles[];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { IsNull, Not, Repository } from 'typeorm';
|
||||||
import { Device } from '../entities';
|
import { Device } from '../entities';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -16,6 +16,10 @@ export class DeviceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||||
return this.deviceRepository.update({ deviceId }, data);
|
return this.deviceRepository.save({ deviceId, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokens(userId: string) {
|
||||||
|
return this.deviceRepository.find({ where: { userId, fcmToken: Not(IsNull()) }, select: ['fcmToken'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||||
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
|
import { User } from '../entities';
|
||||||
import { Customer } from '~/customer/entities';
|
|
||||||
import { User, UserNotificationSettings } from '../entities';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepository {
|
export class UserRepository {
|
||||||
@ -15,27 +13,23 @@ export class UserRepository {
|
|||||||
phoneNumber: data.phoneNumber,
|
phoneNumber: data.phoneNumber,
|
||||||
countryCode: data.countryCode,
|
countryCode: data.countryCode,
|
||||||
roles: data.roles,
|
roles: data.roles,
|
||||||
notificationSettings: UserNotificationSettings.create(),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(where: FindOptionsWhere<User>) {
|
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||||
return this.userRepository.findOne({ where });
|
return this.userRepository.findOne({ where });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNotificationSettings(user: User, body: UpdateNotificationsSettingsRequestDto) {
|
|
||||||
user.notificationSettings = UserNotificationSettings.create({ ...user.notificationSettings, ...body });
|
|
||||||
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>) {
|
update(userId: string, data: Partial<User>) {
|
||||||
return this.userRepository.update(userId, data);
|
return this.userRepository.update(userId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createUser(data: Partial<User>) {
|
||||||
|
const user = this.userRepository.create({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.userRepository.save(user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,11 @@ import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { CacheService } from '~/common/modules/cache/services';
|
||||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||||
import { OtpService } from '~/common/modules/otp/services';
|
import { OtpService } from '~/common/modules/otp/services';
|
||||||
|
import { JuniorTokenService } from '~/junior/services';
|
||||||
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
||||||
import {
|
import {
|
||||||
CreateUnverifiedUserRequestDto,
|
CreateUnverifiedUserRequestDto,
|
||||||
@ -13,11 +16,12 @@ import {
|
|||||||
LoginRequestDto,
|
LoginRequestDto,
|
||||||
SendForgetPasswordOtpRequestDto,
|
SendForgetPasswordOtpRequestDto,
|
||||||
SetEmailRequestDto,
|
SetEmailRequestDto,
|
||||||
|
setJuniorPasswordRequestDto,
|
||||||
} from '../dtos/request';
|
} from '../dtos/request';
|
||||||
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
||||||
import { User } from '../entities';
|
import { User } from '../entities';
|
||||||
import { GrantType, Roles } from '../enums';
|
import { GrantType, Roles } from '../enums';
|
||||||
import { ILoginResponse } from '../interfaces';
|
import { IJwtPayload, ILoginResponse } from '../interfaces';
|
||||||
import { removePadding, verifySignature } from '../utils';
|
import { removePadding, verifySignature } from '../utils';
|
||||||
import { DeviceService } from './device.service';
|
import { DeviceService } from './device.service';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
@ -32,6 +36,8 @@ export class AuthService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly deviceService: DeviceService,
|
private readonly deviceService: DeviceService,
|
||||||
|
private readonly juniorTokenService: JuniorTokenService,
|
||||||
|
private readonly cacheService: CacheService,
|
||||||
) {}
|
) {}
|
||||||
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
||||||
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
||||||
@ -181,11 +187,45 @@ export class AuthService {
|
|||||||
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
|
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deviceService.updateDevice(deviceId, { lastAccessOn: new Date() });
|
this.deviceService.updateDevice(deviceId, {
|
||||||
|
lastAccessOn: new Date(),
|
||||||
|
fcmToken: loginDto.fcmToken,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
return [tokens, user];
|
return [tokens, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
||||||
|
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
|
||||||
|
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||||
|
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
|
||||||
|
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
|
||||||
|
await this.juniorTokenService.invalidateToken(body.qrToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(refreshToken: string): Promise<[ILoginResponse, User]> {
|
||||||
|
try {
|
||||||
|
const isValid = await this.jwtService.verifyAsync<IJwtPayload>(refreshToken, {
|
||||||
|
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await this.userService.findUserOrThrow({ id: isValid.sub });
|
||||||
|
|
||||||
|
const tokens = await this.generateAuthToken(user);
|
||||||
|
|
||||||
|
return [tokens, user];
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException('AUTH.INVALID_REFRESH_TOKEN');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(req: Request) {
|
||||||
|
const accessToken = req.headers.authorization?.split(' ')[1] as string;
|
||||||
|
const expiryInTtl = this.jwtService.decode(accessToken).exp - Date.now() / ONE_THOUSAND;
|
||||||
|
return this.cacheService.set(accessToken, 'LOGOUT', expiryInTtl);
|
||||||
|
}
|
||||||
|
|
||||||
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
|
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
|
||||||
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
||||||
|
|
||||||
@ -209,7 +249,7 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
|
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanToken = removePadding(loginDto.deviceToken);
|
const cleanToken = removePadding(loginDto.signature);
|
||||||
const isValidToken = await verifySignature(
|
const isValidToken = await verifySignature(
|
||||||
device.publicKey,
|
device.publicKey,
|
||||||
cleanToken,
|
cleanToken,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Device } from '../entities';
|
import { Device } from '../entities';
|
||||||
import { DeviceRepository } from '../repositories';
|
import { DeviceRepository } from '../repositories';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceService {
|
export class DeviceService {
|
||||||
constructor(private readonly deviceRepository: DeviceRepository) {}
|
constructor(private readonly deviceRepository: DeviceRepository) {}
|
||||||
@ -16,4 +15,10 @@ export class DeviceService {
|
|||||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||||
return this.deviceRepository.updateDevice(deviceId, data);
|
return this.deviceRepository.updateDevice(deviceId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTokens(userId: string): Promise<string[]> {
|
||||||
|
const devices = await this.deviceRepository.getTokens(userId);
|
||||||
|
|
||||||
|
return devices.map((device) => device.fcmToken!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { FindOptionsWhere } from 'typeorm';
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
|
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
||||||
|
import { CustomerService } from '~/customer/services';
|
||||||
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
|
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
|
||||||
import { User } from '../entities';
|
import { User } from '../entities';
|
||||||
import { Roles } from '../enums';
|
import { Roles } from '../enums';
|
||||||
@ -8,18 +10,12 @@ import { UserRepository } from '../repositories';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
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) {
|
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||||
const user = await this.findUserOrThrow({ id: userId });
|
|
||||||
|
|
||||||
const notificationSettings = (await this.userRepository.updateNotificationSettings(user, body))
|
|
||||||
.notificationSettings;
|
|
||||||
|
|
||||||
return notificationSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
findUser(where: FindOptionsWhere<User>) {
|
|
||||||
return this.userRepository.findOne(where);
|
return this.userRepository.findOne(where);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,12 +40,19 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user && user.roles.includes(Roles.JUNIOR)) {
|
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
|
//TODO add role Guardian to the existing user and send OTP
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createUser(data: Partial<User>) {
|
||||||
|
const user = await this.userRepository.createUser(data);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
setEmail(userId: string, email: string) {
|
setEmail(userId: string, email: string) {
|
||||||
return this.userRepository.update(userId, { email });
|
return this.userRepository.update(userId, { email });
|
||||||
}
|
}
|
||||||
@ -58,7 +61,15 @@ export class UserService {
|
|||||||
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
|
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyUserAndCreateCustomer(user: User) {
|
async verifyUserAndCreateCustomer(user: User) {
|
||||||
return this.userRepository.verifyUserAndCreateCustomer(user);
|
await this.customerService.createCustomer(
|
||||||
|
{
|
||||||
|
guardian: Guardian.create({ id: user.id }),
|
||||||
|
notificationSettings: new CustomerNotificationSettings(),
|
||||||
|
},
|
||||||
|
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 './public.decorator';
|
||||||
export * from './user.decorator';
|
export * from './user.decorator';
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { IS_PUBLIC_KEY } from '../decorators';
|
import { IS_PUBLIC_KEY } from '../decorators';
|
||||||
|
import { CacheService } from '../modules/cache/services';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccessTokenGuard extends AuthGuard('access-token') {
|
export class AccessTokenGuard extends AuthGuard('access-token') {
|
||||||
constructor(private reflector: Reflector) {
|
constructor(protected reflector: Reflector, private readonly cacheService: CacheService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
async canActivate(context: ExecutionContext) {
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
context.getClass(),
|
context.getClass(),
|
||||||
@ -18,6 +18,17 @@ export class AccessTokenGuard extends AuthGuard('access-token') {
|
|||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.canActivate(context);
|
|
||||||
|
await super.canActivate(context);
|
||||||
|
|
||||||
|
const token = context.switchToHttp().getRequest().headers['authorization']?.split(' ')[1];
|
||||||
|
|
||||||
|
const isRevoked = await this.cacheService.get(token);
|
||||||
|
|
||||||
|
if (isRevoked) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './access-token.guard';
|
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));
|
||||||
|
}
|
||||||
|
}
|
18
src/common/modules/cache/cache.module.ts
vendored
Normal file
18
src/common/modules/cache/cache.module.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { buildKeyvOptions } from '~/core/module-options';
|
||||||
|
import { CacheService } from './services';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: 'CACHE_INSTANCE',
|
||||||
|
useFactory: (config: ConfigService) => buildKeyvOptions(config),
|
||||||
|
inject: [ConfigService],
|
||||||
|
},
|
||||||
|
CacheService,
|
||||||
|
],
|
||||||
|
exports: ['CACHE_INSTANCE', CacheService],
|
||||||
|
})
|
||||||
|
@Global()
|
||||||
|
export class CacheModule {}
|
19
src/common/modules/cache/services/cache.services.ts
vendored
Normal file
19
src/common/modules/cache/services/cache.services.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Cacheable } from 'cacheable';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheService {
|
||||||
|
constructor(@Inject('CACHE_INSTANCE') private readonly cache: Cacheable) {}
|
||||||
|
|
||||||
|
get<T>(key: string): Promise<T | undefined> {
|
||||||
|
return this.cache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T, ttl?: number | string): Promise<void> {
|
||||||
|
await this.cache.set(key, value, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string): Promise<void> {
|
||||||
|
await this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
1
src/common/modules/cache/services/index.ts
vendored
Normal file
1
src/common/modules/cache/services/index.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './cache.services';
|
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';
|
32
src/common/modules/lookup/controllers/lookup.controller.ts
Normal file
32
src/common/modules/lookup/controllers/lookup.controller.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@Get('default-task-logos')
|
||||||
|
@ApiDataArrayResponse(DocumentMetaResponseDto)
|
||||||
|
async findDefaultTaskLogos() {
|
||||||
|
const avatars = await this.lookupService.findDefaultTasksLogo();
|
||||||
|
|
||||||
|
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';
|
31
src/common/modules/lookup/services/lookup.service.ts
Normal file
31
src/common/modules/lookup/services/lookup.service.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DocumentType } from '~/document/enums';
|
||||||
|
import { DocumentService, OciService } from '~/document/services';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LookupService {
|
||||||
|
constructor(private readonly documentService: DocumentService, private readonly ociService: OciService) {}
|
||||||
|
async findDefaultAvatar() {
|
||||||
|
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
documents.map(async (document) => {
|
||||||
|
document.url = await this.ociService.generatePreSignedUrl(document);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findDefaultTasksLogo() {
|
||||||
|
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_TASKS_LOGO });
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
documents.map(async (document) => {
|
||||||
|
document.url = await this.ociService.generatePreSignedUrl(document);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return documents;
|
||||||
|
}
|
||||||
|
}
|
1
src/common/modules/notification/controllers/index.ts
Normal file
1
src/common/modules/notification/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './notifications.controller';
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Controller, Get, HttpCode, HttpStatus, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { AuthenticatedUser } from '~/common/decorators';
|
||||||
|
import { AccessTokenGuard } from '~/common/guards';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { NotificationsPageResponseDto } from '../dtos/response';
|
||||||
|
import { NotificationsService } from '../services/notifications.service';
|
||||||
|
|
||||||
|
@Controller('notifications')
|
||||||
|
@ApiTags('Notifications')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class NotificationsController {
|
||||||
|
constructor(private readonly notificationsService: NotificationsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiResponse({ type: NotificationsPageResponseDto })
|
||||||
|
async getNotifications(@AuthenticatedUser() { sub }: IJwtPayload, @Query() pageOptionsDto: PageOptionsRequestDto) {
|
||||||
|
const { notifications, count, unreadCount } = await this.notificationsService.getNotifications(sub, pageOptionsDto);
|
||||||
|
|
||||||
|
return new NotificationsPageResponseDto(notifications, {
|
||||||
|
itemCount: count,
|
||||||
|
unreadCount,
|
||||||
|
page: pageOptionsDto.page,
|
||||||
|
size: pageOptionsDto.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('mark-as-read')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
markAsRead(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||||
|
return this.notificationsService.markAsRead(sub);
|
||||||
|
}
|
||||||
|
}
|
2
src/common/modules/notification/dtos/response/index.ts
Normal file
2
src/common/modules/notification/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './notifications-page.response.dto';
|
||||||
|
export * from './notifications.response.dto';
|
@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { PageMetaResponseDto } from '~/core/dtos';
|
||||||
|
import { INotificationPageMeta } from '../../interfaces';
|
||||||
|
|
||||||
|
export class NotificationMetaResponseDto extends PageMetaResponseDto {
|
||||||
|
@ApiProperty({ example: 4 })
|
||||||
|
readonly unreadCount!: number;
|
||||||
|
|
||||||
|
constructor(meta: INotificationPageMeta) {
|
||||||
|
super(meta);
|
||||||
|
this.unreadCount = meta.unreadCount;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Notification } from '../../entities';
|
||||||
|
import { INotificationPageMeta } from '../../interfaces';
|
||||||
|
import { NotificationMetaResponseDto } from './notifications-meta.response.dto';
|
||||||
|
import { NotificationsResponseDto } from './notifications.response.dto';
|
||||||
|
|
||||||
|
export class NotificationsPageResponseDto {
|
||||||
|
@ApiProperty({ type: [NotificationsResponseDto] })
|
||||||
|
data!: NotificationsResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: NotificationMetaResponseDto })
|
||||||
|
meta: INotificationPageMeta;
|
||||||
|
|
||||||
|
constructor(data: Notification[], meta: INotificationPageMeta) {
|
||||||
|
this.data = data.map((notification) => new NotificationsResponseDto(notification));
|
||||||
|
this.meta = new NotificationMetaResponseDto(meta);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Notification } from '../../entities';
|
||||||
|
import { NotificationStatus } from '../../enums';
|
||||||
|
|
||||||
|
export class NotificationsResponseDto {
|
||||||
|
@ApiProperty({ example: 'f1b1b9b1-1b1b-1b1b-1b1b-1b1b1b1b1b1b' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Test' })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'TestBody' })
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: NotificationStatus.UNREAD })
|
||||||
|
status!: NotificationStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2021-09-01T00:00:00.000Z' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
constructor(notification: Notification) {
|
||||||
|
this.id = notification.id;
|
||||||
|
this.title = notification.title;
|
||||||
|
this.body = notification.message;
|
||||||
|
this.status = notification.status!;
|
||||||
|
this.createdAt = notification.createdAt;
|
||||||
|
}
|
||||||
|
}
|
1
src/common/modules/notification/entities/index.ts
Normal file
1
src/common/modules/notification/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './notification.entity';
|
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from '~/auth/entities';
|
||||||
|
import { NotificationChannel, NotificationScope, NotificationStatus } from '../enums';
|
||||||
|
|
||||||
|
@Entity('notifications')
|
||||||
|
export class Notification {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'title' })
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'message' })
|
||||||
|
message!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'recipient', nullable: true })
|
||||||
|
recipient?: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'scope' })
|
||||||
|
scope!: NotificationScope;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'status', nullable: true })
|
||||||
|
status!: NotificationStatus | null;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'channel' })
|
||||||
|
channel!: NotificationChannel;
|
||||||
|
|
||||||
|
@Column('uuid', { name: 'user_id', nullable: true })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.notifications, { onDelete: 'CASCADE', nullable: true })
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
3
src/common/modules/notification/enums/index.ts
Normal file
3
src/common/modules/notification/enums/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './notification-channel.enum';
|
||||||
|
export * from './notification-scope.enum';
|
||||||
|
export * from './notification-status.enum';
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum NotificationChannel {
|
||||||
|
EMAIL = 'EMAIL',
|
||||||
|
SMS = 'SMS',
|
||||||
|
PUSH = 'PUSH',
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum NotificationScope {
|
||||||
|
USER_REGISTERED = 'USER_REGISTERED',
|
||||||
|
TASK_COMPLETED = 'TASK_COMPLETED',
|
||||||
|
GIFT_RECEIVED = 'GIFT_RECEIVED',
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum NotificationStatus {
|
||||||
|
READ = 'READ',
|
||||||
|
UNREAD = 'UNREAD',
|
||||||
|
}
|
1
src/common/modules/notification/interfaces/index.ts
Normal file
1
src/common/modules/notification/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './notification-page-meta.interface';
|
@ -0,0 +1,5 @@
|
|||||||
|
import { IPageMeta } from '~/core/dtos';
|
||||||
|
|
||||||
|
export interface INotificationPageMeta extends IPageMeta {
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
26
src/common/modules/notification/notification.module.ts
Normal file
26
src/common/modules/notification/notification.module.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TwilioModule } from 'nestjs-twilio';
|
||||||
|
import { AuthModule } from '~/auth/auth.module';
|
||||||
|
import { buildTwilioOptions } from '~/core/module-options';
|
||||||
|
import { NotificationsController } from './controllers';
|
||||||
|
import { Notification } from './entities';
|
||||||
|
import { NotificationsRepository } from './repositories';
|
||||||
|
import { FirebaseService } from './services/firebase.service';
|
||||||
|
import { NotificationsService } from './services/notifications.service';
|
||||||
|
import { TwilioService } from './services/twilio.service';
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Notification]),
|
||||||
|
AuthModule,
|
||||||
|
TwilioModule.forRootAsync({
|
||||||
|
useFactory: buildTwilioOptions,
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService],
|
||||||
|
exports: [NotificationsService],
|
||||||
|
controllers: [NotificationsController],
|
||||||
|
})
|
||||||
|
export class NotificationModule {}
|
1
src/common/modules/notification/repositories/index.ts
Normal file
1
src/common/modules/notification/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './notifications.repository';
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { Notification } from '../entities';
|
||||||
|
import { NotificationChannel, NotificationStatus } from '../enums';
|
||||||
|
const ONE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationsRepository {
|
||||||
|
constructor(@InjectRepository(Notification) private readonly notificationsRepository: Repository<Notification>) {}
|
||||||
|
|
||||||
|
getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
|
||||||
|
const readQuery = this.notificationsRepository.createQueryBuilder('notification');
|
||||||
|
readQuery.where('notification.userId = :userId', { userId });
|
||||||
|
readQuery.andWhere('notification.channel = :channel', { channel: NotificationChannel.PUSH });
|
||||||
|
readQuery.orderBy('notification.createdAt', 'DESC');
|
||||||
|
readQuery.skip((pageOptionsDto.page - ONE) * pageOptionsDto.size);
|
||||||
|
readQuery.take(pageOptionsDto.size);
|
||||||
|
return readQuery.getManyAndCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnreadNotificationsCount(userId: string) {
|
||||||
|
return this.notificationsRepository.count({ where: { userId, status: NotificationStatus.UNREAD } });
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsRead(userId: string) {
|
||||||
|
return this.notificationsRepository.update(
|
||||||
|
{ userId, status: NotificationStatus.UNREAD },
|
||||||
|
{ status: NotificationStatus.READ },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification(notification: Partial<Notification>) {
|
||||||
|
return this.notificationsRepository.save(notification);
|
||||||
|
}
|
||||||
|
}
|
27
src/common/modules/notification/services/firebase.service.ts
Normal file
27
src/common/modules/notification/services/firebase.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as admin from 'firebase-admin';
|
||||||
|
@Injectable()
|
||||||
|
export class FirebaseService {
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert({
|
||||||
|
projectId: this.configService.get('FIREBASE_PROJECT_ID'),
|
||||||
|
clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'),
|
||||||
|
privateKey: this.configService.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNotification(tokens: string | string[], title: string, body: string) {
|
||||||
|
const message = {
|
||||||
|
notification: {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
tokens: Array.isArray(tokens) ? tokens : [tokens],
|
||||||
|
};
|
||||||
|
|
||||||
|
admin.messaging().sendEachForMulticast(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DeviceService } from '~/auth/services';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { Notification } from '../entities';
|
||||||
|
import { NotificationsRepository } from '../repositories';
|
||||||
|
import { FirebaseService } from './firebase.service';
|
||||||
|
import { TwilioService } from './twilio.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deviceService: DeviceService,
|
||||||
|
private readonly firebaseService: FirebaseService,
|
||||||
|
private readonly notificationRepository: NotificationsRepository,
|
||||||
|
private readonly twilioService: TwilioService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async sendPushNotification(userId: string, title: string, body: string) {
|
||||||
|
// Get the device tokens for the user
|
||||||
|
|
||||||
|
const tokens = await this.deviceService.getTokens(userId);
|
||||||
|
|
||||||
|
if (!tokens.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Send the notification
|
||||||
|
return this.firebaseService.sendNotification(tokens, title, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendSMS(to: string, body: string) {
|
||||||
|
await this.twilioService.sendSMS(to, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
|
||||||
|
const [[notifications, count], unreadCount] = await Promise.all([
|
||||||
|
this.notificationRepository.getNotifications(userId, pageOptionsDto),
|
||||||
|
this.notificationRepository.getUnreadNotificationsCount(userId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { notifications, count, unreadCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification(notification: Partial<Notification>) {
|
||||||
|
return this.notificationRepository.createNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsRead(userId: string) {
|
||||||
|
return this.notificationRepository.markAsRead(userId);
|
||||||
|
}
|
||||||
|
}
|
21
src/common/modules/notification/services/twilio.service.ts
Normal file
21
src/common/modules/notification/services/twilio.service.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TwilioService as TwilioApiService } from 'nestjs-twilio';
|
||||||
|
import { Environment } from '~/core/enums';
|
||||||
|
@Injectable()
|
||||||
|
export class TwilioService {
|
||||||
|
private logger = new Logger(TwilioService.name);
|
||||||
|
constructor(private readonly twilioService: TwilioApiService, private readonly configService: ConfigService) {}
|
||||||
|
private from = this.configService.getOrThrow('TWILIO_PHONE_NUMBER');
|
||||||
|
sendSMS(to: string, body: string) {
|
||||||
|
if (this.configService.get('NODE_ENV') === Environment.DEV) {
|
||||||
|
this.logger.log(`Skipping SMS sending in DEV environment. Message: ${body} to: ${to}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.twilioService.client.messages.create({
|
||||||
|
body,
|
||||||
|
to,
|
||||||
|
from: this.from,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,6 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|||||||
const httpCtx: HttpArgumentsHost = host.switchToHttp();
|
const httpCtx: HttpArgumentsHost = host.switchToHttp();
|
||||||
const res = httpCtx.getResponse<ExpressResponse>();
|
const res = httpCtx.getResponse<ExpressResponse>();
|
||||||
const i18n = new I18nContextWrapper(I18nContext.current());
|
const i18n = new I18nContextWrapper(I18nContext.current());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = this.extractStatusCode(exception);
|
const status = this.extractStatusCode(exception);
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
export * from '././keyv-options';
|
||||||
export * from './config-options';
|
export * from './config-options';
|
||||||
export * from './typeorm-options';
|
|
||||||
export * from './logger-options';
|
export * from './logger-options';
|
||||||
|
export * from './twilio-options';
|
||||||
|
export * from './typeorm-options';
|
||||||
|
8
src/core/module-options/keyv-options.ts
Normal file
8
src/core/module-options/keyv-options.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import KeyvRedis from '@keyv/redis';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Cacheable } from 'cacheable';
|
||||||
|
|
||||||
|
export function buildKeyvOptions(config: ConfigService) {
|
||||||
|
const secondary = new KeyvRedis(config.get('REDIS_URL'));
|
||||||
|
return new Cacheable({ secondary, ttl: config.get('REDIS_TTL') });
|
||||||
|
}
|
9
src/core/module-options/twilio-options.ts
Normal file
9
src/core/module-options/twilio-options.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TwilioModuleOptions } from 'nestjs-twilio';
|
||||||
|
|
||||||
|
export function buildTwilioOptions(config: ConfigService): TwilioModuleOptions {
|
||||||
|
return {
|
||||||
|
accountSid: config.get('TWILIO_ACCOUNT_SID'),
|
||||||
|
authToken: config.get('TWILIO_AUTH_TOKEN'),
|
||||||
|
};
|
||||||
|
}
|
1
src/core/types/index.ts
Normal file
1
src/core/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './object-values.type';
|
13
src/core/types/object-values.type.ts
Normal file
13
src/core/types/object-values.type.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Converts an object's values to a union type.
|
||||||
|
* Should be used in conjunction with constant objects to define enums.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const Status = { SUCCEEDED: 'Success', FAILED: 'Failure' } as const;
|
||||||
|
* type Status = ObjectValues<typeof Status>; // 'Success' | 'Failure'
|
||||||
|
* const foo: Status = Status.SUCCEEDED;
|
||||||
|
* const bar: Status = 'Failure';
|
||||||
|
*
|
||||||
|
* @see https://youtu.be/jjMbPt_H3RQ
|
||||||
|
*/
|
||||||
|
export type ObjectValues<T> = T[keyof T];
|
@ -1,8 +1,10 @@
|
|||||||
import { Body, Controller, Patch, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Headers, Patch, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { IJwtPayload } from '~/auth/interfaces';
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||||
import { AuthenticatedUser } from '~/common/decorators';
|
import { AuthenticatedUser } from '~/common/decorators';
|
||||||
import { AccessTokenGuard } from '~/common/guards';
|
import { AccessTokenGuard } from '~/common/guards';
|
||||||
|
import { ApiDataResponse } from '~/core/decorators';
|
||||||
import { ResponseFactory } from '~/core/utils';
|
import { ResponseFactory } from '~/core/utils';
|
||||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||||
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
|
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
|
||||||
@ -11,11 +13,21 @@ import { CustomerService } from '../services';
|
|||||||
@Controller('customers')
|
@Controller('customers')
|
||||||
@ApiTags('Customers')
|
@ApiTags('Customers')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(AccessTokenGuard)
|
|
||||||
export class CustomerController {
|
export class CustomerController {
|
||||||
constructor(private readonly customerService: CustomerService) {}
|
constructor(private readonly customerService: CustomerService) {}
|
||||||
|
|
||||||
|
@Get('/profile')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiDataResponse(CustomerResponseDto)
|
||||||
|
async getCustomerProfile(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||||
|
const customer = await this.customerService.findCustomerById(sub);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||||
|
}
|
||||||
|
|
||||||
@Patch('')
|
@Patch('')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiDataResponse(CustomerResponseDto)
|
||||||
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
|
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
|
||||||
const customer = await this.customerService.updateCustomer(sub, body);
|
const customer = await this.customerService.updateCustomer(sub, body);
|
||||||
|
|
||||||
@ -23,11 +35,14 @@ export class CustomerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch('settings/notifications')
|
@Patch('settings/notifications')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiDataResponse(NotificationSettingsResponseDto)
|
||||||
async updateNotificationSettings(
|
async updateNotificationSettings(
|
||||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
@Body() body: UpdateNotificationsSettingsRequestDto,
|
@Body() body: UpdateNotificationsSettingsRequestDto,
|
||||||
|
@Headers(DEVICE_ID_HEADER) deviceId: string,
|
||||||
) {
|
) {
|
||||||
const notificationSettings = await this.customerService.updateNotificationSettings(sub, body);
|
const notificationSettings = await this.customerService.updateNotificationSettings(sub, body, deviceId);
|
||||||
|
|
||||||
return ResponseFactory.data(new NotificationSettingsResponseDto(notificationSettings));
|
return ResponseFactory.data(new NotificationSettingsResponseDto(notificationSettings));
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthModule } from '~/auth/auth.module';
|
import { AuthModule } from '~/auth/auth.module';
|
||||||
import { CustomerController } from './controllers';
|
import { CustomerController } from './controllers';
|
||||||
import { Customer } from './entities';
|
import { Customer } from './entities';
|
||||||
|
import { CustomerNotificationSettings } from './entities/customer-notification-settings.entity';
|
||||||
import { CustomerRepository } from './repositories/customer.repository';
|
import { CustomerRepository } from './repositories/customer.repository';
|
||||||
import { CustomerService } from './services';
|
import { CustomerService } from './services';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Customer]), AuthModule],
|
imports: [TypeOrmModule.forFeature([Customer, CustomerNotificationSettings]), forwardRef(() => AuthModule)],
|
||||||
controllers: [CustomerController],
|
controllers: [CustomerController],
|
||||||
providers: [CustomerService, CustomerRepository],
|
providers: [CustomerService, CustomerRepository],
|
||||||
|
exports: [CustomerService],
|
||||||
})
|
})
|
||||||
export class CustomerModule {}
|
export class CustomerModule {}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
import { IsAbove18 } from '~/core/decorators/validations';
|
import { IsAbove18 } from '~/core/decorators/validations';
|
||||||
export class UpdateCustomerRequestDto {
|
export class UpdateCustomerRequestDto {
|
||||||
@ -26,4 +26,8 @@ export class UpdateCustomerRequestDto {
|
|||||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
dateOfBirth!: Date;
|
dateOfBirth!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) })
|
||||||
|
profilePictureId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsBoolean, IsOptional } from 'class-validator';
|
import { IsBoolean, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
export class UpdateNotificationsSettingsRequestDto {
|
export class UpdateNotificationsSettingsRequestDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean()
|
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isEmailEnabled' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isEmailEnabled!: boolean;
|
isEmailEnabled!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean()
|
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isPushEnabled' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isPushEnabled!: boolean;
|
isPushEnabled!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean()
|
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isSmsEnabled' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isSmsEnabled!: boolean;
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsString({ message: i18n('validation.isString', { path: 'general', property: 'customer.fcmToken' }) })
|
||||||
|
@ValidateIf((o) => o.isPushEnabled)
|
||||||
|
fcmToken?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
|
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||||
|
import { NotificationSettingsResponseDto } from './notification-settings.response.dto';
|
||||||
|
|
||||||
export class CustomerResponseDto {
|
export class CustomerResponseDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@ -50,6 +52,12 @@ export class CustomerResponseDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isGuardian!: boolean;
|
isGuardian!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
notificationSettings!: NotificationSettingsResponseDto;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
|
||||||
|
profilePicture!: DocumentMetaResponseDto | null;
|
||||||
|
|
||||||
constructor(customer: Customer) {
|
constructor(customer: Customer) {
|
||||||
this.id = customer.id;
|
this.id = customer.id;
|
||||||
this.customerStatus = customer.customerStatus;
|
this.customerStatus = customer.customerStatus;
|
||||||
@ -67,5 +75,7 @@ export class CustomerResponseDto {
|
|||||||
this.gender = customer.gender;
|
this.gender = customer.gender;
|
||||||
this.isJunior = customer.isJunior;
|
this.isJunior = customer.isJunior;
|
||||||
this.isGuardian = customer.isGuardian;
|
this.isGuardian = customer.isGuardian;
|
||||||
|
this.notificationSettings = new NotificationSettingsResponseDto(customer.notificationSettings);
|
||||||
|
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { UserNotificationSettings } from '~/auth/entities';
|
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
||||||
|
|
||||||
export class NotificationSettingsResponseDto {
|
export class NotificationSettingsResponseDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@ -11,7 +11,7 @@ export class NotificationSettingsResponseDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isSmsEnabled!: boolean;
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
constructor(notificationSettings: UserNotificationSettings) {
|
constructor(notificationSettings: CustomerNotificationSettings) {
|
||||||
this.isEmailEnabled = notificationSettings.isEmailEnabled;
|
this.isEmailEnabled = notificationSettings.isEmailEnabled;
|
||||||
this.isPushEnabled = notificationSettings.isPushEnabled;
|
this.isPushEnabled = notificationSettings.isPushEnabled;
|
||||||
this.isSmsEnabled = notificationSettings.isSmsEnabled;
|
this.isSmsEnabled = notificationSettings.isSmsEnabled;
|
||||||
|
@ -8,10 +8,10 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from './user.entity';
|
import { Customer } from '~/customer/entities';
|
||||||
|
|
||||||
@Entity('user_notification_settings')
|
@Entity('cutsomer_notification_settings')
|
||||||
export class UserNotificationSettings extends BaseEntity {
|
export class CustomerNotificationSettings extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ -24,9 +24,9 @@ export class UserNotificationSettings extends BaseEntity {
|
|||||||
@Column({ name: 'is_sms_enabled', default: false })
|
@Column({ name: 'is_sms_enabled', default: false })
|
||||||
isSmsEnabled!: boolean;
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
@OneToOne(() => User, (user) => user.notificationSettings, { onDelete: 'CASCADE' })
|
@OneToOne(() => Customer, (customer) => customer.notificationSettings, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'user_id' })
|
@JoinColumn({ name: 'customer_id' })
|
||||||
user!: User;
|
customer!: Customer;
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
@ -9,8 +9,12 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '~/auth/entities';
|
import { User } from '~/auth/entities';
|
||||||
|
import { Document } from '~/document/entities';
|
||||||
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { CustomerNotificationSettings } from './customer-notification-settings.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity('customers')
|
||||||
export class Customer extends BaseEntity {
|
export class Customer extends BaseEntity {
|
||||||
@PrimaryColumn('uuid')
|
@PrimaryColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -63,10 +67,29 @@ export class Customer extends BaseEntity {
|
|||||||
@Column('varchar', { name: 'user_id' })
|
@Column('varchar', { name: 'user_id' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
||||||
|
profilePictureId!: string;
|
||||||
|
|
||||||
|
@OneToOne(() => CustomerNotificationSettings, (notificationSettings) => notificationSettings.customer, {
|
||||||
|
cascade: true,
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
notificationSettings!: CustomerNotificationSettings;
|
||||||
|
|
||||||
|
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
|
||||||
|
@JoinColumn({ name: 'profile_picture_id' })
|
||||||
|
profilePicture!: Document;
|
||||||
|
|
||||||
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
|
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'user_id' })
|
@JoinColumn({ name: 'user_id' })
|
||||||
user!: User;
|
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' })
|
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -1,18 +1,41 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||||
import { UpdateCustomerRequestDto } from '../dtos/request';
|
import { User } from '~/auth/entities';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||||
import { Customer } from '../entities';
|
import { Customer } from '../entities';
|
||||||
|
import { CustomerNotificationSettings } from '../entities/customer-notification-settings.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomerRepository {
|
export class CustomerRepository {
|
||||||
constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {}
|
constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {}
|
||||||
|
|
||||||
updateCustomer(id: string, data: UpdateCustomerRequestDto) {
|
updateCustomer(id: string, data: Partial<Customer>) {
|
||||||
return this.customerRepository.update(id, data);
|
return this.customerRepository.update(id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(where: FindOptionsWhere<Customer>) {
|
findOne(where: FindOptionsWhere<Customer>) {
|
||||||
return this.customerRepository.findOne({ where });
|
return this.customerRepository.findOne({ where, relations: ['profilePicture'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNotificationSettings(customer: Customer, body: UpdateNotificationsSettingsRequestDto) {
|
||||||
|
customer.notificationSettings = CustomerNotificationSettings.create({
|
||||||
|
...customer.notificationSettings,
|
||||||
|
...body,
|
||||||
|
});
|
||||||
|
return this.customerRepository.save(customer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,32 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { UserService } from '~/auth/services/user.service';
|
import { User } from '~/auth/entities';
|
||||||
|
import { DeviceService } from '~/auth/services';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||||
import { Customer } from '../entities';
|
import { Customer } from '../entities';
|
||||||
import { CustomerRepository } from '../repositories/customer.repository';
|
import { CustomerRepository } from '../repositories/customer.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomerService {
|
export class CustomerService {
|
||||||
constructor(private readonly userService: UserService, private readonly customerRepository: CustomerRepository) {}
|
constructor(
|
||||||
updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
|
private readonly customerRepository: CustomerRepository,
|
||||||
return this.userService.updateNotificationSettings(userId, data);
|
private readonly ociService: OciService,
|
||||||
|
@Inject(forwardRef(() => DeviceService)) private readonly deviceService: DeviceService,
|
||||||
|
) {}
|
||||||
|
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId: string) {
|
||||||
|
const customer = await this.findCustomerById(userId);
|
||||||
|
|
||||||
|
const notificationSettings = (await this.customerRepository.updateNotificationSettings(customer, data))
|
||||||
|
.notificationSettings;
|
||||||
|
|
||||||
|
if (data.isPushEnabled && deviceId) {
|
||||||
|
await this.deviceService.updateDevice(deviceId, {
|
||||||
|
fcmToken: data.fcmToken,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
|
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
|
||||||
@ -16,11 +34,20 @@ export class CustomerService {
|
|||||||
return this.findCustomerById(userId);
|
return this.findCustomerById(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createCustomer(customerData: Partial<Customer>, user: User) {
|
||||||
|
return this.customerRepository.createCustomer(customerData, user);
|
||||||
|
}
|
||||||
|
|
||||||
async findCustomerById(id: string) {
|
async findCustomerById(id: string) {
|
||||||
const customer = await this.customerRepository.findOne({ id });
|
const customer = await this.customerRepository.findOne({ id });
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
throw new BadRequestException('CUSTOMER.NOT_FOUND');
|
throw new BadRequestException('CUSTOMER.NOT_FOUND');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customer.profilePicture) {
|
||||||
|
customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture);
|
||||||
|
}
|
||||||
return customer;
|
return customer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export class CreateDocumentEntity1732434281561 implements MigrationInterface {
|
|||||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
"name" character varying(255) NOT NULL,
|
"name" character varying(255) NOT NULL,
|
||||||
"extension" character varying(255) NOT NULL,
|
"extension" character varying(255) NOT NULL,
|
||||||
"documentType" character varying(255) NOT NULL,
|
"document_type" character varying(255) NOT NULL,
|
||||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
|
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
|
||||||
|
@ -16,16 +16,10 @@ export class CreateUserEntity1733206728721 implements MigrationInterface {
|
|||||||
"apple_id" character varying(255),
|
"apple_id" character varying(255),
|
||||||
"is_profile_completed" boolean NOT NULL DEFAULT false,
|
"is_profile_completed" boolean NOT NULL DEFAULT false,
|
||||||
"roles" text array,
|
"roles" text array,
|
||||||
"profile_picture_id" uuid,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
|
|
||||||
CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class CreateNotificationSettingsTable1733231692252 implements MigrationInterface {
|
|
||||||
name = 'CreateNotificationSettingsTable1733231692252';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "user_notification_settings"
|
|
||||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
|
||||||
"is_email_enabled" boolean NOT NULL DEFAULT false,
|
|
||||||
"is_push_enabled" boolean NOT NULL DEFAULT false,
|
|
||||||
"is_sms_enabled" boolean NOT NULL DEFAULT false,
|
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
||||||
"user_id" uuid, CONSTRAINT "REL_52182ffd0f785e8256f8fcb4fd" UNIQUE ("user_id"),
|
|
||||||
CONSTRAINT "PK_a195de67d093e096152f387afbd" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "user_notification_settings" ADD CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "user_notification_settings" DROP CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(`DROP TABLE "user_notification_settings"`);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
|
|||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`CREATE TABLE "customer"
|
`CREATE TABLE "customers"
|
||||||
("id" uuid NOT NULL,
|
("id" uuid NOT NULL,
|
||||||
"customer_status" character varying(255) NOT NULL DEFAULT 'PENDING',
|
"customer_status" character varying(255) NOT NULL DEFAULT 'PENDING',
|
||||||
"rejection_reason" text,
|
"rejection_reason" text,
|
||||||
@ -22,19 +22,25 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
|
|||||||
"gender" character varying(255),
|
"gender" character varying(255),
|
||||||
"is_junior" boolean NOT NULL DEFAULT false,
|
"is_junior" boolean NOT NULL DEFAULT false,
|
||||||
"is_guardian" boolean NOT NULL DEFAULT false,
|
"is_guardian" boolean NOT NULL DEFAULT false,
|
||||||
|
"profile_picture_id" uuid,
|
||||||
"user_id" uuid NOT NULL,
|
"user_id" uuid NOT NULL,
|
||||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
|
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
|
||||||
|
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
|
||||||
CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`,
|
CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`,
|
||||||
);
|
);
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`ALTER TABLE "customer" ADD CONSTRAINT "FK_5d1f609371a285123294fddcf3a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "customers" ADD CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "customer" DROP CONSTRAINT "FK_5d1f609371a285123294fddcf3a"`);
|
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661"`);
|
||||||
await queryRunner.query(`DROP TABLE "customer"`);
|
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "customers"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 "customers"("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 "customers"("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"`);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user