diff --git a/src/modules/notification-queue/notificationQueue.module.ts b/src/modules/notification-queue/notificationQueue.module.ts index bd4cf0f..dc9a9b5 100644 --- a/src/modules/notification-queue/notificationQueue.module.ts +++ b/src/modules/notification-queue/notificationQueue.module.ts @@ -13,13 +13,14 @@ import { NotificationActionTemplates } from '../notification_events/entity/notif import { NotificationActions } from '../notification_events/entity/notificationActions.entity'; import { NotificationLog } from '../notification/entity/notificationLogs.entity'; import { RabbitmqModule } from '../rabbitmq/rabbitmq.module'; +import { TypeormService } from '../typeorm/typeorm.service'; @Module({ imports: [ TypeOrmModule.forFeature([NotificationQueue, NotificationActionTemplates, NotificationActions, NotificationLog]), RabbitmqModule ], - providers: [NotificationQueueService, NotificationService, NotificationAdapterFactory, EmailAdapter, SmsAdapter, PushAdapter], + providers: [NotificationQueueService, NotificationService, NotificationAdapterFactory, EmailAdapter, SmsAdapter, PushAdapter, TypeormService], controllers: [NotificationQueueController], exports: [NotificationQueueService] }) diff --git a/src/modules/notification-queue/notificationQueue.service.ts b/src/modules/notification-queue/notificationQueue.service.ts index 1c5d406..11563a4 100644 --- a/src/modules/notification-queue/notificationQueue.service.ts +++ b/src/modules/notification-queue/notificationQueue.service.ts @@ -1,25 +1,28 @@ import { BadRequestException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { Response } from 'express'; -import APIResponse from 'src/common/utils/response'; +import APIResponse from '../../common/utils/response'; import { NotificationQueueDTO } from './dto/notificationQueue.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { NotificationQueue } from './entities/notificationQueue.entity'; import { Repository } from 'typeorm'; import { SearchQueueDTO } from './dto/searchQueue.dto'; import { UpdateQueueDTO } from './dto/updateQueue.dto'; -import { APIID } from 'src/common/utils/api-id.config'; -import { ERROR_MESSAGES, SUCCESS_MESSAGES } from 'src/common/utils/constant.util'; +import { APIID } from '../../common/utils/api-id.config'; +import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../../common/utils/constant.util'; +import { TypeormService } from '../typeorm/typeorm.service'; + @Injectable() export class NotificationQueueService { constructor( @InjectRepository(NotificationQueue) - private readonly notificationQueueRepo: Repository + private readonly notificationQueueRepo: Repository, + private readonly typeormService: TypeormService ) { } async create(notificationQueueDTO: NotificationQueueDTO, response) { const apiId = APIID.QUEUE_CREATE; - const result = await this.notificationQueueRepo.save(notificationQueueDTO); + const result = await this.typeormService.save(NotificationQueue, notificationQueueDTO); return response .status(HttpStatus.CREATED) .json(APIResponse.success(apiId, result, SUCCESS_MESSAGES.CREATED)); @@ -28,7 +31,7 @@ export class NotificationQueueService { async getList(searchQueueDTO: SearchQueueDTO, response: Response) { const apiId = APIID.QUEUE_LIST; const { channel, context, status } = searchQueueDTO; - const result = await this.notificationQueueRepo.find({ + const result = await this.typeormService.find(NotificationQueue, { where: { channel: channel, context: context, status: status } }) if (result.length === 0) { @@ -41,12 +44,12 @@ export class NotificationQueueService { async updateQueue(id: string, updateQueueDTO: UpdateQueueDTO) { const apiId = APIID.QUEUE_UPDATE - const queue = await this.notificationQueueRepo.findOne({ where: { id } }); + const queue = await this.typeormService.findOne(NotificationQueue, { where: { id } }); if (!queue) { throw new BadRequestException(ERROR_MESSAGES.QUEUE_UPDATE(id)) } Object.assign(queue, updateQueueDTO); - const updateResult = await this.notificationQueueRepo.save(queue); + const updateResult = await this.typeormService.save(NotificationQueue, queue); if (!updateResult) { throw new BadRequestException(ERROR_MESSAGES.EVENT_UPDATE_FAILED); } diff --git a/src/modules/notification/adapters/emailService.adapter.ts b/src/modules/notification/adapters/emailService.adapter.ts index 3f1d0a1..be75b26 100644 --- a/src/modules/notification/adapters/emailService.adapter.ts +++ b/src/modules/notification/adapters/emailService.adapter.ts @@ -8,8 +8,8 @@ import { NotificationActionTemplates } from "src/modules/notification_events/ent import NotifmeSdk from 'notifme-sdk'; 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 { LoggerUtil } from "../../../common/logger/LoggerUtil"; +import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "../../../common/utils/constant.util"; @Injectable() diff --git a/src/modules/notification/adapters/pushService.adapter.ts b/src/modules/notification/adapters/pushService.adapter.ts index 284afc2..7612d23 100644 --- a/src/modules/notification/adapters/pushService.adapter.ts +++ b/src/modules/notification/adapters/pushService.adapter.ts @@ -5,8 +5,8 @@ import * as admin from "firebase-admin"; import { ConfigService } from "@nestjs/config"; 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"; +import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "../../../common/utils/constant.util"; +import { LoggerUtil } from "../../../common/logger/LoggerUtil"; @Injectable() export class PushAdapter implements NotificationServiceInterface { private readonly fcmurl: string; diff --git a/src/modules/notification/adapters/smsService.adapter.ts b/src/modules/notification/adapters/smsService.adapter.ts index a966b61..d166e58 100644 --- a/src/modules/notification/adapters/smsService.adapter.ts +++ b/src/modules/notification/adapters/smsService.adapter.ts @@ -1,15 +1,15 @@ import { BadRequestException, Inject, Injectable, forwardRef } from "@nestjs/common"; import { NotificationServiceInterface } from "../interface/notificationService"; import { NotificationDto } from "../dto/notificationDto.dto"; -import { NotificationActions } from "src/modules/notification_events/entity/notificationActions.entity"; +import { NotificationActions } from "../../notification_events/entity/notificationActions.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; -import { NotificationActionTemplates } from "src/modules/notification_events/entity/notificationActionTemplates.entity" +import { NotificationActionTemplates } from "../../notification_events/entity/notificationActionTemplates.entity" import { ConfigService } from "@nestjs/config"; 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 { LoggerUtil } from "../../../common/logger/LoggerUtil"; +import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "../../../common/utils/constant.util"; @Injectable() export class SmsAdapter implements NotificationServiceInterface { diff --git a/src/modules/notification/notification.module.ts b/src/modules/notification/notification.module.ts index ee46660..67424f9 100644 --- a/src/modules/notification/notification.module.ts +++ b/src/modules/notification/notification.module.ts @@ -13,6 +13,7 @@ import { NotificationService } from './notification.service'; import { NotificationQueue } from '../notification-queue/entities/notificationQueue.entity'; import { RabbitmqModule } from '../rabbitmq/rabbitmq.module'; import { NotificationQueueService } from '../notification-queue/notificationQueue.service'; +import { TypeormService } from '../typeorm/typeorm.service'; @Module({ @@ -20,7 +21,8 @@ import { NotificationQueueService } from '../notification-queue/notificationQueu TypeOrmModule.forFeature([NotificationActions, NotificationActionTemplates, NotificationLog, NotificationQueue]), // import entity here NotificationEventsModule, RabbitmqModule ], - providers: [NotificationAdapterFactory, PushAdapter, SmsAdapter, EmailAdapter, NotificationService, NotificationQueueService], + + providers: [NotificationAdapterFactory, PushAdapter, SmsAdapter, EmailAdapter, NotificationService, NotificationQueueService, TypeormService], controllers: [NotificationController], exports: [NotificationService] }) diff --git a/src/modules/notification/notification.service.spec.ts b/src/modules/notification/notification.service.spec.ts new file mode 100644 index 0000000..f5b538b --- /dev/null +++ b/src/modules/notification/notification.service.spec.ts @@ -0,0 +1,374 @@ + +import { Test, TestingModule } from '@nestjs/testing'; +import { NotificationService } from './notification.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +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 { NotificationLog } from './entity/notificationLogs.entity'; +import { TypeormService } from '../typeorm/typeorm.service'; +import { EntityManager } from 'typeorm'; +import { Response } from 'express'; +import * as dotenv from 'dotenv'; +import { NotificationQueueService } from '../notification-queue/notificationQueue.service'; +import { AmqpConnection } from '@nestjs-plus/rabbitmq'; +import { NotificationAdapterFactory } from './notificationadapters'; +import { EmailAdapter } from './adapters/emailService.adapter'; +import { SmsAdapter } from './adapters/smsService.adapter'; +import { PushAdapter } from './adapters/pushService.adapter'; +import { EmailDTO, NotificationDto, PushDTO, SMSDTO } from './dto/notificationDto.dto'; +import { BadRequestException, HttpStatus } from '@nestjs/common'; +dotenv.config(); + +describe('NotificationService', () => { + let service: NotificationService; + let responseMock: Partial; + let mockAmqpConnection: Partial + beforeEach(async () => { + responseMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + mockAmqpConnection = { + publish: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + TypeOrmModule.forRootAsync({ + useFactory: async () => ({ + type: "postgres", + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT, 10), + username: process.env.POSTGRES_USERNAME, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DATABASE, + entities: [NotificationActions, NotificationActionTemplates, NotificationLog, NotificationQueue], + synchronize: false, // Auto synchronize (use cautiously in production) + }), + }), + TypeOrmModule.forFeature([NotificationActionTemplates, NotificationActions, NotificationLog, NotificationQueue]), // Register your repositories + ], + providers: [NotificationService, TypeormService, EntityManager, NotificationQueueService, ConfigService, AmqpConnection, NotificationAdapterFactory, EmailAdapter, SmsAdapter, PushAdapter, { + provide: AmqpConnection, + useValue: mockAmqpConnection, + },], + }).compile(); + service = module.get(NotificationService); + const typeormService = module.get(TypeormService); + // Explicitly initialize TypeormService + await typeormService.initialize(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + // Test case - when template is not found for send notification + it('should throw BadRequestException when notification template is not found', async () => { + const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + const notificationDto: NotificationDto = { + isQueue: false, + context: 'OTP1', + key: 'Reset_OTP1', + replacements: { + '{OTP}': '445566', + '{otpExpiry}': '2', + '{programName}': 'SCP', + '{username}': 'Tets' + }, + email: { + receipients: ["example@example.com"], + }, + push: new PushDTO, + sms: new SMSDTO + }; + + // Assert that the method throws the expected exception + await expect(service.sendNotification(notificationDto, userId, responseMock as Response)) + .rejects.toThrow(new BadRequestException('Template not found')); + }); + //sucess -> Email + it('should send email notification successfully and return expected response structure', async () => { + const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + const notificationDto: NotificationDto = { + isQueue: false, + context: 'OTP', + key: 'Reset_OTP', + replacements: { + '{OTP}': '445566', + '{otpExpiry}': '2', + '{programName}': 'SCP', + '{username}': 'Tets' + }, + email: { + receipients: ["example@example.com"], + }, + push: new PushDTO, + sms: new SMSDTO + }; + + const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + return result; // Just return the result passed to json + }); + await service.sendNotification(notificationDto, userId, responseMock as Response); + + const status = jest.spyOn(responseMock, 'status').mockReturnThis(); + const result = jsonSpy.mock.calls[0][0].result; + expect(status).toHaveBeenCalledWith(HttpStatus.OK); + expect(result).toEqual({ + email: { + data: [ + { + recipient: "example@example.com", + result: 'Email notification sent successfully', + status: 200, + }, + ], + errors: [], + }, + }); + }); + // //sucess -> push -> it eill be fail beacuse device Id we are not set atual for security reason + // it('should send Push notification successfully and return expected response structure', async () => { + // const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + // const notificationDto: NotificationDto = { + // isQueue: false, + // context: 'USER', + // key: 'SESSION_UPDATE_NOTIFICATION', + // replacements: { + // '{sessionName}': 'Math', + // }, + // push: { + // receipients: ['fcmDeviceId123:APA91bGfEXAMPLEr123EXAMPLEoHuEXAMPLE'], + // }, + // email: new EmailDTO, + // sms: new SMSDTO + // }; + + // const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + // return result; // Just return the result passed to json + // }); + // await service.sendNotification(notificationDto, userId, responseMock as Response); + // const status = jest.spyOn(responseMock, 'status').mockReturnThis(); + // const result = jsonSpy.mock.calls[0][0].result; + // expect(status).toHaveBeenCalledWith(HttpStatus.OK); + // expect(result).toEqual({ + // push: { + // data: [ + // { + // recipient: 'fcmDeviceId123:APA91bGfEXAMPLEr123EXAMPLEoHuEXAMPLE', + // status: 200, + // result: 'Push notification sent successfully', + // }, + // ], + // errors: [], + // }, + // }); + // }); + // //sucess -> SMS - + // it('should send SMS notification successfully and return expected response structure', async () => { + // const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + // const notificationDto: NotificationDto = { + // isQueue: false, + // context: 'TEST_CONTEXT', + // key: 'TEST_KEY', + // replacements: { + // '{otp}': '4444', + // '{otpExpiry}': '10MIN' + // }, + // sms: { + // receipients: ['9876543210'], + // }, + // email: new EmailDTO, + // push: new PushDTO + // }; + + // const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + // return result; // Just return the result passed to json + // }); + // await service.sendNotification(notificationDto, userId, responseMock as Response); + // const status = jest.spyOn(responseMock, 'status').mockReturnThis(); + // const result = jsonSpy.mock.calls[0][0].result; + // expect(status).toHaveBeenCalledWith(HttpStatus.OK); + // // Use toEqual or toMatchObject to verify the structure + // expect(result).toEqual({ + // sms: { + // data: [ + // { + // recipient: '9876543210', + // status: 200, + // result: 'SMS notification sent successfully', + // }, + // ], + // errors: [], + // }, + // }); + // }); + + // send all three notification -> sucess + // it('should process multiple notification types (email, SMS, push) simultaneously', async () => { + // const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + // const notificationDto: NotificationDto = { + // isQueue: false, + // context: 'TEST_CONTEXT', + // key: 'TEST_KEY', + // replacements: { + // '{otp}': '4444', + // '{otpExpiry}': '10MIN' + // }, + // sms: { + // receipients: ['9876543210'], + // }, + // email: { + // receipients: [ + // "example@example.com", + // ] + // }, + // push: { + // receipients: [ + // "fcmDeviceId123:APA91bGfEXAMPLEr123EXAMPLEoHuEXAMPLE" + // ] + // } + // }; + // const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + // return result; // Just return the result passed to json + // }); + // await service.sendNotification(notificationDto, userId, responseMock as Response); + // const status = jest.spyOn(responseMock, 'status').mockReturnThis(); + // const result = jsonSpy.mock.calls[0][0].result; + // expect(status).toHaveBeenCalledWith(HttpStatus.OK); + // expect(result).toEqual({ + // sms: { + // data: [ + // { + // recipient: '9876543210', + // status: 200, + // result: 'SMS notification sent successfully', + // }, + // ], + // errors: [], + // }, + // email: { + // data: [ + // { + // recipient: "example@example.com", + // status: 200, + // result: 'Email notification sent successfully', + // }, + // ], + // errors: [], + // }, + // push: { + // data: [ + // { + // recipient: "fcmDeviceId123:APA91bGfEXAMPLEr123EXAMPLEoHuEXAMPLE", + // status: 200, + // result: 'Push notification sent successfully', + // }, + // ], + // errors: [], + // }, + // }); + // }); + + //test case for occure one sucess for one notification and error for another notification + it('should filter out channels with no data or errors in the final response', async () => { + const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + const notificationDto: NotificationDto = { + isQueue: false, + context: 'OTP', + key: 'Reset_OTP', + replacements: { + '{OTP}': '445566', + '{otpExpiry}': '2', + '{programName}': 'SCP', + '{username}': 'Tets' + }, + email: { + receipients: ["example@example.com"], + }, + push: { + receipients: [ + "fcmDeviceId123:APA91bGfEXAMPLEr123EXAMPLEoHuEXAMPLE" + ] + }, + sms: new SMSDTO, + }; + const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + return result; // Just return the result passed to json + }); + await service.sendNotification(notificationDto, userId, responseMock as Response); + const status = jest.spyOn(responseMock, 'status').mockReturnThis(); + const result = jsonSpy.mock.calls[0][0].result; + + expect(status).toHaveBeenCalledWith(HttpStatus.OK); + // Use toEqual or toMatchObject to verify the structure + expect(result).toEqual({ + email: { + data: [ + { + recipient: "example@example.com", + status: 200, + result: 'Email notification sent successfully', + }, + ], + errors: [], + }, + push: { + data: [ + ], + errors: [ + { + "error": "Template Config not found for this context: push", + "code": 400 + } + ], + }, + }); + }); + + //getting error need to resolve not able to send notification -> rabbitmq error + // it('should return appropriate response structure when isQueue is true', async () => { + // const userId = '6e828f36-0d53-4f62-b34c-8a94e165ceed'; + // const notificationDto: NotificationDto = { + // isQueue: true, + // context: 'TEST_CONTEXT', + // key: 'TEST_KEY', + // replacements: { + // "{learnerName}": "xyz" + // }, + // email: { + // receipients: ["example@example.com"], + // }, + // push: new PushDTO, + // sms: new SMSDTO + // }; + + // const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + // return result; // Just return the result passed to json + // }); + // await service.sendNotification(notificationDto, userId, responseMock as Response); + // const status = jest.spyOn(responseMock, 'status').mockReturnThis(); + // const result = jsonSpy.mock.calls[0][0].result; + // expect(status).toHaveBeenCalledWith(HttpStatus.OK); + // expect(result).toEqual({ + // email: { + // data: [ + // { + // status: 200, + // message: "Notification saved in queue successfully" + // } + // ], + // errors: [], + // }, + // }); + // }); +}); diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index 3a84660..7d76b28 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -4,7 +4,7 @@ import { Repository } from 'typeorm'; import axios from 'axios'; import { NotificationDto } from './dto/notificationDto.dto'; import { NotificationAdapterFactory } from './notificationadapters'; -import APIResponse from 'src/common/utils/response'; +import APIResponse from '../../common/utils/response'; // import * as FCM from 'fcm-node'; // import { SubscribeToDeviceTopicDto } from './dto/subscribtotopic.dto'; import { ConfigService } from '@nestjs/config'; @@ -16,9 +16,10 @@ import { NotificationActionTemplates } from '../notification_events/entity/notif import { NotificationQueue } from '../notification-queue/entities/notificationQueue.entity'; import { AmqpConnection, RabbitSubscribe } from '@nestjs-plus/rabbitmq'; import { NotificationQueueService } from '../notification-queue/notificationQueue.service'; -import { APIID } from 'src/common/utils/api-id.config'; -import { SUCCESS_MESSAGES, ERROR_MESSAGES } from 'src/common/utils/constant.util'; -import { LoggerUtil } from 'src/common/logger/LoggerUtil'; +import { APIID } from '../../common/utils/api-id.config'; +import { SUCCESS_MESSAGES, ERROR_MESSAGES } from '../../common/utils/constant.util'; +import { LoggerUtil } from '../../common/logger/LoggerUtil' +import { TypeormService } from '../typeorm/typeorm.service'; @Injectable() export class NotificationService { @@ -28,18 +29,19 @@ export class NotificationService { private readonly fcmurl; constructor( - @InjectRepository(NotificationLog) - private readonly notificationLogRepository: Repository, - @InjectRepository(NotificationActions) - private readonly notificationActions: Repository, - @InjectRepository(NotificationActionTemplates) - private readonly notificationActionTemplates: Repository, - @InjectRepository(NotificationQueue) - private readonly notificationQueue: Repository, + // @InjectRepository(NotificationLog) + // private readonly notificationLogRepository: Repository, + // @InjectRepository(NotificationActions) + // private readonly notificationActions: Repository, + // @InjectRepository(NotificationActionTemplates) + // private readonly notificationActionTemplates: Repository, + // @InjectRepository(NotificationQueue) + // private readonly notificationQueue: Repository, private readonly notificationQueueService: NotificationQueueService, private readonly adapterFactory: NotificationAdapterFactory, private readonly configService: ConfigService, private readonly amqpConnection: AmqpConnection, + private readonly typeormService: TypeormService ) { this.fcmkey = this.configService.get('FCM_KEY'); this.fcmurl = this.configService.get('FCM_URL') @@ -53,11 +55,10 @@ export class NotificationService { sms: { data: [], errors: [] }, push: { data: [], errors: [] }, }; - try { const { email, push, sms, context, replacements, key } = notificationDto; // Check if notification template exists - const notification_event = await this.notificationActions.findOne({ where: { context, key } }); + const notification_event = await this.typeormService.findOne(NotificationActions, { where: { context, key } }); if (!notification_event) { LoggerUtil.error(`template not found with this ${context} and ${key}`, ERROR_MESSAGES.TEMPLATE_NOTFOUND, apiId, userId); throw new BadRequestException(ERROR_MESSAGES.TEMPLATE_NOTFOUND); @@ -149,7 +150,7 @@ export class NotificationService { // Helper function to handle sending notifications for a specific channel async notificationHandler(channel, recipients, type, replacements, notificationDto, notification_event, userId) { if (recipients && recipients.length > 0 && Object.keys(recipients).length > 0) { - const notification_details = await this.notificationActionTemplates.find({ where: { actionId: notification_event.actionId, type } }); + const notification_details: any = await this.typeormService.find(NotificationActionTemplates, { where: { actionId: notification_event.actionId, type } }); if (notification_details.length === 0) { LoggerUtil.error(ERROR_MESSAGES.TEMPLATE_CONFIG_NOTFOUND, `/Send ${channel} Notification`, userId); throw new BadRequestException(`${ERROR_MESSAGES.TEMPLATE_CONFIG_NOTFOUND} ${type}`); @@ -238,11 +239,9 @@ export class NotificationService { } } - - //Provider which store in Queue async saveNotificationQueue(notificationDataArray) { - const arrayofResult = await this.notificationQueue.save(notificationDataArray); + const arrayofResult: any = await this.typeormService.save(NotificationQueue, notificationDataArray); if (arrayofResult) { if (this.amqpConnection) { try { @@ -360,7 +359,7 @@ export class NotificationService { async saveNotificationLogs(notificationLogs: NotificationLog) { try { - await this.notificationLogRepository.save(notificationLogs); + await this.typeormService.save(NotificationLog, notificationLogs); } catch (e) { LoggerUtil.error(`error ${e}`, ERROR_MESSAGES.NOTIFICATION_LOG_SAVE_FAILED); diff --git a/src/modules/notification_events/entity/notificationActionTemplates.entity.ts b/src/modules/notification_events/entity/notificationActionTemplates.entity.ts index 76b4e15..57520c8 100644 --- a/src/modules/notification_events/entity/notificationActionTemplates.entity.ts +++ b/src/modules/notification_events/entity/notificationActionTemplates.entity.ts @@ -10,10 +10,10 @@ export class NotificationActionTemplates { @Column() language: string; - @Column() + @Column({ nullable: false }) subject: string; - @Column() + @Column({ nullable: false }) body: string; @CreateDateColumn({ type: 'timestamp' }) @@ -38,7 +38,7 @@ export class NotificationActionTemplates { @Column({ type: 'uuid', nullable: true }) updatedBy: string; - @Column() + @Column({ nullable: false }) actionId: number; @ManyToOne(() => NotificationActions, template => template.templateconfig) diff --git a/src/modules/notification_events/notification_events.module.ts b/src/modules/notification_events/notification_events.module.ts index 6df46bd..0cdc6ca 100644 --- a/src/modules/notification_events/notification_events.module.ts +++ b/src/modules/notification_events/notification_events.module.ts @@ -4,13 +4,14 @@ import { NotificationEventsController } from './notification_events.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { NotificationActions } from './entity/notificationActions.entity'; import { NotificationActionTemplates } from './entity/notificationActionTemplates.entity'; +import { TypeormService } from '../typeorm/typeorm.service'; @Module( { imports: [ TypeOrmModule.forFeature([NotificationActions, NotificationActionTemplates]) ], - providers: [NotificationEventsService], + providers: [NotificationEventsService, TypeormService], controllers: [NotificationEventsController], exports: [NotificationEventsService] }) diff --git a/src/modules/notification_events/notification_events.service.spec.ts b/src/modules/notification_events/notification_events.service.spec.ts new file mode 100644 index 0000000..91a1243 --- /dev/null +++ b/src/modules/notification_events/notification_events.service.spec.ts @@ -0,0 +1,187 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotificationEventsService } from './notification_events.service'; +import { TypeormService } from '../typeorm/typeorm.service'; +import { Response } from 'express'; +import { NotificationActions } from './entity/notificationActions.entity'; +import * as dotenv from 'dotenv'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule } from '@nestjs/config'; +import { NotificationActionTemplates } from './entity/notificationActionTemplates.entity'; +import { EntityManager } from 'typeorm'; +import { BadRequestException, HttpStatus, NotFoundException } from '@nestjs/common'; +import { SearchFilterDto } from './dto/searchTemplateType.dto'; +import { CreateEventDto } from './dto/createTemplate.dto'; +import { UpdateEventDto } from './dto/updateEventTemplate.dto'; +dotenv.config(); + + +describe('NotificationEventsService', () => { + let createdTemplateId: number; + let service: NotificationEventsService; + let req: Request; + let responseMock: Partial; + + beforeEach(async () => { + responseMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), // Ensure ConfigModule is loaded globally in tests + TypeOrmModule.forRootAsync({ + useFactory: async () => ({ + type: "postgres", + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT, 10), + username: process.env.POSTGRES_USERNAME, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DATABASE, + entities: [NotificationActions, NotificationActionTemplates], + synchronize: false, // Auto synchronize (use cautiously in production) + }), + }), + TypeOrmModule.forFeature([NotificationActionTemplates, NotificationActions]), // Register your repositories + + ], + providers: [NotificationEventsService, TypeormService, EntityManager], + }).compile(); + + service = module.get(NotificationEventsService); + const typeormService = module.get(TypeormService); + // Explicitly initialize TypeormService + await typeormService.initialize(); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('NotificationTemplate', () => { + // create if not exist template - Success + it('should successfully create a new template', async () => { + const userId = '3d7f0cb6-0dbd-4ae2-b937-61b9708baa0c'; + const data: CreateEventDto = { + title: 'Test Template', + key: 'TEST_KEY', + status: 'ACTIVE', + context: 'TEST_CONTEXT', + replacementTags: [ + { name: 'campaign.first_name', description: 'Name of Campaign Promoter' }, + { name: 'campaign.last_name', description: 'Last Name of Campaign Promoter' }, + ], + email: { subject: 'Test Email Subject', body: 'Test Email Body' }, + push: { subject: 'Test Push Subject', body: 'Test Push Body', image: 'image.png', link: 'http://example.com' }, + sms: { subject: 'Test SMS Subject', body: 'Test SMS Body' }, + }; + const createdTemplate = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + return result; // Just return the result passed to json + }); + await service.createTemplate(userId, data, responseMock as Response); + const result = createdTemplate.mock.calls[0][0].result; + createdTemplateId = result.actionId; + expect(responseMock.status).toHaveBeenCalledWith(HttpStatus.CREATED); + }); + + //error alrady exist tempate + it('should throw an error if the template already exists', async () => { + const userId = '3d7f0cb6-0dbd-4ae2-b937-61b9708baa0c'; + const data: CreateEventDto = { + title: 'Test Template', + key: 'TEST_KEY', + status: 'ACTIVE', + context: 'TEST_CONTEXT', + replacementTags: [ + { name: 'campaign.first_name', description: 'Name of Campaign Promoter' }, + { name: 'campaign.last_name', description: 'Last Name of Campaign Promoter' }, + ], + email: { subject: 'Test Email Subject', body: 'Test Email Body' }, + push: { subject: 'Test Push Subject', body: 'Test Push Body', image: 'image.png', link: 'http://example.com' }, + sms: { subject: 'Test SMS Subject', body: 'Test SMS Body' }, + }; + await expect(service.createTemplate(userId, data, responseMock as Response)).rejects.toThrow( + new NotFoundException('Template already exist') + ); + }); + + // //Get API sucesss + it("should fetch template", async () => { + const searchFilterDto: SearchFilterDto = { + filters: { + context: 'TEST_CONTEXT', + key: '' + } + }; + const userId = '9991759f-e829-46f7-9350-68e11c8af2f2'; + + const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + return result; // Just return the result passed to json + }); + await service.getTemplates(searchFilterDto, userId, responseMock as Response); + const result = jsonSpy.mock.calls[0][0].result; // Get the result array from the json response + expect(result.length).toBeGreaterThan(0); // Pass if result length is greater than 0 + }); + + it('should delete if the template exist', async () => { + const jsonSpy = jest.spyOn(responseMock, 'json').mockImplementation((result) => { + return result; // Just return the result passed to json + }); + await service.deleteTemplate(createdTemplateId, '3d7f0cb6-0dbd-4ae2-b937-61b9708baa0c', responseMock as Response) + // Extract the JSON response + const jsonResult = jsonSpy.mock.calls[0][0]; // Get the first call's first argument + expect(jsonResult.result).toEqual({ + id: createdTemplateId, + }); + }); + + //getting error if template is not found -> delete the template + it('should throw a NotFoundException if the template does not exist', async () => { + await expect( + service.deleteTemplate(createdTemplateId, '3d7f0cb6-0dbd-4ae2-b937-61b9708baa0c', responseMock as Response) + ).rejects.toThrow(new NotFoundException(`No template id found: ${createdTemplateId}`)); + }); + + //update API -> pending to run + //error -> not exist acrtion id + // it('should successfully update an existing template', async () => { + // const userId = '3d7f0cb6-0dbd-4ae2-b937-61b9708baa0c'; + // const actionId = 1; + // const updateEventDto: UpdateEventDto = { + // title: 'Updated Template Title', + // key: 'UPDATED_KEY', + // status: 'INACTIVE', + // email: { subject: 'Updated Email Subject', body: 'Updated Email Body' }, + // // push: { subject: 'Updated Push Subject', body: 'Updated Push Body', image: 'new-image.png', link: 'http://newlink.com' }, + // // sms: { subject: 'Updated SMS Subject', body: 'Updated SMS Body' }, + // updatedBy: '', + // replacementTags: [] + // }; + // // Execute the method + // await service.updateNotificationTemplate(actionId, updateEventDto, userId, responseMock as Response); + // // Assertions + // expect(responseMock.status).toHaveBeenCalledWith(HttpStatus.OK); + // }); + + //sucess mean existing actionId -> Getting error now + // it('should successfully update an existing template', async () => { + // const userId = '3d7f0cb6-0dbd-4ae2-b937-61b9708baa0c'; + // const actionId = 75; + // const updateEventDto: UpdateEventDto = { + // title: 'Updated Template', + // status: 'INACTIVE', + // email: { subject: 'Updated Email Subject1', body: 'Updated Email Body' }, + // updatedBy: '', + // replacementTags: [], + // key: '' + // }; + // // Execute the method + // await service.updateNotificationTemplate(actionId, updateEventDto, userId, responseMock as Response); + // // Assertions + // expect(responseMock.status).toHaveBeenCalledWith(HttpStatus.OK); + // }); + }); +}); diff --git a/src/modules/notification_events/notification_events.service.ts b/src/modules/notification_events/notification_events.service.ts index b5c2e2b..80cb1d9 100644 --- a/src/modules/notification_events/notification_events.service.ts +++ b/src/modules/notification_events/notification_events.service.ts @@ -3,14 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { NotificationActions } from './entity/notificationActions.entity'; import { SearchFilterDto } from './dto/searchTemplateType.dto'; -import APIResponse from 'src/common/utils/response'; +import APIResponse from '../../common/utils/response'; import { Response } from 'express'; import { CreateEventDto } from './dto/createTemplate.dto'; import { NotificationActionTemplates } from './entity/notificationActionTemplates.entity'; import { UpdateEventDto } from './dto/updateEventTemplate.dto'; -import { APIID } from 'src/common/utils/api-id.config'; -import { ERROR_MESSAGES, SUCCESS_MESSAGES } from 'src/common/utils/constant.util'; -import { LoggerUtil } from 'src/common/logger/LoggerUtil'; +import { APIID } from '../..//common/utils/api-id.config'; +import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../../common/utils/constant.util'; +import { LoggerUtil } from '../../common/logger/LoggerUtil'; +import { TypeormService } from '../typeorm/typeorm.service'; @Injectable() export class NotificationEventsService { @@ -18,13 +19,14 @@ export class NotificationEventsService { @InjectRepository(NotificationActions) private notificationTemplatesRepository: Repository, @InjectRepository(NotificationActionTemplates) - private notificationTemplateConfigRepository: Repository + private notificationTemplateConfigRepository: Repository, + private typeormService: TypeormService ) { } async createTemplate(userId: string, data: CreateEventDto, response: Response): Promise { const apiId = APIID.TEMPLATE_CREATE; const existingTemplate = - await this.notificationTemplatesRepository.findOne({ + await this.typeormService.findOne(NotificationActions, { where: { context: data.context, key: data.key }, }); if (existingTemplate) { @@ -40,7 +42,7 @@ export class NotificationEventsService { notificationTemplate.replacementTags = data.replacementTags; notificationTemplate.createdBy = userId; notificationTemplate.updatedBy = userId; - const notificationTemplateResult = await this.notificationTemplatesRepository.save(notificationTemplate); + const notificationTemplateResult: NotificationActions = await this.typeormService.save(NotificationActions, notificationTemplate); // create config details const createConfig = async (type: string, configData: any) => { const templateConfig = new NotificationActionTemplates(); @@ -56,7 +58,7 @@ export class NotificationEventsService { image: type === 'push' ? configData?.image || null : null, link: type === 'push' ? configData?.link || null : null }); - return await this.notificationTemplateConfigRepository.save(templateConfig); + return await this.typeormService.save(NotificationActionTemplates, templateConfig); }; if (data.email && Object.keys(data.email).length > 0) { @@ -85,7 +87,7 @@ export class NotificationEventsService { const apiId = APIID.TEMPLATE_UPDATE; updateEventDto.updatedBy = userId; //check actionId exist or not - const existingTemplate = await this.notificationTemplatesRepository.findOne({ + const existingTemplate = await this.typeormService.findOne(NotificationActions, { where: { actionId: id }, }); if (!existingTemplate) { @@ -95,7 +97,7 @@ export class NotificationEventsService { //check key already exist for this context if (updateEventDto.key) { const checkKeyAlreadyExist = - await this.notificationTemplatesRepository.findOne({ + await this.typeormService.findOne(NotificationActions, { where: { context: existingTemplate.context, key: updateEventDto.key }, }); if (checkKeyAlreadyExist) { @@ -106,22 +108,23 @@ export class NotificationEventsService { Object.assign(existingTemplate, updateEventDto); - const result = await this.notificationTemplatesRepository.save(existingTemplate); + const result: NotificationActions = await this.typeormService.save(NotificationActions, existingTemplate); + const createConfig = async (type: string, configData?: any) => { if (configData && Object.keys(configData).length > 0) { - let existingConfig = await this.notificationTemplateConfigRepository.findOne({ + let existingConfig = await this.typeormService.findOne(NotificationActionTemplates, { where: { actionId: id, type: type }, }); if (existingConfig) { Object.assign(existingConfig, configData); existingConfig.updatedBy = userId - return await this.notificationTemplateConfigRepository.save(existingConfig); + return await this.typeormService.save(NotificationActionTemplates, existingConfig); } else { if (!configData.subject || !configData.body) { throw new BadRequestException(ERROR_MESSAGES.NOT_EMPTY_SUBJECT_OR_BODY); } - const newConfig = this.notificationTemplateConfigRepository.create({ + const newConfig = { actionId: id, type: type, subject: configData.subject, @@ -132,8 +135,8 @@ export class NotificationEventsService { createdBy: userId, // getting null constraint error image: configData.image || null, link: configData.link || null - }); - return await this.notificationTemplateConfigRepository.save(newConfig); + }; + return await this.typeormService.save(NotificationActionTemplates, newConfig); } } }; @@ -150,13 +153,13 @@ export class NotificationEventsService { } if (updateEventDto.status) { - let existingConfig = await this.notificationTemplateConfigRepository.find({ + let existingConfig = await this.typeormService.find(NotificationActionTemplates, { where: { actionId: id }, }); existingConfig.forEach(async (config) => { if (updateEventDto.status) { config.status = updateEventDto.status; - await this.notificationTemplateConfigRepository.save(config); + await this.typeormService.save(NotificationActionTemplates, config); } }); } @@ -175,7 +178,7 @@ export class NotificationEventsService { if (key) { whereCondition.key = key; } - const result = await this.notificationTemplatesRepository.find({ + const result = await this.typeormService.find(NotificationActions, { where: whereCondition, relations: ["templateconfig"], }); @@ -200,15 +203,17 @@ export class NotificationEventsService { async deleteTemplate(actionId: number, userId: string, response: Response) { const apiId = APIID.TEMPLATE_DELETE; - const templateId = await this.notificationTemplatesRepository.findOne({ where: { actionId } }); + const templateId = await this.typeormService.findOne(NotificationActions, { + where: { actionId } + }); if (!templateId) { LoggerUtil.error(ERROR_MESSAGES.TEMPLATE_NOT_EXIST, ERROR_MESSAGES.NOT_FOUND, apiId, userId); throw new NotFoundException(ERROR_MESSAGES.TEMPLATE_ID_NOTFOUND(actionId)) } - const deleteTemplate = await this.notificationTemplatesRepository.delete({ actionId }); - if (deleteTemplate.affected !== 1) { - throw new BadRequestException(ERROR_MESSAGES.TEMPLATE_NOT_DELETED); - } + const deleteTemplate = await this.typeormService.delete(NotificationActions, actionId); + // if (deleteTemplate.affected !== 1) { + // throw new BadRequestException(ERROR_MESSAGES.TEMPLATE_NOT_DELETED); + // } LoggerUtil.log(SUCCESS_MESSAGES.DELETE_TEMPLATE(userId), apiId, userId); return response .status(HttpStatus.OK) diff --git a/src/modules/typeorm/typeorm.module.ts b/src/modules/typeorm/typeorm.module.ts new file mode 100644 index 0000000..5f0729b --- /dev/null +++ b/src/modules/typeorm/typeorm.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeormService } from './typeorm.service'; +import { ConfigService } from '@nestjs/config'; + + +@Module({ + providers: [TypeormService, ConfigService], + exports: [TypeormService, ConfigService], // Ensure that TypeormService is exported +}) +export class TypeormModule { + +} diff --git a/src/modules/typeorm/typeorm.service.spec.ts b/src/modules/typeorm/typeorm.service.spec.ts new file mode 100644 index 0000000..f410604 --- /dev/null +++ b/src/modules/typeorm/typeorm.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TypeormService } from './typeorm.service'; + +describe('TypeormService', () => { + let service: TypeormService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TypeormService], + }).compile(); + + service = module.get(TypeormService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/typeorm/typeorm.service.ts b/src/modules/typeorm/typeorm.service.ts new file mode 100644 index 0000000..576eb9a --- /dev/null +++ b/src/modules/typeorm/typeorm.service.ts @@ -0,0 +1,143 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DataSource, EntityManager, EntityTarget, Repository } from 'typeorm'; +import { NotificationActions } from '../notification_events/entity/notificationActions.entity'; +import { NotificationActionTemplates } from '../notification_events/entity/notificationActionTemplates.entity'; +import { NotificationLog } from '../notification/entity/notificationLogs.entity'; +import { NotificationQueue } from '../notification-queue/entities/notificationQueue.entity'; + + +@Injectable() +export class TypeormService { + private dataSource: DataSource; + private entityManager: EntityManager; + + constructor(private configService: ConfigService) { + this.initialize(); + } + + async initialize() { + try { + // Create DataSource instance + this.dataSource = new DataSource({ + type: "postgres", + host: this.configService.get("POSTGRES_HOST"), + port: this.configService.get("POSTGRES_PORT"), + database: this.configService.get("POSTGRES_DATABASE"), + username: this.configService.get("POSTGRES_USERNAME"), + password: this.configService.get("POSTGRES_PASSWORD"), + entities: [NotificationActions, NotificationActionTemplates, NotificationLog, NotificationQueue], // Add your entities here, e.g., [User, Product] + // synchronize: true, // For development only, set to false in production + }); + + // Initialize DataSource + await this.dataSource.initialize(); + // After DataSource is initialized, set the entity manager + this.entityManager = this.dataSource.manager; + // console.log("DataSource initialized and EntityManager set."); + } catch (error) { + // console.error("Error initializing DataSource:", error); + throw new Error("TypeORM DataSource initialization failed."); + } + } + + // Now you can use this.entityManager to access repositories + private getRepository(entity: EntityTarget): Repository { + if (!this.entityManager) { + throw new Error("EntityManager is not initialized."); + } + return this.entityManager.getRepository(entity); + } + + // Find all records or by custom condition + async find(entity: EntityTarget, options?: object): Promise { + try { + const repository = this.getRepository(entity); + + if (!repository) { + throw new Error( + "Repository is not available or not properly initialized." + ); + } + return repository.find(options); + } catch (err) { + throw new Error(err); + } + } + + // Find one record by conditions + async findOne( + entity: EntityTarget, + conditions: object + ): Promise { + try { + const repository = this.getRepository(entity); + return repository.findOne(conditions); + } catch (err) { + throw new Error(err); + } + } + + // Save a new entity or update existing + async save(entity, createData): Promise { + try { + console.log(entity, "entity"); + + const repository = this.getRepository(entity); + return repository.save(createData); + } catch (err) { + throw new Error(err); + } + } + + async create(entity, createData): Promise { + try { + const repository = this.getRepository(entity); + const newEntity = repository.create(createData); // Ensure createData is an object, not an array + return newEntity as T; // Explicitly cast to type T + } catch (err) { + throw new Error(`Error creating entity: ${err.message}`); + } + } + + + // Update an entity + async update(entity, id: number, updateData): Promise { + try { + const repository = this.getRepository(entity); + await repository.update(id, updateData); + return this.findOne(entity, { where: { id } }); + } catch (err) { + throw new Error(err); + } + } + + // Remove an entity + async delete(entity, actionId: number): Promise { + try { + const repository = this.getRepository(entity); + const entityToRemove = await this.findOne(entity, { where: { actionId } }); + if (!entityToRemove) { + throw new Error(`${entity.constructor.name} not found`); + } + await repository.remove(entityToRemove); + } catch (err) { + throw new Error(err); + } + } + + async query(entity, query: string, parameters?: any[]): Promise { + try { + // Get the repository for the given entity + const repository: Repository = this.getRepository(entity); + + // Execute the raw query + const result = await repository.query(query, parameters); + + // Return the result (could be rows, status, etc., depending on the query) + return result; + } catch (err) { + throw new Error(err); + } + } +}