mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 21:59:40 +00:00
feat: add money request cronjob, and edit money request entity
This commit is contained in:
@ -84,7 +84,13 @@ export class Allowance {
|
||||
const calculateNextDate = (unit: moment.unitOfTime.Diff) => {
|
||||
const diff = now.diff(startDate, unit);
|
||||
const nextDate = startDate.clone().add(diff, unit);
|
||||
return nextDate.isSameOrAfter(now) ? nextDate.toDate() : nextDate.add('1', unit).toDate();
|
||||
const adjustedDate = nextDate.isSameOrAfter(now) ? nextDate : nextDate.add('1', unit);
|
||||
|
||||
if (endDate && adjustedDate.isAfter(endDate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return adjustedDate.toDate();
|
||||
};
|
||||
|
||||
switch (this.frequency) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
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 } from './tasks';
|
||||
import { AllowanceTask, MoneyRequestTask } from './tasks';
|
||||
|
||||
@Module({
|
||||
imports: [AllowanceModule],
|
||||
providers: [AllowanceTask, BaseCronService],
|
||||
imports: [AllowanceModule, MoneyRequestModule],
|
||||
providers: [AllowanceTask, MoneyRequestTask, BaseCronService],
|
||||
})
|
||||
export class CronModule {}
|
||||
|
@ -15,7 +15,7 @@ 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 allowanceService: AllowancesService) {
|
||||
constructor(cacheService: CacheService, private readonly allowanceService: AllowancesService) {
|
||||
super(cacheService);
|
||||
}
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './allowance.task';
|
||||
export * from './money-request.task';
|
||||
|
54
src/cron/tasks/money-request.task.ts
Normal file
54
src/cron/tasks/money-request.task.ts
Normal file
@ -0,0 +1,54 @@
|
||||
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,6 +12,7 @@ export class CreateMoneyRequestEntity1734503895302 implements MigrationInterface
|
||||
"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,
|
||||
|
@ -1,5 +1,15 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
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;
|
||||
@ -30,6 +40,14 @@ export class CreateMoneyRequestRequestDto {
|
||||
@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()
|
||||
|
@ -22,6 +22,9 @@ export class MoneyRequestResponseDto {
|
||||
@ApiProperty({ example: MoneyRequestFrequency.ONE_TIME })
|
||||
frequency!: MoneyRequestFrequency;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
startDate!: Date | null;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
endDate!: Date | null;
|
||||
|
||||
@ -41,6 +44,7 @@ export class MoneyRequestResponseDto {
|
||||
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;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@ -31,6 +32,9 @@ export class MoneyRequest {
|
||||
@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;
|
||||
|
||||
@ -53,4 +57,41 @@ export class MoneyRequest {
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { MoneyRequestsService } from './services';
|
||||
@Module({
|
||||
controllers: [MoneyRequestsController],
|
||||
providers: [MoneyRequestsService, MoneyRequestsRepository],
|
||||
exports: [],
|
||||
exports: [MoneyRequestsService],
|
||||
imports: [TypeOrmModule.forFeature([MoneyRequest]), JuniorModule],
|
||||
})
|
||||
export class MoneyRequestModule {}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { IsNull, MoreThan, Not, Repository } from 'typeorm';
|
||||
import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequest } from '../entities';
|
||||
import { MoneyRequestStatus } from '../enums';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class MoneyRequestsRepository {
|
||||
@ -16,6 +16,7 @@ export class MoneyRequestsRepository {
|
||||
reviewerId,
|
||||
requestedAmount: body.requestedAmount,
|
||||
message: body.message,
|
||||
startDate: body.startDate,
|
||||
endDate: body.endDate,
|
||||
frequency: body.frequency,
|
||||
}),
|
||||
@ -46,4 +47,33 @@ export class MoneyRequestsRepository {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,12 @@ export class MoneyRequestsService {
|
||||
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()) {
|
||||
@ -27,6 +33,11 @@ export class MoneyRequestsService {
|
||||
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);
|
||||
@ -86,6 +97,11 @@ export class MoneyRequestsService {
|
||||
//@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(
|
||||
@ -107,6 +123,12 @@ export class MoneyRequestsService {
|
||||
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');
|
||||
|
Reference in New Issue
Block a user