From f383f6d14d6bd51a5208f3fc6ef4ea1b28badb06 Mon Sep 17 00:00:00 2001 From: Abdalhamid Alhamad Date: Sun, 29 Dec 2024 11:44:51 +0300 Subject: [PATCH] feat: handle notification async using event emitter --- src/app.module.ts | 8 ++--- src/auth/auth.module.ts | 2 +- src/auth/services/auth.service.ts | 2 +- .../notification/enums/event-type.enum.ts | 3 ++ .../modules/notification/enums/index.ts | 1 + .../enums/notification-scope.enum.ts | 1 + .../notification/events/notification.event.ts | 3 ++ .../notification/notification.module.ts | 2 +- .../services/notifications.service.ts | 32 +++++++++++++++++-- src/common/modules/otp/constants/index.ts | 1 + .../otp/constants/otp-contant.constants.ts | 2 ++ src/common/modules/otp/otp.module.ts | 3 +- .../modules/otp/services/otp.service.ts | 12 ++++--- 13 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/common/modules/notification/enums/event-type.enum.ts create mode 100644 src/common/modules/notification/events/notification.event.ts create mode 100644 src/common/modules/otp/constants/otp-contant.constants.ts diff --git a/src/app.module.ts b/src/app.module.ts index 1131163..cde9f93 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,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 { TypeOrmModule } from '@nestjs/typeorm'; import { I18nMiddleware, I18nModule } from 'nestjs-i18n'; import { LoggerModule } from 'nestjs-pino'; @@ -52,9 +53,8 @@ import { TaskModule } from './task/task.module'; inject: [ConfigService], }), I18nModule.forRoot(buildI18nOptions()), - CacheModule, - + EventEmitterModule.forRoot(), // App modules AuthModule, CustomerModule, @@ -66,14 +66,12 @@ import { TaskModule } from './task/task.module'; AllowanceModule, MoneyRequestModule, GiftModule, - + NotificationModule, OtpModule, DocumentModule, LookupModule, HealthModule, - - NotificationModule, ], providers: [ // Global Pipes diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index a051a91..fff54ce 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -19,6 +19,6 @@ import { AccessTokenStrategy } from './strategies'; ], providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy], controllers: [AuthController], - exports: [UserService, DeviceService], + exports: [DeviceService, UserService], }) export class AuthModule {} diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 3b33963..9d83b17 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -44,7 +44,7 @@ export class AuthService { return this.otpService.generateAndSendOtp({ userId: user.id, - recipient: user.phoneNumber, + recipient: user.countryCode + user.phoneNumber, scope: OtpScope.VERIFY_PHONE, otpType: OtpType.SMS, }); diff --git a/src/common/modules/notification/enums/event-type.enum.ts b/src/common/modules/notification/enums/event-type.enum.ts new file mode 100644 index 0000000..ba4ab9a --- /dev/null +++ b/src/common/modules/notification/enums/event-type.enum.ts @@ -0,0 +1,3 @@ +export enum EventType { + NOTIFICATION_CREATED = 'NOTIFICATION_CREATED', +} diff --git a/src/common/modules/notification/enums/index.ts b/src/common/modules/notification/enums/index.ts index 1e495a1..ea3fd97 100644 --- a/src/common/modules/notification/enums/index.ts +++ b/src/common/modules/notification/enums/index.ts @@ -1,3 +1,4 @@ +export * from './event-type.enum'; export * from './notification-channel.enum'; export * from './notification-scope.enum'; export * from './notification-status.enum'; diff --git a/src/common/modules/notification/enums/notification-scope.enum.ts b/src/common/modules/notification/enums/notification-scope.enum.ts index b8801f2..787e715 100644 --- a/src/common/modules/notification/enums/notification-scope.enum.ts +++ b/src/common/modules/notification/enums/notification-scope.enum.ts @@ -2,4 +2,5 @@ export enum NotificationScope { USER_REGISTERED = 'USER_REGISTERED', TASK_COMPLETED = 'TASK_COMPLETED', GIFT_RECEIVED = 'GIFT_RECEIVED', + OTP = 'OTP', } diff --git a/src/common/modules/notification/events/notification.event.ts b/src/common/modules/notification/events/notification.event.ts new file mode 100644 index 0000000..20e88cf --- /dev/null +++ b/src/common/modules/notification/events/notification.event.ts @@ -0,0 +1,3 @@ +export class NotificationEvent { + constructor(public readonly notification: Notification) {} +} diff --git a/src/common/modules/notification/notification.module.ts b/src/common/modules/notification/notification.module.ts index 4b1f1ab..2ecb31f 100644 --- a/src/common/modules/notification/notification.module.ts +++ b/src/common/modules/notification/notification.module.ts @@ -13,11 +13,11 @@ import { TwilioService } from './services/twilio.service'; @Module({ imports: [ TypeOrmModule.forFeature([Notification]), - AuthModule, TwilioModule.forRootAsync({ useFactory: buildTwilioOptions, inject: [ConfigService], }), + AuthModule, ], providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService], exports: [NotificationsService], diff --git a/src/common/modules/notification/services/notifications.service.ts b/src/common/modules/notification/services/notifications.service.ts index 46f8880..bfb4e30 100644 --- a/src/common/modules/notification/services/notifications.service.ts +++ b/src/common/modules/notification/services/notifications.service.ts @@ -1,7 +1,12 @@ -import { Injectable } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { DeviceService } from '~/auth/services'; import { PageOptionsRequestDto } from '~/core/dtos'; +import { OTP_BODY, OTP_TITLE } from '../../otp/constants'; +import { OtpType } from '../../otp/enums'; +import { ISendOtp } from '../../otp/interfaces'; import { Notification } from '../entities'; +import { EventType, NotificationChannel, NotificationScope } from '../enums'; import { NotificationsRepository } from '../repositories'; import { FirebaseService } from './firebase.service'; import { TwilioService } from './twilio.service'; @@ -9,10 +14,11 @@ import { TwilioService } from './twilio.service'; @Injectable() export class NotificationsService { constructor( - private readonly deviceService: DeviceService, private readonly firebaseService: FirebaseService, private readonly notificationRepository: NotificationsRepository, private readonly twilioService: TwilioService, + private readonly eventEmitter: EventEmitter2, + @Inject(forwardRef(() => DeviceService)) private readonly deviceService: DeviceService, ) {} async sendPushNotification(userId: string, title: string, body: string) { @@ -47,4 +53,26 @@ export class NotificationsService { markAsRead(userId: string) { return this.notificationRepository.markAsRead(userId); } + + async sendOtpNotification(sendOtpRequest: ISendOtp, otp: string) { + const notification = await this.createNotification({ + recipient: sendOtpRequest.recipient, + title: OTP_TITLE, + message: OTP_BODY.replace('{otp}', otp), + scope: NotificationScope.OTP, + channel: sendOtpRequest.otpType === OtpType.EMAIL ? NotificationChannel.EMAIL : NotificationChannel.SMS, + }); + + return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification); + } + + @OnEvent(EventType.NOTIFICATION_CREATED) + handleNotificationCreatedEvent(notification: Notification) { + switch (notification.channel) { + case NotificationChannel.SMS: + return this.sendSMS(notification.recipient!, notification.message); + case NotificationChannel.PUSH: + return this.sendPushNotification(notification.userId, notification.title, notification.message); + } + } } diff --git a/src/common/modules/otp/constants/index.ts b/src/common/modules/otp/constants/index.ts index 664c667..3b1a3c8 100644 --- a/src/common/modules/otp/constants/index.ts +++ b/src/common/modules/otp/constants/index.ts @@ -1 +1,2 @@ +export * from './otp-contant.constants'; export * from './otp-default.constant'; diff --git a/src/common/modules/otp/constants/otp-contant.constants.ts b/src/common/modules/otp/constants/otp-contant.constants.ts new file mode 100644 index 0000000..fa2f31e --- /dev/null +++ b/src/common/modules/otp/constants/otp-contant.constants.ts @@ -0,0 +1,2 @@ +export const OTP_TITLE = 'ZOD - OTP'; +export const OTP_BODY = 'Your verification code for ZOD is {otp}'; diff --git a/src/common/modules/otp/otp.module.ts b/src/common/modules/otp/otp.module.ts index 6a8097c..48d1d3d 100644 --- a/src/common/modules/otp/otp.module.ts +++ b/src/common/modules/otp/otp.module.ts @@ -1,13 +1,14 @@ import { Global, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationModule } from '../notification/notification.module'; import { Otp } from './entities'; import { OtpRepository } from './repositories'; import { OtpService } from './services/otp.service'; @Global() @Module({ - imports: [TypeOrmModule.forFeature([Otp])], providers: [OtpService, OtpRepository], exports: [OtpService], + imports: [TypeOrmModule.forFeature([Otp]), NotificationModule], }) export class OtpModule {} diff --git a/src/common/modules/otp/services/otp.service.ts b/src/common/modules/otp/services/otp.service.ts index b5c45b4..8c9f859 100644 --- a/src/common/modules/otp/services/otp.service.ts +++ b/src/common/modules/otp/services/otp.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { NotificationsService } from '../../notification/services/notifications.service'; import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants'; import { OtpType } from '../enums'; import { ISendOtp, IVerifyOtp } from '../interfaces'; @@ -8,7 +9,11 @@ import { generateRandomOtp } from '../utils'; @Injectable() export class OtpService { - constructor(private readonly configService: ConfigService, private readonly otpRepository: OtpRepository) {} + constructor( + private readonly configService: ConfigService, + private readonly otpRepository: OtpRepository, + private readonly notificationService: NotificationsService, + ) {} private useMock = this.configService.get('USE_MOCK', false); async generateAndSendOtp(sendotpRequest: ISendOtp): Promise { const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH); @@ -28,8 +33,7 @@ export class OtpService { return !!otp; } - private sendOtp(sendotpRequest: ISendOtp, otp: string) { - // TODO: send OTP to the user - return; + private sendOtp(sendOtpRequest: ISendOtp, otp: string) { + return this.notificationService.sendOtpNotification(sendOtpRequest, otp); } }