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:
53
package-lock.json
generated
53
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"@nestjs/microservices": "^10.4.7",
|
"@nestjs/microservices": "^10.4.7",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.4.8",
|
"@nestjs/platform-express": "^10.4.8",
|
||||||
|
"@nestjs/schedule": "^4.1.2",
|
||||||
"@nestjs/swagger": "^8.0.5",
|
"@nestjs/swagger": "^8.0.5",
|
||||||
"@nestjs/terminus": "^10.2.3",
|
"@nestjs/terminus": "^10.2.3",
|
||||||
"@nestjs/throttler": "^6.2.1",
|
"@nestjs/throttler": "^6.2.1",
|
||||||
@ -2198,6 +2199,33 @@
|
|||||||
"@nestjs/core": "^10.0.0"
|
"@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": {
|
"node_modules/@nestjs/schematics": {
|
||||||
"version": "10.2.3",
|
"version": "10.2.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -2842,6 +2870,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/@types/methods": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -5005,6 +5039,16 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -9238,6 +9282,15 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.8",
|
"version": "0.30.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
"@nestjs/microservices": "^10.4.7",
|
"@nestjs/microservices": "^10.4.7",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.4.8",
|
"@nestjs/platform-express": "^10.4.8",
|
||||||
|
"@nestjs/schedule": "^4.1.2",
|
||||||
"@nestjs/swagger": "^8.0.5",
|
"@nestjs/swagger": "^8.0.5",
|
||||||
"@nestjs/terminus": "^10.2.3",
|
"@nestjs/terminus": "^10.2.3",
|
||||||
"@nestjs/throttler": "^6.2.1",
|
"@nestjs/throttler": "^6.2.1",
|
||||||
|
@ -15,6 +15,6 @@ import { AllowanceChangeRequestsService, AllowancesService } from './services';
|
|||||||
AllowanceChangeRequestsService,
|
AllowanceChangeRequestsService,
|
||||||
AllowanceChangeRequestsRepository,
|
AllowanceChangeRequestsRepository,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [AllowancesService],
|
||||||
})
|
})
|
||||||
export class AllowanceModule {}
|
export class AllowanceModule {}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@ -13,7 +14,6 @@ import { Guardian } from '~/guardian/entities/guradian.entity';
|
|||||||
import { Junior } from '~/junior/entities';
|
import { Junior } from '~/junior/entities';
|
||||||
import { AllowanceFrequency, AllowanceType } from '../enums';
|
import { AllowanceFrequency, AllowanceType } from '../enums';
|
||||||
import { AllowanceChangeRequest } from './allowance-change-request.entity';
|
import { AllowanceChangeRequest } from './allowance-change-request.entity';
|
||||||
|
|
||||||
@Entity('allowances')
|
@Entity('allowances')
|
||||||
export class Allowance {
|
export class Allowance {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -71,4 +71,31 @@ export class Allowance {
|
|||||||
|
|
||||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true })
|
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true })
|
||||||
deletedAt?: Date;
|
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) {
|
deleteAllowance(guardianId: string, allowanceId: string) {
|
||||||
return this.allowancesRepository.softDelete({ id: allowanceId, guardianId });
|
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;
|
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[]) {
|
private async prepareAllowanceDocuments(allowance: Allowance[]) {
|
||||||
this.logger.log(`Preparing document for allowances`);
|
this.logger.log(`Preparing document for allowances`);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -2,6 +2,7 @@ import { MiddlewareConsumer, Module } from '@nestjs/common';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
||||||
import { LoggerModule } from 'nestjs-pino';
|
import { LoggerModule } from 'nestjs-pino';
|
||||||
@ -17,6 +18,7 @@ import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/
|
|||||||
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
||||||
import { buildI18nOptions } from './core/module-options/i18n-options';
|
import { buildI18nOptions } from './core/module-options/i18n-options';
|
||||||
import { buildValidationPipe } from './core/pipes';
|
import { buildValidationPipe } from './core/pipes';
|
||||||
|
import { CronModule } from './cron/cron.module';
|
||||||
import { CustomerModule } from './customer/customer.module';
|
import { CustomerModule } from './customer/customer.module';
|
||||||
import { migrations } from './db';
|
import { migrations } from './db';
|
||||||
import { DocumentModule } from './document/document.module';
|
import { DocumentModule } from './document/document.module';
|
||||||
@ -56,6 +58,7 @@ import { UserModule } from './user/user.module';
|
|||||||
I18nModule.forRoot(buildI18nOptions()),
|
I18nModule.forRoot(buildI18nOptions()),
|
||||||
CacheModule,
|
CacheModule,
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
// App modules
|
// App modules
|
||||||
AuthModule,
|
AuthModule,
|
||||||
CustomerModule,
|
CustomerModule,
|
||||||
@ -75,6 +78,8 @@ import { UserModule } from './user/user.module';
|
|||||||
HealthModule,
|
HealthModule,
|
||||||
|
|
||||||
UserModule,
|
UserModule,
|
||||||
|
|
||||||
|
CronModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Global Pipes
|
// 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