Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/common/logger/LoggerUtil.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
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,
user: user,
level: level,
message: message,
error: error,
metadata: metadata,
});
},
);
Expand All @@ -35,13 +41,15 @@ export class LoggerUtil {
context?: string,
user?: string,
level: string = 'info',
metadata?: Metadata,
) {
this.getLogger().log({
level: level,
message: message,
context: context,
user: user,
timestamp: new Date().toISOString(),
metadata: metadata,
});
}

Expand All @@ -50,13 +58,15 @@ export class LoggerUtil {
error?: string,
context?: string,
user?: string,
metadata?: Metadata,
) {
this.getLogger().error({
message: message,
error: error,
context: context,
user: user,
timestamp: new Date().toISOString(),
metadata: metadata,
});
}

Expand Down
168 changes: 111 additions & 57 deletions src/modules/notification/adapters/emailService.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
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";

/**
* 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[];
Comment on lines +18 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

RawEmailData.to now allows arrays, but this validator still rejects them.

Line 18 advertises string | string[], yet Line 113 passes singleEmailData.to straight into isValidEmail(). Any caller that uses the new multi-recipient shape will fail validation before sendRawEmail() even though the rest of this method already handles arrays.

🛠️ Proposed fix
-                if (!singleEmailData.to || !this.isValidEmail(singleEmailData.to)) {
+                const recipients = Array.isArray(singleEmailData.to)
+                    ? singleEmailData.to
+                    : [singleEmailData.to];
+
+                if (!recipients.length || recipients.some((email) => !this.isValidEmail(email))) {
                     throw new BadRequestException(ERROR_MESSAGES.INVALID_EMAIL);
                 }

Also applies to: 113-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/notification/adapters/emailService.adapter.ts` around lines 18 -
24, The validator incorrectly calls isValidEmail(singleEmailData.to) while
RawEmailData.to can be string | string[]; update the validation in sendRawEmail
(or the surrounding method handling singleEmailData) to accept arrays by
checking if singleEmailData.to is an array and, if so, iterate and call
isValidEmail on each entry (failing if any are invalid), otherwise call
isValidEmail on the single string; ensure this change is applied to the same
pattern at the other occurrences noted (lines around 113–115) so multi-recipient
inputs pass validation.

}

@Injectable()
Expand All @@ -35,35 +35,61 @@
* @param notificationDataArray Array of notification data objects
* @returns Results of notification attempts
*/
async sendNotification(notificationDataArray) {
async sendNotification(notificationDataArray, traceId?: string) {

Check failure on line 38 in src/modules/notification/adapters/emailService.adapter.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=tekdi_notification-microservice&issues=AZzC8q_fFey6E8D9NRMQ&open=AZzC8q_fFey6E8D9NRMQ&pullRequest=50
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}']) {

Check warning on line 59 in src/modules/notification/adapters/emailService.adapter.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=tekdi_notification-microservice&issues=AZzC8q_fFey6E8D9NRMR&open=AZzC8q_fFey6E8D9NRMR&pullRequest=50
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 });
Comment on lines 86 to +88
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

error.startTime is undefined—timeTakenInMs calculation is incorrect.

Line 87 references error.startTime, which is never set. The fallback Date.now() makes timeTakenInMs effectively zero, providing misleading timing data in error logs. The same issue exists at Line 164.

🛠️ Proposed fix — capture startTime before try block
     async sendNotification(notificationDataArray, traceId?: string) {
         const results = [];
         for (const notificationData of notificationDataArray) {
-
+            const startTime = Date.now();
             try {
                 const recipient = notificationData.recipient;
                 if (!recipient || !this.isValidEmail(recipient)) {
                     throw new BadRequestException(ERROR_MESSAGES.INVALID_EMAIL);
                 }
 // ... existing masking code ...
 
-                const startTime = Date.now();
                 const result = await this.send(notificationData, traceId);
                 const timeTakenInMs = Date.now() - startTime;
 // ... rest of try block ...
             }
             catch (error) {
-                const timeTakenInMs = Date.now() - (error.startTime || Date.now());
+                const timeTakenInMs = Date.now() - startTime;
                 LoggerUtil.error(`FAILED ${traceId}`, error.message, traceId, '', { status: 'FAILED', traceId: traceId, timeTaken: timeTakenInMs });

Apply the same pattern to sendRawEmails (Lines 142, 164).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/notification/adapters/emailService.adapter.ts` around lines 86 -
88, The error timing is wrong because startTime is never set on the thrown
error; capture a startTime variable before entering the try blocks and use that
to compute timeTakenInMs in the catch instead of error.startTime; update the
functions where this occurs (the failing block and sendRawEmails) to declare
const startTime = Date.now() before their try and compute timeTakenInMs =
Date.now() - startTime in their catch handlers, then pass that timeTakenInMs to
LoggerUtil.error.

results.push({
recipient: notificationData.recipient,
status: 'error',
error: error.toString()
error: error.message
});
}
}
Expand All @@ -75,49 +101,77 @@
* @param rawEmailDataArray Array of raw email data objects
* @returns Results of raw email sending attempts
*/
async sendRawEmails(emailData) {
async sendRawEmails(traceId, emailData) {

Check failure on line 104 in src/modules/notification/adapters/emailService.adapter.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=tekdi_notification-microservice&issues=AZzC8q_fFey6E8D9NRMS&open=AZzC8q_fFey6E8D9NRMS&pullRequest=50
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}']) {

Check warning on line 136 in src/modules/notification/adapters/emailService.adapter.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=tekdi_notification-microservice&issues=AZzC8q_fFey6E8D9NRMT&open=AZzC8q_fFey6E8D9NRMT&pullRequest=50
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
Expand Down Expand Up @@ -186,14 +240,14 @@
/**
* 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,
Expand All @@ -207,17 +261,17 @@
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",);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in log message—string interpolation includes literal text.

Line 264 logs "traceId: ${traceId} SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY" where SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY is literal text instead of the constant value.

🛠️ Proposed fix
-                LoggerUtil.log(`traceId: ${traceId} SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY`, traceId, "", "info",);
+                LoggerUtil.log(`traceId: ${traceId} ${SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY}`, traceId, "", "info");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LoggerUtil.log(`traceId: ${traceId} SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY`, traceId, "", "info",);
LoggerUtil.log(`traceId: ${traceId} ${SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY}`, traceId, "", "info");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/notification/adapters/emailService.adapter.ts` at line 264, The
log currently prints the literal text
"SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY" instead of the
constant's value; update the LoggerUtil.log call in emailService.adapter.ts (the
line using LoggerUtil.log with traceId) to interpolate or concatenate the
SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY constant (e.g., use a
template literal with ${SUCCESS_MESSAGES.EMAIL_NOTIFICATION_SEND_SUCCESSFULLY}
or string concatenation) so the actual success message value is logged alongside
traceId.

return result;
}
else {
throw new Error(`Email not send ${JSON.stringify(result.errors)}`)
}
}
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;
}
Expand All @@ -236,11 +290,11 @@
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,
Expand All @@ -251,14 +305,14 @@
...(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)}`)
Expand Down
Loading