diff --git a/src/common/logger/LoggerUtil.ts b/src/common/logger/LoggerUtil.ts index a1096ea..9db4454 100644 --- a/src/common/logger/LoggerUtil.ts +++ b/src/common/logger/LoggerUtil.ts @@ -1,12 +1,17 @@ import * as winston from 'winston'; +type Metadata = { + traceId: string; + status?: string; + [key: string]: any; +} export class LoggerUtil { private static logger: winston.Logger; static getLogger() { if (!this.logger) { const customFormat = winston.format.printf( - ({ timestamp, level, message, context, user, error }) => { + ({ timestamp, level, message, context, user, error, metadata }) => { return JSON.stringify({ timestamp: timestamp, context: context, @@ -14,6 +19,7 @@ export class LoggerUtil { level: level, message: message, error: error, + metadata: metadata, }); }, ); @@ -35,6 +41,7 @@ export class LoggerUtil { context?: string, user?: string, level: string = 'info', + metadata?: Metadata, ) { this.getLogger().log({ level: level, @@ -42,6 +49,7 @@ export class LoggerUtil { context: context, user: user, timestamp: new Date().toISOString(), + metadata: metadata, }); } @@ -50,6 +58,7 @@ export class LoggerUtil { error?: string, context?: string, user?: string, + metadata?: Metadata, ) { this.getLogger().error({ message: message, @@ -57,6 +66,7 @@ export class LoggerUtil { context: context, user: user, timestamp: new Date().toISOString(), + metadata: metadata, }); } diff --git a/src/modules/notification/adapters/emailService.adapter.ts b/src/modules/notification/adapters/emailService.adapter.ts index 09427d8..2432915 100644 --- a/src/modules/notification/adapters/emailService.adapter.ts +++ b/src/modules/notification/adapters/emailService.adapter.ts @@ -7,7 +7,7 @@ import { Repository } from "typeorm"; import { NotificationActionTemplates } from "src/modules/notification_events/entity/notificationActionTemplates.entity"; import NotifmeSdk from 'notifme-sdk'; import { NotificationLog } from "../entity/notificationLogs.entity"; -import { NotificationService } from "../notification.service"; +import { NotificationService, maskEmail } from "../notification.service"; import { LoggerUtil } from "src/common/logger/LoggerUtil"; import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util"; @@ -15,13 +15,13 @@ import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util * Interface for raw email data */ export interface RawEmailData { - to: string | string[]; - subject: string; - body: string; - from?: string; - isHtml?: boolean; - cc?: string[]; - bcc?: string[]; + to: string | string[]; + subject: string; + body: string; + from?: string; + isHtml?: boolean; + cc?: string[]; + bcc?: string[]; } @Injectable() @@ -35,35 +35,61 @@ export class EmailAdapter implements NotificationServiceInterface { * @param notificationDataArray Array of notification data objects * @returns Results of notification attempts */ - async sendNotification(notificationDataArray) { + async sendNotification(notificationDataArray, traceId?: string) { const results = []; for (const notificationData of notificationDataArray) { + try { const recipient = notificationData.recipient; if (!recipient || !this.isValidEmail(recipient)) { throw new BadRequestException(ERROR_MESSAGES.INVALID_EMAIL); } - const result = await this.send(notificationData); + + const loggingData = { ...notificationData }; + if (loggingData.recipient && typeof loggingData.recipient === 'string') { + loggingData.recipient = maskEmail(loggingData.recipient); + } + if (loggingData.cc && Array.isArray(loggingData.cc)) { + loggingData.cc = loggingData.cc.map((email: string) => maskEmail(email)); + } + if (loggingData.bcc && Array.isArray(loggingData.bcc)) { + loggingData.bcc = loggingData.bcc.map((email: string) => maskEmail(email)); + } + delete loggingData.body; + if (loggingData.replacements && loggingData.replacements['{OTP}']) { + loggingData.replacements = { ...loggingData.replacements }; + delete loggingData.replacements['{OTP}']; + } + + LoggerUtil.log(`ADAPTER_PREP ${traceId}`, traceId, '', 'info', { ...loggingData, status: 'ADAPTER_PREP', traceId: traceId }); + + const startTime = Date.now(); + const result = await this.send(notificationData, traceId); + const timeTakenInMs = Date.now() - startTime; + if (result.status === 'success') { + LoggerUtil.log(`SENT ${traceId}`, traceId, '', 'info', { status: 'SENT', traceId: traceId, timeTaken: timeTakenInMs }); results.push({ recipient: recipient, status: 200, result: SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY }); } else { + LoggerUtil.error(`FAILED ${traceId}`, result.message, traceId, '', { status: 'FAILED', traceId: traceId, timeTaken: timeTakenInMs }); results.push({ recipient: recipient, status: 'error', - error: `Email not sent: ${JSON.stringify(result.errors)}` + error: `Email not sent: ${JSON.stringify(result.message)}` }); } } catch (error) { - LoggerUtil.error(ERROR_MESSAGES.EMAIL_NOTIFICATION_FAILED, error); + const timeTakenInMs = Date.now() - (error.startTime || Date.now()); + LoggerUtil.error(`FAILED ${traceId}`, error.message, traceId, '', { status: 'FAILED', traceId: traceId, timeTaken: timeTakenInMs }); results.push({ recipient: notificationData.recipient, status: 'error', - error: error.toString() + error: error.message }); } } @@ -75,49 +101,77 @@ export class EmailAdapter implements NotificationServiceInterface { * @param rawEmailDataArray Array of raw email data objects * @returns Results of raw email sending attempts */ - async sendRawEmails(emailData) { + async sendRawEmails(traceId, emailData) { const results = []; - + // Convert to array if not already an array const emailDataArray = Array.isArray(emailData) ? emailData : [emailData]; - + for (const singleEmailData of emailDataArray) { - try { - if (!singleEmailData.to || !this.isValidEmail(singleEmailData.to)) { - throw new BadRequestException(ERROR_MESSAGES.INVALID_EMAIL); - } - - if (!singleEmailData.subject || !singleEmailData.body) { - throw new BadRequestException("Subject and Email body are required"); + try { + + if (!singleEmailData.to || !this.isValidEmail(singleEmailData.to)) { + throw new BadRequestException(ERROR_MESSAGES.INVALID_EMAIL); + } + + if (!singleEmailData.subject || !singleEmailData.body) { + throw new BadRequestException("Subject and Email body are required"); + } + + const loggingData = { ...singleEmailData }; + if (loggingData.to) { + if (Array.isArray(loggingData.to)) { + loggingData.to = loggingData.to.map((email: string) => maskEmail(email)); + } else if (typeof loggingData.to === 'string') { + loggingData.to = maskEmail(loggingData.to as string); + } + } + if (loggingData.cc && Array.isArray(loggingData.cc)) { + loggingData.cc = loggingData.cc.map((email: string) => maskEmail(email)); + } + if (loggingData.bcc && Array.isArray(loggingData.bcc)) { + loggingData.bcc = loggingData.bcc.map((email: string) => maskEmail(email)); + } + delete loggingData.body; + if (loggingData.replacements && loggingData.replacements['{OTP}']) { + loggingData.replacements = { ...loggingData.replacements }; + delete loggingData.replacements['{OTP}']; + } + LoggerUtil.log(`ADAPTER_PREP ${traceId}`, traceId, '', 'info', { ...loggingData, status: 'ADAPTER_PREP', traceId: traceId }); + + const startTime = Date.now(); + const result = await this.sendRawEmail(singleEmailData); + const timeTakenInMs = Date.now() - startTime; + + if (result.status === 'success') { + LoggerUtil.log(`SENT ${traceId}`, traceId, '', 'info', { status: 'SENT', traceId: traceId, timeTaken: timeTakenInMs }); + results.push({ + to: singleEmailData.to, + status: 200, + result: SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY, + messageId: result.messageId || `email-${Date.now()}` + }); + } else { + LoggerUtil.error(`FAILED ${traceId}`, JSON.stringify(result), traceId, '', { status: 'FAILED', traceId: traceId, timeTaken: timeTakenInMs }); + results.push({ + to: singleEmailData.to, + status: 400, + error: `Email not sent: ${JSON.stringify(result.errors)}` + }); + } } - - const result = await this.sendRawEmail(singleEmailData); - if (result.status === 'success') { - results.push({ - to: singleEmailData.to, - status: 200, - result: SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY, - messageId: result.messageId || `email-${Date.now()}` - }); - } else { - results.push({ - to: singleEmailData.to, - status: 400, - error: `Email not sent: ${JSON.stringify(result.errors)}` - }); + catch (error) { + const timeTakenInMs = Date.now() - (error.startTime || Date.now()); + LoggerUtil.error(`FAILED ${traceId}`, error.message || error.toString(), traceId, '', { status: 'FAILED', traceId: traceId, timeTaken: timeTakenInMs }); + results.push({ + recipient: singleEmailData.to, + status: 500, + error: error.message || error.toString() + }); } - } - catch (error) { - LoggerUtil.error(ERROR_MESSAGES.EMAIL_NOTIFICATION_FAILED, error); - results.push({ - recipient: singleEmailData.to, - status: 500, - error: error.message || error.toString() - }); - } } return results; - } + } /** * Creates a notification log entry @@ -186,14 +240,14 @@ export class EmailAdapter implements NotificationServiceInterface { /** * Sends template-based email */ - async send(notificationData) { + async send(notificationData, traceId) { // Note: CC and BCC are not logged in notificationLogs for privacy/security reasons // The NotificationLog entity doesn't have CC/BCC fields, and BCC is meant to be hidden const notificationLogs = this.createNotificationLog(notificationData, notificationData.subject, notificationData.key, notificationData.body, notificationData.recipient); try { const emailConfig = this.getEmailConfig(notificationData.context); const notifmeSdk = new NotifmeSdk(emailConfig); - + const result = await notifmeSdk.send({ email: { from: emailConfig.email.from, @@ -207,7 +261,7 @@ export class EmailAdapter implements NotificationServiceInterface { if (result.status === 'success') { notificationLogs.status = true; await this.notificationServices.saveNotificationLogs(notificationLogs); - LoggerUtil.log(SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY); + LoggerUtil.log(`traceId: ${traceId} SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY`, traceId, "", "info",); return result; } else { @@ -215,9 +269,9 @@ export class EmailAdapter implements NotificationServiceInterface { } } catch (e) { - LoggerUtil.error(ERROR_MESSAGES.EMAIL_NOTIFICATION_FAILED, e); + LoggerUtil.error(ERROR_MESSAGES.EMAIL_NOTIFICATION_FAILED, e.message); notificationLogs.status = false; - notificationLogs.error = e.toString(); + notificationLogs.error = e.message; await this.notificationServices.saveNotificationLogs(notificationLogs); return e; } @@ -236,11 +290,11 @@ export class EmailAdapter implements NotificationServiceInterface { emailData.body, emailData.to as string ); - + try { const emailConfig = this.getEmailConfig('raw-email'); const notifmeSdk = new NotifmeSdk(emailConfig); - + const result = await notifmeSdk.send({ email: { from: emailData.from || emailConfig.email.from, @@ -251,14 +305,14 @@ export class EmailAdapter implements NotificationServiceInterface { ...(emailData.bcc && Array.isArray(emailData.bcc) && emailData.bcc.length > 0 ? { bcc: emailData.bcc } : {}), }, }); - + if (result.status === 'success') { notificationLogs.status = true; await this.notificationServices.saveNotificationLogs(notificationLogs); LoggerUtil.log(SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY); return { ...result, - messageId: result.id || `email-${Date.now()}` + messageId: result.id || `email - ${Date.now()}` }; } else { throw new Error(`Email not sent: ${JSON.stringify(result.errors)}`) diff --git a/src/modules/notification/adapters/smsService.adapter.ts b/src/modules/notification/adapters/smsService.adapter.ts index 10e65c3..2f689ab 100644 --- a/src/modules/notification/adapters/smsService.adapter.ts +++ b/src/modules/notification/adapters/smsService.adapter.ts @@ -7,7 +7,7 @@ import { Repository } from "typeorm"; import { NotificationActionTemplates } from "src/modules/notification_events/entity/notificationActionTemplates.entity"; import { ConfigService } from "@nestjs/config"; import { NotificationLog } from "../entity/notificationLogs.entity"; -import { NotificationService } from "../notification.service"; +import { NotificationService, maskPhone } from "../notification.service"; import { LoggerUtil } from "src/common/logger/LoggerUtil"; import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util"; import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"; @@ -21,7 +21,7 @@ export interface RawSmsData { from?: string; templateId?: string; replacements?: { [key: string]: string }; - } +} @Injectable() export class SmsAdapter implements NotificationServiceInterface { @@ -76,7 +76,7 @@ export class SmsAdapter implements NotificationServiceInterface { } } - async sendNotification(notificationDataArray) { + async sendNotification(notificationDataArray, traceId) { const results = []; for (const notificationData of notificationDataArray) { try { @@ -94,7 +94,23 @@ export class SmsAdapter implements NotificationServiceInterface { replacements: notificationData.replacements, }; + const loggingData = { ...smsNotificationDto, smsProvider: this.smsProvider }; + if (loggingData.recipient) { + loggingData.recipient = maskPhone(String(loggingData.recipient)); + } + delete loggingData.body; + if (loggingData.replacements && loggingData.replacements['{OTP}']) { + loggingData.replacements = { ...loggingData.replacements }; + delete loggingData.replacements['{OTP}']; + } + + LoggerUtil.log(`ADAPTER_PREP`, traceId, '', 'info', { ...loggingData, traceId: traceId, status: 'ADAPTER_PREP' }); + + const startTime = Date.now(); const result = await this.send(smsNotificationDto); + const timeTakenInMs = Date.now() - startTime; + + LoggerUtil.log(`SENT`, '', '', 'info', { traceId: traceId, status: 'SENT', timeTaken: timeTakenInMs }); results.push({ recipient: recipient, @@ -102,11 +118,12 @@ export class SmsAdapter implements NotificationServiceInterface { result: SUCCESS_MESSAGES.SMS_NOTIFICATION_SEND_SUCCESSFULLY, }); } catch (error) { - LoggerUtil.error('Failed to send SMS notification', error); + const timeTakenInMs = Date.now() - (error.startTime || Date.now()); + LoggerUtil.error(`FAILED`, error.message, '', 'info', { error: error.message, traceId: traceId, status: 'FAILED', timeTaken: timeTakenInMs }); results.push({ recipient: notificationData.recipient, status: 'error', - error: `SMS not sent: ${JSON.stringify(error)}`, + error: `SMS not sent: ${error.message}`, }); } } @@ -144,13 +161,13 @@ export class SmsAdapter implements NotificationServiceInterface { // Don't modify the original replacements object, create a copy const processedReplacements = notificationData.replacements ? { ...notificationData.replacements } : {}; createReplacementsForMsg91(processedReplacements); - + // Create a new notification data object with processed replacements const msg91NotificationData = { ...notificationData, replacements: processedReplacements }; - + response = await this.sendViaMsg91(msg91NotificationData); } LoggerUtil.log(SUCCESS_MESSAGES.SMS_NOTIFICATION_SEND_SUCCESSFULLY); @@ -158,9 +175,9 @@ export class SmsAdapter implements NotificationServiceInterface { await this.notificationServices.saveNotificationLogs(notificationLogs); return response; } catch (error) { - LoggerUtil.error(ERROR_MESSAGES.SMS_NOTIFICATION_FAILED, error); + LoggerUtil.error(ERROR_MESSAGES.SMS_NOTIFICATION_FAILED, error.message); notificationLogs.status = false; - notificationLogs.error = error.toString(); + notificationLogs.error = error.message; await this.notificationServices.saveNotificationLogs(notificationLogs); throw error; } @@ -205,10 +222,10 @@ export class SmsAdapter implements NotificationServiceInterface { notificationLog.action = 'send-raw-sms'; notificationLog.type = 'sms'; notificationLog.recipient = smsData.to; - + try { let response; - + // Prepare notification data with replacements if provided const notificationData = { recipient: smsData.to, @@ -216,7 +233,7 @@ export class SmsAdapter implements NotificationServiceInterface { replacements: smsData.replacements || {}, templateId: smsData.templateId, }; - + if (this.smsProvider === SMS_PROVIDER.TWILIO) { response = await this.sendViaTwilio(notificationData); } else if (this.smsProvider === SMS_PROVIDER.AWS_SNS) { @@ -227,7 +244,7 @@ export class SmsAdapter implements NotificationServiceInterface { } response = await this.sendViaMsg91(notificationData, smsData.templateId); } - + notificationLog.status = true; await this.notificationServices.saveNotificationLogs(notificationLog); return response; @@ -241,59 +258,65 @@ export class SmsAdapter implements NotificationServiceInterface { } } - async sendRawSmsMessages(smsData) { + async sendRawSmsMessages(traceId, smsData) { const results = []; - + // Convert to array if not already an array const smsDataArray = Array.isArray(smsData) ? smsData : [smsData]; - + for (const singleSmsData of smsDataArray) { - try { - // Validate based on provider - if (this.smsProvider === SMS_PROVIDER.MSG_91) { - // For MSG91, templateId is required, body is optional - if (!singleSmsData.templateId) { - throw new BadRequestException("templateId is required for MSG91"); - } - } else { - // For other providers, body is required - if (!singleSmsData.body) { - throw new BadRequestException("SMS body is required"); - } + try { + // Validate based on provider + if (this.smsProvider === SMS_PROVIDER.MSG_91) { + // For MSG91, templateId is required, body is optional + if (!singleSmsData.templateId) { + throw new BadRequestException("templateId is required for MSG91"); + } + } else { + // For other providers, body is required + if (!singleSmsData.body) { + throw new BadRequestException("SMS body is required"); + } + } + LoggerUtil.log(`ADAPTER_PREP`, 'info', undefined, 'info', { ...singleSmsData, traceId: traceId, status: 'ADAPTER_PREP' }); + + const startTime = Date.now(); + const result = await this.sendRawSms(singleSmsData); + const timeTakenInMs = Date.now() - startTime; + + if (result?.$metadata?.httpStatusCode === 200 && result?.MessageId) { + LoggerUtil.log(`SENT`, 'info', undefined, 'info', { ...result, traceId: traceId, status: 'SENT', timeTaken: timeTakenInMs }); + results.push({ + to: singleSmsData.to, + status: 200, + result: SUCCESS_MESSAGES.SMS_NOTIFICATION_SEND_SUCCESSFULLY, + messageId: result.MessageId || `sms-${Date.now()}` + }); + } else { + LoggerUtil.log(`FAILED`, 'info', undefined, 'info', { ...result, traceId: traceId, status: 'FAILED', timeTaken: timeTakenInMs }); + // Safe stringification - only include serializable data + const safeResult = { + data: result?.data, + status: result?.status + }; + results.push({ + to: singleSmsData.to, + status: 400, + error: `SMS not sent: ${JSON.stringify(safeResult)}` + }); + } } - - const result = await this.sendRawSms(singleSmsData); - - if(result?.$metadata?.httpStatusCode === 200 && result?.MessageId) { - results.push({ - to: singleSmsData.to, - status: 200, - result: SUCCESS_MESSAGES.SMS_NOTIFICATION_SEND_SUCCESSFULLY, - messageId: result.MessageId || `sms-${Date.now()}` - }); - } else { - // Safe stringification - only include serializable data - const safeResult = { - data: result?.data, - status: result?.status - }; - results.push({ - to: singleSmsData.to, - status: 400, - error: `SMS not sent: ${JSON.stringify(safeResult)}` - }); + catch (error) { + const timeTakenInMs = Date.now() - (error.startTime || Date.now()); + LoggerUtil.error(`FAILED`, ERROR_MESSAGES.SMS_NOTIFICATION_FAILED, undefined, undefined, { error: error.message, traceId: traceId, status: 'FAILED', timeTaken: timeTakenInMs }); + + // Safe error object without circular references + results.push({ + recipient: singleSmsData.to, + status: error.status || 500, + error: error.message || error.toString() + }); } - } - catch (error) { - LoggerUtil.error(ERROR_MESSAGES.SMS_NOTIFICATION_FAILED, error.message); - - // Safe error object without circular references - results.push({ - recipient: singleSmsData.to, - status: error.status || 500, - error: error.message || error.toString() - }); - } } return results; } @@ -307,38 +330,38 @@ export class SmsAdapter implements NotificationServiceInterface { if (!this.msg91url) { throw new Error('MSG91_URL is not configured'); } - + // Use customTemplateId if provided, otherwise fall back to env variable const templateId = customTemplateId || this.configService.get('MSG91_DEFAULT_TEMPLATE_ID'); if (!templateId) { throw new Error('Template ID is required. Either pass it in the request or configure MSG91_DEFAULT_TEMPLATE_ID'); } - + // For MSG91, directly use all replacements as template variables const templateVariables: any = {}; - + if (notificationData.replacements) { // Pass all replacements directly as template variables Object.keys(notificationData.replacements).forEach(key => { templateVariables[key] = notificationData.replacements[key]; }); - + // Legacy support: Map OTP to var if OTP is provided if (notificationData.replacements.OTP) { templateVariables.var = notificationData.replacements.OTP; } } - + const recipients = [{ mobiles: `91${notificationData.recipient}`, ...templateVariables }]; - + const requestData = { template_id: templateId, recipients: recipients }; - + try { const axiosResponse = await axios.post(this.msg91url, requestData, { headers: { @@ -348,7 +371,7 @@ export class SmsAdapter implements NotificationServiceInterface { }, timeout: 30000 }); - + // Return only safe, serializable data to avoid circular references const safeResponse = { data: axiosResponse.data, @@ -359,14 +382,14 @@ export class SmsAdapter implements NotificationServiceInterface { }, MessageId: axiosResponse.data?.request_id || axiosResponse.data?.message_id || `msg91-${Date.now()}` }; - + return safeResponse; } catch (error) { LoggerUtil.error(ERROR_MESSAGES.SMS_NOTIFICATION_FAILED, error.message); - + // Extract safe error information to avoid circular references const safeError: any = new Error(error.message || 'MSG91 API Error'); - + if (error.response) { LoggerUtil.error(`MSG91 API Error Response: Status ${error.response.status}, Data: ${JSON.stringify(error.response.data, null, 2)}`); safeError.status = error.response.status; @@ -375,7 +398,7 @@ export class SmsAdapter implements NotificationServiceInterface { } else if (error.request) { safeError.message = 'No response received from MSG91 server'; } - + throw safeError; } } diff --git a/src/modules/notification/adapters/whatsappViaGupshup.adapter.ts b/src/modules/notification/adapters/whatsappViaGupshup.adapter.ts index 2bfb66c..a246ca1 100644 --- a/src/modules/notification/adapters/whatsappViaGupshup.adapter.ts +++ b/src/modules/notification/adapters/whatsappViaGupshup.adapter.ts @@ -34,14 +34,14 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { ) { // Initialize Gupshup configuration this.gupshupApiUrl = this.configService.get('GUPSHUP_API_URL', 'https://api.gupshup.io/wa/api/v1'); - + } /** * Sends WhatsApp notifications using template-based approach */ async sendNotification(notificationDataArray) { - + } /** @@ -77,7 +77,7 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { // Format phone number (remove + if present and ensure proper format) const formattedDestination = to.startsWith('+') ? to.substring(1) : to; const formattedSource = from.startsWith('+') ? from.substring(1) : from; - + // Prepare the message payload for Gupshup API let messagePayload: any = { source: formattedSource, @@ -98,9 +98,9 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { } }); - if ((response.status === 202 || response.status === 200 )&& response.data) { + if ((response.status === 202 || response.status === 200) && response.data) { const responseData = response.data; - + // Check if the response indicates success if (responseData.status === 'submitted' || responseData.status === 'success') { LoggerUtil.log('WhatsApp message sent successfully via Gupshup API'); @@ -117,7 +117,7 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { } } catch (error) { LoggerUtil.error('Gupshup WhatsApp API error:', error); - + // Handle different types of errors if (error.response) { // API error response @@ -148,7 +148,7 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { /** * Send template message via Gupshup */ - async sendTemplateMessage(templateData: { + async sendTemplateMessage(traceId, templateData: { to: string; templateId: string; templateParams: any[]; @@ -160,7 +160,7 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { `Template: ${templateData.templateId}`, templateData.to ); - + LoggerUtil.log(`ADAPTER_PREP`, '', '', 'info', { ...templateData, traceId: traceId, status: 'ADAPTER_PREP' }); try { const result = await this.sendViaGupshupProvider({ to: templateData.to, @@ -170,8 +170,9 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { gupshupSource: templateData.gupshupSource, gupshupApiKey: templateData.gupshupApiKey }); - + if (result.status === "success") { + LoggerUtil.log(`ADAPTER_SUCCESS`, '', '', 'info', { ...result, traceId: traceId, status: 'ADAPTER_SUCCESS' }); notificationLogs.status = true; await this.notificationServices.saveNotificationLogs(notificationLogs); LoggerUtil.log('WhatsApp template message submitted successfully via Gupshup'); @@ -182,10 +183,11 @@ export class WhatsappViaGupshupAdapter implements NotificationServiceInterface { messageId: result.id || `gupshup-template-${Date.now()}`, }; } else { + LoggerUtil.log(`ADAPTER_FAILURE`, '', '', 'info', { ...result, traceId: traceId, status: 'ADAPTER_FAILURE' }); throw new Error(`WhatsApp template not sent: ${JSON.stringify(result.errors)}`); } } catch (e) { - LoggerUtil.error('WhatsApp template message failed:', e); + LoggerUtil.error(`ADAPTER_FAILURE`, '', '', 'info', { error: e, traceId: traceId, status: 'ADAPTER_FAILURE' }); notificationLogs.status = false; notificationLogs.error = e.toString(); await this.notificationServices.saveNotificationLogs(notificationLogs); diff --git a/src/modules/notification/interface/notificationService.ts b/src/modules/notification/interface/notificationService.ts index c3a6255..dbf32a7 100644 --- a/src/modules/notification/interface/notificationService.ts +++ b/src/modules/notification/interface/notificationService.ts @@ -1,3 +1,3 @@ export interface NotificationServiceInterface { - sendNotification(notificationDataArray); + sendNotification(notificationDataArray, traceId?: string); } \ No newline at end of file diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts index 344df9d..f883e6b 100644 --- a/src/modules/notification/notification.controller.ts +++ b/src/modules/notification/notification.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Post, Get, UsePipes, ValidationPipe, BadRequestException, Res, UseFilters } from '@nestjs/common'; import { NotificationService } from './notification.service'; import { ApiBadRequestResponse, ApiBasicAuth, ApiBody, ApiCreatedResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { NotificationDto,RawNotificationDto } from './dto/notificationDto.dto'; +import { NotificationDto, RawNotificationDto } from './dto/notificationDto.dto'; import { TopicNotification } from './dto/topicnotification .dto'; import { Response } from 'express'; import { AllExceptionsFilter } from 'src/common/filters/exception.filter'; @@ -12,7 +12,7 @@ import { GetUserId } from 'src/common/decorator/userId.decorator'; @ApiTags('Notification-send') @ApiBasicAuth('access-token') export class NotificationController { - constructor(private notificationService: NotificationService) {} + constructor(private notificationService: NotificationService) { } @UseFilters(new AllExceptionsFilter(APIID.SEND_NOTIFICATION)) @Post("send") @@ -97,5 +97,5 @@ export class NotificationController { ); } - + } diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts index e8ec40c..3ed49c5 100644 --- a/src/modules/notification/notification.service.ts +++ b/src/modules/notification/notification.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, HttpStatus, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import axios from "axios"; -import { NotificationDto,RawNotificationDto } from "./dto/notificationDto.dto"; +import { NotificationDto, RawNotificationDto } from "./dto/notificationDto.dto"; import { NotificationAdapterFactory } from "./notificationadapters"; import APIResponse from "src/common/utils/response"; // import * as FCM from 'fcm-node'; @@ -17,14 +17,15 @@ import { NotificationQueue } from "../notification-queue/entities/notificationQu import { AmqpConnection, RabbitSubscribe } from "@nestjs-plus/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"; -import {SmsAdapter} from "src/modules/notification/adapters/smsService.adapter"; +import { EmailAdapter } from "src/modules/notification/adapters/emailService.adapter"; +import { SmsAdapter } from "src/modules/notification/adapters/smsService.adapter"; import { WhatsappViaGupshupAdapter } from './adapters/whatsappViaGupshup.adapter'; import { SUCCESS_MESSAGES, ERROR_MESSAGES, } from "src/common/utils/constant.util"; import { LoggerUtil } from "src/common/logger/LoggerUtil"; +import { randomUUID } from 'crypto'; @Injectable() export class NotificationService { @@ -60,6 +61,8 @@ export class NotificationService { response: Response ): Promise { const apiId = APIID.SEND_NOTIFICATION; + const traceId = randomUUID(); + LoggerUtil.log(`RECEIVED`, apiId, userId, 'info', { traceId: traceId, status: 'RECEIVED' }); const serverResponses: Record = { email: { data: [], errors: [] }, sms: { data: [], errors: [] }, @@ -69,13 +72,14 @@ export class NotificationService { try { const { email, push, sms, inApp, context, replacements, key } = notificationDto; + // Check if notification template exists const notification_event = await this.notificationActions.findOne({ where: { context, key }, }); if (!notification_event) { LoggerUtil.error( - `template not found with this ${context} and ${key}`, + `Status: VALIDATED, traceId: ${traceId}, template not found with this ${context} and ${key}`, ERROR_MESSAGES.TEMPLATE_NOTFOUND, apiId, userId @@ -88,6 +92,7 @@ export class NotificationService { if (email && email.receipients && email.receipients.length > 0) { const promise = this.notificationHandler( + traceId, "email", email.receipients, "email", @@ -100,7 +105,7 @@ export class NotificationService { } if (sms && sms.receipients && sms.receipients.length > 0) { - const promise = this.notificationHandler( + const promise = this.notificationHandler(traceId, "sms", sms.receipients, "sms", @@ -113,7 +118,7 @@ export class NotificationService { } if (push?.receipients?.length) { - const promise = this.notificationHandler( + const promise = this.notificationHandler(traceId, "push", push.receipients, "push", @@ -125,7 +130,7 @@ export class NotificationService { promises.push({ promise, channel: "push" }); } if (inApp?.receipients?.length) { - const promise = this.notificationHandler( + const promise = this.notificationHandler(traceId, "inApp", inApp.receipients, "inApp", @@ -190,17 +195,18 @@ export class NotificationService { ([_, { data, errors }]) => data.length > 0 || errors.length > 0 ) ); + LoggerUtil.log(`COMPLETED`, apiId, userId, 'info', { traceId: traceId, status: 'COMPLETED' }); return response .status(HttpStatus.OK) - .json(APIResponse.success(apiId, finalResponses, "OK")); + .json(APIResponse.success(traceId, finalResponses, "OK")); } catch (e) { - LoggerUtil.error(`Error: ${e}`, e, apiId, userId); + LoggerUtil.error(`FAILED: ${e}`, e, apiId, userId, { traceId: traceId, status: 'FAILED' }); throw e; } } // Helper function to handle sending notifications for a specific channel - async notificationHandler( + async notificationHandler(traceId: string, channel, recipients, type, @@ -209,6 +215,8 @@ export class NotificationService { notification_event, userId ) { + LoggerUtil.log("templeateID", traceId, userId, 'info', { ...notification_event, traceId: traceId }); + LoggerUtil.log(`VALIDATED`, traceId, userId, 'info', { channel, traceId: traceId, status: 'VALIDATED' }); if ( recipients && recipients.length > 0 && @@ -219,8 +227,8 @@ export class NotificationService { }); if (notification_details.length === 0) { LoggerUtil.error( - ERROR_MESSAGES.TEMPLATE_CONFIG_NOTFOUND, - `/Send ${channel} Notification`, + `Status: VALIDATED, traceId: ${traceId}, ERROR_MESSAGES.TEMPLATE_CONFIG_NOTFOUND`, + `/ Send ${channel} Notification`, traceId, userId ); throw new BadRequestException( @@ -268,7 +276,7 @@ export class NotificationService { link: link || null, replacements: replacements, }; - + // Add CC and BCC for email channel if provided if (type === "email" && notificationDto.email) { if (notificationDto.email.cc && notificationDto.email.cc.length > 0) { @@ -278,25 +286,26 @@ export class NotificationService { notificationData.bcc = notificationDto.email.bcc; } } - + return notificationData; }); - + LoggerUtil.log(`TemplateId: ${notification_details[0].templateId}`, traceId, userId, 'info', { traceId: traceId }); if (notificationDto.isQueue) { try { - const saveQueue = await this.saveNotificationQueue( + const saveQueue = await this.saveNotificationQueue(traceId, notificationDataArray ); if (saveQueue.length === 0) { throw new Error(ERROR_MESSAGES.NOTIFICATION_QUEUE_SAVE_FAILED); } + LoggerUtil.log(`QUEUED`, traceId, userId, 'info', { saveQueue, traceId: traceId, status: 'QUEUED' }); return { status: 200, message: SUCCESS_MESSAGES.NOTIFICATION_QUEUE_SAVE_SUCCESSFULLY, }; } catch (error) { LoggerUtil.error( - ERROR_MESSAGES.NOTIFICATION_QUEUE_SAVE_FAILED, + `Status: QUEUED_FAILED, traceId: ${traceId} ERROR_MESSAGES.NOTIFICATION_QUEUE_SAVE_FAILED`, error, SUCCESS_MESSAGES.MESSAGES_SAVING_IN_QUEUE, userId @@ -305,7 +314,7 @@ export class NotificationService { } } else { const adapter = this.adapterFactory.getAdapter(type); - return adapter.sendNotification(notificationDataArray); + return adapter.sendNotification(notificationDataArray, traceId); } } } @@ -339,13 +348,14 @@ export class NotificationService { throw new BadRequestException( `Missing replacements for placeholders: ${missingReplacements.join( ", " - )}` + ) + } ` ); } } //Provider which store in Queue - async saveNotificationQueue(notificationDataArray) { + async saveNotificationQueue(apiId, notificationDataArray) { const arrayofResult = await this.notificationQueue.save( notificationDataArray ); @@ -353,7 +363,7 @@ export class NotificationService { if (this.amqpConnection) { try { for (const result of arrayofResult) { - this.amqpConnection.publish( + this.amqpConnection.publish(apiId, "notification.exchange", "notification.route", result, @@ -377,15 +387,16 @@ export class NotificationService { routingKey: "notification.route", queue: "notification.queue", }) - async handleNotification(notification, message: any, retryCount = 3) { + async handleNotification(traceId, notification, message: any, retryCount = 3) { try { const adapter = this.adapterFactory.getAdapter(notification.channel); - await adapter.sendNotification([notification]); + await adapter.sendNotification([notification], traceId); const updateQueueDTO = { status: true, retries: 3 - retryCount, last_attempted: new Date(), }; + LoggerUtil.log(`QUEUED`, traceId, '', 'info', { ...notification, traceId: traceId, status: 'QUEUED' }); await this.notificationQueueService.updateQueue( notification.id, updateQueueDTO @@ -416,7 +427,7 @@ export class NotificationService { // }); // } catch (error) { // this.logger.error( - // `Failed to Subscribe to topic ${requestBody.topicName}`, + // `Failed to Subscribe to topic ${ requestBody.topicName } `, // error, // '/Not able to subscribe to topic', // ); @@ -437,7 +448,7 @@ export class NotificationService { // } // } catch (error) { // this.logger.error( - // `Failed to UnSubscribe to topic ${requestBody.topicName}`, + // `Failed to UnSubscribe to topic ${ requestBody.topicName } `, // error, // '/Not able to Unsubscribe to topic', // ); @@ -457,13 +468,12 @@ export class NotificationService { image: requestBody.image, navigate_to: requestBody.navigate_to, }, - to: `/topics/${topic_name}`, + to: `/ topics / ${topic_name} `, }; - const response = await axios.post(fcmUrl, notificationData, { headers: { "Content-Type": "application/json", - Authorization: `key=${fcmKey}`, + Authorization: `key = ${fcmKey} `, }, }); return { @@ -488,7 +498,7 @@ export class NotificationService { await this.notificationLogRepository.save(notificationLogs); } catch (e) { LoggerUtil.error( - `error ${e}`, + `error ${e} `, ERROR_MESSAGES.NOTIFICATION_LOG_SAVE_FAILED ); throw new Error(ERROR_MESSAGES.NOTIFICATION_LOG_SAVE_FAILED); @@ -509,7 +519,7 @@ export class NotificationService { // const message = await client.messages.create({ // body: notificationData.message, // from: 'whatsapp:+14155238886', - // to: `whatsapp: ${ notificationData.to }`, + // to: `whatsapp: ${ notificationData.to } `, // }); // this.logger.info('Message sent successfully to whatsapp'); @@ -599,17 +609,19 @@ export class NotificationService { response: Response ): Promise { const apiId = APIID.SEND_NOTIFICATION; + const traceId = randomUUID(); + LoggerUtil.log(`RECEIVED`, traceId, userId, 'info', { traceId: traceId, status: 'RECEIVED' }); const serverResponses: Record = { email: { data: [], errors: [] }, sms: { data: [], errors: [] }, whatsapp: { data: [], errors: [] }, }; - + try { const { email, sms, whatsapp } = rawNotificationDto; - + const promises: Array<{ promise: Promise; channel: string }> = []; - + if (email && email.to && email.to.length > 0) { if (!email.subject || !email.body) { throw new BadRequestException('Email subject and body are required'); @@ -622,7 +634,7 @@ export class NotificationService { body: email.body, isHtml: true, }; - + // Add CC and BCC if provided if (email.cc && email.cc.length > 0) { singleEmailData.cc = email.cc; @@ -630,21 +642,21 @@ export class NotificationService { if (email.bcc && email.bcc.length > 0) { singleEmailData.bcc = email.bcc; } - - return this.emailService.sendRawEmails(singleEmailData); + return this.emailService.sendRawEmails(traceId, singleEmailData); }); - - promises.push({ - promise: Promise.all(emailPromises), - channel: 'email' + + + promises.push({ + promise: Promise.all(emailPromises), + channel: 'email' }); } - + if (sms && sms.to && sms.to.length > 0) { // For MSG91: templateId is required, body is optional // For other providers: body is required const smsProvider = this.configService.get('SMS_PROVIDER', 'AWSSNS'); - + if (smsProvider === 'MSG91') { if (!sms.templateId) { throw new BadRequestException('templateId is required for MSG91 provider'); @@ -654,7 +666,7 @@ export class NotificationService { throw new BadRequestException('SMS body is required'); } } - + // Pass templateId and replacements for MSG91 variable support const smsPromises = sms.to.map(recipient => { const singleSmsData = { @@ -664,20 +676,20 @@ export class NotificationService { templateId: sms.templateId, // Pass templateId for MSG91 replacements: sms.replacements || {}, // Pass replacements for MSG91 templates }; - return this.smsService.sendRawSmsMessages(singleSmsData); + return this.smsService.sendRawSmsMessages(traceId, singleSmsData); }); - - promises.push({ - promise: Promise.all(smsPromises), - channel: 'sms' + + promises.push({ + promise: Promise.all(smsPromises), + channel: 'sms' }); } - + if (whatsapp && whatsapp.to && whatsapp.to.length > 0) { if (!whatsapp.templateId || !whatsapp.templateParams) { throw new BadRequestException('WhatsApp templateId and templateParams are required for raw sending.'); } - + if (!whatsapp.gupshupSource || !whatsapp.gupshupApiKey) { throw new BadRequestException('WhatsApp gupshupSource and gupshupApiKey are required for raw sending.'); } @@ -691,33 +703,34 @@ export class NotificationService { gupshupApiKey: whatsapp.gupshupApiKey }; // Use the dedicated template sending method from the adapter - return this.whatsappViaGupshup.sendTemplateMessage(singleWhatsappData); + return this.whatsappViaGupshup.sendTemplateMessage(traceId, singleWhatsappData); }); - - promises.push({ - promise: Promise.all(whatsappPromises), - channel: 'whatsapp' + + promises.push({ + promise: Promise.all(whatsappPromises), + channel: 'whatsapp' }); } - + const results = await Promise.allSettled(promises.map(p => p.promise)); - - results.forEach((result, index) => { + + results.forEach((result, index) => { const channel = promises[index].channel; - + if (!serverResponses[channel]) { serverResponses[channel] = { data: [], errors: [] }; } - + if (result.status === 'fulfilled') { const notifications = (result.value || []).flat(); - console.log(result.value,"Shubham"); notifications.forEach(notification => { const status = notification.status; - + if (status === 200) { serverResponses[channel].data.push(notification); + LoggerUtil.log(`SENT`, apiId, userId, 'info', { ...notification, traceId: traceId, status: 'SENT' }); } else { + LoggerUtil.log(`FAILED`, apiId, userId, 'error', { ...notification, traceId: traceId, status: 'FAILED' }); serverResponses[channel].errors.push({ recipient: notification.recipient || notification.to || 'unknown', error: notification.error || notification.message || 'Unknown error', @@ -726,27 +739,49 @@ export class NotificationService { } }); } else { + LoggerUtil.log(`FAILED`, apiId, userId, 'error', { reason: result.reason, traceId: traceId, status: 'FAILED' }); serverResponses[channel].errors.push({ error: result.reason?.message || 'Unhandled rejection', code: result.reason?.status || 500, }); } }); - + // Filter empty channels const finalResponses = Object.fromEntries( Object.entries(serverResponses).filter( ([_, { data, errors }]) => data.length > 0 || errors.length > 0 ) ); - + LoggerUtil.log(`COMPLETED`, apiId, userId, 'info', { ...finalResponses, traceId: traceId, status: 'COMPLETED' }); return response .status(HttpStatus.OK) .json(APIResponse.success(apiId, finalResponses, 'OK')); - + } catch (e) { + LoggerUtil.log(`FAILED`, apiId, userId, 'error', { error: e, traceId: traceId, status: 'FAILED' }); LoggerUtil.error(`Error: ${e}`, e, apiId, userId); throw e; } } } + +//console.log(maskEmail("testuser@gmail.com")); +// te****er@gmail.com +export function maskEmail(email: string) { + const [name, domain] = email.split("@"); + + const maskedName = + name.substring(0, 2) + + "*".repeat(name.length - 4) + + name.substring(name.length - 2); + + return `${maskedName}@${domain}`; +} +// console.log(maskPhone("9876543210")); +// 98******10 +export function maskPhone(phone: string) { + return phone.slice(0, 2) + "******" + phone.slice(-2); +} + +