From 36281a56f549951de5c7024c16c2e913c3133edf Mon Sep 17 00:00:00 2001 From: Sebastian Mihalache <sebastian.mihalache@gmail.con> Date: Tue, 31 Jul 2018 18:00:58 +0300 Subject: [PATCH] feat: refactor/rebuild email templating service in component-invite and component-manuscript-manager --- .../src/services/Fragment.js | 19 +- .../src/services/email/Email.js | 185 +++--------------- .../src/services/email/helpers.js | 96 ++------- .../services/email/templates/invitation.html | 8 +- .../src/services/email/templates/noCTA.html | 3 - .../email/templates/notification.html | 7 +- .../{notificationBody.hbs => body.hbs} | 3 - .../partials/{mainButton.hbs => button.hbs} | 0 .../email/templates/partials/header.hbs | 2 +- .../{invitationButtons.hbs => invButtons.hbs} | 4 +- .../{invitationHeader.hbs => invHeader.hbs} | 2 +- ...onLowerContent.hbs => invLowerContent.hbs} | 4 +- ...nuscriptData.hbs => invManuscriptData.hbs} | 0 ...onUpperContent.hbs => invUpperContent.hbs} | 2 +- .../email/templates/partials/mainBody.hbs | 10 - .../partials/manuscriptDetailsLink.hbs | 1 - .../templates/partials/notificationHeader.hbs | 161 --------------- .../services/email/templates/simpleCTA.html | 4 - .../src/services/services.js | 14 ++ .../routes/collectionsInvitations/delete.js | 18 +- .../emails/emailCopy.js | 28 +++ .../emails/notifications.js | 144 ++++++++++++++ .../routes/collectionsInvitations/patch.js | 55 ++---- .../src/routes/collectionsInvitations/post.js | 22 ++- .../routes/fragmentsInvitations/decline.js | 29 +-- .../src/routes/fragmentsInvitations/delete.js | 32 ++- .../fragmentsInvitations/emails/emailCopy.js | 58 ++++++ .../emails/invitations.js | 136 +++++++++++++ .../emails/notifications.js | 148 ++++++++++++++ .../src/routes/fragmentsInvitations/patch.js | 63 +++--- .../src/routes/fragmentsInvitations/post.js | 60 +++--- .../collectionsInvitations/delete.test.js | 4 +- .../collectionsInvitations/patch.test.js | 6 +- .../tests/collectionsInvitations/post.test.js | 7 +- .../fragmentsInvitations/decline.test.js | 4 +- .../tests/fragmentsInvitations/delete.test.js | 5 +- .../tests/fragmentsInvitations/get.test.js | 7 +- .../tests/fragmentsInvitations/patch.test.js | 6 +- .../tests/fragmentsInvitations/post.test.js | 7 +- .../fragments/notifications/emailCopy.js | 25 +++ .../fragments/notifications/notifications.js | 176 +++++++++++++++++ .../src/routes/fragments/patch.js | 31 ++- .../src/routes/fragments/post.js | 23 +-- .../notifications/emailCopy.js | 74 +++++++ .../notifications/notifications.js | 109 ++++++++--- .../routes/fragmentsRecommendations/patch.js | 38 +--- .../routes/fragmentsRecommendations/post.js | 2 +- .../src/tests/fragments/patch.test.js | 4 +- .../src/tests/fragments/post.test.js | 5 +- .../fragmentsRecommendations/post.test.js | 5 +- 50 files changed, 1113 insertions(+), 743 deletions(-) delete mode 100644 packages/component-helper-service/src/services/email/templates/noCTA.html rename packages/component-helper-service/src/services/email/templates/partials/{notificationBody.hbs => body.hbs} (85%) rename packages/component-helper-service/src/services/email/templates/partials/{mainButton.hbs => button.hbs} (100%) rename packages/component-helper-service/src/services/email/templates/partials/{invitationButtons.hbs => invButtons.hbs} (96%) rename packages/component-helper-service/src/services/email/templates/partials/{invitationHeader.hbs => invHeader.hbs} (98%) rename packages/component-helper-service/src/services/email/templates/partials/{invitationLowerContent.hbs => invLowerContent.hbs} (84%) rename packages/component-helper-service/src/services/email/templates/partials/{manuscriptData.hbs => invManuscriptData.hbs} (100%) rename packages/component-helper-service/src/services/email/templates/partials/{invitationUpperContent.hbs => invUpperContent.hbs} (85%) delete mode 100644 packages/component-helper-service/src/services/email/templates/partials/mainBody.hbs delete mode 100644 packages/component-helper-service/src/services/email/templates/partials/manuscriptDetailsLink.hbs delete mode 100644 packages/component-helper-service/src/services/email/templates/partials/notificationHeader.hbs delete mode 100644 packages/component-helper-service/src/services/email/templates/simpleCTA.html create mode 100644 packages/component-invite/src/routes/collectionsInvitations/emails/emailCopy.js create mode 100644 packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js create mode 100644 packages/component-invite/src/routes/fragmentsInvitations/emails/emailCopy.js create mode 100644 packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js create mode 100644 packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js create mode 100644 packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js create mode 100644 packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js create mode 100644 packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js rename packages/component-manuscript-manager/src/{ => routes/fragmentsRecommendations}/notifications/notifications.js (72%) diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js index 1ad57433e..09b0e42fa 100644 --- a/packages/component-helper-service/src/services/Fragment.js +++ b/packages/component-helper-service/src/services/Fragment.js @@ -21,11 +21,16 @@ class Fragment { return owners } - async getFragmentData({ handlingEditor = {} }) { + async getFragmentData({ handlingEditor } = {}) { const { fragment: { metadata = {}, recommendations = [], id } } = this - const heRecommendation = recommendations.find( - rec => rec.userId === handlingEditor.id, - ) + let heRecommendation + + if (handlingEditor) { + heRecommendation = recommendations.find( + rec => rec.userId === handlingEditor.id, + ) + } + let { title = '', abstract = '' } = metadata const { type } = metadata title = title.replace(/<(.|\n)*?>/g, '') @@ -73,10 +78,6 @@ class Fragment { const userHelper = new User({ UserModel }) const activeAuthors = await userHelper.getActiveAuthors(authors) - // const authorsList = activeAuthors.map( - // author => `${author.firstName} ${author.lastName}`, - // ) - return { activeAuthors, submittingAuthor, @@ -97,7 +98,7 @@ class Fragment { ) : invitations.filter(inv => inv.role === role && inv.hasAnswer === false) - if (type === 'submitting') { + if (type === 'submitted') { filteredInvitations = filteredInvitations.filter(inv => recommendations.find( rec => diff --git a/packages/component-helper-service/src/services/email/Email.js b/packages/component-helper-service/src/services/email/Email.js index fe1714136..0114386f3 100644 --- a/packages/component-helper-service/src/services/email/Email.js +++ b/packages/component-helper-service/src/services/email/Email.js @@ -13,12 +13,9 @@ class Email { }, content = { subject: '', - comments: '', - titleText: '', - timestamp: '', - detailsLink: '', + ctaLink: '', + ctaText: '', signatureName: '', - sourceUserName: '', unsubscribeLink: '', }, }) { @@ -39,164 +36,32 @@ class Email { this.content = newContent } - getBody({ emailType }) { - const replacements = { - hasLink: true, - toUserName: this.toUser.name, - beforeAnchor: 'Please visit the', - afterAnchor: 'to see the full review', - detailsLink: this.content.detailsLink, - signatureName: this.content.signatureName, - unsubscribeLink: this.content.unsubscribeLink, - } - switch (emailType) { - case 'unassign-reviewer': - replacements.paragraph = `You are no longer needed to review ${ - this.content.titleText - }. If you have comments on this manuscript you believe the Editor should - see, please email them to ${config.get( - 'mailer.from', - )} as soon as possible. Thank you for your - time and I hope you will consider reviewing for Hindawi again.` - break - case 'review-submitted': - replacements.paragraph = `We are pleased to inform you that Dr. ${ - this.content.sourceUserName - } has submitted a review for ${this.content.titleText}.` - break - case 'reviewer-agreed': - // subject = `${meta.collection.customId}: Manuscript Reviews` - replacements.paragraph = `We are pleased to inform you that Dr. ${ - this.content.sourceUserName - } has agreed to review ${this.content.titleText}. <br/> - You should receive the report by Dr. ${ - this.content.sourceUserName - } before ${helpers.getExpectedDate(this.content.timestamp, 14)}.` - replacements.beforeAnchor = - 'If you have any queries, or would like to send a reminder if you no report has been submitted, then please visit the' - break - case 'reviewer-declined': - // subject = `${meta.collection.customId}: Manuscript Reviews` - replacements.paragraph = `We regret to inform you that Dr. ${ - this.content.sourceUserName - } has declined to review ${this.content.titleText}.` - replacements.afterAnchor = - 'to see if you need to invite any additional reviewers in order to reach a decision on the manuscript' - break - case 'reviewer-thank-you': - // subject = `${meta.collection.customId}: Manuscript Review` - replacements.paragraph = `Thank you for agreeing to review ${ - this.content.titleText - }.` - replacements.beforeAnchor = - 'You can view the full PDF file of the manuscript and post your review report using the following URL:' - replacements.afterAnchor = '' - break - case 'agreed-reviewers-after-he-recommendation': - replacements.hasLink = false - replacements.paragraph = `I appreciate any time you may have spent reviewing ${ - this.content.titleText - }. However, an editorial decision has been made and the review of this manuscript is now complete. I apologize for any inconvenience. <br/> - If you have comments on this manuscript you believe the Editor should see, please email them to Hindawi as soon as possible. <br/> - Thank you for your interest and I hope you will consider reviewing for Hindawi again.` - break - case 'pending-reviewers-after-he-recommendation': - replacements.hasLink = false - - replacements.paragraph = `An editorial decision has been made regarding ${ - this.content.titleText - }. So, you do not need to proceed with the review of this manuscript. <br/><br/> - If you have comments on this manuscript you believe the Editor should see, please email them to Hindawi as soon as possible.` - break - case 'eic-recommendation': - // subject = `${meta.collection.customId}: Manuscript Recommendation` - replacements.beforeAnchor = - 'For more information about what is required, please visit the ' - break - case 'author-request-to-revision': - replacements.paragraph = `In order for ${ - this.content.titleText - } to proceed to publication, there needs to be a revision. <br/><br/> - ${this.content.comments}<br/><br/>` - replacements.beforeAnchor = - 'For more information about what is required, please visit the ' - break - case 'author-manuscript-rejected': - replacements.paragraph = `I am sorry to inform you that ${ - this.content.titleText - } has been rejected for publication. <br/><br/> - ${this.content.comments}<br/><br/>` - replacements.hasLink = false - break - case 'author-manuscript-published': - replacements.paragraph = `I am delighted to inform you that ${ - this.content.titleText - } has passed through the review process and will be published in Hindawi.<br/><br/> - ${this.content.comments}<br/><br/> - Thanks again for choosing to publish with us.` - replacements.hasLink = false - break - case 'he-manuscript-rejected': - // subject = meta.emailSubject - replacements.hasLink = false - replacements.paragraph = `Thank you for your recommendation to reject ${ - this.content.titleText - } based on the reviews you received.<br/><br/> - I can confirm this article has now been rejected.` - break - case 'he-manuscript-published': - replacements.hasLink = false - replacements.paragraph = `Thank you for your recommendation to publish ${ - this.content.titleText - } based on the reviews you received.<br/><br/> - I can confirm this article will now go through to publication.` - break - case 'he-manuscript-return-with-comments': - // subject = meta.emailSubject - replacements.hasLink = false - replacements.paragraph = `Thank you for your recommendation for ${ - this.content.titleText - } based on the reviews you received.<br/><br/> - ${this.content.comments}<br/><br/>` - break - case 'submitting-reviewers-after-decision': - replacements.hasLink = false - replacements.paragraph = `Thank you for your review on ${ - this.content.titleText - }. After taking into account the reviews and the recommendation of the Handling Editor, I can confirm this article ${ - this.content.comments - }.<br/><br/> - If you have any queries about this decision, then please email them to Hindawi as soon as possible.` - break - case 'new-version-submitted': - replacements.paragraph = `A new version of ${ - this.content.titleText - } has been submitted.` - replacements.beforeAnchor = - 'Previous reviewers have been automatically invited to review the manuscript again. Please visit the' - replacements.afterAnchor = - 'to see the latest version and any other actions you may need to take' - break - case 'submitting-reviewers-after-revision': - replacements.paragraph = `A new version of ${ - this.content.titleText - } has been submitted.` - replacements.beforeAnchor = `As you have reviewed the previous version of this manuscript, I would be grateful if you can review this revised version and submit a review report by ${helpers.getExpectedDate( - this.content.timestamp, - 14, - )}. You can download the PDF of the revised version and submit your new review from the following URL:` - break - default: - throw new Error(`undefined email type: ${emailType}`) + getBody({ body = {}, hasLink = true, isReviewerInvitation = false }) { + if (isReviewerInvitation) { + return { + html: helpers.getInvitationBody({ + replacements: { + toUserName: this.toUser.name, + ...this.content, + ...body, + }, + }), + text: `${this.content.signatureName}`, + } } return { - html: helpers.getNotificationBody('notification', replacements), - text: `${replacements.intro} ${replacements.paragraph} ${ - replacements.beforeAnchor - } ${replacements.detailsUrl} ${replacements.afterAnchor} ${ - replacements.signatureName - }`, + html: helpers.getNotificationBody({ + replacements: { + hasLink, + paragraph: body.paragraph, + toUserName: this.toUser.name, + ...this.content, + }, + }), + text: `${body.paragraph} ${this.content.ctaLink} ${ + this.content.ctaText + } ${this.content.signatureName}`, } } diff --git a/packages/component-helper-service/src/services/email/helpers.js b/packages/component-helper-service/src/services/email/helpers.js index d2a080261..4a8559e12 100644 --- a/packages/component-helper-service/src/services/email/helpers.js +++ b/packages/component-helper-service/src/services/email/helpers.js @@ -1,81 +1,27 @@ -const querystring = require('querystring') +// const querystring = require('querystring') const fs = require('fs') const handlebars = require('handlebars') -const createUrl = (baseUrl, slug, queryParams = null) => - !queryParams - ? `${baseUrl}${slug}` - : `${baseUrl}${slug}?${querystring.encode(queryParams)}` - -const getEmailBody = (emailType, replacements) => { +const getNotificationBody = ({ replacements }) => { handlePartial('header', replacements) handlePartial('footer', replacements) - handlePartial('mainButton', replacements) - handlePartial('mainBody', replacements) - - return getMainTemplate(emailType, replacements) -} - -const getNotificationBody = (emailType, replacements) => { - handlePartial('notificationHeader', replacements) - handlePartial('footer', replacements) handlePartial('signature', replacements) - if (replacements.hasLink) handlePartial('manuscriptDetailsLink', replacements) - handlePartial('notificationBody', replacements) + if (replacements.hasLink) handlePartial('button', replacements) + handlePartial('body', replacements) - return getMainTemplate(emailType, replacements) + return getMainTemplate({ fileName: 'notification', context: replacements }) } -const getInvitationBody = (emailType, replacements) => { - handlePartial('invitationHeader', replacements) +const getInvitationBody = ({ replacements }) => { + handlePartial('invHeader', replacements) handlePartial('footer', replacements) - handlePartial('invitationUpperContent', replacements) - handlePartial('invitationButtons', replacements) - handlePartial('manuscriptData', replacements) + handlePartial('invUpperContent', replacements) + handlePartial('invButtons', replacements) + handlePartial('invManuscriptData', replacements) handlePartial('signature', replacements) - handlePartial('invitationLowerContent', replacements) + handlePartial('invLowerContent', replacements) - return getMainTemplate(emailType, replacements) -} - -const getBody = (emailType, replacements) => { - const simplePartials = ['header', 'footer', 'mainButton', 'mainBody'] - const notificationPartials = [ - 'notificationHeader', - 'footer', - 'signature', - 'manuscriptDetailsLink', - 'notificationBody', - ] - const invitationPartials = [ - 'invitationHeader', - 'footer', - 'invitationUpperContent', - 'invitationButtons', - 'manuscriptData', - 'signature', - 'invitationLowerContent', - ] - - switch (emailType) { - case 'simpleCTA': - case 'noCTA': - simplePartials.forEach(partial => handlePartial(partial, replacements)) - break - case 'invitation': - invitationPartials.forEach(partial => - handlePartial(partial, replacements), - ) - break - case 'notification': - notificationPartials.forEach(partial => - handlePartial(partial, replacements), - ) - break - default: - break - } - return getMainTemplate(emailType, replacements) + return getMainTemplate({ fileName: 'invitation', context: replacements }) } const readFile = path => @@ -94,30 +40,14 @@ const handlePartial = (partialName = 'signature', context = {}) => { handlebars.registerPartial(partialName, partial) } -const getMainTemplate = (fileName, context) => { +const getMainTemplate = ({ fileName, context }) => { const htmlFile = readFile(`${__dirname}/templates/${fileName}.html`) const htmlTemplate = handlebars.compile(htmlFile) const htmlBody = htmlTemplate(context) return htmlBody } -const getExpectedDate = (timestamp, daysExpected) => { - const date = new Date(timestamp) - let expectedDate = date.getDate() + daysExpected - date.setDate(expectedDate) - expectedDate = date.toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - }) - return expectedDate -} - module.exports = { - getBody, - createUrl, - getEmailBody, - getExpectedDate, getNotificationBody, getInvitationBody, } diff --git a/packages/component-helper-service/src/services/email/templates/invitation.html b/packages/component-helper-service/src/services/email/templates/invitation.html index e48b106ec..f9d5ed18c 100644 --- a/packages/component-helper-service/src/services/email/templates/invitation.html +++ b/packages/component-helper-service/src/services/email/templates/invitation.html @@ -1,5 +1,5 @@ -{{> invitationHeader }} -{{> invitationUpperContent }} -{{> invitationButtons }} -{{> invitationLowerContent }} +{{> invHeader }} +{{> invUpperContent }} +{{> invButtons }} +{{> invLowerContent }} {{> footer }} \ No newline at end of file diff --git a/packages/component-helper-service/src/services/email/templates/noCTA.html b/packages/component-helper-service/src/services/email/templates/noCTA.html deleted file mode 100644 index 632140917..000000000 --- a/packages/component-helper-service/src/services/email/templates/noCTA.html +++ /dev/null @@ -1,3 +0,0 @@ -{{> header }} -{{> mainBody }} -{{> footer }} diff --git a/packages/component-helper-service/src/services/email/templates/notification.html b/packages/component-helper-service/src/services/email/templates/notification.html index 56554d0f7..3576bee8f 100644 --- a/packages/component-helper-service/src/services/email/templates/notification.html +++ b/packages/component-helper-service/src/services/email/templates/notification.html @@ -1,3 +1,6 @@ -{{> notificationHeader}} -{{> notificationBody}} +{{> header }} +{{> body }} +{{#if hasLink }} + {{> button }} +{{/if}} {{> footer}} \ No newline at end of file diff --git a/packages/component-helper-service/src/services/email/templates/partials/notificationBody.hbs b/packages/component-helper-service/src/services/email/templates/partials/body.hbs similarity index 85% rename from packages/component-helper-service/src/services/email/templates/partials/notificationBody.hbs rename to packages/component-helper-service/src/services/email/templates/partials/body.hbs index 310722e99..30baf1925 100644 --- a/packages/component-helper-service/src/services/email/templates/partials/notificationBody.hbs +++ b/packages/component-helper-service/src/services/email/templates/partials/body.hbs @@ -6,9 +6,6 @@ <p> </p> <p> {{{paragraph}}} - {{#if hasLink }} - {{> manuscriptDetailsLink}} - {{/if}} </p> <p> </p> {{> signature}} diff --git a/packages/component-helper-service/src/services/email/templates/partials/mainButton.hbs b/packages/component-helper-service/src/services/email/templates/partials/button.hbs similarity index 100% rename from packages/component-helper-service/src/services/email/templates/partials/mainButton.hbs rename to packages/component-helper-service/src/services/email/templates/partials/button.hbs diff --git a/packages/component-helper-service/src/services/email/templates/partials/header.hbs b/packages/component-helper-service/src/services/email/templates/partials/header.hbs index 3e8efa3ea..a62d4be74 100644 --- a/packages/component-helper-service/src/services/email/templates/partials/header.hbs +++ b/packages/component-helper-service/src/services/email/templates/partials/header.hbs @@ -145,7 +145,7 @@ width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;"> <tr> <td role="module-content"> - <p>{{ previewText }}</p> + <p>you have a new notification</p> </td> </tr> </table> diff --git a/packages/component-helper-service/src/services/email/templates/partials/invitationButtons.hbs b/packages/component-helper-service/src/services/email/templates/partials/invButtons.hbs similarity index 96% rename from packages/component-helper-service/src/services/email/templates/partials/invitationButtons.hbs rename to packages/component-helper-service/src/services/email/templates/partials/invButtons.hbs index 002013ea2..5c5041dec 100644 --- a/packages/component-helper-service/src/services/email/templates/partials/invitationButtons.hbs +++ b/packages/component-helper-service/src/services/email/templates/partials/invButtons.hbs @@ -27,7 +27,7 @@ <tr> <td align="center" bgcolor="#0d78f2" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit"> <a style="background-color:#0d78f2;border:1px solid #333333;border-color:#0d78f2;border-radius:0px;border-width:1px;color:#ffffff;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none" - href="{{agreeUrl}}" target="_blank">AGREE</a> + href="{{ agreeLink }}" target="_blank">AGREE</a> </td> </tr> </tbody> @@ -62,7 +62,7 @@ <tr> <td align="center" bgcolor="#e4dfdf" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit"> <a style="background-color:#e4dfdf;border:1px solid #333333;border-color:#E4DFDF;border-radius:0px;border-width:1px;color:#302e2e;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none" - href="{{declineUrl}}" target="_blank">DECLINE</a> + href="{{ declineLink }}" target="_blank">DECLINE</a> </td> </tr> </tbody> diff --git a/packages/component-helper-service/src/services/email/templates/partials/invitationHeader.hbs b/packages/component-helper-service/src/services/email/templates/partials/invHeader.hbs similarity index 98% rename from packages/component-helper-service/src/services/email/templates/partials/invitationHeader.hbs rename to packages/component-helper-service/src/services/email/templates/partials/invHeader.hbs index 3e8efa3ea..a8390f81f 100644 --- a/packages/component-helper-service/src/services/email/templates/partials/invitationHeader.hbs +++ b/packages/component-helper-service/src/services/email/templates/partials/invHeader.hbs @@ -145,7 +145,7 @@ width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;"> <tr> <td role="module-content"> - <p>{{ previewText }}</p> + <p>new invitation</p> </td> </tr> </table> diff --git a/packages/component-helper-service/src/services/email/templates/partials/invitationLowerContent.hbs b/packages/component-helper-service/src/services/email/templates/partials/invLowerContent.hbs similarity index 84% rename from packages/component-helper-service/src/services/email/templates/partials/invitationLowerContent.hbs rename to packages/component-helper-service/src/services/email/templates/partials/invLowerContent.hbs index ef60e6b2a..05a44b844 100644 --- a/packages/component-helper-service/src/services/email/templates/partials/invitationLowerContent.hbs +++ b/packages/component-helper-service/src/services/email/templates/partials/invLowerContent.hbs @@ -2,12 +2,12 @@ <tr> <td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> <p data-pm-slice="1 1 []"> - <a href="{{ manuscriptDetailsUrl }}">See more information</a> + <a href="{{ detailsLink }}">See more information</a> </p> <p data-pm-slice="1 1 []"> </p> - {{> manuscriptData }} + {{> invManuscriptData }} <p data-pm-slice="1 1 []">{{{ lowerContent }}}</p> <p> </p> diff --git a/packages/component-helper-service/src/services/email/templates/partials/manuscriptData.hbs b/packages/component-helper-service/src/services/email/templates/partials/invManuscriptData.hbs similarity index 100% rename from packages/component-helper-service/src/services/email/templates/partials/manuscriptData.hbs rename to packages/component-helper-service/src/services/email/templates/partials/invManuscriptData.hbs diff --git a/packages/component-helper-service/src/services/email/templates/partials/invitationUpperContent.hbs b/packages/component-helper-service/src/services/email/templates/partials/invUpperContent.hbs similarity index 85% rename from packages/component-helper-service/src/services/email/templates/partials/invitationUpperContent.hbs rename to packages/component-helper-service/src/services/email/templates/partials/invUpperContent.hbs index 1bacf2e79..c32a513a9 100644 --- a/packages/component-helper-service/src/services/email/templates/partials/invitationUpperContent.hbs +++ b/packages/component-helper-service/src/services/email/templates/partials/invUpperContent.hbs @@ -2,7 +2,7 @@ <tr> <td style="padding:30px 23px 20px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> <div> - <p data-pm-slice="1 1 []">{{ intro }},</p> + <p data-pm-slice="1 1 []">Dear Dr. {{ toUserName }},</p> <p> </p> <p>{{ upperContent }}</p> diff --git a/packages/component-helper-service/src/services/email/templates/partials/mainBody.hbs b/packages/component-helper-service/src/services/email/templates/partials/mainBody.hbs deleted file mode 100644 index 6fb579666..000000000 --- a/packages/component-helper-service/src/services/email/templates/partials/mainBody.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> - <tr> - <td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> - <h1 style="text-align: center;">{{ headline }}</h1> - <div style="text-align: center;">{{ paragraph }}</div> - <div style="text-align: center;"> </div> - <div style="text-align: center;"> </div> - </td> - </tr> -</table> \ No newline at end of file diff --git a/packages/component-helper-service/src/services/email/templates/partials/manuscriptDetailsLink.hbs b/packages/component-helper-service/src/services/email/templates/partials/manuscriptDetailsLink.hbs deleted file mode 100644 index f1ec72bb2..000000000 --- a/packages/component-helper-service/src/services/email/templates/partials/manuscriptDetailsLink.hbs +++ /dev/null @@ -1 +0,0 @@ - {{ beforeAnchor }} <a href="{{ detailsLink }}">manuscript details page</a> {{ afterAnchor}}. \ No newline at end of file diff --git a/packages/component-helper-service/src/services/email/templates/partials/notificationHeader.hbs b/packages/component-helper-service/src/services/email/templates/partials/notificationHeader.hbs deleted file mode 100644 index a62d4be74..000000000 --- a/packages/component-helper-service/src/services/email/templates/partials/notificationHeader.hbs +++ /dev/null @@ -1,161 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"> - -<head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" /> - <!--[if !mso]><!--> - <meta http-equiv="X-UA-Compatible" content="IE=Edge" /> - <!--<![endif]--> - <!--[if (gte mso 9)|(IE)]> - <xml> - <o:OfficeDocumentSettings> - <o:AllowPNG/> - <o:PixelsPerInch>96</o:PixelsPerInch> - </o:OfficeDocumentSettings> - </xml> - <![endif]--> - <!--[if (gte mso 9)|(IE)]> - <style type="text/css"> - body {width: 600px;margin: 0 auto;} - table {border-collapse: collapse;} - table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;} - img {-ms-interpolation-mode: bicubic;} - </style> - <![endif]--> - - <style type="text/css"> - body, - p, - div { - font-family: helvetica, arial, sans-serif; - font-size: 14px; - } - - body { - color: #626262; - } - - body a { - color: #0D78F2; - text-decoration: none; - } - - p { - margin: 0; - padding: 0; - } - - table.wrapper { - width: 100% !important; - table-layout: fixed; - -webkit-font-smoothing: antialiased; - -webkit-text-size-adjust: 100%; - -moz-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; - } - - img.max-width { - max-width: 100% !important; - } - - .column.of-2 { - width: 50%; - } - - .column.of-3 { - width: 33.333%; - } - - .column.of-4 { - width: 25%; - } - - @media screen and (max-width:480px) { - .preheader .rightColumnContent, - .footer .rightColumnContent { - text-align: left !important; - } - .preheader .rightColumnContent div, - .preheader .rightColumnContent span, - .footer .rightColumnContent div, - .footer .rightColumnContent span { - text-align: left !important; - } - .preheader .rightColumnContent, - .preheader .leftColumnContent { - font-size: 80% !important; - padding: 5px 0; - } - table.wrapper-mobile { - width: 100% !important; - table-layout: fixed; - } - img.max-width { - height: auto !important; - max-width: 480px !important; - } - a.bulletproof-button { - display: block !important; - width: auto !important; - font-size: 80%; - padding-left: 0 !important; - padding-right: 0 !important; - } - .columns { - width: 100% !important; - } - .column { - display: block !important; - width: 100% !important; - padding-left: 0 !important; - padding-right: 0 !important; - margin-left: 0 !important; - margin-right: 0 !important; - } - } - </style> - <!--user entered Head Start--> - - <!--End Head user entered--> -</head> - -<body> - <center class="wrapper" data-link-color="#0D78F2" data-body-style="font-size: 14px; font-family: helvetica,arial,sans-serif; color: #626262; background-color: #F4F4F4;"> - <div class="webkit"> - <table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F4F4F4"> - <tr> - <td valign="top" bgcolor="#F4F4F4" width="100%"> - <table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0"> - <tr> - <td width="100%"> - <table width="100%" cellpadding="0" cellspacing="0" border="0"> - <tr> - <td> - <!--[if mso]> - <center> - <table><tr><td width="600"> - <![endif]--> - <table width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width:600px;" align="center"> - <tr> - <td role="modules-container" style="padding: 0px 0px 0px 0px; color: #626262; text-align: left;" bgcolor="#F4F4F4" width="100%" - align="left"> - - <table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" - width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;"> - <tr> - <td role="module-content"> - <p>you have a new notification</p> - </td> - </tr> - </table> - - <table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> - <tr> - <td style="font-size:6px;line-height:10px;padding:20px 0px 20px 0px;" valign="top" align="center"> - <img class="max-width" border="0" style="display:block;color:#000000;text-decoration:none;font-family:Helvetica, arial, sans-serif;font-size:16px;max-width:10% !important;width:10%;height:auto !important;" - src="https://marketing-image-production.s3.amazonaws.com/uploads/bb39b20cf15e52c1c0933676e25f2b2402737c6560b8098c204ad6932b84eb2058804376dbc4db138c7a21dcaed9325bde36185648afac5bc97e3d73d4e12718.png" - alt="" width="60"> - </td> - </tr> - </table> \ No newline at end of file diff --git a/packages/component-helper-service/src/services/email/templates/simpleCTA.html b/packages/component-helper-service/src/services/email/templates/simpleCTA.html deleted file mode 100644 index 4d5a97b25..000000000 --- a/packages/component-helper-service/src/services/email/templates/simpleCTA.html +++ /dev/null @@ -1,4 +0,0 @@ -{{> header }} -{{> mainBody }} -{{> mainButton }} -{{> footer }} diff --git a/packages/component-helper-service/src/services/services.js b/packages/component-helper-service/src/services/services.js index d489c2825..dab0fae58 100644 --- a/packages/component-helper-service/src/services/services.js +++ b/packages/component-helper-service/src/services/services.js @@ -81,10 +81,24 @@ const createUrl = (baseUrl, slug, queryParams = null) => ? `${baseUrl}${slug}` : `${baseUrl}${slug}?${querystring.encode(queryParams)}` +const getExpectedDate = ({ timestamp = Date.now(), daysExpected = 0 }) => { + const date = new Date(timestamp) + let expectedDate = date.getDate() + daysExpected + date.setDate(expectedDate) + + expectedDate = date.toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + + return expectedDate +} module.exports = { checkForUndefinedParams, validateEmailAndToken, handleNotFoundError, getBaseUrl, createUrl, + getExpectedDate, } diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index 5a3b04a96..5686d03f2 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -1,11 +1,11 @@ -// const mailService = require('pubsweet-component-mail-service') - const { - services, Team, + services, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const notifications = require('./emails/notifications') + module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params const teamHelper = new Team({ TeamModel: models.Team, collectionId }) @@ -56,10 +56,14 @@ module.exports = models => async (req, res) => { user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() - // mailService.sendSimpleEmail({ - // toEmail: user.email, - // emailType: 'revoke-handling-editor', - // }) + notifications.sendNotifications({ + models, + collection, + isEiC: true, + invitedHE: user, + isCanceled: true, + baseUrl: services.getBaseUrl(req), + }) return res.status(200).json({}) } catch (e) { diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/emailCopy.js b/packages/component-invite/src/routes/collectionsInvitations/emails/emailCopy.js new file mode 100644 index 000000000..eb8f75cbd --- /dev/null +++ b/packages/component-invite/src/routes/collectionsInvitations/emails/emailCopy.js @@ -0,0 +1,28 @@ +const getEmailCopy = ({ emailType, titleText, targetUserName, comments }) => { + let paragraph, hasLink + switch (emailType) { + case 'he-assigned': + paragraph = `You have been assigned as a Handling Editor to ${titleText}. Please click on the link below to access the manuscript and make a decision.` + break + case 'he-accepted': + paragraph = `Dr. ${targetUserName} agreed to be a Handling Editor on ${titleText}. Please click on the link below to access the manuscript.` + break + case 'he-declined': + paragraph = `Dr. ${targetUserName} has declined to be a Handling Editor on ${titleText}.<br/><br/> + ${comments}` + hasLink = false + break + case 'he-revoked': + paragraph = `Your Handling Editor assignment to ${titleText} has been revoked.` + hasLink = false + break + default: + throw new Error(`The ${emailType} email type is not defined.`) + } + + return { paragraph, hasLink } +} + +module.exports = { + getEmailCopy, +} diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js new file mode 100644 index 000000000..3b02231bd --- /dev/null +++ b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js @@ -0,0 +1,144 @@ +const config = require('config') +const { last } = require('lodash') + +const unsubscribeSlug = config.get('unsubscribe.url') + +const { + User, + Email, + services, + Fragment, +} = require('pubsweet-component-helper-service') + +const { getEmailCopy } = require('./emailCopy') + +module.exports = { + async sendNotifications({ + reason, + baseUrl, + invitedHE, + collection, + isEiC = false, + isCanceled = false, + isAccepted = 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.firstName}` + + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() + const eicName = `${eic.firstName} ${eic.lastName}` + const subjectBaseText = `${collection.customId}: Manuscript ` + + const email = new Email({ + type: 'user', + content: { + signatureName: eicName, + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + }, + }) + + if (isEiC) { + sendInvitedHEEmail({ + email, + baseUrl, + eicName, + isCanceled, + titleText, + subjectBaseText, + handlingEditor: invitedHE, + }) + } else { + sendEiCEmail({ + eic, + email, + baseUrl, + comments: reason ? `Reason: "${reason}"` : '', + titleText, + isAccepted, + subjectBaseText, + targetUserName: `${invitedHE.firstName} ${invitedHE.lastName}`, + }) + } + }, +} + +const sendInvitedHEEmail = ({ + email, + eicName, + baseUrl, + titleText, + isCanceled, + handlingEditor, + subjectBaseText, +}) => { + email.toUser = { + email: handlingEditor.email, + name: handlingEditor.name, + } + + email.content.subject = isCanceled + ? `${subjectBaseText} Assignment Revoked` + : `${subjectBaseText} Assignment` + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: handlingEditor.id, + }) + email.content.signatureName = eicName + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType: isCanceled ? 'he-revoked' : 'he-assigned', + titleText, + }), + }) + + email.sendEmail({ html, text }) +} + +const sendEiCEmail = async ({ + eic, + email, + baseUrl, + comments, + titleText, + isAccepted, + targetUserName, + subjectBaseText, +}) => { + email.content.subject = `${subjectBaseText} Assignment Response` + const emailType = isAccepted ? 'he-accepted' : 'he-declined' + + email.toUser = { + email: eic.email, + name: `${eic.firstName} ${eic.lastName}`, + } + + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: eic.id, + }) + + const { html, text } = email.getBody({ + body: getEmailCopy({ + comments, + emailType, + titleText, + targetUserName, + }), + }) + + email.sendEmail({ html, text }) +} diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index caa61f182..28452a347 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -1,12 +1,10 @@ -// const mailService = require('pubsweet-component-mail-service') - const { Team, - // User, services, Collection, Invitation, } = require('pubsweet-component-helper-service') +const notifications = require('./emails/notifications') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params @@ -35,53 +33,34 @@ module.exports = models => async (req, res) => { }) const collectionHelper = new Collection({ collection }) - // const baseUrl = services.getBaseUrl(req) const teamHelper = new Team({ TeamModel: models.Team, collectionId }) - // const userHelper = new User({ UserModel }) await collectionHelper.updateHandlingEditor({ isAccepted }) invitation.respondedOn = Date.now() invitation.hasAnswer = true - // const eic = await userHelper.getEditorInChief() - // const toEmail = eic.email - - if (isAccepted) { - invitation.isAccepted = true - await collection.save() + invitation.isAccepted = isAccepted - // mailService.sendSimpleEmail({ - // toEmail, - // user, - // emailType: 'handling-editor-agreed', - // dashboardUrl: baseUrl, - // meta: { - // collectionId: collection.customId, - // }, - // }) + if (!isAccepted) { + await teamHelper.deleteHandlingEditor({ + collection, + role: invitation.role, + user, + }) - return res.status(200).json(invitation) + if (reason) invitation.reason = reason } - await teamHelper.deleteHandlingEditor({ - collection, - role: invitation.role, - user, - }) - - invitation.isAccepted = false - if (reason) invitation.reason = reason await collection.save() - // mailService.sendSimpleEmail({ - // toEmail, - // user, - // emailType: 'handling-editor-declined', - // meta: { - // reason, - // collectionId: collection.customId, - // }, - // }) + notifications.sendNotifications({ + models, + reason, + collection, + isAccepted, + invitedHE: user, + baseUrl: services.getBaseUrl(req), + }) return res.status(200).json(invitation) } catch (e) { diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index 93be34ea5..d1c3eae14 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -1,13 +1,15 @@ const logger = require('@pubsweet/logger') -// const mailService = require('pubsweet-component-mail-service') + const { + Team, services, - authsome: authsomeHelper, Collection, - Team, Invitation, + authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const notifications = require('./emails/notifications') + module.exports = models => async (req, res) => { const { email, role } = req.body @@ -50,7 +52,6 @@ module.exports = models => async (req, res) => { }) const collectionHelper = new Collection({ collection }) - // const baseUrl = services.getBaseUrl(req) const teamHelper = new Team({ TeamModel: models.Team, @@ -83,12 +84,13 @@ module.exports = models => async (req, res) => { await collection.save() await collectionHelper.addHandlingEditor({ user, invitation }) - // mailService.sendSimpleEmail({ - // toEmail: user.email, - // user, - // emailType: 'assign-handling-editor', - // dashboardUrl: baseUrl, - // }) + notifications.sendNotifications({ + models, + collection, + isEiC: true, + invitedHE: user, + baseUrl: services.getBaseUrl(req), + }) return res.status(200).json(invitation) } catch (e) { diff --git a/packages/component-invite/src/routes/fragmentsInvitations/decline.js b/packages/component-invite/src/routes/fragmentsInvitations/decline.js index eff9fb09c..ce05ea782 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/decline.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/decline.js @@ -1,11 +1,11 @@ const { - Email, services, - Fragment, Invitation, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const notifications = require('./emails/notifications') + module.exports = models => async (req, res) => { const { collectionId, invitationId, fragmentId } = req.params const { invitationToken } = req.body @@ -54,27 +54,16 @@ module.exports = models => async (req, res) => { invitation.isAccepted = false await fragment.save() - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await fragmentHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, + + notifications.sendNotifications({ baseUrl, - authors, - }) - emailHelper.setupReviewerDecisionEmail({ - authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, - agree: false, - user, + fragment, + collection, + UserModel: models.User, + reviewer: user, }) + return res.status(200).json({}) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'item') diff --git a/packages/component-invite/src/routes/fragmentsInvitations/delete.js b/packages/component-invite/src/routes/fragmentsInvitations/delete.js index af74f13d3..cd72deeb2 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/delete.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/delete.js @@ -1,12 +1,12 @@ const { - services, Team, - Email, - Fragment, + services, Collection, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const notifications = require('./emails/notifications') + module.exports = models => async (req, res) => { const { collectionId, invitationId, fragmentId } = req.params const teamHelper = new Team({ @@ -67,26 +67,16 @@ module.exports = models => async (req, res) => { user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await fragmentHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, - baseUrl, - authors, - }) - emailHelper.setupReviewerUnassignEmail({ - user, - authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, + notifications.sendNotifications({ + baseUrl, + fragment, + collection, + isHE: true, + reviewer: user, + isCanceled: true, + UserModel: models.User, }) return res.status(200).json({}) diff --git a/packages/component-invite/src/routes/fragmentsInvitations/emails/emailCopy.js b/packages/component-invite/src/routes/fragmentsInvitations/emails/emailCopy.js new file mode 100644 index 000000000..7f7f86859 --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/emails/emailCopy.js @@ -0,0 +1,58 @@ +const config = require('config') + +const getEmailCopy = ({ + emailType, + titleText, + expectedDate, + targetUserName, +}) => { + let upperContent, manuscriptText, lowerContent, paragraph, hasLink + switch (emailType) { + case 'reviewer-invitation': + upperContent = `${titleText}, has been submitted for possible publication in Hindawi. As the Academic Editor handling the manuscript, I would be delighted if you would agree to review it and let me know whether you feel it is suitable for publication.` + manuscriptText = + "The manuscript's abstract, and author information is below to help you decide. Once you have agreed to review, you will be able to download the full article PDF." + lowerContent = `If a potential conflict of interest exists between yourself and either the authors or + the subject of the manuscript, please decline to handle the manuscript. If a conflict + becomes apparent during the review process, please let me know at the earliest possible + opportunity. For more information about our conflicts of interest policies, please + see: + <a href="https://www.hindawi.com/ethics/#coi">https://www.hindawi.com/ethics/#coi</a>. + If you are able to review the manuscript, I would be grateful if you could submit your + report by ${expectedDate}.` + break + case 'reviewer-resend-invitation': + upperContent = `On ${expectedDate} I sent you a request to review ${titleText}, submitted for possible publication in Hindawi. + I would be grateful if you would agree to review it and let me know whether you feel + it is suitable for publication. If you are unable to review this manuscript then + please decline to review. More details are available by clicking the link.` + lowerContent = + 'I would like to thank you in advance for your help with the evaluation of this manuscript, since it would not be possible for us to run the journal without the help of our reviewers. I am looking forward to hearing from you.' + break + case 'reviewer-accepted': + paragraph = `We are pleased to inform you that Dr. ${targetUserName} has agreed to review ${titleText}. You should receive the report by Dr. ${targetUserName} before ${expectedDate}. If you have any queries, or would like to send a reminder if you no report has been submitted, then please visit the manuscript details page to see the full review.` + break + case 'reviewer-declined': + paragraph = `We regret to inform you that Dr. ${targetUserName} has declined to review ${titleText}. Please visit the manuscript details page to see if you need to invite any additional reviewers in order to reach a decision on the manuscript` + break + case 'reviewer-thank-you': + paragraph = `Thank you for agreeing to review ${titleText}. You can view the full PDF file of the manuscript and post your review report using the following URL:` + break + case 'reviewer-cancel-invitation': + paragraph = `You are no longer needed to review ${titleText}. If you have comments on this manuscript you believe the Editor should + see, please email them to ${config.get( + 'mailer.from', + )} as soon as possible. Thank you for your + time and I hope you will consider reviewing for Hindawi again.` + hasLink = false + break + default: + throw new Error(`The ${emailType} email type is not defined.`) + } + + return { upperContent, manuscriptText, lowerContent, paragraph, hasLink } +} + +module.exports = { + getEmailCopy, +} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js b/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js new file mode 100644 index 000000000..fb2ab742c --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/emails/invitations.js @@ -0,0 +1,136 @@ +const config = require('config') +const { get } = require('lodash') + +const unsubscribeSlug = config.get('unsubscribe.url') +const resetPasswordPath = config.get('invite-reviewer.url') + +const { + Email, + services, + Fragment, +} = require('pubsweet-component-helper-service') + +const { getEmailCopy } = require('./emailCopy') + +module.exports = { + async sendInvitations({ + resend, + baseUrl, + fragment, + UserModel, + timestamp, + collection, + invitation, + invitedUser, + }) { + const fragmentHelper = new Fragment({ fragment }) + const { title, abstract } = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const { + activeAuthors: authors, + submittingAuthor, + } = await fragmentHelper.getAuthorData({ + UserModel, + }) + + const subjectBaseText = `${collection.customId}: Review` + const titleText = `The manuscript titled "${title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.firstName}` + + let queryParams = { + invitationId: invitation.id, + agree: true, + } + + const detailsPath = `/projects/${collection.id}/versions/${ + fragment.id + }/details` + + const declineLink = services.createUrl(baseUrl, resetPasswordPath, { + ...queryParams, + agree: false, + fragmentId: fragment.id, + collectionId: collection.id, + invitationToken: invitedUser.invitationToken, + }) + + let agreeLink = services.createUrl(baseUrl, detailsPath, queryParams) + + if (!invitedUser.isConfirmed) { + queryParams = { + ...queryParams, + email: invitedUser.email, + token: invitedUser.passwordResetToken, + collectionId: collection.id, + fragmentId: fragment.id, + agree: true, + } + agreeLink = services.createUrl(baseUrl, resetPasswordPath, queryParams) + } + + const email = new Email({ + type: 'user', + toUser: { + email: invitedUser.email, + name: `${invitedUser.firstName} ${invitedUser.lastName}`, + }, + content: { + authors, + abstract, + agreeLink, + declineLink, + subject: `${subjectBaseText} Requested`, + detailsLink: services.createUrl(baseUrl, detailsPath), + signatureName: get(collection, 'handlingEditor.name', 'Hindawi'), + unsubscribeLink: services.createUrl(baseUrl, unsubscribeSlug, { + id: invitedUser.id, + }), + }, + }) + + sendInvitedUserEmail({ + email, + titleText, + resend, + timestamp, + }) + }, +} + +const sendInvitedUserEmail = async ({ + email, + titleText, + resend, + timestamp, +}) => { + const emailType = + resend === true ? 'reviewer-resend-invitation ' : 'reviewer-invitation' + const daysExpected = resend === true ? 0 : 14 + + const { html, text } = email.getBody({ + isReviewerInvitation: true, + body: getEmailCopy({ + emailType, + titleText, + expectedDate: getExpectedDate({ timestamp, daysExpected }), + }), + }) + + email.sendEmail({ html, text }) +} + +const getExpectedDate = ({ timestamp = Date.now(), daysExpected = 0 }) => { + const date = new Date(timestamp) + let expectedDate = date.getDate() + daysExpected + date.setDate(expectedDate) + + expectedDate = date.toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + + return expectedDate +} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js b/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js new file mode 100644 index 000000000..34b6e6f51 --- /dev/null +++ b/packages/component-invite/src/routes/fragmentsInvitations/emails/notifications.js @@ -0,0 +1,148 @@ +const config = require('config') +const { get } = require('lodash') + +const unsubscribeSlug = config.get('unsubscribe.url') + +const { + User, + Email, + services, + Fragment, +} = require('pubsweet-component-helper-service') + +const { getEmailCopy } = require('./emailCopy') + +module.exports = { + async sendNotifications({ + baseUrl, + fragment, + reviewer, + UserModel, + collection, + isHE = false, + isAccepted = false, + isCanceled = false, + }) { + const fragmentHelper = new Fragment({ fragment }) + const { title } = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const { submittingAuthor } = await fragmentHelper.getAuthorData({ + UserModel, + }) + + const titleText = `the manuscript titled "${title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.firstName}` + + const handlingEditor = get(collection, 'handlingEditor') + const userHelper = new User({ UserModel }) + const { firstName, lastName } = await userHelper.getEditorInChief() + const eicName = `${firstName} ${lastName}` + const subjectBaseText = isCanceled + ? `${collection.customId}: Reviewer ` + : `${collection.customId}: Manuscript ` + + const email = new Email({ + type: 'user', + content: { + signatureName: handlingEditor.name, + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + }, + }) + + if (isHE) { + sendReviewerEmail({ + email, + baseUrl, + reviewer, + titleText, + isCanceled, + subjectBaseText, + }) + } else { + sendHandlingEditorEmail({ + email, + eicName, + titleText, + handlingEditor, + targetUserName: `${reviewer.firstName} ${reviewer.lastName}`, + emailType: + isAccepted === true ? 'reviewer-accepted' : 'reviewer-declined', + }) + } + }, +} + +const sendHandlingEditorEmail = ({ + email, + eicName, + baseUrl, + titleText, + emailType, + handlingEditor, + targetUserName, + subjectBaseText, +}) => { + email.toUser = { + email: handlingEditor.email, + name: handlingEditor.name, + } + + email.content.subject = `${subjectBaseText} Reviews` + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: handlingEditor.id, + }) + email.content.signatureName = eicName + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + titleText, + expectedDate: services.getExpectedDate({ + timestamp: Date.now(), + daysExpected: 14, + }), + targetUserName, + }), + }) + email.sendEmail({ html, text }) +} + +const sendReviewerEmail = async ({ + email, + baseUrl, + reviewer, + titleText, + isCanceled, + subjectBaseText, +}) => { + email.content.subject = isCanceled + ? `${subjectBaseText} Unassigned` + : `${subjectBaseText} Review` + const emailType = isCanceled + ? 'reviewer-cancel-invitation' + : 'reviewer-thank-you' + + email.toUser = { + email: reviewer.email, + name: `${reviewer.firstName} ${reviewer.lastName}`, + } + + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: reviewer.id, + }) + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + titleText, + }), + }) + + email.sendEmail({ html, text }) +} diff --git a/packages/component-invite/src/routes/fragmentsInvitations/patch.js b/packages/component-invite/src/routes/fragmentsInvitations/patch.js index 15c8359e9..f9f7bc9ce 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/patch.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/patch.js @@ -1,12 +1,12 @@ const { - Email, services, - Fragment, Collection, Invitation, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const notifications = require('./emails/notifications') + module.exports = models => async (req, res) => { const { collectionId, invitationId, fragmentId } = req.params const { isAccepted, reason } = req.body @@ -20,7 +20,9 @@ module.exports = models => async (req, res) => { return res.status(400).json({ error: `Fragment ${fragmentId} does not match collection ${collectionId}`, }) + const fragment = await models.Fragment.find(fragmentId) + fragment.invitations = fragment.invitations || [] const invitation = await fragment.invitations.find( invitation => invitation.id === invitationId, @@ -31,6 +33,7 @@ module.exports = models => async (req, res) => { role: 'reviewer', invitation, }) + const invitationValidation = invitationHelper.validateInvitation() if (invitationValidation.error) return res.status(invitationValidation.status).json({ @@ -45,54 +48,32 @@ module.exports = models => async (req, res) => { }) const collectionHelper = new Collection({ collection }) - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await fragmentHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, - baseUrl, - authors, - }) invitation.respondedOn = Date.now() invitation.hasAnswer = true + invitation.isAccepted = isAccepted + if (isAccepted) { - invitation.isAccepted = true - if (collection.status === 'reviewersInvited') - await collectionHelper.updateStatus({ newStatus: 'underReview' }) + if (collection.status === 'reviewersInvited') { + collectionHelper.updateStatus({ newStatus: 'underReview' }) + } + fragment.save() + } else { + if (reason) invitation.reason = reason await fragment.save() - - emailHelper.setupReviewerDecisionEmail({ - agree: true, - timestamp: invitation.respondedOn, - user, - authorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, + collectionHelper.updateStatusByNumberOfReviewers({ + invitations: fragment.invitations, }) - - return res.status(200).json(invitation) } - invitation.isAccepted = false - if (reason) invitation.reason = reason - await fragment.save() - - collectionHelper.updateStatusByNumberOfReviewers({ - invitations: fragment.invitations, - }) - - emailHelper.setupReviewerDecisionEmail({ - agree: false, - user, + notifications.sendNotifications({ + baseUrl, + fragment, + isAccepted, + collection, + reviewer: user, + UserModel: models.User, }) return res.status(200).json(invitation) diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js index e6bab2404..29b022464 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/post.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -1,15 +1,15 @@ const logger = require('@pubsweet/logger') const { - Email, + Team, + User, services, - authsome: authsomeHelper, - Fragment, Collection, - Team, Invitation, - User, + authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const emailInvitations = require('./emails/invitations') + module.exports = models => async (req, res) => { const { email, role } = req.body @@ -59,23 +59,9 @@ module.exports = models => async (req, res) => { }) const collectionHelper = new Collection({ collection }) - const fragmentHelper = new Fragment({ fragment }) - const handlingEditor = collection.handlingEditor || {} - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor, - }) + const baseUrl = services.getBaseUrl(req) - const { - authorsList: authors, - submittingAuthor, - } = await fragmentHelper.getAuthorData({ UserModel }) - const emailHelper = new Email({ - UserModel, - collection, - parsedFragment, - baseUrl, - authors, - }) + const teamHelper = new Team({ TeamModel: models.Team, collectionId, @@ -102,10 +88,11 @@ module.exports = models => async (req, res) => { let resend = false if (invitation) { - if (invitation.hasAnswer) + if (invitation.hasAnswer) { return res .status(400) .json({ error: 'User has already replied to a previous invitation.' }) + } invitation.invitedOn = Date.now() await fragment.save() @@ -116,15 +103,19 @@ module.exports = models => async (req, res) => { }) } - if (collection.status === 'heAssigned') - await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) + if (collection.status === 'heAssigned') { + collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) + } - emailHelper.setupReviewerInvitationEmail({ - user, - invitationId: invitation.id, - timestamp: invitation.invitedOn, + emailInvitations.sendInvitations({ resend, - authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, + baseUrl, + fragment, + collection, + invitation, + invitedUser: user, + UserModel: models.User, + timestamp: invitation.invitedOn, }) return res.status(200).json(invitation) @@ -155,11 +146,14 @@ module.exports = models => async (req, res) => { parentObject: fragment, }) - emailHelper.setupReviewerInvitationEmail({ - user: newUser, - invitationId: invitation.id, + emailInvitations.sendInvitations({ + baseUrl, + fragment, + collection, + invitation, + invitedUser: newUser, + UserModel: models.User, timestamp: invitation.invitedOn, - authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, }) return res.status(200).json(invitation) diff --git a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js index 7fdef707c..c6e6089b0 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js @@ -6,8 +6,8 @@ const fixturesService = require('pubsweet-component-fixture-service') const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const path = '../routes/collectionsInvitations/delete' diff --git a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js index 11c7b1ded..3bc33a435 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js @@ -6,10 +6,8 @@ const cloneDeep = require('lodash/cloneDeep') const fixturesService = require('pubsweet-component-fixture-service') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), - sendReviewerInvitationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const reqBody = { diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js index 0608aced4..18e7ce2b1 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js @@ -8,11 +8,10 @@ const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), - sendReviewerInvitationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) + const chance = new Chance() const reqBody = { email: chance.email(), diff --git a/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js index 5365092f9..0357e6c50 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js @@ -7,8 +7,8 @@ const fixturesService = require('pubsweet-component-fixture-service') const { Model, fixtures } = fixturesService const cloneDeep = require('lodash/cloneDeep') -jest.mock('pubsweet-component-mail-service', () => ({ - sendNotificationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const reqBody = { diff --git a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js index e19c54ffa..d7ee683a8 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js @@ -6,9 +6,8 @@ const fixturesService = require('pubsweet-component-fixture-service') const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const path = '../routes/fragmentsInvitations/delete' diff --git a/packages/component-invite/src/tests/fragmentsInvitations/get.test.js b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js index b6d6ac082..d30877398 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/get.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js @@ -6,11 +6,10 @@ const fixturesService = require('pubsweet-component-fixture-service') const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), - sendReviewerInvitationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) + const path = '../routes/fragmentsInvitations/get' const route = { path: diff --git a/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js index ce759e90a..8ce62849a 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js @@ -6,10 +6,8 @@ const cloneDeep = require('lodash/cloneDeep') const fixturesService = require('pubsweet-component-fixture-service') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), - sendReviewerInvitationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const reqBody = { diff --git a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js index c8e76803c..ce8719050 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js @@ -8,11 +8,10 @@ const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), - sendReviewerInvitationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) + const chance = new Chance() const reqBody = { email: chance.email(), diff --git a/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js new file mode 100644 index 000000000..c9e4d53a4 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/notifications/emailCopy.js @@ -0,0 +1,25 @@ +const getEmailCopy = ({ emailType, titleText, expectedDate }) => { + let paragraph + const hasLink = true + switch (emailType) { + case 'he-new-version-submitted': + paragraph = `A new version of ${titleText} has been submitted. + Previous reviewers have been automatically invited to review the manuscript again. Please visit the manuscript details page to see the latest version and any other actions you may need to take.` + break + case 'submitted-reviewers-after-revision': + paragraph = `A new version of ${titleText} has been submitted. As you have reviewed the previous version of this manuscript, I would be grateful if you can review this revised version and submit a review report by ${expectedDate}. You can download the PDF of the revised version and submit your new review from the following URL:` + break + case 'eic-manuscript-submitted': + paragraph = `A new manuscript has been submitted. You can view ${titleText} and take further actions by clicking on the following link:` + break + + default: + throw new Error(`The ${emailType} email type is not defined.`) + } + + return { paragraph, hasLink } +} + +module.exports = { + getEmailCopy, +} diff --git a/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js new file mode 100644 index 000000000..e7deca937 --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/notifications/notifications.js @@ -0,0 +1,176 @@ +const config = require('config') +const { get } = require('lodash') + +const { + User, + Email, + services, + Fragment, +} = require('pubsweet-component-helper-service') + +const { getEmailCopy } = require('./emailCopy') + +const unsubscribeSlug = config.get('unsubscribe.url') + +module.exports = { + async sendNotifications({ + baseUrl, + fragment, + UserModel, + collection, + isForEditorInChief, + isMajorRecommendation, + }) { + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const fragmentAuthors = await fragmentHelper.getAuthorData({ UserModel }) + + const subjectBaseText = `${collection.customId}: Manuscript` + const titleText = `the manuscript titled "${parsedFragment.title}" by ${ + fragmentAuthors.submittingAuthor.firstName + } ${fragmentAuthors.submittingAuthor.firstName}` + + const email = new Email({ + type: 'user', + content: { + signatureName: get(collection, 'handlingEditor.name', 'Hindawi'), + ctaLink: services.createUrl( + baseUrl, + `/projects/${collection.id}/versions/${fragment.id}/details`, + ), + ctaText: 'MANUSCRIPT DETAILS', + }, + }) + + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() + const eicName = `${eic.firstName} ${eic.lastName}` + + sendHandlingEditorEmail({ + email, + eicName, + baseUrl, + titleText, + subjectBaseText, + handlingEditor: get(collection, 'handlingEditor', {}), + }) + + if (isMajorRecommendation) { + sendReviewersEmail({ + email, + baseUrl, + titleText, + fragmentHelper, + subjectBaseText, + }) + } + + if (isForEditorInChief) { + sendEiCEmail({ + eic, + email, + baseUrl, + titleText, + fragmentHelper, + subjectBaseText, + }) + } + }, +} + +const sendHandlingEditorEmail = ({ + email, + eicName, + baseUrl, + titleText, + handlingEditor, + subjectBaseText, +}) => { + const emailType = 'he-new-version-submitted' + + email.toUser = { + email: handlingEditor.email, + name: `${handlingEditor.name}`, + } + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: handlingEditor.id, + }) + email.content.signatureName = eicName + email.content.subject = `${subjectBaseText} Update` + + const { html, text } = email.getBody({ + template: 'notification', + ...getEmailCopy({ + emailType, + titleText, + }), + }) + email.sendEmail({ html, text }) +} + +const sendReviewersEmail = async ({ + email, + baseUrl, + titleText, + UserModel, + fragmentHelper, + subjectBaseText, +}) => { + email.content.subject = `${subjectBaseText} Update` + const emailType = 'submitted-reviewers-after-revision' + + const reviewers = await fragmentHelper.getReviewers({ + UserModel, + type: 'submitted', + }) + + reviewers.forEach(reviewer => { + email.toUser = { + email: reviewer.email, + name: `${reviewer.firstName} ${reviewer.lastName}`, + } + + email.content.unsubscribeLink = services.createUrl( + baseUrl, + unsubscribeSlug, + { + id: reviewer.id, + }, + ) + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + titleText, + expectedDate: services.getExpectedDate({ daysExpected: 14 }), + }), + }) + + email.sendEmail({ html, text }) + }) +} + +const sendEiCEmail = ({ eic, email, baseUrl, titleText, subjectBaseText }) => { + const emailType = 'eic-manuscript-submitted' + + email.toUser = { + email: eic.email, + name: `${eic.firstName} ${eic.lastName}`, + } + + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: eic.id, + }) + email.content.signatureName = 'Hindawi Submission System' + email.content.subject = `${subjectBaseText} Submitted` + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + titleText, + }), + }) + 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 7359ed96f..e5620211b 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js @@ -1,12 +1,14 @@ +const { union, omit } = require('lodash') + const { Team, - Email, services, Fragment, Collection, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') -const { union, omit } = require('lodash') + +const notifications = require('./notifications/notifications') module.exports = models => async (req, res) => { const { collectionId, fragmentId } = req.params @@ -49,7 +51,10 @@ module.exports = models => async (req, res) => { const newFragmentBody = { ...omit(fragment, ['revision', 'recommendations', 'id']), ...fragment.revision, - invitations: await fragmentHelper.getInvitationsForSubmittingReviewers(), + invitations: fragmentHelper.getInvitations({ + isAccepted: true, + type: 'submitted', + }), version: fragment.version + 1, created: new Date(), } @@ -110,27 +115,13 @@ module.exports = models => async (req, res) => { collection.fragments.push(newFragment.id) collection.save() - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) - const authors = await fragmentHelper.getAuthorData({ - UserModel: models.User, - }) - const email = new Email({ - authors, + notifications.sendNotifications({ + fragment, collection, - parsedFragment: { ...parsedFragment, id: fragment.id }, UserModel: models.User, baseUrl: services.getBaseUrl(req), + isMajorRecommendation: heRecommendation.recommendation === 'major', }) - email.setupNewVersionSubmittedEmail() - - if (heRecommendation.recommendation === 'major') { - email.sendNewVersionSubmittedReviewersEmail({ - invitations: newFragment.invitations, - newFragmentId: newFragment.id, - }) - } return res.status(200).json(newFragment) } catch (e) { diff --git a/packages/component-manuscript-manager/src/routes/fragments/post.js b/packages/component-manuscript-manager/src/routes/fragments/post.js index b54399b0c..c8588d6fd 100644 --- a/packages/component-manuscript-manager/src/routes/fragments/post.js +++ b/packages/component-manuscript-manager/src/routes/fragments/post.js @@ -1,10 +1,10 @@ const { - Email, - Fragment, services, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const notifications = require('./notifications/notifications') + module.exports = models => async (req, res) => { const { collectionId, fragmentId } = req.params let collection, fragment @@ -31,25 +31,16 @@ module.exports = models => async (req, res) => { fragment.submitted = Date.now() fragment = await fragment.save() - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) - const authors = await fragmentHelper.getAuthorData({ - UserModel: models.User, - }) + collection.status = 'submitted' + collection.save() - const email = new Email({ - authors, + notifications.sendNotifications({ + fragment, collection, - parsedFragment, UserModel: models.User, + isForEditorInChief: true, baseUrl: services.getBaseUrl(req), }) - email.setupManuscriptSubmittedEmail() - - collection.status = 'submitted' - collection.save() return res.status(200).json(fragment) } catch (e) { diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js new file mode 100644 index 000000000..5b46e6c1c --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js @@ -0,0 +1,74 @@ +const getEmailCopy = ({ + emailType, + titleText, + comments = '', + targetUserName = '', +}) => { + let paragraph + let hasLink = true + switch (emailType) { + case 'author-request-to-revision': + paragraph = `In order for ${titleText} to proceed to publication, there needs to be a revision. <br/><br/> + ${comments}<br/><br/> + For more information about what is required, please visit the manuscript details page.` + break + case 'author-manuscript-rejected': + paragraph = `I am sorry to inform you that ${titleText} has been rejected for publication. <br/><br/> + ${comments}<br/><br/>` + hasLink = false + break + case 'author-manuscript-published': + paragraph = `I am delighted to inform you that ${titleText} has passed through the review process and will be published in Hindawi.<br/><br/> + ${comments}<br/><br/> + Thanks again for choosing to publish with us.` + hasLink = false + break + case 'he-manuscript-rejected': + hasLink = false + paragraph = `Thank you for your recommendation to reject ${titleText} based on the reviews you received.<br/><br/> + I can confirm this article has now been rejected.` + break + case 'he-manuscript-published': + hasLink = false + paragraph = `Thank you for your recommendation to publish ${titleText} based on the reviews you received.<br/><br/> + I can confirm this article will now go through to publication.` + break + case 'he-manuscript-return-with-comments': + hasLink = false + paragraph = `Thank you for your recommendation for ${titleText} based on the reviews you received.<br/><br/> + ${comments}<br/><br/>` + break + case 'accepted-reviewers-after-recommendation': + hasLink = false + paragraph = `I appreciate any time you may have spent reviewing ${titleText}. However, an editorial decision has been made and the review of this manuscript is now complete. I apologize for any inconvenience. <br/> + If you have comments on this manuscript you believe the Editor should see, please email them to Hindawi as soon as possible. <br/> + Thank you for your interest and I hope you will consider reviewing for Hindawi again.` + break + case 'pending-reviewers-after-he-recommendation': + hasLink = false + paragraph = `An editorial decision has been made regarding ${titleText}. So, you do not need to proceed with the review of this manuscript. <br/><br/> + If you have comments on this manuscript you believe the Editor should see, please email them to Hindawi as soon as possible.` + break + case 'submitted-reviewers-after-publish': + hasLink = false + paragraph = `Thank you for your review on ${titleText}. After taking into account the reviews and the recommendation of the Handling Editor, I can confirm this article will now be published.<br/><br/> + If you have any queries about this decision, then please email them to Hindawi as soon as possible.` + break + case 'submitted-reviewers-after-reject': + hasLink = false + paragraph = `Thank you for your review on ${titleText}. After taking into account the reviews and the recommendation of the Handling Editor, I can confirm this article has now been rejected.<br/><br/> + If you have any queries about this decision, then please email them to Hindawi as soon as possible.` + break + case 'review-submitted': + paragraph = `We are pleased to inform you that Dr. ${targetUserName} has submitted a review for ${titleText}.` + break + default: + throw new Error(`The ${emailType} email type is not defined.`) + } + + return { paragraph, hasLink } +} + +module.exports = { + getEmailCopy, +} diff --git a/packages/component-manuscript-manager/src/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js similarity index 72% rename from packages/component-manuscript-manager/src/notifications/notifications.js rename to packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js index c0fea465a..d5302e1fe 100644 --- a/packages/component-manuscript-manager/src/notifications/notifications.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js @@ -8,6 +8,8 @@ const { Fragment, } = require('pubsweet-component-helper-service') +const { getEmailCopy } = require('./emailCopy') + const unsubscribeSlug = config.get('unsubscribe.url') module.exports = { @@ -16,7 +18,7 @@ module.exports = { fragment, UserModel, collection, - sourceUserName, + targetUserName, isEditorInChief, newRecommendation, }) { @@ -25,20 +27,21 @@ module.exports = { handlingEditor: collection.handlingEditor, }) const fragmentAuthors = await fragmentHelper.getAuthorData({ UserModel }) + const subjectBaseText = `${collection.customId}: Manuscript` + const titleText = `the manuscript titled "${parsedFragment.title}" by ${ + fragmentAuthors.submittingAuthor.firstName + } ${fragmentAuthors.submittingAuthor.firstName}` + const email = new Email({ type: 'user', content: { - comments: '', - sourceUserName, signatureName: get(collection, 'handlingEditor.name', 'Hindawi'), - titleText: `the manuscript titled "${parsedFragment.title}" by ${ - fragmentAuthors.submittingAuthor.firstName - } ${fragmentAuthors.submittingAuthor.firstName}`, - detailsLink: services.createUrl( + ctaLink: services.createUrl( baseUrl, `/projects/${collection.id}/versions/${fragment.id}/details`, ), + ctaText: 'MANUSCRIPT DETAILS', }, }) @@ -46,6 +49,7 @@ module.exports = { const { firstName, lastName } = await userHelper.getEditorInChief() const eicName = `${firstName} ${lastName}` + let comments if (isEditorInChief) { const eicComments = chain(newRecommendation) .get('comments') @@ -53,7 +57,7 @@ module.exports = { .get('content') .value() - email.content.comments = eicComments + comments = eicComments } if (isEditorInChief || newRecommendation.recommendationType === 'review') { @@ -61,6 +65,9 @@ module.exports = { email, eicName, baseUrl, + comments, + titleText, + targetUserName, subjectBaseText, handlingEditor: get(collection, 'handlingEditor', {}), recommendation: newRecommendation.recommendation, @@ -72,6 +79,7 @@ module.exports = { sendAuthorsEmail({ email, baseUrl, + titleText, parsedFragment, fragmentAuthors, isEditorInChief, @@ -82,10 +90,13 @@ module.exports = { sendReviewersEmail({ email, baseUrl, + UserModel, + titleText, fragmentHelper, isEditorInChief, subjectBaseText, recommendation: newRecommendation.recommendation, + handlingEditorName: get(collection, 'handlingEditor.name', 'Faraday'), }) } }, @@ -95,7 +106,10 @@ const sendHandlingEditorEmail = ({ email, eicName, baseUrl, + comments, + titleText, handlingEditor, + targetUserName, recommendation, subjectBaseText, recommendationType, @@ -103,7 +117,7 @@ const sendHandlingEditorEmail = ({ let emailType if (recommendationType === 'review') { email.content.subject = `${subjectBaseText} Review` - emailType = 'review-submitted' + emailType = 'he-review-submitted' } else { email.content.subject = `${subjectBaseText} Decision` switch (recommendation) { @@ -129,30 +143,38 @@ const sendHandlingEditorEmail = ({ id: handlingEditor.id, }) email.content.signatureName = eicName - const { html, text } = email.getBody({ emailType }) + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + titleText, + comments, + targetUserName, + }), + }) email.sendEmail({ html, text }) } const sendAuthorsEmail = async ({ email, baseUrl, + titleText, recommendation, isEditorInChief, subjectBaseText, fragmentAuthors, parsedFragment: { heRecommendation, newComments }, }) => { - let emailType, authors + let emailType, authors, comments if (isEditorInChief) { - const comments = get(heRecommendation, 'comments', []) - if (comments.length) { - const publicComment = comments.find(comm => comm.public) + const heComments = get(heRecommendation, 'comments', []) + if (heComments.length) { + const publicComment = heComments.find(comm => comm.public) const content = get(publicComment, 'content') if (!content) { throw new Error('a public comment cannot be without content') } - email.content.comments = `Reason & Details: "${content}"` + comments = `Reason & Details: "${content}"` } if (recommendation === 'publish') { @@ -165,12 +187,24 @@ const sendAuthorsEmail = async ({ authors = fragmentAuthors.activeAuthors.map(author => ({ ...author, - emailType, + ...getEmailCopy({ + emailType, + titleText, + comments, + }), })) } else { - email.content.comments = newComments emailType = 'author-request-to-revision' - authors = [{ ...fragmentAuthors.submittingAuthor, emailType }] + authors = [ + { + ...fragmentAuthors.submittingAuthor, + ...getEmailCopy({ + emailType, + titleText, + comments: newComments, + }), + }, + ] } authors.forEach(author => { @@ -185,7 +219,12 @@ const sendAuthorsEmail = async ({ id: author.id, }, ) - const { html, text } = email.getBody({ emailType: author.emailType }) + const { html, text } = email.getBody({ + body: { + paragraph: author.paragraph, + }, + hasLink: author.hasLink, + }) email.sendEmail({ html, text }) }) } @@ -194,6 +233,7 @@ const sendReviewersEmail = async ({ email, baseUrl, eicName, + titleText, UserModel, fragmentHelper, recommendation, @@ -203,28 +243,39 @@ const sendReviewersEmail = async ({ }) => { let reviewers if (isEditorInChief) { - email.content.subject = `${email.content.subject} Decision` + email.content.subject = `${subjectBaseText} Decision` + const emailType = + recommendation === 'publish' + ? 'submitting-reviewers-after-publish' + : 'submitting-reviewers-after-reject' reviewers = (await fragmentHelper.getReviewers({ UserModel, type: 'submitting', })).map(rev => ({ ...rev, - emailType: 'submitted-reviewers-after-recommendation', + emailType, + ...getEmailCopy({ + emailType, + titleText, + }), })) - email.content.signatureName = eicName } else { email.content.signatureName = handlingEditorName - email.contet.subject = `${subjectBaseText} ${getSubjectByRecommendation( + email.content.subject = `${subjectBaseText} ${getSubjectByRecommendation( recommendation, )}` + const acceptedReviewers = (await fragmentHelper.getReviewers({ UserModel, type: 'accepted', })).map(rev => ({ ...rev, - emailType: 'accepted-reviewers-after-recommendation', + ...getEmailCopy({ + emailType: 'accepted-reviewers-after-recommendation', + titleText, + }), })) const pendingReviewers = (await fragmentHelper.getReviewers({ @@ -232,7 +283,10 @@ const sendReviewersEmail = async ({ type: 'pending', })).map(rev => ({ ...rev, - emailType: 'pending-reviewers-after-recommendation', + ...getEmailCopy({ + emailType: 'pending-reviewers-after-recommendation', + titleText, + }), })) reviewers = [...acceptedReviewers, ...pendingReviewers] @@ -250,7 +304,10 @@ const sendReviewersEmail = async ({ id: reviewer.id, }, ) - const { html, text } = email.getBody({ emailType: reviewer.emailType }) + const { html, text } = email.getBody({ + body: { paragraph: reviewer.paragraph }, + hasLink: reviewer.hasLink, + }) email.sendEmail({ html, text }) }) } diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index c9ea11971..1e0e4f242 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -1,12 +1,10 @@ const { - // Email, services, authsome: authsomeHelper, - // Fragment, Collection, } = require('pubsweet-component-helper-service') -const notifications = require('../../notifications/notifications') +const notifications = require('./notifications/notifications') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params @@ -48,30 +46,8 @@ module.exports = models => async (req, res) => { Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() - if (req.body.submittedOn) { - // const fragmentHelper = new Fragment({ fragment }) - // const parsedFragment = await fragmentHelper.getFragmentData({ - // handlingEditor: collection.handlingEditor, - // }) - // const baseUrl = services.getBaseUrl(req) - const collectionHelper = new Collection({ collection }) - - // const authors = await fragmentHelper.getAuthorData({ - // UserModel, - // }) - - // const email = new Email({ - // UserModel, - // collection, - // parsedFragment, - // baseUrl, - // authors, - // }) - // email.setupHandlingEditorEmail({ - // reviewSubmitted: true, - // reviewerName: `${user.firstName} ${user.lastName}`, - // }) + if (req.body.submittedOn) { notifications.sendNotifications({ fragment, collection, @@ -79,13 +55,17 @@ module.exports = models => async (req, res) => { UserModel: models.User, baseUrl: services.getBaseUrl(req), newRecommendation: recommendation, - sourceUserName: `${user.firstName} ${user.lastName}`, + targetUserName: `${user.firstName} ${user.lastName}`, }) - if (['underReview'].includes(collection.status)) + if (['underReview'].includes(collection.status)) { + const collectionHelper = new Collection({ collection }) collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) + } } - await fragment.save() + + fragment.save() + return res.status(200).json(recommendation) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index 4ce2374fb..61b5cc07c 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -7,7 +7,7 @@ const { Collection, } = require('pubsweet-component-helper-service') -const notifications = require('../../notifications/notifications') +const notifications = require('./notifications/notifications') module.exports = models => async (req, res) => { const { recommendation, comments, recommendationType } = req.body diff --git a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js index 556203add..f4e2147a5 100644 --- a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js @@ -6,8 +6,8 @@ const fixturesService = require('pubsweet-component-fixture-service') const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendNotificationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const reqBody = {} diff --git a/packages/component-manuscript-manager/src/tests/fragments/post.test.js b/packages/component-manuscript-manager/src/tests/fragments/post.test.js index 07b6ff7a8..d1446faf3 100644 --- a/packages/component-manuscript-manager/src/tests/fragments/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragments/post.test.js @@ -6,9 +6,8 @@ const fixturesService = require('pubsweet-component-fixture-service') const requests = require('../requests') const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendNotificationEmail: jest.fn(), - sendSimpleEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) const reqBody = {} diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js index 9013b7424..0f0f048e6 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -8,9 +8,10 @@ const requests = require('../requests') const { Model, fixtures } = fixturesService const chance = new Chance() -jest.mock('pubsweet-component-mail-service', () => ({ - sendNotificationEmail: jest.fn(), +jest.mock('@pubsweet/component-send-email', () => ({ + send: jest.fn(), })) + const reqBody = { recommendation: 'accept', comments: [ -- GitLab