mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-10-23 18:37:56 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7028fa64c | ||
|
|
0750509a85 | ||
|
|
4d9ebe729e | ||
|
|
bb8cc33d53 | ||
|
|
e933cacdcf | ||
|
|
3719498c2f | ||
|
|
c7470302bd | ||
|
|
5e9e83cb74 | ||
|
|
4cef58580e | ||
|
|
0ba09cbf8b | ||
|
|
28a2cb5d75 | ||
|
|
4961a192ea | ||
|
|
8ab47f3835 | ||
|
|
8112fb81a2 | ||
|
|
2c3c862c4a | ||
|
|
93f5d83825 | ||
|
|
ea60ac3d7b | ||
|
|
0748695f23 | ||
|
|
a201692c0c | ||
|
|
fd6c1d1442 |
Generated
+1211
-48
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,7 @@
|
||||
"cacheable": "^1.8.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"firebase-admin": "^13.0.2",
|
||||
"google-libphonenumber": "^3.2.39",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.4.1",
|
||||
@@ -56,6 +57,7 @@
|
||||
"moment": "^2.30.1",
|
||||
"nestjs-i18n": "^10.4.9",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"nestjs-twilio": "^4.4.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"oci-common": "^2.99.0",
|
||||
"oci-sdk": "^2.99.0",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './allowance-change-request.response.dto';
|
||||
export * from './allowance.response.dto';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum AllowanceFrequency {
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum AllowanceType {
|
||||
BY_END_DATE = 'BY_END_DATE',
|
||||
BY_COUNT = 'BY_COUNT',
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './allowance-change-request.repository';
|
||||
export * from './allowances.repository';
|
||||
@@ -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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './allowance-change-requests.service';
|
||||
export * from './allowances.service';
|
||||
@@ -6,9 +6,11 @@ import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
||||
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 { 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 { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
||||
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
||||
@@ -17,12 +19,14 @@ import { buildValidationPipe } from './core/pipes';
|
||||
import { CustomerModule } from './customer/customer.module';
|
||||
import { migrations } from './db';
|
||||
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 { 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({
|
||||
controllers: [],
|
||||
imports: [
|
||||
@@ -48,22 +52,28 @@ import { TaskModule } from './task/task.module';
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
I18nModule.forRoot(buildI18nOptions()),
|
||||
|
||||
CacheModule,
|
||||
|
||||
// App modules
|
||||
AuthModule,
|
||||
CustomerModule,
|
||||
JuniorModule,
|
||||
|
||||
TaskModule,
|
||||
GuardianModule,
|
||||
SavingGoalsModule,
|
||||
AllowanceModule,
|
||||
MoneyRequestModule,
|
||||
GiftModule,
|
||||
|
||||
OtpModule,
|
||||
DocumentModule,
|
||||
LookupModule,
|
||||
|
||||
HealthModule,
|
||||
|
||||
NotificationModule,
|
||||
],
|
||||
providers: [
|
||||
// Global Pipes
|
||||
|
||||
@@ -19,6 +19,6 @@ import { AccessTokenStrategy } from './strategies';
|
||||
],
|
||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [UserService],
|
||||
exports: [UserService, DeviceService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { Request } from 'express';
|
||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
EnableBiometricRequestDto,
|
||||
ForgetPasswordRequestDto,
|
||||
LoginRequestDto,
|
||||
RefreshTokenRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SetEmailRequestDto,
|
||||
setJuniorPasswordRequestDto,
|
||||
@@ -85,9 +87,23 @@ export class AuthController {
|
||||
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')
|
||||
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
||||
const [res, user] = await this.authService.login(loginDto, deviceId);
|
||||
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,6 +3,7 @@ export * from './disable-biometric.request.dto';
|
||||
export * from './enable-biometric.request.dto';
|
||||
export * from './forget-password.request.dto';
|
||||
export * from './login.request.dto';
|
||||
export * from './refresh-token.request.dto';
|
||||
export * from './send-forget-password-otp.request.dto';
|
||||
export * from './set-email.request.dto';
|
||||
export * from './set-junior-password.request.dto';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { GrantType } from '~/auth/enums';
|
||||
export class LoginRequestDto {
|
||||
@@ -17,8 +17,14 @@ export class LoginRequestDto {
|
||||
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ example: 'device-token' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceToken' }) })
|
||||
@ApiProperty({ example: 'fcm-device-token' })
|
||||
@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)
|
||||
deviceToken!: string;
|
||||
signature!: string;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,6 +15,9 @@ export class Device {
|
||||
@Column('varchar', { name: 'public_key', nullable: true })
|
||||
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' })
|
||||
lastAccessOn!: Date;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Notification } from '~/common/modules/notification/entities';
|
||||
import { Otp } from '~/common/modules/otp/entities';
|
||||
import { Customer } from '~/customer/entities/customer.entity';
|
||||
import { Roles } from '../enums';
|
||||
@@ -54,6 +55,9 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => Device, (device) => device.user)
|
||||
devices!: Device[];
|
||||
|
||||
@OneToMany(() => Notification, (notification) => notification.user)
|
||||
notifications!: Notification[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { Device } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
@@ -16,6 +16,10 @@ export class DeviceRepository {
|
||||
}
|
||||
|
||||
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'] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
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 { OtpService } from '~/common/modules/otp/services';
|
||||
import { JuniorTokenService } from '~/junior/services';
|
||||
@@ -19,7 +21,7 @@ import {
|
||||
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
||||
import { User } from '../entities';
|
||||
import { GrantType, Roles } from '../enums';
|
||||
import { ILoginResponse } from '../interfaces';
|
||||
import { IJwtPayload, ILoginResponse } from '../interfaces';
|
||||
import { removePadding, verifySignature } from '../utils';
|
||||
import { DeviceService } from './device.service';
|
||||
import { UserService } from './user.service';
|
||||
@@ -35,6 +37,7 @@ export class AuthService {
|
||||
private readonly userService: UserService,
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly juniorTokenService: JuniorTokenService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
||||
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
||||
@@ -184,7 +187,11 @@ export class AuthService {
|
||||
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];
|
||||
}
|
||||
@@ -197,6 +204,28 @@ export class AuthService {
|
||||
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> {
|
||||
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
||||
|
||||
@@ -220,7 +249,7 @@ export class AuthService {
|
||||
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
|
||||
}
|
||||
|
||||
const cleanToken = removePadding(loginDto.deviceToken);
|
||||
const cleanToken = removePadding(loginDto.signature);
|
||||
const isValidToken = await verifySignature(
|
||||
device.publicKey,
|
||||
cleanToken,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Device } from '../entities';
|
||||
import { DeviceRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class DeviceService {
|
||||
constructor(private readonly deviceRepository: DeviceRepository) {}
|
||||
@@ -16,4 +15,10 @@ export class DeviceService {
|
||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||
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,15 +1,15 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Observable } from 'rxjs';
|
||||
import { IS_PUBLIC_KEY } from '../decorators';
|
||||
import { CacheService } from '../modules/cache/services';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenGuard extends AuthGuard('access-token') {
|
||||
constructor(protected reflector: Reflector) {
|
||||
constructor(protected reflector: Reflector, private readonly cacheService: CacheService) {
|
||||
super();
|
||||
}
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
@@ -18,6 +18,17 @@ export class AccessTokenGuard extends AuthGuard('access-token') {
|
||||
if (isPublic) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { buildKeyvOptions } from '~/core/module-options';
|
||||
import { CacheService } from './services';
|
||||
@@ -14,4 +14,5 @@ import { CacheService } from './services';
|
||||
],
|
||||
exports: ['CACHE_INSTANCE', CacheService],
|
||||
})
|
||||
@Global()
|
||||
export class CacheModule {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 res = httpCtx.getResponse<ExpressResponse>();
|
||||
const i18n = new I18nContextWrapper(I18nContext.current());
|
||||
|
||||
try {
|
||||
const status = this.extractStatusCode(exception);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from '././keyv-options';
|
||||
export * from './config-options';
|
||||
export * from './logger-options';
|
||||
export * from './twilio-options';
|
||||
export * from './typeorm-options';
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Headers, Patch, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ApiDataResponse } from '~/core/decorators';
|
||||
@@ -39,8 +40,9 @@ export class CustomerController {
|
||||
async updateNotificationSettings(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@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));
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class UpdateNotificationsSettingsRequestDto {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isEmailEnabled' }) })
|
||||
@IsOptional()
|
||||
isEmailEnabled!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isPushEnabled' }) })
|
||||
@IsOptional()
|
||||
isPushEnabled!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isSmsEnabled' }) })
|
||||
@IsOptional()
|
||||
isSmsEnabled!: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsString({ message: i18n('validation.isString', { path: 'general', property: 'customer.fcmToken' }) })
|
||||
@ValidateIf((o) => o.isPushEnabled)
|
||||
fcmToken?: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { User } from '~/auth/entities';
|
||||
import { DeviceService } from '~/auth/services';
|
||||
import { OciService } from '~/document/services';
|
||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { Customer } from '../entities';
|
||||
@@ -7,13 +8,24 @@ import { CustomerRepository } from '../repositories/customer.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerService {
|
||||
constructor(private readonly customerRepository: CustomerRepository, private readonly ociService: OciService) {}
|
||||
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
|
||||
constructor(
|
||||
private readonly customerRepository: CustomerRepository,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateAllowanceEntities1734601976591 implements MigrationInterface {
|
||||
name = 'CreateAllowanceEntities1734601976591';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "allowances"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"amount" numeric(10,2) NOT NULL,
|
||||
"frequency" character varying(255) NOT NULL,
|
||||
"type" character varying(255) NOT NULL,
|
||||
"start_date" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
"end_date" TIMESTAMP WITH TIME ZONE,
|
||||
"number_of_transactions" integer,
|
||||
"guardian_id" uuid NOT NULL,
|
||||
"junior_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"deleted_at" TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT "PK_3731e781e7c4e932ba4d4213ac1" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "allowance_change_requests"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"reason" text NOT NULL,
|
||||
"amount" numeric(10,2) NOT NULL,
|
||||
"status" character varying(255) NOT NULL DEFAULT 'PENDING',
|
||||
"allowance_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_664715670e1e72c64ce65a078de" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "allowances" ADD CONSTRAINT "FK_80b144a74e630ed63311e97427b" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "allowances" ADD CONSTRAINT "FK_61e6e612f6d4644f8910d453cc9" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "allowance_change_requests" ADD CONSTRAINT "FK_4ea6382927f50cb93873fae16d2" FOREIGN KEY ("allowance_id") REFERENCES "allowances"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "allowance_change_requests" DROP CONSTRAINT "FK_4ea6382927f50cb93873fae16d2"`);
|
||||
await queryRunner.query(`ALTER TABLE "allowances" DROP CONSTRAINT "FK_61e6e612f6d4644f8910d453cc9"`);
|
||||
await queryRunner.query(`ALTER TABLE "allowances" DROP CONSTRAINT "FK_80b144a74e630ed63311e97427b"`);
|
||||
await queryRunner.query(`DROP TABLE "allowance_change_requests"`);
|
||||
await queryRunner.query(`DROP TABLE "allowances"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateGiftEntities1734861516657 implements MigrationInterface {
|
||||
name = 'CreateGiftEntities1734861516657';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "gift_replies"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"status" character varying NOT NULL DEFAULT 'PENDING',
|
||||
"color" character varying NOT NULL,
|
||||
"gift_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "REL_8292da97f1ceb9a806b8bc812f" UNIQUE ("gift_id"),
|
||||
CONSTRAINT "PK_ec6567bb5ab318bb292fa6599a2" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "gift"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"description" text NOT NULL,
|
||||
"color" character varying NOT NULL,
|
||||
"amount" numeric(10,3) NOT NULL,
|
||||
"status" character varying NOT NULL DEFAULT 'AVAILABLE',
|
||||
"redeemed_at" TIMESTAMP WITH TIME ZONE,
|
||||
"giver_id" uuid NOT NULL, "image_id" uuid NOT NULL,
|
||||
"recipient_id" uuid NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_f91217caddc01a085837ebe0606" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "gift_redemptions"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"gift_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_6ad7ac76169c3a224ce4a3afff4" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "gift_replies" ADD CONSTRAINT "FK_8292da97f1ceb9a806b8bc812f2" FOREIGN KEY ("gift_id") REFERENCES "gift"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "gift" ADD CONSTRAINT "FK_0d317b68508819308455db9b9be" FOREIGN KEY ("giver_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "gift" ADD CONSTRAINT "FK_4a46b5734fb573dc956904c18d0" FOREIGN KEY ("recipient_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "gift" ADD CONSTRAINT "FK_83bb54c127d0e6ee487b90e2996" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "gift_redemptions" ADD CONSTRAINT "FK_243c4349f0c45ce5385ac316aaa" FOREIGN KEY ("gift_id") REFERENCES "gift"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "gift_redemptions" DROP CONSTRAINT "FK_243c4349f0c45ce5385ac316aaa"`);
|
||||
await queryRunner.query(`ALTER TABLE "gift" DROP CONSTRAINT "FK_83bb54c127d0e6ee487b90e2996"`);
|
||||
await queryRunner.query(`ALTER TABLE "gift" DROP CONSTRAINT "FK_4a46b5734fb573dc956904c18d0"`);
|
||||
await queryRunner.query(`ALTER TABLE "gift" DROP CONSTRAINT "FK_0d317b68508819308455db9b9be"`);
|
||||
await queryRunner.query(`ALTER TABLE "gift_replies" DROP CONSTRAINT "FK_8292da97f1ceb9a806b8bc812f2"`);
|
||||
await queryRunner.query(`DROP TABLE "gift_redemptions"`);
|
||||
await queryRunner.query(`DROP TABLE "gift"`);
|
||||
await queryRunner.query(`DROP TABLE "gift_replies"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateNotificationEntityAndEditDevice1734944692999 implements MigrationInterface {
|
||||
name = 'CreateNotificationEntityAndEditDevice1734944692999';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notifications"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"title" character varying NOT NULL,
|
||||
"message" character varying NOT NULL,
|
||||
"recipient" character varying,
|
||||
"scope" character varying NOT NULL,
|
||||
"status" character varying,
|
||||
"channel" character varying NOT NULL,
|
||||
"user_id" uuid,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "devices" ADD "fcm_token" character varying`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notifications" ADD CONSTRAINT "FK_9a8a82462cab47c73d25f49261f" 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 "notifications" DROP CONSTRAINT "FK_9a8a82462cab47c73d25f49261f"`);
|
||||
await queryRunner.query(`ALTER TABLE "devices" DROP COLUMN "fcm_token"`);
|
||||
await queryRunner.query(`DROP TABLE "notifications"`);
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,6 @@ export * from './1734246386471-create-saving-goals-entities';
|
||||
export * from './1734247702310-seeds-goals-categories';
|
||||
export * from './1734262619426-create-junior-registration-token-table';
|
||||
export * from './1734503895302-create-money-request-entity';
|
||||
export * from './1734601976591-create-allowance-entities';
|
||||
export * from './1734861516657-create-gift-entities';
|
||||
export * from './1734944692999-create-notification-entity-and-edit-device';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CacheModule } from '~/common/modules/cache/cache.module';
|
||||
import { DocumentController } from './controllers';
|
||||
import { Document } from './entities';
|
||||
import { DocumentRepository } from './repositories';
|
||||
@@ -8,7 +7,7 @@ import { DocumentService, OciService } from './services';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Document]), CacheModule],
|
||||
imports: [TypeOrmModule.forFeature([Document])],
|
||||
controllers: [DocumentController],
|
||||
providers: [DocumentService, OciService, DocumentRepository],
|
||||
exports: [DocumentService, OciService],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Gift } from '~/gift/entities';
|
||||
import { Junior, Theme } from '~/junior/entities';
|
||||
import { SavingGoal } from '~/saving-goals/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
@@ -42,6 +43,9 @@ export class Document {
|
||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
|
||||
goals?: SavingGoal[];
|
||||
|
||||
@OneToMany(() => Gift, (gift) => gift.image)
|
||||
gifts?: Gift[];
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Body, Controller, 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 { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateGiftRequestDto, GiftFiltersRequestDto, GiftReplyRequestDto } from '../dtos/request';
|
||||
import { GiftDetailsResponseDto, GiftListResponseDto } from '../dtos/response';
|
||||
import { GiftsService } from '../services';
|
||||
|
||||
@Controller('gift')
|
||||
@ApiTags('Gifts')
|
||||
@ApiBearerAuth()
|
||||
export class GiftsController {
|
||||
constructor(private readonly giftsService: GiftsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(GiftDetailsResponseDto)
|
||||
async createGift(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateGiftRequestDto) {
|
||||
const gift = await this.giftsService.createGift(sub, body);
|
||||
|
||||
return ResponseFactory.data(new GiftDetailsResponseDto(gift));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiDataPageResponse(GiftListResponseDto)
|
||||
async findGifts(@AuthenticatedUser() user: IJwtPayload, @Query() filters: GiftFiltersRequestDto) {
|
||||
const [gifts, itemCount] = await this.giftsService.findGifts(user, filters);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
gifts.map((gift) => new GiftListResponseDto(gift)),
|
||||
{
|
||||
size: filters.size,
|
||||
page: filters.page,
|
||||
itemCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':giftId')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiDataResponse(GiftDetailsResponseDto)
|
||||
async findGiftById(@AuthenticatedUser() user: IJwtPayload, @Param('giftId') giftId: string) {
|
||||
const gift = await this.giftsService.findUserGiftById(user, giftId);
|
||||
|
||||
return ResponseFactory.data(new GiftDetailsResponseDto(gift));
|
||||
}
|
||||
|
||||
@Post(':giftId/redeem')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
redeemGift(@AuthenticatedUser() { sub }: IJwtPayload, @Param('giftId') giftId: string) {
|
||||
return this.giftsService.redeemGift(sub, giftId);
|
||||
}
|
||||
|
||||
@Post(':giftId/undo-redeem')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
UndoGiftRedemption(@AuthenticatedUser() { sub }: IJwtPayload, @Param('giftId') giftId: string) {
|
||||
return this.giftsService.UndoGiftRedemption(sub, giftId);
|
||||
}
|
||||
|
||||
@Post(':giftId/reply')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
replyToGift(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('giftId') giftId: string,
|
||||
@Body() body: GiftReplyRequestDto,
|
||||
) {
|
||||
return this.giftsService.replyToGift(sub, giftId, body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './gifts.controller';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { GiftColor } from '~/gift/enums';
|
||||
export class CreateGiftRequestDto {
|
||||
@ApiProperty({ example: 'Happy Birthday' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'gift.name' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'gift.name' }) })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: 'Description of the gift' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'gift.description' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'gift.description' }) })
|
||||
description!: string;
|
||||
|
||||
@ApiProperty({ example: GiftColor.VIOLET })
|
||||
@IsEnum(GiftColor, { message: i18n('validation.IsEnum', { path: 'general', property: 'gift.color' }) })
|
||||
color!: GiftColor;
|
||||
|
||||
@ApiProperty({ example: 100 })
|
||||
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'gift.amount' }) })
|
||||
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'gift.amount' }) })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'gift.imageId' }) })
|
||||
imageId!: string;
|
||||
|
||||
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'gift.recipientId' }) })
|
||||
recipientId!: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { GiftStatus } from '~/gift/enums';
|
||||
export class GiftFiltersRequestDto extends PageOptionsRequestDto {
|
||||
@ApiProperty({ enum: GiftStatus })
|
||||
@IsEnum(GiftStatus, { message: i18n('validation.IsEnum', { path: 'general', property: 'gift.status' }) })
|
||||
status!: GiftStatus;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { GiftColor } from '~/gift/enums';
|
||||
export class GiftReplyRequestDto {
|
||||
@ApiProperty()
|
||||
@IsEnum(GiftColor, { message: i18n('validation.IsEnum', { path: 'general', property: 'gift.color' }) })
|
||||
color!: GiftColor;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './create-gift.request.dto';
|
||||
export * from './gift-filters.request.dto';
|
||||
export * from './gift-reply.request.dto';
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { Gift } from '~/gift/entities';
|
||||
import { GiftColor, GiftStatus } from '~/gift/enums';
|
||||
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||
|
||||
export class GiftDetailsResponseDto {
|
||||
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: 'Happy Birthday' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: 'Description of the gift' })
|
||||
description!: string;
|
||||
|
||||
@ApiProperty({ example: '200' })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: GiftStatus.AVAILABLE })
|
||||
status!: GiftStatus;
|
||||
|
||||
@ApiProperty({ example: GiftColor.GREEN })
|
||||
color!: GiftColor;
|
||||
|
||||
@ApiProperty({ type: JuniorResponseDto })
|
||||
recipient!: JuniorResponseDto;
|
||||
|
||||
@ApiProperty({ example: 'Ahmad Khalid' })
|
||||
giverName!: string;
|
||||
|
||||
@ApiProperty({ type: DocumentMetaResponseDto })
|
||||
image!: DocumentMetaResponseDto;
|
||||
|
||||
@ApiProperty({ example: '2022-01-01T00:00:00.000Z' })
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty({ example: '2022-01-01T00:00:00.000Z' })
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(gift: Gift) {
|
||||
this.id = gift.id;
|
||||
this.name = gift.name;
|
||||
this.description = gift.description;
|
||||
this.amount = gift.amount;
|
||||
this.status = gift.status;
|
||||
this.color = gift.color;
|
||||
this.giverName = gift.giver.customer.firstName + ' ' + gift.giver.customer.lastName;
|
||||
this.recipient = new JuniorResponseDto(gift.recipient);
|
||||
this.image = new DocumentMetaResponseDto(gift.image);
|
||||
this.createdAt = gift.createdAt;
|
||||
this.updatedAt = gift.updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Gift } from '~/gift/entities';
|
||||
import { GiftColor, GiftStatus } from '~/gift/enums';
|
||||
|
||||
export class GiftListResponseDto {
|
||||
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: 'Happy Birthday' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: '242' })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: GiftStatus.AVAILABLE })
|
||||
status!: GiftStatus;
|
||||
|
||||
@ApiProperty({ example: GiftColor.GREEN })
|
||||
color!: GiftColor;
|
||||
|
||||
@ApiProperty({ example: '2022-01-01T00:00:00.000Z' })
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(gift: Gift) {
|
||||
this.id = gift.id;
|
||||
this.name = gift.name;
|
||||
this.amount = gift.amount;
|
||||
this.status = gift.status;
|
||||
this.color = gift.color;
|
||||
this.createdAt = gift.createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './gift-details.response.dto';
|
||||
export * from './gift-list.response.dto';
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Gift } from './gift.entity';
|
||||
|
||||
@Entity('gift_redemptions')
|
||||
export class GiftRedemption extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'gift_id' })
|
||||
giftId!: string;
|
||||
|
||||
@ManyToOne(() => Gift, (gift) => gift.redemptions, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'gift_id' })
|
||||
gift!: Gift;
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { GiftColor, GiftReplyStatus } from '../enums';
|
||||
import { Gift } from './gift.entity';
|
||||
|
||||
@Entity('gift_replies')
|
||||
export class GiftReply extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', name: 'status', default: GiftReplyStatus.PENDING })
|
||||
status!: GiftReplyStatus;
|
||||
|
||||
@Column({ type: 'varchar', name: 'color' })
|
||||
color!: GiftColor;
|
||||
|
||||
@Column({ type: 'uuid', name: 'gift_id' })
|
||||
giftId!: string;
|
||||
|
||||
@OneToOne(() => Gift, (gift) => gift.reply, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'gift_id' })
|
||||
gift!: Gift;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { GiftColor, GiftStatus } from '../enums';
|
||||
import { GiftRedemption } from './gift-redemption.entity';
|
||||
import { GiftReply } from './gift-reply.entity';
|
||||
|
||||
@Entity('gift')
|
||||
export class Gift {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'text', name: 'description' })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'varchar', name: 'color' })
|
||||
color!: GiftColor;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
name: 'amount',
|
||||
precision: 10,
|
||||
scale: 3,
|
||||
transformer: {
|
||||
to: (value: number) => value,
|
||||
from: (value: string) => parseFloat(value),
|
||||
},
|
||||
})
|
||||
amount!: number;
|
||||
|
||||
@Column({ type: 'varchar', name: 'status', default: GiftStatus.AVAILABLE })
|
||||
status!: GiftStatus;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'redeemed_at', nullable: true })
|
||||
redeemedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'uuid', name: 'giver_id' })
|
||||
giverId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'image_id' })
|
||||
imageId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'recipient_id' })
|
||||
recipientId!: string;
|
||||
|
||||
@ManyToOne(() => Guardian, (guardian) => guardian.gifts)
|
||||
@JoinColumn({ name: 'giver_id' })
|
||||
giver!: Guardian;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.gifts)
|
||||
@JoinColumn({ name: 'recipient_id' })
|
||||
recipient!: Junior;
|
||||
|
||||
@ManyToOne(() => Document, (document) => document.gifts)
|
||||
@JoinColumn({ name: 'image_id' })
|
||||
image!: Document;
|
||||
|
||||
@OneToMany(() => GiftRedemption, (giftRedemption) => giftRedemption.gift, { cascade: true })
|
||||
redemptions!: GiftRedemption[];
|
||||
|
||||
@OneToOne(() => GiftReply, (giftReply) => giftReply.gift, { cascade: true })
|
||||
reply?: GiftReply;
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './gift-redemption.entity';
|
||||
export * from './gift-reply.entity';
|
||||
export * from './gift.entity';
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum GiftColor {
|
||||
VIOLET = 'VIOLET',
|
||||
PINK = 'PINK',
|
||||
BLUE = 'BLUE',
|
||||
GREEN = 'GREEN',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum GiftReplyStatus {
|
||||
PENDING = 'PENDING',
|
||||
OPENED = 'OPENED',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum GiftStatus {
|
||||
AVAILABLE = 'AVAILABLE',
|
||||
REDEEMED = 'REDEEMED',
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './gift-color.enum';
|
||||
export * from './gift-reply-status.enum';
|
||||
export * from './gift-status.enum';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JuniorModule } from '~/junior/junior.module';
|
||||
import { GiftsController } from './controllers';
|
||||
import { Gift, GiftRedemption, GiftReply } from './entities';
|
||||
import { GiftsRepository } from './repositories';
|
||||
import { GiftsService } from './services';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Gift, GiftReply, GiftRedemption]), JuniorModule],
|
||||
controllers: [GiftsController],
|
||||
providers: [GiftsService, GiftsRepository],
|
||||
})
|
||||
export class GiftModule {}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { CreateGiftRequestDto, GiftFiltersRequestDto, GiftReplyRequestDto } from '../dtos/request';
|
||||
import { Gift, GiftRedemption, GiftReply } from '../entities';
|
||||
import { GiftStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
|
||||
@Injectable()
|
||||
export class GiftsRepository {
|
||||
constructor(@InjectRepository(Gift) private readonly giftsRepository: Repository<Gift>) {}
|
||||
|
||||
create(guardianId: string, gift: CreateGiftRequestDto): Promise<Gift> {
|
||||
return this.giftsRepository.save(
|
||||
this.giftsRepository.create({
|
||||
name: gift.name,
|
||||
description: gift.description,
|
||||
amount: gift.amount,
|
||||
recipientId: gift.recipientId,
|
||||
imageId: gift.imageId,
|
||||
color: gift.color,
|
||||
giverId: guardianId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findJuniorGiftById(juniorId: string, giftId: string): Promise<Gift | null> {
|
||||
return this.giftsRepository.findOne({
|
||||
where: { recipientId: juniorId, id: giftId },
|
||||
relations: ['recipient', 'recipient.customer', 'image', 'giver', 'giver.customer', 'reply'],
|
||||
});
|
||||
}
|
||||
|
||||
findGuardianGiftById(guardianId: string, giftId: string): Promise<Gift | null> {
|
||||
return this.giftsRepository.findOne({
|
||||
where: { giverId: guardianId, id: giftId },
|
||||
relations: ['recipient', 'recipient.customer', 'image', 'giver', 'giver.customer', 'reply'],
|
||||
});
|
||||
}
|
||||
|
||||
findGifts(user: IJwtPayload, filters: GiftFiltersRequestDto) {
|
||||
const query = this.giftsRepository.createQueryBuilder('gift');
|
||||
if (user.roles.includes(Roles.GUARDIAN)) {
|
||||
query.where('gift.giverId = :giverId', { giverId: user.sub });
|
||||
} else {
|
||||
query.where('gift.recipientId = :recipientId', { recipientId: user.sub });
|
||||
}
|
||||
query.andWhere('gift.status = :status', { status: filters.status });
|
||||
query.skip((filters.page - ONE) * filters.size);
|
||||
query.take(filters.size);
|
||||
query.orderBy('gift.createdAt', 'DESC');
|
||||
return query.getManyAndCount();
|
||||
}
|
||||
|
||||
redeemGift(gift: Gift) {
|
||||
const redemptions = gift.redemptions || [];
|
||||
gift.status = GiftStatus.REDEEMED;
|
||||
gift.redeemedAt = new Date();
|
||||
gift.redemptions = [...redemptions, new GiftRedemption()];
|
||||
return this.giftsRepository.save(gift);
|
||||
}
|
||||
|
||||
UndoGiftRedemption(gift: Gift) {
|
||||
gift.status = GiftStatus.AVAILABLE;
|
||||
gift.redeemedAt = null;
|
||||
return this.giftsRepository.save(gift);
|
||||
}
|
||||
|
||||
replyToGift(gift: Gift, reply: GiftReplyRequestDto) {
|
||||
gift.reply = GiftReply.create({ ...reply });
|
||||
|
||||
return this.giftsRepository.save(gift);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './gifts.repository';
|
||||
@@ -0,0 +1,101 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { OciService } from '~/document/services';
|
||||
import { JuniorService } from '~/junior/services';
|
||||
import { CreateGiftRequestDto, GiftFiltersRequestDto, GiftReplyRequestDto } from '../dtos/request';
|
||||
import { Gift } from '../entities';
|
||||
import { GiftStatus } from '../enums';
|
||||
import { GiftsRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class GiftsService {
|
||||
constructor(
|
||||
private readonly juniorService: JuniorService,
|
||||
private readonly giftsRepository: GiftsRepository,
|
||||
private readonly ociService: OciService,
|
||||
) {}
|
||||
|
||||
async createGift(guardianId: string, body: CreateGiftRequestDto) {
|
||||
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(
|
||||
guardianId,
|
||||
body.recipientId,
|
||||
);
|
||||
|
||||
if (!doesJuniorBelongToGuardian) {
|
||||
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||
}
|
||||
|
||||
const gift = await this.giftsRepository.create(guardianId, body);
|
||||
|
||||
return this.findUserGiftById({ sub: guardianId, roles: [Roles.GUARDIAN] }, gift.id);
|
||||
}
|
||||
|
||||
async findUserGiftById(user: IJwtPayload, giftId: string) {
|
||||
const gift = user.roles.includes(Roles.GUARDIAN)
|
||||
? await this.giftsRepository.findGuardianGiftById(user.sub, giftId)
|
||||
: await this.giftsRepository.findJuniorGiftById(user.sub, giftId);
|
||||
|
||||
if (!gift) {
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
await this.prepareGiftImages([gift]);
|
||||
|
||||
return gift;
|
||||
}
|
||||
|
||||
async redeemGift(juniorId: string, giftId: string) {
|
||||
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||
|
||||
if (!gift) {
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (gift.status === GiftStatus.REDEEMED) {
|
||||
throw new BadRequestException('GIFT.ALREADY_REDEEMED');
|
||||
}
|
||||
|
||||
return this.giftsRepository.redeemGift(gift);
|
||||
}
|
||||
|
||||
async UndoGiftRedemption(juniorId: string, giftId: string) {
|
||||
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||
|
||||
if (!gift) {
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (gift.status === GiftStatus.AVAILABLE) {
|
||||
throw new BadRequestException('GIFT.NOT_REDEEMED');
|
||||
}
|
||||
|
||||
return this.giftsRepository.UndoGiftRedemption(gift);
|
||||
}
|
||||
|
||||
async replyToGift(juniorId: string, giftId: string, body: GiftReplyRequestDto) {
|
||||
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||
|
||||
if (!gift) {
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (gift.reply) {
|
||||
throw new BadRequestException('GIFT.ALREADY_REPLIED');
|
||||
}
|
||||
|
||||
return this.giftsRepository.replyToGift(gift, body);
|
||||
}
|
||||
|
||||
findGifts(user: IJwtPayload, filters: GiftFiltersRequestDto) {
|
||||
return this.giftsRepository.findGifts(user, filters);
|
||||
}
|
||||
|
||||
private async prepareGiftImages(gifts: Gift[]) {
|
||||
await Promise.all(
|
||||
gifts.map(async (gift) => {
|
||||
gift.image.url = await this.ociService.generatePreSignedUrl(gift.image);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './gifts.service';
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Allowance } from '~/allowance/entities';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Gift } from '~/gift/entities';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { MoneyRequest } from '~/money-request/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
@@ -35,6 +37,12 @@ export class Guardian extends BaseEntity {
|
||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.reviewer)
|
||||
moneyRequests?: MoneyRequest[];
|
||||
|
||||
@OneToMany(() => Allowance, (allowance) => allowance.guardian)
|
||||
allowances?: Allowance[];
|
||||
|
||||
@OneToMany(() => Gift, (gift) => gift.giver)
|
||||
gifts?: Gift[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Allowance } from '~/allowance/entities';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Gift } from '~/gift/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { MoneyRequest } from '~/money-request/entities';
|
||||
import { Category, SavingGoal } from '~/saving-goals/entities';
|
||||
@@ -74,6 +76,12 @@ export class Junior extends BaseEntity {
|
||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
|
||||
moneyRequests!: MoneyRequest[];
|
||||
|
||||
@OneToMany(() => Allowance, (allowance) => allowance.junior)
|
||||
allowances!: Allowance[];
|
||||
|
||||
@OneToMany(() => Gift, (gift) => gift.recipient)
|
||||
gifts!: Gift[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ import { JuniorService, JuniorTokenService, QrcodeService } from './services';
|
||||
forwardRef(() => AuthModule),
|
||||
CustomerModule,
|
||||
],
|
||||
exports: [JuniorTokenService, JuniorService],
|
||||
exports: [JuniorService, JuniorTokenService],
|
||||
})
|
||||
export class JuniorModule {}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class JuniorRepository {
|
||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||
return this.juniorRepository.findAndCount({
|
||||
where: { guardianId },
|
||||
relations: ['customer', 'customer.user'],
|
||||
relations: ['customer', 'customer.user', 'customer.profilePicture'],
|
||||
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
||||
take: pageOptions.size,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { UserService } from '~/auth/services';
|
||||
@@ -14,9 +14,9 @@ import { JuniorTokenService } from './junior-token.service';
|
||||
export class JuniorService {
|
||||
constructor(
|
||||
private readonly juniorRepository: JuniorRepository,
|
||||
private readonly userService: UserService,
|
||||
private readonly customerService: CustomerService,
|
||||
private readonly juniorTokenService: JuniorTokenService,
|
||||
@Inject(forwardRef(() => UserService)) private readonly userService: UserService,
|
||||
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
|
||||
) {}
|
||||
|
||||
@Transactional()
|
||||
@@ -86,4 +86,10 @@ export class JuniorService {
|
||||
generateToken(juniorId: string) {
|
||||
return this.juniorTokenService.generateToken(juniorId);
|
||||
}
|
||||
|
||||
async doesJuniorBelongToGuardian(guardianId: string, juniorId: string) {
|
||||
const junior = await this.findJuniorById(juniorId, false, guardianId);
|
||||
|
||||
return !!junior;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Repository } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CreateGoalRequestDto } from '../dtos/request';
|
||||
import { Category, SavingGoal } from '../entities';
|
||||
const ZERO = 0;
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class SavingGoalsRepository {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SavingGoal } from '../entities';
|
||||
import { IGoalStats } from '../interfaces';
|
||||
import { SavingGoalsRepository } from '../repositories';
|
||||
import { CategoryService } from './category.service';
|
||||
const ZERO = 0;
|
||||
@Injectable()
|
||||
export class SavingGoalsService {
|
||||
constructor(
|
||||
|
||||
Reference in New Issue
Block a user