diff --git a/packages/component-email-templating/README.md b/packages/component-email-templating/README.md index 7bb4beb6968da7e1f6eea8f912d37369b15c8786..0249e6568fcda89d746eee9ed34478112184fdbe 100644 --- a/packages/component-email-templating/README.md +++ b/packages/component-email-templating/README.md @@ -56,13 +56,15 @@ The `Email` class also provides a `constructor` whose properties will be used wh  ```javascript - const emailTemplate = require('@pubsweet/component-email-template') + const EmailTemplate = require('@pubsweet/component-email-template') const config = require('config') const { name: journalName, fromEmail: staffEmail } = config.get('journal') + + const paragraph = `We are please to inform you that the manuscript has passed the technical check process and is now submitted. Please click the link below to access the manuscript.` const sendNotifications = ({ user, editor, collection, fragment }) => { - const email = new emailTemplate({ + const email = new EmailTemplate({ type: 'user', fromEmail, toUser: { @@ -74,18 +76,19 @@ The `Email` class also provides a `constructor` whose properties will be used wh signatureJournal: journalName, signatureName: `${editor.name}`, subject: `${collection.customId}: Manuscript Update`, + paragraph, unsubscribeLink: `http://localhost:3000/unsubscribe/${user.id}`, ctaLink: `http://localhost:3000/projects/${collection.id}/versions/${ fragment.id }/details`, }, + bodyProps: { + hasLink: true, + hasIntro: true, + hasSignature: true } }) - const paragraph = `We are please to inform you that the manuscript has passed the technical check process and is now submitted. Please click the link below to access the manuscript.` - - const { html, text } = email.getNotificationBody({ emailBodyProps: { paragraph, hasLink: true, hasIntro: true, hasSignature: true }}) - - email.sendEmail({ html, text }) + return email.sendEmail() } ``` diff --git a/packages/component-email-templating/src/EmailTemplate.js b/packages/component-email-templating/src/EmailTemplate.js index 6c08722790cb83b9265917a2950ff56b56919216..ebffbc334d108fc7877c38938b61b1010cd5b263 100644 --- a/packages/component-email-templating/src/EmailTemplate.js +++ b/packages/component-email-templating/src/EmailTemplate.js @@ -1,6 +1,7 @@ const config = require('config') -const helpers = require('./helpers') +const htmlTemplateService = require('./HTMLTemplateService') const SendEmail = require('@pubsweet/component-send-email') +const logger = require('@pubsweet/logger') const configData = { logo: config.get('journal.logo'), @@ -10,9 +11,10 @@ const configData = { logoLink: config.get('journal.logoLink'), publisher: config.get('journal.publisher'), } -class Email { +class EmailTemplate { constructor({ type = 'system', + templateType = 'notification', fromEmail = config.get('journal.staffEmail'), toUser = { id: '', @@ -23,15 +25,19 @@ class Email { subject: '', ctaLink: '', ctaText: '', + paragraph: '', signatureName: '', unsubscribeLink: '', signatureJournal: '', }, + bodyProps = { hasLink: false, hasIntro: false, hasSignature: false }, }) { this.type = type this.toUser = toUser this.content = content + this.bodyProps = bodyProps this.fromEmail = fromEmail + this.templateType = templateType } set _toUser(newToUser) { @@ -46,41 +52,49 @@ class Email { this.content = newContent } - getInvitationBody({ emailBodyProps }) { + _getInvitationBody() { return { - html: helpers.getCompiledInvitationBody({ + html: htmlTemplateService.getCompiledInvitationBody({ replacements: { ...configData, ...this.content, - ...emailBodyProps, + ...this.bodyProps, toEmail: this.toUser.email, toUserName: this.toUser.name, }, }), - text: `${emailBodyProps.resend} ${emailBodyProps.upperContent} ${ - emailBodyProps.manuscriptText - } ${emailBodyProps.lowerContent} ${this.content.signatureName}`, + text: `${this.bodyProps.resend} ${this.bodyProps.upperContent} ${ + this.bodyProps.manuscriptText + } ${this.bodyProps.lowerContent} ${this.content.signatureName}`, } } - getNotificationBody({ emailBodyProps }) { + _getNotificationBody() { return { - html: helpers.getCompiledNotificationBody({ + html: htmlTemplateService.getCompiledNotificationBody({ replacements: { ...configData, ...this.content, - ...emailBodyProps, + ...this.bodyProps, toEmail: this.toUser.email, toUserName: this.toUser.name, }, }), - text: `${emailBodyProps.paragraph} ${this.content.ctaLink} ${ + text: `${this.content.paragraph} ${this.content.ctaLink} ${ this.content.ctaText } ${this.content.signatureName}`, } } - async sendEmail({ text, html }) { + _getEmailTemplate() { + return this.templateType === 'notification' + ? this._getNotificationBody() + : this._getInvitationBody() + } + + async sendEmail() { + const { html, text } = this._getEmailTemplate() + const { fromEmail: from } = this const { email: to } = this.toUser const { subject } = this.content @@ -95,10 +109,17 @@ class Email { try { await SendEmail.send(mailData) + logger.info( + `Sent email from: ${from} to: ${to} with subject: "${subject}"`, + ) + logger.debug( + `Sent email from: ${from} to: ${to} with subject: "${subject}"`, + ) } catch (e) { + logger.error(e) throw new Error(e) } } } -module.exports = Email +module.exports = EmailTemplate diff --git a/packages/component-email-templating/src/helpers.js b/packages/component-email-templating/src/HTMLTemplateService.js similarity index 90% rename from packages/component-email-templating/src/helpers.js rename to packages/component-email-templating/src/HTMLTemplateService.js index 8df0e7950b1093bd8df9e03fa096f48d26232644..6dc314521e5533c7fda4166de3726ee3c5c21980 100644 --- a/packages/component-email-templating/src/helpers.js +++ b/packages/component-email-templating/src/HTMLTemplateService.js @@ -24,14 +24,7 @@ const getCompiledInvitationBody = ({ replacements }) => { return compileBody({ fileName: 'invitation', context: replacements }) } -const readFile = path => - fs.readFileSync(path, { encoding: 'utf-8' }, (err, file) => { - if (err) { - throw err - } else { - return file - } - }) +const readFile = path => fs.readFileSync(path, 'utf-8') const handlePartial = (partialName, context = {}) => { let partial = readFile(`${__dirname}/templates/partials/${partialName}.hbs`) diff --git a/packages/component-email-templating/tests/helpers.test.js b/packages/component-email-templating/tests/HTMLTemplateService.test.js similarity index 82% rename from packages/component-email-templating/tests/helpers.test.js rename to packages/component-email-templating/tests/HTMLTemplateService.test.js index e655ec191cf012021e8de5ba690c88b0b5afbf38..f30ddf90902d5e14c49acf54dbae54c566b588f5 100644 --- a/packages/component-email-templating/tests/helpers.test.js +++ b/packages/component-email-templating/tests/HTMLTemplateService.test.js @@ -2,7 +2,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' process.env.SUPPRESS_NO_CONFIG_WARNING = true const { cloneDeep } = require('lodash') -const helpers = require('../src/helpers') +const htmlTemplateService = require('../src/HTMLTemplateService') const emailProps = { toUserName: 'Peter Griffin', @@ -19,7 +19,7 @@ describe('Email template helpers', () => { replacements = cloneDeep(emailProps) }) it('should return the notification HTML with CTA', () => { - const notificationBody = helpers.getCompiledNotificationBody({ + const notificationBody = htmlTemplateService.getCompiledNotificationBody({ replacements, }) @@ -32,7 +32,7 @@ describe('Email template helpers', () => { it('should return the notification HTML without CTA', () => { replacements.hasLink = false - const notificationBody = helpers.getCompiledNotificationBody({ + const notificationBody = htmlTemplateService.getCompiledNotificationBody({ replacements, }) expect(notificationBody).toContain('Peter Griffin') @@ -41,7 +41,7 @@ describe('Email template helpers', () => { }) it('should return the notification HTML without intro', () => { replacements.hasIntro = false - const notificationBody = helpers.getCompiledNotificationBody({ + const notificationBody = htmlTemplateService.getCompiledNotificationBody({ replacements, }) expect(notificationBody).not.toContain('Peter Griffin') @@ -50,7 +50,7 @@ describe('Email template helpers', () => { }) it('should return the notification HTML without signature', () => { replacements.hasSignature = false - const notificationBody = helpers.getCompiledNotificationBody({ + const notificationBody = htmlTemplateService.getCompiledNotificationBody({ replacements, }) expect(notificationBody).toContain('Peter Griffin') diff --git a/packages/component-email/src/routes/emails/emailCopy.js b/packages/component-email/src/routes/emails/emailCopy.js index d14178be9815e574a83f2e16aefff8495e59affd..34900efe02066d5f4eaff9a081dfc670bfefad4a 100644 --- a/packages/component-email/src/routes/emails/emailCopy.js +++ b/packages/component-email/src/routes/emails/emailCopy.js @@ -3,8 +3,6 @@ const config = require('config') const journalName = config.get('journal.name') const getEmailCopy = ({ emailType, role }) => { let paragraph - const hasIntro = true - const hasSignature = true switch (emailType) { case 'user-signup': paragraph = `Thank you for creating an account on Hindawi’s review system. @@ -23,7 +21,7 @@ const getEmailCopy = ({ emailType, role }) => { throw new Error(`The ${emailType} email type is not defined.`) } - return { paragraph, hasLink: true, hasIntro, hasSignature } + return { paragraph, hasLink: true, hasIntro: true, hasSignature: true } } module.exports = { diff --git a/packages/component-email/src/routes/emails/helpers.js b/packages/component-email/src/routes/emails/helpers.js deleted file mode 100644 index 35395e3da3bb1fac36c7d2006ca49122f955cb3e..0000000000000000000000000000000000000000 --- a/packages/component-email/src/routes/emails/helpers.js +++ /dev/null @@ -1,39 +0,0 @@ -const config = require('config') -const { services } = require('pubsweet-component-helper-service') - -const { getEmailCopy } = require('./emailCopy') - -const confirmSignUp = config.get('confirm-signup.url') - -module.exports = { - sendNewUserEmail: ({ email, role }) => { - email.content.subject = 'Confirm your account' - - const emailType = - role === 'Handling Editor' ? 'he-added-by-admin' : 'user-added-by-admin' - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - role, - emailType, - }), - }) - - email.sendEmail({ html, text }) - }, - sendSignupEmail: ({ email, baseUrl, user }) => { - email.content.subject = 'Confirm your email address' - email.content.ctaLink = services.createUrl(baseUrl, confirmSignUp, { - userId: user.id, - confirmationToken: user.accessTokens.confirmation, - }) - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType: 'user-signup', - }), - }) - - email.sendEmail({ html, text }) - }, -} diff --git a/packages/component-email/src/routes/emails/notifications.js b/packages/component-email/src/routes/emails/notifications.js index 5fe6fc1183bef9386e5b8f811a0ee6084e1387b3..1ff2853be2d98e67d8fb2475e0c9a37bee71ab2f 100644 --- a/packages/component-email/src/routes/emails/notifications.js +++ b/packages/component-email/src/routes/emails/notifications.js @@ -1,15 +1,23 @@ const config = require('config') const unsubscribeSlug = config.get('unsubscribe.url') -const resetPath = config.get('invite-reset-password.url') const { staffEmail, name: journalName } = config.get('journal') const Email = require('@pubsweet/component-email-templating') const { services } = require('pubsweet-component-helper-service') - -const { sendNewUserEmail, sendSignupEmail } = require('./helpers') +const { getEmailCopy } = require('./emailCopy') module.exports = { - async sendNotifications({ user, baseUrl, role }) { + sendNewUserEmail: ({ user, baseUrl, role }) => { + const resetPath = config.get('invite-reset-password.url') + + const emailType = + role === 'Handling Editor' ? 'he-added-by-admin' : 'user-added-by-admin' + + const { paragraph, ...bodyProps } = getEmailCopy({ + role, + emailType, + }) + const email = new Email({ type: 'user', fromEmail: `${journalName} <${staffEmail}>`, @@ -18,6 +26,7 @@ module.exports = { name: `${user.lastName}`, }, content: { + subject: 'Confirm your account', ctaLink: services.createUrl(baseUrl, resetPath, { email: user.email, token: user.accessTokens.passwordReset, @@ -28,18 +37,49 @@ module.exports = { country: user.country, }), ctaText: 'CONFIRM ACCOUNT', + paragraph, + signatureName: 'Hindawi', + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: user.id, + token: user.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) + + return email.sendEmail() + }, + sendSignupEmail: ({ user, baseUrl }) => { + const confirmSignUp = config.get('confirm-signup.url') + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'user-signup', + }) + + const email = new Email({ + type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: user.email, + name: `${user.lastName}`, + }, + content: { + subject: 'Confirm your email address', + ctaLink: services.createUrl(baseUrl, confirmSignUp, { + userId: user.id, + confirmationToken: user.accessTokens.confirmation, + }), + ctaText: 'CONFIRM ACCOUNT', + paragraph, signatureName: 'Hindawi', unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { id: user.id, token: user.accessTokens.unsubscribe, }), }, + bodyProps, }) - if (role) { - sendNewUserEmail({ email, role }) - } else { - sendSignupEmail({ email, baseUrl, user }) - } + return email.sendEmail() }, } diff --git a/packages/component-email/src/routes/emails/post.js b/packages/component-email/src/routes/emails/post.js index 246322fa3a7da378a16d3ade2b684c405b6ec5b8..67e071e283188b1ed038a3bdf02941ceff7ca862 100644 --- a/packages/component-email/src/routes/emails/post.js +++ b/packages/component-email/src/routes/emails/post.js @@ -16,53 +16,49 @@ module.exports = models => async (req, res) => { return res.status(400).json({ error: `Email type ${type} is not defined.` }) } - const UserModel = models.User - + let user try { - const user = await UserModel.findByEmail(email) + user = await models.User.findByEmail(email) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'User') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } - if (type === 'signup') { - if (!user.accessTokens.confirmation) { - return res - .status(400) - .json({ error: 'User does not have a confirmation token.' }) - } + if (type === 'signup') { + if (!user.accessTokens.confirmation) { + return res + .status(400) + .json({ error: 'User does not have a confirmation token.' }) } - + notifications.sendSignupEmail({ user, baseUrl: services.getBaseUrl(req) }) + } else if (type === 'invite') { let emailRole - if (type === 'invite') { - switch (role) { - case 'handlingEditor': - emailRole = 'Handling Editor' - break - case 'editorInChief': - emailRole = 'Editor in Chief' - break - case 'admin': - emailRole = 'Administrator' - break - case 'author': - emailRole = 'Author' - break - default: - return res.status(400).json({ - error: `Role ${role} is not defined.`, - }) - } + switch (role) { + case 'handlingEditor': + emailRole = 'Handling Editor' + break + case 'editorInChief': + emailRole = 'Editor in Chief' + break + case 'admin': + emailRole = 'Administrator' + break + case 'author': + emailRole = 'Author' + break + default: + return res.status(400).json({ + error: `Role ${role} is not defined.`, + }) } - - notifications.sendNotifications({ + notifications.sendNewUserEmail({ user, - role: emailRole, - UserModel: models.User, baseUrl: services.getBaseUrl(req), - }) - - return res.status(200).json({}) - } catch (e) { - const notFoundError = await services.handleNotFoundError(e, 'User') - return res.status(notFoundError.status).json({ - error: notFoundError.message, + role: emailRole, }) } + + return res.status(200).json({}) } diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index 5686d03f2ee8df3c62d1e212fea56cc3bc5a46e0..6b3b6764bc654b1f05ea8b4402645649abf7243a 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -56,10 +56,9 @@ module.exports = models => async (req, res) => { user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() - notifications.sendNotifications({ + notifications.sendInvitedHEEmail({ models, collection, - isEiC: true, invitedHE: user, isCanceled: true, baseUrl: services.getBaseUrl(req), diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js b/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js deleted file mode 100644 index 11a81820196eb1e7d4ac0048892def94fa91b031..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js +++ /dev/null @@ -1,78 +0,0 @@ -const config = require('config') -const { services } = require('pubsweet-component-helper-service') - -const { getEmailCopy } = require('./emailCopy') - -const unsubscribeSlug = config.get('unsubscribe.url') - -module.exports = { - sendInvitedHEEmail: ({ - email, - eicName, - baseUrl, - customId, - titleText, - isCanceled, - handlingEditor, - }) => { - email.toUser = { - email: handlingEditor.email, - } - - email.content.subject = isCanceled - ? `${customId}: Editor invitation cancelled` - : `${customId}: Invitation to edit a manuscript` - - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: handlingEditor.id, - token: handlingEditor.accessTokens.unsubscribe, - }, - ) - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - titleText, - targetUserName: eicName, - emailType: isCanceled ? 'he-revoked' : 'he-assigned', - }), - }) - - email.sendEmail({ html, text }) - }, - sendEiCEmail: async ({ - eic, - email, - baseUrl, - customId, - comments, - titleText, - isAccepted, - targetUserName, - }) => { - email.content.subject = isAccepted - ? `${customId}: Editor invitation accepted` - : `${customId}: Editor invitation declined` - - const emailType = isAccepted ? 'he-accepted' : 'he-declined' - - email.toUser = { - email: eic.email, - } - - email.content.unsubscribeLink = services.createUrl(baseUrl) - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - comments, - emailType, - titleText, - targetUserName, - }), - }) - - email.sendEmail({ html, text }) - }, -} diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js index 65e1375189adc5fc06576082ad8c8b206ae35dc0..2b85380e873f1a686ed362c33c0b991eabac3aef 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js +++ b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js @@ -10,19 +10,77 @@ const { Fragment, } = require('pubsweet-component-helper-service') -const { sendInvitedHEEmail, sendEiCEmail } = require('./helpers') +const { getEmailCopy } = require('./emailCopy') + +const unsubscribeSlug = config.get('unsubscribe.url') module.exports = { - async sendNotifications({ - reason, + sendInvitedHEEmail: async ({ baseUrl, invitedHE, collection, - isEiC = false, isCanceled = false, + models: { User: UserModel, Fragment: FragmentModel }, + }) => { + const fragmentId = last(collection.fragments) + const fragment = await FragmentModel.find(fragmentId) + const fragmentHelper = new Fragment({ fragment }) + const { title } = await fragmentHelper.getFragmentData() + const { submittingAuthor } = await fragmentHelper.getAuthorData({ + UserModel, + }) + + const titleText = `the manuscript titled "${title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.lastName}` + + const userHelper = new User({ UserModel }) + const eics = await userHelper.getEditorsInChief() + const eic = eics[0] + const eicName = `${eic.firstName} ${eic.lastName}` + const { customId } = collection + + const { paragraph, ...bodyProps } = getEmailCopy({ + titleText, + targetUserName: eicName, + emailType: isCanceled ? 'he-revoked' : 'he-assigned', + }) + + const email = new Email({ + type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: invitedHE.email, + }, + content: { + subject: isCanceled + ? `${customId}: Editor invitation cancelled` + : `${customId}: Invitation to edit a manuscript`, + paragraph, + signatureName: eicName, + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: invitedHE.id, + token: invitedHE.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) + + return email.sendEmail() + }, + sendEiCEmail: async ({ + reason, + baseUrl, + invitedHE, + collection, isAccepted = false, models: { User: UserModel, Fragment: FragmentModel }, - }) { + }) => { const fragmentId = last(collection.fragments) const fragment = await FragmentModel.find(fragmentId) const fragmentHelper = new Fragment({ fragment }) @@ -41,40 +99,37 @@ module.exports = { const eicName = `${eic.firstName} ${eic.lastName}` const { customId } = collection + const emailType = isAccepted ? 'he-accepted' : 'he-declined' + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType, + titleText, + comments: reason ? `Reason: "${reason}"` : '', + targetUserName: `${invitedHE.lastName}`, + }) + const email = new Email({ type: 'user', fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: eic.email, + }, content: { + subject: isAccepted + ? `${customId}: Editor invitation accepted` + : `${customId}: Editor invitation declined`, + paragraph, signatureName: eicName, ctaLink: services.createUrl( baseUrl, `/projects/${collection.id}/versions/${fragment.id}/details`, ), ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(baseUrl), }, + bodyProps, }) - if (isEiC) { - sendInvitedHEEmail({ - email, - baseUrl, - eicName, - customId, - titleText, - isCanceled, - handlingEditor: invitedHE, - }) - } else { - sendEiCEmail({ - eic, - email, - baseUrl, - customId, - titleText, - isAccepted, - comments: reason ? `Reason: "${reason}"` : '', - targetUserName: `${invitedHE.lastName}`, - }) - } + return email.sendEmail() }, } diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index 28452a347b73af471b0ff6eaaa8bbffc8e259df9..792711ff1717bb6104a72b5b7e9eb3bed4cbf0c4 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -10,8 +10,7 @@ module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params const { isAccepted, reason } = req.body - const UserModel = models.User - const user = await UserModel.find(req.user) + const user = await models.User.find(req.user) try { const collection = await models.Collection.find(collectionId) @@ -53,7 +52,7 @@ module.exports = models => async (req, res) => { await collection.save() - notifications.sendNotifications({ + notifications.sendEiCEmail({ models, reason, collection, diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index 6f28624ea068201d27a33274b2d4dac016d7a57f..7d404e0cec4a91d3a74808722332a7266963aa78 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -21,8 +21,7 @@ module.exports = models => async (req, res) => { error: `Role ${role} is invalid. Only handlingEditor is allowed.`, }) - const UserModel = models.User - const reqUser = await UserModel.find(req.user) + const reqUser = await models.User.find(req.user) if (email === reqUser.email && !reqUser.admin) { logger.error(`${reqUser.email} tried to invite his own email`) @@ -68,7 +67,7 @@ module.exports = models => async (req, res) => { const invitationHelper = new Invitation({ role }) try { - const user = await UserModel.findByEmail(email) + const user = await models.User.findByEmail(email) invitationHelper.userId = user.id let invitation = invitationHelper.getInvitation({ invitations: collection.invitations, @@ -92,10 +91,9 @@ module.exports = models => async (req, res) => { await collection.save() await collectionHelper.addHandlingEditor({ user, invitation }) - notifications.sendNotifications({ + notifications.sendInvitedHEEmail({ models, collection, - isEiC: true, invitedHE: user, baseUrl: services.getBaseUrl(req), }) diff --git a/packages/component-invite/src/routes/fragmentsInvitations/decline.js b/packages/component-invite/src/routes/fragmentsInvitations/decline.js index 8d7e5d97541e08dc4a9ae912867ce58a8e61df4e..41be1c65538556c53046c4c3956176cdfb74c810 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/decline.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/decline.js @@ -93,7 +93,7 @@ module.exports = models => async (req, res) => { user.teams = user.teams.filter(userTeamId => reviewerTeam.id !== userTeamId) await user.save() - notifications.sendNotifications({ + notifications.sendHandlingEditorEmail({ baseUrl, fragment, collection, diff --git a/packages/component-invite/src/routes/fragmentsInvitations/delete.js b/packages/component-invite/src/routes/fragmentsInvitations/delete.js index 306829c0a9c794b1de1b7fa4932fc69e2a3da46b..68c7e77c2d33ebffb425b0df485c2dbdd547e4c0 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/delete.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/delete.js @@ -70,14 +70,13 @@ module.exports = models => async (req, res) => { const baseUrl = services.getBaseUrl(req) - notifications.sendNotifications({ + notifications.sendReviewerEmail({ baseUrl, fragment, collection, reviewer: user, isCanceled: true, UserModel: models.User, - emailType: 'reviewer-cancel-invitation', }) return res.status(200).json({ fragment }) diff --git a/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js b/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js index 2d0df7d2f3effbbc2e51270f0082c74fd770bcdc..2a2e5e0d3f76877c410e835cbe3fc2c9e092da3d 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js @@ -11,7 +11,7 @@ const { services, Fragment } = require('pubsweet-component-helper-service') const { getEmailCopy } = require('./emailCopy') module.exports = { - async sendInvitations({ + async sendReviewInvitations({ resend, baseUrl, fragment, @@ -28,41 +28,36 @@ module.exports = { const { activeAuthors: authors, submittingAuthor, - } = await fragmentHelper.getAuthorData({ - UserModel, - }) + } = await fragmentHelper.getAuthorData({ UserModel }) - const subjectBaseText = `${collection.customId}: Review` - - let queryParams = { - invitationId: invitation.id, - agree: true, - } + let subjectBaseText = `${collection.customId}: Review invitation` const detailsPath = `/projects/${collection.id}/versions/${ fragment.id }/details` const declineLink = services.createUrl(baseUrl, inviteReviewerPath, { - ...queryParams, + invitationId: invitation.id, agree: false, fragmentId: fragment.id, collectionId: collection.id, invitationToken: invitedUser.accessTokens.invitation, }) - let agreeLink = services.createUrl(baseUrl, detailsPath, queryParams) + let agreeLink = services.createUrl(baseUrl, detailsPath, { + invitationId: invitation.id, + agree: true, + }) if (!invitedUser.isConfirmed) { - queryParams = { - ...queryParams, + agreeLink = services.createUrl(baseUrl, inviteReviewerPath, { + invitationId: invitation.id, agree: true, fragmentId: fragment.id, email: invitedUser.email, collectionId: collection.id, token: invitedUser.accessTokens.passwordReset, - } - agreeLink = services.createUrl(baseUrl, inviteReviewerPath, queryParams) + }) } const authorsList = authors.map( @@ -70,12 +65,35 @@ module.exports = { ) const handlingEditor = get(collection, 'handlingEditor', {}) + + const authorName = `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }` + + let daysExpected = 14 + let emailType = 'reviewer-invitation' + let titleText = `A manuscript titled <strong>"${title}"</strong> by <strong>${authorName}</strong> et al.` + + if (resend) { + emailType = 'reviewer-resend-invitation' + daysExpected = 0 + subjectBaseText = `${subjectBaseText} reminder` + titleText = `the manuscript titled "${title}" by ${authorName}` + } + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType, + titleText, + expectedDate: services.getExpectedDate({ timestamp, daysExpected }), + }) + const email = new Email({ type: 'user', + templateType: 'invitation', fromEmail: `${handlingEditor.name} <${staffEmail}>`, toUser: { email: invitedUser.email, - name: `${invitedUser.lastName}`, + name: invitedUser.lastName, }, content: { title, @@ -85,50 +103,17 @@ module.exports = { signatureJournal: journalName, signatureName: handlingEditor.name, authorsList: authorsList.join(', '), - subject: `${subjectBaseText} invitation`, + subject: subjectBaseText, + paragraph, detailsLink: services.createUrl(baseUrl, detailsPath), unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { id: invitedUser.id, token: invitedUser.accessTokens.unsubscribe, }), }, + bodyProps, }) - sendInvitedUserEmail({ - email, - title, - resend, - timestamp, - authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, - }) + return email.sendEmail() }, } - -const sendInvitedUserEmail = async ({ - email, - resend, - title, - authorName, - timestamp, -}) => { - let daysExpected = 14 - let emailType = 'reviewer-invitation' - let titleText = `A manuscript titled <strong>"${title}"</strong> by <strong>${authorName}</strong> et al.` - - if (resend) { - emailType = 'reviewer-resend-invitation' - daysExpected = 0 - email.content.subject = `${email.content.subject} reminder` - titleText = `the manuscript titled "${title}" by ${authorName}` - } - - const { html, text } = email.getInvitationBody({ - emailBodyProps: getEmailCopy({ - emailType, - titleText, - expectedDate: services.getExpectedDate({ timestamp, daysExpected }), - }), - }) - - email.sendEmail({ html, text }) -} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js b/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js index c2c04f72476a23ce91749544a279e10a459b16c3..17b38e769558e10ac9f1a1a917356cec91190bbe 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js @@ -15,15 +15,14 @@ const { const { getEmailCopy } = require('./emailCopy') module.exports = { - async sendNotifications({ + sendHandlingEditorEmail: async ({ baseUrl, fragment, reviewer, UserModel, emailType, collection, - isCanceled = false, - }) { + }) => { const fragmentHelper = new Fragment({ fragment }) const { title } = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, @@ -40,119 +39,106 @@ module.exports = { const userHelper = new User({ UserModel }) const eicName = await userHelper.getEiCName() + const heUser = await UserModel.find(handlingEditor.id) + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType, + titleText, + expectedDate: services.getExpectedDate({ + timestamp: Date.now(), + daysExpected: 14, + }), + targetUserName: reviewer.lastName, + }) + const email = new Email({ - fromEmail: `${handlingEditor.name} <${staffEmail}>`, + fromEmail: `${eicName} <${staffEmail}>`, type: 'user', + toUser: { + email: heUser.email, + name: heUser.lastName, + }, content: { + subject: + emailType === 'reviewer-accepted' + ? `${collection.customId}: A reviewer has agreed` + : `${collection.customId}: A reviewer has declined`, + paragraph, ctaText: 'MANUSCRIPT DETAILS', - signatureJournal: journalName, - signatureName: handlingEditor.name, + signatureJournal: '', + signatureName: eicName, ctaLink: services.createUrl( baseUrl, `/projects/${collection.id}/versions/${fragment.id}/details`, ), + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: heUser.id, + token: heUser.accessTokens.unsubscribe, + }), }, + bodyProps, }) - if (emailType !== 'reviewer-declined') { - sendReviewerEmail({ - email, - baseUrl, - reviewer, - titleText, - isCanceled, - customId: collection.customId, - }) - } - - if (['reviewer-accepted', 'reviewer-declined'].includes(emailType)) { - const heUser = await UserModel.find(handlingEditor.id) - email.fromEmail = `${eicName} <${staffEmail}>` - sendHandlingEditorEmail({ - email, - eicName, - titleText, - emailType, - handlingEditor: heUser, - customId: collection.customId, - targetUserName: `${reviewer.lastName}`, - }) - } + return email.sendEmail() }, -} + sendReviewerEmail: async ({ + baseUrl, + fragment, + reviewer, + UserModel, + collection, + isCanceled = false, + }) => { + const fragmentHelper = new Fragment({ fragment }) + const { title } = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const { submittingAuthor } = await fragmentHelper.getAuthorData({ + UserModel, + }) -const sendHandlingEditorEmail = ({ - email, - eicName, - baseUrl, - customId, - titleText, - emailType, - handlingEditor, - targetUserName, -}) => { - email.toUser = { - email: handlingEditor.email, - name: handlingEditor.lastName, - } - - email.content.subject = - emailType === 'reviewer-accepted' - ? `${customId}: A reviewer has agreed` - : `${customId}: A reviewer has declined` - email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { - id: handlingEditor.id, - token: handlingEditor.accessTokens.unsubscribe, - }) - email.content.signatureName = eicName - email.content.signatureJournal = '' - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType, - titleText, - expectedDate: services.getExpectedDate({ - timestamp: Date.now(), - daysExpected: 14, - }), - targetUserName, - }), - }) - email.sendEmail({ html, text }) -} + const titleText = `the manuscript titled "${title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.lastName}` + + const handlingEditor = get(collection, 'handlingEditor') + + const emailType = isCanceled + ? 'reviewer-cancel-invitation' + : 'reviewer-thank-you' -const sendReviewerEmail = async ({ - email, - baseUrl, - reviewer, - customId, - titleText, - isCanceled, -}) => { - email.content.subject = isCanceled - ? `${customId}: Review no longer required` - : `${customId}: Thank you for agreeing to review` - - const emailType = isCanceled - ? 'reviewer-cancel-invitation' - : 'reviewer-thank-you' - - email.toUser = { - email: reviewer.email, - name: `${reviewer.lastName}`, - } - - email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { - id: reviewer.id, - token: reviewer.accessTokens.unsubscribe, - }) - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ + const { paragraph, ...bodyProps } = getEmailCopy({ emailType, titleText, - }), - }) + }) - email.sendEmail({ html, text }) + const email = new Email({ + fromEmail: `${handlingEditor.name} <${staffEmail}>`, + type: 'user', + toUser: { + email: reviewer.email, + name: `${reviewer.lastName}`, + }, + content: { + subject: isCanceled + ? `${collection.customId}: Review no longer required` + : `${collection.customId}: Thank you for agreeing to review`, + paragraph, + ctaText: 'MANUSCRIPT DETAILS', + signatureJournal: journalName, + signatureName: handlingEditor.name, + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: reviewer.id, + token: reviewer.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) + + return email.sendEmail() + }, } diff --git a/packages/component-invite/src/routes/fragmentsInvitations/patch.js b/packages/component-invite/src/routes/fragmentsInvitations/patch.js index 20d059bcb68b96842426a1f6a71c246c98238c5c..97b397be70feee1e04587001f69ba2fb25ad99bd 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/patch.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/patch.js @@ -90,7 +90,7 @@ module.exports = models => async (req, res) => { await user.save() } - notifications.sendNotifications({ + notifications.sendHandlingEditorEmail({ baseUrl, fragment, collection, @@ -100,6 +100,17 @@ module.exports = models => async (req, res) => { isAccepted === true ? 'reviewer-accepted' : 'reviewer-declined', }) + if (isAccepted) { + notifications.sendReviewerEmail({ + baseUrl, + fragment, + collection, + reviewer: user, + isCanceled: false, + UserModel: models.User, + }) + } + return res.status(200).json(invitation) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js index 43504b33418afb1807932ac303c275d65a291f84..1a441531473daef14c295e408fc86e7d57fed96d 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/post.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -120,7 +120,7 @@ module.exports = models => async (req, res) => { collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) } - emailInvitations.sendInvitations({ + emailInvitations.sendReviewInvitations({ resend, baseUrl, fragment, @@ -174,7 +174,7 @@ module.exports = models => async (req, res) => { parentObject: fragment, }) - emailInvitations.sendInvitations({ + emailInvitations.sendReviewInvitations({ baseUrl, fragment, collection, diff --git a/packages/component-manuscript-manager/src/notifications/notification.js b/packages/component-manuscript-manager/src/notifications/notification.js index dfb831c3585207a0aacbb7b0f20f4e8f5a20e4eb..506e9ba191a51fd7a1e34142dac3f5a03895de23 100644 --- a/packages/component-manuscript-manager/src/notifications/notification.js +++ b/packages/component-manuscript-manager/src/notifications/notification.js @@ -30,11 +30,17 @@ class Notification { } async notifyHEWhenReviewerSubmitsReport(reviewerLastName) { - const { eicName, titleText } = await this.getNotificationProperties() + const { eicName, titleText } = await this._getNotificationProperties() const handlingEditorId = get(this.collection, 'handlingEditor.id') const heUser = await this.UserModel.find(handlingEditorId) + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'he-review-submitted', + titleText, + targetUserName: reviewerLastName, + }) + const email = new Email({ type: 'user', toUser: { @@ -43,9 +49,10 @@ class Notification { }, fromEmail: `${eicName} <${staffEmail}>`, content: { + subject: `${this.collection.customId}: A review has been submitted`, + paragraph, signatureName: eicName, ctaText: 'MANUSCRIPT DETAILS', - subject: `${this.collection.customId}: A review has been submitted`, unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { id: heUser.id, token: heUser.accessTokens.unsubscribe, @@ -57,29 +64,31 @@ class Notification { }/details`, ), }, + bodyProps, }) - const emailBodyProps = getEmailCopy({ - emailType: 'he-review-submitted', - titleText, - targetUserName: reviewerLastName, - }) - - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } - // EiC decided to either publish or reject the manuscript async notifyHEWhenEiCMakesDecision() { const { eicName, titleText, recommendation, - } = await this.getNotificationProperties() + } = await this._getNotificationProperties() const handlingEditorId = get(this.collection, 'handlingEditor.id') const heUser = await this.UserModel.find(handlingEditorId) + const { paragraph, ...bodyProps } = getEmailCopy({ + titleText, + targetUserName: eicName, + emailType: + recommendation === 'publish' + ? 'he-manuscript-published' + : 'he-manuscript-rejected', + }) + const email = new Email({ type: 'user', toUser: { @@ -87,8 +96,9 @@ class Notification { }, fromEmail: `${journalName} <${staffEmail}>`, content: { - ctaText: 'MANUSCRIPT DETAILS', subject: `${this.collection.customId}: Editorial decision confirmed`, + paragraph, + ctaText: 'MANUSCRIPT DETAILS', unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { id: heUser.id, token: heUser.accessTokens.unsubscribe, @@ -100,27 +110,31 @@ class Notification { }/details`, ), }, + bodyProps, }) - const emailBodyProps = getEmailCopy({ - titleText, - targetUserName: eicName, - emailType: - recommendation === 'publish' - ? 'he-manuscript-published' - : 'he-manuscript-rejected', - }) - - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } async notifyHEWhenEiCReturnsToHE() { - const { eicName, titleText } = await this.getNotificationProperties() + const { eicName, titleText } = await this._getNotificationProperties() const handlingEditorId = get(this.collection, 'handlingEditor.id') const heUser = await this.UserModel.find(handlingEditorId) + const eicComments = chain(this.newRecommendation) + .get('comments') + .find(comm => !comm.public) + .get('content') + .value() + + const { paragraph, ...bodyProps } = getEmailCopy({ + titleText, + comments: eicComments, + targetUserName: eicName, + emailType: 'he-manuscript-return-with-comments', + }) + const email = new Email({ type: 'user', toUser: { @@ -132,6 +146,7 @@ class Notification { subject: `${ this.collection.customId }: Editorial decision returned with comments`, + paragraph, signatureName: eicName, ctaText: 'MANUSCRIPT DETAILS', unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { @@ -145,29 +160,22 @@ class Notification { }/details`, ), }, + bodyProps, }) - const eicComments = chain(this.newRecommendation) - .get('comments') - .find(comm => !comm.public) - .get('content') - .value() - - const emailBodyProps = getEmailCopy({ - titleText, - comments: eicComments, - targetUserName: eicName, - emailType: 'he-manuscript-return-with-comments', - }) - - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } async notifyEAWhenEiCMakesFinalDecision() { - const { eicName, titleText } = await this.getNotificationProperties() + const { eicName, titleText } = await this._getNotificationProperties() const subjectBaseText = `${this.collection.customId}: Manuscript` + const { paragraph, ...bodyProps } = getEmailCopy({ + eicName, + titleText, + emailType: 'eqa-manuscript-published', + }) + const email = new Email({ type: 'system', toUser: { @@ -176,23 +184,25 @@ class Notification { fromEmail: `${journalName} <${staffEmail}>`, content: { subject: `${subjectBaseText} decision finalized`, + paragraph, unsubscribeLink: this.baseUrl, }, + bodyProps, }) - const emailBodyProps = getEmailCopy({ - eicName, - titleText, - emailType: 'eqa-manuscript-published', - }) - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } async notifyEAWhenEiCRequestsEQAApproval() { - const { eicName } = await this.getNotificationProperties() + const { eicName } = await this._getNotificationProperties() const subjectBaseText = `${this.collection.customId}: Manuscript` + const { paragraph, ...bodyProps } = getEmailCopy({ + eicName, + customId: this.collection.customId, + emailType: 'eqa-manuscript-request-for-approval', + }) + const email = new Email({ type: 'system', toUser: { @@ -201,6 +211,7 @@ class Notification { fromEmail: `${journalName} <${staffEmail}>`, content: { subject: `${subjectBaseText} Request for EQA approval`, + paragraph, unsubscribeLink: this.baseUrl, ctaText: 'MAKE DECISION', ctaLink: services.createUrl( @@ -213,16 +224,10 @@ class Notification { }, ), }, + bodyProps, }) - const emailBodyProps = getEmailCopy({ - eicName, - customId: this.collection.customId, - emailType: 'eqa-manuscript-request-for-approval', - }) - - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } async notifyAuthorsWhenEiCMakesDecision() { @@ -232,7 +237,7 @@ class Notification { activeAuthors, recommendation, parsedFragment, - } = await this.getNotificationProperties() + } = await this._getNotificationProperties() const subjectOpts = { publish: `${this.collection.customId}: Manuscript accepted`, @@ -255,6 +260,12 @@ class Notification { }) : this.newRecommendation.comments[0].content + const { paragraph, ...bodyProps } = getEmailCopy({ + titleText, + emailType, + comments, + }) + activeAuthors.forEach(async author => { const email = new Email({ type: 'user', @@ -264,9 +275,10 @@ class Notification { }, fromEmail: `${eicName} <${staffEmail}>`, content: { + subject, + paragraph, signatureName: eicName, ctaText: 'MANUSCRIPT DETAILS', - subject, unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { id: author.id, token: author.accessTokens.unsubscribe, @@ -279,16 +291,10 @@ class Notification { ), signatureJournal: journalName, }, + bodyProps, }) - const emailBodyProps = getEmailCopy({ - titleText, - emailType, - comments, - }) - - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() }) } @@ -298,7 +304,7 @@ class Notification { titleText, recommendation, fragmentHelper, - } = await this.getNotificationProperties() + } = await this._getNotificationProperties() const emailType = recommendation === 'publish' @@ -315,7 +321,7 @@ class Notification { type: 'submitted', }) - const emailBodyProps = getEmailCopy({ + const { paragraph, ...bodyProps } = getEmailCopy({ emailType, titleText, }) @@ -329,9 +335,10 @@ class Notification { }, fromEmail: `${eicName} <${staffEmail}>`, content: { + subject: `${this.collection.customId}: ${subject}`, + paragraph, signatureName: eicName, ctaText: 'MANUSCRIPT DETAILS', - subject: `${this.collection.customId}: ${subject}`, unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { id: reviewer.id, token: reviewer.accessTokens.unsubscribe, @@ -344,10 +351,10 @@ class Notification { ), signatureJournal: journalName, }, + bodyProps, }) - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() }) } @@ -356,7 +363,7 @@ class Notification { eicName, submittingAuthor, parsedFragment: { title }, - } = await this.getNotificationProperties() + } = await this._getNotificationProperties() const authorNoteText = helpers.getPrivateNoteTextForAuthor({ newRecommendation: this.newRecommendation, @@ -364,6 +371,12 @@ class Notification { const signatureName = get(this.collection, 'handlingEditor.name', eicName) + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'author-request-to-revision', + titleText: `your submission "${title}" to ${journalName}`, + comments: authorNoteText, + }) + const email = new Email({ type: 'user', toUser: { @@ -372,10 +385,11 @@ class Notification { }, fromEmail: `${signatureName} <${staffEmail}>`, content: { + subject: `${this.collection.customId}: Revision requested`, + paragraph, signatureName, ctaText: 'MANUSCRIPT DETAILS', signatureJournal: journalName, - subject: `${this.collection.customId}: Revision requested`, unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { id: submittingAuthor.id, token: submittingAuthor.accessTokens.unsubscribe, @@ -387,16 +401,10 @@ class Notification { }/details`, ), }, + bodyProps, }) - const emailBodyProps = getEmailCopy({ - emailType: 'author-request-to-revision', - titleText: `your submission "${title}" to ${journalName}`, - comments: authorNoteText, - }) - - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } async notifyReviewersWhenHEMakesRecommendation() { @@ -404,7 +412,7 @@ class Notification { eicName, titleText, fragmentHelper, - } = await this.getNotificationProperties() + } = await this._getNotificationProperties() const signatureName = get(this.collection, 'handlingEditor.name', eicName) @@ -427,6 +435,8 @@ class Notification { }) const buildSendEmailFunction = emailBodyProps => reviewer => { + const { paragraph, ...bodyProps } = emailBodyProps + const email = new Email({ type: 'user', toUser: { @@ -435,9 +445,10 @@ class Notification { }, fromEmail: `${signatureName} <${staffEmail}>`, content: { + subject: `${this.collection.customId}: Review no longer required`, + paragraph, signatureName, ctaText: 'MANUSCRIPT DETAILS', - subject: `${this.collection.customId}: Review no longer required`, unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { id: reviewer.id, token: reviewer.accessTokens.unsubscribe, @@ -450,10 +461,10 @@ class Notification { ), signatureJournal: journalName, }, + bodyProps, }) - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() } acceptedReviewers.forEach( @@ -467,7 +478,7 @@ class Notification { eicName, titleText, recommendation, - } = await this.getNotificationProperties() + } = await this._getNotificationProperties() let emailType, subject switch (recommendation) { @@ -502,7 +513,7 @@ class Notification { const userHelper = new User({ UserModel: this.UserModel }) const editors = await userHelper.getEditorsInChief() - const emailBodyProps = getEmailCopy({ + const { paragraph, ...bodyProps } = getEmailCopy({ comments, emailType, titleText, @@ -518,8 +529,9 @@ class Notification { }, fromEmail: `${journalName} <${staffEmail}>`, content: { - signatureName: eicName, subject, + paragraph, + signatureName: eicName, ctaText: 'MANUSCRIPT DETAILS', unsubscribeLink: this.baseUrl, ctaLink: services.createUrl( @@ -529,55 +541,22 @@ class Notification { }/details`, ), }, + bodyProps, }) - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() }) } - async getNotificationProperties() { - const fragmentHelper = new Fragment({ fragment: this.fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: this.collection.handlingEditor, - }) - const { - submittingAuthor, - activeAuthors, - } = await fragmentHelper.getAuthorData({ - UserModel: this.UserModel, - }) - - const userHelper = new User({ UserModel: this.UserModel }) - const eicName = await userHelper.getEiCName() - - const titleText = `the manuscript titled "${parsedFragment.title}" by ${ - submittingAuthor.firstName - } ${submittingAuthor.lastName}` - - const { recommendation, recommendationType } = this.newRecommendation - - return { - recommendation, - recommendationType, - eicName, - titleText, - submittingAuthor, - activeAuthors, - parsedFragment, - fragmentHelper, - } - } - async notifyEiCWhenEQSAcceptsManuscript() { - const { submittingAuthor } = await this.getNotificationProperties() + const { submittingAuthor } = await this._getNotificationProperties() const userHelper = new User({ UserModel: this.UserModel }) const editors = await userHelper.getEditorsInChief() const fragmentHelper = new Fragment({ fragment: this.fragment }) const parsedFragment = await fragmentHelper.getFragmentData({}) - const emailBodyProps = getEmailCopy({ + const { paragraph, ...bodyProps } = getEmailCopy({ emailType: 'eic-manuscript-accepted-by-eqs', titleText: `manuscript titled "${parsedFragment.title}" by ${ submittingAuthor.firstName @@ -593,6 +572,7 @@ class Notification { fromEmail: `${journalName} <${staffEmail}>`, content: { subject: `${this.collection.customId}: New manuscript submitted`, + paragraph, ctaText: 'MANUSCRIPT DETAILS', unsubscribeLink: this.baseUrl, ctaLink: services.createUrl( @@ -602,20 +582,20 @@ class Notification { }/details`, ), }, + bodyProps, }) - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() }) } async notifyEiCWhenEQARejectsManuscript(comments) { - const { titleText } = await this.getNotificationProperties() + const { titleText } = await this._getNotificationProperties() const userHelper = new User({ UserModel: this.UserModel }) const editors = await userHelper.getEditorsInChief() - const emailBodyProps = getEmailCopy({ + const { paragraph, ...bodyProps } = getEmailCopy({ comments, titleText, emailType: 'eic-manuscript-returned-by-eqa', @@ -632,6 +612,7 @@ class Notification { subject: `${ this.collection.customId }: Manuscript returned with comments`, + paragraph, ctaText: 'MANUSCRIPT DETAILS', unsubscribeLink: this.baseUrl, ctaLink: services.createUrl( @@ -641,11 +622,44 @@ class Notification { }/details`, ), }, + bodyProps, }) - const { html, text } = email.getNotificationBody({ emailBodyProps }) - email.sendEmail({ html, text }) + return email.sendEmail() + }) + } + + async _getNotificationProperties() { + const fragmentHelper = new Fragment({ fragment: this.fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: this.collection.handlingEditor, + }) + const { + submittingAuthor, + activeAuthors, + } = await fragmentHelper.getAuthorData({ + UserModel: this.UserModel, }) + + const userHelper = new User({ UserModel: this.UserModel }) + const eicName = await userHelper.getEiCName() + + const titleText = `the manuscript titled "${parsedFragment.title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.lastName}` + + const { recommendation, recommendationType } = this.newRecommendation + + return { + recommendation, + recommendationType, + eicName, + titleText, + submittingAuthor, + activeAuthors, + parsedFragment, + fragmentHelper, + } } } diff --git a/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js index 7f0b8fd3d3364ddc038385f6424ec53b9578fba3..dcbe3c22f7278a55af53a9e9477f80860ea4609f 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js +++ b/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js @@ -15,34 +15,29 @@ const unsubscribeSlug = config.get('unsubscribe.url') const { getEmailCopy } = require('./emailCopy') module.exports = { - async sendNotifications({ - baseUrl, - fragment, - UserModel, - collection, - isNewVersion = false, - previousVersion, - isTechnicalCheck, - isMajorRecommendation, - }) { + async sendHandlingEditorEmail({ baseUrl, fragment, UserModel, collection }) { const fragmentHelper = new Fragment({ fragment }) const handlingEditor = get(collection, 'handlingEditor') const parsedFragment = await fragmentHelper.getFragmentData({ handlingEditor, }) - const { - submittingAuthor, - activeAuthors: authors, - } = await fragmentHelper.getAuthorData({ - UserModel, + + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'he-new-version-submitted', + titleText: `the manuscript titled "${parsedFragment.title}"`, }) - const { customId } = collection + const heUser = await UserModel.find(handlingEditor.id) const email = new Email({ type: 'user', fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: heUser.email, + }, content: { + subject: `${collection.customId}: Revision submitted`, + paragraph, signatureName: '', signatureJournal: journalName, ctaLink: services.createUrl( @@ -50,240 +45,199 @@ module.exports = { `/projects/${collection.id}/versions/${fragment.id}/details`, ), ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: heUser.id, + token: heUser.accessTokens.unsubscribe, + }), }, + bodyProps, }) - const userHelper = new User({ UserModel }) - const eicName = await userHelper.getEiCName() - - if (isNewVersion) { - const heUser = await UserModel.find(handlingEditor.id) - sendHandlingEditorEmail({ - email, - baseUrl, - customId, - title: parsedFragment.title, - handlingEditor: heUser, - }) - } - - if (isMajorRecommendation) { - sendReviewersEmail({ - email, - baseUrl, - customId, - UserModel, - previousVersion, - title: parsedFragment.title, - heName: collection.handlingEditor.name, - }) - } - - if (isTechnicalCheck) { - sendEQSEmail({ - email, - eicName, - baseUrl, - collection, - }) - sendAuthorsEmail({ - email, - baseUrl, - UserModel, - fragmentId: fragment.id, - fragmentAuthors: authors, - collectionId: collection.id, - title: parsedFragment.title, - submittingAuthorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, - }) - } + return email.sendEmail() }, -} - -const sendHandlingEditorEmail = ({ - email, - title, - baseUrl, - customId, - handlingEditor, -}) => { - const emailType = 'he-new-version-submitted' - - email.toUser = { - email: handlingEditor.email, - } - email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { - id: handlingEditor.id, - token: handlingEditor.accessTokens.unsubscribe, - }) - email.content.subject = `${customId}: Revision submitted` - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType, - titleText: `the manuscript titled "${title}"`, - }), - }) - email.sendEmail({ html, text }) -} - -const sendReviewersEmail = async ({ - email, - title, - heName, - baseUrl, - customId, - UserModel, - previousVersion, -}) => { - email.content.subject = `${customId}: A manuscript you reviewed has been revised` - email.content.signatureName = heName - email.fromEmail = `${heName} <${staffEmail}>` - const emailType = 'submitted-reviewers-after-revision' - const fragmentHelper = new Fragment({ fragment: previousVersion }) - const reviewers = await fragmentHelper.getReviewers({ + async sendReviewersEmail({ + baseUrl, + fragment, UserModel, - type: 'submitted', - }) - - reviewers.forEach(reviewer => { - email.toUser = { - email: reviewer.email, - name: `${reviewer.lastName}`, - } + collection, + previousVersion, + }) { + const fragmentHelper = new Fragment({ fragment: previousVersion }) + const handlingEditor = get(collection, 'handlingEditor') + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor, + }) - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: reviewer.id, - token: reviewer.accessTokens.unsubscribe, - }, - ) + const reviewers = await fragmentHelper.getReviewers({ + UserModel, + type: 'submitted', + }) - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType, - titleText: `the manuscript titled "${title}"`, - expectedDate: services.getExpectedDate({ daysExpected: 14 }), - }), + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'submitted-reviewers-after-revision', + titleText: `the manuscript titled "${parsedFragment.title}"`, + expectedDate: services.getExpectedDate({ daysExpected: 14 }), }) - email.sendEmail({ html, text }) - }) -} + reviewers.forEach(reviewer => { + const email = new Email({ + type: 'user', + fromEmail: `${handlingEditor.name} <${staffEmail}>`, + toUser: { + email: reviewer.email, + name: `${reviewer.lastName}`, + }, + content: { + subject: `${ + collection.customId + }: A manuscript you reviewed has been revised`, + paragraph, + signatureName: handlingEditor.name, + signatureJournal: journalName, + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: reviewer.id, + token: reviewer.accessTokens.unsubscribe, + }), + }, + bodyProps, + }) -const sendEQSEmail = ({ email, eicName, baseUrl, collection }) => { - const emailType = 'eqs-manuscript-submitted' + return email.sendEmail() + }) + }, - email.toUser = { - email: staffEmail, - name: 'Editorial Assistant', - } + async sendEQSEmail({ baseUrl, fragment, UserModel, collection }) { + const userHelper = new User({ UserModel }) + const eicName = await userHelper.getEiCName() - email.content.unsubscribeLink = baseUrl - email.content.signatureName = eicName - email.content.subject = `Manuscript Submitted` - email.content.ctaLink = services.createUrl( - baseUrl, - config.get('eqs-decision.url'), - { - collectionId: collection.id, + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'eqs-manuscript-submitted', customId: collection.customId, - token: collection.technicalChecks.token, - }, - ) - email.content.ctaText = 'MAKE DECISION' + }) - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType, - customId: collection.customId, - }), - }) - email.sendEmail({ html, text }) -} + const email = new Email({ + type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: staffEmail, + name: 'Editorial Assistant', + }, + content: { + subject: `Manuscript Submitted`, + paragraph, + signatureName: eicName, + signatureJournal: journalName, + ctaLink: services.createUrl(baseUrl, config.get('eqs-decision.url'), { + collectionId: collection.id, + customId: collection.customId, + token: collection.technicalChecks.token, + }), + ctaText: 'MAKE DECISION', + unsubscribeLink: baseUrl, + }, + bodyProps, + }) -const sendAuthorsEmail = async ({ - email, - title, - baseUrl, - UserModel, - fragmentId, - collectionId, - fragmentAuthors, - submittingAuthorName, -}) => { - email.content.subject = `Manuscript submitted to ${journalName}` - email.content.signatureName = '' + return email.sendEmail() + }, - const userEmailData = await Promise.all( - fragmentAuthors.map(async author => { - const user = await UserModel.find(author.id) - return { - ...author, - isConfirmed: user.isConfirmed, - unsubscribeToken: user.accessTokens.unsubscribe, - passwordResetToken: user.accessTokens.passwordReset, - ...getEmailCopy({ + async sendAuthorsEmail({ baseUrl, fragment, UserModel, collection }) { + const fragmentHelper = new Fragment({ fragment }) + const handlingEditor = get(collection, 'handlingEditor') + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor, + }) + const { + submittingAuthor, + activeAuthors: fragmentAuthors, + } = await fragmentHelper.getAuthorData({ + UserModel, + }) + + const submittingAuthorName = `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }` + + const userEmailData = await Promise.all( + fragmentAuthors.map(async author => { + const { paragraph, ...bodyProps } = getEmailCopy({ emailType: author.isSubmitting ? 'submitting-author-manuscript-submitted' : 'coauthors-manuscript-submitted', titleText: author.isSubmitting - ? `the manuscript titled "${title}"` - : `The manuscript titled "${title}" has been submitted to ${journalName} by ${submittingAuthorName}`, - }), - } - }), - ) - - userEmailData.forEach(author => { - email.toUser = { - email: author.email, - name: `${author.lastName}`, - } - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: author.id, - token: author.unsubscribeToken, - }, + ? `the manuscript titled "${parsedFragment.title}"` + : `The manuscript titled "${ + parsedFragment.title + }" has been submitted to ${journalName} by ${submittingAuthorName}`, + }) + const user = await UserModel.find(author.id) + return { + author: Object.assign(author, { + isConfirmed: user.isConfirmed, + unsubscribeToken: user.accessTokens.unsubscribe, + passwordResetToken: user.accessTokens.passwordReset, + }), + paragraph, + bodyProps, + } + }), ) - if (author.isSubmitting) { - email.content.ctaLink = services.createUrl( - baseUrl, - `/projects/${collectionId}/versions/${fragmentId}/details`, - ) - email.content.ctaText = 'MANUSCRIPT DETAILS' - } else if (author.isConfirmed) { - email.content.ctaLink = services.createUrl(baseUrl, '') - email.content.ctaText = 'LOG IN' - } else { - email.content.ctaLink = services.createUrl(baseUrl, resetPath, { - email: author.email, - token: author.passwordResetToken, - firstName: author.firstName, - lastName: author.lastName, - affiliation: author.affiliation, - title: author.title, - country: author.country, + userEmailData.forEach(({ author, paragraph, bodyProps }) => { + const email = new Email({ + type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, + toUser: { + email: author.email, + name: `${author.lastName}`, + }, + content: { + subject: `Manuscript submitted to ${journalName}`, + paragraph, + signatureName: '', + signatureJournal: journalName, + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: author.id, + token: author.unsubscribeToken, + }), + }, + bodyProps, }) - email.content.ctaText = 'CONFIRM ACCOUNT' - } - const { html, text } = email.getNotificationBody({ - emailBodyProps: { - paragraph: author.paragraph, - hasLink: author.hasLink, - hasIntro: author.hasIntro, - hasSignature: author.hasSignature, - }, + if (author.isSubmitting) { + email.content.ctaLink = services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ) + email.content.ctaText = 'MANUSCRIPT DETAILS' + } else if (author.isConfirmed) { + email.content.ctaLink = services.createUrl(baseUrl, '') + email.content.ctaText = 'LOG IN' + } else { + email.content.ctaLink = services.createUrl(baseUrl, resetPath, { + email: author.email, + token: author.passwordResetToken, + firstName: author.firstName, + lastName: author.lastName, + affiliation: author.affiliation, + title: author.title, + country: author.country, + }) + email.content.ctaText = 'CONFIRM ACCOUNT' + } + + return email.sendEmail() }) - email.sendEmail({ html, text }) - }) + }, } diff --git a/packages/component-manuscript-manager/src/routes/fragments/patch.js b/packages/component-manuscript-manager/src/routes/fragments/patch.js index f28b8cc0847abbbdaff894bc7f4337572a28877c..396672ff07542005238642045ebe226e45d8fa3c 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js @@ -112,16 +112,23 @@ module.exports = models => async (req, res) => { collection.fragments.push(newFragment.id) collection.save() - notifications.sendNotifications({ - collection, - isNewVersion: true, + notifications.sendHandlingEditorEmail({ + baseUrl: services.getBaseUrl(req), fragment: newFragment, UserModel: models.User, - previousVersion: fragment, - baseUrl: services.getBaseUrl(req), - isMajorRecommendation: heRecommendation.recommendation === 'major', + collection, }) + if (heRecommendation.recommendation === 'major') { + notifications.sendReviewersEmail({ + baseUrl: services.getBaseUrl(req), + fragment: newFragment, + UserModel: models.User, + collection, + previousVersion: fragment, + }) + } + return res.status(200).json(newFragment) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') diff --git a/packages/component-manuscript-manager/src/routes/fragments/post.js b/packages/component-manuscript-manager/src/routes/fragments/post.js index 8463b54afefc6d3873350b0ea8d07bdf2bceadfd..0d2b1f900df0cf60913afbff6229dad71b2b9bd2 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/post.js +++ b/packages/component-manuscript-manager/src/routes/fragments/post.js @@ -68,11 +68,17 @@ module.exports = models => async (req, res) => { await sendMTSPackage(collection, fragment) } - notifications.sendNotifications({ + notifications.sendEQSEmail({ + fragment, + collection, + UserModel: models.User, + baseUrl: services.getBaseUrl(req), + }) + + notifications.sendAuthorsEmail({ fragment, collection, UserModel: models.User, - isTechnicalCheck: true, baseUrl: services.getBaseUrl(req), }) diff --git a/packages/component-user-manager/src/routes/users/emails/notifications.js b/packages/component-user-manager/src/routes/users/emails/notifications.js index b292e701971c0aa5098ad116a44a88cb8bcda44c..6c292ae2dcff7761c3146c157ec374923249bc74 100644 --- a/packages/component-user-manager/src/routes/users/emails/notifications.js +++ b/packages/component-user-manager/src/routes/users/emails/notifications.js @@ -10,39 +10,33 @@ const { name: journalName, staffEmail } = config.get('journal') const unsubscribeSlug = config.get('unsubscribe.url') module.exports = { - async sendNotifications({ user, baseUrl }) { + async sendForgotPasswordEmail({ user, baseUrl }) { + const { paragraph, ...bodyProps } = getEmailCopy({ + emailType: 'user-forgot-password', + }) + const email = new Email({ type: 'system', + toUser: { + email: user.email, + }, fromEmail: `${journalName} <${staffEmail}>`, content: { + subject: 'Forgot Password', ctaLink: services.createUrl(baseUrl, forgotPath, { email: user.email, token: user.accessTokens.passwordReset, }), ctaText: 'RESET PASSWORD', + paragraph, + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: user.id, + token: user.accessTokens.unsubscribe, + }), }, + bodyProps, }) - sendForgotPasswordEmail({ email, baseUrl, user }) + return email.sendEmail() }, } - -const sendForgotPasswordEmail = ({ email, baseUrl, user }) => { - email.toUser = { - email: user.email, - } - - email.content.subject = 'Forgot Password' - email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { - id: user.id, - token: user.accessTokens.unsubscribe, - }) - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType: 'user-forgot-password', - }), - }) - - email.sendEmail({ html, text }) -} diff --git a/packages/component-user-manager/src/routes/users/forgotPassword.js b/packages/component-user-manager/src/routes/users/forgotPassword.js index 55b750e69bfaa886aaae329843c1967e8be6c634..23b80f6e2d723cf955a653f66d4f735a3c432318 100644 --- a/packages/component-user-manager/src/routes/users/forgotPassword.js +++ b/packages/component-user-manager/src/routes/users/forgotPassword.js @@ -36,7 +36,7 @@ module.exports = models => async (req, res) => { user.passwordResetTimestamp = Date.now() await user.save() - notifications.sendNotifications({ + notifications.sendForgotPasswordEmail({ user, baseUrl: services.getBaseUrl(req), })