mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 05:42:27 +00:00
Merge branch 'main' into dev
This commit is contained in:
@ -1,20 +0,0 @@
|
||||
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: [AllowancesService],
|
||||
})
|
||||
export class AllowanceModule {}
|
@ -1,81 +0,0 @@
|
||||
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, ApiLangRequestHeader } 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()
|
||||
@ApiLangRequestHeader()
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
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, ApiLangRequestHeader } 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()
|
||||
@ApiLangRequestHeader()
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.controller';
|
||||
export * from './allowances.controller';
|
@ -1,28 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './create-allowance-change.request.dto';
|
||||
export * from './create-allowance.request.dto';
|
@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.response.dto';
|
||||
export * from './allowance.response.dto';
|
@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import moment from 'moment';
|
||||
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;
|
||||
|
||||
get nextPaymentDate(): Date | null {
|
||||
const startDate = moment(this.startDate).clone().startOf('day');
|
||||
const endDate = this.endDate ? moment(this.endDate).endOf('day') : null;
|
||||
const now = moment().startOf('day');
|
||||
|
||||
if (endDate && moment().isAfter(endDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const calculateNextDate = (unit: moment.unitOfTime.Diff) => {
|
||||
const diff = now.diff(startDate, unit);
|
||||
const nextDate = startDate.clone().add(diff, unit);
|
||||
const adjustedDate = nextDate.isSameOrAfter(now) ? nextDate : nextDate.add('1', unit);
|
||||
|
||||
if (endDate && adjustedDate.isAfter(endDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return adjustedDate.toDate();
|
||||
};
|
||||
|
||||
switch (this.frequency) {
|
||||
case AllowanceFrequency.DAILY:
|
||||
return calculateNextDate('days');
|
||||
case AllowanceFrequency.WEEKLY:
|
||||
return calculateNextDate('weeks');
|
||||
case AllowanceFrequency.MONTHLY:
|
||||
return calculateNextDate('months');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.entity';
|
||||
export * from './allowance.entity';
|
@ -1,5 +0,0 @@
|
||||
export enum AllowanceChangeRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export enum AllowanceFrequency {
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export enum AllowanceType {
|
||||
BY_END_DATE = 'BY_END_DATE',
|
||||
BY_COUNT = 'BY_COUNT',
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './allowance-change-request-status.enum';
|
||||
export * from './allowance-frequency.enum';
|
||||
export * from './allowance-type.enum';
|
@ -1,57 +0,0 @@
|
||||
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.user',
|
||||
'allowance.junior.customer.user.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.user',
|
||||
'allowance.junior.customer.user.profilePicture',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
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.user', 'junior.customer.user.profilePicture'],
|
||||
});
|
||||
}
|
||||
|
||||
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
|
||||
return this.allowancesRepository.findAndCount({
|
||||
where: { guardianId },
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.user', 'junior.customer.user.profilePicture'],
|
||||
take: query.size,
|
||||
skip: query.size * (query.page - ONE),
|
||||
});
|
||||
}
|
||||
|
||||
deleteAllowance(guardianId: string, allowanceId: string) {
|
||||
return this.allowancesRepository.softDelete({ id: allowanceId, guardianId });
|
||||
}
|
||||
|
||||
async *findAllowancesChunks(chunkSize: number) {
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const allowances = await this.allowancesRepository.find({
|
||||
take: chunkSize,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
if (!allowances.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield allowances;
|
||||
offset += chunkSize;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.repository';
|
||||
export * from './allowances.repository';
|
@ -1,132 +0,0 @@
|
||||
import { BadRequestException, Injectable, Logger } 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 {
|
||||
private readonly logger = new Logger(AllowanceChangeRequestsService.name);
|
||||
constructor(
|
||||
private readonly allowanceChangeRequestsRepository: AllowanceChangeRequestsRepository,
|
||||
private readonly ociService: OciService,
|
||||
private readonly allowanceService: AllowancesService,
|
||||
) {}
|
||||
|
||||
async createAllowanceChangeRequest(juniorId: string, body: CreateAllowanceChangeRequestDto) {
|
||||
this.logger.log(`Creating allowance change request for junior ${juniorId}`);
|
||||
const allowance = await this.allowanceService.validateAllowanceForJunior(juniorId, body.allowanceId);
|
||||
|
||||
if (allowance.amount === body.amount) {
|
||||
this.logger.error(`Amount is the same as the current allowance 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) {
|
||||
this.logger.error(`There is a pending request with the same amount`);
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT_PENDING');
|
||||
}
|
||||
|
||||
return this.allowanceChangeRequestsRepository.createAllowanceChangeRequest(body.allowanceId, body);
|
||||
}
|
||||
|
||||
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>) {
|
||||
this.logger.log(`Finding allowance change request by ${JSON.stringify(where)}`);
|
||||
return this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(where);
|
||||
}
|
||||
|
||||
async approveAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||
this.logger.log(`Approving allowance change request ${requestId} by guardian ${guardianId}`);
|
||||
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||
|
||||
if (!request) {
|
||||
this.logger.error(`Allowance change request ${requestId} not found for guardian ${guardianId}`);
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||
}
|
||||
if (request.status === AllowanceChangeRequestStatus.APPROVED) {
|
||||
this.logger.error(`Allowance change request ${requestId} already approved`);
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_APPROVED');
|
||||
}
|
||||
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||
requestId,
|
||||
AllowanceChangeRequestStatus.APPROVED,
|
||||
);
|
||||
}
|
||||
|
||||
async rejectAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||
this.logger.log(`Rejecting allowance change request ${requestId} by guardian ${guardianId}`);
|
||||
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||
|
||||
if (!request) {
|
||||
this.logger.error(`Allowance change request ${requestId} not found for guardian ${guardianId}`);
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||
}
|
||||
if (request.status === AllowanceChangeRequestStatus.REJECTED) {
|
||||
this.logger.error(`Allowance change request ${requestId} already 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]> {
|
||||
this.logger.log(`Finding allowance change requests for guardian ${guardianId}`);
|
||||
const [requests, itemCount] = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequests(
|
||||
guardianId,
|
||||
query,
|
||||
);
|
||||
|
||||
await this.prepareAllowanceChangeRequestsImages(requests);
|
||||
|
||||
this.logger.log(`Returning allowance change requests for guardian ${guardianId}`);
|
||||
return [requests, itemCount];
|
||||
}
|
||||
|
||||
async findAllowanceChangeRequestById(guardianId: string, requestId: string) {
|
||||
this.logger.log(`Finding allowance change request ${requestId} for guardian ${guardianId}`);
|
||||
const request = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(
|
||||
{
|
||||
id: requestId,
|
||||
allowance: { guardianId },
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (!request) {
|
||||
this.logger.error(`Allowance change request ${requestId} not found for guardian ${guardianId}`);
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||
}
|
||||
|
||||
await this.prepareAllowanceChangeRequestsImages([request]);
|
||||
|
||||
this.logger.log(`Allowance change request ${requestId} found successfully`);
|
||||
return request;
|
||||
}
|
||||
|
||||
private prepareAllowanceChangeRequestsImages(requests: AllowanceChangeRequest[]) {
|
||||
this.logger.log(`Preparing allowance change requests images`);
|
||||
return Promise.all(
|
||||
requests.map(async (request) => {
|
||||
const profilePicture = request.allowance.junior.customer.user.profilePicture;
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import { BadRequestException, Injectable, Logger } 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 {
|
||||
private readonly logger = new Logger(AllowancesService.name);
|
||||
constructor(
|
||||
private readonly allowancesRepository: AllowancesRepository,
|
||||
private readonly juniorService: JuniorService,
|
||||
private readonly ociService: OciService,
|
||||
) {}
|
||||
|
||||
async createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||
this.logger.log(`Creating allowance for junior ${body.juniorId} by guardian ${guardianId}`);
|
||||
if (moment(body.startDate).isBefore(moment().startOf('day'))) {
|
||||
this.logger.error(`Start date ${body.startDate} is before today`);
|
||||
throw new BadRequestException('ALLOWANCE.START_DATE_BEFORE_TODAY');
|
||||
}
|
||||
if (moment(body.startDate).isAfter(body.endDate)) {
|
||||
this.logger.error(`Start date ${body.startDate} is after end date ${body.endDate}`);
|
||||
throw new BadRequestException('ALLOWANCE.START_DATE_AFTER_END_DATE');
|
||||
}
|
||||
|
||||
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(guardianId, body.juniorId);
|
||||
|
||||
if (!doesJuniorBelongToGuardian) {
|
||||
this.logger.error(`Junior ${body.juniorId} does not belong to guardian ${guardianId}`);
|
||||
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||
}
|
||||
|
||||
const allowance = await this.allowancesRepository.createAllowance(guardianId, body);
|
||||
|
||||
this.logger.log(`Allowance ${allowance.id} created successfully`);
|
||||
return this.findAllowanceById(allowance.id);
|
||||
}
|
||||
|
||||
async findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||
this.logger.log(`Finding allowance ${allowanceId} ${guardianId ? `by guardian ${guardianId}` : ''}`);
|
||||
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId, guardianId);
|
||||
|
||||
if (!allowance) {
|
||||
this.logger.error(`Allowance ${allowanceId} not found ${guardianId ? `for guardian ${guardianId}` : ''}`);
|
||||
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||
}
|
||||
await this.prepareAllowanceDocuments([allowance]);
|
||||
this.logger.log(`Allowance ${allowanceId} found successfully`);
|
||||
return allowance;
|
||||
}
|
||||
|
||||
async findAllowances(guardianId: string, query: PageOptionsRequestDto): Promise<[Allowance[], number]> {
|
||||
this.logger.log(`Finding allowances for guardian ${guardianId}`);
|
||||
const [allowances, itemCount] = await this.allowancesRepository.findAllowances(guardianId, query);
|
||||
await this.prepareAllowanceDocuments(allowances);
|
||||
this.logger.log(`Returning allowances for guardian ${guardianId}`);
|
||||
return [allowances, itemCount];
|
||||
}
|
||||
|
||||
async deleteAllowance(guardianId: string, allowanceId: string) {
|
||||
this.logger.log(`Deleting allowance ${allowanceId} for guardian ${guardianId}`);
|
||||
const { affected } = await this.allowancesRepository.deleteAllowance(guardianId, allowanceId);
|
||||
|
||||
if (!affected) {
|
||||
this.logger.error(`Allowance ${allowanceId} not found`);
|
||||
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||
}
|
||||
this.logger.log(`Allowance ${allowanceId} deleted successfully`);
|
||||
}
|
||||
|
||||
async validateAllowanceForJunior(juniorId: string, allowanceId: string) {
|
||||
this.logger.log(`Validating allowance ${allowanceId} for junior ${juniorId}`);
|
||||
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId);
|
||||
|
||||
if (!allowance) {
|
||||
this.logger.error(`Allowance ${allowanceId} not found`);
|
||||
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (allowance.juniorId !== juniorId) {
|
||||
this.logger.error(`Allowance ${allowanceId} does not belong to junior ${juniorId}`);
|
||||
throw new BadRequestException('ALLOWANCE.DOES_NOT_BELONG_TO_JUNIOR');
|
||||
}
|
||||
|
||||
return allowance;
|
||||
}
|
||||
|
||||
async findAllowancesChunks(chunkSize: number) {
|
||||
this.logger.log(`Finding allowances chunks`);
|
||||
const allowances = await this.allowancesRepository.findAllowancesChunks(chunkSize);
|
||||
this.logger.log(`Returning allowances chunks`);
|
||||
return allowances;
|
||||
}
|
||||
|
||||
private async prepareAllowanceDocuments(allowance: Allowance[]) {
|
||||
this.logger.log(`Preparing document for allowances`);
|
||||
await Promise.all(
|
||||
allowance.map(async (allowance) => {
|
||||
const profilePicture = allowance.junior.customer.user.profilePicture;
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-requests.service';
|
||||
export * from './allowances.service';
|
@ -8,7 +8,6 @@ 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';
|
||||
@ -23,13 +22,9 @@ import { CronModule } from './cron/cron.module';
|
||||
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';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { CardModule } from './card/card.module';
|
||||
|
||||
@ -43,7 +38,6 @@ import { CardModule } from './card/card.module';
|
||||
useFactory: (config: ConfigService) => {
|
||||
return buildTypeormOptions(config, migrations);
|
||||
},
|
||||
/* eslint-disable require-await */
|
||||
async dataSourceFactory(options) {
|
||||
if (!options) {
|
||||
throw new Error('Invalid options passed');
|
||||
@ -51,7 +45,6 @@ import { CardModule } from './card/card.module';
|
||||
|
||||
return addTransactionalDataSource(new DataSource(options));
|
||||
},
|
||||
/* eslint-enable require-await */
|
||||
}),
|
||||
LoggerModule.forRootAsync({
|
||||
useFactory: (config: ConfigService) => buildLoggerOptions(config),
|
||||
@ -63,15 +56,12 @@ import { CardModule } from './card/card.module';
|
||||
ScheduleModule.forRoot(),
|
||||
// App modules
|
||||
AuthModule,
|
||||
UserModule,
|
||||
|
||||
CustomerModule,
|
||||
JuniorModule,
|
||||
|
||||
TaskModule,
|
||||
GuardianModule,
|
||||
SavingGoalsModule,
|
||||
AllowanceModule,
|
||||
MoneyRequestModule,
|
||||
GiftModule,
|
||||
|
||||
NotificationModule,
|
||||
OtpModule,
|
||||
DocumentModule,
|
||||
@ -79,8 +69,6 @@ import { CardModule } from './card/card.module';
|
||||
|
||||
HealthModule,
|
||||
|
||||
UserModule,
|
||||
|
||||
CronModule,
|
||||
NeoLeapModule,
|
||||
CardModule,
|
||||
|
@ -165,11 +165,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// generate a token for the user to reset password
|
||||
const token = await this.userTokenService.generateToken(
|
||||
user.id,
|
||||
user.roles.includes(Roles.GUARDIAN) ? UserType.GUARDIAN : UserType.JUNIOR,
|
||||
moment().add(5, 'minutes').toDate(),
|
||||
);
|
||||
const token = await this.userTokenService.generateToken(user.id, moment().add(5, 'minutes').toDate());
|
||||
|
||||
return { token, user };
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AllowanceModule } from '~/allowance/allowance.module';
|
||||
import { MoneyRequestModule } from '~/money-request/money-request.module';
|
||||
import { BaseCronService } from './services';
|
||||
import { AllowanceTask, MoneyRequestTask } from './tasks';
|
||||
|
||||
@Module({
|
||||
imports: [AllowanceModule, MoneyRequestModule],
|
||||
providers: [AllowanceTask, MoneyRequestTask, BaseCronService],
|
||||
imports: [],
|
||||
providers: [BaseCronService],
|
||||
})
|
||||
export class CronModule {}
|
||||
|
@ -1,54 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import moment from 'moment';
|
||||
import { AllowancesService } from '~/allowance/services';
|
||||
import { CacheService } from '~/common/modules/cache/services';
|
||||
import { BaseCronService } from '../services/base-cron.service';
|
||||
const TEN = 10;
|
||||
const SIXTY = 60;
|
||||
const THOUSAND = 1000;
|
||||
const TEN_MINUTES = TEN * SIXTY * THOUSAND;
|
||||
const CHUNK_SIZE = 50;
|
||||
|
||||
@Injectable()
|
||||
export class AllowanceTask extends BaseCronService {
|
||||
private readonly logger = new Logger(AllowanceTask.name);
|
||||
private readonly cronLockKey = `${AllowanceTask.name}-lock`;
|
||||
private readonly cronLockTtl = TEN_MINUTES;
|
||||
constructor(cacheService: CacheService, private readonly allowanceService: AllowancesService) {
|
||||
super(cacheService);
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_3AM)
|
||||
async handleCron() {
|
||||
try {
|
||||
const isLockAcquired = await this.acquireLock(this.cronLockKey, this.cronLockTtl);
|
||||
if (!isLockAcquired) {
|
||||
this.logger.log('Lock already acquired. Skipping cron job.');
|
||||
return;
|
||||
}
|
||||
this.logger.log('Processing cron job');
|
||||
await this.processJob();
|
||||
} catch (error) {
|
||||
this.logger.error('Error processing cron job', error);
|
||||
} finally {
|
||||
this.logger.log('Releasing lock');
|
||||
await this.releaseLock(this.cronLockKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async processJob() {
|
||||
const today = moment().startOf('day');
|
||||
const allowancesChunks = await this.allowanceService.findAllowancesChunks(CHUNK_SIZE);
|
||||
for await (const allowances of allowancesChunks) {
|
||||
for (const allowance of allowances) {
|
||||
this.logger.log(`Processing allowance ${allowance.id} with next payment date ${allowance.nextPaymentDate}`);
|
||||
// if today is the same as allowance payment date
|
||||
if (moment(allowance.nextPaymentDate).startOf('day').isSame(today)) {
|
||||
this.logger.log(`Today is the payment date for allowance ${allowance.id}`);
|
||||
//@TODO: Implement payment logic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './allowance.task';
|
||||
export * from './money-request.task';
|
||||
|
@ -1,54 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import moment from 'moment';
|
||||
import { CacheService } from '~/common/modules/cache/services';
|
||||
import { MoneyRequestsService } from '~/money-request/services';
|
||||
import { BaseCronService } from '../services';
|
||||
const TEN = 10;
|
||||
const SIXTY = 60;
|
||||
const THOUSAND = 1000;
|
||||
const TEN_MINUTES = TEN * SIXTY * THOUSAND;
|
||||
const CHUNK_SIZE = 50;
|
||||
@Injectable()
|
||||
export class MoneyRequestTask extends BaseCronService {
|
||||
private readonly cronLockKey = `${MoneyRequestTask.name}-lock`;
|
||||
private readonly cronLockTtl = TEN_MINUTES;
|
||||
private readonly logger = new Logger(MoneyRequestTask.name);
|
||||
constructor(cacheService: CacheService, private readonly moneyRequestService: MoneyRequestsService) {
|
||||
super(cacheService);
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_3AM)
|
||||
async handleCron() {
|
||||
try {
|
||||
const isLockAcquired = await this.acquireLock(this.cronLockKey, this.cronLockTtl);
|
||||
if (!isLockAcquired) {
|
||||
this.logger.log('Lock already acquired. Skipping cron job for MoneyRequestTask.');
|
||||
return;
|
||||
}
|
||||
this.logger.log('Processing cron job for MoneyRequestTask');
|
||||
await this.processJob();
|
||||
} catch (error) {
|
||||
this.logger.error('Error processing MoneyRequestTask cron job', error);
|
||||
} finally {
|
||||
this.logger.log('Releasing lock for MoneyRequestTask');
|
||||
await this.releaseLock(this.cronLockKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async processJob() {
|
||||
const today = moment().startOf('day');
|
||||
const moneyRequestsChunks = await this.moneyRequestService.findMoneyRequestsChunks(CHUNK_SIZE);
|
||||
for await (const moneyRequests of moneyRequestsChunks) {
|
||||
for (const moneyRequest of moneyRequests) {
|
||||
this.logger.log(
|
||||
`Processing money request ${moneyRequest.id} with next payment date ${moneyRequest.nextPaymentDate}`,
|
||||
);
|
||||
// if today is the same as money request payment date
|
||||
if (moment(moneyRequest.nextPaymentDate).startOf('day').isSame(today)) {
|
||||
this.logger.log(`Today is the payment date for money request ${moneyRequest.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,6 @@ import {
|
||||
} from 'typeorm';
|
||||
import { Card } from '~/card/entities';
|
||||
import { CountryIso } from '~/common/enums';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { User } from '~/user/entities';
|
||||
@ -106,20 +105,6 @@ export class Customer extends BaseEntity {
|
||||
@OneToOne(() => Guardian, (guardian) => guardian.customer, { cascade: true })
|
||||
guardian!: Guardian;
|
||||
|
||||
@Column('uuid', { name: 'civil_id_front_id', nullable: true })
|
||||
civilIdFrontId!: string;
|
||||
|
||||
@Column('uuid', { name: 'civil_id_back_id', nullable: true })
|
||||
civilIdBackId!: string;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.customerCivilIdFront, { nullable: true })
|
||||
@JoinColumn({ name: 'civil_id_front_id' })
|
||||
civilIdFront!: Document;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.customerCivilIdBack, { nullable: true })
|
||||
@JoinColumn({ name: 'civil_id_back_id' })
|
||||
civilIdBack!: Document;
|
||||
|
||||
// relation ship between customer and card
|
||||
@OneToMany(() => Card, (card) => card.customer)
|
||||
cards!: Card[];
|
||||
|
@ -32,10 +32,4 @@ export class CustomerRepository {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findCustomerByCivilId(civilIdFrontId: string, civilIdBackId: string) {
|
||||
return this.customerRepository.findOne({
|
||||
where: [{ civilIdFrontId, civilIdBackId }, { civilIdFrontId }, { civilIdBackId }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,249 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class InitialMigration1733750228289 implements MigrationInterface {
|
||||
name = 'InitialMigration1733750228289';
|
||||
|
||||
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(
|
||||
`CREATE TABLE "saving_goals" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "description" character varying(255), "due_date" TIMESTAMP WITH TIME ZONE NOT NULL, "target_amount" numeric(12,3) NOT NULL, "current_amount" numeric(12,3) NOT NULL DEFAULT '0', "image_id" uuid, "junior_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_5193f14c1c3a38e6657a159795e" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "categories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" character varying(255) NOT NULL, "junior_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_24dbc6126a28ff948da33e97d3b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "task_submissions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "status" character varying NOT NULL, "submitted_at" TIMESTAMP WITH TIME ZONE NOT NULL, "task_id" uuid NOT NULL, "proof_of_completion_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_d6cfaee118a0300d652e28ee16" UNIQUE ("task_id"), CONSTRAINT "PK_8d19d6b5dd776e373113de50018" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "tasks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(255) NOT NULL, "description" character varying(255) NOT NULL, "reward_amount" numeric(12,3) NOT NULL, "image_id" uuid NOT NULL, "task_frequency" character varying NOT NULL, "start_date" TIMESTAMP WITH TIME ZONE NOT NULL, "due_date" TIMESTAMP WITH TIME ZONE NOT NULL, "is_proof_required" boolean NOT NULL, "assigned_to_id" uuid NOT NULL, "assigned_by_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8d12ff38fcc62aaba2cab748772" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`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(
|
||||
`CREATE TABLE "otp" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "value" character varying(255) NOT NULL, "scope" character varying(255) NOT NULL, "otp_type" character varying(255) NOT NULL, "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, "user_id" uuid NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_32556d9d7b22031d7d0e1fd6723" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_6427c192ef35355ebac18fb683" ON "otp" ("scope") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_258d028d322ea3b856bf9f12f2" ON "otp" ("user_id") `);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_registration_tokens" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying(255) NOT NULL, "user_type" character varying(255) NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "expiry_date" TIMESTAMP NOT NULL, "user_id" uuid, "junior_id" uuid, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_5881556d05b46fc7bd9e3bba935" UNIQUE ("token"), CONSTRAINT "PK_135a2d86443071ff0ba1c14135c" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5881556d05b46fc7bd9e3bba93" ON "user_registration_tokens" ("token") `);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying(255), "phone_number" character varying(255), "country_code" character varying(10), "password" character varying(255), "salt" character varying(255), "google_id" character varying(255), "apple_id" character varying(255), "is_phone_verified" boolean NOT NULL DEFAULT false, "is_email_verified" boolean NOT NULL DEFAULT false, "is_profile_completed" boolean NOT NULL DEFAULT false, "is_email_enabled" boolean NOT NULL DEFAULT false, "is_push_enabled" boolean NOT NULL DEFAULT false, "is_sms_enabled" boolean NOT NULL DEFAULT false, "roles" text array, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "devices" ("deviceId" character varying(255) NOT NULL, "user_id" uuid NOT NULL, "device_name" character varying, "public_key" character varying, "fcm_token" character varying, "last_access_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_666c9b59efda8ca85b29157152c" PRIMARY KEY ("deviceId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "documents" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "extension" character varying(255) NOT NULL, "document_type" character varying(255) NOT NULL, "created_by_id" uuid, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "customers" ("id" uuid NOT NULL, "customer_status" character varying(255) NOT NULL DEFAULT 'PENDING', "kyc_status" character varying(255) NOT NULL DEFAULT 'PENDING', "rejection_reason" text, "first_name" character varying(255), "last_name" character varying(255), "date_of_birth" date, "national_id" character varying(255), "national_id_expiry" date, "country_of_residence" character varying(255), "source_of_income" character varying(255), "profession" character varying(255), "profession_type" character varying(255), "is_pep" boolean NOT NULL DEFAULT false, "gender" character varying(255), "is_junior" boolean NOT NULL DEFAULT false, "is_guardian" boolean NOT NULL DEFAULT false, "application_number" SERIAL NOT NULL, "user_id" uuid NOT NULL, "profile_picture_id" uuid, "civil_id_front_id" uuid, "civil_id_back_id" uuid, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "REL_e7574892da11dd01de5cfc4649" UNIQUE ("profile_picture_id"), CONSTRAINT "REL_11d81cd7be87b6f8865b0cf766" UNIQUE ("user_id"), CONSTRAINT "REL_d5f99c497892ce31598ba19a72" UNIQUE ("civil_id_front_id"), CONSTRAINT "REL_2191662d124c56dd968ba01bf1" UNIQUE ("civil_id_back_id"), CONSTRAINT "PK_133ec679a801fab5e070f73d3ea" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "money_requests" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "requested_amount" numeric(10,3) NOT NULL, "message" character varying NOT NULL, "frequency" character varying NOT NULL DEFAULT 'ONE_TIME', "status" character varying NOT NULL DEFAULT 'PENDING', "reviewed_at" TIMESTAMP WITH TIME ZONE, "start_date" TIMESTAMP WITH TIME ZONE, "end_date" TIMESTAMP WITH TIME ZONE, "requester_id" uuid NOT NULL, "reviewer_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_28cff23e9fb06cd5dbf73cd53e7" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "themes" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "color" character varying(255) NOT NULL, "avatar_id" uuid, "junior_id" uuid NOT NULL, CONSTRAINT "REL_73fcb76399a308cdd2d431a8f2" UNIQUE ("junior_id"), CONSTRAINT "PK_ddbeaab913c18682e5c88155592" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "juniors" ("id" uuid NOT NULL, "relationship" character varying(255) NOT NULL, "customer_id" uuid NOT NULL, "guardian_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_dfbf64ede1ff823a489902448a" UNIQUE ("customer_id"), CONSTRAINT "PK_2d273092322c1f8bf26296fa608" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
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(
|
||||
`CREATE TABLE "guardians" ("id" uuid NOT NULL, "customer_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_6c46a1b6af00e6457cb1b70f7e" UNIQUE ("customer_id"), CONSTRAINT "PK_3dcf02f3dc96a2c017106f280be" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "saving_goals_categories" ("saving_goal_id" uuid NOT NULL, "category_id" uuid NOT NULL, CONSTRAINT "PK_a49d4f57d06d0a36a8385b6c28f" PRIMARY KEY ("saving_goal_id", "category_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_d421de423f21c01672ea7c2e98" ON "saving_goals_categories" ("saving_goal_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b0a721a8f7f5b6fe93f3603ebc" ON "saving_goals_categories" ("category_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`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals" ADD CONSTRAINT "FK_dad35932272342c1a247a2cee1c" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals" ADD CONSTRAINT "FK_f494ba0a361b2f9cbe5f6e56e38" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "categories" ADD CONSTRAINT "FK_4f98e0b010d5e90cae0a2007748" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_submissions" ADD CONSTRAINT "FK_d6cfaee118a0300d652e28ee166" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_submissions" ADD CONSTRAINT "FK_87876dfe440de7aafce216e9f58" FOREIGN KEY ("proof_of_completion_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_f1f00d41b1e95d0bbda2710a62b" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_9430f12c5a1604833f64595a57f" FOREIGN KEY ("assigned_to_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_3e08a7ca125a175cf899b09f71a" FOREIGN KEY ("assigned_by_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notifications" ADD CONSTRAINT "FK_9a8a82462cab47c73d25f49261f" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "otp" ADD CONSTRAINT "FK_258d028d322ea3b856bf9f12f25" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_registration_tokens" ADD CONSTRAINT "FK_57cbbe079a7945d6ed1df114825" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_registration_tokens" ADD CONSTRAINT "FK_e41bec3ed6e549cbf90f57cc344" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "devices" ADD CONSTRAINT "FK_5e9bee993b4ce35c3606cda194c" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "documents" ADD CONSTRAINT "FK_7f46f4f77acde1dcedba64cb220" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_d5f99c497892ce31598ba19a72c" FOREIGN KEY ("civil_id_front_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_2191662d124c56dd968ba01bf18" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_5cce02836c6033b6e2412995e34" FOREIGN KEY ("requester_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_75ba0766db9a7bf03126facf31c" FOREIGN KEY ("reviewer_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "themes" ADD CONSTRAINT "FK_169b672cc28cc757e1f4464864d" FOREIGN KEY ("avatar_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "themes" ADD CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "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`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals_categories" ADD CONSTRAINT "FK_d421de423f21c01672ea7c2e98f" FOREIGN KEY ("saving_goal_id") REFERENCES "saving_goals"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals_categories" ADD CONSTRAINT "FK_b0a721a8f7f5b6fe93f3603ebc8" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals_categories" DROP CONSTRAINT "FK_b0a721a8f7f5b6fe93f3603ebc8"`);
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals_categories" DROP CONSTRAINT "FK_d421de423f21c01672ea7c2e98f"`);
|
||||
await queryRunner.query(`ALTER TABLE "guardians" DROP CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7"`);
|
||||
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(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_0b11aa56264184690e2220da4a0"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_dfbf64ede1ff823a489902448a2"`);
|
||||
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e"`);
|
||||
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_169b672cc28cc757e1f4464864d"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_75ba0766db9a7bf03126facf31c"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_5cce02836c6033b6e2412995e34"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_2191662d124c56dd968ba01bf18"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_d5f99c497892ce31598ba19a72c"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
|
||||
await queryRunner.query(`ALTER TABLE "documents" DROP CONSTRAINT "FK_7f46f4f77acde1dcedba64cb220"`);
|
||||
await queryRunner.query(`ALTER TABLE "devices" DROP CONSTRAINT "FK_5e9bee993b4ce35c3606cda194c"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_registration_tokens" DROP CONSTRAINT "FK_e41bec3ed6e549cbf90f57cc344"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_registration_tokens" DROP CONSTRAINT "FK_57cbbe079a7945d6ed1df114825"`);
|
||||
await queryRunner.query(`ALTER TABLE "otp" DROP CONSTRAINT "FK_258d028d322ea3b856bf9f12f25"`);
|
||||
await queryRunner.query(`ALTER TABLE "notifications" DROP CONSTRAINT "FK_9a8a82462cab47c73d25f49261f"`);
|
||||
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_3e08a7ca125a175cf899b09f71a"`);
|
||||
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_9430f12c5a1604833f64595a57f"`);
|
||||
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_f1f00d41b1e95d0bbda2710a62b"`);
|
||||
await queryRunner.query(`ALTER TABLE "task_submissions" DROP CONSTRAINT "FK_87876dfe440de7aafce216e9f58"`);
|
||||
await queryRunner.query(`ALTER TABLE "task_submissions" DROP CONSTRAINT "FK_d6cfaee118a0300d652e28ee166"`);
|
||||
await queryRunner.query(`ALTER TABLE "categories" DROP CONSTRAINT "FK_4f98e0b010d5e90cae0a2007748"`);
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals" DROP CONSTRAINT "FK_f494ba0a361b2f9cbe5f6e56e38"`);
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals" DROP CONSTRAINT "FK_dad35932272342c1a247a2cee1c"`);
|
||||
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 INDEX "public"."IDX_b0a721a8f7f5b6fe93f3603ebc"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d421de423f21c01672ea7c2e98"`);
|
||||
await queryRunner.query(`DROP TABLE "saving_goals_categories"`);
|
||||
await queryRunner.query(`DROP TABLE "guardians"`);
|
||||
await queryRunner.query(`DROP TABLE "allowance_change_requests"`);
|
||||
await queryRunner.query(`DROP TABLE "allowances"`);
|
||||
await queryRunner.query(`DROP TABLE "juniors"`);
|
||||
await queryRunner.query(`DROP TABLE "themes"`);
|
||||
await queryRunner.query(`DROP TABLE "money_requests"`);
|
||||
await queryRunner.query(`DROP TABLE "customers"`);
|
||||
await queryRunner.query(`DROP TABLE "documents"`);
|
||||
await queryRunner.query(`DROP TABLE "devices"`);
|
||||
await queryRunner.query(`DROP TABLE "users"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_5881556d05b46fc7bd9e3bba93"`);
|
||||
await queryRunner.query(`DROP TABLE "user_registration_tokens"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_258d028d322ea3b856bf9f12f2"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_6427c192ef35355ebac18fb683"`);
|
||||
await queryRunner.query(`DROP TABLE "otp"`);
|
||||
await queryRunner.query(`DROP TABLE "notifications"`);
|
||||
await queryRunner.query(`DROP TABLE "tasks"`);
|
||||
await queryRunner.query(`DROP TABLE "task_submissions"`);
|
||||
await queryRunner.query(`DROP TABLE "categories"`);
|
||||
await queryRunner.query(`DROP TABLE "saving_goals"`);
|
||||
await queryRunner.query(`DROP TABLE "gift_redemptions"`);
|
||||
await queryRunner.query(`DROP TABLE "gift"`);
|
||||
await queryRunner.query(`DROP TABLE "gift_replies"`);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Document } from '~/document/entities';
|
||||
import { DocumentType, UploadStatus } from '~/document/enums';
|
||||
const DEFAULT_TASK_LOGOS = [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'bed-furniture',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
uploadStatus: UploadStatus.UPLOADED,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'dog',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
uploadStatus: UploadStatus.UPLOADED,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'dish-washing',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
uploadStatus: UploadStatus.UPLOADED,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'walking-the-dog',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
uploadStatus: UploadStatus.UPLOADED,
|
||||
},
|
||||
];
|
||||
export class SeedsDefaultTasksLogo1733990253208 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.manager.getRepository(Document).save(DEFAULT_TASK_LOGOS);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await DEFAULT_TASK_LOGOS.forEach(async (logo) => {
|
||||
await queryRunner.manager.getRepository(Document).delete({ name: logo.name, documentType: logo.documentType });
|
||||
});
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { CategoryType } from '~/saving-goals/enums';
|
||||
const DEFAULT_CATEGORIES = [
|
||||
{
|
||||
name: 'School',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Toys',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Games',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Clothes',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Hobbies',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Party',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Sport',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'University',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Travel',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
];
|
||||
export class SeedsGoalsCategories1734247702310 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`INSERT INTO categories (name, type) VALUES ${DEFAULT_CATEGORIES.map(
|
||||
(category) => `('${category.name}', '${category.type}')`,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM categories WHERE name IN (${DEFAULT_CATEGORIES.map(
|
||||
(category) => `'${category.name}'`,
|
||||
)}) AND type = '${CategoryType.GLOBAL}'`,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddNeoleapRelatedEntities1753874205042 implements MigrationInterface {
|
||||
name = 'AddNeoleapRelatedEntities1753874205042'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "transactions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "transaction_scope" character varying NOT NULL, "transaction_type" character varying NOT NULL DEFAULT 'EXTERNAL', "card_reference" character varying, "account_reference" character varying, "transaction_id" character varying, "card_masked_number" character varying, "transaction_date" TIMESTAMP WITH TIME ZONE, "rrn" character varying, "transaction_amount" numeric(12,2) NOT NULL, "transaction_currency" character varying NOT NULL, "billing_amount" numeric(12,2) NOT NULL, "settlement_amount" numeric(12,2) NOT NULL, "fees" numeric(12,2) NOT NULL, "vat_on_fees" numeric(12,2) NOT NULL DEFAULT '0', "card_id" uuid, "account_id" uuid, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_9162bf9ab4e31961a8f7932974c" UNIQUE ("transaction_id"), CONSTRAINT "PK_a219afd8dd77ed80f5a862f1db9" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "accounts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "account_reference" character varying(255) NOT NULL, "currency" character varying(255) NOT NULL, "balance" numeric(10,2) NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_829183fe026a0ce5fc8026b2417" UNIQUE ("account_reference"), CONSTRAINT "PK_5a7a02c20412299d198e097a8fe" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_829183fe026a0ce5fc8026b241" ON "accounts" ("account_reference") `);
|
||||
await queryRunner.query(`CREATE TABLE "cards" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "card_reference" character varying NOT NULL, "first_six_digits" character varying(6) NOT NULL, "last_four_digits" character varying(4) NOT NULL, "expiry" character varying NOT NULL, "customer_type" character varying NOT NULL, "color" character varying NOT NULL DEFAULT 'BLUE', "status" character varying NOT NULL DEFAULT 'PENDING', "statusDescription" character varying NOT NULL DEFAULT 'PENDING_ACTIVATION', "limit" numeric(10,2) NOT NULL DEFAULT '0', "scheme" character varying NOT NULL DEFAULT 'VISA', "issuer" character varying NOT NULL, "customer_id" uuid NOT NULL, "parent_id" uuid, "account_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_5f3269634705fdff4a9935860fc" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4f7df5c5dc950295dc417a11d8" ON "cards" ("card_reference") `);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "country" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "region" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "city" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "neighborhood" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "street" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "building" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "transactions" ADD CONSTRAINT "FK_80ad48141be648db2d84ff32f79" FOREIGN KEY ("card_id") REFERENCES "cards"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "transactions" ADD CONSTRAINT "FK_49c0d6e8ba4bfb5582000d851f0" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "cards" ADD CONSTRAINT "FK_8ba18e7060c38ddae12e5bdf907" FOREIGN KEY ("parent_id") REFERENCES "customers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "cards" ADD CONSTRAINT "FK_2fd0ee722ec57594d2e448c73d7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "cards" ADD CONSTRAINT "FK_b2874ef49ff7da2dee49e4bc6d3" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_b2874ef49ff7da2dee49e4bc6d3"`);
|
||||
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_2fd0ee722ec57594d2e448c73d7"`);
|
||||
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_8ba18e7060c38ddae12e5bdf907"`);
|
||||
await queryRunner.query(`ALTER TABLE "transactions" DROP CONSTRAINT "FK_49c0d6e8ba4bfb5582000d851f0"`);
|
||||
await queryRunner.query(`ALTER TABLE "transactions" DROP CONSTRAINT "FK_80ad48141be648db2d84ff32f79"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "building"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "street"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "neighborhood"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "city"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "region"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "country"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4f7df5c5dc950295dc417a11d8"`);
|
||||
await queryRunner.query(`DROP TABLE "cards"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_829183fe026a0ce5fc8026b241"`);
|
||||
await queryRunner.query(`DROP TABLE "accounts"`);
|
||||
await queryRunner.query(`DROP TABLE "transactions"`);
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
export class AddAccountNumberAndIbanToAccountEntity1753948642040 implements MigrationInterface {
|
||||
name = 'AddAccountNumberAndIbanToAccountEntity1753948642040';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Step 1: Add columns as nullable
|
||||
await queryRunner.query(`ALTER TABLE "accounts" ADD "account_number" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "accounts" ADD "iban" character varying(255)`);
|
||||
|
||||
// Step 2: Populate dummy values or correct ones
|
||||
await queryRunner.query(`
|
||||
UPDATE "accounts"
|
||||
SET "account_number" = 'TEMP_ACC_' || id,
|
||||
"iban" = 'TEMP_IBAN_' || id
|
||||
`);
|
||||
|
||||
// Step 3: Alter columns to be NOT NULL
|
||||
await queryRunner.query(`ALTER TABLE "accounts" ALTER COLUMN "account_number" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "accounts" ALTER COLUMN "iban" SET NOT NULL`);
|
||||
|
||||
// Step 4: Add unique indexes
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ffd1ae96513bfb2c6eada0f7d3" ON "accounts" ("account_number")`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9a4b004902294416b096e7556e" ON "accounts" ("iban")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_9a4b004902294416b096e7556e"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ffd1ae96513bfb2c6eada0f7d3"`);
|
||||
await queryRunner.query(`ALTER TABLE "accounts" DROP COLUMN "iban"`);
|
||||
await queryRunner.query(`ALTER TABLE "accounts" DROP COLUMN "account_number"`);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddVpanToCard1754210729273 implements MigrationInterface {
|
||||
name = 'AddVpanToCard1754210729273';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "cards" ADD "vpan" character varying`);
|
||||
await queryRunner.query(`
|
||||
UPDATE "cards"
|
||||
SET "vpan" = 'TEMP_VPAN_' || id
|
||||
`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "cards" ALTER COLUMN "vpan" SET NOT NULL`);
|
||||
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_1ec2ef68b0370f26639261e87b" ON "cards" ("vpan") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_1ec2ef68b0370f26639261e87b"`);
|
||||
await queryRunner.query(`ALTER TABLE "cards" DROP COLUMN "vpan"`);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUploadStatusToDocumentEntity1754226754947 implements MigrationInterface {
|
||||
name = 'AddUploadStatusToDocumentEntity1754226754947';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "documents" ADD "upload_status" character varying(255) NOT NULL DEFAULT 'NOT_UPLOADED'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "documents" DROP COLUMN "upload_status"`);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddDisplayNameToUser1754399872619 implements MigrationInterface {
|
||||
name = 'AddDisplayNameToUser1754399872619';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Step 1: Add columns as nullable
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "first_name" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "last_name" character varying(255)`);
|
||||
|
||||
// Step 2: Populate the new columns with fallback to test values
|
||||
await queryRunner.query(`
|
||||
UPDATE "users"
|
||||
SET "first_name" = COALESCE(c."first_name", 'TEST_FIRST_NAME'),
|
||||
"last_name" = COALESCE(c."last_name", 'TEST_LAST_NAME')
|
||||
FROM "customers" c
|
||||
WHERE c.user_id = "users"."id"
|
||||
`);
|
||||
|
||||
// Step 2b: Handle users without a matching customer row
|
||||
await queryRunner.query(`
|
||||
UPDATE "users"
|
||||
SET "first_name" = COALESCE("first_name", 'TEST_FIRST_NAME'),
|
||||
"last_name" = COALESCE("last_name", 'TEST_LAST_NAME')
|
||||
WHERE "first_name" IS NULL OR "last_name" IS NULL
|
||||
`);
|
||||
|
||||
// Step 3: Make the columns NOT NULL
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "first_name" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "last_name" SET NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "last_name"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "first_name"`);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddProfilePictureToUserInsteadOfCustomer1754401348483 implements MigrationInterface {
|
||||
name = 'AddProfilePictureToUserInsteadOfCustomer1754401348483';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "REL_e7574892da11dd01de5cfc4649"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "profile_picture_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "profile_picture_id" uuid`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD CONSTRAINT "UQ_02ec15de199e79a0c46869895f4" UNIQUE ("profile_picture_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_02ec15de199e79a0c46869895f4"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_02ec15de199e79a0c46869895f4"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profile_picture_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" ADD "profile_picture_id" uuid`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "REL_e7574892da11dd01de5cfc4649" UNIQUE ("profile_picture_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
105
src/db/migrations/1754913378460-initial-migration.ts
Normal file
105
src/db/migrations/1754913378460-initial-migration.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class InitialMigration1754913378460 implements MigrationInterface {
|
||||
name = 'InitialMigration1754913378460';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "themes" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "color" character varying(255) NOT NULL, "avatar_id" uuid, "junior_id" uuid NOT NULL, CONSTRAINT "REL_73fcb76399a308cdd2d431a8f2" UNIQUE ("junior_id"), CONSTRAINT "PK_ddbeaab913c18682e5c88155592" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "juniors" ("id" uuid NOT NULL, "relationship" character varying(255) NOT NULL, "customer_id" uuid NOT NULL, "guardian_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 "REL_dfbf64ede1ff823a489902448a" UNIQUE ("customer_id"), CONSTRAINT "PK_2d273092322c1f8bf26296fa608" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
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(
|
||||
`CREATE TABLE "otp" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "value" character varying(255) NOT NULL, "scope" character varying(255) NOT NULL, "otp_type" character varying(255) NOT NULL, "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, "user_id" uuid NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_32556d9d7b22031d7d0e1fd6723" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_6427c192ef35355ebac18fb683" ON "otp" ("scope") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_258d028d322ea3b856bf9f12f2" ON "otp" ("user_id") `);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_auth_tokens" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying(255) NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "expiry_date" TIMESTAMP WITH TIME ZONE NOT NULL, "user_id" uuid NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_579eaaadf51ae3542caf3491062" UNIQUE ("token"), CONSTRAINT "PK_e15c7c76bf967080b272104d828" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_579eaaadf51ae3542caf349106" ON "user_auth_tokens" ("token") `);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying(255), "phone_number" character varying(255), "country_code" character varying(10), "password" character varying(255), "salt" character varying(255), "google_id" character varying(255), "apple_id" character varying(255), "is_phone_verified" boolean NOT NULL DEFAULT false, "is_email_verified" boolean NOT NULL DEFAULT false, "is_profile_completed" boolean NOT NULL DEFAULT false, "is_email_enabled" boolean NOT NULL DEFAULT false, "is_push_enabled" boolean NOT NULL DEFAULT false, "is_sms_enabled" boolean NOT NULL DEFAULT false, "roles" text array, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "devices" ("deviceId" character varying(255) NOT NULL, "user_id" uuid NOT NULL, "device_name" character varying, "public_key" character varying, "fcm_token" character varying, "last_access_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_666c9b59efda8ca85b29157152c" PRIMARY KEY ("deviceId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "documents" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "extension" character varying(255) NOT NULL, "document_type" character varying(255) NOT NULL, "created_by_id" uuid, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "customers" ("id" uuid NOT NULL, "customer_status" character varying(255) NOT NULL DEFAULT 'PENDING', "kyc_status" character varying(255) NOT NULL DEFAULT 'PENDING', "rejection_reason" text, "first_name" character varying(255), "last_name" character varying(255), "date_of_birth" date, "national_id" character varying(255), "national_id_expiry" date, "country_of_residence" character varying(255), "source_of_income" character varying(255), "profession" character varying(255), "profession_type" character varying(255), "is_pep" boolean NOT NULL DEFAULT false, "gender" character varying(255), "is_junior" boolean NOT NULL DEFAULT false, "is_guardian" boolean NOT NULL DEFAULT false, "application_number" SERIAL NOT NULL, "user_id" uuid NOT NULL, "profile_picture_id" uuid, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "REL_e7574892da11dd01de5cfc4649" UNIQUE ("profile_picture_id"), CONSTRAINT "REL_11d81cd7be87b6f8865b0cf766" UNIQUE ("user_id"), CONSTRAINT "PK_133ec679a801fab5e070f73d3ea" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "guardians" ("id" uuid NOT NULL, "customer_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 "REL_6c46a1b6af00e6457cb1b70f7e" UNIQUE ("customer_id"), CONSTRAINT "PK_3dcf02f3dc96a2c017106f280be" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "themes" ADD CONSTRAINT "FK_169b672cc28cc757e1f4464864d" FOREIGN KEY ("avatar_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "themes" ADD CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notifications" ADD CONSTRAINT "FK_9a8a82462cab47c73d25f49261f" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "otp" ADD CONSTRAINT "FK_258d028d322ea3b856bf9f12f25" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_auth_tokens" ADD CONSTRAINT "FK_bab7def1955bd13dcc47c036c03" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "devices" ADD CONSTRAINT "FK_5e9bee993b4ce35c3606cda194c" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "documents" ADD CONSTRAINT "FK_7f46f4f77acde1dcedba64cb220" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "guardians" DROP CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
|
||||
await queryRunner.query(`ALTER TABLE "documents" DROP CONSTRAINT "FK_7f46f4f77acde1dcedba64cb220"`);
|
||||
await queryRunner.query(`ALTER TABLE "devices" DROP CONSTRAINT "FK_5e9bee993b4ce35c3606cda194c"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_auth_tokens" DROP CONSTRAINT "FK_bab7def1955bd13dcc47c036c03"`);
|
||||
await queryRunner.query(`ALTER TABLE "otp" DROP CONSTRAINT "FK_258d028d322ea3b856bf9f12f25"`);
|
||||
await queryRunner.query(`ALTER TABLE "notifications" DROP CONSTRAINT "FK_9a8a82462cab47c73d25f49261f"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_0b11aa56264184690e2220da4a0"`);
|
||||
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_dfbf64ede1ff823a489902448a2"`);
|
||||
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e"`);
|
||||
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_169b672cc28cc757e1f4464864d"`);
|
||||
await queryRunner.query(`DROP TABLE "guardians"`);
|
||||
await queryRunner.query(`DROP TABLE "customers"`);
|
||||
await queryRunner.query(`DROP TABLE "documents"`);
|
||||
await queryRunner.query(`DROP TABLE "devices"`);
|
||||
await queryRunner.query(`DROP TABLE "users"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_579eaaadf51ae3542caf349106"`);
|
||||
await queryRunner.query(`DROP TABLE "user_auth_tokens"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_258d028d322ea3b856bf9f12f2"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_6427c192ef35355ebac18fb683"`);
|
||||
await queryRunner.query(`DROP TABLE "otp"`);
|
||||
await queryRunner.query(`DROP TABLE "notifications"`);
|
||||
await queryRunner.query(`DROP TABLE "juniors"`);
|
||||
await queryRunner.query(`DROP TABLE "themes"`);
|
||||
}
|
||||
}
|
@ -67,7 +67,7 @@ const DEFAULT_AVATARS = [
|
||||
uploadStatus: UploadStatus.UPLOADED,
|
||||
},
|
||||
];
|
||||
export class SeedDefaultAvatar1753869637732 implements MigrationInterface {
|
||||
export class SeedDefaultAvatar1754913378460 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.manager.getRepository(Document).save(DEFAULT_AVATARS);
|
||||
}
|
@ -1,10 +1,2 @@
|
||||
export * from './1733750228289-initial-migration';
|
||||
export * from './1733990253208-seeds-default-tasks-logo';
|
||||
export * from './1734247702310-seeds-goals-categories';
|
||||
export * from './1753869637732-seed-default-avatar';
|
||||
export * from './1753874205042-add-neoleap-related-entities';
|
||||
export * from './1753948642040-add-account-number-and-iban-to-account-entity';
|
||||
export * from './1754210729273-add-vpan-to-card';
|
||||
export * from './1754226754947-add-upload-status-to-document-entity';
|
||||
export * from './1754399872619-add-display-name-to-user';
|
||||
export * from './1754401348483-add-profile-picture-to-user-instead-of-customer';
|
||||
export * from './1754913378460-initial-migration';
|
||||
export * from './1754913378461-seed-default-avatar';
|
||||
|
@ -9,11 +9,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Gift } from '~/gift/entities';
|
||||
import { Theme } from '~/junior/entities';
|
||||
import { SavingGoal } from '~/saving-goals/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
|
||||
import { User } from '~/user/entities';
|
||||
import { DocumentType, UploadStatus } from '../enums';
|
||||
|
||||
@ -40,35 +36,17 @@ export class Document {
|
||||
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'SET NULL' })
|
||||
userPicture?: Customer;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.civilIdFront, { onDelete: 'SET NULL' })
|
||||
customerCivilIdFront?: User;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.civilIdBack, { onDelete: 'SET NULL' })
|
||||
customerCivilIdBack?: User;
|
||||
|
||||
@OneToMany(() => Theme, (theme) => theme.avatar)
|
||||
themes?: Theme[];
|
||||
|
||||
@OneToMany(() => Task, (task) => task.image)
|
||||
tasks?: Task[];
|
||||
|
||||
@OneToMany(() => TaskSubmission, (taskSubmission) => taskSubmission.proofOfCompletion)
|
||||
submissions?: TaskSubmission[];
|
||||
|
||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
|
||||
goals?: SavingGoal[];
|
||||
|
||||
@OneToMany(() => Gift, (gift) => gift.image)
|
||||
gifts?: Gift[];
|
||||
|
||||
@ManyToOne(() => User, (user) => user.createdDocuments, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'created_by_id' })
|
||||
createdBy?: User;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
@Column({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
// virtual field
|
||||
|
@ -1,82 +0,0 @@
|
||||
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, ApiLangRequestHeader } 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()
|
||||
@ApiLangRequestHeader()
|
||||
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);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './gifts.controller';
|
@ -1,32 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './create-gift.request.dto';
|
||||
export * from './gift-filters.request.dto';
|
||||
export * from './gift-reply.request.dto';
|
@ -1,54 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './gift-details.response.dto';
|
||||
export * from './gift-list.response.dto';
|
@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './gift-redemption.entity';
|
||||
export * from './gift-reply.entity';
|
||||
export * from './gift.entity';
|
@ -1,6 +0,0 @@
|
||||
export enum GiftColor {
|
||||
VIOLET = 'VIOLET',
|
||||
PINK = 'PINK',
|
||||
BLUE = 'BLUE',
|
||||
GREEN = 'GREEN',
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export enum GiftReplyStatus {
|
||||
PENDING = 'PENDING',
|
||||
OPENED = 'OPENED',
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export enum GiftStatus {
|
||||
AVAILABLE = 'AVAILABLE',
|
||||
REDEEMED = 'REDEEMED',
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './gift-color.enum';
|
||||
export * from './gift-reply-status.enum';
|
||||
export * from './gift-status.enum';
|
@ -1,14 +0,0 @@
|
||||
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 {}
|
@ -1,76 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './gifts.repository';
|
@ -1,139 +0,0 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { DocumentService, 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 {
|
||||
private readonly logger = new Logger(GiftsService.name);
|
||||
constructor(
|
||||
private readonly juniorService: JuniorService,
|
||||
private readonly giftsRepository: GiftsRepository,
|
||||
private readonly ociService: OciService,
|
||||
private readonly documentService: DocumentService,
|
||||
) {}
|
||||
|
||||
async createGift(guardianId: string, body: CreateGiftRequestDto) {
|
||||
this.logger.log(`Creating gift for junior ${body.recipientId} by guardian ${guardianId}`);
|
||||
|
||||
await this.validateGiftImage(guardianId, body.imageId);
|
||||
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(
|
||||
guardianId,
|
||||
body.recipientId,
|
||||
);
|
||||
|
||||
if (!doesJuniorBelongToGuardian) {
|
||||
this.logger.error(`Junior ${body.recipientId} does not belong to guardian ${guardianId}`);
|
||||
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||
}
|
||||
|
||||
const gift = await this.giftsRepository.create(guardianId, body);
|
||||
this.logger.log(`Gift ${gift.id} created successfully`);
|
||||
return this.findUserGiftById({ sub: guardianId, roles: [Roles.GUARDIAN] }, gift.id);
|
||||
}
|
||||
|
||||
async findUserGiftById(user: IJwtPayload, giftId: string) {
|
||||
this.logger.log(`Finding gift ${giftId} for user ${user.sub} with roles ${user.roles}`);
|
||||
const gift = user.roles.includes(Roles.GUARDIAN)
|
||||
? await this.giftsRepository.findGuardianGiftById(user.sub, giftId)
|
||||
: await this.giftsRepository.findJuniorGiftById(user.sub, giftId);
|
||||
|
||||
if (!gift) {
|
||||
this.logger.error(`Gift ${giftId} not found`);
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
await this.prepareGiftImages([gift]);
|
||||
|
||||
this.logger.log(`Gift ${giftId} found successfully`);
|
||||
|
||||
return gift;
|
||||
}
|
||||
|
||||
async redeemGift(juniorId: string, giftId: string) {
|
||||
this.logger.log(`Redeeming gift ${giftId} for junior ${juniorId}`);
|
||||
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||
|
||||
if (!gift) {
|
||||
this.logger.error(`Gift ${giftId} not found`);
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (gift.status === GiftStatus.REDEEMED) {
|
||||
this.logger.error(`Gift ${giftId} already redeemed`);
|
||||
throw new BadRequestException('GIFT.ALREADY_REDEEMED');
|
||||
}
|
||||
|
||||
return this.giftsRepository.redeemGift(gift);
|
||||
}
|
||||
|
||||
async UndoGiftRedemption(juniorId: string, giftId: string) {
|
||||
this.logger.log(`Undoing gift redemption for junior ${juniorId} and gift ${giftId}`);
|
||||
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||
|
||||
if (!gift) {
|
||||
this.logger.error(`Gift ${giftId} not found`);
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (gift.status === GiftStatus.AVAILABLE) {
|
||||
this.logger.error(`Gift ${giftId} is not redeemed yet`);
|
||||
throw new BadRequestException('GIFT.NOT_REDEEMED');
|
||||
}
|
||||
|
||||
return this.giftsRepository.UndoGiftRedemption(gift);
|
||||
}
|
||||
|
||||
async replyToGift(juniorId: string, giftId: string, body: GiftReplyRequestDto) {
|
||||
this.logger.log(`Replying to gift ${giftId} for junior ${juniorId}`);
|
||||
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||
|
||||
if (!gift) {
|
||||
this.logger.error(`Gift ${giftId} not found`);
|
||||
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (gift.reply) {
|
||||
this.logger.error(`Gift ${giftId} already replied`);
|
||||
throw new BadRequestException('GIFT.ALREADY_REPLIED');
|
||||
}
|
||||
|
||||
return this.giftsRepository.replyToGift(gift, body);
|
||||
}
|
||||
|
||||
findGifts(user: IJwtPayload, filters: GiftFiltersRequestDto) {
|
||||
this.logger.log(`Finding gifts for user ${user.sub} with roles ${user.roles}`);
|
||||
return this.giftsRepository.findGifts(user, filters);
|
||||
}
|
||||
|
||||
private async prepareGiftImages(gifts: Gift[]) {
|
||||
this.logger.log('Preparing gifts image');
|
||||
await Promise.all(
|
||||
gifts.map(async (gift) => {
|
||||
gift.image.url = await this.ociService.generatePreSignedUrl(gift.image);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async validateGiftImage(userId: string, imageId?: string) {
|
||||
if (!imageId) return;
|
||||
|
||||
this.logger.log(`Validating gift image ${imageId}`);
|
||||
const image = await this.documentService.findDocumentById(imageId);
|
||||
|
||||
if (!image) {
|
||||
this.logger.error(`Gift image ${imageId} not found`);
|
||||
throw new BadRequestException('DOCUMENT.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (image.createdById && image.createdById !== userId) {
|
||||
this.logger.error(`Gift image ${imageId} does not belong to user ${userId}`);
|
||||
throw new BadRequestException('DOCUMENT.NOT_CREATED_BY_USER');
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './gifts.service';
|
@ -9,12 +9,8 @@ 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';
|
||||
|
||||
@Entity('guardians')
|
||||
export class Guardian extends BaseEntity {
|
||||
@ -31,21 +27,9 @@ export class Guardian extends BaseEntity {
|
||||
@OneToMany(() => Junior, (junior) => junior.guardian)
|
||||
juniors!: Junior[];
|
||||
|
||||
@OneToMany(() => Task, (task) => task.assignedBy)
|
||||
tasks?: Task[];
|
||||
|
||||
@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' })
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
@ -5,19 +5,12 @@ import {
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Allowance } from '~/allowance/entities';
|
||||
import { Customer } from '~/customer/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';
|
||||
import { Task } from '~/task/entities';
|
||||
import { UserRegistrationToken } from '~/user/entities';
|
||||
import { Relationship } from '../enums';
|
||||
import { Theme } from './theme.entity';
|
||||
|
||||
@ -46,30 +39,9 @@ export class Junior extends BaseEntity {
|
||||
@JoinColumn({ name: 'guardian_id' })
|
||||
guardian!: Guardian;
|
||||
|
||||
@OneToMany(() => Task, (task) => task.assignedTo)
|
||||
tasks!: Task[];
|
||||
|
||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.junior)
|
||||
goals!: SavingGoal[];
|
||||
|
||||
@OneToMany(() => Category, (category) => category.junior)
|
||||
categories!: Category[];
|
||||
|
||||
@OneToMany(() => UserRegistrationToken, (token) => token.junior)
|
||||
registrationTokens!: UserRegistrationToken[];
|
||||
|
||||
@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' })
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ export class JuniorService {
|
||||
async generateToken(juniorId: string) {
|
||||
this.logger.log(`Generating token for junior ${juniorId}`);
|
||||
|
||||
const token = await this.userTokenService.generateToken(juniorId, UserType.JUNIOR);
|
||||
const token = await this.userTokenService.generateToken(juniorId);
|
||||
|
||||
return this.qrCodeService.generateQrCode(token);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './money-requests.controller';
|
@ -1,82 +0,0 @@
|
||||
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, ApiLangRequestHeader } from '~/core/decorators';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequestResponseDto } from '../dtos/response/money-request.response.dto';
|
||||
import { MoneyRequestsService } from '../services';
|
||||
|
||||
@Controller('money-requests')
|
||||
@ApiTags('Money Requests')
|
||||
@ApiBearerAuth()
|
||||
@ApiLangRequestHeader()
|
||||
export class MoneyRequestsController {
|
||||
constructor(private readonly moneyRequestsService: MoneyRequestsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@ApiDataResponse(MoneyRequestResponseDto)
|
||||
async createMoneyRequest(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateMoneyRequestRequestDto) {
|
||||
const moneyRequest = await this.moneyRequestsService.createMoneyRequest(sub, body);
|
||||
|
||||
return ResponseFactory.data(new MoneyRequestResponseDto(moneyRequest));
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Get()
|
||||
@ApiDataPageResponse(MoneyRequestResponseDto)
|
||||
async findMoneyRequests(@AuthenticatedUser() { sub }: IJwtPayload, @Query() filters: MoneyRequestsFiltersRequestDto) {
|
||||
const [moneyRequests, itemCount] = await this.moneyRequestsService.findMoneyRequests(sub, filters);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
moneyRequests.map((moneyRequest) => new MoneyRequestResponseDto(moneyRequest)),
|
||||
{
|
||||
itemCount,
|
||||
page: filters.page,
|
||||
size: filters.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Get(':moneyRequestId')
|
||||
@ApiDataResponse(MoneyRequestResponseDto)
|
||||
async findMoneyRequestById(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('moneyRequestId', CustomParseUUIDPipe) moneyRequestId: string,
|
||||
) {
|
||||
const moneyRequest = await this.moneyRequestsService.findMoneyRequestById(moneyRequestId, sub);
|
||||
|
||||
return ResponseFactory.data(new MoneyRequestResponseDto(moneyRequest));
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Patch(':moneyRequestId/approve')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async approveMoneyRequest(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('moneyRequestId', CustomParseUUIDPipe) moneyRequestId: string,
|
||||
) {
|
||||
await this.moneyRequestsService.approveMoneyRequest(moneyRequestId, sub);
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Patch(':moneyRequestId/reject')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async rejectMoneyRequest(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('moneyRequestId', CustomParseUUIDPipe) moneyRequestId: string,
|
||||
) {
|
||||
await this.moneyRequestsService.rejectMoneyRequest(moneyRequestId, sub);
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { MoneyRequestFrequency } from '~/money-request/enums';
|
||||
const MIN_REQUESTED_AMOUNT = 0.01;
|
||||
const MAX_REQUESTED_AMOUNT = 1000000;
|
||||
export class CreateMoneyRequestRequestDto {
|
||||
@Min(MIN_REQUESTED_AMOUNT, {
|
||||
message: i18n('validation.Min', { path: 'general', property: 'moneyRequest.requestedAmount' }),
|
||||
})
|
||||
@Max(MAX_REQUESTED_AMOUNT, {
|
||||
message: i18n('validation.Max', { path: 'general', property: 'moneyRequest.requestedAmount' }),
|
||||
})
|
||||
@IsNumber(
|
||||
{ allowNaN: false },
|
||||
{ message: i18n('validation.IsNumber', { path: 'general', property: 'moneyRequest.requestedAmount' }) },
|
||||
)
|
||||
@ApiProperty()
|
||||
requestedAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'moneyRequest.message' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'moneyRequest.message' }) })
|
||||
message!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: MoneyRequestFrequency.ONE_TIME })
|
||||
@IsEnum(MoneyRequestFrequency, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'moneyRequest.frequency' }),
|
||||
})
|
||||
@IsOptional()
|
||||
frequency: MoneyRequestFrequency = MoneyRequestFrequency.ONE_TIME;
|
||||
|
||||
@ApiPropertyOptional({ example: '2021-01-01' })
|
||||
@IsDateString(
|
||||
{},
|
||||
{ message: i18n('validation.IsDateString', { path: 'general', property: 'moneyRequest.startDate' }) },
|
||||
)
|
||||
@ValidateIf((o) => o.frequency !== MoneyRequestFrequency.ONE_TIME)
|
||||
startDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '2021-01-01' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'moneyRequest.endDate' }) })
|
||||
@IsOptional()
|
||||
endDate?: string;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './create-money-request.request.dto';
|
||||
export * from './money-requests-filters.request.dto';
|
@ -1,12 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { MoneyRequestStatus } from '~/money-request/enums';
|
||||
export class MoneyRequestsFiltersRequestDto extends PageOptionsRequestDto {
|
||||
@ApiProperty({ example: MoneyRequestStatus.PENDING, enum: MoneyRequestStatus })
|
||||
@IsEnum(MoneyRequestStatus, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'moneyRequest.status' }),
|
||||
})
|
||||
status: MoneyRequestStatus = MoneyRequestStatus.PENDING;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||
import { MoneyRequest } from '~/money-request/entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '~/money-request/enums';
|
||||
|
||||
export class MoneyRequestResponseDto {
|
||||
@ApiProperty({ example: 'f5c7e193-bc5e-4aa5-837b-c1edc6449880' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ type: JuniorResponseDto })
|
||||
requester!: JuniorResponseDto;
|
||||
|
||||
@ApiProperty({ example: 'f5c7e193-bc5e-4aa5-837b-c1edc6449880' })
|
||||
reviewerId!: string;
|
||||
|
||||
@ApiProperty({ example: 100.0 })
|
||||
requestedAmount!: number;
|
||||
|
||||
@ApiProperty({ example: 'Please give me money' })
|
||||
message!: string;
|
||||
|
||||
@ApiProperty({ example: MoneyRequestFrequency.ONE_TIME })
|
||||
frequency!: MoneyRequestFrequency;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
startDate!: Date | null;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
endDate!: Date | null;
|
||||
|
||||
@ApiProperty({ example: MoneyRequestStatus.PENDING })
|
||||
status!: MoneyRequestStatus;
|
||||
|
||||
@ApiProperty()
|
||||
reviewedAt!: Date | null;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(moneyRequest: MoneyRequest) {
|
||||
this.id = moneyRequest.id;
|
||||
this.requester = new JuniorResponseDto(moneyRequest.requester);
|
||||
this.reviewerId = moneyRequest.reviewerId;
|
||||
this.requestedAmount = moneyRequest.requestedAmount;
|
||||
this.message = moneyRequest.message;
|
||||
this.frequency = moneyRequest.frequency;
|
||||
this.startDate = moneyRequest.startDate || null;
|
||||
this.endDate = moneyRequest.endDate || null;
|
||||
this.status = moneyRequest.status;
|
||||
this.reviewedAt = moneyRequest.reviewedAt || null;
|
||||
this.createdAt = moneyRequest.createdAt;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './money-request.entity';
|
@ -1,97 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '../enums';
|
||||
|
||||
@Entity('money_requests')
|
||||
export class MoneyRequest {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 3, name: 'requested_amount' })
|
||||
requestedAmount!: number;
|
||||
|
||||
@Column({ type: 'varchar', name: 'message' })
|
||||
message!: string;
|
||||
|
||||
@Column({ type: 'varchar', name: 'frequency', default: MoneyRequestFrequency.ONE_TIME })
|
||||
frequency!: MoneyRequestFrequency;
|
||||
|
||||
@Column({ type: 'varchar', name: 'status', default: MoneyRequestStatus.PENDING })
|
||||
status!: MoneyRequestStatus;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'reviewed_at', nullable: true })
|
||||
reviewedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'start_date', nullable: true })
|
||||
startDate!: Date | null;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'end_date', nullable: true })
|
||||
endDate!: Date | null;
|
||||
|
||||
@Column({ type: 'uuid', name: 'requester_id' })
|
||||
requesterId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'reviewer_id' })
|
||||
reviewerId!: string;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.moneyRequests)
|
||||
@JoinColumn({ name: 'requester_id' })
|
||||
requester!: Junior;
|
||||
|
||||
@ManyToOne(() => Guardian, (guardian) => guardian.moneyRequests)
|
||||
@JoinColumn({ name: 'reviewer_id' })
|
||||
reviewer!: Guardian;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp with time zone', name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp with time zone', name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
|
||||
get nextPaymentDate(): Date | null {
|
||||
if (this.frequency === MoneyRequestFrequency.ONE_TIME) {
|
||||
return null;
|
||||
}
|
||||
const startDate = moment(this.startDate).clone().startOf('day');
|
||||
const endDate = this.endDate ? moment(this.endDate).endOf('day') : null;
|
||||
const now = moment().startOf('day');
|
||||
|
||||
if (endDate && moment().isAfter(endDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const calculateNextDate = (unit: moment.unitOfTime.Diff) => {
|
||||
const diff = now.diff(startDate, unit);
|
||||
const nextDate = startDate.clone().add(diff, unit);
|
||||
|
||||
const adjustedDate = nextDate.isSameOrAfter(now) ? nextDate : nextDate.add('1', unit);
|
||||
|
||||
if (endDate && adjustedDate.isAfter(endDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return adjustedDate.toDate();
|
||||
};
|
||||
|
||||
switch (this.frequency) {
|
||||
case MoneyRequestFrequency.DAILY:
|
||||
return calculateNextDate('days');
|
||||
case MoneyRequestFrequency.WEEKLY:
|
||||
return calculateNextDate('weeks');
|
||||
case MoneyRequestFrequency.MONTHLY:
|
||||
return calculateNextDate('months');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './money-request-frequency.enum';
|
||||
export * from './money-request-status.enum';
|
@ -1,6 +0,0 @@
|
||||
export enum MoneyRequestFrequency {
|
||||
ONE_TIME = 'ONE_TIME',
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export enum MoneyRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JuniorModule } from '~/junior/junior.module';
|
||||
import { MoneyRequestsController } from './controllers';
|
||||
import { MoneyRequest } from './entities';
|
||||
import { MoneyRequestsRepository } from './repositories';
|
||||
import { MoneyRequestsService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [MoneyRequestsController],
|
||||
providers: [MoneyRequestsService, MoneyRequestsRepository],
|
||||
exports: [MoneyRequestsService],
|
||||
imports: [TypeOrmModule.forFeature([MoneyRequest]), JuniorModule],
|
||||
})
|
||||
export class MoneyRequestModule {}
|
@ -1 +0,0 @@
|
||||
export * from './money-requests.repository';
|
@ -1,80 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, MoreThan, Not, Repository } from 'typeorm';
|
||||
import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequest } from '../entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class MoneyRequestsRepository {
|
||||
constructor(@InjectRepository(MoneyRequest) private readonly moneyRequestRepository: Repository<MoneyRequest>) {}
|
||||
|
||||
createMoneyRequest(requesterId: string, reviewerId: string, body: CreateMoneyRequestRequestDto) {
|
||||
return this.moneyRequestRepository.save(
|
||||
this.moneyRequestRepository.create({
|
||||
requesterId,
|
||||
reviewerId,
|
||||
requestedAmount: body.requestedAmount,
|
||||
message: body.message,
|
||||
startDate: body.startDate,
|
||||
endDate: body.endDate,
|
||||
frequency: body.frequency,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findMoneyRequestById(moneyRequestId: string, reviewerId?: string) {
|
||||
return this.moneyRequestRepository.findOne({
|
||||
where: { id: moneyRequestId, reviewerId },
|
||||
relations: ['requester', 'requester.customer', 'requester.customer.profilePicture'],
|
||||
});
|
||||
}
|
||||
|
||||
findMoneyRequests(reviewerId: string, filters: MoneyRequestsFiltersRequestDto) {
|
||||
const query = this.moneyRequestRepository.createQueryBuilder('moneyRequest');
|
||||
query.leftJoinAndSelect('moneyRequest.requester', 'requester');
|
||||
query.leftJoinAndSelect('requester.customer', 'customer');
|
||||
query.leftJoinAndSelect('customer.user', 'user');
|
||||
query.leftJoinAndSelect('user.profilePicture', 'profilePicture');
|
||||
query.orderBy('moneyRequest.createdAt', 'DESC');
|
||||
query.where('moneyRequest.reviewerId = :reviewerId', { reviewerId });
|
||||
query.andWhere('moneyRequest.status = :status', { status: filters.status });
|
||||
query.skip((filters.page - ONE) * filters.size);
|
||||
query.take(filters.size);
|
||||
|
||||
return query.getManyAndCount();
|
||||
}
|
||||
|
||||
updateMoneyRequestStatus(moneyRequestId: string, reviewerId: string, status: MoneyRequestStatus) {
|
||||
return this.moneyRequestRepository.update({ id: moneyRequestId, reviewerId }, { status, reviewedAt: new Date() });
|
||||
}
|
||||
|
||||
async *findMoneyRequestsChunks(chunkSize: number) {
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const moneyRequests = await this.moneyRequestRepository.find({
|
||||
take: chunkSize,
|
||||
skip: offset,
|
||||
where: [
|
||||
{
|
||||
status: MoneyRequestStatus.APPROVED,
|
||||
frequency: Not(MoneyRequestFrequency.ONE_TIME),
|
||||
endDate: MoreThan(new Date()),
|
||||
},
|
||||
{
|
||||
status: MoneyRequestStatus.APPROVED,
|
||||
frequency: Not(MoneyRequestFrequency.ONE_TIME),
|
||||
endDate: IsNull(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!moneyRequests.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield moneyRequests;
|
||||
offset += chunkSize;
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './money-requests.service';
|
@ -1,137 +0,0 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { OciService } from '~/document/services';
|
||||
import { JuniorService } from '~/junior/services';
|
||||
import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequest } from '../entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '../enums';
|
||||
import { MoneyRequestsRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class MoneyRequestsService {
|
||||
private readonly logger = new Logger(MoneyRequestsService.name);
|
||||
constructor(
|
||||
private readonly moneyRequestsRepository: MoneyRequestsRepository,
|
||||
private readonly juniorService: JuniorService,
|
||||
private readonly ociService: OciService,
|
||||
) {}
|
||||
|
||||
async createMoneyRequest(userId: string, body: CreateMoneyRequestRequestDto) {
|
||||
this.logger.log(`Creating money request for junior ${userId}`);
|
||||
if (body.frequency === MoneyRequestFrequency.ONE_TIME) {
|
||||
this.logger.log(`Setting end date to null for one time money request`);
|
||||
delete body.endDate;
|
||||
delete body.startDate;
|
||||
}
|
||||
|
||||
if (body.startDate && new Date(body.startDate) < new Date()) {
|
||||
this.logger.error(`Start date ${body.startDate} is in the past`);
|
||||
throw new BadRequestException('MONEY_REQUEST.START_DATE_IN_THE_PAST');
|
||||
}
|
||||
|
||||
if (body.endDate && new Date(body.endDate) < new Date()) {
|
||||
this.logger.error(`End date ${body.endDate} is in the past`);
|
||||
throw new BadRequestException('MONEY_REQUEST.END_DATE_IN_THE_PAST');
|
||||
}
|
||||
|
||||
if (body.endDate && body.startDate && new Date(body.endDate) < new Date(body.startDate)) {
|
||||
this.logger.error(`End date ${body.endDate} is before start date ${body.startDate}`);
|
||||
throw new BadRequestException('MONEY_REQUEST.END_DATE_BEFORE_START_DATE');
|
||||
}
|
||||
|
||||
const junior = await this.juniorService.findJuniorById(userId, true);
|
||||
|
||||
const moneyRequest = await this.moneyRequestsRepository.createMoneyRequest(junior.id, junior.guardianId, body);
|
||||
this.logger.log(`Money request ${moneyRequest.id} created successfully`);
|
||||
return this.findMoneyRequestById(moneyRequest.id);
|
||||
}
|
||||
|
||||
async findMoneyRequestById(moneyRequestId: string, reviewerId?: string) {
|
||||
this.logger.log(`Finding money request ${moneyRequestId} ${reviewerId ? `by reviewer ${reviewerId}` : ''}`);
|
||||
const moneyRequest = await this.moneyRequestsRepository.findMoneyRequestById(moneyRequestId, reviewerId);
|
||||
|
||||
if (!moneyRequest) {
|
||||
this.logger.error(`Money request ${moneyRequestId} not found ${reviewerId ? `for reviewer ${reviewerId}` : ''}`);
|
||||
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||
}
|
||||
|
||||
await this.prepareMoneyRequestDocument([moneyRequest]);
|
||||
|
||||
this.logger.log(`Money request ${moneyRequestId} found successfully`);
|
||||
return moneyRequest;
|
||||
}
|
||||
|
||||
async findMoneyRequests(
|
||||
ReviewerId: string,
|
||||
filters: MoneyRequestsFiltersRequestDto,
|
||||
): Promise<[MoneyRequest[], number]> {
|
||||
this.logger.log(`Finding money requests for reviewer ${ReviewerId}`);
|
||||
const [moneyRequests, itemCount] = await this.moneyRequestsRepository.findMoneyRequests(ReviewerId, filters);
|
||||
await this.prepareMoneyRequestDocument(moneyRequests);
|
||||
|
||||
this.logger.log(`Returning money requests for reviewer ${ReviewerId}`);
|
||||
return [moneyRequests, itemCount];
|
||||
}
|
||||
|
||||
async approveMoneyRequest(moneyRequestId: string, reviewerId: string) {
|
||||
this.logger.log(`Approving money request ${moneyRequestId} by reviewer ${reviewerId}`);
|
||||
await this.validateMoneyRequestForReview(moneyRequestId, reviewerId);
|
||||
await this.moneyRequestsRepository.updateMoneyRequestStatus(
|
||||
moneyRequestId,
|
||||
reviewerId,
|
||||
MoneyRequestStatus.APPROVED,
|
||||
);
|
||||
|
||||
//@TODO send notification and update junior balance
|
||||
}
|
||||
|
||||
async rejectMoneyRequest(moneyRequestId: string, reviewerId: string) {
|
||||
this.logger.log(`Rejecting money request ${moneyRequestId} by reviewer ${reviewerId}`);
|
||||
await this.validateMoneyRequestForReview(moneyRequestId, reviewerId);
|
||||
|
||||
await this.moneyRequestsRepository.updateMoneyRequestStatus(
|
||||
moneyRequestId,
|
||||
reviewerId,
|
||||
MoneyRequestStatus.REJECTED,
|
||||
);
|
||||
|
||||
//@TODO send notification
|
||||
}
|
||||
|
||||
findMoneyRequestsChunks(chunkSize: number) {
|
||||
this.logger.log(`Finding money requests chunks`);
|
||||
return this.moneyRequestsRepository.findMoneyRequestsChunks(chunkSize);
|
||||
}
|
||||
|
||||
private async prepareMoneyRequestDocument(moneyRequests: MoneyRequest[]) {
|
||||
this.logger.log(`Preparing document for money requests`);
|
||||
await Promise.all(
|
||||
moneyRequests.map(async (moneyRequest) => {
|
||||
const profilePicture = moneyRequest.requester.customer.user.profilePicture;
|
||||
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async validateMoneyRequestForReview(moneyRequestId: string, reviewerId: string) {
|
||||
this.logger.log(`Validating money request ${moneyRequestId} for reviewer ${reviewerId}`);
|
||||
const moneyRequest = await this.moneyRequestsRepository.findMoneyRequestById(moneyRequestId, reviewerId);
|
||||
|
||||
if (!moneyRequest) {
|
||||
this.logger.error(`Money request ${moneyRequestId} not found`);
|
||||
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (moneyRequest.endDate && new Date(moneyRequest.endDate) < new Date()) {
|
||||
this.logger.error(`Money request ${moneyRequestId} has already ended`);
|
||||
throw new BadRequestException('MONEY_REQUEST.ENDED');
|
||||
}
|
||||
|
||||
if (moneyRequest.status !== MoneyRequestStatus.PENDING) {
|
||||
this.logger.error(`Money request ${moneyRequestId} already reviewed`);
|
||||
throw new BadRequestException('MONEY_REQUEST.ALREADY_REVIEWED');
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './saving-goals.controller';
|
@ -1,96 +0,0 @@
|
||||
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 { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { createCategoryRequestDto, CreateGoalRequestDto, FundGoalRequestDto } from '../dtos/request';
|
||||
import {
|
||||
CategoriesListResponseDto,
|
||||
CategoryResponseDto,
|
||||
GoalsStatsResponseDto,
|
||||
SavingGoalDetailsResponseDto,
|
||||
} from '../dtos/response';
|
||||
import { SavingGoalsService } from '../services';
|
||||
import { CategoryService } from '../services/category.service';
|
||||
|
||||
@Controller('saving-goals')
|
||||
@ApiTags('Saving Goals')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@ApiBearerAuth()
|
||||
@ApiLangRequestHeader()
|
||||
export class SavingGoalsController {
|
||||
constructor(
|
||||
private readonly savingGoalsService: SavingGoalsService,
|
||||
private readonly categoryService: CategoryService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiDataResponse(SavingGoalDetailsResponseDto)
|
||||
async createGoal(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateGoalRequestDto) {
|
||||
const goal = await this.savingGoalsService.createGoal(sub, body);
|
||||
|
||||
return ResponseFactory.data(new SavingGoalDetailsResponseDto(goal));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiDataPageResponse(SavingGoalDetailsResponseDto)
|
||||
async getGoals(@AuthenticatedUser() { sub }: IJwtPayload, @Query() pageOptions: PageOptionsRequestDto) {
|
||||
const [goals, itemCount] = await this.savingGoalsService.findGoals(sub, pageOptions);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
goals.map((goal) => new SavingGoalDetailsResponseDto(goal)),
|
||||
{
|
||||
page: pageOptions.page,
|
||||
size: pageOptions.size,
|
||||
itemCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiDataResponse(GoalsStatsResponseDto)
|
||||
async getStats(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
const stats = await this.savingGoalsService.getStats(sub);
|
||||
|
||||
return ResponseFactory.data(new GoalsStatsResponseDto(stats));
|
||||
}
|
||||
|
||||
@Get('categories')
|
||||
@ApiDataResponse(CategoriesListResponseDto)
|
||||
async getCategories(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
const categories = await this.categoryService.findCategories(sub);
|
||||
|
||||
return ResponseFactory.data(new CategoriesListResponseDto(categories));
|
||||
}
|
||||
|
||||
@Get(':goalId')
|
||||
@ApiDataResponse(SavingGoalDetailsResponseDto)
|
||||
async getGoal(@AuthenticatedUser() { sub }: IJwtPayload, @Param('goalId', CustomParseUUIDPipe) goalId: string) {
|
||||
const goal = await this.savingGoalsService.findGoalById(sub, goalId);
|
||||
|
||||
return ResponseFactory.data(new SavingGoalDetailsResponseDto(goal));
|
||||
}
|
||||
|
||||
@Post('categories')
|
||||
@ApiDataResponse(CategoryResponseDto)
|
||||
async createCategory(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: createCategoryRequestDto) {
|
||||
const category = await this.categoryService.createCustomCategory(sub, body);
|
||||
return ResponseFactory.data(new CategoryResponseDto(category));
|
||||
}
|
||||
|
||||
@Post(':goalId/fund')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async fundGoal(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('goalId', CustomParseUUIDPipe) goalId: string,
|
||||
@Body() body: FundGoalRequestDto,
|
||||
) {
|
||||
await this.savingGoalsService.fundGoal(sub, goalId, body);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class createCategoryRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'category.name' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'category.name' }) })
|
||||
name!: string;
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class CreateGoalRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'goal.name' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'goal.name' }) })
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'goal.description' }) })
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '2021-12-31' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'goal.dueDate' }) })
|
||||
dueDate!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'goal.targetAmount' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'goal.targetAmount' }) })
|
||||
targetAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsArray({ message: i18n('validation.IsArray', { path: 'general', property: 'goal.categoryIds' }) })
|
||||
@IsUUID('4', { each: true, message: i18n('validation.IsUUID', { path: 'general', property: 'goal.categoryIds' }) })
|
||||
@Transform(({ value }) => (typeof value === 'string' ? [value] : value))
|
||||
categoryIds!: string[];
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'goal.imageId' }) })
|
||||
@IsOptional()
|
||||
imageId?: string;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, Min } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
const MIN_FUND = 1;
|
||||
export class FundGoalRequestDto {
|
||||
@ApiProperty({ example: '200' })
|
||||
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'goal.fundAmount' }) })
|
||||
@Min(MIN_FUND)
|
||||
fundAmount!: number;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './create-category.request.dto';
|
||||
export * from './create-goal.request.dto';
|
||||
export * from './fund-goal.request.dto';
|
@ -1,16 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Category } from '~/saving-goals/entities';
|
||||
import { CategoryResponseDto } from './category-response.dto';
|
||||
|
||||
export class CategoriesListResponseDto {
|
||||
@ApiProperty({ type: CategoryResponseDto, isArray: true })
|
||||
globalCategories!: CategoryResponseDto[];
|
||||
|
||||
@ApiProperty({ type: CategoryResponseDto, isArray: true })
|
||||
customCategories!: CategoryResponseDto[];
|
||||
|
||||
constructor(data: { globalCategories: Category[]; customCategories: Category[] }) {
|
||||
this.globalCategories = data.globalCategories.map((category) => new CategoryResponseDto(category));
|
||||
this.customCategories = data.customCategories.map((category) => new CategoryResponseDto(category));
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CategoryType } from '~/saving-goals/enums';
|
||||
|
||||
export class CategoryResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: CategoryType.CUSTOM })
|
||||
type!: CategoryType;
|
||||
|
||||
constructor(data: CategoryResponseDto) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IGoalStats } from '~/saving-goals/interfaces';
|
||||
const ZERO = 0;
|
||||
export class GoalsStatsResponseDto {
|
||||
@ApiProperty()
|
||||
totalTarget: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalSaved: number;
|
||||
|
||||
constructor(stats: IGoalStats) {
|
||||
this.totalTarget = stats.totalTarget || ZERO;
|
||||
this.totalSaved = stats.totalSaved || ZERO;
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export * from './categories-list.response.dto';
|
||||
export * from './category-response.dto';
|
||||
export * from './goals-stats.response.dto';
|
||||
export * from './saving-goal-details.response.dto';
|
@ -1,49 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { SavingGoal } from '~/saving-goals/entities';
|
||||
import { CategoryResponseDto } from './category-response.dto';
|
||||
|
||||
export class SavingGoalDetailsResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
dueDate!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
targetAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
currentAmount!: number;
|
||||
|
||||
@ApiPropertyOptional({ type: CategoryResponseDto, isArray: true })
|
||||
categories?: CategoryResponseDto[];
|
||||
|
||||
@ApiPropertyOptional({ type: DocumentMetaResponseDto, isArray: true })
|
||||
image: DocumentMetaResponseDto | null;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(data: SavingGoal) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.dueDate = data.dueDate;
|
||||
this.targetAmount = data.targetAmount;
|
||||
this.currentAmount = data.currentAmount;
|
||||
this.categories = data.categories ? data.categories.map((category) => new CategoryResponseDto(category)) : [];
|
||||
this.image = data.image ? new DocumentMetaResponseDto(data.image) : null;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { CategoryType } from '../enums';
|
||||
import { SavingGoal } from './saving-goal.entity';
|
||||
|
||||
@Entity('categories')
|
||||
export class Category {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'type' })
|
||||
type!: CategoryType;
|
||||
|
||||
@Column({ type: 'uuid', name: 'junior_id', nullable: true })
|
||||
juniorId!: string;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.categories, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
|
||||
@ManyToMany(() => SavingGoal, (savingGoal) => savingGoal.categories)
|
||||
goals!: SavingGoal[];
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './category.entity';
|
||||
export * from './saving-goal.entity';
|
@ -1,76 +0,0 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { Category } from './category.entity';
|
||||
const ZERO = 0;
|
||||
@Entity('saving_goals')
|
||||
export class SavingGoal extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'description', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'due_date' })
|
||||
dueDate!: Date;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
name: 'target_amount',
|
||||
precision: 12,
|
||||
scale: 3,
|
||||
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||
})
|
||||
targetAmount!: number;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
name: 'current_amount',
|
||||
precision: 12,
|
||||
scale: 3,
|
||||
default: ZERO,
|
||||
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||
})
|
||||
currentAmount!: number;
|
||||
|
||||
@Column({ type: 'uuid', name: 'image_id', nullable: true })
|
||||
imageId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'junior_id' })
|
||||
juniorId!: string;
|
||||
|
||||
@ManyToOne(() => Document, (document) => document.goals, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'image_id' })
|
||||
image!: Document;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.goals, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
|
||||
@ManyToMany(() => Category, (category) => category.goals)
|
||||
@JoinTable({
|
||||
name: 'saving_goals_categories',
|
||||
joinColumn: { name: 'saving_goal_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'category_id', referencedColumnName: 'id' },
|
||||
})
|
||||
categories!: Category[];
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export class CategoryType {
|
||||
static readonly GLOBAL = 'GLOBAL';
|
||||
static readonly CUSTOM = 'CUSTOM';
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user