From 395b95dae2a5b4a75b7745347e5ee2fe54d6d47e Mon Sep 17 00:00:00 2001 From: jmgasper Date: Fri, 24 Apr 2026 09:31:12 +1000 Subject: [PATCH] Add optional flag to registration to indicate whether or not to send the registration email, useful for when we import historical challenges into the new platform and don't want to send emails to members. --- docs/swagger.yaml | 28 +++++++++++- src/services/ResourceService.js | 16 +++++-- test/unit/createResource.test.js | 76 ++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 259c01f..67a8a12 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -135,7 +135,7 @@ paths: in: body required: true schema: - $ref: '#/definitions/ResourceRequestBody' + $ref: '#/definitions/CreateResourceRequestBody' responses: '200': description: OK - the request was successful @@ -741,6 +741,32 @@ definitions: type: string format: UUID description: The resource role ID + CreateResourceRequestBody: + type: object + required: + - challengeId + - memberHandle + - memberId + - roleId + properties: + challengeId: + type: string + format: UUID + description: The challenge id + memberHandle: + type: string + description: The member handle + memberId: + type: string + description: The member id + roleId: + type: string + format: UUID + description: The resource role ID + sendEmail: + type: boolean + default: true + description: Set to `false` to create the resource without emitting the registration email event. ResourceRolePhaseDependencyRequestBody: type: object required: diff --git a/src/services/ResourceService.js b/src/services/ResourceService.js index 335f96e..6305168 100644 --- a/src/services/ResourceService.js +++ b/src/services/ResourceService.js @@ -492,12 +492,16 @@ async function init (currentUser, challengeId, resource, isCreated) { * Create resource for a challenge. * @param {Object} currentUser the current user * @param {Object} resource the resource to be created + * @param {Boolean} [resource.sendEmail=true] whether submitter registration should emit + * the registration email event * @returns {Object} the created resource */ async function createResource (currentUser, resource) { try { const challengeId = resource.challengeId const { memberId, handle, email, challenge, closeRegistration } = await init(currentUser, challengeId, resource, true) + const shouldSendRegistrationEmail = resource.sendEmail !== false + const resourceData = _.omit(resource, 'sendEmail') const timelineTemplateId = _.get(challenge, 'timelineTemplateId', null) @@ -507,7 +511,7 @@ async function createResource (currentUser, resource) { createdBy: _.toString(memberId), createdAt: moment().utc().format(), memberHandle: handle - }, resource) + }, resourceData) const createdResource = await prisma.resource.create({ data: prismaData, include: { @@ -537,7 +541,9 @@ async function createResource (currentUser, resource) { logger.warn(`Failed to increment numOfRegistrants for challenge ${challengeId}: ${e}`) } } - if (!_.get(challenge, 'task.isTask', false) && resource.roleId === config.SUBMITTER_RESOURCE_ROLE_ID) { + if (!_.get(challenge, 'task.isTask', false) && + resource.roleId === config.SUBMITTER_RESOURCE_ROLE_ID && + shouldSendRegistrationEmail) { const forumUrl = _.get(challenge, 'discussions[0].url') let templateId = config.REGISTRATION_EMAIL.SENDGRID_TEMPLATE_ID if (_.isUndefined(forumUrl)) { @@ -595,13 +601,15 @@ createResource.schema = { challengeId: Joi.id(), memberId: Joi.string(), memberHandle: Joi.string().required(), - roleId: Joi.id() + roleId: Joi.id(), + sendEmail: Joi.boolean().default(true) }), Joi.object().keys({ challengeId: Joi.id(), memberId: Joi.string().required(), memberHandle: Joi.string(), - roleId: Joi.id() + roleId: Joi.id(), + sendEmail: Joi.boolean().default(true) }) ) } diff --git a/test/unit/createResource.test.js b/test/unit/createResource.test.js index 68416e2..8b52055 100644 --- a/test/unit/createResource.test.js +++ b/test/unit/createResource.test.js @@ -265,6 +265,69 @@ module.exports = describe('Create resource', () => { await assertResource(ret.id, ret) }) + it('create submitter resource sends registration email by default', async () => { + const entity = resources.createBody('emailnotifyuser', submitterRoleId, challengeId3) + const postedEvents = [] + const originalPostEvent = helper.postEvent + let createdResourceId + + helper.postEvent = async (topic, payload) => { + postedEvents.push({ topic, payload }) + } + + try { + const ret = await service.createResource(user.m2m, entity) + createdResourceId = ret.id + const emailEvent = postedEvents.find((event) => event.topic === config.EMAIL_NOTIFICATIN_TOPIC) + + should.exist(emailEvent) + should.deepEqual(emailEvent.payload.recipients, ['test@topcoder.com']) + should.equal(emailEvent.payload.data.handle, 'emailnotifyuser') + } finally { + helper.postEvent = originalPostEvent + if (createdResourceId) { + await prisma.resource.deleteMany({ + where: { id: createdResourceId } + }) + } + } + }) + + it('create submitter resource skips registration email when sendEmail is false', async () => { + const entity = { + ...resources.createBody('emailnotifyuser-noemail', submitterRoleId, challengeId3), + sendEmail: false + } + const postedEvents = [] + const originalPostEvent = helper.postEvent + let createdResourceId + + helper.postEvent = async (topic, payload) => { + postedEvents.push({ topic, payload }) + } + + try { + const ret = await service.createResource(user.m2m, entity) + createdResourceId = ret.id + + should.equal( + postedEvents.some((event) => event.topic === config.EMAIL_NOTIFICATIN_TOPIC), + false + ) + should.equal( + postedEvents.some((event) => event.topic === config.RESOURCE_CREATE_TOPIC), + true + ) + } finally { + helper.postEvent = originalPostEvent + if (createdResourceId) { + await prisma.resource.deleteMany({ + where: { id: createdResourceId } + }) + } + } + }) + it('copilot can manage resources without full access flags', async () => { const originalRole = await helper.getById('ResourceRole', copilotRoleId) await ResourceRoleService.updateResourceRole(user.admin, copilotRoleId, { @@ -385,6 +448,19 @@ module.exports = describe('Create resource', () => { }) } + it('test invalid parameters, sendEmail must be boolean', async () => { + try { + const entity = { + ..._.cloneDeep(testBody), + sendEmail: 'invalid' + } + await service.createResource(user.m2m, entity) + throw new Error('should not throw error here') + } catch (err) { + assertValidationError(err, '"sendEmail" must be a boolean') + } + }) + for (const requiredField of requiredFields) { it(`test invalid parameters, required field ${requiredField} is missing`, async () => { let entity = _.cloneDeep(testBody)