feat: add money request cronjob, and edit money request entity

This commit is contained in:
Abdalhamid Alhamad
2025-01-02 11:22:33 +03:00
parent 557ef4cd33
commit aefa866ae7
12 changed files with 187 additions and 9 deletions
@@ -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;
}
}
}
+1 -1
View File
@@ -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');