mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 21:59:40 +00:00
feat: allowance journey
This commit is contained in:
20
src/allowance/allowance.module.ts
Normal file
20
src/allowance/allowance.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { JuniorModule } from '~/junior/junior.module';
|
||||||
|
import { AllowanceChangeRequestController, AllowancesController } from './controllers';
|
||||||
|
import { Allowance, AllowanceChangeRequest } from './entities';
|
||||||
|
import { AllowanceChangeRequestsRepository, AllowancesRepository } from './repositories';
|
||||||
|
import { AllowanceChangeRequestsService, AllowancesService } from './services';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AllowancesController, AllowanceChangeRequestController],
|
||||||
|
imports: [TypeOrmModule.forFeature([Allowance, AllowanceChangeRequest]), JuniorModule],
|
||||||
|
providers: [
|
||||||
|
AllowancesService,
|
||||||
|
AllowancesRepository,
|
||||||
|
AllowanceChangeRequestsService,
|
||||||
|
AllowanceChangeRequestsRepository,
|
||||||
|
],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class AllowanceModule {}
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||||
|
import { RolesGuard } from '~/common/guards';
|
||||||
|
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||||
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceChangeRequestResponseDto } from '../dtos/response';
|
||||||
|
import { AllowanceChangeRequestsService } from '../services';
|
||||||
|
|
||||||
|
@Controller('allowance-change-requests')
|
||||||
|
@ApiTags('Allowance Change Requests')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AllowanceChangeRequestController {
|
||||||
|
constructor(private readonly allowanceChangeRequestsService: AllowanceChangeRequestsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
requestAllowanceChange(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceChangeRequestDto) {
|
||||||
|
return this.allowanceChangeRequestsService.createAllowanceChangeRequest(sub, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataPageResponse(AllowanceChangeRequestResponseDto)
|
||||||
|
async findAllowanceChangeRequests(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
|
||||||
|
const [requests, itemCount] = await this.allowanceChangeRequestsService.findAllowanceChangeRequests(sub, query);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
requests.map((request) => new AllowanceChangeRequestResponseDto(request)),
|
||||||
|
{
|
||||||
|
itemCount,
|
||||||
|
page: query.page,
|
||||||
|
size: query.size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:changeRequestId')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceChangeRequestResponseDto)
|
||||||
|
async findAllowanceChangeRequestById(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||||
|
) {
|
||||||
|
const request = await this.allowanceChangeRequestsService.findAllowanceChangeRequestById(sub, changeRequestId);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new AllowanceChangeRequestResponseDto(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':changeRequestId/approve')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
approveAllowanceChangeRequest(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||||
|
) {
|
||||||
|
return this.allowanceChangeRequestsService.approveAllowanceChangeRequest(sub, changeRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':changeRequestId/reject')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
rejectAllowanceChangeRequest(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||||
|
) {
|
||||||
|
return this.allowanceChangeRequestsService.rejectAllowanceChangeRequest(sub, changeRequestId);
|
||||||
|
}
|
||||||
|
}
|
72
src/allowance/controllers/allowances.controller.ts
Normal file
72
src/allowance/controllers/allowances.controller.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||||
|
import { RolesGuard } from '~/common/guards';
|
||||||
|
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||||
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceResponseDto } from '../dtos/response';
|
||||||
|
import { AllowancesService } from '../services';
|
||||||
|
|
||||||
|
@Controller('allowances')
|
||||||
|
@ApiTags('Allowances')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AllowancesController {
|
||||||
|
constructor(private readonly allowancesService: AllowancesService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceResponseDto)
|
||||||
|
async createAllowance(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceRequestDto) {
|
||||||
|
const allowance = await this.allowancesService.createAllowance(sub, body);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new AllowanceResponseDto(allowance));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataPageResponse(AllowanceResponseDto)
|
||||||
|
async findAllowances(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
|
||||||
|
const [allowances, itemCount] = await this.allowancesService.findAllowances(sub, query);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
allowances.map((allowance) => new AllowanceResponseDto(allowance)),
|
||||||
|
{
|
||||||
|
itemCount,
|
||||||
|
page: query.page,
|
||||||
|
size: query.size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':allowanceId')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceResponseDto)
|
||||||
|
async findAllowanceById(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
|
||||||
|
) {
|
||||||
|
const allowance = await this.allowancesService.findAllowanceById(allowanceId, sub);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new AllowanceResponseDto(allowance));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':allowanceId')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(AllowanceResponseDto)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
deleteAllowance(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
|
||||||
|
) {
|
||||||
|
return this.allowancesService.deleteAllowance(sub, allowanceId);
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/controllers/index.ts
Normal file
2
src/allowance/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.controller';
|
||||||
|
export * from './allowances.controller';
|
@ -0,0 +1,28 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
export class CreateAllowanceChangeRequestDto {
|
||||||
|
@ApiProperty({ example: 'I want to change the amount of the allowance' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowanceChangeRequest.reason' }) })
|
||||||
|
@IsNotEmpty({
|
||||||
|
message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowanceChangeRequest.reason' }),
|
||||||
|
})
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
@IsNumber(
|
||||||
|
{},
|
||||||
|
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowanceChangeRequest.amount' }) },
|
||||||
|
)
|
||||||
|
@IsPositive({
|
||||||
|
message: i18n('validation.IsPositive', { path: 'general', property: 'allowanceChangeRequest.amount' }),
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
@IsUUID('4', {
|
||||||
|
message: i18n('validation.IsUUID', { path: 'general', property: 'allowanceChangeRequest.allowanceId' }),
|
||||||
|
})
|
||||||
|
allowanceId!: string;
|
||||||
|
}
|
52
src/allowance/dtos/request/create-allowance.request.dto.ts
Normal file
52
src/allowance/dtos/request/create-allowance.request.dto.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { IsDate, IsEnum, IsInt, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID, ValidateIf } from 'class-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
|
||||||
|
export class CreateAllowanceRequestDto {
|
||||||
|
@ApiProperty({ example: 'Allowance name' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowance.name' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowance.name' }) })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
|
||||||
|
@IsEnum(AllowanceFrequency, {
|
||||||
|
message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.frequency' }),
|
||||||
|
})
|
||||||
|
frequency!: AllowanceFrequency;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceType.BY_END_DATE })
|
||||||
|
@IsEnum(AllowanceType, { message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.type' }) })
|
||||||
|
type!: AllowanceType;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.startDate' }) })
|
||||||
|
@Transform(({ value }) => moment(value).startOf('day').toDate())
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.endDate' }) })
|
||||||
|
@Transform(({ value }) => moment(value).endOf('day').toDate())
|
||||||
|
@ValidateIf((o) => o.type === AllowanceType.BY_END_DATE)
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10 })
|
||||||
|
@IsNumber(
|
||||||
|
{},
|
||||||
|
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.numberOfTransactions' }) },
|
||||||
|
)
|
||||||
|
@IsInt({ message: i18n('validation.IsInt', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
|
||||||
|
@ValidateIf((o) => o.type === AllowanceType.BY_COUNT)
|
||||||
|
numberOfTransactions?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'e7b1b3b4-4b3b-4b3b-4b3b-4b3b4b3b4b3b' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'allowance.juniorId' }) })
|
||||||
|
juniorId!: string;
|
||||||
|
}
|
2
src/allowance/dtos/request/index.ts
Normal file
2
src/allowance/dtos/request/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './create-allowance-change.request.dto';
|
||||||
|
export * from './create-allowance.request.dto';
|
@ -0,0 +1,45 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { AllowanceChangeRequest } from '~/allowance/entities';
|
||||||
|
import { AllowanceChangeRequestStatus } from '~/allowance/enums';
|
||||||
|
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||||
|
|
||||||
|
export class AllowanceChangeRequestResponseDto {
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceChangeRequestStatus.APPROVED })
|
||||||
|
status!: AllowanceChangeRequestStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Allowance name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '100' })
|
||||||
|
oldAmount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '200' })
|
||||||
|
newAmount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Some reason' })
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
allowanceId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JuniorResponseDto })
|
||||||
|
junior!: JuniorResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
constructor(allowanceChangeRequest: AllowanceChangeRequest) {
|
||||||
|
this.id = allowanceChangeRequest.id;
|
||||||
|
this.status = allowanceChangeRequest.status;
|
||||||
|
this.name = allowanceChangeRequest.allowance.name;
|
||||||
|
this.oldAmount = allowanceChangeRequest.allowance.amount;
|
||||||
|
this.newAmount = allowanceChangeRequest.amount;
|
||||||
|
this.reason = allowanceChangeRequest.reason;
|
||||||
|
this.allowanceId = allowanceChangeRequest.allowanceId;
|
||||||
|
this.junior = new JuniorResponseDto(allowanceChangeRequest.allowance.junior);
|
||||||
|
this.createdAt = allowanceChangeRequest.createdAt;
|
||||||
|
}
|
||||||
|
}
|
53
src/allowance/dtos/response/allowance.response.dto.ts
Normal file
53
src/allowance/dtos/response/allowance.response.dto.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Allowance } from '~/allowance/entities';
|
||||||
|
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
|
||||||
|
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||||
|
|
||||||
|
export class AllowanceResponseDto {
|
||||||
|
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Allowance name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
|
||||||
|
frequency!: AllowanceFrequency;
|
||||||
|
|
||||||
|
@ApiProperty({ example: AllowanceType.BY_END_DATE })
|
||||||
|
type!: AllowanceType;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10 })
|
||||||
|
numberOfTransactions?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JuniorResponseDto })
|
||||||
|
junior!: JuniorResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: new Date() })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
constructor(allowance: Allowance) {
|
||||||
|
this.id = allowance.id;
|
||||||
|
this.name = allowance.name;
|
||||||
|
this.amount = allowance.amount;
|
||||||
|
this.frequency = allowance.frequency;
|
||||||
|
this.type = allowance.type;
|
||||||
|
this.startDate = allowance.startDate;
|
||||||
|
this.endDate = allowance.endDate;
|
||||||
|
this.numberOfTransactions = allowance.numberOfTransactions;
|
||||||
|
this.junior = new JuniorResponseDto(allowance.junior);
|
||||||
|
this.createdAt = allowance.createdAt;
|
||||||
|
this.updatedAt = allowance.updatedAt;
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/dtos/response/index.ts
Normal file
2
src/allowance/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.response.dto';
|
||||||
|
export * from './allowance.response.dto';
|
45
src/allowance/entities/allowance-change-request.entity.ts
Normal file
45
src/allowance/entities/allowance-change-request.entity.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { AllowanceChangeRequestStatus } from '../enums';
|
||||||
|
import { Allowance } from './allowance.entity';
|
||||||
|
|
||||||
|
@Entity('allowance_change_requests')
|
||||||
|
export class AllowanceChangeRequest {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', name: 'reason' })
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
name: 'amount',
|
||||||
|
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'status', default: AllowanceChangeRequestStatus.PENDING })
|
||||||
|
status!: AllowanceChangeRequestStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'allowance_id' })
|
||||||
|
allowanceId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Allowance, (allowance) => allowance.changeRequests)
|
||||||
|
@JoinColumn({ name: 'allowance_id' })
|
||||||
|
allowance!: Allowance;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
87
src/allowance/entities/allowance.entity.ts
Normal file
87
src/allowance/entities/allowance.entity.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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';
|
||||||
|
/**
|
||||||
|
* id string [primary key]
|
||||||
|
amount number
|
||||||
|
freq enum //[DAILY,WEEKLY,MONTHLY]
|
||||||
|
type enum // [BY_END_DATE, BY_COUNT]
|
||||||
|
startDate date
|
||||||
|
endDate date
|
||||||
|
numberOfTransactions number
|
||||||
|
guardianId string [ref:> Guardians.id]
|
||||||
|
juniorId string [ref:> Juniors.id]
|
||||||
|
createdAt datetime
|
||||||
|
updatedAt datetime
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Entity('allowances')
|
||||||
|
export class Allowance {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
name: 'amount',
|
||||||
|
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'frequency' })
|
||||||
|
frequency!: AllowanceFrequency;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'type' })
|
||||||
|
type!: AllowanceType;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', name: 'start_date' })
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', name: 'end_date', nullable: true })
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'int', name: 'number_of_transactions', nullable: true })
|
||||||
|
numberOfTransactions?: number;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'guardian_id' })
|
||||||
|
guardianId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'junior_id' })
|
||||||
|
juniorId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Guardian, (guardian) => guardian.allowances)
|
||||||
|
@JoinColumn({ name: 'guardian_id' })
|
||||||
|
guardian!: Guardian;
|
||||||
|
|
||||||
|
@ManyToOne(() => Junior, (junior) => junior.allowances)
|
||||||
|
@JoinColumn({ name: 'junior_id' })
|
||||||
|
junior!: Junior;
|
||||||
|
|
||||||
|
@OneToMany(() => AllowanceChangeRequest, (changeRequest) => changeRequest.allowance)
|
||||||
|
changeRequests!: AllowanceChangeRequest[];
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true })
|
||||||
|
deletedAt?: Date;
|
||||||
|
}
|
2
src/allowance/entities/index.ts
Normal file
2
src/allowance/entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.entity';
|
||||||
|
export * from './allowance.entity';
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum AllowanceChangeRequestStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
APPROVED = 'APPROVED',
|
||||||
|
REJECTED = 'REJECTED',
|
||||||
|
}
|
5
src/allowance/enums/allowance-frequency.enum.ts
Normal file
5
src/allowance/enums/allowance-frequency.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum AllowanceFrequency {
|
||||||
|
DAILY = 'DAILY',
|
||||||
|
WEEKLY = 'WEEKLY',
|
||||||
|
MONTHLY = 'MONTHLY',
|
||||||
|
}
|
4
src/allowance/enums/allowance-type.enum.ts
Normal file
4
src/allowance/enums/allowance-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum AllowanceType {
|
||||||
|
BY_END_DATE = 'BY_END_DATE',
|
||||||
|
BY_COUNT = 'BY_COUNT',
|
||||||
|
}
|
3
src/allowance/enums/index.ts
Normal file
3
src/allowance/enums/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './allowance-change-request-status.enum';
|
||||||
|
export * from './allowance-frequency.enum';
|
||||||
|
export * from './allowance-type.enum';
|
@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceChangeRequest } from '../entities';
|
||||||
|
import { AllowanceChangeRequestStatus } from '../enums';
|
||||||
|
const ONE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class AllowanceChangeRequestsRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AllowanceChangeRequest)
|
||||||
|
private readonly allowanceChangeRequestsRepository: Repository<AllowanceChangeRequest>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createAllowanceChangeRequest(allowanceId: string, body: CreateAllowanceChangeRequestDto) {
|
||||||
|
return this.allowanceChangeRequestsRepository.save(
|
||||||
|
this.allowanceChangeRequestsRepository.create({
|
||||||
|
allowanceId,
|
||||||
|
amount: body.amount,
|
||||||
|
reason: body.reason,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>, withRelations = false) {
|
||||||
|
const relations = withRelations
|
||||||
|
? ['allowance', 'allowance.junior', 'allowance.junior.customer', 'allowance.junior.customer.profilePicture']
|
||||||
|
: [];
|
||||||
|
return this.allowanceChangeRequestsRepository.findOne({ where, relations });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllowanceChangeRequestStatus(requestId: string, status: AllowanceChangeRequestStatus) {
|
||||||
|
return this.allowanceChangeRequestsRepository.update({ id: requestId }, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceChangeRequests(guardianId: string, query: PageOptionsRequestDto) {
|
||||||
|
return this.allowanceChangeRequestsRepository.findAndCount({
|
||||||
|
where: { allowance: { guardianId } },
|
||||||
|
take: query.size,
|
||||||
|
skip: query.size * (query.page - ONE),
|
||||||
|
relations: [
|
||||||
|
'allowance',
|
||||||
|
'allowance.junior',
|
||||||
|
'allowance.junior.customer',
|
||||||
|
'allowance.junior.customer.profilePicture',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
47
src/allowance/repositories/allowances.repository.ts
Normal file
47
src/allowance/repositories/allowances.repository.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||||
|
import { Allowance } from '../entities';
|
||||||
|
const ONE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class AllowancesRepository {
|
||||||
|
constructor(@InjectRepository(Allowance) private readonly allowancesRepository: Repository<Allowance>) {}
|
||||||
|
|
||||||
|
createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||||
|
return this.allowancesRepository.save(
|
||||||
|
this.allowancesRepository.create({
|
||||||
|
guardianId,
|
||||||
|
name: body.name,
|
||||||
|
amount: body.amount,
|
||||||
|
frequency: body.frequency,
|
||||||
|
type: body.type,
|
||||||
|
startDate: body.startDate,
|
||||||
|
endDate: body.endDate,
|
||||||
|
numberOfTransactions: body.numberOfTransactions,
|
||||||
|
juniorId: body.juniorId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||||
|
return this.allowancesRepository.findOne({
|
||||||
|
where: { id: allowanceId, guardianId },
|
||||||
|
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
|
||||||
|
return this.allowancesRepository.findAndCount({
|
||||||
|
where: { guardianId },
|
||||||
|
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||||
|
take: query.size,
|
||||||
|
skip: query.size * (query.page - ONE),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllowance(guardianId: string, allowanceId: string) {
|
||||||
|
return this.allowancesRepository.softDelete({ id: allowanceId, guardianId });
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/repositories/index.ts
Normal file
2
src/allowance/repositories/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-request.repository';
|
||||||
|
export * from './allowances.repository';
|
115
src/allowance/services/allowance-change-requests.service.ts
Normal file
115
src/allowance/services/allowance-change-requests.service.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||||
|
import { AllowanceChangeRequest } from '../entities';
|
||||||
|
import { AllowanceChangeRequestStatus } from '../enums';
|
||||||
|
import { AllowanceChangeRequestsRepository } from '../repositories';
|
||||||
|
import { AllowancesService } from './allowances.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AllowanceChangeRequestsService {
|
||||||
|
constructor(
|
||||||
|
private readonly allowanceChangeRequestsRepository: AllowanceChangeRequestsRepository,
|
||||||
|
private readonly ociService: OciService,
|
||||||
|
private readonly allowanceService: AllowancesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createAllowanceChangeRequest(juniorId: string, body: CreateAllowanceChangeRequestDto) {
|
||||||
|
const allowance = await this.allowanceService.validateAllowanceForJunior(juniorId, body.allowanceId);
|
||||||
|
|
||||||
|
if (allowance.amount === body.amount) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestWithTheSameAmount = await this.findAllowanceChangeRequestBy({
|
||||||
|
allowanceId: body.allowanceId,
|
||||||
|
amount: body.amount,
|
||||||
|
status: AllowanceChangeRequestStatus.PENDING,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestWithTheSameAmount) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT_PENDING');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allowanceChangeRequestsRepository.createAllowanceChangeRequest(body.allowanceId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>) {
|
||||||
|
return this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(where);
|
||||||
|
}
|
||||||
|
|
||||||
|
async approveAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||||
|
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
if (request.status === AllowanceChangeRequestStatus.APPROVED) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_APPROVED');
|
||||||
|
}
|
||||||
|
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||||
|
requestId,
|
||||||
|
AllowanceChangeRequestStatus.APPROVED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||||
|
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
if (request.status === AllowanceChangeRequestStatus.REJECTED) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_REJECTED');
|
||||||
|
}
|
||||||
|
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||||
|
requestId,
|
||||||
|
AllowanceChangeRequestStatus.REJECTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowanceChangeRequests(
|
||||||
|
guardianId: string,
|
||||||
|
query: PageOptionsRequestDto,
|
||||||
|
): Promise<[AllowanceChangeRequest[], number]> {
|
||||||
|
const [requests, itemCount] = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequests(
|
||||||
|
guardianId,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.prepareAllowanceChangeRequestsImages(requests);
|
||||||
|
|
||||||
|
return [requests, itemCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowanceChangeRequestById(guardianId: string, requestId: string) {
|
||||||
|
const request = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(
|
||||||
|
{
|
||||||
|
id: requestId,
|
||||||
|
allowance: { guardianId },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prepareAllowanceChangeRequestsImages([request]);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareAllowanceChangeRequestsImages(requests: AllowanceChangeRequest[]) {
|
||||||
|
return Promise.all(
|
||||||
|
requests.map(async (request) => {
|
||||||
|
const profilePicture = request.allowance.junior.customer.profilePicture;
|
||||||
|
if (profilePicture) {
|
||||||
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
85
src/allowance/services/allowances.service.ts
Normal file
85
src/allowance/services/allowances.service.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { JuniorService } from '~/junior/services';
|
||||||
|
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||||
|
import { Allowance } from '../entities';
|
||||||
|
import { AllowancesRepository } from '../repositories';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AllowancesService {
|
||||||
|
constructor(
|
||||||
|
private readonly allowancesRepository: AllowancesRepository,
|
||||||
|
private readonly juniorService: JuniorService,
|
||||||
|
private readonly ociService: OciService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||||
|
if (moment(body.startDate).isBefore(moment().startOf('day'))) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.START_DATE_BEFORE_TODAY');
|
||||||
|
}
|
||||||
|
if (moment(body.startDate).isAfter(body.endDate)) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.START_DATE_AFTER_END_DATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(guardianId, body.juniorId);
|
||||||
|
|
||||||
|
if (!doesJuniorBelongToGuardian) {
|
||||||
|
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowance = await this.allowancesRepository.createAllowance(guardianId, body);
|
||||||
|
|
||||||
|
return this.findAllowanceById(allowance.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||||
|
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId, guardianId);
|
||||||
|
|
||||||
|
if (!allowance) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||||
|
}
|
||||||
|
await this.prepareAllowanceDocuments([allowance]);
|
||||||
|
return allowance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllowances(guardianId: string, query: PageOptionsRequestDto): Promise<[Allowance[], number]> {
|
||||||
|
const [allowances, itemCount] = await this.allowancesRepository.findAllowances(guardianId, query);
|
||||||
|
await this.prepareAllowanceDocuments(allowances);
|
||||||
|
return [allowances, itemCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllowance(guardianId: string, allowanceId: string) {
|
||||||
|
const { affected } = await this.allowancesRepository.deleteAllowance(guardianId, allowanceId);
|
||||||
|
|
||||||
|
if (!affected) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAllowanceForJunior(juniorId: string, allowanceId: string) {
|
||||||
|
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId);
|
||||||
|
|
||||||
|
if (!allowance) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowance.juniorId !== juniorId) {
|
||||||
|
throw new BadRequestException('ALLOWANCE.DOES_NOT_BELONG_TO_JUNIOR');
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareAllowanceDocuments(allowance: Allowance[]) {
|
||||||
|
await Promise.all(
|
||||||
|
allowance.map(async (allowance) => {
|
||||||
|
const profilePicture = allowance.junior.customer.profilePicture;
|
||||||
|
if (profilePicture) {
|
||||||
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
2
src/allowance/services/index.ts
Normal file
2
src/allowance/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './allowance-change-requests.service';
|
||||||
|
export * from './allowances.service';
|
@ -6,6 +6,7 @@ import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
|||||||
import { LoggerModule } from 'nestjs-pino';
|
import { LoggerModule } from 'nestjs-pino';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { addTransactionalDataSource } from 'typeorm-transactional';
|
import { addTransactionalDataSource } from 'typeorm-transactional';
|
||||||
|
import { AllowanceModule } from './allowance/allowance.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { CacheModule } from './common/modules/cache/cache.module';
|
import { CacheModule } from './common/modules/cache/cache.module';
|
||||||
import { LookupModule } from './common/modules/lookup/lookup.module';
|
import { LookupModule } from './common/modules/lookup/lookup.module';
|
||||||
@ -54,9 +55,12 @@ import { TaskModule } from './task/task.module';
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
CustomerModule,
|
CustomerModule,
|
||||||
JuniorModule,
|
JuniorModule,
|
||||||
|
|
||||||
TaskModule,
|
TaskModule,
|
||||||
GuardianModule,
|
GuardianModule,
|
||||||
SavingGoalsModule,
|
SavingGoalsModule,
|
||||||
|
AllowanceModule,
|
||||||
|
|
||||||
MoneyRequestModule,
|
MoneyRequestModule,
|
||||||
|
|
||||||
OtpModule,
|
OtpModule,
|
||||||
|
53
src/db/migrations/1734601976591-create-allowance-entities.ts
Normal file
53
src/db/migrations/1734601976591-create-allowance-entities.ts
Normal file
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
@ -14,3 +14,4 @@ export * from './1734246386471-create-saving-goals-entities';
|
|||||||
export * from './1734247702310-seeds-goals-categories';
|
export * from './1734247702310-seeds-goals-categories';
|
||||||
export * from './1734262619426-create-junior-registration-token-table';
|
export * from './1734262619426-create-junior-registration-token-table';
|
||||||
export * from './1734503895302-create-money-request-entity';
|
export * from './1734503895302-create-money-request-entity';
|
||||||
|
export * from './1734601976591-create-allowance-entities';
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { Allowance } from '~/allowance/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Junior } from '~/junior/entities';
|
import { Junior } from '~/junior/entities';
|
||||||
import { MoneyRequest } from '~/money-request/entities';
|
import { MoneyRequest } from '~/money-request/entities';
|
||||||
@ -35,6 +36,9 @@ export class Guardian extends BaseEntity {
|
|||||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.reviewer)
|
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.reviewer)
|
||||||
moneyRequests?: MoneyRequest[];
|
moneyRequests?: MoneyRequest[];
|
||||||
|
|
||||||
|
@OneToMany(() => Allowance, (allowance) => allowance.guardian)
|
||||||
|
allowances?: Allowance[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { Allowance } from '~/allowance/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Document } from '~/document/entities';
|
import { Document } from '~/document/entities';
|
||||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
@ -74,6 +75,9 @@ export class Junior extends BaseEntity {
|
|||||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
|
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
|
||||||
moneyRequests!: MoneyRequest[];
|
moneyRequests!: MoneyRequest[];
|
||||||
|
|
||||||
|
@OneToMany(() => Allowance, (allowance) => allowance.junior)
|
||||||
|
allowances!: Allowance[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -15,6 +15,6 @@ import { JuniorService, JuniorTokenService, QrcodeService } from './services';
|
|||||||
forwardRef(() => AuthModule),
|
forwardRef(() => AuthModule),
|
||||||
CustomerModule,
|
CustomerModule,
|
||||||
],
|
],
|
||||||
exports: [JuniorTokenService, JuniorService],
|
exports: [JuniorService, JuniorTokenService],
|
||||||
})
|
})
|
||||||
export class JuniorModule {}
|
export class JuniorModule {}
|
||||||
|
@ -13,7 +13,7 @@ export class JuniorRepository {
|
|||||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||||
return this.juniorRepository.findAndCount({
|
return this.juniorRepository.findAndCount({
|
||||||
where: { guardianId },
|
where: { guardianId },
|
||||||
relations: ['customer', 'customer.user'],
|
relations: ['customer', 'customer.user', 'customer.profilePicture'],
|
||||||
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size,
|
||||||
take: 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 { Transactional } from 'typeorm-transactional';
|
||||||
import { Roles } from '~/auth/enums';
|
import { Roles } from '~/auth/enums';
|
||||||
import { UserService } from '~/auth/services';
|
import { UserService } from '~/auth/services';
|
||||||
@ -14,9 +14,9 @@ import { JuniorTokenService } from './junior-token.service';
|
|||||||
export class JuniorService {
|
export class JuniorService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly juniorRepository: JuniorRepository,
|
private readonly juniorRepository: JuniorRepository,
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly customerService: CustomerService,
|
private readonly customerService: CustomerService,
|
||||||
private readonly juniorTokenService: JuniorTokenService,
|
private readonly juniorTokenService: JuniorTokenService,
|
||||||
|
@Inject(forwardRef(() => UserService)) private readonly userService: UserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
@ -86,4 +86,10 @@ export class JuniorService {
|
|||||||
generateToken(juniorId: string) {
|
generateToken(juniorId: string) {
|
||||||
return this.juniorTokenService.generateToken(juniorId);
|
return this.juniorTokenService.generateToken(juniorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doesJuniorBelongToGuardian(guardianId: string, juniorId: string) {
|
||||||
|
const junior = await this.findJuniorById(juniorId, false, guardianId);
|
||||||
|
|
||||||
|
return !!junior;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user