mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-07-10 15:17:44 +00:00
feat: add cron job for allowance and implement cron lock
This commit is contained in:
@ -29,7 +29,7 @@ module.exports = {
|
||||
'require-await': ['error'],
|
||||
'no-console': ['error'],
|
||||
'no-multi-assign': ['error'],
|
||||
'no-magic-numbers': ['error', { ignoreArrayIndexes: true }],
|
||||
'no-magic-numbers': ['error', { ignoreArrayIndexes: true}],
|
||||
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1, maxBOF: 0 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
|
53
package-lock.json
generated
53
package-lock.json
generated
@ -22,6 +22,7 @@
|
||||
"@nestjs/microservices": "^10.4.7",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.4.8",
|
||||
"@nestjs/schedule": "^4.1.2",
|
||||
"@nestjs/swagger": "^8.0.5",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
@ -2198,6 +2199,33 @@
|
||||
"@nestjs/core": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz",
|
||||
"integrity": "sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron": "3.2.1",
|
||||
"uuid": "11.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule/node_modules/uuid": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
||||
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "10.2.3",
|
||||
"dev": true,
|
||||
@ -2842,6 +2870,12 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
@ -5005,6 +5039,16 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz",
|
||||
"integrity": "sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/luxon": "~3.4.0",
|
||||
"luxon": "~3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.5",
|
||||
"license": "MIT",
|
||||
@ -9238,6 +9282,15 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.8",
|
||||
"dev": true,
|
||||
|
@ -39,6 +39,7 @@
|
||||
"@nestjs/microservices": "^10.4.7",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.4.8",
|
||||
"@nestjs/schedule": "^4.1.2",
|
||||
"@nestjs/swagger": "^8.0.5",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
|
@ -15,6 +15,6 @@ import { AllowanceChangeRequestsService, AllowancesService } from './services';
|
||||
AllowanceChangeRequestsService,
|
||||
AllowanceChangeRequestsRepository,
|
||||
],
|
||||
exports: [],
|
||||
exports: [AllowancesService],
|
||||
})
|
||||
export class AllowanceModule {}
|
||||
|
@ -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(
|
||||
|
@ -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
src/cron/cron.module.ts
Normal file
10
src/cron/cron.module.ts
Normal 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
src/cron/services/base-cron.service.ts
Normal file
23
src/cron/services/base-cron.service.ts
Normal 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
src/cron/services/index.ts
Normal file
1
src/cron/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './base-cron.service';
|
54
src/cron/tasks/allowance.task.ts
Normal file
54
src/cron/tasks/allowance.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 { 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
src/cron/tasks/index.ts
Normal file
1
src/cron/tasks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './allowance.task';
|
Reference in New Issue
Block a user