diff --git a/package.json b/package.json index 8f2f674..1336087 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "crud-app", + "name": "notification", "version": "0.0.1", "description": "", "author": "", @@ -21,16 +21,15 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@golevelup/nestjs-rabbitmq": "^5.3.0", "@aws-sdk/client-sns": "^3.750.0", - "@nestjs-plus/rabbitmq": "^1.4.4", - "@nestjs/common": "^9.0.0", - "@nestjs/config": "^2.3.0", - "@nestjs/core": "^8.4.7", - "jwt-decode": "^4.0.0", - "@nestjs/platform-express": "^8.0.0", + "@golevelup/nestjs-rabbitmq": "^5.7.0", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.1", - "@nestjs/typeorm": "^8.1.4", + "@nestjs/typeorm": "^10.0.0", + "@sendgrid/mail": "^8.1.5", "@types/amqplib": "^0.10.5", "amqp-connection-manager": "^4.1.14", "axios": "^1.6.5", @@ -38,9 +37,10 @@ "class-validator": "^0.14.1", "dotenv": "^16.0.1", "firebase-admin": "^12.6.0", + "jwt-decode": "^4.0.0", "notifme-sdk": "^1.11.0", "pg": "^8.11.3", - "reflect-metadata": "^0.1.13", + "reflect-metadata": "^0.2.2", "rimraf": "^3.0.2", "rxjs": "^7.2.0", "twilio": "^5.0.0-rc.1", @@ -51,7 +51,7 @@ "devDependencies": { "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", - "@nestjs/testing": "^8.0.0", + "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.13", "@types/jest": "27.5.0", "@types/node": "^16.0.0", diff --git a/src/common/utils/constant.util.ts b/src/common/utils/constant.util.ts index 3f9ec06..d07655b 100644 --- a/src/common/utils/constant.util.ts +++ b/src/common/utils/constant.util.ts @@ -55,7 +55,9 @@ export const ERROR_MESSAGES = { SMS_NOTIFICATION_FAILED: 'Failed to Send SMS Notification', ALREADY_EXIST_KEY_FOR_CONTEXT: 'Already Exist key with this context', ALREADY_EXIST_KEY_FOR_CONTEXT_ENTER_ANOTHER: 'Key already exist for this context. Please enter another key', - NOT_EMPTY_SUBJECT_OR_BODY: 'Subject and body cannot be empty.' + NOT_EMPTY_SUBJECT_OR_BODY: 'Subject and body cannot be empty.', + PUSH_ADAPTER_NOT_INITIALIZED: 'PushAdapter is not initialized. Check Firebase configuration.', + EMAIL_ADAPTER_NOT_CONFIGURED: 'EmailAdapter is not configured. Check Email configuration.' } export const SMS_PROVIDER = { diff --git a/src/modules/notification/adapters/emailService.adapter.ts b/src/modules/notification/adapters/emailService.adapter.ts index 76d8f27..4b76562 100644 --- a/src/modules/notification/adapters/emailService.adapter.ts +++ b/src/modules/notification/adapters/emailService.adapter.ts @@ -10,6 +10,8 @@ import { NotificationLog } from "../entity/notificationLogs.entity"; import { NotificationService } from "../notification.service"; import { LoggerUtil } from "src/common/logger/LoggerUtil"; import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util"; +import { ConfigService } from "@nestjs/config"; +import * as sgMail from '@sendgrid/mail'; /** * Interface for raw email data @@ -24,10 +26,44 @@ export interface RawEmailData { @Injectable() export class EmailAdapter implements NotificationServiceInterface { + private provider: 'smtp' | 'sendgrid' = 'smtp'; + private isConfigured = false; constructor( - @Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService - ) { } + @Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService, + private readonly configService: ConfigService + ) { + this.provider = (this.configService.get('EMAIL_PROVIDER') || 'smtp') as 'smtp' | 'sendgrid'; + if (this.provider === 'smtp' && this.hasSmtpConfig()) { + this.isConfigured = true; + } + + if (this.provider === 'sendgrid' && this.hasSendGridConfig()) { + sgMail.setApiKey(this.configService.get('SENDGRID_API_KEY')); + this.isConfigured = true; + } + + if (!this.isConfigured) { + console.warn('EmailAdapter not configured: Missing required settings.'); + LoggerUtil.error(ERROR_MESSAGES.EMAIL_ADAPTER_NOT_CONFIGURED); + } + } + + private hasSmtpConfig(): boolean { + return ( + !!this.configService.get('EMAIL_HOST') && + !!this.configService.get('EMAIL_PORT') && + !!this.configService.get('EMAIL_USER') && + !!this.configService.get('EMAIL_PASS') + ); + } + + private hasSendGridConfig(): boolean { + return !!( + this.configService.get('SENDGRID_API_KEY') && + this.configService.get('EMAIL_FROM') + ); + } /** * Sends notifications using template-based approach * @param notificationDataArray Array of notification data objects @@ -155,7 +191,7 @@ export class EmailAdapter implements NotificationServiceInterface { email: { providers: [ { - type: process.env.EMAIL_TYPE, + type: process.env.EMAIL_PROVIDER || 'smtp', host: process.env.EMAIL_HOST, port: process.env.EMAIL_PORT, secure: false, @@ -186,17 +222,34 @@ export class EmailAdapter implements NotificationServiceInterface { */ async send(notificationData) { const notificationLogs = this.createNotificationLog(notificationData, notificationData.subject, notificationData.key, notificationData.body, notificationData.recipient); + let result; try { - const emailConfig = this.getEmailConfig(notificationData.context); - const notifmeSdk = new NotifmeSdk(emailConfig); - const result = await notifmeSdk.send({ - email: { - from: emailConfig.email.from, + if (this.provider === 'smtp') { + const emailConfig = this.getEmailConfig(notificationData.context); + const notifmeSdk = new NotifmeSdk(emailConfig); + result = await notifmeSdk.send({ + email: { + from: emailConfig.email.from, + to: notificationData.recipient, + subject: notificationData.subject, + html: notificationData.body, + }, + }); + } + else if (this.provider === 'sendgrid') { + const msg = { to: notificationData.recipient, + from: this.configService.get('EMAIL_FROM'), subject: notificationData.subject, html: notificationData.body, - }, - }); + }; + const sgResponse = await sgMail.send(msg); + result = { + status: sgResponse[0]?.statusCode === 202 ? 'success' : 'error', + id: sgResponse[0]?.headers['x-message-id'] || null, + errors: sgResponse[0]?.statusCode !== 202 ? sgResponse[0]?.body : undefined, + }; + } if (result.status === 'success') { notificationLogs.status = true; await this.notificationServices.saveNotificationLogs(notificationLogs); diff --git a/src/modules/notification/adapters/pushService.adapter.ts b/src/modules/notification/adapters/pushService.adapter.ts index 284afc2..383637d 100644 --- a/src/modules/notification/adapters/pushService.adapter.ts +++ b/src/modules/notification/adapters/pushService.adapter.ts @@ -7,29 +7,47 @@ import { NotificationLog } from "../entity/notificationLogs.entity"; import { NotificationService } from "../notification.service"; import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util"; import { LoggerUtil } from "src/common/logger/LoggerUtil"; -@Injectable() export class PushAdapter implements NotificationServiceInterface { private readonly fcmurl: string; + private isInitialized = false; constructor( @Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService, private readonly configService: ConfigService ) { this.fcmurl = this.configService.get('FCM_URL'); + if (this.hasRequiredConfig()) { + //Initialize Firebase Admin SDK with environment variables + const serviceAccount = { + projectId: this.configService.get('FIREBASE_PROJECT_ID'), + clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'), + privateKey: this.configService.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'), // Replace escaped newlines + }; - // Initialize Firebase Admin SDK with environment variables - const serviceAccount = { - projectId: this.configService.get('FIREBASE_PROJECT_ID'), - clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'), - privateKey: this.configService.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'), // Replace escaped newlines - }; - - if (admin.apps.length === 0) { - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - }); + if (admin.apps.length === 0) { + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + } + this.isInitialized = true; + } + else { + console.warn('PushAdapter not initialized: Missing Firebase config.'); + LoggerUtil.error(ERROR_MESSAGES.PUSH_ADAPTER_NOT_INITIALIZED); } } + + private hasRequiredConfig(): boolean { + return !!( + this.configService.get('FIREBASE_PROJECT_ID') && + this.configService.get('FIREBASE_CLIENT_EMAIL') && + this.configService.get('FIREBASE_PRIVATE_KEY') + ); + } async sendNotification(notificationDataArray) { + if (!this.isInitialized) { + LoggerUtil.error(ERROR_MESSAGES.PUSH_ADAPTER_NOT_INITIALIZED); + throw new Error('PushAdapter is not initialized. Check Firebase configuration.'); + } const results = []; for (const notificationData of notificationDataArray) { try { diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 1ae8179..ebf4e63 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -14,7 +14,7 @@ import { Response } from "express"; import { NotificationActions } from "../notification_events/entity/notificationActions.entity"; import { NotificationActionTemplates } from "../notification_events/entity/notificationActionTemplates.entity"; import { NotificationQueue } from "../notification-queue/entities/notificationQueue.entity"; -import { AmqpConnection, RabbitSubscribe } from "@nestjs-plus/rabbitmq"; +import { AmqpConnection, RabbitSubscribe } from "@golevelup/nestjs-rabbitmq"; import { NotificationQueueService } from "../notification-queue/notificationQueue.service"; import { APIID } from "src/common/utils/api-id.config"; import {EmailAdapter} from "src/modules/notification/adapters/emailService.adapter"; diff --git a/src/modules/notification_events/entity/notificationActionTemplates.entity.ts b/src/modules/notification_events/entity/notificationActionTemplates.entity.ts index 76b4e15..5059ddd 100644 --- a/src/modules/notification_events/entity/notificationActionTemplates.entity.ts +++ b/src/modules/notification_events/entity/notificationActionTemplates.entity.ts @@ -28,13 +28,12 @@ export class NotificationActionTemplates { @Column({ type: 'uuid' }) createdBy: string; - @Column() + @Column({ nullable: true }) image: string; - @Column() + @Column({ nullable: true }) link: string; - @Column({ type: 'uuid', nullable: true }) updatedBy: string; diff --git a/src/modules/rabbitmq/rabbitmq.module.ts b/src/modules/rabbitmq/rabbitmq.module.ts index 288edd7..2e58579 100644 --- a/src/modules/rabbitmq/rabbitmq.module.ts +++ b/src/modules/rabbitmq/rabbitmq.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { RabbitMQModule } from '@nestjs-plus/rabbitmq'; +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { ConfigService } from '@nestjs/config'; @Module({