mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 08:34:55 +00:00
feat: money requests
This commit is contained in:
@ -28,6 +28,7 @@ import { HealthModule } from './health/health.module';
|
|||||||
import { JuniorModule } from './junior/junior.module';
|
import { JuniorModule } from './junior/junior.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import { WebhookModule } from './webhook/webhook.module';
|
import { WebhookModule } from './webhook/webhook.module';
|
||||||
|
import { MoneyRequestModule } from './money-request/money-request.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [],
|
controllers: [],
|
||||||
@ -74,6 +75,7 @@ import { WebhookModule } from './webhook/webhook.module';
|
|||||||
CronModule,
|
CronModule,
|
||||||
NeoLeapModule,
|
NeoLeapModule,
|
||||||
WebhookModule,
|
WebhookModule,
|
||||||
|
MoneyRequestModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Global Pipes
|
// Global Pipes
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateMoneyRequestsTable1757349525708 implements MigrationInterface {
|
||||||
|
name = 'CreateMoneyRequestsTable1757349525708';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "money_requests" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "amount" numeric(10,2) NOT NULL, "reason" character varying NOT NULL, "status" character varying NOT NULL DEFAULT 'PENDING', "rejection_reason" text, "junior_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 "PK_28cff23e9fb06cd5dbf73cd53e7" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" ALTER COLUMN "color" SET DEFAULT 'DEEP_MAGENTA'`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_f7084c83efe7efaca37297d57ae" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_09eadf4c4133b323f467ffc90f3" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_09eadf4c4133b323f467ffc90f3"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_f7084c83efe7efaca37297d57ae"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" ALTER COLUMN "color" SET DEFAULT 'BLUE'`);
|
||||||
|
await queryRunner.query(`DROP TABLE "money_requests"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './1754913378460-initial-migration';
|
export * from './1754913378460-initial-migration';
|
||||||
export * from './1754915164809-create-neoleap-related-entities';
|
export * from './1754915164809-create-neoleap-related-entities';
|
||||||
export * from './1754915164810-seed-default-avatar';
|
export * from './1754915164810-seed-default-avatar';
|
||||||
|
export * from './1757349525708-create-money-requests-table';
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Junior } from '~/junior/entities';
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { MoneyRequest } from '~/money-request/entities/money-request.entity';
|
||||||
|
|
||||||
@Entity('guardians')
|
@Entity('guardians')
|
||||||
export class Guardian extends BaseEntity {
|
export class Guardian extends BaseEntity {
|
||||||
@ -27,6 +28,9 @@ export class Guardian extends BaseEntity {
|
|||||||
@OneToMany(() => Junior, (junior) => junior.guardian)
|
@OneToMany(() => Junior, (junior) => junior.guardian)
|
||||||
juniors!: Junior[];
|
juniors!: Junior[];
|
||||||
|
|
||||||
|
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.guardian)
|
||||||
|
moneyRequests!: MoneyRequest[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
|||||||
@ -72,12 +72,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"MONEY_REQUEST": {
|
"MONEY_REQUEST": {
|
||||||
"START_DATE_IN_THE_PAST": "لا يمكن أن يكون تاريخ البدء في الماضي.",
|
|
||||||
"END_DATE_IN_THE_PAST": "لا يمكن أن يكون تاريخ النهاية في الماضي.",
|
|
||||||
"END_DATE_BEFORE_START_DATE": "لا يمكن أن يكون تاريخ النهاية قبل تاريخ البدء.",
|
|
||||||
"NOT_FOUND": "لم يتم العثور على طلب المال.",
|
"NOT_FOUND": "لم يتم العثور على طلب المال.",
|
||||||
"ENDED": "تم انتهاء طلب المال.",
|
"ALREADY_APPROVED": "تمت الموافقة على طلب المال بالفعل.",
|
||||||
"ALREADY_REVIEWED": "تمت مراجعة طلب المال بالفعل."
|
"ALREADY_REJECTED": "تم رفض طلب المال بالفعل."
|
||||||
},
|
},
|
||||||
|
|
||||||
"GOAL": {
|
"GOAL": {
|
||||||
|
|||||||
@ -71,12 +71,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"MONEY_REQUEST": {
|
"MONEY_REQUEST": {
|
||||||
"START_DATE_IN_THE_PAST": "The start date cannot be in the past.",
|
|
||||||
"END_DATE_IN_THE_PAST": "The end date cannot be in the past.",
|
|
||||||
"END_DATE_BEFORE_START_DATE": "The end date cannot be before the start date.",
|
|
||||||
"NOT_FOUND": "The money request was not found.",
|
"NOT_FOUND": "The money request was not found.",
|
||||||
"ENDED": "The money request has ended.",
|
"ALREADY_APPROVED": "The money request has already been approved.",
|
||||||
"ALREADY_REVIEWED": "The money request has already been reviewed."
|
"ALREADY_REJECTED": "The money request has already been rejected."
|
||||||
},
|
},
|
||||||
|
|
||||||
"GOAL": {
|
"GOAL": {
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { MoneyRequest } from '~/money-request/entities/money-request.entity';
|
||||||
import { Relationship } from '../enums';
|
import { Relationship } from '../enums';
|
||||||
import { Theme } from './theme.entity';
|
import { Theme } from './theme.entity';
|
||||||
|
|
||||||
@ -39,6 +41,9 @@ export class Junior extends BaseEntity {
|
|||||||
@JoinColumn({ name: 'guardian_id' })
|
@JoinColumn({ name: 'guardian_id' })
|
||||||
guardian!: Guardian;
|
guardian!: Guardian;
|
||||||
|
|
||||||
|
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.junior)
|
||||||
|
moneyRequests!: MoneyRequest[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
|||||||
1
src/money-request/controllers/index.ts
Normal file
1
src/money-request/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './money-requests.controller';
|
||||||
81
src/money-request/controllers/money-requests.controller.ts
Normal file
81
src/money-request/controllers/money-requests.controller.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||||
|
import { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||||
|
import { ApiDataResponse } from '~/core/decorators';
|
||||||
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { CreateMoneyRequestDto, MoneyRequestsFiltersRequestDto, RejectionMoneyRequestDto } from '../dtos/request';
|
||||||
|
import { MoneyRequestResponseDto } from '../dtos/response/money-request.response.dto';
|
||||||
|
import { MoneyRequestsService } from '../services/money-requests.service';
|
||||||
|
|
||||||
|
@Controller('money-requests')
|
||||||
|
@ApiTags('Money Requests')
|
||||||
|
@UseGuards(AccessTokenGuard, RolesGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class MoneyRequestsController {
|
||||||
|
constructor(private readonly moneyRequestsService: MoneyRequestsService) {}
|
||||||
|
@Post()
|
||||||
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@ApiDataResponse(MoneyRequestResponseDto)
|
||||||
|
async createMoneyRequest(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateMoneyRequestDto) {
|
||||||
|
const moneyRequest = await this.moneyRequestsService.createMoneyRequest(sub, body);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new MoneyRequestResponseDto(moneyRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@AllowedRoles(Roles.JUNIOR, Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(MoneyRequestResponseDto)
|
||||||
|
async getMoneyRequests(
|
||||||
|
@AuthenticatedUser() { sub, roles }: IJwtPayload,
|
||||||
|
@Query() filters: MoneyRequestsFiltersRequestDto,
|
||||||
|
) {
|
||||||
|
const [moneyRequests, count] = await this.moneyRequestsService.findMoneyRequests(
|
||||||
|
sub,
|
||||||
|
roles.includes(Roles.GUARDIAN) ? Roles.GUARDIAN : Roles.JUNIOR,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
moneyRequests.map((mr) => new MoneyRequestResponseDto(mr)),
|
||||||
|
{
|
||||||
|
page: filters.page,
|
||||||
|
size: filters.size,
|
||||||
|
itemCount: count,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@AllowedRoles(Roles.JUNIOR, Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(MoneyRequestResponseDto)
|
||||||
|
async getMoneyRequest(@Param('id') id: string, @AuthenticatedUser() { sub, roles }: IJwtPayload) {
|
||||||
|
const moneyRequest = await this.moneyRequestsService.findById(
|
||||||
|
id,
|
||||||
|
sub,
|
||||||
|
roles.includes(Roles.GUARDIAN) ? Roles.GUARDIAN : Roles.JUNIOR,
|
||||||
|
);
|
||||||
|
return ResponseFactory.data(new MoneyRequestResponseDto(moneyRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/approve')
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(MoneyRequestResponseDto)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async approveMoneyRequest(@Param('id') id: string, @AuthenticatedUser() { sub }: IJwtPayload) {
|
||||||
|
await this.moneyRequestsService.approveMoneyRequest(id, sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/reject')
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(MoneyRequestResponseDto)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async rejectMoneyRequest(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Body() rejectionReasondto: RejectionMoneyRequestDto,
|
||||||
|
) {
|
||||||
|
await this.moneyRequestsService.rejectMoneyRequest(id, sub, rejectionReasondto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNumber, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
export class CreateMoneyRequestDto {
|
||||||
|
@ApiProperty({ example: 300.42 })
|
||||||
|
@IsNumber(
|
||||||
|
{ maxDecimalPlaces: 3 },
|
||||||
|
{ message: i18n('validation.IsNumber', { path: 'general', property: 'moneyRequest.amount' }) },
|
||||||
|
)
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'For school supplies' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'moneyRequest.reason' }) })
|
||||||
|
reason!: string;
|
||||||
|
}
|
||||||
3
src/money-request/dtos/request/index.ts
Normal file
3
src/money-request/dtos/request/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './create-money-request.request.dto';
|
||||||
|
export * from './money-requests-filters.request.dto';
|
||||||
|
export * from './rejection.request.dto';
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsOptional } 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 {
|
||||||
|
@ApiPropertyOptional({ example: MoneyRequestStatus.APPROVED, enum: MoneyRequestStatus })
|
||||||
|
@IsEnum(MoneyRequestStatus, {
|
||||||
|
message: i18n('validation.enum', { property: 'moneyRequest.status', enum: MoneyRequestStatus }),
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
status?: MoneyRequestStatus;
|
||||||
|
}
|
||||||
9
src/money-request/dtos/request/rejection.request.dto.ts
Normal file
9
src/money-request/dtos/request/rejection.request.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
export class RejectionMoneyRequestDto {
|
||||||
|
@ApiProperty({ example: 'You are spending too much' })
|
||||||
|
@IsString({ message: i18n('validation.string', { property: 'rejectionReason' }) })
|
||||||
|
@IsOptional()
|
||||||
|
rejectionReason!: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||||
|
import { MoneyRequest } from '~/money-request/entities/money-request.entity';
|
||||||
|
import { MoneyRequestStatus } from '~/money-request/enums';
|
||||||
|
|
||||||
|
export class MoneyRequestResponseDto {
|
||||||
|
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 300.42 })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'For school supplies' })
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: MoneyRequestStatus, example: MoneyRequestStatus.PENDING })
|
||||||
|
status!: MoneyRequestStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ example: null })
|
||||||
|
rejectionReason!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JuniorResponseDto })
|
||||||
|
junior!: JuniorResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2024-01-01T00:00:00.000Z' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2024-01-02T00:00:00.000Z' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
constructor(data: MoneyRequest) {
|
||||||
|
this.id = data.id;
|
||||||
|
this.amount = Number(data.amount);
|
||||||
|
this.reason = data.reason;
|
||||||
|
this.status = data.status;
|
||||||
|
this.rejectionReason = data.rejectionReason;
|
||||||
|
this.junior = new JuniorResponseDto(data.junior);
|
||||||
|
this.createdAt = data.createdAt;
|
||||||
|
this.updatedAt = data.updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/money-request/entities/money-request.entity.ts
Normal file
51
src/money-request/entities/money-request.entity.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { MoneyRequestStatus } from '../enums';
|
||||||
|
|
||||||
|
@Entity('money_requests')
|
||||||
|
export class MoneyRequest extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ precision: 10, scale: 2, type: 'decimal', name: 'amount' })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'reason' })
|
||||||
|
reason!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'status', default: MoneyRequestStatus.PENDING })
|
||||||
|
status!: MoneyRequestStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'text', name: 'rejection_reason', nullable: true })
|
||||||
|
rejectionReason!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'junior_id' })
|
||||||
|
juniorId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'guardian_id' })
|
||||||
|
guardianId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Junior, (junior) => junior.moneyRequests, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'junior_id' })
|
||||||
|
junior!: Junior;
|
||||||
|
|
||||||
|
@ManyToOne(() => Guardian, (guardian) => guardian.moneyRequests, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'guardian_id' })
|
||||||
|
guardian!: Guardian;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP', name: 'updated_at' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
||||||
1
src/money-request/enums/index.ts
Normal file
1
src/money-request/enums/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './money-request-status.enum';
|
||||||
5
src/money-request/enums/money-request-status.enum.ts
Normal file
5
src/money-request/enums/money-request-status.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum MoneyRequestStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
APPROVED = 'APPROVED',
|
||||||
|
REJECTED = 'REJECTED',
|
||||||
|
}
|
||||||
14
src/money-request/money-request.module.ts
Normal file
14
src/money-request/money-request.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { JuniorModule } from '~/junior/junior.module';
|
||||||
|
import { MoneyRequestsController } from './controllers';
|
||||||
|
import { MoneyRequest } from './entities/money-request.entity';
|
||||||
|
import { MoneyRequestsRepository } from './repositories';
|
||||||
|
import { MoneyRequestsService } from './services';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([MoneyRequest]), JuniorModule],
|
||||||
|
controllers: [MoneyRequestsController],
|
||||||
|
providers: [MoneyRequestsService, MoneyRequestsRepository],
|
||||||
|
})
|
||||||
|
export class MoneyRequestModule {}
|
||||||
1
src/money-request/repositories/index.ts
Normal file
1
src/money-request/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './money-requests.repository';
|
||||||
70
src/money-request/repositories/money-requests.repository.ts
Normal file
70
src/money-request/repositories/money-requests.repository.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Roles } from '~/auth/enums/roles.enum';
|
||||||
|
import { CreateMoneyRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||||
|
import { MoneyRequest } from '../entities/money-request.entity';
|
||||||
|
import { MoneyRequestStatus } from '../enums';
|
||||||
|
const FIRST_PAGE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class MoneyRequestsRepository {
|
||||||
|
constructor(@InjectRepository(MoneyRequest) private readonly moneyRequestRepository: Repository<MoneyRequest>) {}
|
||||||
|
|
||||||
|
findAll(userId: string, role: Roles, filters: MoneyRequestsFiltersRequestDto): Promise<[MoneyRequest[], number]> {
|
||||||
|
const queryBuilder = this.moneyRequestRepository.createQueryBuilder('moneyRequest');
|
||||||
|
|
||||||
|
if (role === Roles.JUNIOR) {
|
||||||
|
queryBuilder.where('moneyRequest.juniorId = :userId', { userId });
|
||||||
|
} else if (role === Roles.GUARDIAN) {
|
||||||
|
queryBuilder.where('moneyRequest.guardianId = :userId', { userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryBuilder.leftJoinAndSelect('moneyRequest.junior', 'junior');
|
||||||
|
queryBuilder.leftJoinAndSelect('junior.customer', 'customer');
|
||||||
|
queryBuilder.leftJoinAndSelect('customer.user', 'user');
|
||||||
|
queryBuilder.leftJoinAndSelect('user.profilePicture', 'profilePicture');
|
||||||
|
|
||||||
|
if (filters.status) {
|
||||||
|
queryBuilder.andWhere('moneyRequest.status = :status', { status: filters.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryBuilder.skip((filters.page - FIRST_PAGE) * filters.size);
|
||||||
|
queryBuilder.take(filters.size);
|
||||||
|
|
||||||
|
queryBuilder.orderBy('moneyRequest.createdAt', 'DESC');
|
||||||
|
return queryBuilder.getManyAndCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMoneyRequest(juniorId: string, guardianId: string, body: CreateMoneyRequestDto): Promise<MoneyRequest> {
|
||||||
|
return this.moneyRequestRepository.save(
|
||||||
|
this.moneyRequestRepository.create({
|
||||||
|
amount: body.amount,
|
||||||
|
reason: body.reason,
|
||||||
|
status: MoneyRequestStatus.PENDING,
|
||||||
|
juniorId,
|
||||||
|
guardianId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string, userId?: string, role?: Roles): Promise<MoneyRequest | null> {
|
||||||
|
const whereCondition: any = { id };
|
||||||
|
if (role === Roles.JUNIOR) {
|
||||||
|
whereCondition.juniorId = userId;
|
||||||
|
} else {
|
||||||
|
whereCondition.guardianId = userId;
|
||||||
|
}
|
||||||
|
return this.moneyRequestRepository.findOne({
|
||||||
|
where: whereCondition,
|
||||||
|
relations: ['junior', 'junior.customer', 'junior.customer.user', 'junior.customer.user.profilePicture'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
approveMoneyRequest(id: string) {
|
||||||
|
return this.moneyRequestRepository.update({ id }, { status: MoneyRequestStatus.APPROVED });
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectMoneyRequest(id: string, rejectionReason?: string) {
|
||||||
|
return this.moneyRequestRepository.update({ id }, { status: MoneyRequestStatus.REJECTED, rejectionReason });
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/money-request/services/index.ts
Normal file
1
src/money-request/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './money-requests.service';
|
||||||
102
src/money-request/services/money-requests.service.ts
Normal file
102
src/money-request/services/money-requests.service.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Transactional } from 'typeorm-transactional';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { Junior } from '~/junior/entities/junior.entity';
|
||||||
|
import { JuniorService } from '~/junior/services';
|
||||||
|
import { CreateMoneyRequestDto, MoneyRequestsFiltersRequestDto, RejectionMoneyRequestDto } from '../dtos/request';
|
||||||
|
import { MoneyRequest } from '../entities/money-request.entity';
|
||||||
|
import { 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(juniorId: string, body: CreateMoneyRequestDto) {
|
||||||
|
const junior = await this.juniorService.findJuniorById(juniorId);
|
||||||
|
const moneyRequest = await this.moneyRequestsRepository.createMoneyRequest(junior.id, junior.guardianId, body);
|
||||||
|
return this.findById(moneyRequest.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string, userId?: string, role?: Roles): Promise<MoneyRequest> {
|
||||||
|
const moneyRequest = await this.moneyRequestsRepository.findById(id, userId, role);
|
||||||
|
if (!moneyRequest) {
|
||||||
|
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
await this.prepareJuniorImages([moneyRequest.junior]);
|
||||||
|
return moneyRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findMoneyRequests(
|
||||||
|
userId: string,
|
||||||
|
role: Roles,
|
||||||
|
filters: MoneyRequestsFiltersRequestDto,
|
||||||
|
): Promise<[MoneyRequest[], number]> {
|
||||||
|
const [moneyRequests, count] = await this.moneyRequestsRepository.findAll(userId, role, filters);
|
||||||
|
const juniors = moneyRequests.map((moneyRequest) => moneyRequest.junior);
|
||||||
|
await this.prepareJuniorImages(juniors);
|
||||||
|
return [moneyRequests, count];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async approveMoneyRequest(id: string, guardianId: string): Promise<void> {
|
||||||
|
const moneyRequest = await this.moneyRequestsRepository.findById(id, guardianId, Roles.GUARDIAN);
|
||||||
|
|
||||||
|
if (!moneyRequest) {
|
||||||
|
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moneyRequest.status == MoneyRequestStatus.APPROVED) {
|
||||||
|
throw new BadRequestException('MONEY_REQUEST.ALREADY_APPROVED');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.moneyRequestsRepository.approveMoneyRequest(id),
|
||||||
|
this.juniorService.transferToJunior(
|
||||||
|
moneyRequest.juniorId,
|
||||||
|
{ amount: moneyRequest.amount },
|
||||||
|
moneyRequest.guardianId,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectMoneyRequest(
|
||||||
|
id: string,
|
||||||
|
guardianId: string,
|
||||||
|
rejectionReasondto: RejectionMoneyRequestDto,
|
||||||
|
): Promise<void> {
|
||||||
|
const moneyRequest = await this.moneyRequestsRepository.findById(id, guardianId, Roles.GUARDIAN);
|
||||||
|
|
||||||
|
if (!moneyRequest) {
|
||||||
|
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moneyRequest.status == MoneyRequestStatus.APPROVED) {
|
||||||
|
throw new BadRequestException('MONEY_REQUEST.ALREADY_APPROVED');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moneyRequest.status == MoneyRequestStatus.REJECTED) {
|
||||||
|
throw new BadRequestException('MONEY_REQUEST.ALREADY_REJECTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.moneyRequestsRepository.rejectMoneyRequest(id, rejectionReasondto?.rejectionReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareJuniorImages(juniors: Junior[]) {
|
||||||
|
this.logger.log(`Preparing junior images`);
|
||||||
|
await Promise.all(
|
||||||
|
juniors.map(async (junior) => {
|
||||||
|
const profilePicture = junior.customer.user.profilePicture;
|
||||||
|
|
||||||
|
if (profilePicture) {
|
||||||
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user