diff --git a/package.json b/package.json index e7b18829b..5732dc8db 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "tasks:charger-vues": "IS_WEB=false TASK_NAME=CHARGER_LES_VUES_ANALYTICS node dist/main", "tasks:initialiser-les-vues": "IS_WEB=false TASK_NAME=INITIALISER_LES_VUES node dist/main", "tasks:creer-tables-ae-annuelles": "IS_WEB=false TASK_NAME=CREER_TABLES_AE_ANNUELLES_ANALYTICS node dist/main", + "tasks:notifier-bonne-alternance": "IS_WEB=false TASK_NAME=NOTIFIER_BONNE_ALTERNANCE node dist/main", + "tasks:notifier-cje": "IS_WEB=false TASK_NAME=NOTIFIER_CJE node dist/main", "dump-restore-db": "scripts/analytics/0_db_dump_restore.sh 2>&1", "dump-restore-db:local": "dotenv -e .environment yarn dump-restore-db", "release:patch": "scripts/release_version_only.sh patch", diff --git a/src/app.module.ts b/src/app.module.ts index d1c5a8c41..3c3a858e0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -366,6 +366,8 @@ import { configureLoggerModule } from './utils/logger.module' import { RateLimiterService } from './utils/rate-limiter.service' import { CJEController } from './infrastructure/routes/cje.controller' import { GetCJETokenQueryHandler } from './application/queries/get-cje-token.query.handler' +import { NotifierBonneAlternanceJobHandler } from './application/jobs/notifier-bonne-alternance.job.handler.db' +import { NotifierCJEJobHandler } from './application/jobs/notifier-cje.job.handler.db' export const buildModuleMetadata = (): ModuleMetadata => ({ imports: [ @@ -830,7 +832,9 @@ export const JobHandlerProviders = [ CreerTablesAEAnnuellesJobHandler, QualifierActionsJobHandler, RecupererAnalyseAntivirusJobHandler, - NotifierRappelCreationActionsDemarchesJobHandler + NotifierRappelCreationActionsDemarchesJobHandler, + NotifierBonneAlternanceJobHandler, + NotifierCJEJobHandler ] @Module(buildModuleMetadata()) diff --git a/src/application/jobs/notifier-bonne-alternance.job.handler.db.ts b/src/application/jobs/notifier-bonne-alternance.job.handler.db.ts new file mode 100644 index 000000000..34655e21e --- /dev/null +++ b/src/application/jobs/notifier-bonne-alternance.job.handler.db.ts @@ -0,0 +1,122 @@ +import { Inject, Injectable } from '@nestjs/common' +import { Op } from 'sequelize' +import { Job } from '../../building-blocks/types/job' +import { JobHandler } from '../../building-blocks/types/job-handler' +import { Core } from '../../domain/core' +import { + Notification, + NotificationRepositoryToken +} from '../../domain/notification/notification' +import { + Planificateur, + PlanificateurRepositoryToken, + ProcessJobType +} from '../../domain/planificateur' +import { SuiviJob, SuiviJobServiceToken } from '../../domain/suivi-job' +import { JeuneSqlModel } from '../../infrastructure/sequelize/models/jeune.sql-model' +import { DateService } from '../../utils/date-service' + +interface Stats { + nbPersonnesNotifies: number + estLaDerniereExecution: boolean +} + +const PAGINATION_NOMBRE_DE_JEUNES_MAXIMUM = 2000 + +@Injectable() +@ProcessJobType(Planificateur.JobType.NOTIFIER_BONNE_ALTERNANCE) +export class NotifierBonneAlternanceJobHandler extends JobHandler { + constructor( + @Inject(NotificationRepositoryToken) + private notificationRepository: Notification.Repository, + @Inject(SuiviJobServiceToken) + suiviJobService: SuiviJob.Service, + private dateService: DateService, + @Inject(PlanificateurRepositoryToken) + private planificateurRepository: Planificateur.Repository + ) { + super(Planificateur.JobType.NOTIFIER_BONNE_ALTERNANCE, suiviJobService) + } + + async handle( + job?: Planificateur.Job + ): Promise { + let succes = true + const stats: Stats = { + nbPersonnesNotifies: job?.contenu?.nbPersonnesNotifies || 0, + estLaDerniereExecution: false + } + const maintenant = this.dateService.now() + + try { + const structuresConcernees = [ + Core.Structure.MILO, + Core.Structure.POLE_EMPLOI, + Core.Structure.POLE_EMPLOI_AIJ + ] + + const offset = job?.contenu?.offset || 0 + + const idsJeunesANotifier = await JeuneSqlModel.findAll({ + where: { + structure: { + [Op.in]: structuresConcernees + }, + pushNotificationToken: { + [Op.ne]: null + } + }, + attributes: ['id', 'pushNotificationToken'] + }) + + this.logger.log(`${idsJeunesANotifier.length} ids jeunes à notifier`) + + stats.nbPersonnesNotifies += idsJeunesANotifier.length + for (const jeune of idsJeunesANotifier) { + try { + const notification: Notification.Message = { + token: jeune.pushNotificationToken!, + notification: { + title: `La bonne alternance`, + body: `La bonne alternance` + }, + data: { + type: 'BONNE_ALTERNANCE' + } + } + await this.notificationRepository.send(notification) + this.logger.log(`Notification envoyée pour le jeune ${jeune.id}`) + } catch (e) { + this.logger.error(e) + this.logger.log(`Echec envoi notif pour le jeune ${jeune.id}`) + } + await new Promise(resolve => setTimeout(resolve, 250)) + } + + if (idsJeunesANotifier.length === PAGINATION_NOMBRE_DE_JEUNES_MAXIMUM) { + this.planificateurRepository.creerJob({ + dateExecution: maintenant.plus({ seconds: 30 }).toJSDate(), + type: Planificateur.JobType.NOTIFIER_BONNE_ALTERNANCE, + contenu: { + offset: offset + PAGINATION_NOMBRE_DE_JEUNES_MAXIMUM, + nbPersonnesNotifies: stats.nbPersonnesNotifies + } + }) + } else { + stats.estLaDerniereExecution = true + } + } catch (e) { + this.logger.error(e) + succes = false + } + + return { + jobType: this.jobType, + nbErreurs: 0, + succes, + dateExecution: maintenant, + tempsExecution: DateService.calculerTempsExecution(maintenant), + resultat: stats + } + } +} diff --git a/src/application/jobs/notifier-cje.job.handler.db.ts b/src/application/jobs/notifier-cje.job.handler.db.ts new file mode 100644 index 000000000..4754dd5d9 --- /dev/null +++ b/src/application/jobs/notifier-cje.job.handler.db.ts @@ -0,0 +1,128 @@ +import { Inject, Injectable } from '@nestjs/common' +import { Op } from 'sequelize' +import { Job } from '../../building-blocks/types/job' +import { JobHandler } from '../../building-blocks/types/job-handler' +import { Core } from '../../domain/core' +import { + Notification, + NotificationRepositoryToken +} from '../../domain/notification/notification' +import { + Planificateur, + PlanificateurRepositoryToken, + ProcessJobType +} from '../../domain/planificateur' +import { SuiviJob, SuiviJobServiceToken } from '../../domain/suivi-job' +import { JeuneSqlModel } from '../../infrastructure/sequelize/models/jeune.sql-model' +import { DateService } from '../../utils/date-service' + +interface Stats { + nbPersonnesNotifies: number + estLaDerniereExecution: boolean +} + +const PAGINATION_NOMBRE_DE_JEUNES_MAXIMUM = 2000 + +@Injectable() +@ProcessJobType(Planificateur.JobType.NOTIFIER_CJE) +export class NotifierCJEJobHandler extends JobHandler { + constructor( + @Inject(NotificationRepositoryToken) + private notificationRepository: Notification.Repository, + @Inject(SuiviJobServiceToken) + suiviJobService: SuiviJob.Service, + private dateService: DateService, + @Inject(PlanificateurRepositoryToken) + private planificateurRepository: Planificateur.Repository + ) { + super(Planificateur.JobType.NOTIFIER_CJE, suiviJobService) + } + + async handle( + job?: Planificateur.Job + ): Promise { + let succes = true + const stats: Stats = { + nbPersonnesNotifies: job?.contenu?.nbPersonnesNotifies || 0, + estLaDerniereExecution: false + } + const maintenant = this.dateService.now() + + try { + const structuresConcernees = [ + Core.Structure.MILO, + Core.Structure.POLE_EMPLOI + ] + + const offset = job?.contenu?.offset || 0 + + const idsJeunesANotifier = await JeuneSqlModel.findAll({ + where: { + structure: { + [Op.in]: structuresConcernees + }, + pushNotificationToken: { + [Op.ne]: null + } + }, + attributes: ['id', 'pushNotificationToken'] + }) + + this.logger.log(`${idsJeunesANotifier.length} ids jeunes à notifier`) + + stats.nbPersonnesNotifies += idsJeunesANotifier.length + for (const jeune of idsJeunesANotifier) { + try { + const notification: Notification.Message = { + token: jeune.pushNotificationToken!, + notification: { + title: `👋 Retrouvez vos avantages du CEJ`, + body: `+ de 65 réductions disponibles grâce à la carte "Jeune Engagé"` + }, + data: { + type: 'CJE' + } + } + await this.notificationRepository.send(notification) + this.logger.log(`Notification envoyée pour le jeune ${jeune.id}`) + } catch (e) { + this.logger.error(e) + this.logger.log(`Echec envoi notif pour le jeune ${jeune.id}`) + } + await new Promise(resolve => setTimeout(resolve, 500)) + } + + if (idsJeunesANotifier.length === PAGINATION_NOMBRE_DE_JEUNES_MAXIMUM) { + const dateExecution = + maintenant.hour <= 19 && maintenant.hour >= 8 + ? maintenant.plus({ minutes: 30 }).toJSDate() + : maintenant + .plus({ days: 1 }) + .set({ hour: 8, minute: 0, second: 0, millisecond: 0 }) + .toJSDate() + this.planificateurRepository.creerJob({ + dateExecution, + type: Planificateur.JobType.NOTIFIER_CJE, + contenu: { + offset: offset + PAGINATION_NOMBRE_DE_JEUNES_MAXIMUM, + nbPersonnesNotifies: stats.nbPersonnesNotifies + } + }) + } else { + stats.estLaDerniereExecution = true + } + } catch (e) { + this.logger.error(e) + succes = false + } + + return { + jobType: this.jobType, + nbErreurs: 0, + succes, + dateExecution: maintenant, + tempsExecution: DateService.calculerTempsExecution(maintenant), + resultat: stats + } + } +} diff --git a/src/domain/planificateur.ts b/src/domain/planificateur.ts index 8b1af06c6..3018bc2dd 100644 --- a/src/domain/planificateur.ts +++ b/src/domain/planificateur.ts @@ -74,7 +74,9 @@ export namespace Planificateur { CREER_TABLES_AE_ANNUELLES_ANALYTICS = 'CREER_TABLES_AE_ANNUELLES_ANALYTICS', QUALIFIER_ACTIONS = 'QUALIFIER_ACTIONS', RECUPERER_ANALYSE_ANTIVIRUS = 'RECUPERER_ANALYSE_ANTIVIRUS', - NOTIFIER_RAPPEL_CREATION_ACTIONS_DEMARCHES = 'NOTIFIER_RAPPEL_CREATION_ACTIONS_DEMARCHES' + NOTIFIER_RAPPEL_CREATION_ACTIONS_DEMARCHES = 'NOTIFIER_RAPPEL_CREATION_ACTIONS_DEMARCHES', + NOTIFIER_BONNE_ALTERNANCE = 'NOTIFIER_BONNE_ALTERNANCE', + NOTIFIER_CJE = 'NOTIFIER_CJE' } export interface JobRendezVous { @@ -92,6 +94,11 @@ export namespace Planificateur { nbJeunesNotifies?: number } + export interface JobNotifierParGroupe { + offset: number + nbPersonnesNotifies: number + } + export interface JobRappelAction { idAction: string }