feat: add cron job for allowance and implement cron lock

This commit is contained in:
Abdalhamid Alhamad
2024-12-30 16:10:19 +03:00
parent 0fd2066c4a
commit c0fafd3f7c
13 changed files with 202 additions and 3 deletions
+1 -1
View File
@@ -15,6 +15,6 @@ import { AllowanceChangeRequestsService, AllowancesService } from './services';
AllowanceChangeRequestsService,
AllowanceChangeRequestsRepository,
],
exports: [],
exports: [AllowancesService],
})
export class AllowanceModule {}
+28 -1
View File
@@ -1,3 +1,4 @@
import moment from 'moment';
import {
Column,
CreateDateColumn,
@@ -13,7 +14,6 @@ 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')
@@ -71,4 +71,31 @@ export class Allowance {
@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);
return nextDate.isSameOrAfter(now) ? nextDate.toDate() : nextDate.add('1', unit).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;
}
}
}
@@ -44,4 +44,21 @@ export class AllowancesRepository {
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;
}
}
}
@@ -89,6 +89,13 @@ export class AllowancesService {
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(
+5
View File
@@ -2,6 +2,7 @@ import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
import { LoggerModule } from 'nestjs-pino';
@@ -17,6 +18,7 @@ import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
import { buildI18nOptions } from './core/module-options/i18n-options';
import { buildValidationPipe } from './core/pipes';
import { CronModule } from './cron/cron.module';
import { CustomerModule } from './customer/customer.module';
import { migrations } from './db';
import { DocumentModule } from './document/document.module';
@@ -56,6 +58,7 @@ import { UserModule } from './user/user.module';
I18nModule.forRoot(buildI18nOptions()),
CacheModule,
EventEmitterModule.forRoot(),
ScheduleModule.forRoot(),
// App modules
AuthModule,
CustomerModule,
@@ -75,6 +78,8 @@ import { UserModule } from './user/user.module';
HealthModule,
UserModule,
CronModule,
],
providers: [
// Global Pipes
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AllowanceModule } from '~/allowance/allowance.module';
import { BaseCronService } from './services';
import { AllowanceTask } from './tasks';
@Module({
imports: [AllowanceModule],
providers: [AllowanceTask, BaseCronService],
})
export class CronModule {}
+23
View File
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common';
import { CacheService } from '~/common/modules/cache/services';
@Injectable()
export class BaseCronService {
constructor(private readonly cacheService: CacheService) {}
async acquireLock(key: string, ttl: number) {
const lock = await this.cacheService.get(key);
if (lock) {
return false;
}
await this.cacheService.set(key, true, ttl);
return true;
}
async releaseLock(key: string) {
await this.cacheService.delete(key);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './base-cron.service';
+54
View File
@@ -0,0 +1,54 @@
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 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
View File
@@ -0,0 +1 @@
export * from './allowance.task';