From 6d392684c0464e5ec603857931c2829a6405cda4 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 12:34:31 +0530 Subject: [PATCH 01/18] PM-3351 Send notification on manual phase change [initial commit] --- app-constants.js | 8 +++ config/default.js | 1 + src/common/helper.js | 75 ++++++++++++++++++++++++ src/services/ChallengePhaseService.js | 82 +++++++++++++++++++++++++++ 4 files changed, 166 insertions(+) diff --git a/app-constants.js b/app-constants.js index 0c6f361..5fe401c 100644 --- a/app-constants.js +++ b/app-constants.js @@ -151,6 +151,14 @@ const PhaseFact = { UNRECOGNIZED: -1 } +exports.PhaseChangeNotificationSettings = { + PHASE_CHANGE: { + sendgridTemplateId: process.env.PHASE_CHANGE_SENDGRID_TEMPLATE_ID, + cc: [], + }, +}; + + const auditFields = [ 'createdAt', 'createdBy', 'updatedAt', 'updatedBy' ] diff --git a/config/default.js b/config/default.js index cc0da6e..ec71ecf 100644 --- a/config/default.js +++ b/config/default.js @@ -133,4 +133,5 @@ module.exports = { RESOURCES_DB_SCHEMA: process.env.RESOURCES_DB_SCHEMA || "resources", REVIEW_DB_SCHEMA: process.env.REVIEW_DB_SCHEMA || "reviews", CHALLENGE_SERVICE_PRISMA_TIMEOUT: process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT ? parseInt(process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT, 10) : 10000, + CHALLENGE_URL: process.env.CHALLENGE_URL || 'https://www.topcoder-dev.com/challenges' }; diff --git a/src/common/helper.js b/src/common/helper.js index a7453fb..7420218 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1638,6 +1638,79 @@ async function sendSelfServiceNotification(type, recipients, data) { } } +/** + * Build payload for phase change email notification + * @param {String} challenge Id + * @param {String} challenge name + * @param {String} challenge phase name + * @param {String} operation to be performed on the phase - open | close | reopen + */ +function buildPhaseChangeEmailData({ challengeId, challengeName, phaseName, operation, at }) { + const isOpen = operation === 'open' || operation === 'reopen'; + const isClose = operation === 'close'; + + return { + challengeURL: `${config.CHALLENGE_URL}/${challengeId}`, + challengeName, + phaseOpen: isOpen ? phaseName : null, + phaseOpenDate: isOpen ? at : null, + phaseClose: isClose ? phaseName : null, + phaseCloseDate: isClose ? at : null, + }; +} + + +/** + * Send phase change notification + * @param {String} type the notification type + * @param {Array} recipients the array of recipients in { userId || email || handle } format + * @param {Object} data the data + */ +async function sendPhaseChangeNotification(type, recipients, data) { + try { + const settings = constants.PhaseChangeNotificationSettings?.[type]; + + if (!settings) { + logger.debug(`sendPhaseChangeNotification: unknown type ${type}`); + return; + } + + if (!settings.sendgridTemplateId) { + logger.debug( + `sendPhaseChangeNotification: sendgridTemplateId not configured for type ${type}` + ); + return; + } + const safeRecipients = Array.isArray(recipients) ? recipients.filter(Boolean) : []; + + if (!safeRecipients.length) { + logger.debug(`sendPhaseChangeNotification: no recipients for type ${type}`); + return; + } + + await postBusEvent(constants.Topics.Notifications, { + notifications: [ + { + serviceId: 'email', + type, + details: { + from: config.EMAIL_FROM, + recipients: [...safeRecipients], + cc: [...(settings.cc || [])], + data: { + ...data, + }, + sendgridTemplateId: settings.sendgridTemplateId, + version: 'v3', + }, + }, + ], + }); + } catch (e) { + logger.debug(`Failed to post notification ${type}: ${e.message}`); + } +} + /** * Submit a request to zendesk * @param {Object} request the request @@ -1756,6 +1829,8 @@ module.exports = { setToInternalCache, flushInternalCache, removeNullProperties, + buildPhaseChangeEmailData, + sendPhaseChangeNotification }; logger.buildService(module.exports); diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index 507eb2f..61e0cde 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -813,9 +813,91 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) _.assignIn({ id: result.id }, data) ); await postChallengeUpdatedNotification(challengeId); + + // send notification logic + try { + const shouldNotifyClose = Boolean(isClosingPhase); + const shouldNotifyOpen = Boolean(isOpeningPhase); // includes reopen + + if (!shouldNotifyClose && !shouldNotifyOpen) { + return _.omit(result, constants.auditFields); + } + + // Single template - single type + const notificationType = "PHASE_CHANGE"; + + const operation = shouldNotifyClose + ? "close" + : (isReopeningPhase ? "reopen" : "open"); + + const at = shouldNotifyClose + ? (result.actualEndDate || new Date().toISOString()) + : (result.actualStartDate || new Date().toISOString()); + + // fetch challenge name + const challenge = await prisma.challenge.findUnique({ + where: { id: challengeId }, + select: { name: true }, + }); + + const challengeName = challenge?.name; + + // build recipients + const resources = await helper.getChallengeResources(challengeId); + + const seen = new Set(); + const recipients = []; + + for (const r of resources || []) { + const userId = r?.memberId ? String(r.memberId).trim() : null; + const handle = r?.memberHandle ? String(r.memberHandle).trim() : null; + + let key = null; + let rec = null; + + if (userId) { + key = `userId:${userId}`; + rec = { userId }; + } else if (handle) { + const norm = handle.toLowerCase(); + key = `handle:${norm}`; + rec = { handle: norm }; + } + + if (!key || seen.has(key)) continue; + seen.add(key); + recipients.push(rec); + } + + if (!recipients.length) { + logger.debug( + `phase change notification skipped: no recipients for challenge ${challengeId}` + ); + return _.omit(result, constants.auditFields); + } + + // build payload that matches the SendGrid HTML template + const phaseName = result.name || data.name || challengePhase.name; + + const payload = helper.buildPhaseChangeEmailData({ + challengeId, + challengeName, + phaseName, + operation, + at, + }); + + await sendPhaseChangeNotification(notificationType, recipients, payload); + } catch (e) { + logger.debug( + `phase change notification failed for challenge ${challengeId}, phase ${id}: ${e.message}` + ); + } + return _.omit(result, constants.auditFields); } + partiallyUpdateChallengePhase.schema = { currentUser: Joi.any(), challengeId: Joi.id(), From f7ee6df0c80d9759ad7cef81dd86544901e517ed Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 12:35:37 +0530 Subject: [PATCH 02/18] deploy PM-3351 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d4b135..3c0c8c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,7 +91,7 @@ workflows: only: - develop - security - - PM-3327 + - PM-3351 - "build-qa": context: org-global From 23c3a606fa870dff02dcc418143b9bac2176a81b Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 12:55:29 +0530 Subject: [PATCH 03/18] Add sendgrid appvar to config --- app-constants.js | 2 +- config/default.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app-constants.js b/app-constants.js index 5fe401c..bc7e075 100644 --- a/app-constants.js +++ b/app-constants.js @@ -153,7 +153,7 @@ const PhaseFact = { exports.PhaseChangeNotificationSettings = { PHASE_CHANGE: { - sendgridTemplateId: process.env.PHASE_CHANGE_SENDGRID_TEMPLATE_ID, + sendgridTemplateId: config.PHASE_CHANGE_SENDGRID_TEMPLATE_ID, cc: [], }, }; diff --git a/config/default.js b/config/default.js index ec71ecf..93e524f 100644 --- a/config/default.js +++ b/config/default.js @@ -133,5 +133,6 @@ module.exports = { RESOURCES_DB_SCHEMA: process.env.RESOURCES_DB_SCHEMA || "resources", REVIEW_DB_SCHEMA: process.env.REVIEW_DB_SCHEMA || "reviews", CHALLENGE_SERVICE_PRISMA_TIMEOUT: process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT ? parseInt(process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT, 10) : 10000, - CHALLENGE_URL: process.env.CHALLENGE_URL || 'https://www.topcoder-dev.com/challenges' + CHALLENGE_URL: process.env.CHALLENGE_URL || 'https://www.topcoder-dev.com/challenges' , + PHASE_CHANGE_SENDGRID_TEMPLATE_ID: process.env.PHASE_CHANGE_SENDGRID_TEMPLATE_ID || "", }; From c17d651c1159b5e3a7040cea3eb8f3204f8e9356 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 13:36:56 +0530 Subject: [PATCH 04/18] Fix typo --- src/services/ChallengePhaseService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index 61e0cde..b3789d2 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -887,7 +887,7 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) at, }); - await sendPhaseChangeNotification(notificationType, recipients, payload); + await helper.sendPhaseChangeNotification(notificationType, recipients, payload); } catch (e) { logger.debug( `phase change notification failed for challenge ${challengeId}, phase ${id}: ${e.message}` From 95fd2e6dfd4f7ed71af6c6fb4d2b9a14e4a58409 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 14:11:14 +0530 Subject: [PATCH 05/18] Fix typo --- app-constants.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app-constants.js b/app-constants.js index bc7e075..5c8d576 100644 --- a/app-constants.js +++ b/app-constants.js @@ -151,7 +151,7 @@ const PhaseFact = { UNRECOGNIZED: -1 } -exports.PhaseChangeNotificationSettings = { +const PhaseChangeNotificationSettings = { PHASE_CHANGE: { sendgridTemplateId: config.PHASE_CHANGE_SENDGRID_TEMPLATE_ID, cc: [], @@ -176,4 +176,5 @@ module.exports = { SelfServiceNotificationSettings, PhaseFact, auditFields, + PhaseChangeNotificationSettings, }; From 57c5063aebedbee9c0fa86d4a3beb514f57af58d Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 14:24:00 +0530 Subject: [PATCH 06/18] Add logging --- src/common/helper.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/common/helper.js b/src/common/helper.js index 7420218..47fc1dd 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1688,6 +1688,21 @@ async function sendPhaseChangeNotification(type, recipients, data) { return; } + logger.debug( + `sendPhaseChangeNotification: preparing email`, + { + type, + recipientsCount: safeRecipients.length, + recipients: safeRecipients, + } + ); + + logger.debug( + `sendPhaseChangeNotification: payload`, + data + ); + + await postBusEvent(constants.Topics.Notifications, { notifications: [ { From 4ecb6bf6c2726195c7782101d39c97de64d2b39f Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 14:42:14 +0530 Subject: [PATCH 07/18] Fix recipient emails and add logging --- src/services/ChallengePhaseService.js | 35 ++++++++++----------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index b3789d2..411f745 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -845,29 +845,20 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) // build recipients const resources = await helper.getChallengeResources(challengeId); - const seen = new Set(); - const recipients = []; - - for (const r of resources || []) { - const userId = r?.memberId ? String(r.memberId).trim() : null; - const handle = r?.memberHandle ? String(r.memberHandle).trim() : null; - - let key = null; - let rec = null; - - if (userId) { - key = `userId:${userId}`; - rec = { userId }; - } else if (handle) { - const norm = handle.toLowerCase(); - key = `handle:${norm}`; - rec = { handle: norm }; - } + const recipients = Array.from( + new Set( + (resources || []) + .map(r => r?.email || r?.memberEmail) + .filter(Boolean) + .map(e => String(e).trim().toLowerCase()) + ) + ); - if (!key || seen.has(key)) continue; - seen.add(key); - recipients.push(rec); - } + logger.debug(`phase change: resolved emails`, { + challengeId, + emailsCount: recipientEmails.length, + emails: recipientEmails, + }); if (!recipients.length) { logger.debug( From e814a6d7cda1a1d0e6bb452397dcbe0da1339972 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 14:52:36 +0530 Subject: [PATCH 08/18] fix typo --- src/services/ChallengePhaseService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index 411f745..fc36eb2 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -856,8 +856,8 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) logger.debug(`phase change: resolved emails`, { challengeId, - emailsCount: recipientEmails.length, - emails: recipientEmails, + emailsCount: recipients.length, + emails: recipients, }); if (!recipients.length) { From b39f1048be0eb177a435d2a6dfaaa46065eadeb6 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 15:04:00 +0530 Subject: [PATCH 09/18] Enable challenge phase update topic --- app-constants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app-constants.js b/app-constants.js index 5c8d576..bce6ef8 100644 --- a/app-constants.js +++ b/app-constants.js @@ -96,7 +96,6 @@ const DisabledTopics = [ Topics.ChallengeTimelineTemplateCreated, Topics.ChallengeTimelineTemplateUpdated, Topics.ChallengeTimelineTemplateDeleted, - Topics.ChallengePhaseUpdated, Topics.ChallengePhaseDeleted, Topics.DefaultChallengeReviewerCreated, Topics.DefaultChallengeReviewerUpdated, From b1812d720f14b85878d8ca6330e98d2d16b190e4 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 15:21:53 +0530 Subject: [PATCH 10/18] always return updated phase --- src/services/ChallengePhaseService.js | 96 +++++++++++++-------------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index fc36eb2..48bc8a1 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -819,66 +819,64 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) const shouldNotifyClose = Boolean(isClosingPhase); const shouldNotifyOpen = Boolean(isOpeningPhase); // includes reopen - if (!shouldNotifyClose && !shouldNotifyOpen) { - return _.omit(result, constants.auditFields); - } - - // Single template - single type - const notificationType = "PHASE_CHANGE"; + if (shouldNotifyClose && shouldNotifyOpen) { + // Single template - single type + const notificationType = "PHASE_CHANGE"; - const operation = shouldNotifyClose - ? "close" - : (isReopeningPhase ? "reopen" : "open"); + const operation = shouldNotifyClose + ? "close" + : (isReopeningPhase ? "reopen" : "open"); - const at = shouldNotifyClose - ? (result.actualEndDate || new Date().toISOString()) - : (result.actualStartDate || new Date().toISOString()); + const at = shouldNotifyClose + ? (result.actualEndDate || new Date().toISOString()) + : (result.actualStartDate || new Date().toISOString()); - // fetch challenge name - const challenge = await prisma.challenge.findUnique({ - where: { id: challengeId }, - select: { name: true }, - }); + // fetch challenge name + const challenge = await prisma.challenge.findUnique({ + where: { id: challengeId }, + select: { name: true }, + }); - const challengeName = challenge?.name; + const challengeName = challenge?.name; - // build recipients - const resources = await helper.getChallengeResources(challengeId); + // build recipients + const resources = await helper.getChallengeResources(challengeId); - const recipients = Array.from( - new Set( - (resources || []) - .map(r => r?.email || r?.memberEmail) - .filter(Boolean) - .map(e => String(e).trim().toLowerCase()) - ) - ); + const recipients = Array.from( + new Set( + (resources || []) + .map(r => r?.email || r?.memberEmail) + .filter(Boolean) + .map(e => String(e).trim().toLowerCase()) + ) + ); - logger.debug(`phase change: resolved emails`, { - challengeId, - emailsCount: recipients.length, - emails: recipients, - }); + logger.debug(`phase change: resolved emails`, { + challengeId, + emailsCount: recipients.length, + emails: recipients, + }); - if (!recipients.length) { - logger.debug( - `phase change notification skipped: no recipients for challenge ${challengeId}` - ); - return _.omit(result, constants.auditFields); - } + if (!recipients.length) { + logger.debug( + `phase change notification skipped: no recipients for challenge ${challengeId}` + ); + return _.omit(result, constants.auditFields); + } - // build payload that matches the SendGrid HTML template - const phaseName = result.name || data.name || challengePhase.name; + // build payload that matches the SendGrid HTML template + const phaseName = result.name || data.name || challengePhase.name; - const payload = helper.buildPhaseChangeEmailData({ - challengeId, - challengeName, - phaseName, - operation, - at, - }); + const payload = helper.buildPhaseChangeEmailData({ + challengeId, + challengeName, + phaseName, + operation, + at, + }); - await helper.sendPhaseChangeNotification(notificationType, recipients, payload); + await helper.sendPhaseChangeNotification(notificationType, recipients, payload); + } } catch (e) { logger.debug( `phase change notification failed for challenge ${challengeId}, phase ${id}: ${e.message}` From 1bd719a68879b27ddf929a64b85a9b4d8921a66d Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 15:28:07 +0530 Subject: [PATCH 11/18] AI feedback --- src/services/ChallengePhaseService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index 48bc8a1..530f63f 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -819,7 +819,7 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) const shouldNotifyClose = Boolean(isClosingPhase); const shouldNotifyOpen = Boolean(isOpeningPhase); // includes reopen - if (shouldNotifyClose && shouldNotifyOpen) { + if (shouldNotifyClose || shouldNotifyOpen) { // Single template - single type const notificationType = "PHASE_CHANGE"; From b235f717a1ae447db5a3cad53990d26248bfbd48 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 15:39:32 +0530 Subject: [PATCH 12/18] Change topic --- src/common/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index 47fc1dd..1374155 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1614,7 +1614,7 @@ async function getStandSkills(ids) { */ async function sendSelfServiceNotification(type, recipients, data) { try { - await postBusEvent(constants.Topics.Notifications, { + await postBusEvent(constants.Topics.ChallengePhaseUpdated, { notifications: [ { serviceId: "email", From eaca715af427714c507cd4360b79d3f76f0c17b1 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 15:53:00 +0530 Subject: [PATCH 13/18] Add valid topic and test --- src/common/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index 1374155..7574c3d 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1614,7 +1614,7 @@ async function getStandSkills(ids) { */ async function sendSelfServiceNotification(type, recipients, data) { try { - await postBusEvent(constants.Topics.ChallengePhaseUpdated, { + await postBusEvent('external.action.email', { notifications: [ { serviceId: "email", From aa809538d07d22e3ffad2982c6084ffe021e0cd6 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 16:02:03 +0530 Subject: [PATCH 14/18] Wrong method updated --- src/common/helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 7574c3d..33db8fb 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1614,7 +1614,7 @@ async function getStandSkills(ids) { */ async function sendSelfServiceNotification(type, recipients, data) { try { - await postBusEvent('external.action.email', { + await postBusEvent(constants.Topics.Notifications, { notifications: [ { serviceId: "email", @@ -1703,7 +1703,7 @@ async function sendPhaseChangeNotification(type, recipients, data) { ); - await postBusEvent(constants.Topics.Notifications, { + await postBusEvent('external.action.email', { notifications: [ { serviceId: 'email', From 629b0b28d20f3245f828b8ee1fc9ea9ac70c6ffc Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 16:10:03 +0530 Subject: [PATCH 15/18] Keep the invalid topic disabled --- app-constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app-constants.js b/app-constants.js index bce6ef8..5c8d576 100644 --- a/app-constants.js +++ b/app-constants.js @@ -96,6 +96,7 @@ const DisabledTopics = [ Topics.ChallengeTimelineTemplateCreated, Topics.ChallengeTimelineTemplateUpdated, Topics.ChallengeTimelineTemplateDeleted, + Topics.ChallengePhaseUpdated, Topics.ChallengePhaseDeleted, Topics.DefaultChallengeReviewerCreated, Topics.DefaultChallengeReviewerUpdated, From 979eb82d8013048fb13c0284cc28329f78ee78f4 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 16:41:16 +0530 Subject: [PATCH 16/18] Match autopilot bus payload --- src/common/helper.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 33db8fb..992995c 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1703,23 +1703,20 @@ async function sendPhaseChangeNotification(type, recipients, data) { ); - await postBusEvent('external.action.email', { - notifications: [ - { - serviceId: 'email', - type, - details: { - from: config.EMAIL_FROM, - recipients: [...safeRecipients], - cc: [...(settings.cc || [])], - data: { - ...data, - }, - sendgridTemplateId: settings.sendgridTemplateId, - version: 'v3', - }, - }, - ], + await postBusEvent('external.action.email', + { + from: config.EMAIL_FROM, + replyTo: config.EMAIL_FROM, + recipients: safeRecipients, + data: data, + sendgrid_template_id: settings.sendgridTemplateId, + version: 'v3', + }, + ); + + logger.debug(`sendPhaseChangeNotification: published`, { + type, + recipientsCount: safeRecipients.length, }); } catch (e) { logger.debug(`Failed to post notification ${type}: ${e.message}`); From 867eec448d9b7d5aced9f9d00f98b4130d2809bd Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 16:53:22 +0530 Subject: [PATCH 17/18] cleanup logs --- src/common/helper.js | 20 -------------------- src/services/ChallengePhaseService.js | 6 ------ 2 files changed, 26 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 992995c..aed437b 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1688,21 +1688,6 @@ async function sendPhaseChangeNotification(type, recipients, data) { return; } - logger.debug( - `sendPhaseChangeNotification: preparing email`, - { - type, - recipientsCount: safeRecipients.length, - recipients: safeRecipients, - } - ); - - logger.debug( - `sendPhaseChangeNotification: payload`, - data - ); - - await postBusEvent('external.action.email', { from: config.EMAIL_FROM, @@ -1713,11 +1698,6 @@ async function sendPhaseChangeNotification(type, recipients, data) { version: 'v3', }, ); - - logger.debug(`sendPhaseChangeNotification: published`, { - type, - recipientsCount: safeRecipients.length, - }); } catch (e) { logger.debug(`Failed to post notification ${type}: ${e.message}`); } diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index 530f63f..4c6c4da 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -851,12 +851,6 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) ) ); - logger.debug(`phase change: resolved emails`, { - challengeId, - emailsCount: recipients.length, - emails: recipients, - }); - if (!recipients.length) { logger.debug( `phase change notification skipped: no recipients for challenge ${challengeId}` From 1c0edd7104d7d08101716505de7ec75dda5f58de Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 20 Feb 2026 17:02:02 +0530 Subject: [PATCH 18/18] Fix JSdocs --- src/common/helper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index aed437b..d9e3ef9 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1644,6 +1644,7 @@ async function sendSelfServiceNotification(type, recipients, data) { * @param {String} challenge name * @param {String} challenge phase name * @param {String} operation to be performed on the phase - open | close | reopen + * @param {String|Date} at - The date/time when the phase opened/closed */ function buildPhaseChangeEmailData({ challengeId, challengeName, phaseName, operation, at }) { const isOpen = operation === 'open' || operation === 'reopen'; @@ -1663,7 +1664,7 @@ function buildPhaseChangeEmailData({ challengeId, challengeName, phaseName, oper /** * Send phase change notification * @param {String} type the notification type - * @param {Array} recipients the array of recipients in { userId || email || handle } format + * @param {Array} recipients the array of recipients emails * @param {Object} data the data */ async function sendPhaseChangeNotification(type, recipients, data) {