feat(notify): setup notifications packages

This commit is contained in:
Matthieu Haineault 2025-08-07 14:04:48 -04:00
parent 13a3ccb292
commit 1a0f8f8b0a
8 changed files with 1364 additions and 771 deletions

1898
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,15 +20,18 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/bullmq": "^11.0.3",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.11.1",
"bullmq": "^5.56.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"express-session": "^1.18.2",
@ -41,7 +44,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
@ -50,6 +53,7 @@
"@types/express-session": "^1.18.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/nodemailer": "^6.4.17",
"@types/passport-jwt": "^4.0.1",
"@types/passport-openidconnect": "^0.1.3",
"@types/supertest": "^6.0.2",

View File

@ -1,45 +1,47 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { HealthModule } from './health/health.module';
import { HealthController } from './health/health.controller';
import { UsersModule } from './modules/users-management/users.module';
import { ArchivalModule } from './modules/archival/archival.module';
import { AuthenticationModule } from './modules/authentication/auth.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
import { CsvExportModule } from './modules/exports/csv-exports.module';
import { CustomersModule } from './modules/customers/customers.module';
import { EmployeesModule } from './modules/employees/employees.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ExpensesModule } from './modules/expenses/expenses.module';
import { HealthModule } from './health/health.module';
import { HealthController } from './health/health.controller';
import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module';
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
import { PrismaModule } from './prisma/prisma.module';
import { ScheduleModule } from '@nestjs/schedule';
import { ShiftsModule } from './modules/shifts/shifts.module';
import { TimesheetsModule } from './modules/timesheets/timesheets.module';
import { AuthenticationModule } from './modules/authentication/auth.module';
import { ExpensesModule } from './modules/expenses/expenses.module';
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
import { ScheduleModule } from '@nestjs/schedule';
import { ArchivalModule } from './modules/archival/archival.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
import { CsvExportModule } from './modules/exports/csv-exports.module';
import { UsersModule } from './modules/users-management/users.module';
@Module({
imports: [
ScheduleModule.forRoot(),
ArchivalModule,
AuthenticationModule,
BankCodesModule,
BusinessLogicsModule,
CsvExportModule,
CustomersModule,
EmployeesModule,
ExpensesModule,
HealthModule,
LeaveRequestsModule,
OauthSessionsModule,
PayperiodsModule,
PrismaModule,
ShiftsModule,
TimesheetsModule,
UsersModule,
ArchivalModule,
AuthenticationModule,
BankCodesModule,
BusinessLogicsModule,
CsvExportModule,
CustomersModule,
EmployeesModule,
EventEmitterModule.forRoot(),
ExpensesModule,
HealthModule,
LeaveRequestsModule,
OauthSessionsModule,
PayperiodsModule,
PrismaModule,
ScheduleModule.forRoot(),
ShiftsModule,
TimesheetsModule,
UsersModule,
],
controllers: [AppController, HealthController],
providers: [AppService, OvertimeService],

View File

@ -0,0 +1,73 @@
import { InjectQueue } from "@nestjs/bullmq";
import { Injectable, Logger } from "@nestjs/common";
import { Queue } from "bullmq";
import { TimesheetsService } from "../timesheets/services/timesheets.service";
import { ShiftsService } from "../shifts/services/shifts.service";
import { ExpensesService } from "../expenses/services/expenses.service";
import { EmployeesService } from "../employees/services/employees.service";
import { LeaveRequestsService } from "../leave-requests/services/leave-requests.service";
export interface DigestItem {
title: string;
description: string;
link?: string;
}
@Injectable()
export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);
constructor(
@InjectQueue('notifications') private readonly queue: Queue,
private readonly timesheetsService : TimesheetsService,
private readonly shiftsService : ShiftsService,
private readonly expensesService : ExpensesService,
private readonly employeesService : EmployeesService,
private readonly leaveRequestsService: LeaveRequestsService,
) {}
async queueNotification(channel: string, payload: Record<string,any>): Promise<void> {
await this.queue.add(channel, payload);
this.logger.debug(`Enqueued notification on channel= "${channel}"`);
}
async buildWeeklyDigest(): Promise<{recipients: string[], items: DigestItem[]}> {
//TO DO add logic of missing shifts, overtime alert, vacation alerts, leave-requests, etc...
//fetching all business datas
//const missingShifts = await this.timesheetsService.findMissingShftsLastWeek();
const items: DigestItem[] = [
//example:
{title: 'Carte de temps incomplete', description: 'Des employes n`ont pas saisi leurs quarts de travail'},
{title: 'Overtime détecté', description: '....'},
];
const recipients = [
//exemple : await this.userService.findSupervisorsEmails();
'supervisor@targointernet.com',
'accounting@targointernet.com',
];
return {recipients, items}
}
async buildMonthlyDigest(): Promise<{ recipients: string[], items: DigestItem[]}> {
//const anniversaries = await this.employeesService.findAnniversariesthisMonth();
//const totalOvertime = await this.timesheetsService.calculateTotalOvertimeThisMonth();
const items: DigestItem[] = [
{title: '5 ans d`ancienneté', description:'Marc-André, Jessy'},
{title: '10 ans d`ancienneté', description:'Kadi, Maxime'},
{title: 'Calendrier Annuel', description: 'Nouveau calendrier de l`an prochain maintenant disponible!'},
// ...
];
const recipients = [
'allemployees@targointernent.com',
];
return { recipients, items };
}
}

View File

@ -0,0 +1,94 @@
import { Injectable, Logger } from "@nestjs/common";
import { Cron, Interval, SchedulerRegistry } from "@nestjs/schedule";
import { NotificationsService as Orchestrator } from './notifications.service';
@Injectable()
export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);
constructor(
private readonly schedulerRegistry: SchedulerRegistry,
private readonly orchestrator: Orchestrator,
) {}
//cache purging
//@TimeOut(15_000)
async onStartup() {
this.logger.debug('Startup cleanup: initial verifications');
//clean up of useless cache on start up
}
//Q monitoring
@Interval(300_000)
async monitorQueueHealth() {
this.logger.debug('monitoring notification queue')
//this.orchestrator.checkQueueLength();
//monitor backlog for overload and such
}
//weekly cron jobs
@Cron('0 0 8 * * 1', {
name: 'weeklyDigest',
timeZone: 'America/Toronto',
})
async sendWeeklyDigest() {
this.logger.debug('Building Weekly digest');
const { recipients, items } = await this.orchestrator.buildWeeklyDigest();
await this.orchestrator.queueNotification('email', {
to: recipients,
subject: '[Journal Hebdo] Sommaire de la semaine dernière',
template: 'weekly-digest',
context: { items },
});
this.logger.debug('Weekly digest Queued');
}
async disableWeeklyDigest() {
const job = this.schedulerRegistry.getCronJob('weeklyDigest');
job.stop();
this.logger.debug(`Weekly digest stopped`);
}
async enableWeeklyDigest() {
const job = this.schedulerRegistry.getCronJob('weeklyDigest');
job.start();
this.logger.debug(`Weekly digest started`);
}
//monthly cron jobs
@Cron('0 0 9 1 * *', {
name: 'monthlyDigest',
timeZone: 'America/Toronto',
})
async sendMonthlyDigest() {
this.logger.debug('Building Monthly digest');
const {recipients, items} = await this.orchestrator.buildMonthlyDigest();
await this.orchestrator.queueNotification('email', {
to: recipients,
subject: '[Journal Mensuel] Sommaire du mois',
template: 'monthly-digest',
context: { items },
})
this.logger.debug('Monthly digest queued');
}
async disableMonthlyDigest() {
const job = this.schedulerRegistry.getCronJob('monthlyDigest');
job.stop();
this.logger.debug(`Monthly digest stopped`);
}
async enableMonthlyDigest() {
const job = this.schedulerRegistry.getCronJob('monthlyDigest');
job.start();
this.logger.debug(`Monthly digest stopped`);
}
}
function TimeOut(arg0: number): (target: Orchestrator, propertyKey: "onStartup", descriptor: TypedPropertyDescriptor<() => Promise<void>>) => void | TypedPropertyDescriptor<() => Promise<void>> {
throw new Error("Function not implemented.");
}