From 835765da7f9f01a9a030877f34fd0984780860b0 Mon Sep 17 00:00:00 2001
From: Sebastian Mihalache <sebastian.mihalache@gmail.con>
Date: Wed, 8 Aug 2018 15:59:23 +0300
Subject: [PATCH 1/7] feat(manuscript-manager): add eqa emails and statuses

---
 .../notifications/emailCopy.js                |   5 +-
 .../notifications/notifications.js            | 149 +++++++++-----
 .../routes/fragmentsRecommendations/post.js   |   6 +-
 .../notifications/emailCopy.js                |  18 +-
 .../notifications/notifications.js            | 184 ++++++++++++++++--
 .../src/routes/technicalChecks/patch.js       |   6 +-
 packages/xpub-faraday/config/default.js       |   3 +
 7 files changed, 303 insertions(+), 68 deletions(-)

diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js
index d4046f5a9..5b3840f79 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js
@@ -1,4 +1,5 @@
 const getEmailCopy = ({
+  customId,
   emailType,
   titleText,
   comments = '',
@@ -19,7 +20,6 @@ const getEmailCopy = ({
       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
@@ -77,6 +77,9 @@ const getEmailCopy = ({
       paragraph = `In order for ${titleText} to proceed to publication, there needs to be a revision. <br/><br/>
         For more information about what is required, please visit the manuscript details page.`
       break
+    case 'eqa-manuscript-request-for-approval':
+      paragraph = `Manuscript ID ${customId} has passed peer-review and is now ready for EQA. Please click on the link below to either approve or return the manuscript to the Editor in Chief:`
+      break
     default:
       throw new Error(`The ${emailType} email type is not defined.`)
   }
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js
index b31859583..643f25cfb 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js
@@ -10,10 +10,12 @@ const {
 
 const { getEmailCopy } = require('./emailCopy')
 
+const editorialAssistantEmail = config.get('mailer.editorialAssistant')
 const unsubscribeSlug = config.get('unsubscribe.url')
 
 module.exports = {
   async sendNotifications({
+    hasEQA,
     baseUrl,
     fragment,
     UserModel,
@@ -47,71 +49,91 @@ module.exports = {
     })
 
     const userHelper = new User({ UserModel })
+    const eicName = await userHelper.getEiCName()
 
-    let comments
-    if (isEditorInChief) {
-      const eicComments = chain(newRecommendation)
-        .get('comments')
-        .find(comm => !comm.public)
-        .get('content')
-        .value()
-
-      comments = eicComments
-    }
     if (
-      (isEditorInChief || newRecommendation.recommendationType === 'review') &&
-      collection.status !== 'rejected'
+      !hasEQA &&
+      isEditorInChief &&
+      newRecommendation.recommendation === 'publish'
     ) {
-      // the request came from either the Editor in Chief or a reviewer, so the HE needs to be notified
-      sendHandlingEditorEmail({
+      sendEQAEmail({
         email,
-        eicName: await userHelper.getEiCName(),
+        eicName,
         baseUrl,
-        comments,
-        titleText,
-        targetUserName,
+        collection,
         subjectBaseText,
-        handlingEditor: get(collection, 'handlingEditor', {}),
-        recommendation: newRecommendation.recommendation,
-        recommendationType: newRecommendation.recommendationType,
       })
-    }
+    } else {
+      let comments
+      if (isEditorInChief) {
+        const eicComments = chain(newRecommendation)
+          .get('comments')
+          .find(comm => !comm.public)
+          .get('content')
+          .value()
 
-    if (
-      newRecommendation.recommendationType !== 'review' &&
-      newRecommendation.recommendation !== 'return-to-handling-editor'
-    ) {
-      sendAuthorsEmail({
-        email,
-        baseUrl,
-        titleText,
-        parsedFragment,
-        fragmentAuthors,
-        isEditorInChief,
-        subjectBaseText,
-        newRecommendation,
-      })
-      if (collection.status !== 'rejected') {
-        sendReviewersEmail({
+        comments = eicComments
+      }
+      if (
+        (isEditorInChief ||
+          newRecommendation.recommendationType === 'review') &&
+        collection.status !== 'rejected'
+      ) {
+        // the request came from either the Editor in Chief or a reviewer, so the HE needs to be notified
+        sendHandlingEditorEmail({
           email,
+          eicName,
           baseUrl,
-          UserModel,
+          comments,
           titleText,
-          fragmentHelper,
-          isEditorInChief,
+          targetUserName,
           subjectBaseText,
+          handlingEditor: get(collection, 'handlingEditor', {}),
           recommendation: newRecommendation.recommendation,
-          handlingEditorName: get(collection, 'handlingEditor.name', 'N/A'),
+          recommendationType: newRecommendation.recommendationType,
         })
+      }
 
-        sendEiCsEmail({
+      if (
+        newRecommendation.recommendationType !== 'review' &&
+        newRecommendation.recommendation !== 'return-to-handling-editor'
+      ) {
+        sendAuthorsEmail({
           email,
           baseUrl,
-          userHelper,
           titleText,
+          parsedFragment,
+          fragmentAuthors,
+          isEditorInChief,
           subjectBaseText,
-          recommendation: newRecommendation,
+          newRecommendation,
         })
+        if (collection.status !== 'rejected') {
+          sendReviewersEmail({
+            email,
+            baseUrl,
+            UserModel,
+            titleText,
+            fragmentHelper,
+            isEditorInChief,
+            subjectBaseText,
+            recommendation: newRecommendation.recommendation,
+            handlingEditorName: get(
+              collection,
+              'handlingEditor.name',
+              'Faraday',
+            ),
+          })
+
+          sendEiCsEmail({
+            email,
+            baseUrl,
+            userHelper,
+            titleText,
+            subjectBaseText,
+            recommendation: newRecommendation,
+          })
+        }
       }
     }
   },
@@ -380,6 +402,43 @@ const sendEiCsEmail = async ({
   })
 }
 
+const sendEQAEmail = ({
+  email,
+  eicName,
+  baseUrl,
+  collection,
+  subjectBaseText,
+}) => {
+  const emailType = 'eqa-manuscript-request-for-approval'
+
+  email.toUser = {
+    email: editorialAssistantEmail,
+    name: 'Editorial Assistant',
+  }
+
+  email.content.unsubscribeLink = baseUrl
+  email.content.signatureName = eicName
+  email.content.subject = `${subjectBaseText} Request for EQA Approval`
+  email.content.ctaLink = services.createUrl(
+    baseUrl,
+    config.get('eqa-decision.url'),
+    {
+      collectionId: collection.id,
+      customId: collection.customId,
+      token: collection.technicalChecks.token,
+    },
+  )
+  email.content.ctaText = 'MAKE DECISION'
+
+  const { html, text } = email.getBody({
+    body: getEmailCopy({
+      emailType,
+      customId: collection.customId,
+    }),
+  })
+  email.sendEmail({ html, text })
+}
+
 const getSubjectByRecommendation = recommendation =>
   ['minor', 'major'].includes(recommendation)
     ? 'Revision Requested'
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
index 69135f088..5ee90e91e 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
@@ -75,7 +75,10 @@ module.exports = models => async (req, res) => {
       fragment.revision = pick(fragment, ['authors', 'files', 'metadata'])
     }
 
-    if (isEditorInChief && recommendation === 'publish') {
+    const technicalChecks = get(collection, 'technicalChecks', {})
+    const { hasEQA } = technicalChecks
+
+    if (isEditorInChief && recommendation === 'publish' && !hasEQA) {
       const { journal, xmlParser, ftp } = mtsConfig
       const MTS = new MTSService(journal, xmlParser, s3Config, ftp)
       const packageFragment = {
@@ -90,6 +93,7 @@ module.exports = models => async (req, res) => {
     }
 
     notifications.sendNotifications({
+      hasEQA,
       fragment,
       collection,
       isEditorInChief,
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
index c562e8be4..6d8160a84 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
@@ -1,14 +1,30 @@
 const getEmailCopy = ({ emailType, titleText }) => {
   let paragraph
+  let hasLink = true
   switch (emailType) {
     case 'eqs-manuscript-accepted':
       paragraph = `We are please to inform you that ${titleText} has passed the Hindawi technical check process and is now submitted. Please click the link below to access the manuscript.`
       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 '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/>
+        Thanks again for choosing to publish with us.`
+      hasLink = false
+      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
     default:
       throw new Error(`The ${emailType} email type is not defined.`)
   }
 
-  return { paragraph, hasLink: true }
+  return { paragraph, hasLink }
 }
 
 module.exports = {
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
index 782cdbb0c..c097ac541 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
@@ -1,3 +1,6 @@
+const config = require('config')
+const { get } = require('lodash')
+
 const {
   User,
   Email,
@@ -6,8 +9,11 @@ const {
 } = require('pubsweet-component-helper-service')
 const { getEmailCopy } = require('./emailCopy')
 
+const unsubscribeSlug = config.get('unsubscribe.url')
+
 module.exports = {
   async sendNotifications({
+    isEQA,
     baseUrl,
     collection,
     User: UserModel,
@@ -18,21 +24,23 @@ module.exports = {
     const parsedFragment = await fragmentHelper.getFragmentData({
       handlingEditor: collection.handlingEditor,
     })
-    const { submittingAuthor } = await fragmentHelper.getAuthorData({
+
+    const { authors, submittingAuthor } = await fragmentHelper.getAuthorData({
       UserModel,
     })
 
     const titleText = `the manuscript titled "${parsedFragment.title}" by ${
       submittingAuthor.firstName
     } ${submittingAuthor.lastName}`
+    const subjectBaseText = `${collection.customId}: Manuscript`
 
     const userHelper = new User({ UserModel })
 
     const email = new Email({
       type: 'user',
       content: {
-        subject: `${collection.customId}: Manuscript Passed Technical Checks`,
-        signatureName: 'EQS Team',
+        subject: `${subjectBaseText} Passed Technical Checks`,
+        signatureName: 'EQA Team',
         ctaLink: services.createUrl(
           baseUrl,
           `/projects/${collection.id}/versions/${fragment.id}/details`,
@@ -42,23 +50,161 @@ module.exports = {
       },
     })
 
-    const editors = (await userHelper.getEditorsInChief()).map(eic => ({
-      ...eic,
-      ...getEmailCopy({
-        emailType: 'eqs-manuscript-accepted',
+    if (isEQA) {
+      const eicName = await userHelper.getEiCName()
+      email.content.signatureName = eicName
+      sendAuthorsEmail({
+        email,
+        baseUrl,
         titleText,
-      }),
-    }))
-
-    editors.forEach(eic => {
-      email.toUser = {
-        email: eic.email,
-        name: `${eic.firstName} ${eic.lastName}`,
-      }
-      const { html, text } = email.getBody({
-        body: { paragraph: eic.paragraph, hasLink: eic.hasLink },
+        subjectBaseText,
+        fragmentAuthors: authors,
       })
-      email.sendEmail({ html, text })
-    })
+      sendHandlingEditorEmail({
+        email,
+        baseUrl,
+        titleText,
+        subjectBaseText,
+        handlingEditor: get(collection, 'handlingEditor', {}),
+      })
+      sendSubmittedReviewersEmail({
+        email,
+        baseUrl,
+        titleText,
+        UserModel,
+        fragmentHelper,
+        subjectBaseText,
+      })
+    } else {
+      sendEditorsEmail({ email, userHelper, titleText })
+    }
   },
 }
+
+const sendEditorsEmail = async ({ email, userHelper, titleText }) => {
+  const editors = (await userHelper.getEditorsInChief()).map(eic => ({
+    ...eic,
+    ...getEmailCopy({
+      emailType: 'eqs-manuscript-accepted',
+      titleText,
+    }),
+  }))
+
+  editors.forEach(eic => {
+    email.toUser = {
+      email: eic.email,
+      name: `${eic.firstName} ${eic.lastName}`,
+    }
+    const { html, text } = email.getBody({
+      body: { paragraph: eic.paragraph, hasLink: eic.hasLink },
+    })
+    email.sendEmail({ html, text })
+  })
+}
+
+const sendHandlingEditorEmail = ({
+  email,
+  baseUrl,
+  titleText,
+  handlingEditor,
+  subjectBaseText,
+}) => {
+  email.content.subject = `${subjectBaseText} Decision`
+  const emailType = 'he-manuscript-published'
+
+  email.toUser = {
+    email: handlingEditor.email,
+    name: handlingEditor.name,
+  }
+  email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, {
+    id: handlingEditor.id,
+  })
+
+  const { html, text } = email.getBody({
+    body: getEmailCopy({
+      emailType,
+      titleText,
+    }),
+  })
+  email.sendEmail({ html, text })
+}
+
+const sendSubmittedReviewersEmail = async ({
+  email,
+  baseUrl,
+  titleText,
+  UserModel,
+  fragmentHelper,
+  subjectBaseText,
+}) => {
+  email.content.subject = `${subjectBaseText} Decision`
+
+  const reviewers = (await fragmentHelper.getReviewers({
+    UserModel,
+    type: 'submitted',
+  })).map(rev => ({
+    ...rev,
+    ...getEmailCopy({
+      emailType: 'submitted-reviewers-after-publish',
+      titleText,
+    }),
+  }))
+
+  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: { paragraph: reviewer.paragraph, hasLink: reviewer.hasLink },
+    })
+    email.sendEmail({ html, text })
+  })
+}
+
+const sendAuthorsEmail = ({
+  email,
+  baseUrl,
+  titleText,
+  subjectBaseText,
+  fragmentAuthors,
+}) => {
+  const emailType = 'author-manuscript-published'
+  email.content.subject = `${subjectBaseText} Published`
+
+  const authors = fragmentAuthors.map(author => ({
+    ...author,
+    ...getEmailCopy({
+      emailType,
+      titleText,
+    }),
+  }))
+
+  authors.forEach(author => {
+    email.toUser = {
+      email: author.email,
+      name: `${author.firstName} ${author.lastName}`,
+    }
+    email.content.unsubscribeLink = services.createUrl(
+      baseUrl,
+      unsubscribeSlug,
+      {
+        id: author.id,
+      },
+    )
+    const { html, text } = email.getBody({
+      body: {
+        paragraph: author.paragraph,
+        hasLink: author.hasLink,
+      },
+    })
+    email.sendEmail({ html, text })
+  })
+}
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
index 934709a74..f959d3809 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
@@ -12,7 +12,7 @@ const setNewStatus = (step, agree) => {
   if (step === TECHNICAL_STEPS.EQS) {
     return agree ? 'submitted' : 'rejected'
   } else if (step === TECHNICAL_STEPS.EQA) {
-    return agree ? 'accepted' : 'rejected'
+    return agree ? 'accepted' : 'pendingApproval'
   }
 }
 
@@ -37,6 +37,9 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => {
     }
 
     delete collection.technicalChecks.token
+    if (step === TECHNICAL_STEPS.EQA) {
+      collection.technicalChecks.hasEQA = agree
+    }
     collection.status = setNewStatus(step, agree)
     await collection.save()
 
@@ -46,6 +49,7 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => {
         Fragment,
         collection,
         baseUrl: services.getBaseUrl(req),
+        isEQA: step === TECHNICAL_STEPS.EQA,
       })
     }
 
diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js
index d967fc6b6..fa57cd133 100644
--- a/packages/xpub-faraday/config/default.js
+++ b/packages/xpub-faraday/config/default.js
@@ -97,6 +97,9 @@ module.exports = {
   'eqs-decision': {
     url: process.env.PUBSWEET_EQS_DECISION || '/eqs-decision',
   },
+  'eqa-decision': {
+    url: process.env.PUBSWEET_EQA_DECISION || '/eqa-decision',
+  },
   unsubscribe: {
     url: process.env.PUBSWEET_UNSUBSCRIBE_URL || '/unsubscribe',
   },
-- 
GitLab


From 1658f7e350df4fca293d207d299ddb2f4213cb9e Mon Sep 17 00:00:00 2001
From: Sebastian Mihalache <sebastian.mihalache@gmail.con>
Date: Thu, 9 Aug 2018 16:15:23 +0300
Subject: [PATCH 2/7] =?UTF-8?q?feat(manuscript-manager):=20send=20email=20?=
 =?UTF-8?q?on=20eqa=20comments=C2=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../routes/fragmentsRecommendations/post.js   |  8 ++++++-
 .../notifications/emailCopy.js                |  6 +++++-
 .../notifications/notifications.js            | 21 +++++++++++++++----
 .../src/routes/technicalChecks/patch.js       | 20 +++++++++---------
 4 files changed, 39 insertions(+), 16 deletions(-)

diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
index 5ee90e91e..37010631d 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
@@ -1,6 +1,7 @@
 const uuid = require('uuid')
-const { pick, get } = require('lodash')
+const { pick, get, set } = require('lodash')
 const config = require('config')
+const { v4 } = require('uuid')
 
 const {
   services,
@@ -90,6 +91,11 @@ module.exports = models => async (req, res) => {
       }
 
       await MTS.sendPackage({ fragment: packageFragment, isEQA: true })
+
+      collection.status = 'inQA'
+      set(collection, 'technicalChecks.token', v4())
+      set(collection, 'technicalChecks.hasEQA', false)
+      await collection.save()
     }
 
     notifications.sendNotifications({
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
index 6d8160a84..0c36acca5 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js
@@ -1,4 +1,4 @@
-const getEmailCopy = ({ emailType, titleText }) => {
+const getEmailCopy = ({ emailType, titleText, comments }) => {
   let paragraph
   let hasLink = true
   switch (emailType) {
@@ -20,6 +20,10 @@ const getEmailCopy = ({ emailType, titleText }) => {
       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 'eqa-manuscript-returned-to-eic':
+      paragraph = `We regret to inform you that ${titleText} has been returned with comments. Please click the link below to access the manuscript.<br/><br/>
+        Comments: ${comments}<br/><br/>`
+      break
     default:
       throw new Error(`The ${emailType} email type is not defined.`)
   }
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
index c097ac541..5102aff7e 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
@@ -14,8 +14,10 @@ const unsubscribeSlug = config.get('unsubscribe.url')
 module.exports = {
   async sendNotifications({
     isEQA,
+    agree,
     baseUrl,
     collection,
+    comments = '',
     User: UserModel,
     Fragment: FragmentModel,
   }) {
@@ -50,7 +52,7 @@ module.exports = {
       },
     })
 
-    if (isEQA) {
+    if (isEQA && agree) {
       const eicName = await userHelper.getEiCName()
       email.content.signatureName = eicName
       sendAuthorsEmail({
@@ -76,17 +78,28 @@ module.exports = {
         subjectBaseText,
       })
     } else {
-      sendEditorsEmail({ email, userHelper, titleText })
+      sendEditorsEmail({ email, agree, comments, userHelper, titleText })
     }
   },
 }
 
-const sendEditorsEmail = async ({ email, userHelper, titleText }) => {
+const sendEditorsEmail = async ({
+  email,
+  agree,
+  comments = '',
+  userHelper,
+  titleText,
+}) => {
+  const emailType = agree
+    ? 'eqs-manuscript-accepted'
+    : 'eqa-manuscript-returned-to-eic'
+
   const editors = (await userHelper.getEditorsInChief()).map(eic => ({
     ...eic,
     ...getEmailCopy({
-      emailType: 'eqs-manuscript-accepted',
+      emailType,
       titleText,
+      comments,
     }),
   }))
 
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
index f959d3809..3e6061fd3 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js
@@ -18,7 +18,7 @@ const setNewStatus = (step, agree) => {
 
 module.exports = ({ Collection, Fragment, User }) => async (req, res) => {
   const { collectionId } = req.params
-  const { token, agree, step } = req.body
+  const { token, agree, step, comments } = req.body
 
   try {
     const collection = await Collection.find(collectionId)
@@ -43,15 +43,15 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => {
     collection.status = setNewStatus(step, agree)
     await collection.save()
 
-    if (agree) {
-      sendNotifications({
-        User,
-        Fragment,
-        collection,
-        baseUrl: services.getBaseUrl(req),
-        isEQA: step === TECHNICAL_STEPS.EQA,
-      })
-    }
+    sendNotifications({
+      User,
+      agree,
+      comments,
+      Fragment,
+      collection,
+      baseUrl: services.getBaseUrl(req),
+      isEQA: step === TECHNICAL_STEPS.EQA,
+    })
 
     return res.status(200).json(collection)
   } catch (e) {
-- 
GitLab


From e5c2dcd00cba6c9d6db84287c01ee53db9ff385d Mon Sep 17 00:00:00 2001
From: Sebastian Mihalache <sebastian.mihalache@gmail.con>
Date: Fri, 10 Aug 2018 15:55:29 +0300
Subject: [PATCH 3/7] test(manuscript-manager): update unit tests

---
 .../src/fixtures/collections.js               |   3 +-
 .../routes/fragmentsRecommendations/post.js   |  15 ++-
 .../notifications/notifications.js            |   6 +-
 .../fragmentsRecommendations/post.test.js     | 126 ++++++++++++++++++
 .../src/tests/technicalChecks/patch.test.js   |  58 +++++++-
 5 files changed, 204 insertions(+), 4 deletions(-)

diff --git a/packages/component-fixture-manager/src/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js
index c6d401815..84583485f 100644
--- a/packages/component-fixture-manager/src/fixtures/collections.js
+++ b/packages/component-fixture-manager/src/fixtures/collections.js
@@ -11,7 +11,7 @@ const collections = {
     type: 'collection',
     fragments: [fragment.id],
     owners: [user.id],
-    save: jest.fn(),
+    save: jest.fn(() => collections.collection),
     invitations: [
       {
         id: chance.guid(),
@@ -44,6 +44,7 @@ const collections = {
     technicalChecks: {
       token: chance.guid(),
     },
+    status: 'pendingApproval',
   },
 }
 
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
index 37010631d..f67754387 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
@@ -1,5 +1,5 @@
 const uuid = require('uuid')
-const { pick, get, set } = require('lodash')
+const { pick, get, set, has } = require('lodash')
 const config = require('config')
 const { v4 } = require('uuid')
 
@@ -98,6 +98,19 @@ module.exports = models => async (req, res) => {
       await collection.save()
     }
 
+    /* if the EiC returns the manuscript to the HE after the EQA has been performed
+       then remove all properties from the technicalChecks property so that the manuscript
+       can go through the EQA process again
+    */
+    if (
+      isEditorInChief &&
+      recommendation === 'return-to-handling-editor' &&
+      has(collection.technicalChecks, 'hasEQA')
+    ) {
+      collection.technicalChecks = {}
+      await collection.save()
+    }
+
     notifications.sendNotifications({
       hasEQA,
       fragment,
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
index 5102aff7e..bff64b56e 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
@@ -27,7 +27,10 @@ module.exports = {
       handlingEditor: collection.handlingEditor,
     })
 
-    const { authors, submittingAuthor } = await fragmentHelper.getAuthorData({
+    const {
+      activeAuthors: authors,
+      submittingAuthor,
+    } = await fragmentHelper.getAuthorData({
       UserModel,
     })
 
@@ -55,6 +58,7 @@ module.exports = {
     if (isEQA && agree) {
       const eicName = await userHelper.getEiCName()
       email.content.signatureName = eicName
+
       sendAuthorsEmail({
         email,
         baseUrl,
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 0f0f048e6..686124cb9 100644
--- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
+++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
@@ -11,6 +11,7 @@ const chance = new Chance()
 jest.mock('@pubsweet/component-send-email', () => ({
   send: jest.fn(),
 }))
+jest.mock('pubsweet-component-mts-package')
 
 const reqBody = {
   recommendation: 'accept',
@@ -206,4 +207,129 @@ describe('Post fragments recommendations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('Unauthorized.')
   })
+  it('should return success when the EiC recommends to reject without peer review', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    body.recommendation = 'reject'
+    body.recommendationType = 'editorRecommendation'
+
+    delete fragment.recommendations
+    delete fragment.revision
+    delete fragment.invitations
+    delete collection.invitations
+    delete collection.handlingEditor
+
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+
+    expect(data.userId).toEqual(editorInChief.id)
+    expect(data.recommendation).toBe('reject')
+  })
+  it('should return success when the EiC recommends to publish without EQA', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    body.recommendation = 'publish'
+    body.recommendationType = 'editorRecommendation'
+    delete collection.technicalChecks
+
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+
+    expect(collection.status).toBe('inQA')
+    expect(collection.technicalChecks).toHaveProperty('hasEQA')
+    expect(collection.technicalChecks.hasEQA).toBeFalsy()
+    expect(data.userId).toEqual(editorInChief.id)
+    expect(data.recommendation).toBe('publish')
+  })
+  it('should return success when the EiC recommends to publish with EQA accepted', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    body.recommendation = 'publish'
+    body.recommendationType = 'editorRecommendation'
+
+    collection.technicalChecks.hasEQA = true
+
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+
+    expect(collection.status).toBe('accepted')
+    expect(data.userId).toEqual(editorInChief.id)
+    expect(data.recommendation).toBe('publish')
+  })
+  it('should return success when the EiC returns the manuscript to HE with comments after EQA returned to EiC', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    body.recommendation = 'return-to-handling-editor'
+    body.recommendationType = 'editorRecommendation'
+    body.comments = 'This needs more work'
+
+    delete fragment.recommendations
+    delete fragment.revision
+    delete fragment.invitations
+    delete collection.invitations
+    delete collection.handlingEditor
+    collection.technicalChecks.hasEQA = false
+
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+
+    expect(collection.status).toBe('reviewCompleted')
+    expect(collection.technicalChecks).not.toHaveProperty('token')
+    expect(collection.technicalChecks).not.toHaveProperty('hasEQA')
+
+    expect(data.userId).toEqual(editorInChief.id)
+    expect(data.recommendation).toBe('return-to-handling-editor')
+  })
 })
diff --git a/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js b/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js
index 37c2ca388..f8b6ccd6b 100644
--- a/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js
+++ b/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js
@@ -29,7 +29,7 @@ describe('Patch technical checks route handler', () => {
     models = Model.build(testFixtures)
   })
 
-  it('should return success when the parameters are correct', async () => {
+  it('should return success when the EQS is accepted', async () => {
     const { collection } = testFixtures.collections
     body.token = collection.technicalChecks.token
 
@@ -46,6 +46,62 @@ describe('Patch technical checks route handler', () => {
     expect(res.statusCode).toBe(200)
   })
 
+  it('should return success when the EQS is rejected', async () => {
+    const { collection } = testFixtures.collections
+    body.token = collection.technicalChecks.token
+    body.agree = false
+
+    const res = await requests.sendRequest({
+      body,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+  })
+
+  it('should return success when the EQA is accepted', async () => {
+    const { collection } = testFixtures.collections
+    body.token = collection.technicalChecks.token
+    body.step = 'eqa'
+
+    const res = await requests.sendRequest({
+      body,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+  })
+
+  it('should return success when the EQA is returned with comments', async () => {
+    const { collection } = testFixtures.collections
+    body.token = collection.technicalChecks.token
+    body.agree = false
+    body.step = 'eqa'
+    body.comments = 'suspicion of plagiarism'
+
+    const res = await requests.sendRequest({
+      body,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+  })
+
   it('should return an error when the collection does not exist', async () => {
     const res = await requests.sendRequest({
       body,
-- 
GitLab


From f4f59a2485bb02ca36d0fd6254feea629ccbb32c Mon Sep 17 00:00:00 2001
From: Bogdan Cochior <bogdan.cochior@thinslices.com>
Date: Wed, 15 Aug 2018 10:34:12 +0300
Subject: [PATCH 4/7] feat(eqa): implement EQA decision page

---
 .../component-faraday-selectors/src/index.js  |   2 +-
 .../notifications/notifications.js            |   4 +-
 .../UIComponents/EQADecisionPage.js           | 231 ++++++++++++++++++
 .../UIComponents/EQSDecisionPage.js           |   4 +-
 .../src/components/UIComponents/index.js      |   1 +
 .../src/redux/technicalCheck.js               |  13 +-
 packages/xpub-faraday/app/routes.js           |   2 +
 packages/xpub-faraday/config/default.js       |   2 +-
 packages/xpub-faraday/config/validations.js   |   1 +
 9 files changed, 252 insertions(+), 8 deletions(-)
 create mode 100644 packages/components-faraday/src/components/UIComponents/EQADecisionPage.js

diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js
index 82d6b27ce..199161edf 100644
--- a/packages/component-faraday-selectors/src/index.js
+++ b/packages/component-faraday-selectors/src/index.js
@@ -79,7 +79,7 @@ export const canMakeDecision = (state, collection, fragment = {}) => {
   return isEIC && canMakeDecisionStatuses.includes(status)
 }
 
-const canEditManuscriptStatuses = ['draft', 'technicalChecks']
+const canEditManuscriptStatuses = ['draft', 'technicalChecks', 'inQA']
 export const canEditManuscript = (state, collection, fragment = {}) => {
   if (fragment.id !== last(collection.fragments)) return false
   const status = get(collection, 'status')
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
index bff64b56e..e63386ac9 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
@@ -40,11 +40,13 @@ module.exports = {
     const subjectBaseText = `${collection.customId}: Manuscript`
 
     const userHelper = new User({ UserModel })
+    const subject = `${subjectBaseText} ${!agree &&
+      'Not '}Passed Technical Checks`
 
     const email = new Email({
       type: 'user',
       content: {
-        subject: `${subjectBaseText} Passed Technical Checks`,
+        subject,
         signatureName: 'EQA Team',
         ctaLink: services.createUrl(
           baseUrl,
diff --git a/packages/components-faraday/src/components/UIComponents/EQADecisionPage.js b/packages/components-faraday/src/components/UIComponents/EQADecisionPage.js
new file mode 100644
index 000000000..c1fb89f34
--- /dev/null
+++ b/packages/components-faraday/src/components/UIComponents/EQADecisionPage.js
@@ -0,0 +1,231 @@
+import React from 'react'
+import { isEmpty } from 'lodash'
+import { connect } from 'react-redux'
+import { Button } from '@pubsweet/ui'
+import styled from 'styled-components'
+import { th } from '@pubsweet/ui-toolkit'
+import {
+  compose,
+  withState,
+  lifecycle,
+  withHandlers,
+  setDisplayName,
+} from 'recompose'
+
+import {
+  withModal,
+  ConfirmationModal,
+} from 'pubsweet-component-modal/src/components'
+
+import { Err, Subtitle } from './FormItems'
+import { parseSearchParams } from '../utils'
+import {
+  technicalDecision,
+  technicalCheckFetching,
+} from '../../redux/technicalCheck'
+
+const EQADecisionPage = ({
+  params,
+  showEQAModal,
+  errorMessage,
+  successMessage,
+}) => (
+  <Root>
+    <Title>
+      Take a decision for manuscript <b>{params.customId}</b>.
+    </Title>
+    {errorMessage && <Err>{errorMessage}</Err>}
+    {successMessage && <Subtitle>{successMessage}</Subtitle>}
+    {isEmpty(errorMessage) &&
+      isEmpty(successMessage) && (
+        <ButtonContainer>
+          <Button onClick={showEQAModal(false)}>RETURN TO EiC</Button>
+          <Button onClick={showEQAModal(true)} primary>
+            ACCEPT
+          </Button>
+        </ButtonContainer>
+      )}
+  </Root>
+)
+
+const DeclineModal = compose(
+  withState('reason', 'setReason', ''),
+  withHandlers({
+    changeReason: ({ setReason }) => e => {
+      setReason(e.target.value)
+    },
+  }),
+)(({ reason, changeReason, hideModal, onConfirm, modalError }) => (
+  <DeclineRoot>
+    <span>Return Manuscript to Editor in Chief</span>
+    <textarea
+      onChange={changeReason}
+      placeholder="Return reason*"
+      value={reason}
+    />
+    {modalError && <ErrorMessage>{modalError}</ErrorMessage>}
+    <ButtonContainer data-test="eqa-buttons">
+      <Button onClick={hideModal}>Cancel</Button>
+      <Button disabled={!reason} onClick={onConfirm(reason)} primary>
+        Send
+      </Button>
+    </ButtonContainer>
+  </DeclineRoot>
+))
+
+const ModalComponent = ({ type, ...rest }) =>
+  type === 'decline' ? (
+    <DeclineModal {...rest} />
+  ) : (
+    <ConfirmationModal {...rest} />
+  )
+
+export default compose(
+  setDisplayName('EQA Decision page'),
+  connect(
+    state => ({
+      isFetching: technicalCheckFetching(state),
+    }),
+    { technicalDecision },
+  ),
+  withModal(({ isFetching }) => ({
+    isFetching,
+    modalComponent: ModalComponent,
+  })),
+  withState('params', 'setParams', {
+    token: null,
+    customId: null,
+    collectionId: null,
+  }),
+  withState('successMessage', 'setSuccess', ''),
+  lifecycle({
+    componentDidMount() {
+      const { location, setParams } = this.props
+      const { customId, collectionId, token } = parseSearchParams(
+        location.search,
+      )
+      setParams({ customId, collectionId, token })
+    },
+  }),
+  withHandlers({
+    showEQAModal: ({
+      showModal,
+      hideModal,
+      setSuccess,
+      setModalError,
+      technicalDecision,
+      params: { collectionId, token },
+    }) => decision => () => {
+      const acceptConfig = {
+        title: `Are you sure you want to accept this EQA package?`,
+        onConfirm: () => {
+          technicalDecision({
+            step: 'eqa',
+            agree: decision,
+            collectionId,
+            token,
+          }).then(() => {
+            setSuccess(
+              `Manuscript accepted. Thank you for your technical check!`,
+            )
+            hideModal()
+          }, setModalError)
+        },
+        onCancel: hideModal,
+      }
+      const declineConfig = {
+        type: 'decline',
+        title: 'Return Manuscript to Editor in Chief',
+        onConfirm: reason => () => {
+          technicalDecision({
+            step: 'eqa',
+            agree: decision,
+            comments: reason,
+            collectionId,
+            token,
+          }).then(() => {
+            setSuccess(
+              `Manuscript returned with comments. An email has been sent to Editor In Chief. Thank you for your technical check!`,
+            )
+            hideModal()
+          }, setModalError)
+        },
+      }
+
+      const cfg = decision ? acceptConfig : declineConfig
+      showModal(cfg)
+    },
+  }),
+)(EQADecisionPage)
+
+// #region styles
+const Root = styled.div`
+  align-items: center;
+  color: ${th('colorText')};
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  margin: 0 auto;
+  text-align: center;
+  width: 70vw;
+
+  a {
+    color: ${th('colorText')};
+  }
+`
+
+const Title = styled.div`
+  color: ${th('colorPrimary')};
+  font-size: ${th('fontSizeHeading5')};
+  font-family: ${th('fontHeading')};
+  margin: 10px auto;
+`
+
+const ButtonContainer = styled.div`
+  align-items: center;
+  display: flex;
+  justify-content: space-around;
+  padding: calc(${th('gridUnit')} / 2);
+  width: calc(${th('gridUnit')} * 15);
+`
+const ErrorMessage = styled.div`
+  color: ${th('colorError')};
+  margin: ${th('subGridUnit')};
+  text-align: center;
+`
+const DeclineRoot = styled.div`
+  align-items: center;
+  background-color: ${th('backgroundColor')};
+  display: flex;
+  flex-direction: column;
+  height: calc(${th('gridUnit')} * 13);
+  justify-content: space-between;
+  padding: calc(${th('subGridUnit')} * 7);
+  width: calc(${th('gridUnit')} * 24);
+
+  & span {
+    color: ${th('colorPrimary')};
+    font-size: ${th('fontSizeHeading5')};
+    font-family: ${th('fontHeading')};
+    margin-bottom: ${th('gridUnit')};
+  }
+
+  & textarea {
+    height: 100%;
+    padding: calc(${th('subGridUnit')} * 2);
+    width: 100%;
+  }
+
+  & textarea:focus,
+  & textarea:active {
+    outline: none;
+  }
+
+  & div {
+    display: flex;
+    justify-content: space-evenly;
+    margin: ${th('gridUnit')} auto 0;
+    width: 100%;
+  }
+`
+// #endregion
diff --git a/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js b/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js
index 6aaa02bb3..165361b6f 100644
--- a/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js
+++ b/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js
@@ -21,7 +21,7 @@ import { Err, Subtitle } from './FormItems'
 import { parseSearchParams } from '../utils'
 import {
   technicalDecision,
-  technicalCheckFetcing,
+  technicalCheckFetching,
 } from '../../redux/technicalCheck'
 
 const EQSDecisionPage = ({
@@ -52,7 +52,7 @@ export default compose(
   setDisplayName('EQS Decision page'),
   connect(
     state => ({
-      isFetching: technicalCheckFetcing(state),
+      isFetching: technicalCheckFetching(state),
     }),
     { technicalDecision },
   ),
diff --git a/packages/components-faraday/src/components/UIComponents/index.js b/packages/components-faraday/src/components/UIComponents/index.js
index a619f97e1..ff001b8fb 100644
--- a/packages/components-faraday/src/components/UIComponents/index.js
+++ b/packages/components-faraday/src/components/UIComponents/index.js
@@ -8,5 +8,6 @@ export { default as InfoPage } from './InfoPage'
 export { default as ErrorPage } from './ErrorPage'
 export { default as DateParser } from './DateParser'
 export { default as EQSDecisionPage } from './EQSDecisionPage'
+export { default as EQADecisionPage } from './EQADecisionPage'
 export { default as ConfirmationPage } from './ConfirmationPage'
 export { default as BreadcrumbsHeader } from './BreadcrumbsHeader'
diff --git a/packages/components-faraday/src/redux/technicalCheck.js b/packages/components-faraday/src/redux/technicalCheck.js
index 8cb3a074b..5c8d7b9c2 100644
--- a/packages/components-faraday/src/redux/technicalCheck.js
+++ b/packages/components-faraday/src/redux/technicalCheck.js
@@ -22,6 +22,7 @@ export const technicalDecision = ({
   step,
   agree,
   token,
+  comments,
   collectionId,
 }) => dispatch => {
   dispatch(decisionRequest())
@@ -29,19 +30,25 @@ export const technicalDecision = ({
     step,
     token,
     agree,
+    comments,
   }).then(
     r => {
       dispatch(decisionSuccess())
       return r
     },
     err => {
-      dispatch(decisionError(err))
-      throw err
+      const errorMessage = get(
+        JSON.parse(err.response),
+        'error',
+        'Oops! Something went wrong!',
+      )
+      dispatch(decisionError(errorMessage))
+      throw errorMessage
     },
   )
 }
 
-export const technicalCheckFetcing = state =>
+export const technicalCheckFetching = state =>
   get(state, 'technicalCheck.fetching', false)
 
 export default (state = {}, action = {}) => {
diff --git a/packages/xpub-faraday/app/routes.js b/packages/xpub-faraday/app/routes.js
index 05e3106eb..e3cb4ba8b 100644
--- a/packages/xpub-faraday/app/routes.js
+++ b/packages/xpub-faraday/app/routes.js
@@ -16,6 +16,7 @@ import {
   InfoPage,
   ErrorPage,
   EQSDecisionPage,
+  EQADecisionPage,
   ConfirmationPage,
 } from 'pubsweet-components-faraday/src/components/UIComponents/'
 import {
@@ -120,6 +121,7 @@ const Routes = () => (
         path="/projects/:project/versions/:version/details"
       />
       <Route component={EQSDecisionPage} exact path="/eqs-decision" />
+      <Route component={EQADecisionPage} exact path="/eqa-decision" />
       <Route component={ErrorPage} exact path="/error-page" />
       <Route component={InfoPage} exact path="/info-page" />
       <Route component={NotFound} />
diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js
index c9370cd79..b3789882f 100644
--- a/packages/xpub-faraday/config/default.js
+++ b/packages/xpub-faraday/config/default.js
@@ -115,7 +115,7 @@ module.exports = {
   mailer: {
     from: 'hindawi@thinslices.com',
     path: `${__dirname}/mailer`,
-    editorialAssistant: 'hindawi+editorial@thinslices.com',
+    editorialAssistant: 'bogdan.cochior+editorial@thinslices.com',
   },
   publicKeys: ['pubsweet-client', 'authsome', 'validations'],
   statuses: {
diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js
index 4a96afcdd..25f2501d3 100644
--- a/packages/xpub-faraday/config/validations.js
+++ b/packages/xpub-faraday/config/validations.js
@@ -13,6 +13,7 @@ module.exports = {
     handlingEditor: Joi.object(),
     technicalChecks: Joi.object({
       token: Joi.string(),
+      hasEQA: Joi.boolean(),
     }),
   },
   fragment: [
-- 
GitLab


From 9d39e4a69787dda6313b70dbde1c33535dd7ee9d Mon Sep 17 00:00:00 2001
From: Bogdan Cochior <bogdan.cochior@thinslices.com>
Date: Wed, 15 Aug 2018 11:15:41 +0300
Subject: [PATCH 5/7] feat(eqa): allow admin to pass technical checks

---
 .../component-faraday-selectors/src/index.js  | 13 +++++-
 .../notifications/notifications.js            |  5 ++-
 .../src/components/SideBarActions.js          | 40 ++++++++++++++-----
 3 files changed, 44 insertions(+), 14 deletions(-)

diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js
index 199161edf..f1d9bcb19 100644
--- a/packages/component-faraday-selectors/src/index.js
+++ b/packages/component-faraday-selectors/src/index.js
@@ -81,11 +81,20 @@ export const canMakeDecision = (state, collection, fragment = {}) => {
 
 const canEditManuscriptStatuses = ['draft', 'technicalChecks', 'inQA']
 export const canEditManuscript = (state, collection, fragment = {}) => {
-  if (fragment.id !== last(collection.fragments)) return false
+  const isAdmin = currentUserIs(state, 'isAdmin')
+  if (!isAdmin || fragment.id !== last(collection.fragments)) return false
   const status = get(collection, 'status')
 
+  return canEditManuscriptStatuses.includes(status)
+}
+
+const canOverrideTechnicalChecksStatuses = ['technicalChecks', 'inQA']
+export const canOverrideTechnicalChecks = (state, collection) => {
   const isAdmin = currentUserIs(state, 'isAdmin')
-  return isAdmin && canEditManuscriptStatuses.includes(status)
+  if (!isAdmin) return false
+  const status = get(collection, 'status')
+
+  return canOverrideTechnicalChecksStatuses.includes(status)
 }
 
 export const canSeeReviewersReports = (state, collectionId) => {
diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
index e63386ac9..89cc1d711 100644
--- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
+++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js
@@ -40,8 +40,9 @@ module.exports = {
     const subjectBaseText = `${collection.customId}: Manuscript`
 
     const userHelper = new User({ UserModel })
-    const subject = `${subjectBaseText} ${!agree &&
-      'Not '}Passed Technical Checks`
+    const subject = `${subjectBaseText} ${
+      agree ? '' : 'Not '
+    }Passed Technical Checks`
 
     const email = new Email({
       type: 'user',
diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js
index 4ab282feb..67d132a51 100644
--- a/packages/component-manuscript/src/components/SideBarActions.js
+++ b/packages/component-manuscript/src/components/SideBarActions.js
@@ -15,6 +15,7 @@ import {
   canMakeDecision,
   canMakeRecommendation,
   canEditManuscript,
+  canOverrideTechnicalChecks,
 } from 'pubsweet-component-faraday-selectors/src'
 
 const SideBarActions = ({
@@ -23,18 +24,11 @@ const SideBarActions = ({
   goToEdit,
   canMakeDecision,
   canEditManuscript,
+  goToTechnicalCheck,
+  canOverrideTechChecks,
   canMakeRecommendation,
 }) => (
   <Root>
-    {canEditManuscript && (
-      <Button
-        data-test="button-edit-manuscript"
-        onClick={goToEdit(project, version)}
-        primary
-      >
-        Edit
-      </Button>
-    )}
     {canMakeDecision && (
       <Decision
         collectionId={project.id}
@@ -43,7 +37,6 @@ const SideBarActions = ({
         status={project.status}
       />
     )}
-
     {canMakeRecommendation && (
       <Recommendation
         collectionId={project.id}
@@ -51,6 +44,24 @@ const SideBarActions = ({
         modalKey={`recommend-${version.id}`}
       />
     )}
+    {canOverrideTechChecks && (
+      <ClickableIcon
+        data-test="button-technical-checks"
+        onClick={goToTechnicalCheck(project)}
+        title="Technical Checks"
+      >
+        <Icon>check-square</Icon>
+      </ClickableIcon>
+    )}
+    {canEditManuscript && (
+      <ClickableIcon
+        data-test="button-edit-manuscript"
+        onClick={goToEdit(project, version)}
+        title="Edit Manuscript"
+      >
+        <Icon>edit</Icon>
+      </ClickableIcon>
+    )}
     <ZipFiles
       archiveName={`ID-${project.customId}`}
       collectionId={project.id}
@@ -69,6 +80,7 @@ export default compose(
     canMakeDecision: canMakeDecision(state, project, version),
     canMakeRecommendation: canMakeRecommendation(state, project, version),
     canEditManuscript: canEditManuscript(state, project, version),
+    canOverrideTechChecks: canOverrideTechnicalChecks(state, project),
   })),
   withHandlers({
     goToEdit: ({ history }) => (project, version) => () => {
@@ -76,6 +88,14 @@ export default compose(
         editMode: true,
       })
     },
+    goToTechnicalCheck: ({ history }) => project => () => {
+      const { status, id, customId, technicalChecks: { token = '' } } = project
+      const stage = status === 'technicalChecks' ? 'eqs' : 'eqa'
+      history.push({
+        pathname: `/${stage}-decision`,
+        search: `?collectionId=${id}&customId=${customId}&token=${token}`,
+      })
+    },
   }),
 )(SideBarActions)
 
-- 
GitLab


From 9baeab31af0c3c9ab2b26d74e310c84fe5e8b2d4 Mon Sep 17 00:00:00 2001
From: Bogdan Cochior <bogdan.cochior@thinslices.com>
Date: Wed, 15 Aug 2018 11:16:27 +0300
Subject: [PATCH 6/7] fix(eqa): change editorial email

---
 packages/xpub-faraday/config/default.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js
index b3789882f..c9370cd79 100644
--- a/packages/xpub-faraday/config/default.js
+++ b/packages/xpub-faraday/config/default.js
@@ -115,7 +115,7 @@ module.exports = {
   mailer: {
     from: 'hindawi@thinslices.com',
     path: `${__dirname}/mailer`,
-    editorialAssistant: 'bogdan.cochior+editorial@thinslices.com',
+    editorialAssistant: 'hindawi+editorial@thinslices.com',
   },
   publicKeys: ['pubsweet-client', 'authsome', 'validations'],
   statuses: {
-- 
GitLab


From ef46291d42c052b171882f81cffb54b6a79b5b43 Mon Sep 17 00:00:00 2001
From: Bogdan Cochior <bogdan.cochior@thinslices.com>
Date: Wed, 15 Aug 2018 11:21:04 +0300
Subject: [PATCH 7/7] fix(eqa): remove unused import

---
 packages/component-manuscript/src/components/SideBarActions.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js
index 67d132a51..ea233bbbc 100644
--- a/packages/component-manuscript/src/components/SideBarActions.js
+++ b/packages/component-manuscript/src/components/SideBarActions.js
@@ -1,6 +1,6 @@
 import React from 'react'
 import { compose, withHandlers } from 'recompose'
-import { Icon, Button } from '@pubsweet/ui'
+import { Icon } from '@pubsweet/ui'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
 import { th } from '@pubsweet/ui-toolkit'
-- 
GitLab