diff --git a/packages/component-email/src/helpers/Email.js b/packages/component-email/src/helpers/Email.js
index 292e4a637f9c740c039e22b5531d3c797dbd5e07..02a9816e095c507e4a6fe984798ed673fb929f2e 100644
--- a/packages/component-email/src/helpers/Email.js
+++ b/packages/component-email/src/helpers/Email.js
@@ -3,6 +3,37 @@ const logger = require('@pubsweet/logger')
 const helpers = require('./helpers')
 
 module.exports = {
+  sendSignupEmail: async ({ dashboardUrl, res, email, UserModel }) => {
+    let user
+    try {
+      user = await UserModel.findByEmail(email)
+    } catch (e) {
+      const notFoundError = await helpers.handleNotFoundError(e, 'User')
+      return res.status(notFoundError.status).json({
+        error: notFoundError.message,
+      })
+    }
+    if (!user.confirmationToken) {
+      return res
+        .status(400)
+        .json({ error: 'User does not have a confirmation token.' })
+    }
+    try {
+      await mailService.sendSimpleEmail({
+        toEmail: user.email,
+        user,
+        emailType: 'signup',
+        dashboardUrl,
+        meta: {
+          confirmationToken: user.confirmationToken,
+        },
+      })
+      return res.status(200).json({})
+    } catch (e) {
+      logger.error(e)
+      return res.status(500).json({ error: 'Email could not be sent.' })
+    }
+  },
   setupNewUserEmail: async ({ dashboardUrl, res, email, role, UserModel }) => {
     let user
     try {
@@ -13,7 +44,7 @@ module.exports = {
         error: notFoundError.message,
       })
     }
-    if (user.passwordResetToken === undefined) {
+    if (!user.passwordResetToken) {
       return res
         .status(400)
         .json({ error: 'User does not have a password reset token.' })
diff --git a/packages/component-email/src/routes/emails/post.js b/packages/component-email/src/routes/emails/post.js
index 9ff44cc054265759cfff2ba4eaa7cbf5416a8f54..33a149738109b8df694383170469a20e83bfc25e 100644
--- a/packages/component-email/src/routes/emails/post.js
+++ b/packages/component-email/src/routes/emails/post.js
@@ -3,21 +3,34 @@ const helpers = require('../../helpers/helpers')
 const emailHelper = require('../../helpers/Email')
 
 module.exports = models => async (req, res) => {
-  const { email, type, role } = req.body
+  const { email, type, role = 'author' } = req.body
   if (!helpers.checkForUndefinedParams(email, type, role)) {
     res.status(400).json({ error: 'Email and type are required.' })
     logger.error('User ID and role are missing')
     return
   }
 
-  if (type !== 'invite')
-    return res.status(400).json({ error: `Email type ${type} is not defined.` })
+  // if (type !== 'invite')
+  //   return res.status(400).json({ error: `Email type ${type} is not defined.` })
 
-  return emailHelper.setupNewUserEmail({
-    dashboardUrl: `${req.protocol}://${req.get('host')}`,
-    res,
-    email,
-    role,
-    UserModel: models.User,
-  })
+  if (type === 'signup') {
+    return emailHelper.sendSignupEmail({
+      res,
+      email,
+      UserModel: models.User,
+      dashboardUrl: `${req.protocol}://${req.get('host')}`,
+    })
+  }
+
+  if (type === 'invite') {
+    return emailHelper.setupNewUserEmail({
+      dashboardUrl: `${req.protocol}://${req.get('host')}`,
+      res,
+      email,
+      role,
+      UserModel: models.User,
+    })
+  }
+
+  return res.end()
 }
diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js
index 2ad7a635be95eb0525f033a1e8de8eecafe9edb8..95f9a5b80adf044e2ed228d636c9ce8d793869be 100644
--- a/packages/component-faraday-selectors/src/index.js
+++ b/packages/component-faraday-selectors/src/index.js
@@ -56,10 +56,11 @@ export const getHERecommendation = (state, collectionId, fragmentId) => {
   )
 }
 
+const cantMakeDecisionStatuses = ['rejected', 'published', 'draft']
 export const canMakeDecision = (state, collection) => {
   const status = get(collection, 'status')
 
-  if (!status || status === 'rejected' || status === 'published') return false
+  if (!status || cantMakeDecisionStatuses.includes(status)) return false
 
   const isEIC = currentUserIs(state, 'adminEiC')
   return isEIC && status
@@ -71,4 +72,31 @@ export const canSeeReviewersReports = (state, collectionId) => {
   return isHE || isEiC
 }
 
-export const canSeeEditorialComments = canSeeReviewersReports
+export const canMakeRevision = (state, collection, fragment) => {
+  const currentUserId = get(state, 'currentUser.user.id')
+  return (
+    collection.status === 'revisionRequested' &&
+    fragment.owners.map(o => o.id).includes(currentUserId)
+  )
+}
+
+export const currentUserIsAuthor = (state, id) => {
+  const permissions = getUserPermissions(state) || []
+
+  return permissions
+    .filter(f => f.role === 'author')
+    .map(p => p.objectId)
+    .includes(id)
+}
+
+export const getUserPermissions = ({ currentUser }) =>
+  get(currentUser, 'user.teams').map(t => ({
+    objectId: t.object.id,
+    objectType: t.object.type,
+    role: t.teamType.permissions,
+  }))
+
+export const userNotConfirmed = ({ currentUser }) =>
+  get(currentUser, 'isAuthenticated') &&
+  !currentUserIs({ currentUser }, 'staff') &&
+  !get(currentUser, 'user.isConfirmed')
diff --git a/packages/component-fixture-manager/.gitignore b/packages/component-fixture-manager/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18
--- /dev/null
+++ b/packages/component-fixture-manager/.gitignore
@@ -0,0 +1,8 @@
+_build/
+api/
+logs/
+node_modules/
+uploads/
+.env.*
+.env
+config/local*.*
\ No newline at end of file
diff --git a/packages/component-fixture-manager/README.md b/packages/component-fixture-manager/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a0ac7afbf2c8255c8a94f4f2ff525dd245cc3c93
--- /dev/null
+++ b/packages/component-fixture-manager/README.md
@@ -0,0 +1,2 @@
+# Helper Service
+
diff --git a/packages/component-fixture-manager/index.js b/packages/component-fixture-manager/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd95e77d96237d61fc1a8f03d5f2d7d2f80a7e0f
--- /dev/null
+++ b/packages/component-fixture-manager/index.js
@@ -0,0 +1 @@
+module.exports = require('./src/Fixture')
diff --git a/packages/component-fixture-manager/package.json b/packages/component-fixture-manager/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..fe566ea163b877842eba597a058293b0a0af7fbe
--- /dev/null
+++ b/packages/component-fixture-manager/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "pubsweet-component-fixture-service",
+  "version": "0.0.1",
+  "description": "fixture service component for pubsweet",
+  "license": "MIT",
+  "author": "Collaborative Knowledge Foundation",
+  "files": [
+    "src"
+  ],
+  "main": "index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://gitlab.coko.foundation/xpub/xpub-faraday",
+    "path": "component-fixture-service"
+  },
+  "dependencies": {
+  },
+  "peerDependencies": {
+    "@pubsweet/logger": "^0.0.1",
+    "pubsweet-server": "^1.0.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}
diff --git a/packages/component-fixture-manager/src/Fixture.js b/packages/component-fixture-manager/src/Fixture.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2c53d05d677167696b1dad66e7b1f294a23ae10
--- /dev/null
+++ b/packages/component-fixture-manager/src/Fixture.js
@@ -0,0 +1,7 @@
+const fixtures = require('./fixtures/fixtures')
+const Model = require('./helpers/Model')
+
+module.exports = {
+  fixtures,
+  Model,
+}
diff --git a/packages/component-fixture-manager/src/fixtures/collectionIDs.js b/packages/component-fixture-manager/src/fixtures/collectionIDs.js
new file mode 100644
index 0000000000000000000000000000000000000000..0633f816e39359721e73dd32252279f05bcdff5e
--- /dev/null
+++ b/packages/component-fixture-manager/src/fixtures/collectionIDs.js
@@ -0,0 +1,8 @@
+const Chance = require('chance')
+
+const chance = new Chance()
+const collId = chance.guid()
+
+module.exports = {
+  standardCollID: collId,
+}
diff --git a/packages/component-invite/src/tests/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js
similarity index 60%
rename from packages/component-invite/src/tests/fixtures/collections.js
rename to packages/component-fixture-manager/src/fixtures/collections.js
index bef6132199981a79b87ea86440146eaab5891883..73aab63ed1139cc27ae14df932c9c49ce855346e 100644
--- a/packages/component-invite/src/tests/fixtures/collections.js
+++ b/packages/component-fixture-manager/src/fixtures/collections.js
@@ -1,29 +1,17 @@
 const Chance = require('chance')
-const {
-  user,
-  handlingEditor,
-  author,
-  reviewer,
-  answerReviewer,
-} = require('./userData')
-const { fragment } = require('./fragments')
+const { user, handlingEditor, answerHE } = require('./userData')
+const { fragment, newVersion } = require('./fragments')
+const { standardCollID } = require('./collectionIDs')
 
 const chance = new Chance()
 const collections = {
   collection: {
-    id: chance.guid(),
+    id: standardCollID,
     title: chance.sentence(),
     type: 'collection',
-    fragments: [fragment.id],
+    fragments: [fragment.id, newVersion.id],
     owners: [user.id],
     save: jest.fn(),
-    authors: [
-      {
-        userId: author.id,
-        isSubmitting: true,
-        isCorresponding: false,
-      },
-    ],
     invitations: [
       {
         id: chance.guid(),
@@ -36,19 +24,10 @@ const collections = {
       },
       {
         id: chance.guid(),
-        role: 'reviewer',
-        hasAnswer: false,
-        isAccepted: false,
-        userId: reviewer.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: null,
-      },
-      {
-        id: chance.guid(),
-        role: 'reviewer',
+        role: 'handlingEditor',
         hasAnswer: true,
         isAccepted: false,
-        userId: answerReviewer.id,
+        userId: answerHE.id,
         invitedOn: chance.timestamp(),
         respondedOn: chance.timestamp(),
       },
diff --git a/packages/component-invite/src/tests/fixtures/fixtures.js b/packages/component-fixture-manager/src/fixtures/fixtures.js
similarity index 100%
rename from packages/component-invite/src/tests/fixtures/fixtures.js
rename to packages/component-fixture-manager/src/fixtures/fixtures.js
diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js
new file mode 100644
index 0000000000000000000000000000000000000000..10fcf908ed077f7ecda22e87cb745af8b0a539f1
--- /dev/null
+++ b/packages/component-fixture-manager/src/fixtures/fragments.js
@@ -0,0 +1,179 @@
+const Chance = require('chance')
+const {
+  submittingAuthor,
+  reviewer,
+  answerReviewer,
+  recReviewer,
+  handlingEditor,
+  admin,
+} = require('./userData')
+const { standardCollID } = require('./collectionIDs')
+const { user } = require('./userData')
+
+const chance = new Chance()
+const fragments = {
+  fragment: {
+    id: chance.guid(),
+    collectionId: standardCollID,
+    metadata: {
+      title: chance.sentence(),
+      abstract: chance.paragraph(),
+    },
+    recommendations: [
+      {
+        recommendation: 'publish',
+        recommendationType: 'review',
+        comments: [
+          {
+            content: chance.paragraph(),
+            public: chance.bool(),
+            files: [
+              {
+                id: chance.guid(),
+                name: 'file.pdf',
+                size: chance.natural(),
+              },
+            ],
+          },
+        ],
+        id: chance.guid(),
+        userId: recReviewer.id,
+        createdOn: chance.timestamp(),
+        updatedOn: chance.timestamp(),
+      },
+      {
+        recommendation: 'minor',
+        recommendationType: 'editorRecommendation',
+        comments: [
+          {
+            content: chance.paragraph(),
+            public: chance.bool(),
+            files: [
+              {
+                id: chance.guid(),
+                name: 'file.pdf',
+                size: chance.natural(),
+              },
+            ],
+          },
+        ],
+        id: chance.guid(),
+        userId: handlingEditor.id,
+        createdOn: chance.timestamp(),
+        updatedOn: chance.timestamp(),
+      },
+      {
+        recommendation: 'publish',
+        recommendationType: 'editorRecommendation',
+        comments: [
+          {
+            content: chance.paragraph(),
+            public: chance.bool(),
+            files: [
+              {
+                id: chance.guid(),
+                name: 'file.pdf',
+                size: chance.natural(),
+              },
+            ],
+          },
+        ],
+        id: chance.guid(),
+        userId: admin.id,
+        createdOn: chance.timestamp(),
+        updatedOn: chance.timestamp(),
+      },
+    ],
+    authors: [
+      {
+        email: chance.email(),
+        id: submittingAuthor.id,
+        isSubmitting: true,
+        isCorresponding: false,
+      },
+    ],
+    invitations: [
+      {
+        id: chance.guid(),
+        role: 'reviewer',
+        hasAnswer: false,
+        isAccepted: false,
+        userId: reviewer.id,
+        invitedOn: chance.timestamp(),
+        respondedOn: null,
+      },
+      {
+        id: chance.guid(),
+        role: 'reviewer',
+        hasAnswer: true,
+        isAccepted: false,
+        userId: answerReviewer.id,
+        invitedOn: chance.timestamp(),
+        respondedOn: chance.timestamp(),
+      },
+    ],
+    save: jest.fn(() => fragments.fragment),
+    owners: [user.id],
+    type: 'fragment',
+  },
+  newVersion: {
+    id: chance.guid(),
+    collectionId: standardCollID,
+    metadata: {
+      title: chance.sentence(),
+      abstract: chance.paragraph(),
+    },
+    authors: [
+      {
+        email: chance.email(),
+        id: submittingAuthor.id,
+        isSubmitting: true,
+        isCorresponding: false,
+      },
+    ],
+    invitations: [
+      {
+        id: chance.guid(),
+        role: 'reviewer',
+        hasAnswer: false,
+        isAccepted: false,
+        userId: reviewer.id,
+        invitedOn: chance.timestamp(),
+        respondedOn: null,
+      },
+      {
+        id: chance.guid(),
+        role: 'reviewer',
+        hasAnswer: true,
+        isAccepted: false,
+        userId: answerReviewer.id,
+        invitedOn: chance.timestamp(),
+        respondedOn: chance.timestamp(),
+      },
+    ],
+    save: jest.fn(() => fragments.fragment),
+    owners: [user.id],
+    type: 'fragment',
+  },
+  noParentFragment: {
+    id: chance.guid(),
+    collectionId: '',
+    metadata: {
+      title: chance.sentence(),
+      abstract: chance.paragraph(),
+    },
+    authors: [
+      {
+        email: chance.email(),
+        id: submittingAuthor.id,
+        isSubmitting: true,
+        isCorresponding: false,
+      },
+    ],
+    save: jest.fn(() => fragments.fragment),
+    owners: [user.id],
+    type: 'fragment',
+  },
+}
+
+module.exports = fragments
diff --git a/packages/component-user-manager/src/tests/fixtures/teamIDs.js b/packages/component-fixture-manager/src/fixtures/teamIDs.js
similarity index 79%
rename from packages/component-user-manager/src/tests/fixtures/teamIDs.js
rename to packages/component-fixture-manager/src/fixtures/teamIDs.js
index f8cfc51464701c2ab93b312dd73ed3ea5e2f7ea3..83ce3a556417bc4650efdc9ff478b8bad64a9f13 100644
--- a/packages/component-user-manager/src/tests/fixtures/teamIDs.js
+++ b/packages/component-fixture-manager/src/fixtures/teamIDs.js
@@ -2,9 +2,11 @@ const Chance = require('chance')
 
 const chance = new Chance()
 const heID = chance.guid()
+const revId = chance.guid()
 const authorID = chance.guid()
 
 module.exports = {
   heTeamID: heID,
+  revTeamID: revId,
   authorTeamID: authorID,
 }
diff --git a/packages/component-invite/src/tests/fixtures/teams.js b/packages/component-fixture-manager/src/fixtures/teams.js
similarity index 59%
rename from packages/component-invite/src/tests/fixtures/teams.js
rename to packages/component-fixture-manager/src/fixtures/teams.js
index 7e4611da3489cceb1308e9ffaf072a85866f4ca2..84d784b4b4be4c38a4757fccada612705bc8b1b1 100644
--- a/packages/component-invite/src/tests/fixtures/teams.js
+++ b/packages/component-fixture-manager/src/fixtures/teams.js
@@ -1,8 +1,12 @@
 const users = require('./users')
 const collections = require('./collections')
-const { heTeamID, revTeamID } = require('./teamIDs')
+const fragments = require('./fragments')
+
+const { heTeamID, revTeamID, authorTeamID } = require('./teamIDs')
+const { submittingAuthor } = require('./userData')
 
 const { collection } = collections
+const { fragment } = fragments
 const { handlingEditor, reviewer } = users
 const teams = {
   heTeam: {
@@ -29,13 +33,29 @@ const teams = {
     group: 'reviewer',
     name: 'reviewer',
     object: {
-      type: 'collection',
-      id: collection.id,
+      type: 'fragment',
+      id: fragment.id,
     },
     members: [reviewer.id],
     save: jest.fn(() => teams.revTeam),
     updateProperties: jest.fn(() => teams.revTeam),
     id: revTeamID,
   },
+  authorTeam: {
+    teamType: {
+      name: 'author',
+      permissions: 'author',
+    },
+    group: 'author',
+    name: 'author',
+    object: {
+      type: 'fragment',
+      id: fragment.id,
+    },
+    members: [submittingAuthor.id],
+    save: jest.fn(() => teams.authorTeam),
+    updateProperties: jest.fn(() => teams.authorTeam),
+    id: authorTeamID,
+  },
 }
 module.exports = teams
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/userData.js b/packages/component-fixture-manager/src/fixtures/userData.js
similarity index 86%
rename from packages/component-manuscript-manager/src/tests/fixtures/userData.js
rename to packages/component-fixture-manager/src/fixtures/userData.js
index 4e3b994a70b782f25ec85e0f41410824dd4307c4..9920552365178966754bf7681df61eaa7a15878c 100644
--- a/packages/component-manuscript-manager/src/tests/fixtures/userData.js
+++ b/packages/component-fixture-manager/src/fixtures/userData.js
@@ -15,5 +15,7 @@ module.exports = {
   author: generateUserData(),
   reviewer: generateUserData(),
   answerReviewer: generateUserData(),
+  submittingAuthor: generateUserData(),
   recReviewer: generateUserData(),
+  answerHE: generateUserData(),
 }
diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-fixture-manager/src/fixtures/users.js
similarity index 63%
rename from packages/component-invite/src/tests/fixtures/users.js
rename to packages/component-fixture-manager/src/fixtures/users.js
index c5f0a5e41f8bca54d62c6534ef263a6de77c99bc..5d8872ba8cd34c9c9b63aec724df9b38a5dfec29 100644
--- a/packages/component-invite/src/tests/fixtures/users.js
+++ b/packages/component-fixture-manager/src/fixtures/users.js
@@ -1,4 +1,4 @@
-const { heTeamID, revTeamID } = require('./teamIDs')
+const { heTeamID, revTeamID, authorTeamID } = require('./teamIDs')
 const {
   handlingEditor,
   user,
@@ -6,6 +6,9 @@ const {
   author,
   reviewer,
   answerReviewer,
+  submittingAuthor,
+  recReviewer,
+  answerHE,
 } = require('./userData')
 const Chance = require('chance')
 
@@ -51,6 +54,21 @@ const users = {
     handlingEditor: true,
     title: 'Mr',
   },
+  answerHE: {
+    type: 'user',
+    username: chance.word(),
+    email: answerHE.email,
+    password: 'password',
+    admin: false,
+    id: answerHE.id,
+    firstName: answerHE.firstName,
+    lastName: answerHE.lastName,
+    teams: [heTeamID],
+    save: jest.fn(() => users.answerHE),
+    editorInChief: false,
+    handlingEditor: true,
+    title: 'Mr',
+  },
   user: {
     type: 'user',
     username: chance.word(),
@@ -65,6 +83,9 @@ const users = {
     title: 'Mr',
     save: jest.fn(() => users.user),
     isConfirmed: false,
+    updateProperties: jest.fn(() => users.user),
+    teams: [],
+    confirmationToken: chance.hash(),
   },
   author: {
     type: 'user',
@@ -79,6 +100,10 @@ const users = {
     title: 'Mr',
     save: jest.fn(() => users.author),
     isConfirmed: true,
+    passwordResetToken: chance.hash(),
+    passwordResetTimestamp: Date.now(),
+    teams: [authorTeamID],
+    confirmationToken: chance.hash(),
   },
   reviewer: {
     type: 'user',
@@ -112,6 +137,36 @@ const users = {
     teams: [revTeamID],
     invitationToken: 'inv-token-123',
   },
+  submittingAuthor: {
+    type: 'user',
+    username: 'sauthor',
+    email: submittingAuthor.email,
+    password: 'password',
+    admin: false,
+    id: submittingAuthor.id,
+    passwordResetToken: chance.hash(),
+    firstName: submittingAuthor.firstName,
+    lastName: submittingAuthor.lastName,
+    affiliation: chance.company(),
+    title: 'Mr',
+    save: jest.fn(() => users.submittingAuthor),
+    isConfirmed: false,
+  },
+  recReviewer: {
+    type: 'user',
+    username: chance.word(),
+    email: recReviewer.email,
+    password: 'password',
+    admin: false,
+    id: recReviewer.id,
+    firstName: recReviewer.firstName,
+    lastName: recReviewer.lastName,
+    affiliation: chance.company(),
+    title: 'Mr',
+    save: jest.fn(() => users.recReviewer),
+    isConfirmed: true,
+    teams: [revTeamID],
+  },
 }
 
 module.exports = users
diff --git a/packages/component-invite/src/tests/helpers/Model.js b/packages/component-fixture-manager/src/helpers/Model.js
similarity index 93%
rename from packages/component-invite/src/tests/helpers/Model.js
rename to packages/component-fixture-manager/src/helpers/Model.js
index df543155a679c4c7e2bb91a8304835c458e38d44..9ff60517c6f1ce111f22a5a4f1e5369737c0a9c6 100644
--- a/packages/component-invite/src/tests/helpers/Model.js
+++ b/packages/component-fixture-manager/src/helpers/Model.js
@@ -1,5 +1,3 @@
-// const fixtures = require('../fixtures/fixtures')
-
 const UserMock = require('../mocks/User')
 const TeamMock = require('../mocks/Team')
 
@@ -18,12 +16,17 @@ const build = fixtures => {
       find: jest.fn(id => findMock(id, 'fragments', fixtures)),
     },
   }
+
   UserMock.find = jest.fn(id => findMock(id, 'users', fixtures))
   UserMock.findByEmail = jest.fn(email => findByEmailMock(email, fixtures))
   UserMock.all = jest.fn(() => Object.values(fixtures.users))
   UserMock.findOneByField = jest.fn((field, value) =>
     findOneByFieldMock(field, value, 'users', fixtures),
   )
+  UserMock.updateProperties = jest.fn(user =>
+    updatePropertiesMock(user, 'users'),
+  )
+
   TeamMock.find = jest.fn(id => findMock(id, 'teams', fixtures))
   TeamMock.updateProperties = jest.fn(team =>
     updatePropertiesMock(team, 'teams', fixtures),
@@ -40,7 +43,7 @@ const findMock = (id, type, fixtures) => {
     fixtureObj => fixtureObj.id === id,
   )
 
-  if (foundObj === undefined) return Promise.reject(notFoundError)
+  if (!foundObj) return Promise.reject(notFoundError)
   return Promise.resolve(foundObj)
 }
 
diff --git a/packages/component-invite/src/tests/mocks/Team.js b/packages/component-fixture-manager/src/mocks/Team.js
similarity index 100%
rename from packages/component-invite/src/tests/mocks/Team.js
rename to packages/component-fixture-manager/src/mocks/Team.js
diff --git a/packages/component-invite/src/tests/mocks/User.js b/packages/component-fixture-manager/src/mocks/User.js
similarity index 100%
rename from packages/component-invite/src/tests/mocks/User.js
rename to packages/component-fixture-manager/src/mocks/User.js
diff --git a/packages/component-helper-service/.gitignore b/packages/component-helper-service/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18
--- /dev/null
+++ b/packages/component-helper-service/.gitignore
@@ -0,0 +1,8 @@
+_build/
+api/
+logs/
+node_modules/
+uploads/
+.env.*
+.env
+config/local*.*
\ No newline at end of file
diff --git a/packages/component-helper-service/README.md b/packages/component-helper-service/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a0ac7afbf2c8255c8a94f4f2ff525dd245cc3c93
--- /dev/null
+++ b/packages/component-helper-service/README.md
@@ -0,0 +1,2 @@
+# Helper Service
+
diff --git a/packages/component-helper-service/index.js b/packages/component-helper-service/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e3c50cf86934045799e812f2851110d36ba7cfec
--- /dev/null
+++ b/packages/component-helper-service/index.js
@@ -0,0 +1 @@
+module.exports = require('./src/Helper')
diff --git a/packages/component-helper-service/package.json b/packages/component-helper-service/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e4aa40e13b33df0be7f7a47e1c00a8ef7019a8b1
--- /dev/null
+++ b/packages/component-helper-service/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "pubsweet-component-helper-service",
+  "version": "0.0.1",
+  "description": "helper service component for pubsweet",
+  "license": "MIT",
+  "author": "Collaborative Knowledge Foundation",
+  "files": [
+    "src"
+  ],
+  "main": "index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://gitlab.coko.foundation/xpub/xpub-faraday",
+    "path": "component-helper-service"
+  },
+  "dependencies": {
+  },
+  "peerDependencies": {
+    "@pubsweet/logger": "^0.0.1",
+    "pubsweet-server": "^1.0.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}
diff --git a/packages/component-helper-service/src/Helper.js b/packages/component-helper-service/src/Helper.js
new file mode 100644
index 0000000000000000000000000000000000000000..0037b50c55ed4e8f5af9baccf34702df12b77731
--- /dev/null
+++ b/packages/component-helper-service/src/Helper.js
@@ -0,0 +1,19 @@
+const Email = require('./services/Email')
+const Collection = require('./services/Collection')
+const Fragment = require('./services/Fragment')
+const services = require('./services/services')
+const authsome = require('./services/authsome')
+const User = require('./services/User')
+const Team = require('./services/Team')
+const Invitation = require('./services/Invitation')
+
+module.exports = {
+  Email,
+  Collection,
+  Fragment,
+  services,
+  authsome,
+  User,
+  Team,
+  Invitation,
+}
diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc3513909fd9a9efb99398080319d7ce0b091bd8
--- /dev/null
+++ b/packages/component-helper-service/src/services/Collection.js
@@ -0,0 +1,85 @@
+class Collection {
+  constructor({ collection = {} }) {
+    this.collection = collection
+  }
+
+  async updateStatusByRecommendation({
+    recommendation,
+    isHandlingEditor = false,
+  }) {
+    let newStatus
+    if (isHandlingEditor) {
+      newStatus = 'pendingApproval'
+      if (['minor', 'major'].includes(recommendation)) {
+        newStatus = 'revisionRequested'
+      }
+    } else {
+      if (recommendation === 'minor') {
+        newStatus = 'reviewCompleted'
+      }
+
+      if (recommendation === 'major') {
+        newStatus = 'underReview'
+      }
+    }
+
+    this.updateStatus({ newStatus })
+  }
+
+  async updateFinalStatusByRecommendation({ recommendation }) {
+    let newStatus
+    switch (recommendation) {
+      case 'reject':
+        newStatus = 'rejected'
+        break
+      case 'publish':
+        newStatus = 'accepted'
+        break
+      case 'return-to-handling-editor':
+        newStatus = 'reviewCompleted'
+        break
+      default:
+        break
+    }
+
+    await this.updateStatus({ newStatus })
+  }
+
+  async updateStatus({ newStatus }) {
+    this.collection.status = newStatus
+    await this.collection.save()
+  }
+
+  async addHandlingEditor({ user, invitation }) {
+    this.collection.handlingEditor = {
+      id: user.id,
+      name: `${user.firstName} ${user.lastName}`,
+      invitedOn: invitation.invitedOn,
+      respondedOn: invitation.respondedOn,
+      email: user.email,
+      hasAnswer: invitation.hasAnswer,
+      isAccepted: invitation.isAccepted,
+    }
+    await this.updateStatus({ newStatus: 'heInvited' })
+  }
+
+  async updateHandlingEditor({ isAccepted }) {
+    const { collection: { handlingEditor } } = this
+    handlingEditor.hasAnswer = true
+    handlingEditor.isAccepted = isAccepted
+    handlingEditor.respondedOn = Date.now()
+    let status
+    isAccepted ? (status = 'heAssigned') : (status = 'submitted')
+    await this.updateStatus({ newStatus: status })
+  }
+
+  async updateStatusByNumberOfReviewers({ invitations }) {
+    const reviewerInvitations = invitations.filter(
+      inv => inv.role === 'reviewer',
+    )
+    if (reviewerInvitations.length === 0)
+      await this.updateStatus({ newStatus: 'heAssigned' })
+  }
+}
+
+module.exports = Collection
diff --git a/packages/component-helper-service/src/services/Email.js b/packages/component-helper-service/src/services/Email.js
new file mode 100644
index 0000000000000000000000000000000000000000..e3ea6688ae4056120140770cb81343f139670034
--- /dev/null
+++ b/packages/component-helper-service/src/services/Email.js
@@ -0,0 +1,430 @@
+const Fragment = require('./Fragment')
+const User = require('./User')
+const get = require('lodash/get')
+const config = require('config')
+const mailService = require('pubsweet-component-mail-service')
+
+const manuscriptTypes = config.get('manuscript-types')
+
+class Email {
+  constructor({
+    baseUrl,
+    authors = {},
+    UserModel = {},
+    collection = {},
+    parsedFragment = {},
+  }) {
+    this.baseUrl = baseUrl
+    this.authors = authors
+    this.UserModel = UserModel
+    this.collection = collection
+    this.parsedFragment = parsedFragment
+  }
+
+  set _fragment(newFragment) {
+    this.parsedFragment = newFragment
+  }
+
+  async setupReviewersEmail({
+    FragmentModel,
+    agree = false,
+    isSubmitted = false,
+    isRevision = false,
+    recommendation = {},
+    newFragmentId = '',
+  }) {
+    const {
+      baseUrl,
+      UserModel,
+      collection,
+      parsedFragment: { recommendations, title, type },
+      authors: { submittingAuthor: { firstName = '', lastName = '' } },
+    } = this
+    let { parsedFragment: { id } } = this
+    const fragment = await FragmentModel.find(id)
+    const fragmentHelper = new Fragment({ fragment })
+    const reviewerInvitations = fragmentHelper.getReviewerInvitations({
+      agree,
+    })
+
+    const hasReview = invUserId => rec =>
+      rec.recommendationType === 'review' &&
+      rec.submittedOn &&
+      invUserId === rec.userId
+
+    const reviewerPromises = await reviewerInvitations.map(async inv => {
+      if (!agree) return UserModel.find(inv.userId)
+      const submittedReview = recommendations.find(hasReview(inv.userId))
+      const shouldReturnUser =
+        (isSubmitted && submittedReview) || (!isSubmitted && !submittedReview)
+      if (shouldReturnUser) return UserModel.find(inv.userId)
+    })
+
+    let emailType = 'agreed-reviewers-after-recommendation'
+    let emailText, subject, manuscriptType
+
+    const userHelper = new User({ UserModel })
+    const eic = await userHelper.getEditorInChief()
+    let editorName = isSubmitted
+      ? `${eic.firstName} ${eic.lastName}`
+      : collection.handlingEditor.name
+
+    let reviewers = await Promise.all(reviewerPromises)
+    reviewers = reviewers.filter(Boolean)
+
+    if (agree) {
+      subject = isSubmitted
+        ? `${collection.customId}: Manuscript Decision`
+        : `${collection.customId}: Manuscript ${getSubject(recommendation)}`
+
+      if (isSubmitted) {
+        emailType = 'submitting-reviewers-after-decision'
+        emailText = 'has now been rejected'
+        if (recommendation === 'publish') emailText = 'will now be published'
+      }
+
+      if (isRevision) {
+        emailType = 'submitting-reviewers-after-revision'
+        subject = `${collection.customId}: Manuscript Update`
+        editorName = collection.handlingEditor.name
+        id = newFragmentId || id
+      }
+    } else {
+      subject = `${collection.customId}: Reviewer Unassigned`
+      manuscriptType = manuscriptTypes[type]
+      emailType = 'no-response-reviewers-after-recommendation'
+    }
+
+    reviewers.forEach(user =>
+      mailService.sendNotificationEmail({
+        emailType,
+        toEmail: user.email,
+        meta: {
+          baseUrl,
+          emailText,
+          editorName,
+          collection,
+          manuscriptType,
+          timestamp: Date.now(),
+          emailSubject: subject,
+          reviewerName: `${user.firstName} ${user.lastName}`,
+          fragment: {
+            title,
+            authorName: `${firstName} ${lastName}`,
+            id,
+          },
+        },
+      }),
+    )
+  }
+
+  async setupAuthorsEmail({
+    requestToRevision = false,
+    publish = false,
+    FragmentModel,
+  }) {
+    const {
+      baseUrl,
+      collection,
+      parsedFragment: { heRecommendation, id, title, newComments },
+      authors: { submittingAuthor: { email, firstName, lastName } },
+    } = this
+    let comments = get(heRecommendation, 'comments') || []
+    if (requestToRevision) comments = newComments
+    const authorNote = comments.find(comm => comm.public)
+    const content = get(authorNote, 'content')
+    const authorNoteText = content ? `Reason & Details: "${content}"` : ''
+    let emailType = requestToRevision
+      ? 'author-request-to-revision'
+      : 'author-manuscript-rejected'
+    if (publish) emailType = 'author-manuscript-published'
+    let toAuthors = null
+    if (emailType === 'author-request-to-revision') {
+      toAuthors = [
+        {
+          email,
+          name: `${firstName} ${lastName}`,
+        },
+      ]
+    } else {
+      const fragment = await FragmentModel.find(id)
+      toAuthors = fragment.authors.map(author => ({
+        email: author.email,
+        name: `${author.firstName} ${author.lastName}`,
+      }))
+    }
+    toAuthors.forEach(toAuthor => {
+      mailService.sendNotificationEmail({
+        emailType,
+        toEmail: toAuthor.email,
+        meta: {
+          handlingEditorName: get(collection, 'handlingEditor.name'),
+          baseUrl,
+          collection,
+          authorNoteText,
+          fragment: {
+            id,
+            title,
+            authorName: toAuthor.name,
+            submittingAuthorName: `${firstName} ${lastName}`,
+          },
+        },
+      })
+    })
+  }
+
+  async setupHandlingEditorEmail({
+    publish = false,
+    returnWithComments = false,
+    reviewSubmitted = false,
+    reviewerName = '',
+  }) {
+    const {
+      baseUrl,
+      UserModel,
+      collection,
+      parsedFragment: { eicComments = '', title, id },
+      authors: { submittingAuthor: { firstName = '', lastName = '' } },
+    } = this
+    const userHelper = new User({ UserModel })
+    const eic = await userHelper.getEditorInChief()
+    const toEmail = get(collection, 'handlingEditor.email')
+    let emailType = publish
+      ? 'he-manuscript-published'
+      : 'he-manuscript-rejected'
+    if (reviewSubmitted) emailType = 'review-submitted'
+    if (returnWithComments) emailType = 'he-manuscript-return-with-comments'
+    mailService.sendNotificationEmail({
+      toEmail,
+      emailType,
+      meta: {
+        baseUrl,
+        collection,
+        reviewerName,
+        eicComments,
+        eicName: `${eic.firstName} ${eic.lastName}`,
+        emailSubject: `${collection.customId}: Manuscript Decision`,
+        handlingEditorName: get(collection, 'handlingEditor.name') || '',
+        fragment: {
+          id,
+          title,
+          authorName: `${firstName} ${lastName}`,
+        },
+      },
+    })
+  }
+
+  async setupEiCEmail({ recommendation, comments }) {
+    const {
+      baseUrl,
+      UserModel,
+      collection,
+      parsedFragment: { title, id },
+      authors: { submittingAuthor: { firstName, lastName } },
+    } = this
+    const privateNote = comments.find(comm => comm.public === false)
+    const content = get(privateNote, 'content')
+    const privateNoteText =
+      content !== undefined ? `Private note: "${content}"` : ''
+    let paragraph
+    const heRecommendation = getHeRecommendation(recommendation)
+    const manuscriptAuthorText = `the manuscript titled "${title}" by ${firstName} ${lastName}`
+    const publishOrRejectText =
+      'It is my recommendation, based on the reviews I have received for'
+    switch (heRecommendation) {
+      case 'publish':
+        paragraph = `${publishOrRejectText} ${manuscriptAuthorText}, that we should proceed to publication. <br/><br/>
+        ${privateNoteText}<br/><br/>`
+        break
+      case 'reject':
+        paragraph = `${publishOrRejectText}
+        ${manuscriptAuthorText}, that we should reject it for publication. <br/><br/>
+        ${privateNoteText}<br/><br/>`
+        break
+      case 'revision':
+        paragraph = `In order for ${manuscriptAuthorText} to proceed to publication, there needs to be a revision. <br/><br/>
+        ${privateNoteText}<br/><br/>`
+        break
+
+      default:
+        throw new Error('undefined HE recommentation type')
+    }
+
+    const userHelper = new User({ UserModel })
+    const eic = await userHelper.getEditorInChief()
+    const toEmail = eic.email
+    mailService.sendNotificationEmail({
+      toEmail,
+      emailType: 'eic-recommendation',
+      meta: {
+        baseUrl,
+        paragraph,
+        collection,
+        fragment: { id },
+        eicName: `${eic.firstName} ${eic.lastName}`,
+        handlingEditorName: get(collection, 'handlingEditor.name'),
+      },
+    })
+  }
+
+  setupReviewerInvitationEmail({
+    user,
+    invitationId,
+    timestamp,
+    resend = false,
+    authorName,
+  }) {
+    const {
+      baseUrl,
+      collection,
+      parsedFragment: { id, title, abstract },
+      authors,
+    } = this
+    const params = {
+      user,
+      baseUrl,
+      subject: `${collection.customId}: Review Requested`,
+      toEmail: user.email,
+      meta: {
+        fragment: {
+          id,
+          title,
+          abstract,
+          authors,
+        },
+        invitation: {
+          id: invitationId,
+          timestamp,
+        },
+        collection: {
+          id: collection.id,
+          authorName,
+          handlingEditor: collection.handlingEditor,
+        },
+      },
+      emailType: resend ? 'resend-reviewer' : 'invite-reviewer',
+    }
+    mailService.sendReviewerInvitationEmail(params)
+  }
+
+  async setupReviewerDecisionEmail({
+    user,
+    agree = false,
+    timestamp = Date.now(),
+    authorName = '',
+  }) {
+    const {
+      baseUrl,
+      UserModel,
+      collection,
+      parsedFragment: { id, title },
+    } = this
+    const userHelper = new User({ UserModel })
+    const eic = await userHelper.getEditorInChief()
+    const toEmail = collection.handlingEditor.email
+    mailService.sendNotificationEmail({
+      toEmail,
+      user,
+      emailType: agree ? 'reviewer-agreed' : 'reviewer-declined',
+      meta: {
+        collection: { customId: collection.customId, id: collection.id },
+        fragment: { id, title, authorName },
+        handlingEditorName: collection.handlingEditor.name,
+        baseUrl,
+        eicName: `${eic.firstName} ${eic.lastName}`,
+        timestamp,
+      },
+    })
+    if (agree)
+      mailService.sendNotificationEmail({
+        toEmail: user.email,
+        user,
+        emailType: 'reviewer-thank-you',
+        meta: {
+          collection: { customId: collection.customId, id: collection.id },
+          fragment: { id, title, authorName },
+          handlingEditorName: collection.handlingEditor.name,
+          baseUrl,
+        },
+      })
+  }
+
+  async setupReviewerUnassignEmail({ user, authorName }) {
+    const { collection, parsedFragment: { title = '' } } = this
+
+    await mailService.sendNotificationEmail({
+      toEmail: user.email,
+      user,
+      emailType: 'unassign-reviewer',
+      meta: {
+        collection: { customId: collection.customId },
+        fragment: { title, authorName },
+        handlingEditorName: collection.handlingEditor.name,
+      },
+    })
+  }
+
+  async setupManuscriptSubmittedEmail() {
+    const {
+      baseUrl,
+      UserModel,
+      collection,
+      parsedFragment: { id, title },
+      authors: { submittingAuthor: { firstName = '', lastName = '' } },
+    } = this
+
+    const userHelper = new User({ UserModel })
+    const eic = await userHelper.getEditorInChief()
+
+    mailService.sendSimpleEmail({
+      toEmail: eic.email,
+      emailType: 'manuscript-submitted',
+      dashboardUrl: baseUrl,
+      meta: {
+        collection,
+        fragment: { id, authorName: `${firstName} ${lastName}`, title },
+        eicName: `${eic.firstName} ${eic.lastName}`,
+      },
+    })
+  }
+
+  async setupNewVersionSubmittedEmail() {
+    const {
+      baseUrl,
+      UserModel,
+      collection,
+      parsedFragment: { id, title },
+      authors: { submittingAuthor: { firstName = '', lastName = '' } },
+    } = this
+
+    const userHelper = new User({ UserModel })
+    const eic = await userHelper.getEditorInChief()
+
+    mailService.sendNotificationEmail({
+      toEmail: collection.handlingEditor.email,
+      emailType: 'new-version-submitted',
+      meta: {
+        baseUrl,
+        collection,
+        eicName: `${eic.firstName} ${eic.lastName}`,
+        fragment: { id, authorName: `${firstName} ${lastName}`, title },
+        handlingEditorName: collection.handlingEditor.name,
+      },
+    })
+  }
+}
+
+const getSubject = recommendation =>
+  ['minor', 'major'].includes(recommendation)
+    ? 'Revision Requested'
+    : 'Recommendation Submitted'
+
+const getHeRecommendation = recommendation => {
+  let heRecommendation = recommendation === 'reject' ? 'reject' : 'publish'
+  if (['minor', 'major'].includes(recommendation)) {
+    heRecommendation = 'revision'
+  }
+  return heRecommendation
+}
+
+module.exports = Email
diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js
new file mode 100644
index 0000000000000000000000000000000000000000..6e418ee90f6d1e80bf308e16e9c89f627efeaaa4
--- /dev/null
+++ b/packages/component-helper-service/src/services/Fragment.js
@@ -0,0 +1,111 @@
+const get = require('lodash/get')
+
+class Fragment {
+  constructor({ fragment }) {
+    this.fragment = fragment
+  }
+
+  set _fragment(newFragment) {
+    this.fragment = newFragment
+  }
+
+  static setFragmentOwners(fragment = {}, author = {}) {
+    const { owners = [] } = fragment
+    if (author.isSubmitting) {
+      const authorAlreadyOwner = owners.includes(author.id)
+      if (!authorAlreadyOwner) {
+        return [author.id, ...owners]
+      }
+    }
+    return owners
+  }
+
+  async getFragmentData({ handlingEditor = {} }) {
+    const { fragment: { metadata = {}, recommendations = [], id } } = this
+    const heRecommendation = recommendations.find(
+      rec => rec.userId === handlingEditor.id,
+    )
+    let { title = '', abstract = '' } = metadata
+    const { type } = metadata
+    title = title.replace(/<(.|\n)*?>/g, '')
+    abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : ''
+
+    return {
+      id,
+      type,
+      title,
+      abstract,
+      recommendations,
+      heRecommendation,
+    }
+  }
+
+  async addAuthor({ user, isSubmitting, isCorresponding }) {
+    const { fragment } = this
+    fragment.authors = fragment.authors || []
+    const author = {
+      id: user.id,
+      firstName: user.firstName || '',
+      lastName: user.lastName || '',
+      email: user.email,
+      title: user.title || '',
+      affiliation: user.affiliation || '',
+      isSubmitting,
+      isCorresponding,
+    }
+    fragment.authors.push(author)
+    fragment.owners = this.constructor.setFragmentOwners(fragment, author)
+    await fragment.save()
+
+    return author
+  }
+
+  async getAuthorData({ UserModel }) {
+    const { fragment: { authors = [] } } = this
+    const submittingAuthorData = authors.find(author => author.isSubmitting)
+
+    try {
+      const submittingAuthor = await UserModel.find(
+        get(submittingAuthorData, 'id'),
+      )
+
+      const authorsPromises = authors.map(async author => {
+        const user = await UserModel.find(author.id)
+        return `${user.firstName} ${user.lastName}`
+      })
+      const authorsList = await Promise.all(authorsPromises)
+
+      return {
+        authorsList,
+        submittingAuthor,
+      }
+    } catch (e) {
+      throw e
+    }
+  }
+
+  getReviewerInvitations({ agree = true }) {
+    const { fragment: { invitations = [] } } = this
+    return agree
+      ? invitations.filter(
+          inv =>
+            inv.role === 'reviewer' &&
+            inv.hasAnswer === true &&
+            inv.isAccepted === true,
+        )
+      : invitations.filter(
+          inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+        )
+  }
+
+  getHeRequestToRevision() {
+    const { fragment: { recommendations = [] } } = this
+    return recommendations.find(
+      rec =>
+        rec.recommendationType === 'editorRecommendation' &&
+        (rec.recommendation === 'minor' || rec.recommendation === 'major'),
+    )
+  }
+}
+
+module.exports = Fragment
diff --git a/packages/component-helper-service/src/services/Invitation.js b/packages/component-helper-service/src/services/Invitation.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce06b5ad4d8617a2c97506bc5759cdbb10200906
--- /dev/null
+++ b/packages/component-helper-service/src/services/Invitation.js
@@ -0,0 +1,78 @@
+const uuid = require('uuid')
+const logger = require('@pubsweet/logger')
+
+class Invitation {
+  constructor({ userId, role }) {
+    this.userId = userId
+    this.role = role
+  }
+
+  set _userId(newUserId) {
+    this.userId = newUserId
+  }
+
+  getInvitationsData({ invitations = [] }) {
+    const { userId, role } = this
+    const matchingInvitation = invitations.find(
+      inv => inv.role === role && inv.userId === userId,
+    )
+    if (!matchingInvitation) {
+      logger.error(
+        `There should be at least one matching invitation between User ${userId} and Role ${role}`,
+      )
+      throw Error('no matching invitation')
+    }
+    let status = 'pending'
+    if (matchingInvitation.isAccepted) {
+      status = 'accepted'
+    } else if (matchingInvitation.hasAnswer) {
+      status = 'declined'
+    }
+
+    const { invitedOn, respondedOn, id } = matchingInvitation
+    return { invitedOn, respondedOn, status, id }
+  }
+
+  async createInvitation({ parentObject }) {
+    const { userId, role } = this
+    const invitation = {
+      role,
+      hasAnswer: false,
+      isAccepted: false,
+      invitedOn: Date.now(),
+      id: uuid.v4(),
+      userId,
+      respondedOn: null,
+    }
+    parentObject.invitations = parentObject.invitations || []
+    parentObject.invitations.push(invitation)
+    await parentObject.save()
+
+    return invitation
+  }
+
+  getInvitation({ invitations = [] }) {
+    return invitations.find(
+      invitation =>
+        invitation.userId === this.userId && invitation.role === this.role,
+    )
+  }
+
+  validateInvitation({ invitation }) {
+    if (invitation === undefined)
+      return { status: 404, error: 'Invitation not found.' }
+
+    if (invitation.hasAnswer)
+      return { status: 400, error: 'Invitation has already been answered.' }
+
+    if (invitation.userId !== this.userId)
+      return {
+        status: 403,
+        error: 'User is not allowed to modify this invitation.',
+      }
+
+    return { error: null }
+  }
+}
+
+module.exports = Invitation
diff --git a/packages/component-helper-service/src/services/Team.js b/packages/component-helper-service/src/services/Team.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e27f9a149198a158d02cb6d8e163c6a880175c5
--- /dev/null
+++ b/packages/component-helper-service/src/services/Team.js
@@ -0,0 +1,145 @@
+const logger = require('@pubsweet/logger')
+const get = require('lodash/get')
+
+class Team {
+  constructor({ TeamModel = {}, fragmentId = '', collectionId = '' }) {
+    this.TeamModel = TeamModel
+    this.fragmentId = fragmentId
+    this.collectionId = collectionId
+  }
+
+  async createTeam({ role = '', members = [], objectType = '' }) {
+    const { fragmentId, TeamModel, collectionId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
+
+    let permissions, group, name
+    switch (role) {
+      case 'handlingEditor':
+        permissions = 'handlingEditor'
+        group = 'handlingEditor'
+        name = 'Handling Editor'
+        break
+      case 'reviewer':
+        permissions = 'reviewer'
+        group = 'reviewer'
+        name = 'Reviewer'
+        break
+      case 'author':
+        permissions = 'author'
+        group = 'author'
+        name = 'author'
+        break
+      default:
+        break
+    }
+
+    const teamBody = {
+      teamType: {
+        name: role,
+        permissions,
+      },
+      group,
+      name,
+      object: {
+        type: objectType,
+        id: objectId,
+      },
+      members,
+    }
+    let team = new TeamModel(teamBody)
+    team = await team.save()
+    return team
+  }
+
+  async setupTeam({ user, role, objectType }) {
+    const { TeamModel, fragmentId, collectionId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
+    const teams = await TeamModel.all()
+    user.teams = user.teams || []
+    let foundTeam = teams.find(
+      team =>
+        team.group === role &&
+        team.object.type === objectType &&
+        team.object.id === objectId,
+    )
+
+    if (foundTeam !== undefined) {
+      if (!foundTeam.members.includes(user.id)) {
+        foundTeam.members.push(user.id)
+      }
+
+      try {
+        foundTeam = await foundTeam.save()
+        if (!user.teams.includes(foundTeam.id)) {
+          user.teams.push(foundTeam.id)
+          await user.save()
+        }
+        return foundTeam
+      } catch (e) {
+        logger.error(e)
+      }
+    } else {
+      const team = await this.createTeam({
+        role,
+        members: [user.id],
+        objectType,
+      })
+      user.teams.push(team.id)
+      await user.save()
+      return team
+    }
+  }
+
+  async removeTeamMember({ teamId, userId }) {
+    const { TeamModel } = this
+    const team = await TeamModel.find(teamId)
+    const members = team.members.filter(member => member !== userId)
+    team.members = members
+    await team.save()
+  }
+
+  async getTeamMembers({ role, objectType }) {
+    const { TeamModel, collectionId, fragmentId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
+
+    const teams = await TeamModel.all()
+
+    const members = get(
+      teams.find(
+        team =>
+          team.group === role &&
+          team.object.type === objectType &&
+          team.object.id === objectId,
+      ),
+      'members',
+    )
+
+    return members
+  }
+
+  async getTeam({ role, objectType }) {
+    const { TeamModel, fragmentId, collectionId } = this
+    const objectId = objectType === 'collection' ? collectionId : fragmentId
+    const teams = await TeamModel.all()
+    return teams.find(
+      team =>
+        team.group === role &&
+        team.object.type === objectType &&
+        team.object.id === objectId,
+    )
+  }
+
+  async deleteHandlingEditor({ collection, role, user }) {
+    const team = await this.getTeam({
+      role,
+      objectType: 'collection',
+    })
+    delete collection.handlingEditor
+    await this.removeTeamMember({ teamId: team.id, userId: user.id })
+    user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
+    await user.save()
+    await collection.save()
+  }
+}
+
+module.exports = Team
diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js
new file mode 100644
index 0000000000000000000000000000000000000000..d165dae7ede1a592950c6b1c417fb96e24982d76
--- /dev/null
+++ b/packages/component-helper-service/src/services/User.js
@@ -0,0 +1,72 @@
+const uuid = require('uuid')
+const crypto = require('crypto')
+const logger = require('@pubsweet/logger')
+const mailService = require('pubsweet-component-mail-service')
+
+class User {
+  constructor({ UserModel = {} }) {
+    this.UserModel = UserModel
+  }
+
+  async createUser({ role, body }) {
+    const { UserModel } = this
+    const { email, firstName, lastName, affiliation, title } = body
+    const username = email
+    const password = uuid.v4()
+    const userBody = {
+      username,
+      email,
+      password,
+      passwordResetToken: crypto.randomBytes(32).toString('hex'),
+      isConfirmed: false,
+      firstName,
+      lastName,
+      affiliation,
+      title,
+      editorInChief: role === 'editorInChief',
+      admin: role === 'admin',
+      handlingEditor: role === 'handlingEditor',
+      invitationToken: role === 'reviewer' ? uuid.v4() : '',
+    }
+
+    let newUser = new UserModel(userBody)
+
+    newUser = await newUser.save()
+    return newUser
+  }
+
+  async setupNewUser({ url, role, invitationType, body = {} }) {
+    const newUser = await this.createUser({ role, body })
+
+    try {
+      if (role !== 'reviewer') {
+        mailService.sendSimpleEmail({
+          toEmail: newUser.email,
+          user: newUser,
+          emailType: invitationType,
+          dashboardUrl: url,
+        })
+      }
+
+      return newUser
+    } catch (e) {
+      logger.error(e.message)
+      return { status: 500, error: 'Email could not be sent.' }
+    }
+  }
+
+  async getEditorInChief() {
+    const { UserModel } = this
+    const users = await UserModel.all()
+    const eic = users.find(user => user.editorInChief || user.admin)
+    return eic
+  }
+
+  async updateUserTeams({ userId, teamId }) {
+    const user = await this.UserModel.find(userId)
+    user.teams.push(teamId)
+    user.save()
+  }
+}
+
+module.exports = User
diff --git a/packages/component-invite/src/helpers/authsome.js b/packages/component-helper-service/src/services/authsome.js
similarity index 94%
rename from packages/component-invite/src/helpers/authsome.js
rename to packages/component-helper-service/src/services/authsome.js
index 212cee2a3ea23a424b1f77dde2f7dd4cf888b2a0..b2215bb20f4d5d50cd0f75fff2607ba79f514443 100644
--- a/packages/component-invite/src/helpers/authsome.js
+++ b/packages/component-helper-service/src/services/authsome.js
@@ -12,6 +12,7 @@ const getAuthsome = models =>
       models: {
         Collection: {
           find: id => models.Collection.find(id),
+          all: () => models.Collection.all(),
         },
         Fragment: {
           find: id => models.Fragment.find(id),
diff --git a/packages/component-manuscript-manager/src/helpers/helpers.js b/packages/component-helper-service/src/services/services.js
similarity index 96%
rename from packages/component-manuscript-manager/src/helpers/helpers.js
rename to packages/component-helper-service/src/services/services.js
index a37f7504aaa1efd3fbacdc94734fc0807cfa5734..cb56e8ec7edd3c811f3f77f11f66567c70d4a898 100644
--- a/packages/component-manuscript-manager/src/helpers/helpers.js
+++ b/packages/component-helper-service/src/services/services.js
@@ -8,7 +8,7 @@ const checkForUndefinedParams = (...params) => {
   return true
 }
 
-const validateEmailAndToken = async (email, token, userModel) => {
+const validateEmailAndToken = async ({ email, token, userModel }) => {
   try {
     const user = await userModel.findByEmail(email)
     if (user) {
diff --git a/packages/component-invite/config/authsome-helpers.js b/packages/component-invite/config/authsome-helpers.js
deleted file mode 100644
index b5f42492b077cee12d5a09d97404fb559e601f5e..0000000000000000000000000000000000000000
--- a/packages/component-invite/config/authsome-helpers.js
+++ /dev/null
@@ -1,83 +0,0 @@
-const omit = require('lodash/omit')
-const config = require('config')
-const get = require('lodash/get')
-
-const statuses = config.get('statuses')
-
-const publicStatusesPermissions = ['author', 'reviewer']
-
-const parseAuthorsData = (coll, matchingCollPerm) => {
-  if (['reviewer'].includes(matchingCollPerm.permission)) {
-    coll.authors = coll.authors.map(a => omit(a, ['email']))
-  }
-}
-
-const setPublicStatuses = (coll, matchingCollPerm) => {
-  const status = get(coll, 'status') || 'draft'
-  coll.visibleStatus = statuses[status].public
-  if (!publicStatusesPermissions.includes(matchingCollPerm.permission)) {
-    coll.visibleStatus = statuses[coll.status].private
-  }
-}
-
-const filterRefusedInvitations = (coll, user) => {
-  const matchingInv = coll.invitations.find(inv => inv.userId === user.id)
-  if (matchingInv === undefined) return null
-  if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null
-  return coll
-}
-
-const filterObjectData = (
-  collectionsPermissions = [],
-  object = {},
-  user = {},
-) => {
-  if (object.type === 'fragment') {
-    const matchingCollPerm = collectionsPermissions.find(
-      collPerm => object.id === collPerm.fragmentId,
-    )
-    if (matchingCollPerm === undefined) return null
-    if (['reviewer'].includes(matchingCollPerm.permission)) {
-      object.files = omit(object.files, ['coverLetter'])
-      if (object.recommendations)
-        object.recommendations = object.recommendations.filter(
-          rec => rec.userId === user.id,
-        )
-    }
-
-    return object
-  }
-  const matchingCollPerm = collectionsPermissions.find(
-    collPerm => object.id === collPerm.id,
-  )
-  if (matchingCollPerm === undefined) return null
-  setPublicStatuses(object, matchingCollPerm)
-  parseAuthorsData(object, matchingCollPerm)
-  if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
-    return filterRefusedInvitations(object, user)
-  }
-
-  return object
-}
-
-const getTeamsByPermissions = async (teamIds, permissions, TeamModel) => {
-  const teams = await Promise.all(
-    teamIds.map(async teamId => {
-      const team = await TeamModel.find(teamId)
-      if (permissions.includes(team.teamType.permissions)) {
-        return team
-      }
-      return null
-    }),
-  )
-
-  return teams.filter(Boolean)
-}
-
-module.exports = {
-  parseAuthorsData,
-  setPublicStatuses,
-  filterRefusedInvitations,
-  filterObjectData,
-  getTeamsByPermissions,
-}
diff --git a/packages/component-invite/config/authsome-mode.js b/packages/component-invite/config/authsome-mode.js
index 762998f83e80d5e678595bb8bf7e57072607adbe..9c663beae1962d9fc13141f8439dc0a84214ae08 100644
--- a/packages/component-invite/config/authsome-mode.js
+++ b/packages/component-invite/config/authsome-mode.js
@@ -1,252 +1,3 @@
-const get = require('lodash/get')
-const pickBy = require('lodash/pickBy')
-const omit = require('lodash/omit')
-const helpers = require('./authsome-helpers')
-
-async function teamPermissions(user, operation, object, context) {
-  const permissions = ['handlingEditor', 'author', 'reviewer']
-  const teams = await helpers.getTeamsByPermissions(
-    user.teams,
-    permissions,
-    context.models.Team,
-  )
-
-  const collectionsPermissions = await Promise.all(
-    teams.map(async team => {
-      const collection = await context.models.Collection.find(team.object.id)
-      const collPerm = {
-        id: collection.id,
-        permission: team.teamType.permissions,
-      }
-      const objectType = get(object, 'type')
-      if (objectType === 'fragment' && collection.fragments.includes(object.id))
-        collPerm.fragmentId = object.id
-
-      return collPerm
-    }),
-  )
-
-  if (collectionsPermissions.length === 0) return {}
-
-  return {
-    filter: filterParam => {
-      if (!filterParam.length) {
-        return helpers.filterObjectData(
-          collectionsPermissions,
-          filterParam,
-          user,
-        )
-      }
-
-      const collections = filterParam
-        .map(coll =>
-          helpers.filterObjectData(collectionsPermissions, coll, user),
-        )
-        .filter(Boolean)
-      return collections
-    },
-  }
-}
-
-function unauthenticatedUser(operation, object) {
-  // Public/unauthenticated users can GET /collections, filtered by 'published'
-  if (operation === 'GET' && object && object.path === '/collections') {
-    return {
-      filter: collections =>
-        collections.filter(collection => collection.published),
-    }
-  }
-
-  // Public/unauthenticated users can GET /collections/:id/fragments, filtered by 'published'
-  if (
-    operation === 'GET' &&
-    object &&
-    object.path === '/collections/:id/fragments'
-  ) {
-    return {
-      filter: fragments => fragments.filter(fragment => fragment.published),
-    }
-  }
-
-  // and filtered individual collection's properties: id, title, source, content, owners
-  if (operation === 'GET' && object && object.type === 'collection') {
-    if (object.published) {
-      return {
-        filter: collection =>
-          pickBy(collection, (_, key) =>
-            ['id', 'title', 'owners'].includes(key),
-          ),
-      }
-    }
-  }
-
-  if (operation === 'GET' && object && object.type === 'fragment') {
-    if (object.published) {
-      return {
-        filter: fragment =>
-          pickBy(fragment, (_, key) =>
-            ['id', 'title', 'source', 'presentation', 'owners'].includes(key),
-          ),
-      }
-    }
-  }
-
-  return false
-}
-
-async function authenticatedUser(user, operation, object, context) {
-  // Allow the authenticated user to POST a collection (but not with a 'filtered' property)
-  if (operation === 'POST' && object.path === '/collections') {
-    return {
-      filter: collection => omit(collection, 'filtered'),
-    }
-  }
-
-  if (
-    operation === 'POST' &&
-    object.path === '/collections/:collectionId/fragments'
-  ) {
-    return true
-  }
-
-  // Allow the authenticated user to GET collections they own
-  if (operation === 'GET' && object === '/collections/') {
-    return {
-      filter: collection => collection.owners.includes(user.id),
-    }
-  }
-
-  // Allow owners of a collection to GET its teams, e.g.
-  // GET /api/collections/1/teams
-  if (operation === 'GET' && get(object, 'path') === '/teams') {
-    const collectionId = get(object, 'params.collectionId')
-    if (collectionId) {
-      const collection = await context.models.Collection.find(collectionId)
-      if (collection.owners.includes(user.id)) {
-        return true
-      }
-    }
-  }
-
-  if (
-    operation === 'GET' &&
-    get(object, 'type') === 'team' &&
-    get(object, 'object.type') === 'collection'
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object, 'object.id'),
-    )
-    if (collection.owners.includes(user.id)) {
-      return true
-    }
-  }
-
-  // Advanced example
-  // Allow authenticated users to create a team based around a collection
-  // if they are one of the owners of this collection
-  if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') {
-    if (get(object, 'object.type') === 'collection') {
-      const collection = await context.models.Collection.find(
-        get(object, 'object.id'),
-      )
-      if (collection.owners.includes(user.id)) {
-        return true
-      }
-    }
-  }
-
-  // only allow the HE to create, delete an invitation, or get invitation details
-  if (
-    ['POST', 'GET', 'DELETE'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('invitations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const handlingEditor = get(collection, 'handlingEditor')
-    if (!handlingEditor) return false
-    if (handlingEditor.id === user.id) return true
-    return false
-  }
-
-  // only allow a reviewer to submit and to modify a recommendation
-  if (
-    ['POST', 'PATCH'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('recommendations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const teams = await helpers.getTeamsByPermissions(
-      user.teams,
-      ['reviewer'],
-      context.models.Team,
-    )
-    if (teams.length === 0) return false
-    const matchingTeam = teams.find(team => team.object.id === collection.id)
-    if (matchingTeam) return true
-    return false
-  }
-
-  if (user.teams.length !== 0 && operation === 'GET') {
-    const permissions = await teamPermissions(user, operation, object, context)
-
-    if (permissions) {
-      return permissions
-    }
-  }
-
-  if (get(object, 'type') === 'fragment') {
-    const fragment = object
-
-    if (fragment.owners.includes(user.id)) {
-      return true
-    }
-  }
-
-  if (get(object, 'type') === 'collection') {
-    if (['GET', 'DELETE'].includes(operation)) {
-      return true
-    }
-
-    // Only allow filtered updating (mirroring filtered creation) for non-admin users)
-    if (operation === 'PATCH') {
-      return {
-        filter: collection => omit(collection, 'filtered'),
-      }
-    }
-  }
-
-  // A user can GET, DELETE and PATCH itself
-  if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
-    if (['GET', 'DELETE', 'PATCH'].includes(operation)) {
-      return true
-    }
-  }
-  // If no individual permissions exist (above), fallback to unauthenticated
-  // user's permission
-  return unauthenticatedUser(operation, object)
-}
-
-const authsomeMode = async (userId, operation, object, context) => {
-  if (!userId) {
-    return unauthenticatedUser(operation, object)
-  }
-
-  // It's up to us to retrieve the relevant models for our
-  // authorization/authsome mode, e.g.
-  const user = await context.models.User.find(userId)
-
-  // Admins and editor in chiefs can do anything
-  if (user && (user.admin === true || user.editorInChief === true)) return true
-
-  if (user) {
-    return authenticatedUser(user, operation, object, context)
-  }
-
-  return false
-}
+const authsomeMode = require('xpub-faraday/config/authsome-mode')
 
 module.exports = authsomeMode
diff --git a/packages/component-invite/config/default.js b/packages/component-invite/config/default.js
index 7afbb2f22c26d8ae4d9f314989bc4a02189360f7..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f 100644
--- a/packages/component-invite/config/default.js
+++ b/packages/component-invite/config/default.js
@@ -1,58 +1,3 @@
-const path = require('path')
+const defaultConfig = require('xpub-faraday/config/default')
 
-module.exports = {
-  authsome: {
-    mode: path.resolve(__dirname, 'authsome-mode.js'),
-    teams: {
-      handlingEditor: {
-        name: 'Handling Editors',
-      },
-      reviewer: {
-        name: 'Reviewer',
-      },
-    },
-  },
-  mailer: {
-    from: 'test@example.com',
-  },
-  'invite-reset-password': {
-    url:
-      process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL ||
-      'http://localhost:3000/invite',
-  },
-  roles: {
-    global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
-    collection: ['handlingEditor', 'reviewer', 'author'],
-    inviteRights: {
-      admin: ['admin', 'editorInChief', 'author'],
-      editorInChief: ['handlingEditor'],
-      handlingEditor: ['reviewer'],
-    },
-  },
-  statuses: {
-    draft: {
-      public: 'Draft',
-      private: 'Draft',
-    },
-    submitted: {
-      public: 'Submitted',
-      private: 'Submitted',
-    },
-    heInvited: {
-      public: 'Submitted',
-      private: 'HE Invited',
-    },
-    heAssigned: {
-      public: 'HE Assigned',
-      private: 'HE Assigned',
-    },
-    reviewersInvited: {
-      public: 'Reviewers Invited',
-      private: 'Reviewers Invited',
-    },
-    underReview: {
-      public: 'Under Review',
-      private: 'Under Review',
-    },
-  },
-}
+module.exports = defaultConfig
diff --git a/packages/component-invite/config/test.js b/packages/component-invite/config/test.js
index 0eb54780a931f66f1b611d9a4d51552908ece2c3..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f 100644
--- a/packages/component-invite/config/test.js
+++ b/packages/component-invite/config/test.js
@@ -1,59 +1,3 @@
-const path = require('path')
+const defaultConfig = require('xpub-faraday/config/default')
 
-module.exports = {
-  authsome: {
-    mode: path.resolve(__dirname, 'authsome-mode.js'),
-    teams: {
-      handlingEditor: {
-        name: 'Handling Editors',
-      },
-      reviewer: {
-        name: 'Reviewer',
-      },
-    },
-  },
-  mailer: {
-    from: 'test@example.com',
-  },
-  'invite-reset-password': {
-    url:
-      process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL ||
-      'http://localhost:3000/invite',
-  },
-  roles: {
-    global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
-    collection: ['handlingEditor', 'reviewer', 'author'],
-    inviteRights: {
-      admin: ['admin', 'editorInChief', 'author', 'handlingEditor', 'author'],
-      editorInChief: ['handlingEditor'],
-      handlingEditor: ['reviewer'],
-      author: ['author'],
-    },
-  },
-  statuses: {
-    draft: {
-      public: 'Draft',
-      private: 'Draft',
-    },
-    submitted: {
-      public: 'Submitted',
-      private: 'Submitted',
-    },
-    heInvited: {
-      public: 'Submitted',
-      private: 'HE Invited',
-    },
-    heAssigned: {
-      public: 'HE Assigned',
-      private: 'HE Assigned',
-    },
-    reviewersInvited: {
-      public: 'Reviewers Invited',
-      private: 'Reviewers Invited',
-    },
-    underReview: {
-      public: 'Under Review',
-      private: 'Under Review',
-    },
-  },
-}
+module.exports = defaultConfig
diff --git a/packages/component-invite/index.js b/packages/component-invite/index.js
index 0e21fe5223f0a0a71bebd53d509b0bef3a5e3e02..f9bc6f5a6ef371301ad40dc80c6f06c59b67577f 100644
--- a/packages/component-invite/index.js
+++ b/packages/component-invite/index.js
@@ -1,5 +1,6 @@
 module.exports = {
   backend: () => app => {
     require('./src/CollectionsInvitations')(app)
+    require('./src/FragmentsInvitations')(app)
   },
 }
diff --git a/packages/component-invite/src/CollectionsInvitations.js b/packages/component-invite/src/CollectionsInvitations.js
index ba6fa2d852bf1a14b39b182297eff50f2c8661b7..2f80dd1b70e3089a7ab7f9068a4e12dbb84985e5 100644
--- a/packages/component-invite/src/CollectionsInvitations.js
+++ b/packages/component-invite/src/CollectionsInvitations.js
@@ -14,13 +14,13 @@ const CollectionsInvitations = app => {
    * @apiParamExample {json} Body
    *    {
    *      "email": "email@example.com",
-   *      "role": "handlingEditor", [acceptedValues: handlingEditor, reviewer]
+   *      "role": "handlingEditor", [acceptedValues: handlingEditor]
    *    }
    * @apiSuccessExample {json} Success
    *    HTTP/1.1 200 OK
    *   {
    *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
-   *     "role": "reviewer",
+   *     "role": "handlingEditor",
    *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
    *     "hasAnswer": false,
    *     "invitedOn": 1525428890167,
@@ -38,32 +38,6 @@ const CollectionsInvitations = app => {
     authBearer,
     require(`${routePath}/post`)(app.locals.models),
   )
-  /**
-   * @api {get} /api/collections/:collectionId/invitations/[:invitationId]?role=:role List collections invitations
-   * @apiGroup CollectionsInvitations
-   * @apiParam {id} collectionId Collection id
-   * @apiParam {id} [invitationId] Invitation id
-   * @apiParam {String} role The role to search for: handlingEditor, reviewer, author
-   * @apiSuccessExample {json} Success
-   *    HTTP/1.1 200 OK
-   *    [{
-   *      "name": "John Smith",
-   *     "invitedOn": 1525428890167,
-   *     "respondedOn": 1525428890299,
-   *      "email": "email@example.com",
-   *      "status": "pending",
-   *      "invitationId": "1990881"
-   *    }]
-   * @apiErrorExample {json} List errors
-   *    HTTP/1.1 403 Forbidden
-   *    HTTP/1.1 400 Bad Request
-   *    HTTP/1.1 404 Not Found
-   */
-  app.get(
-    `${basePath}/:invitationId?`,
-    authBearer,
-    require(`${routePath}/get`)(app.locals.models),
-  )
   /**
    * @api {delete} /api/collections/:collectionId/invitations/:invitationId Delete invitation
    * @apiGroup CollectionsInvitations
@@ -95,7 +69,7 @@ const CollectionsInvitations = app => {
    *    HTTP/1.1 200 OK
    *   {
    *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
-   *     "role": "reviewer",
+   *     "role": "handlingEditor",
    *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
    *     "hasAnswer": true,
    *     "invitedOn": 1525428890167,
@@ -113,28 +87,6 @@ const CollectionsInvitations = app => {
     authBearer,
     require(`${routePath}/patch`)(app.locals.models),
   )
-  /**
-   * @api {patch} /api/collections/:collectionId/invitations/:invitationId/decline Decline an invitation as a reviewer
-   * @apiGroup CollectionsInvitations
-   * @apiParam {collectionId} collectionId Collection id
-   * @apiParam {invitationId} invitationId Invitation id
-   * @apiParamExample {json} Body
-   *    {
-   *      "invitationToken": "f2d814f0-67a5-4590-ba4f-6a83565feb4f",
-   *    }
-   * @apiSuccessExample {json} Success
-   *    HTTP/1.1 200 OK
-   *    {}
-   * @apiErrorExample {json} Update invitations errors
-   *    HTTP/1.1 403 Forbidden
-   *    HTTP/1.1 400 Bad Request
-   *    HTTP/1.1 404 Not Found
-   *    HTTP/1.1 500 Internal Server Error
-   */
-  app.patch(
-    `${basePath}/:invitationId/decline`,
-    require(`${routePath}/decline`)(app.locals.models),
-  )
 }
 
 module.exports = CollectionsInvitations
diff --git a/packages/component-invite/src/FragmentsInvitations.js b/packages/component-invite/src/FragmentsInvitations.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a4d2507424cbeb5a5a0c0357779508fed009a8d
--- /dev/null
+++ b/packages/component-invite/src/FragmentsInvitations.js
@@ -0,0 +1,145 @@
+const bodyParser = require('body-parser')
+
+const FragmentsInvitations = app => {
+  app.use(bodyParser.json())
+  const basePath =
+    '/api/collections/:collectionId/fragments/:fragmentId/invitations'
+  const routePath = './routes/fragmentsInvitations'
+  const authBearer = app.locals.passport.authenticate('bearer', {
+    session: false,
+  })
+  /**
+   * @api {post} /api/collections/:collectionId/fragments/:fragmentId/invitations Invite a user to a fragment
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiParamExample {json} Body
+   *    {
+   *      "email": "email@example.com",
+   *      "role": "reviewer", [acceptedValues: reviewer]
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *   {
+   *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
+   *     "role": "reviewer",
+   *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
+   *     "hasAnswer": false,
+   *     "invitedOn": 1525428890167,
+   *     "isAccepted": false,
+   *     "respondedOn": null
+   *    }
+   * @apiErrorExample {json} Invite user errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.post(
+    basePath,
+    authBearer,
+    require(`${routePath}/post`)(app.locals.models),
+  )
+  /**
+   * @api {get} /api/collections/:collectionId/fragments/:fragmentId/invitations/[:invitationId]?role=:role List fragment invitations
+   * @apiGroup FragmentsInvitations
+   * @apiParam {id} collectionId Collection id
+   * @apiParam {id} fragmentId Fragment id
+   * @apiParam {id} [invitationId] Invitation id
+   * @apiParam {String} role The role to search for: reviewer
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *    [{
+   *      "name": "John Smith",
+   *     "invitedOn": 1525428890167,
+   *     "respondedOn": 1525428890299,
+   *      "email": "email@example.com",
+   *      "status": "pending",
+   *      "invitationId": "1990881"
+   *    }]
+   * @apiErrorExample {json} List errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   */
+  app.get(
+    `${basePath}/:invitationId?`,
+    authBearer,
+    require(`${routePath}/get`)(app.locals.models),
+  )
+  /**
+   * @api {delete} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId Delete invitation
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiParam {invitationId} invitationId Invitation id
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 204 No Content
+   * @apiErrorExample {json} Delete errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.delete(
+    `${basePath}/:invitationId`,
+    authBearer,
+    require(`${routePath}/delete`)(app.locals.models),
+  )
+  /**
+   * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId Update an invitation
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {invitationId} invitationId Invitation id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiParamExample {json} Body
+   *    {
+   *      "isAccepted": false,
+   *      "reason": "I am not ready" [optional]
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *   {
+   *     "id": "7b2431af-210c-49f9-a69a-e19271066ebd",
+   *     "role": "reviewer",
+   *     "userId": "4c3f8ee1-785b-4adb-87b4-407a27f652c6",
+   *     "hasAnswer": true,
+   *     "invitedOn": 1525428890167,
+   *     "isAccepted": false,
+   *     "respondedOn": 1525428890299
+   *    }
+   * @apiErrorExample {json} Update invitations errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.patch(
+    `${basePath}/:invitationId`,
+    authBearer,
+    require(`${routePath}/patch`)(app.locals.models),
+  )
+  /**
+   * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId/decline Decline an invitation as a reviewer
+   * @apiGroup FragmentsInvitations
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {invitationId} invitationId Invitation id
+   * @apiParamExample {json} Body
+   *    {
+   *      "invitationToken": "f2d814f0-67a5-4590-ba4f-6a83565feb4f",
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *    {}
+   * @apiErrorExample {json} Update invitations errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.patch(
+    `${basePath}/:invitationId/decline`,
+    require(`${routePath}/decline`)(app.locals.models),
+  )
+}
+
+module.exports = FragmentsInvitations
diff --git a/packages/component-invite/src/helpers/Collection.js b/packages/component-invite/src/helpers/Collection.js
deleted file mode 100644
index d04841fc244fa3b7619bacd064d3685267d42ccd..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/helpers/Collection.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const config = require('config')
-const last = require('lodash/last')
-
-const statuses = config.get('statuses')
-
-const addHandlingEditor = async (collection, user, invitation) => {
-  collection.handlingEditor = {
-    id: user.id,
-    name: `${user.firstName} ${user.lastName}`,
-    invitedOn: invitation.invitedOn,
-    respondedOn: invitation.respondedOn,
-    email: user.email,
-    hasAnswer: invitation.hasAnswer,
-    isAccepted: invitation.isAccepted,
-  }
-  await updateStatus(collection, 'heInvited')
-}
-
-const updateHandlingEditor = async (collection, isAccepted) => {
-  collection.handlingEditor.hasAnswer = true
-  collection.handlingEditor.isAccepted = isAccepted
-  collection.handlingEditor.respondedOn = Date.now()
-  let status
-  isAccepted ? (status = 'heAssigned') : (status = 'submitted')
-  await updateStatus(collection, status)
-}
-
-const getFragmentAndAuthorData = async ({
-  UserModel,
-  FragmentModel,
-  collection: { fragments, authors },
-}) => {
-  const fragment = await FragmentModel.find(last(fragments))
-  let { title, abstract } = fragment.metadata
-  title = title.replace(/<(.|\n)*?>/g, '')
-  abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : ''
-
-  const submittingAuthorData = authors.find(
-    author => author.isSubmitting === true,
-  )
-  const author = await UserModel.find(submittingAuthorData.userId)
-  const authorName = `${author.firstName} ${author.lastName}`
-  const authorsPromises = authors.map(async author => {
-    const user = await UserModel.find(author.userId)
-    return ` ${user.firstName} ${user.lastName}`
-  })
-  const authorsList = await Promise.all(authorsPromises)
-  const { id } = fragment
-  return { title, authorName, id, abstract, authorsList }
-}
-
-const updateReviewerCollectionStatus = async collection => {
-  const reviewerInvitations = collection.invitations.filter(
-    inv => inv.role === 'reviewer',
-  )
-  if (reviewerInvitations.length === 0)
-    await updateStatus(collection, 'heAssigned')
-}
-
-const updateStatus = async (collection, newStatus) => {
-  collection.status = newStatus
-  collection.visibleStatus = statuses[collection.status].private
-  await collection.save()
-}
-
-module.exports = {
-  addHandlingEditor,
-  updateHandlingEditor,
-  getFragmentAndAuthorData,
-  updateReviewerCollectionStatus,
-  updateStatus,
-}
diff --git a/packages/component-invite/src/helpers/Invitation.js b/packages/component-invite/src/helpers/Invitation.js
deleted file mode 100644
index c543f1f5435d0822a649c96144f14bfa5f93ea1b..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/helpers/Invitation.js
+++ /dev/null
@@ -1,95 +0,0 @@
-const uuid = require('uuid')
-const collectionHelper = require('./Collection')
-
-const getInvitationData = (invitations, userId, role) => {
-  const matchingInvitation = invitations.find(
-    inv => inv.role === role && inv.userId === userId,
-  )
-  let status = 'pending'
-  if (matchingInvitation.isAccepted) {
-    status = 'accepted'
-  } else if (matchingInvitation.hasAnswer) {
-    status = 'declined'
-  }
-
-  const { invitedOn, respondedOn, id } = matchingInvitation
-  return { invitedOn, respondedOn, status, id }
-}
-
-const setupInvitation = async (userId, role, collection) => {
-  const invitation = {
-    role,
-    hasAnswer: false,
-    isAccepted: false,
-    invitedOn: Date.now(),
-    id: uuid.v4(),
-    userId,
-    respondedOn: null,
-  }
-  collection.invitations = collection.invitations || []
-  collection.invitations.push(invitation)
-  collection = await collection.save()
-  return invitation
-}
-
-const setupReviewerInvitation = async ({
-  baseUrl,
-  FragmentModel,
-  UserModel,
-  collection,
-  user,
-  mailService,
-  invitationId,
-  timestamp,
-  resend = false,
-}) => {
-  const {
-    title,
-    authorName,
-    id,
-    abstract,
-    authorsList,
-  } = await collectionHelper.getFragmentAndAuthorData({
-    UserModel,
-    FragmentModel,
-    collection,
-  })
-
-  const params = {
-    user,
-    baseUrl,
-    subject: `${collection.customId}: Review Requested`,
-    toEmail: user.email,
-    meta: {
-      fragment: {
-        id,
-        title,
-        abstract,
-        authors: authorsList,
-      },
-      invitation: {
-        id: invitationId,
-        timestamp,
-      },
-      collection: {
-        id: collection.id,
-        authorName,
-        handlingEditor: collection.handlingEditor,
-      },
-    },
-    emailType: resend ? 'resend-reviewer' : 'invite-reviewer',
-  }
-  await mailService.sendReviewerInvitationEmail(params)
-}
-
-const getInvitation = (invitations = [], userId, role) =>
-  invitations.find(
-    invitation => invitation.userId === userId && invitation.role === role,
-  )
-
-module.exports = {
-  getInvitationData,
-  setupInvitation,
-  setupReviewerInvitation,
-  getInvitation,
-}
diff --git a/packages/component-invite/src/helpers/Team.js b/packages/component-invite/src/helpers/Team.js
deleted file mode 100644
index 8092336a8cdd94720eea378cd4dfae14a3453f7f..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/helpers/Team.js
+++ /dev/null
@@ -1,125 +0,0 @@
-const logger = require('@pubsweet/logger')
-const get = require('lodash/get')
-
-const createNewTeam = async (collectionId, role, userId, TeamModel) => {
-  let permissions, group, name
-  switch (role) {
-    case 'handlingEditor':
-      permissions = 'handlingEditor'
-      group = 'handlingEditor'
-      name = 'Handling Editor'
-      break
-    case 'reviewer':
-      permissions = 'reviewer'
-      group = 'reviewer'
-      name = 'Reviewer'
-      break
-    case 'author':
-      permissions = 'author'
-      group = 'author'
-      name = 'author'
-      break
-    default:
-      break
-  }
-
-  const teamBody = {
-    teamType: {
-      name: role,
-      permissions,
-    },
-    group,
-    name,
-    object: {
-      type: 'collection',
-      id: collectionId,
-    },
-    members: [userId],
-  }
-  let team = new TeamModel(teamBody)
-  team = await team.save()
-  return team
-}
-
-const setupManuscriptTeam = async (models, user, collectionId, role) => {
-  const teams = await models.Team.all()
-  user.teams = user.teams || []
-  let foundTeam = teams.find(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-
-  if (foundTeam !== undefined) {
-    if (!foundTeam.members.includes(user.id)) {
-      foundTeam.members.push(user.id)
-    }
-
-    try {
-      foundTeam = await foundTeam.save()
-      if (!user.teams.includes(foundTeam.id)) {
-        user.teams.push(foundTeam.id)
-        await user.save()
-      }
-      return foundTeam
-    } catch (e) {
-      logger.error(e)
-    }
-  } else {
-    const team = await createNewTeam(collectionId, role, user.id, models.Team)
-    user.teams.push(team.id)
-    await user.save()
-    return team
-  }
-}
-
-const removeTeamMember = async (teamId, userId, TeamModel) => {
-  const team = await TeamModel.find(teamId)
-  const members = team.members.filter(member => member !== userId)
-  team.members = members
-  await team.save()
-}
-
-const getTeamMembersByCollection = async (collectionId, role, TeamModel) => {
-  const teams = await TeamModel.all()
-  const members = get(
-    teams.find(
-      team =>
-        team.group === role &&
-        team.object.type === 'collection' &&
-        team.object.id === collectionId,
-    ),
-    'members',
-  )
-
-  return members
-}
-
-const getTeamByGroupAndCollection = async (collectionId, role, TeamModel) => {
-  const teams = await TeamModel.all()
-  return teams.find(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-}
-
-const updateHETeam = async (collection, role, TeamModel, user) => {
-  const team = await getTeamByGroupAndCollection(collection.id, role, TeamModel)
-  delete collection.handlingEditor
-  await removeTeamMember(team.id, user.id, TeamModel)
-  user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
-  await user.save()
-  await collection.save()
-}
-
-module.exports = {
-  createNewTeam,
-  setupManuscriptTeam,
-  removeTeamMember,
-  getTeamMembersByCollection,
-  getTeamByGroupAndCollection,
-  updateHETeam,
-}
diff --git a/packages/component-invite/src/helpers/User.js b/packages/component-invite/src/helpers/User.js
deleted file mode 100644
index 4b293a2ad476f3ccf4269869234985b7b6dc0c05..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/helpers/User.js
+++ /dev/null
@@ -1,129 +0,0 @@
-const helpers = require('./helpers')
-const mailService = require('pubsweet-component-mail-service')
-const logger = require('@pubsweet/logger')
-const collectionHelper = require('./Collection')
-
-const setupNewUser = async (
-  body,
-  url,
-  res,
-  email,
-  role,
-  UserModel,
-  invitationType,
-) => {
-  const { firstName, lastName, affiliation, title } = body
-  const newUser = await helpers.createNewUser(
-    email,
-    firstName,
-    lastName,
-    affiliation,
-    title,
-    UserModel,
-    role,
-  )
-
-  try {
-    if (role !== 'reviewer') {
-      await mailService.sendSimpleEmail({
-        toEmail: newUser.email,
-        user: newUser,
-        emailType: invitationType,
-        dashboardUrl: url,
-      })
-    }
-
-    return newUser
-  } catch (e) {
-    logger.error(e.message)
-    return { status: 500, error: 'Email could not be sent.' }
-  }
-}
-
-const getEditorInChief = async UserModel => {
-  const users = await UserModel.all()
-  const eic = users.find(user => user.editorInChief || user.admin)
-  return eic
-}
-
-const setupReviewerDecisionEmailData = async ({
-  baseUrl,
-  UserModel,
-  FragmentModel,
-  collection,
-  user,
-  mailService,
-  agree,
-  timestamp = Date.now(),
-}) => {
-  const {
-    title,
-    authorName,
-    id,
-  } = await collectionHelper.getFragmentAndAuthorData({
-    UserModel,
-    FragmentModel,
-    collection,
-  })
-  const eic = await getEditorInChief(UserModel)
-  const toEmail = collection.handlingEditor.email
-  await mailService.sendNotificationEmail({
-    toEmail,
-    user,
-    emailType: agree ? 'reviewer-agreed' : 'reviewer-declined',
-    meta: {
-      collection: { customId: collection.customId, id: collection.id },
-      fragment: { id, title, authorName },
-      handlingEditorName: collection.handlingEditor.name,
-      baseUrl,
-      eicName: `${eic.firstName} ${eic.lastName}`,
-      timestamp,
-    },
-  })
-  if (agree)
-    await mailService.sendNotificationEmail({
-      toEmail: user.email,
-      user,
-      emailType: 'reviewer-thank-you',
-      meta: {
-        collection: { customId: collection.customId, id: collection.id },
-        fragment: { id, title, authorName },
-        handlingEditorName: collection.handlingEditor.name,
-        baseUrl,
-      },
-    })
-}
-
-const setupReviewerUnassignEmail = async ({
-  UserModel,
-  FragmentModel,
-  collection,
-  user,
-  mailService,
-}) => {
-  const { title, authorName } = await collectionHelper.getFragmentAndAuthorData(
-    {
-      UserModel,
-      FragmentModel,
-      collection,
-    },
-  )
-
-  await mailService.sendNotificationEmail({
-    toEmail: user.email,
-    user,
-    emailType: 'unassign-reviewer',
-    meta: {
-      collection: { customId: collection.customId },
-      fragment: { title, authorName },
-      handlingEditorName: collection.handlingEditor.name,
-    },
-  })
-}
-
-module.exports = {
-  setupNewUser,
-  getEditorInChief,
-  setupReviewerDecisionEmailData,
-  setupReviewerUnassignEmail,
-}
diff --git a/packages/component-invite/src/helpers/helpers.js b/packages/component-invite/src/helpers/helpers.js
deleted file mode 100644
index b484721469b65e466195b899a64c3a0c40bed8da..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/helpers/helpers.js
+++ /dev/null
@@ -1,123 +0,0 @@
-const logger = require('@pubsweet/logger')
-const uuid = require('uuid')
-const crypto = require('crypto')
-
-const checkForUndefinedParams = (...params) => {
-  if (params.includes(undefined)) {
-    return false
-  }
-
-  return true
-}
-
-const validateEmailAndToken = async (email, token, userModel) => {
-  try {
-    const user = await userModel.findByEmail(email)
-    if (user) {
-      if (token !== user.passwordResetToken) {
-        logger.error(
-          `invite pw reset tokens do not match: REQ ${token} vs. DB ${
-            user.passwordResetToken
-          }`,
-        )
-        return {
-          success: false,
-          status: 400,
-          message: 'invalid request',
-        }
-      }
-      return { success: true, user }
-    }
-  } catch (e) {
-    if (e.name === 'NotFoundError') {
-      logger.error('invite pw reset on non-existing user')
-      return {
-        success: false,
-        status: 404,
-        message: 'user not found',
-      }
-    } else if (e.name === 'ValidationError') {
-      logger.error('invite pw reset validation error')
-      return {
-        success: false,
-        status: 400,
-        message: e.details[0].message,
-      }
-    }
-    logger.error('internal server error')
-    return {
-      success: false,
-      status: 500,
-      message: e.details[0].message,
-    }
-  }
-  return {
-    success: false,
-    status: 500,
-    message: 'something went wrong',
-  }
-}
-
-const handleNotFoundError = async (error, item) => {
-  const response = {
-    success: false,
-    status: 500,
-    message: 'Something went wrong',
-  }
-  if (error.name === 'NotFoundError') {
-    logger.error(`invalid ${item} id`)
-    response.status = 404
-    response.message = `${item} not found`
-    return response
-  }
-
-  logger.error(error)
-  return response
-}
-
-const createNewUser = async (
-  email,
-  firstName,
-  lastName,
-  affiliation,
-  title,
-  UserModel,
-  role,
-) => {
-  const username = email
-  const password = uuid.v4()
-  const userBody = {
-    username,
-    email,
-    password,
-    passwordResetToken: crypto.randomBytes(32).toString('hex'),
-    isConfirmed: false,
-    firstName,
-    lastName,
-    affiliation,
-    title,
-    editorInChief: role === 'editorInChief',
-    admin: role === 'admin',
-    handlingEditor: role === 'handlingEditor',
-    invitationToken: role === 'reviewer' ? uuid.v4() : '',
-  }
-
-  let newUser = new UserModel(userBody)
-
-  try {
-    newUser = await newUser.save()
-    return newUser
-  } catch (e) {
-    logger.error(e)
-  }
-}
-
-const getBaseUrl = req => `${req.protocol}://${req.get('host')}`
-
-module.exports = {
-  checkForUndefinedParams,
-  validateEmailAndToken,
-  handleNotFoundError,
-  createNewUser,
-  getBaseUrl,
-}
diff --git a/packages/component-invite/src/routes/collectionsInvitations/decline.js b/packages/component-invite/src/routes/collectionsInvitations/decline.js
deleted file mode 100644
index fc88ffe4f4b69da2bf0481db895e247b90864b49..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/routes/collectionsInvitations/decline.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const helpers = require('../../helpers/helpers')
-const mailService = require('pubsweet-component-mail-service')
-const userHelper = require('../../helpers/User')
-
-module.exports = models => async (req, res) => {
-  const { collectionId, invitationId } = req.params
-  const { invitationToken } = req.body
-
-  if (!helpers.checkForUndefinedParams(invitationToken))
-    return res.status(400).json({ error: 'Token is required' })
-
-  try {
-    const user = await models.User.findOneByField(
-      'invitationToken',
-      invitationToken,
-    )
-    const collection = await models.Collection.find(collectionId)
-    const invitation = await collection.invitations.find(
-      invitation => invitation.id === invitationId,
-    )
-    if (invitation === undefined)
-      return res.status(404).json({
-        error: `Invitation ${invitationId} not found`,
-      })
-    if (invitation.hasAnswer)
-      return res
-        .status(400)
-        .json({ error: `Invitation has already been answered.` })
-    if (invitation.userId !== user.id)
-      return res.status(403).json({
-        error: `User ${user.email} is not allowed to modify invitation ${
-          invitation.id
-        }`,
-      })
-
-    invitation.respondedOn = Date.now()
-    invitation.hasAnswer = true
-    invitation.isAccepted = false
-    await collection.save()
-    return await userHelper.setupReviewerDecisionEmailData({
-      baseUrl: helpers.getBaseUrl(req),
-      UserModel: models.User,
-      FragmentModel: models.Fragment,
-      collection,
-      reviewerName: `${user.firstName} ${user.lastName}`,
-      mailService,
-      agree: false,
-      user,
-    })
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'item')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js
index d1640d5d78afd6aab05dbd8aa74c13f3c150bbeb..f82dffef10760d85c495c1a895cb995d0dc13a00 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/delete.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js
@@ -1,17 +1,18 @@
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
 const mailService = require('pubsweet-component-mail-service')
-const logger = require('@pubsweet/logger')
-const config = require('config')
-const userHelper = require('../../helpers/User')
-const collectionHelper = require('../../helpers/Collection')
-const authsomeHelper = require('../../helpers/authsome')
 
-const statuses = config.get('statuses')
+const {
+  services,
+  Team,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
+
 module.exports = models => async (req, res) => {
   const { collectionId, invitationId } = req.params
+  const teamHelper = new Team({ TeamModel: models.Team, collectionId })
+
   try {
     const collection = await models.Collection.find(collectionId)
+
     const authsome = authsomeHelper.getAuthsome(models)
     const target = {
       collection,
@@ -22,59 +23,47 @@ module.exports = models => async (req, res) => {
       return res.status(403).json({
         error: 'Unauthorized.',
       })
-    const invitation = await collection.invitations.find(
+
+    collection.invitations = collection.invitations || []
+    const invitation = collection.invitations.find(
       invitation => invitation.id === invitationId,
     )
-    if (invitation === undefined) {
-      res.status(404).json({
+    if (!invitation)
+      return res.status(404).json({
         error: `Invitation ${invitationId} not found`,
       })
-      return
-    }
-    const team = await teamHelper.getTeamByGroupAndCollection(
-      collectionId,
-      invitation.role,
-      models.Team,
-    )
+
+    const team = await teamHelper.getTeam({
+      role: invitation.role,
+      objectType: 'collection',
+    })
 
     collection.invitations = collection.invitations.filter(
       inv => inv.id !== invitation.id,
     )
-    if (invitation.role === 'handlingEditor') {
-      collection.status = 'submitted'
-      collection.visibleStatus = statuses[collection.status].private
-      delete collection.handlingEditor
-    } else if (invitation.role === 'reviewer') {
-      await collectionHelper.updateReviewerCollectionStatus(collection)
-    }
+
+    collection.status = 'submitted'
+    delete collection.handlingEditor
     await collection.save()
-    await teamHelper.removeTeamMember(team.id, invitation.userId, models.Team)
-    const user = await models.User.find(invitation.userId)
+
+    await teamHelper.removeTeamMember({
+      teamId: team.id,
+      userId: invitation.userId,
+    })
+
+    const UserModel = models.User
+    const user = await UserModel.find(invitation.userId)
     user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
     await user.save()
-    try {
-      if (invitation.role === 'handlingEditor') {
-        await mailService.sendSimpleEmail({
-          toEmail: user.email,
-          emailType: 'revoke-handling-editor',
-        })
-      } else if (invitation.role === 'reviewer') {
-        await userHelper.setupReviewerUnassignEmail({
-          UserModel: models.User,
-          FragmentModel: models.Fragment,
-          collection,
-          user,
-          mailService,
-        })
-      }
 
-      return res.status(200).json({})
-    } catch (e) {
-      logger.error(e.message)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
+    mailService.sendSimpleEmail({
+      toEmail: user.email,
+      emailType: 'revoke-handling-editor',
+    })
+
+    return res.status(200).json({})
   } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'Collection')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js
index 657ba2794843e4e5df353e2b52f26de5b85161f1..d28609053609980f7256195dda6e4c98bdc9f78e 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/patch.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js
@@ -1,127 +1,92 @@
-const logger = require('@pubsweet/logger')
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
 const mailService = require('pubsweet-component-mail-service')
-const userHelper = require('../../helpers/User')
-const collectionHelper = require('../../helpers/Collection')
+
+const {
+  Team,
+  User,
+  services,
+  Collection,
+  Invitation,
+} = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { collectionId, invitationId } = req.params
   const { isAccepted, reason } = req.body
 
-  if (!helpers.checkForUndefinedParams(isAccepted)) {
-    res.status(400).json({ error: 'Missing parameters.' })
-    logger.error('some parameters are missing')
-    return
-  }
-  let user = await models.User.find(req.user)
+  const UserModel = models.User
+  const user = await UserModel.find(req.user)
+
   try {
     const collection = await models.Collection.find(collectionId)
-    const invitation = await collection.invitations.find(
+    collection.invitations = collection.invitations || []
+    const invitation = collection.invitations.find(
       invitation => invitation.id === invitationId,
     )
-    if (invitation === undefined)
-      return res.status(404).json({
-        error: 'Invitation not found.',
-      })
-    if (invitation.hasAnswer)
-      return res
-        .status(400)
-        .json({ error: 'Invitation has already been answered.' })
-    if (invitation.userId !== user.id)
-      return res.status(403).json({
-        error: `User is not allowed to modify this invitation.`,
+
+    const invitationHelper = new Invitation({
+      userId: user.id,
+      role: 'handlingEditor',
+    })
+
+    const invitationValidation = invitationHelper.validateInvitation({
+      invitation,
+    })
+    if (invitationValidation.error)
+      return res.status(invitationValidation.status).json({
+        error: invitationValidation.error,
       })
 
-    const params = {
-      baseUrl: helpers.getBaseUrl(req),
-      UserModel: models.User,
-      FragmentModel: models.Fragment,
-      collection,
-      reviewerName: `${user.firstName} ${user.lastName}`,
-      mailService,
-    }
-    if (invitation.role === 'handlingEditor')
-      await collectionHelper.updateHandlingEditor(collection, isAccepted)
+    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(models.User)
+    const eic = await userHelper.getEditorInChief()
     const toEmail = eic.email
-    if (isAccepted === true) {
-      invitation.isAccepted = true
-      if (
-        invitation.role === 'reviewer' &&
-        collection.status === 'reviewersInvited'
-      )
-        await collectionHelper.updateStatus(collection, 'underReview')
 
-      await collection.save()
-      try {
-        if (invitation.role === 'handlingEditor')
-          await mailService.sendSimpleEmail({
-            toEmail,
-            user,
-            emailType: 'handling-editor-agreed',
-            dashboardUrl: `${req.protocol}://${req.get('host')}`,
-            meta: {
-              collectionId: collection.customId,
-            },
-          })
-        if (invitation.role === 'reviewer')
-          await userHelper.setupReviewerDecisionEmailData({
-            ...params,
-            agree: true,
-            timestamp: invitation.respondedOn,
-            user,
-          })
-        return res.status(200).json(invitation)
-      } catch (e) {
-        logger.error(e)
-        return res.status(500).json({ error: 'Email could not be sent.' })
-      }
-    } else {
-      invitation.isAccepted = false
-
-      if (invitation.role === 'handlingEditor')
-        await teamHelper.updateHETeam(
-          collection,
-          invitation.role,
-          models.Team,
-          user,
-        )
-      if (reason !== undefined) {
-        invitation.reason = reason
-      }
+    if (isAccepted) {
+      invitation.isAccepted = true
       await collection.save()
 
-      try {
-        if (invitation.role === 'handlingEditor') {
-          await mailService.sendSimpleEmail({
-            toEmail,
-            user,
-            emailType: 'handling-editor-declined',
-            meta: {
-              reason,
-              collectionId: collection.customId,
-            },
-          })
-        } else if (invitation.role === 'reviewer') {
-          await collectionHelper.updateReviewerCollectionStatus(collection)
-          await userHelper.setupReviewerDecisionEmailData({
-            ...params,
-            agree: false,
-            user,
-          })
-        }
-      } catch (e) {
-        logger.error(e)
-        return res.status(500).json({ error: 'Email could not be sent.' })
-      }
+      mailService.sendSimpleEmail({
+        toEmail,
+        user,
+        emailType: 'handling-editor-agreed',
+        dashboardUrl: baseUrl,
+        meta: {
+          collectionId: collection.customId,
+        },
+      })
+
+      return res.status(200).json(invitation)
     }
-    user = await user.save()
+
+    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,
+      },
+    })
+
     return res.status(200).json(invitation)
   } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'collection')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js
index 8f21d2c11d3ce636a6998febd9d71a6da31e5d40..3e20a3c89450bce8077a350249b150ac975dcab4 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/post.js
+++ b/packages/component-invite/src/routes/collectionsInvitations/post.js
@@ -1,43 +1,38 @@
 const logger = require('@pubsweet/logger')
-const get = require('lodash/get')
-const helpers = require('../../helpers/helpers')
-const collectionHelper = require('../../helpers/Collection')
-const teamHelper = require('../../helpers/Team')
-const config = require('config')
 const mailService = require('pubsweet-component-mail-service')
-const userHelper = require('../../helpers/User')
-const invitationHelper = require('../../helpers/Invitation')
-const authsomeHelper = require('../../helpers/authsome')
-
-const configRoles = config.get('roles')
+const {
+  services,
+  authsome: authsomeHelper,
+  Collection,
+  Team,
+  Invitation,
+} = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { email, role } = req.body
 
-  if (!helpers.checkForUndefinedParams(email, role)) {
-    res.status(400).json({ error: 'Email and role are required' })
-    logger.error('User ID and role are missing')
-    return
-  }
+  if (!services.checkForUndefinedParams(email, role))
+    return res.status(400).json({ error: 'Email and role are required' })
 
-  if (!configRoles.collection.includes(role)) {
-    res.status(400).json({ error: `Role ${role} is invalid` })
-    logger.error(`invitation attempted on invalid role ${role}`)
-    return
-  }
-  const reqUser = await models.User.find(req.user)
+  if (role !== 'handlingEditor')
+    return res.status(400).json({
+      error: `Role ${role} is invalid. Only handlingEditor is allowed.`,
+    })
+
+  const UserModel = models.User
+  const reqUser = await UserModel.find(req.user)
 
   if (email === reqUser.email && !reqUser.admin) {
     logger.error(`${reqUser.email} tried to invite his own email`)
-    return res.status(400).json({ error: 'Cannot invite yourself' })
+    return res.status(400).json({ error: 'Cannot invite yourself.' })
   }
 
-  const collectionId = get(req, 'params.collectionId')
+  const { collectionId } = req.params
   let collection
   try {
     collection = await models.Collection.find(collectionId)
   } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'Collection')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
@@ -53,107 +48,51 @@ module.exports = models => async (req, res) => {
     return res.status(403).json({
       error: 'Unauthorized.',
     })
-  const baseUrl = `${req.protocol}://${req.get('host')}`
-  const params = {
-    baseUrl,
-    FragmentModel: models.Fragment,
-    UserModel: models.User,
-    collection,
-    mailService,
-    resend: false,
-  }
 
-  try {
-    const user = await models.User.findByEmail(email)
+  const collectionHelper = new Collection({ collection })
+  const baseUrl = services.getBaseUrl(req)
 
-    await teamHelper.setupManuscriptTeam(models, user, collectionId, role)
-    let invitation = invitationHelper.getInvitation(
-      collection.invitations,
-      user.id,
-      role,
-    )
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
+  })
+  const invitationHelper = new Invitation({ role })
 
-    let resend = false
-    if (invitation !== undefined) {
+  try {
+    const user = await UserModel.findByEmail(email)
+    await teamHelper.setupTeam({ user, role, objectType: 'collection' })
+    invitationHelper.userId = user.id
+    let invitation = invitationHelper.getInvitation({
+      invitations: collection.invitations,
+    })
+
+    if (invitation) {
       if (invitation.hasAnswer)
         return res
           .status(400)
           .json({ error: 'User has already replied to a previous invitation.' })
       invitation.invitedOn = Date.now()
       await collection.save()
-      resend = true
     } else {
-      invitation = await invitationHelper.setupInvitation(
-        user.id,
-        role,
-        collection,
-      )
+      invitation = await invitationHelper.createInvitation({
+        parentObject: collection,
+      })
     }
 
-    try {
-      if (role === 'reviewer') {
-        if (collection.status === 'heAssigned')
-          await collectionHelper.updateStatus(collection, 'reviewersInvited')
+    invitation.invitedOn = Date.now()
+    await collection.save()
+    await collectionHelper.addHandlingEditor({ user, invitation })
 
-        await invitationHelper.setupReviewerInvitation({
-          ...params,
-          user,
-          invitationId: invitation.id,
-          timestamp: invitation.invitedOn,
-          resend,
-        })
-      }
+    mailService.sendSimpleEmail({
+      toEmail: user.email,
+      user,
+      emailType: 'assign-handling-editor',
+      dashboardUrl: baseUrl,
+    })
 
-      if (role === 'handlingEditor') {
-        invitation.invitedOn = Date.now()
-        await collection.save()
-        await collectionHelper.addHandlingEditor(collection, user, invitation)
-        await mailService.sendSimpleEmail({
-          toEmail: user.email,
-          user,
-          emailType: 'assign-handling-editor',
-          dashboardUrl: baseUrl,
-        })
-      }
-      return res.status(200).json(invitation)
-    } catch (e) {
-      logger.error(e)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
+    return res.status(200).json(invitation)
   } catch (e) {
-    if (role === 'reviewer') {
-      const newUser = await userHelper.setupNewUser(
-        req.body,
-        baseUrl,
-        res,
-        email,
-        role,
-        models.User,
-        'invite',
-      )
-      if (newUser.error !== undefined) {
-        return res.status(newUser.status).json({
-          error: newUser.message,
-        })
-      }
-      if (collection.status === 'heAssigned')
-        await collectionHelper.updateStatus(collection, 'reviewersInvited')
-      await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role)
-      const invitation = await invitationHelper.setupInvitation(
-        newUser.id,
-        role,
-        collection,
-      )
-
-      await invitationHelper.setupReviewerInvitation({
-        ...params,
-        user: newUser,
-        invitationId: invitation.id,
-        timestamp: invitation.invitedOn,
-      })
-      return res.status(200).json(invitation)
-    }
-    const notFoundError = await helpers.handleNotFoundError(e, 'user')
+    const notFoundError = await services.handleNotFoundError(e, 'User')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/decline.js b/packages/component-invite/src/routes/fragmentsInvitations/decline.js
new file mode 100644
index 0000000000000000000000000000000000000000..e867503771902281e8686ff0330e2508ca693dfa
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/decline.js
@@ -0,0 +1,78 @@
+const {
+  services,
+  Email,
+  Fragment,
+  Invitation,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, invitationId, fragmentId } = req.params
+  const { invitationToken } = req.body
+
+  if (!services.checkForUndefinedParams(invitationToken))
+    return res.status(400).json({ error: 'Token is required' })
+
+  const UserModel = models.User
+  try {
+    const user = await UserModel.findOneByField(
+      'invitationToken',
+      invitationToken,
+    )
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      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,
+    )
+
+    const invitationHelper = new Invitation({
+      userId: user.id,
+      role: 'reviewer',
+    })
+
+    const invitationValidation = invitationHelper.validateInvitation({
+      invitation,
+    })
+    if (invitationValidation.error)
+      return res.status(invitationValidation.status).json({
+        error: invitationValidation.error,
+      })
+
+    invitation.respondedOn = Date.now()
+    invitation.hasAnswer = true
+    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,
+      baseUrl,
+      authors,
+    })
+    emailHelper.setupReviewerDecisionEmail({
+      authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
+      agree: false,
+      user,
+    })
+    return res.status(200).json({})
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/delete.js b/packages/component-invite/src/routes/fragmentsInvitations/delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..af74f13d32a60e8cac0c18837b23c57b384d2570
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/delete.js
@@ -0,0 +1,99 @@
+const {
+  services,
+  Team,
+  Email,
+  Fragment,
+  Collection,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, invitationId, fragmentId } = req.params
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
+    fragmentId,
+  })
+
+  try {
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}.`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+
+    const collectionHelper = new Collection({ collection })
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      collection,
+      path: req.route.path,
+    }
+    const canDelete = await authsome.can(req.user, 'DELETE', target)
+    if (!canDelete)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+    fragment.invitations = fragment.invitations || []
+    const invitation = await fragment.invitations.find(
+      invitation => invitation.id === invitationId,
+    )
+    if (!invitation)
+      return res.status(404).json({
+        error: `Invitation ${invitationId} not found`,
+      })
+
+    const team = await teamHelper.getTeam({
+      role: invitation.role,
+      objectType: 'fragment',
+    })
+
+    fragment.invitations = fragment.invitations.filter(
+      inv => inv.id !== invitation.id,
+    )
+
+    await collectionHelper.updateStatusByNumberOfReviewers({
+      invitations: fragment.invitations,
+    })
+
+    await teamHelper.removeTeamMember({
+      teamId: team.id,
+      userId: invitation.userId,
+    })
+
+    const UserModel = models.User
+    const user = await UserModel.find(invitation.userId)
+    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}`,
+    })
+
+    return res.status(200).json({})
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'collection')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/fragmentsInvitations/get.js
similarity index 58%
rename from packages/component-invite/src/routes/collectionsInvitations/get.js
rename to packages/component-invite/src/routes/fragmentsInvitations/get.js
index 308a57670a9030a82833f445fef7855083077edb..a7e80aa98f6fde7df771d8ec58ea4c4248384211 100644
--- a/packages/component-invite/src/routes/collectionsInvitations/get.js
+++ b/packages/component-invite/src/routes/fragmentsInvitations/get.js
@@ -1,13 +1,16 @@
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
 const config = require('config')
-const invitationHelper = require('../../helpers/Invitation')
-const authsomeHelper = require('../../helpers/authsome')
+const {
+  services,
+  Team,
+  Invitation,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
 
 const configRoles = config.get('roles')
+
 module.exports = models => async (req, res) => {
   const { role } = req.query
-  if (!helpers.checkForUndefinedParams(role)) {
+  if (!services.checkForUndefinedParams(role)) {
     res.status(400).json({ error: 'Role is required' })
     return
   }
@@ -16,40 +19,57 @@ module.exports = models => async (req, res) => {
     res.status(400).json({ error: `Role ${role} is invalid` })
     return
   }
-  const { collectionId } = req.params
+
+  const { collectionId, fragmentId } = req.params
+  const teamHelper = new Team({
+    TeamModel: models.Team,
+    collectionId,
+    fragmentId,
+  })
+
   try {
     const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}.`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+
     const authsome = authsomeHelper.getAuthsome(models)
     const target = {
-      collection,
+      fragment,
       path: req.route.path,
     }
+
     const canGet = await authsome.can(req.user, 'GET', target)
+
     if (!canGet)
       return res.status(403).json({
         error: 'Unauthorized.',
       })
-    const members = await teamHelper.getTeamMembersByCollection(
-      collectionId,
+
+    const members = await teamHelper.getTeamMembers({
       role,
-      models.Team,
-    )
+      objectType: 'fragment',
+    })
 
-    if (members === undefined) return res.status(200).json([])
+    if (!members) return res.status(200).json([])
 
     // TO DO: handle case for when the invitationID is provided
+    const invitationHelper = new Invitation({ role })
+
     const membersData = members.map(async member => {
       const user = await models.User.find(member)
+      invitationHelper.userId = user.id
       const {
         invitedOn,
         respondedOn,
         status,
         id,
-      } = invitationHelper.getInvitationData(
-        collection.invitations,
-        user.id,
-        role,
-      )
+      } = invitationHelper.getInvitationsData({
+        invitations: fragment.invitations,
+      })
+
       return {
         name: `${user.firstName} ${user.lastName}`,
         invitedOn,
@@ -64,7 +84,7 @@ module.exports = models => async (req, res) => {
     const resBody = await Promise.all(membersData)
     res.status(200).json(resBody)
   } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/patch.js b/packages/component-invite/src/routes/fragmentsInvitations/patch.js
new file mode 100644
index 0000000000000000000000000000000000000000..6cb2ca49d9cd4aa8dcbac67b254963ad2edb3ee9
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/patch.js
@@ -0,0 +1,97 @@
+const {
+  Email,
+  services,
+  Fragment,
+  Collection,
+  Invitation,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, invitationId, fragmentId } = req.params
+  const { isAccepted, reason } = req.body
+
+  const UserModel = models.User
+  const user = await UserModel.find(req.user)
+  try {
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      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,
+    )
+
+    const invitationHelper = new Invitation({
+      userId: user.id,
+      role: 'reviewer',
+    })
+    const invitationValidation = invitationHelper.validateInvitation({
+      invitation,
+    })
+    if (invitationValidation.error)
+      return res.status(invitationValidation.status).json({
+        error: invitationValidation.error,
+      })
+
+    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
+    if (isAccepted) {
+      invitation.isAccepted = true
+      if (collection.status === 'reviewersInvited')
+        await collectionHelper.updateStatus({ newStatus: 'underReview' })
+      await fragment.save()
+
+      emailHelper.setupReviewerDecisionEmail({
+        agree: true,
+        timestamp: invitation.respondedOn,
+        user,
+        authorName: `${submittingAuthor.firstName} ${
+          submittingAuthor.lastName
+        }`,
+      })
+
+      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,
+    })
+
+    return res.status(200).json(invitation)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js
new file mode 100644
index 0000000000000000000000000000000000000000..38991ec08d033de565d5685d7ab6077b978d8504
--- /dev/null
+++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js
@@ -0,0 +1,159 @@
+const logger = require('@pubsweet/logger')
+const {
+  Email,
+  services,
+  authsome: authsomeHelper,
+  Fragment,
+  Collection,
+  Team,
+  Invitation,
+  User,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { email, role } = req.body
+
+  if (!services.checkForUndefinedParams(email, role)) {
+    res.status(400).json({ error: 'Email and role are required.' })
+    logger.error('User ID and role are missing')
+    return
+  }
+
+  if (role !== 'reviewer')
+    return res
+      .status(400)
+      .json({ error: `Role ${role} is invalid. Only reviewer is accepted.` })
+
+  const UserModel = models.User
+  const reqUser = await UserModel.find(req.user)
+
+  if (email === reqUser.email && !reqUser.admin)
+    return res.status(400).json({ error: 'Cannot invite yourself.' })
+
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}.`,
+      })
+    fragment = await models.Fragment.find(fragmentId)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+
+  const authsome = authsomeHelper.getAuthsome(models)
+  const target = {
+    collection,
+    path: req.route.path,
+  }
+  const canPost = await authsome.can(req.user, 'POST', target)
+  if (!canPost)
+    return res.status(403).json({
+      error: 'Unauthorized.',
+    })
+
+  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,
+    fragmentId,
+  })
+  const invitationHelper = new Invitation({ role })
+
+  try {
+    const user = await UserModel.findByEmail(email)
+    await teamHelper.setupTeam({ user, role, objectType: 'fragment' })
+    invitationHelper.userId = user.id
+
+    let invitation = invitationHelper.getInvitation({
+      invitations: fragment.invitations,
+    })
+    let resend = false
+
+    if (invitation) {
+      if (invitation.hasAnswer)
+        return res
+          .status(400)
+          .json({ error: 'User has already replied to a previous invitation.' })
+
+      invitation.invitedOn = Date.now()
+      await fragment.save()
+      resend = true
+    } else {
+      invitation = await invitationHelper.createInvitation({
+        parentObject: fragment,
+      })
+    }
+
+    if (collection.status === 'heAssigned')
+      await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
+
+    emailHelper.setupReviewerInvitationEmail({
+      user,
+      invitationId: invitation.id,
+      timestamp: invitation.invitedOn,
+      resend,
+      authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
+    })
+
+    return res.status(200).json(invitation)
+  } catch (e) {
+    const userHelper = new User({ UserModel })
+
+    const newUser = await userHelper.setupNewUser({
+      url: baseUrl,
+      role,
+      invitationType: 'invite',
+      body: req.body,
+    })
+
+    if (newUser.error !== undefined) {
+      return res.status(newUser.status).json({
+        error: newUser.message,
+      })
+    }
+
+    if (collection.status === 'heAssigned')
+      await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
+
+    await teamHelper.setupTeam({ user: newUser, role, objectType: 'fragment' })
+
+    invitationHelper.userId = newUser.id
+
+    const invitation = await invitationHelper.createInvitation({
+      parentObject: fragment,
+    })
+
+    emailHelper.setupReviewerInvitationEmail({
+      user: newUser,
+      invitationId: invitation.id,
+      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 4e6594490c6661e1db150e367b42f348997436bb..7fdef707ccadb721ce6099a69001da73ec29c552 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js
@@ -2,15 +2,15 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const cloneDeep = require('lodash/cloneDeep')
-const Model = require('./../helpers/Model')
-const fixtures = require('./../fixtures/fixtures')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
 }))
 
-const path = '../../routes/collectionsInvitations/delete'
+const path = '../routes/collectionsInvitations/delete'
 const route = {
   path: '/api/collections/:collectionId/invitations/:invitationId',
 }
@@ -35,7 +35,7 @@ describe('Delete Collections Invitations route handler', () => {
     })
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('collection not found')
+    expect(data.error).toEqual('Collection not found')
   })
   it('should return an error when the invitation does not exist', async () => {
     const { editorInChief } = testFixtures.users
diff --git a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js
index 129b791c24ae9fdaf09f3f4bcc8573e1abff2985..7ed11f1546f5b45726c79c3ce683c0eeee0f776e 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js
@@ -2,10 +2,10 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
 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(),
@@ -41,22 +41,6 @@ describe('Patch collections invitations route handler', () => {
     await require(patchPath)(models)(req, res)
     expect(res.statusCode).toBe(200)
   })
-  it('should return success when the reviewer agrees work on a collection', async () => {
-    const { reviewer } = testFixtures.users
-    const { collection } = testFixtures.collections
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = reviewer.id
-    req.params.collectionId = collection.id
-    const reviewerInv = collection.invitations.find(
-      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
-    )
-    req.params.invitationId = reviewerInv.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-    expect(res.statusCode).toBe(200)
-  })
   it('should return success when the handling editor declines work on a collection', async () => {
     const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
@@ -75,41 +59,6 @@ describe('Patch collections invitations route handler', () => {
 
     expect(res.statusCode).toBe(200)
   })
-  it('should return success when the reviewer declines work on a collection', async () => {
-    const { reviewer } = testFixtures.users
-    const { collection } = testFixtures.collections
-    body.isAccepted = false
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = reviewer.id
-    req.params.collectionId = collection.id
-    const inv = collection.invitations.find(
-      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
-    )
-    req.params.invitationId = inv.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(200)
-  })
-  it('should return an error params are missing', async () => {
-    const { handlingEditor } = testFixtures.users
-    const { collection } = testFixtures.collections
-    delete body.isAccepted
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = handlingEditor.id
-    req.params.collectionId = collection.id
-    req.params.invitationId = collection.invitations[0].id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Missing parameters.')
-  })
   it('should return an error if the collection does not exists', async () => {
     const { handlingEditor } = testFixtures.users
     const req = httpMocks.createRequest({
diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js
index 53eda4a07751aebb1120d5c00eadec0a60a87ae3..0608aced450fbf604c14fb2af509309262808ddc 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js
+++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js
@@ -1,15 +1,12 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const random = require('lodash/random')
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
-const config = require('config')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
-const configRoles = config.get('roles')
+const { Model, fixtures } = fixturesService
 
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
@@ -17,10 +14,9 @@ jest.mock('pubsweet-component-mail-service', () => ({
   sendReviewerInvitationEmail: jest.fn(),
 }))
 const chance = new Chance()
-const roles = configRoles.collection
 const reqBody = {
   email: chance.email(),
-  role: roles[random(0, roles.length - 1)],
+  role: 'handlingEditor',
   firstName: chance.first(),
   lastName: chance.last(),
   title: 'Mr',
@@ -31,7 +27,7 @@ const route = {
   path: '/api/collections/:collectionId/invitations',
 }
 
-const path = '../../routes/collectionsInvitations/post'
+const path = '../routes/collectionsInvitations/post'
 describe('Post collections invitations route handler', () => {
   let testFixtures = {}
   let body = {}
@@ -79,31 +75,8 @@ describe('Post collections invitations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.role).toEqual(body.role)
   })
-  it('should return success when the a reviewer is invited', async () => {
-    const { user, editorInChief } = testFixtures.users
-    const { collection } = testFixtures.collections
-    body = {
-      email: user.email,
-      role: 'reviewer',
-    }
-    const res = await requests.sendRequest({
-      body,
-      userId: editorInChief.id,
-      route,
-      models,
-      path,
-      params: {
-        collectionId: collection.id,
-      },
-    })
-
-    expect(res.statusCode).toBe(200)
-    const data = JSON.parse(res._getData())
-    expect(data.role).toEqual(body.role)
-  })
   it('should return an error when inviting his self', async () => {
     const { editorInChief } = testFixtures.users
-    body.role = roles[random(0, roles.length - 1)]
     body.email = editorInChief.email
     const res = await requests.sendRequest({
       body,
@@ -114,7 +87,7 @@ describe('Post collections invitations route handler', () => {
     })
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Cannot invite yourself')
+    expect(data.error).toEqual('Cannot invite yourself.')
   })
   it('should return an error when the role is invalid', async () => {
     const { editorInChief } = testFixtures.users
@@ -127,7 +100,9 @@ describe('Post collections invitations route handler', () => {
       path,
     })
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual(`Role ${body.role} is invalid`)
+    expect(data.error).toEqual(
+      `Role ${body.role} is invalid. Only handlingEditor is allowed.`,
+    )
   })
   it('should return success when the EiC resends an invitation to a handlingEditor with a collection', async () => {
     const { handlingEditor, editorInChief } = testFixtures.users
@@ -152,15 +127,15 @@ describe('Post collections invitations route handler', () => {
     expect(data.role).toEqual(body.role)
   })
   it('should return an error when the invitation is already answered', async () => {
-    const { answerReviewer, handlingEditor } = testFixtures.users
+    const { answerHE, editorInChief } = testFixtures.users
     const { collection } = testFixtures.collections
     body = {
-      email: answerReviewer.email,
-      role: 'reviewer',
+      email: answerHE.email,
+      role: 'handlingEditor',
     }
     const res = await requests.sendRequest({
       body,
-      userId: handlingEditor.id,
+      userId: editorInChief.id,
       route,
       models,
       path,
diff --git a/packages/component-invite/src/tests/fixtures/fragments.js b/packages/component-invite/src/tests/fixtures/fragments.js
deleted file mode 100644
index 32379359150362143172390427a5113121ad9233..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/fragments.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const fragments = {
-  fragment: {
-    id: chance.guid(),
-    metadata: {
-      title: chance.sentence(),
-      abstract: chance.paragraph(),
-    },
-  },
-}
-
-module.exports = fragments
diff --git a/packages/component-invite/src/tests/fixtures/teamIDs.js b/packages/component-invite/src/tests/fixtures/teamIDs.js
deleted file mode 100644
index 607fd6661b1e7c9848bf13257a0d198f230fab00..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/teamIDs.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const heID = chance.guid()
-const revId = chance.guid()
-
-module.exports = {
-  heTeamID: heID,
-  revTeamID: revId,
-}
diff --git a/packages/component-invite/src/tests/fixtures/userData.js b/packages/component-invite/src/tests/fixtures/userData.js
deleted file mode 100644
index 546e2d867998c6de1cab8e10a4df20fb74d5288d..0000000000000000000000000000000000000000
--- a/packages/component-invite/src/tests/fixtures/userData.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const generateUserData = () => ({
-  id: chance.guid(),
-  email: chance.email(),
-  firstName: chance.first(),
-  lastName: chance.last(),
-})
-
-module.exports = {
-  handlingEditor: generateUserData(),
-  user: generateUserData(),
-  admin: generateUserData(),
-  author: generateUserData(),
-  reviewer: generateUserData(),
-  answerReviewer: generateUserData(),
-}
diff --git a/packages/component-invite/src/tests/collectionsInvitations/decline.test.js b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js
similarity index 73%
rename from packages/component-invite/src/tests/collectionsInvitations/decline.test.js
rename to packages/component-invite/src/tests/fragmentsInvitations/decline.test.js
index 562c3d92981611c95a57664e281fe482a0b04fc3..ece17117d96e22a7c4c7cf2288d537c18b4bf788 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/decline.test.js
+++ b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js
@@ -2,8 +2,9 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
 const cloneDeep = require('lodash/cloneDeep')
 
 jest.mock('pubsweet-component-mail-service', () => ({
@@ -13,8 +14,8 @@ jest.mock('pubsweet-component-mail-service', () => ({
 const reqBody = {
   invitationToken: 'inv-token-123',
 }
-const patchPath = '../../routes/collectionsInvitations/decline'
-describe('Patch collections invitations route handler', () => {
+const patchPath = '../../routes/fragmentsInvitations/decline'
+describe('Decline fragments invitations route handler', () => {
   let testFixtures = {}
   let body = {}
   let models
@@ -26,12 +27,16 @@ describe('Patch collections invitations route handler', () => {
   it('should return success when the reviewer declines work on a collection', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    const inv = collection.invitations.find(
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(
       inv => inv.role === 'reviewer' && inv.hasAnswer === false,
     )
     req.params.invitationId = inv.id
@@ -42,13 +47,14 @@ describe('Patch collections invitations route handler', () => {
   it('should return an error params are missing', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+
     delete body.invitationToken
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    req.params.invitationId = collection.invitations[0].id
+
     const res = httpMocks.createResponse()
     await require(patchPath)(models)(req, res)
 
@@ -70,32 +76,59 @@ describe('Patch collections invitations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('item not found')
   })
+  it('should return an error if the fragment does not exists', async () => {
+    const { reviewer } = testFixtures.users
+    const { collection } = testFixtures.collections
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = reviewer.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-id'
+
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-id does not match collection ${collection.id}`,
+    )
+  })
   it('should return an error when the invitation does not exist', async () => {
     const { user } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
     const req = httpMocks.createRequest({
       body,
     })
     req.user = user.id
     req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+
     req.params.invitationId = 'invalid-id'
     const res = httpMocks.createResponse()
     await require(patchPath)(models)(req, res)
 
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Invitation invalid-id not found')
+    expect(data.error).toEqual('Invitation not found.')
   })
   it('should return an error when the token is invalid', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     body.invitationToken = 'invalid-token'
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    const inv = collection.invitations.find(
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(
       inv => inv.role === 'reviewer' && inv.hasAnswer === false,
     )
     req.params.invitationId = inv.id
@@ -108,12 +141,16 @@ describe('Patch collections invitations route handler', () => {
   it('should return an error when the invitation is already answered', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const req = httpMocks.createRequest({
       body,
     })
     req.user = reviewer.id
     req.params.collectionId = collection.id
-    const inv = collection.invitations.find(inv => inv.hasAnswer)
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(inv => inv.hasAnswer)
     req.params.invitationId = inv.id
     const res = httpMocks.createResponse()
     await require(patchPath)(models)(req, res)
diff --git a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..8396173f8262ce7698b2a1ddbed0c17b6fcb7b60
--- /dev/null
+++ b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js
@@ -0,0 +1,117 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
+
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+}))
+
+const path = '../routes/fragmentsInvitations/delete'
+const route = {
+  path:
+    '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId',
+}
+
+describe('Delete Fragments Invitations route handler', () => {
+  let testFixtures = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    models = Model.build(testFixtures)
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: 'invalid-id',
+      },
+    })
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('collection not found')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: 'invalid-id',
+      },
+    })
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-id does not match collection ${collection.id}.`,
+    )
+  })
+  it('should return an error when the invitation does not exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+        invitationId: 'invalid-id',
+      },
+    })
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Invitation invalid-id not found')
+  })
+  it('should return success when the collection and invitation exist', async () => {
+    const { editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+        invitationId: fragment.invitations[0].id,
+      },
+    })
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the user does not have invitation rights', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      userId: user.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+        invitationId: collection.invitations[0].id,
+      },
+    })
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Unauthorized.')
+  })
+})
diff --git a/packages/component-invite/src/tests/collectionsInvitations/get.test.js b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js
similarity index 61%
rename from packages/component-invite/src/tests/collectionsInvitations/get.test.js
rename to packages/component-invite/src/tests/fragmentsInvitations/get.test.js
index 105187c0a834d5c5614b9346318ccdbb715ac8c0..043e162fac230e383f769e197ec47ca0ba8debf9 100644
--- a/packages/component-invite/src/tests/collectionsInvitations/get.test.js
+++ b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js
@@ -1,16 +1,22 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
-const path = '../../routes/collectionsInvitations/get'
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+  sendReviewerInvitationEmail: jest.fn(),
+}))
+const path = '../routes/fragmentsInvitations/get'
 const route = {
-  path: '/api/collections/:collectionId/invitations/:invitationId?',
+  path:
+    '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId?',
 }
-describe('Get collection invitations route handler', () => {
+describe('Get fragment invitations route handler', () => {
   let testFixtures = {}
   let models
   beforeEach(() => {
@@ -18,19 +24,21 @@ describe('Get collection invitations route handler', () => {
     models = Model.build(testFixtures)
   })
   it('should return success when the request data is correct', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+    const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'handlingEditor',
-        userId: handlingEditor.id,
+        role: 'reviewer',
       },
       params: {
         collectionId: collection.id,
+        fragmentId: fragment.id,
       },
     })
 
@@ -39,9 +47,9 @@ describe('Get collection invitations route handler', () => {
     expect(data.length).toBeGreaterThan(0)
   })
   it('should return an error when parameters are missing', async () => {
-    const { editorInChief } = testFixtures.users
+    const { handlingEditor } = testFixtures.users
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
@@ -51,15 +59,14 @@ describe('Get collection invitations route handler', () => {
     expect(data.error).toEqual('Role is required')
   })
   it('should return an error when the collection does not exist', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+    const { handlingEditor } = testFixtures.users
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'handlingEditor',
-        userId: handlingEditor.id,
+        role: 'reviewer',
       },
       params: {
         collectionId: 'invalid-id',
@@ -67,54 +74,59 @@ describe('Get collection invitations route handler', () => {
     })
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('collection not found')
+    expect(data.error).toEqual('Item not found')
   })
-  it('should return an error when the role is invalid', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+  it('should return an error when the fragment does not exist', async () => {
+    const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'invalidRole',
-        userId: handlingEditor.id,
+        role: 'reviewer',
       },
       params: {
         collectionId: collection.id,
+        fragmentId: 'invalid-id',
       },
     })
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
-    expect(data.error).toEqual(`Role invalidRole is invalid`)
+    expect(data.error).toEqual(
+      `Fragment invalid-id does not match collection ${collection.id}.`,
+    )
   })
-  it('should return success with an empty array when the collection does not have a the requested role team', async () => {
-    const { editorInChief, handlingEditor } = testFixtures.users
+  it('should return an error when the role is invalid', async () => {
+    const { handlingEditor } = testFixtures.users
     const { collection } = testFixtures.collections
-    delete collection.invitations
+    const { fragment } = testFixtures.fragments
+
     const res = await requests.sendRequest({
-      userId: editorInChief.id,
+      userId: handlingEditor.id,
       route,
       models,
       path,
       query: {
-        role: 'author',
-        userId: handlingEditor.id,
+        role: 'invalidRole',
       },
       params: {
         collectionId: collection.id,
+        fragmentId: fragment.id,
       },
     })
-    expect(res.statusCode).toBe(200)
+    expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
-    expect(data).toHaveLength(0)
+    expect(data.error).toEqual(`Role invalidRole is invalid`)
   })
   it('should return an error when a user does not have invitation rights', async () => {
-    const { author } = testFixtures.users
+    const { user } = testFixtures.users
     const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
     const res = await requests.sendRequest({
-      userId: author.id,
+      userId: user.id,
       route,
       models,
       path,
@@ -123,6 +135,7 @@ describe('Get collection invitations route handler', () => {
       },
       params: {
         collectionId: collection.id,
+        fragmentId: fragment.id,
       },
     })
     expect(res.statusCode).toBe(403)
diff --git a/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..93006adf157c73b8eff63846e335c3bdb8d46a23
--- /dev/null
+++ b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js
@@ -0,0 +1,160 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const httpMocks = require('node-mocks-http')
+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(),
+}))
+
+const reqBody = {
+  isAccepted: true,
+}
+const patchPath = '../../routes/fragmentsInvitations/patch'
+describe('Patch fragments invitations route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the reviewer agrees work on a collection', async () => {
+    const { reviewer } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = reviewer.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    const reviewerInv = fragment.invitations.find(
+      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+    )
+    req.params.invitationId = reviewerInv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return success when the reviewer declines work on a collection', async () => {
+    const { reviewer } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    body.isAccepted = false
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = reviewer.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(
+      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+    )
+    req.params.invitationId = inv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error if the collection does not exists', async () => {
+    const { handlingEditor } = testFixtures.users
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor.id
+    req.params.collectionId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Item not found')
+  })
+  it('should return an error when the invitation does not exist', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = user.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    req.params.invitationId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Invitation not found.')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = user.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    req.params.invitationId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Invitation not found.')
+  })
+  it("should return an error when a user tries to patch another user's invitation", async () => {
+    const { handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+    const inv = fragment.invitations.find(
+      inv => inv.role === 'reviewer' && inv.hasAnswer === false,
+    )
+    req.params.invitationId = inv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(`User is not allowed to modify this invitation.`)
+  })
+  it('should return an error when the invitation is already answered', async () => {
+    const { handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = handlingEditor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = fragment.id
+
+    const inv = fragment.invitations.find(inv => inv.hasAnswer)
+    req.params.invitationId = inv.id
+    const res = httpMocks.createResponse()
+    await require(patchPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(`Invitation has already been answered.`)
+  })
+})
diff --git a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..21da70379fe9ba9dc1e06df3802f6a331000a6fb
--- /dev/null
+++ b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js
@@ -0,0 +1,155 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const Chance = require('chance')
+const cloneDeep = require('lodash/cloneDeep')
+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(),
+}))
+const chance = new Chance()
+const reqBody = {
+  email: chance.email(),
+  role: 'reviewer',
+  firstName: chance.first(),
+  lastName: chance.last(),
+  title: 'Mr',
+  affiliation: chance.company(),
+  admin: false,
+}
+const route = {
+  path: '/api/collections/:collectionId/fragments/:fragmentId/invitations',
+}
+
+const path = '../routes/fragmentsInvitations/post'
+describe('Post fragments invitations route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return an error params are missing', async () => {
+    const { admin } = testFixtures.users
+    delete body.email
+    const res = await requests.sendRequest({
+      body,
+      userId: admin.id,
+      route,
+      models,
+      path,
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Email and role are required.')
+  })
+  it('should return success when the a reviewer is invited', async () => {
+    const { user, editorInChief } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    body = {
+      email: user.email,
+      role: 'reviewer',
+    }
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+    expect(data.role).toEqual(body.role)
+  })
+  it('should return an error when inviting his self', async () => {
+    const { editorInChief } = testFixtures.users
+    body.email = editorInChief.email
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+    })
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Cannot invite yourself.')
+  })
+  it('should return an error when the role is invalid', async () => {
+    const { editorInChief } = testFixtures.users
+    body.role = 'someRandomRole'
+    const res = await requests.sendRequest({
+      body,
+      userId: editorInChief.id,
+      route,
+      models,
+      path,
+    })
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Role ${body.role} is invalid. Only reviewer is accepted.`,
+    )
+  })
+  it('should return an error when the invitation is already answered', async () => {
+    const { answerReviewer, handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    body = {
+      email: answerReviewer.email,
+      role: 'reviewer',
+    }
+    const res = await requests.sendRequest({
+      body,
+      userId: handlingEditor.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `User has already replied to a previous invitation.`,
+    )
+  })
+  it('should return an error when the user does not have invitation rights', async () => {
+    const { author } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: author.id,
+      route,
+      models,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Unauthorized.')
+  })
+})
diff --git a/packages/component-invite/src/tests/helpers/requests.js b/packages/component-invite/src/tests/requests.js
similarity index 100%
rename from packages/component-invite/src/tests/helpers/requests.js
rename to packages/component-invite/src/tests/requests.js
diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js
index c0886c2c27eb3b21b408b5d88dd36c9aa89204e1..5a57a4e84b7579821c02b5448895d5b13e773130 100644
--- a/packages/component-mail-service/src/Mail.js
+++ b/packages/component-mail-service/src/Mail.js
@@ -2,15 +2,17 @@ const Email = require('@pubsweet/component-send-email')
 const config = require('config')
 const helpers = require('./helpers/helpers')
 
-const resetPasswordPath = config.get('invite-reviewer.url')
+const confirmSignUp = config.get('confirm-signup.url')
 const resetPath = config.get('invite-reset-password.url')
+const resetPasswordPath = config.get('invite-reviewer.url')
+const forgotPath = config.get('forgot-password.url')
 
 module.exports = {
   sendSimpleEmail: async ({
-    toEmail,
-    user,
-    emailType,
-    dashboardUrl,
+    toEmail = '',
+    user = {},
+    emailType = '',
+    dashboardUrl = '',
     meta = {},
   }) => {
     let subject, textBody
@@ -43,12 +45,27 @@ module.exports = {
           replacements.url
         } ${replacements.buttonText}`
         break
+      case 'signup':
+        subject = 'Confirm your email address'
+        replacements.headline = ''
+        replacements.paragraph =
+          'Please confirm your account by clicking on the link below.'
+        replacements.previewText = 'Hindawi account confirmation'
+        replacements.buttonText = 'CONFIRM'
+        replacements.url = helpers.createUrl(dashboardUrl, confirmSignUp, {
+          userId: user.id,
+          confirmationToken: meta.confirmationToken,
+        })
+        textBody = `${replacements.headline} ${replacements.paragraph} ${
+          replacements.url
+        } ${replacements.buttonText}`
+        break
       case 'invite-author':
-        subject = 'Hindawi Invitation'
+        subject = 'Author Invitation'
         replacements.headline =
-          'You have been invited as an Author to a manuscript.'
+          'You have been invited to join Hindawi as an Author.'
         replacements.paragraph =
-          "The manuscript will be visible on your dashboard once it's submitted. Please confirm your account and set your account details by clicking on the link below."
+          'Please confirm your account and set your account details by clicking on the link below.'
         replacements.previewText = 'You have been invited'
         replacements.buttonText = 'CONFIRM'
         replacements.url = helpers.createUrl(dashboardUrl, resetPath, {
@@ -117,6 +134,41 @@ module.exports = {
         textBody = `${replacements.headline}`
         emailTemplate = 'noCTA'
         break
+      case 'manuscript-submitted':
+        subject = `${meta.collection.customId}: Manuscript Submitted`
+        replacements.previewText = 'A new manuscript has been submitted'
+        replacements.headline = `A new manuscript has been submitted.`
+        replacements.paragraph = `You can view the full manuscript titled "${
+          meta.fragment.title
+        }" by ${
+          meta.fragment.authorName
+        } and take further actions by clicking on the following link:`
+        replacements.buttonText = 'MANUSCRIPT DETAILS'
+        replacements.url = helpers.createUrl(
+          dashboardUrl,
+          `/projects/${meta.collection.id}/versions/${
+            meta.fragment.id
+          }/details`,
+        )
+        textBody = `${replacements.headline} ${replacements.paragraph} ${
+          replacements.url
+        } ${replacements.buttonText}`
+        break
+      case 'forgot-password':
+        subject = 'Forgot Password'
+        replacements.headline = 'You have requested a password reset.'
+        replacements.paragraph =
+          'In order to reset your password please click on the following link:'
+        replacements.previewText = 'Click button to reset your password'
+        replacements.buttonText = 'RESET PASSWORD'
+        replacements.url = helpers.createUrl(dashboardUrl, forgotPath, {
+          email: user.email,
+          token: user.passwordResetToken,
+        })
+        textBody = `${replacements.headline} ${replacements.paragraph} ${
+          replacements.url
+        } ${replacements.buttonText}`
+        break
       default:
         subject = 'Welcome to Hindawi!'
         break
@@ -152,6 +204,7 @@ module.exports = {
     const declineUrl = helpers.createUrl(baseUrl, resetPasswordPath, {
       ...queryParams,
       agree: false,
+      fragmentId: meta.fragment.id,
       collectionId: meta.collection.id,
       invitationToken: user.invitationToken,
     })
@@ -305,8 +358,8 @@ module.exports = {
         replacements.intro = `Dear ${meta.handlingEditorName}`
 
         replacements.paragraph = `We are pleased to inform you that Dr. ${
-          user.firstName
-        } ${user.lastName} has submitted a review for the manuscript titled "${
+          meta.reviewerName
+        } has submitted a review for the manuscript titled "${
           meta.fragment.title
         }" by ${meta.fragment.authorName}.`
         replacements.beforeAnchor = 'Please visit the'
@@ -404,16 +457,16 @@ module.exports = {
         subject = meta.emailSubject
         replacements.hasLink = false
         replacements.previewText = 'a manuscript has reached a decision'
-        replacements.intro = `Dear Dr. ${user.firstName} ${user.lastName}`
+        replacements.intro = `Dear Dr. ${meta.reviewerName}`
 
         replacements.paragraph = `An editorial decision has been made regarding the ${
           meta.manuscriptType
         } titled "${meta.fragment.title}" by ${
           meta.fragment.authorName
-        }. So, you do not need to proceed with the review of this manuscript. <br/>
+        }. 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.`
         delete replacements.detailsUrl
-        replacements.signatureName = meta.handlingEditorName
+        replacements.signatureName = meta.editorName
         textBody = `${replacements.intro} ${replacements.paragraph} ${
           replacements.signatureName
         }`
@@ -522,6 +575,25 @@ module.exports = {
           replacements.signatureName
         }`
         break
+      case 'he-manuscript-return-with-comments':
+        subject = meta.emailSubject
+        replacements.hasLink = false
+        replacements.previewText =
+          'a manuscript has been returned with comments'
+        replacements.intro = `Dear Dr. ${meta.handlingEditorName}`
+
+        replacements.paragraph = `Thank you for your recommendation for the manuscript titled "${
+          meta.fragment.title
+        }" by ${
+          meta.fragment.authorName
+        } based on the reviews you received.<br/><br/>
+        ${meta.eicComments}<br/><br/>`
+        delete replacements.detailsUrl
+        replacements.signatureName = meta.eicName
+        textBody = `${replacements.intro} ${replacements.paragraph} ${
+          replacements.signatureName
+        }`
+        break
       case 'submitting-reviewers-after-decision':
         subject = meta.emailSubject
         replacements.hasLink = false
@@ -542,6 +614,44 @@ module.exports = {
           replacements.signatureName
         }`
         break
+      case 'new-version-submitted':
+        subject = `${meta.collection.customId}: Manuscript Update`
+        replacements.previewText = 'A manuscript has been updated'
+        replacements.intro = `Dear Dr. ${meta.handlingEditorName}`
+
+        replacements.paragraph = `A new version of the manuscript titled "${
+          meta.fragment.title
+        }" by ${meta.fragment.authorName} 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'
+
+        replacements.signatureName = meta.eicName
+        textBody = `${replacements.intro} ${replacements.paragraph} ${
+          replacements.beforeAnchor
+        } ${replacements.detailsUrl} ${replacements.afterAnchor} ${
+          replacements.signatureName
+        }`
+        break
+      case 'submitting-reviewers-after-revision':
+        subject = meta.emailSubject
+        replacements.previewText = 'A manuscript has been updated'
+        replacements.intro = `Dear Dr. ${meta.reviewerName}`
+
+        replacements.paragraph = `A new version of the manuscript titled "${
+          meta.fragment.title
+        }" by ${meta.fragment.authorName} 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(
+          meta.timestamp,
+          14,
+        )}. You can download the PDF of the revised version and submit your new review from the following URL:`
+
+        replacements.signatureName = meta.editorName
+        textBody = `${replacements.intro} ${replacements.paragraph} ${
+          replacements.beforeAnchor
+        } ${replacements.detailsUrl} ${replacements.signatureName}`
+        break
       default:
         subject = 'Hindawi Notification!'
         break
diff --git a/packages/component-manuscript-manager/config/authsome-helpers.js b/packages/component-manuscript-manager/config/authsome-helpers.js
deleted file mode 100644
index 1add8d99b7fb8eca56a2abb7bf43088d599e5efc..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/config/authsome-helpers.js
+++ /dev/null
@@ -1,83 +0,0 @@
-const omit = require('lodash/omit')
-const config = require('config')
-const get = require('lodash/get')
-
-const statuses = config.get('statuses')
-
-const publicStatusesPermissions = ['author', 'reviewer']
-
-const parseAuthorsData = (coll, matchingCollPerm) => {
-  if (['reviewer'].includes(matchingCollPerm.permission)) {
-    coll.authors = coll.authors.map(a => omit(a, ['email']))
-  }
-}
-
-const setPublicStatuses = (coll, matchingCollPerm) => {
-  const status = get(coll, 'status') || 'draft'
-  // coll.visibleStatus = statuses[status].public
-  if (publicStatusesPermissions.includes(matchingCollPerm.permission)) {
-    coll.visibleStatus = statuses[status].public
-  }
-}
-
-const filterRefusedInvitations = (coll, user) => {
-  const matchingInv = coll.invitations.find(inv => inv.userId === user.id)
-  if (matchingInv === undefined) return null
-  if (matchingInv.hasAnswer === true && !matchingInv.isAccepted) return null
-  return coll
-}
-
-const filterObjectData = (
-  collectionsPermissions = [],
-  object = {},
-  user = {},
-) => {
-  if (object.type === 'fragment') {
-    const matchingCollPerm = collectionsPermissions.find(
-      collPerm => object.id === collPerm.fragmentId,
-    )
-    if (matchingCollPerm === undefined) return null
-    if (['reviewer'].includes(matchingCollPerm.permission)) {
-      object.files = omit(object.files, ['coverLetter'])
-      if (object.recommendations)
-        object.recommendations = object.recommendations.filter(
-          rec => rec.userId === user.id,
-        )
-    }
-
-    return object
-  }
-  const matchingCollPerm = collectionsPermissions.find(
-    collPerm => object.id === collPerm.id,
-  )
-  if (matchingCollPerm === undefined) return null
-  setPublicStatuses(object, matchingCollPerm)
-  parseAuthorsData(object, matchingCollPerm)
-  if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
-    return filterRefusedInvitations(object, user)
-  }
-
-  return object
-}
-
-const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => {
-  const teams = await Promise.all(
-    teamIds.map(async teamId => {
-      const team = await TeamModel.find(teamId)
-      if (permissions.includes(team.teamType.permissions)) {
-        return team
-      }
-      return null
-    }),
-  )
-
-  return teams.filter(Boolean)
-}
-
-module.exports = {
-  parseAuthorsData,
-  setPublicStatuses,
-  filterRefusedInvitations,
-  filterObjectData,
-  getTeamsByPermissions,
-}
diff --git a/packages/component-manuscript-manager/config/authsome-mode.js b/packages/component-manuscript-manager/config/authsome-mode.js
index 667879b274b86a59a28515ed3d595d97e6ad2808..9c663beae1962d9fc13141f8439dc0a84214ae08 100644
--- a/packages/component-manuscript-manager/config/authsome-mode.js
+++ b/packages/component-manuscript-manager/config/authsome-mode.js
@@ -1,252 +1,3 @@
-const get = require('lodash/get')
-const pickBy = require('lodash/pickBy')
-const omit = require('lodash/omit')
-const helpers = require('./authsome-helpers')
-
-async function teamPermissions(user, operation, object, context) {
-  const permissions = ['handlingEditor', 'author', 'reviewer']
-  const teams = await helpers.getTeamsByPermissions(
-    user.teams,
-    permissions,
-    context.models.Team,
-  )
-
-  const collectionsPermissions = await Promise.all(
-    teams.map(async team => {
-      const collection = await context.models.Collection.find(team.object.id)
-      const collPerm = {
-        id: collection.id,
-        permission: team.teamType.permissions,
-      }
-      const objectType = get(object, 'type')
-      if (objectType === 'fragment' && collection.fragments.includes(object.id))
-        collPerm.fragmentId = object.id
-
-      return collPerm
-    }),
-  )
-
-  if (collectionsPermissions.length === 0) return {}
-
-  return {
-    filter: filterParam => {
-      if (!filterParam.length) {
-        return helpers.filterObjectData(
-          collectionsPermissions,
-          filterParam,
-          user,
-        )
-      }
-
-      const collections = filterParam
-        .map(coll =>
-          helpers.filterObjectData(collectionsPermissions, coll, user),
-        )
-        .filter(Boolean)
-      return collections
-    },
-  }
-}
-
-function unauthenticatedUser(operation, object) {
-  // Public/unauthenticated users can GET /collections, filtered by 'published'
-  if (operation === 'GET' && object && object.path === '/collections') {
-    return {
-      filter: collections =>
-        collections.filter(collection => collection.published),
-    }
-  }
-
-  // Public/unauthenticated users can GET /collections/:id/fragments, filtered by 'published'
-  if (
-    operation === 'GET' &&
-    object &&
-    object.path === '/collections/:id/fragments'
-  ) {
-    return {
-      filter: fragments => fragments.filter(fragment => fragment.published),
-    }
-  }
-
-  // and filtered individual collection's properties: id, title, source, content, owners
-  if (operation === 'GET' && object && object.type === 'collection') {
-    if (object.published) {
-      return {
-        filter: collection =>
-          pickBy(collection, (_, key) =>
-            ['id', 'title', 'owners'].includes(key),
-          ),
-      }
-    }
-  }
-
-  if (operation === 'GET' && object && object.type === 'fragment') {
-    if (object.published) {
-      return {
-        filter: fragment =>
-          pickBy(fragment, (_, key) =>
-            ['id', 'title', 'source', 'presentation', 'owners'].includes(key),
-          ),
-      }
-    }
-  }
-
-  return false
-}
-
-async function authenticatedUser(user, operation, object, context) {
-  // Allow the authenticated user to POST a collection (but not with a 'filtered' property)
-  if (operation === 'POST' && object.path === '/collections') {
-    return {
-      filter: collection => omit(collection, 'filtered'),
-    }
-  }
-
-  if (
-    operation === 'POST' &&
-    object.path === '/collections/:collectionId/fragments'
-  ) {
-    return true
-  }
-
-  // Allow the authenticated user to GET collections they own
-  if (operation === 'GET' && object === '/collections/') {
-    return {
-      filter: collection => collection.owners.includes(user.id),
-    }
-  }
-
-  // Allow owners of a collection to GET its teams, e.g.
-  // GET /api/collections/1/teams
-  if (operation === 'GET' && get(object, 'path') === '/teams') {
-    const collectionId = get(object, 'params.collectionId')
-    if (collectionId) {
-      const collection = await context.models.Collection.find(collectionId)
-      if (collection.owners.includes(user.id)) {
-        return true
-      }
-    }
-  }
-
-  if (
-    operation === 'GET' &&
-    get(object, 'type') === 'team' &&
-    get(object, 'object.type') === 'collection'
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object, 'object.id'),
-    )
-    if (collection.owners.includes(user.id)) {
-      return true
-    }
-  }
-
-  // Advanced example
-  // Allow authenticated users to create a team based around a collection
-  // if they are one of the owners of this collection
-  if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') {
-    if (get(object, 'object.type') === 'collection') {
-      const collection = await context.models.Collection.find(
-        get(object, 'object.id'),
-      )
-      if (collection.owners.includes(user.id)) {
-        return true
-      }
-    }
-  }
-
-  // only allow the HE to create, delete an invitation, or get invitation details
-  if (
-    ['POST', 'GET', 'DELETE'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('invitations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const handlingEditor = get(collection, 'handlingEditor')
-    if (!handlingEditor) return false
-    if (handlingEditor.id === user.id) return true
-    return false
-  }
-
-  // only allow a reviewer and an HE to submit and to modify a recommendation
-  if (
-    ['POST', 'PATCH'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('recommendations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const teams = await helpers.getTeamsByPermissions(
-      user.teams,
-      ['reviewer', 'handlingEditor'],
-      context.models.Team,
-    )
-    if (teams.length === 0) return false
-    const matchingTeam = teams.find(team => team.object.id === collection.id)
-    if (matchingTeam) return true
-    return false
-  }
-
-  if (user.teams.length !== 0 && operation === 'GET') {
-    const permissions = await teamPermissions(user, operation, object, context)
-
-    if (permissions) {
-      return permissions
-    }
-  }
-
-  if (get(object, 'type') === 'fragment') {
-    const fragment = object
-
-    if (fragment.owners.includes(user.id)) {
-      return true
-    }
-  }
-
-  if (get(object, 'type') === 'collection') {
-    if (['GET', 'DELETE'].includes(operation)) {
-      return true
-    }
-
-    // Only allow filtered updating (mirroring filtered creation) for non-admin users)
-    if (operation === 'PATCH') {
-      return {
-        filter: collection => omit(collection, 'filtered'),
-      }
-    }
-  }
-
-  // A user can GET, DELETE and PATCH itself
-  if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
-    if (['GET', 'DELETE', 'PATCH'].includes(operation)) {
-      return true
-    }
-  }
-  // If no individual permissions exist (above), fallback to unauthenticated
-  // user's permission
-  return unauthenticatedUser(operation, object)
-}
-
-const authsomeMode = async (userId, operation, object, context) => {
-  if (!userId) {
-    return unauthenticatedUser(operation, object)
-  }
-
-  // It's up to us to retrieve the relevant models for our
-  // authorization/authsome mode, e.g.
-  const user = await context.models.User.find(userId)
-
-  // Admins and editor in chiefs can do anything
-  if (user && (user.admin === true || user.editorInChief === true)) return true
-
-  if (user) {
-    return authenticatedUser(user, operation, object, context)
-  }
-
-  return false
-}
+const authsomeMode = require('xpub-faraday/config/authsome-mode')
 
 module.exports = authsomeMode
diff --git a/packages/component-manuscript-manager/config/default.js b/packages/component-manuscript-manager/config/default.js
index 276960b2fa5ce4133c55e91766a40dc4f69ea69f..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f 100644
--- a/packages/component-manuscript-manager/config/default.js
+++ b/packages/component-manuscript-manager/config/default.js
@@ -1,82 +1,3 @@
-const path = require('path')
+const defaultConfig = require('xpub-faraday/config/default')
 
-module.exports = {
-  authsome: {
-    mode: path.resolve(__dirname, 'authsome-mode.js'),
-    teams: {
-      handlingEditor: {
-        name: 'Handling Editors',
-      },
-      reviewer: {
-        name: 'Reviewer',
-      },
-    },
-  },
-  mailer: {
-    from: 'test@example.com',
-  },
-  'invite-reset-password': {
-    url:
-      process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL ||
-      'http://localhost:3000/invite',
-  },
-  roles: {
-    global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
-    collection: ['handlingEditor', 'reviewer', 'author'],
-    inviteRights: {
-      admin: ['admin', 'editorInChief', 'author'],
-      editorInChief: ['handlingEditor'],
-      handlingEditor: ['reviewer'],
-    },
-  },
-  statuses: {
-    draft: {
-      public: 'Draft',
-      private: 'Draft',
-    },
-    submitted: {
-      public: 'Submitted',
-      private: 'Submitted',
-    },
-    heInvited: {
-      public: 'Submitted',
-      private: 'HE Invited',
-    },
-    heAssigned: {
-      public: 'HE Assigned',
-      private: 'HE Assigned',
-    },
-    reviewersInvited: {
-      public: 'Reviewers Invited',
-      private: 'Reviewers Invited',
-    },
-    underReview: {
-      public: 'Under Review',
-      private: 'Under Review',
-    },
-    pendingApproval: {
-      public: 'Under Review',
-      private: 'Pending Approval',
-    },
-    rejected: {
-      public: 'Rejected',
-      private: 'Rejected',
-    },
-    published: {
-      public: 'Published',
-      private: 'Published',
-    },
-  },
-  'manuscript-types': {
-    research: 'Research',
-    review: 'Review',
-    'clinical-study': 'Clinical Study',
-    'case-report': 'Case Report',
-    'letter-to-editor': 'Letter to the Editor',
-    editorial: 'Editorial',
-    corrigendum: 'Corrigendum',
-    erratum: 'Erratum',
-    'expression-of-concern': 'Expression of Concern',
-    retraction: 'Retraction',
-  },
-}
+module.exports = defaultConfig
diff --git a/packages/component-manuscript-manager/config/test.js b/packages/component-manuscript-manager/config/test.js
index 6869d659a3af2b1b643561889f4f81d4c486d1cf..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f 100644
--- a/packages/component-manuscript-manager/config/test.js
+++ b/packages/component-manuscript-manager/config/test.js
@@ -1,83 +1,3 @@
-const path = require('path')
+const defaultConfig = require('xpub-faraday/config/default')
 
-module.exports = {
-  authsome: {
-    mode: path.resolve(__dirname, 'authsome-mode.js'),
-    teams: {
-      handlingEditor: {
-        name: 'Handling Editors',
-      },
-      reviewer: {
-        name: 'Reviewer',
-      },
-    },
-  },
-  mailer: {
-    from: 'test@example.com',
-  },
-  'invite-reset-password': {
-    url:
-      process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL ||
-      'http://localhost:3000/invite',
-  },
-  roles: {
-    global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
-    collection: ['handlingEditor', 'reviewer', 'author'],
-    inviteRights: {
-      admin: ['admin', 'editorInChief', 'author', 'handlingEditor', 'author'],
-      editorInChief: ['handlingEditor'],
-      handlingEditor: ['reviewer'],
-      author: ['author'],
-    },
-  },
-  statuses: {
-    draft: {
-      public: 'Draft',
-      private: 'Draft',
-    },
-    submitted: {
-      public: 'Submitted',
-      private: 'Submitted',
-    },
-    heInvited: {
-      public: 'Submitted',
-      private: 'HE Invited',
-    },
-    heAssigned: {
-      public: 'HE Assigned',
-      private: 'HE Assigned',
-    },
-    reviewersInvited: {
-      public: 'Reviewers Invited',
-      private: 'Reviewers Invited',
-    },
-    underReview: {
-      public: 'Under Review',
-      private: 'Under Review',
-    },
-    pendingApproval: {
-      public: 'Under Review',
-      private: 'Pending Approval',
-    },
-    rejected: {
-      public: 'Rejected',
-      private: 'Rejected',
-    },
-    published: {
-      public: 'Published',
-      private: 'Published',
-    },
-  },
-  'manuscript-types': {
-    research: 'Research',
-    review: 'Review',
-    'clinical-study': 'Clinical Study',
-    'case-report': 'Case Report',
-    'letter-to-editor': 'Letter to the Editor',
-    editorial: 'Editorial',
-    corrigendum: 'Corrigendum',
-    erratum: 'Erratum',
-    'expression-of-concern': 'Expression of Concern',
-    retraction: 'Retraction',
-  },
-}
+module.exports = defaultConfig
diff --git a/packages/component-manuscript-manager/index.js b/packages/component-manuscript-manager/index.js
index 0c04f744119898af8a4ae1dc44a6eabbc5c2b24b..63ee7dbde1665993000139f654d8669b7465f8cb 100644
--- a/packages/component-manuscript-manager/index.js
+++ b/packages/component-manuscript-manager/index.js
@@ -1,5 +1,6 @@
 module.exports = {
   backend: () => app => {
     require('./src/FragmentsRecommendations')(app)
+    require('./src/Fragments')(app)
   },
 }
diff --git a/packages/component-manuscript-manager/package.json b/packages/component-manuscript-manager/package.json
index 9d40f80a6ae8f8706e7e0fc235e9940a444c8b6b..55ffd9d800fbc02181a01ef7821f74637be8296b 100644
--- a/packages/component-manuscript-manager/package.json
+++ b/packages/component-manuscript-manager/package.json
@@ -25,7 +25,8 @@
   "peerDependencies": {
     "@pubsweet/logger": "^0.0.1",
     "pubsweet-component-mail-service": "0.0.1",
-    "pubsweet-server": "^1.0.1"
+    "pubsweet-server": "^1.0.1",
+    "component-helper-service": "0.0.1"
   },
   "devDependencies": {
     "apidoc": "^0.17.6",
diff --git a/packages/component-manuscript-manager/src/Fragments.js b/packages/component-manuscript-manager/src/Fragments.js
new file mode 100644
index 0000000000000000000000000000000000000000..01e11a93582bdc5ff37497d2bb7a7c6a980b68c0
--- /dev/null
+++ b/packages/component-manuscript-manager/src/Fragments.js
@@ -0,0 +1,54 @@
+const bodyParser = require('body-parser')
+
+const Fragments = app => {
+  app.use(bodyParser.json())
+  const basePath = '/api/collections/:collectionId/fragments/:fragmentId/submit'
+  const routePath = './routes/fragments'
+  const authBearer = app.locals.passport.authenticate('bearer', {
+    session: false,
+  })
+  /**
+   * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a revision for a manuscript
+   * @apiGroup Fragments
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiSuccessExample {json} Success
+   *   HTTP/1.1 200 OK
+   *   {
+   *
+   *   }
+   * @apiErrorExample {json} Invite user errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.patch(
+    `${basePath}`,
+    authBearer,
+    require(`${routePath}/patch`)(app.locals.models),
+  )
+  /**
+   * @api {post} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a manuscript
+   * @apiGroup Fragments
+   * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
+   * @apiSuccessExample {json} Success
+   *   HTTP/1.1 200 OK
+   *   {
+   *
+   *   }
+   * @apiErrorExample {json} Invite user errors
+   *    HTTP/1.1 403 Forbidden
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   *    HTTP/1.1 500 Internal Server Error
+   */
+  app.post(
+    `${basePath}`,
+    authBearer,
+    require(`${routePath}/post`)(app.locals.models),
+  )
+}
+
+module.exports = Fragments
diff --git a/packages/component-manuscript-manager/src/helpers/Collection.js b/packages/component-manuscript-manager/src/helpers/Collection.js
deleted file mode 100644
index b5e783c80c7fb320ef269fc863544fafd5604eed..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/helpers/Collection.js
+++ /dev/null
@@ -1,89 +0,0 @@
-const config = require('config')
-
-const statuses = config.get('statuses')
-
-const updateStatusByRecommendation = async (collection, recommendation) => {
-  let newStatus = 'pendingApproval'
-  if (['minor', 'major'].includes(recommendation))
-    newStatus = 'revisionRequested'
-  collection.status = newStatus
-  collection.visibleStatus = statuses[collection.status].private
-  await collection.save()
-}
-
-const updateFinalStatusByRecommendation = async (
-  collection,
-  recommendation,
-) => {
-  let newStatus
-  switch (recommendation) {
-    case 'reject':
-      newStatus = 'rejected'
-      break
-    case 'publish':
-      newStatus = 'published'
-      break
-    case 'return-to-handling-editor':
-      newStatus = 'reviewCompleted'
-      break
-    default:
-      break
-  }
-  collection.status = newStatus
-  collection.visibleStatus = statuses[collection.status].private
-  await collection.save()
-}
-
-const updateStatus = async (collection, newStatus) => {
-  collection.status = newStatus
-  collection.visibleStatus = statuses[collection.status].private
-  await collection.save()
-}
-
-const getFragmentAndAuthorData = async ({
-  UserModel,
-  fragment,
-  collection: { authors, handlingEditor },
-}) => {
-  const heRecommendation = fragment.recommendations.find(
-    rec => rec.userId === handlingEditor.id,
-  )
-  let { title, abstract } = fragment.metadata
-  const { type } = fragment.metadata
-  title = title.replace(/<(.|\n)*?>/g, '')
-  abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : ''
-
-  const submittingAuthorData = authors.find(
-    author => author.isSubmitting === true,
-  )
-  const submittingAuthor = await UserModel.find(submittingAuthorData.userId)
-  const authorsPromises = authors.map(async author => {
-    const user = await UserModel.find(author.userId)
-    return `${user.firstName} ${user.lastName}`
-  })
-  const authorsList = await Promise.all(authorsPromises)
-  return {
-    title,
-    submittingAuthor,
-    abstract,
-    authorsList,
-    type,
-    heRecommendation,
-  }
-}
-
-const getAgreedReviewerInvitation = (invitations = []) =>
-  invitations.filter(
-    inv =>
-      inv.role === 'reviewer' &&
-      inv.hasAnswer === true &&
-      inv.isAccepted === true,
-  )
-
-module.exports = {
-  updateStatusByRecommendation,
-  getFragmentAndAuthorData,
-  getAgreedReviewerInvitation,
-  updateStatus,
-  updateFinalStatusByRecommendation,
-}
diff --git a/packages/component-manuscript-manager/src/helpers/User.js b/packages/component-manuscript-manager/src/helpers/User.js
deleted file mode 100644
index c9945c0ee80f60f09add3de038b360de877e56f9..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/helpers/User.js
+++ /dev/null
@@ -1,275 +0,0 @@
-const collectionHelper = require('./Collection')
-const get = require('lodash/get')
-const config = require('config')
-const mailService = require('pubsweet-component-mail-service')
-
-const manuscriptTypes = config.get('manuscript-types')
-const getEditorInChief = async UserModel => {
-  const users = await UserModel.all()
-  const eic = users.find(user => user.editorInChief || user.admin)
-  return eic
-}
-
-const setupReviewSubmittedEmailData = async ({
-  baseUrl,
-  UserModel,
-  fragment: { id, title, submittingAuthor },
-  collection,
-  user,
-}) => {
-  const eic = await getEditorInChief(UserModel)
-  const toEmail = collection.handlingEditor.email
-  await mailService.sendNotificationEmail({
-    toEmail,
-    user,
-    emailType: 'review-submitted',
-    meta: {
-      collection: { customId: collection.customId, id: collection.id },
-      fragment: {
-        id,
-        title,
-        authorName: `${submittingAuthor.firstName} ${
-          submittingAuthor.lastName
-        }`,
-      },
-      handlingEditorName: collection.handlingEditor.name,
-      baseUrl,
-      eicName: `${eic.firstName} ${eic.lastName}`,
-    },
-  })
-}
-
-const setupReviewersEmail = async ({
-  fragment: { title, authorName, recommendations },
-  collection,
-  UserModel,
-  recommendation,
-  isSubmitted = false,
-}) => {
-  const agreedReviewerInvitations = collectionHelper.getAgreedReviewerInvitation(
-    collection.invitations,
-  )
-  const hasReview = invUserId => rec =>
-    rec.recommendationType === 'review' &&
-    rec.submittedOn &&
-    invUserId === rec.userId
-  const reviewerPromises = await agreedReviewerInvitations.map(async inv => {
-    const submittedReview = recommendations.find(hasReview(inv.userId))
-    const shouldReturnUser =
-      (isSubmitted && submittedReview) || (!isSubmitted && !submittedReview)
-    if (shouldReturnUser) return UserModel.find(inv.userId)
-  })
-  let reviewers = await Promise.all(reviewerPromises)
-  reviewers = reviewers.filter(Boolean)
-  const subject = isSubmitted
-    ? `${collection.customId}: Manuscript Decision`
-    : `${collection.customId}: Manuscript ${getSubject(recommendation)}`
-  let emailType = 'agreed-reviewers-after-recommendation'
-  let emailText
-  if (isSubmitted) {
-    emailType = 'submitting-reviewers-after-decision'
-    emailText = 'has now been rejected'
-    if (recommendation === 'publish') emailText = 'will now be published'
-  }
-
-  const eic = await getEditorInChief(UserModel)
-  const editorName = isSubmitted
-    ? `${eic.firstName} ${eic.lastName}`
-    : collection.handlingEditor.name
-  reviewers.forEach(user =>
-    mailService.sendNotificationEmail({
-      toEmail: user.email,
-      emailType,
-      meta: {
-        fragment: { title, authorName },
-        editorName,
-        emailSubject: subject,
-        reviewerName: `${user.firstName} ${user.lastName}`,
-        emailText,
-      },
-    }),
-  )
-}
-
-const setupNoResponseReviewersEmailData = async ({
-  baseUrl,
-  fragment: { title, authorName, type },
-  collection,
-  UserModel,
-}) => {
-  const invitations = collection.invitations.filter(
-    inv => inv.role === 'reviewer' && inv.hasAnswer === false,
-  )
-  const userPromises = await invitations.map(async inv =>
-    UserModel.find(inv.userId),
-  )
-  let users = await Promise.all(userPromises)
-  users = users.filter(Boolean)
-  const subject = `${collection.customId}: Reviewer Unassigned`
-  const manuscriptType = manuscriptTypes[type]
-  users.forEach(user =>
-    mailService.sendNotificationEmail({
-      toEmail: user.email,
-      user,
-      emailType: 'no-response-reviewers-after-recommendation',
-      meta: {
-        collection: { customId: collection.customId },
-        fragment: { title, authorName },
-        handlingEditorName: collection.handlingEditor.name,
-        baseUrl,
-        emailSubject: subject,
-        manuscriptType,
-      },
-    }),
-  )
-}
-
-const setupEiCRecommendationEmailData = async ({
-  baseUrl,
-  UserModel,
-  fragment: { id, title, authorName },
-  collection,
-  recommendation,
-  comments,
-}) => {
-  // to do: get private note from recommendation
-  const privateNote = comments.find(comm => comm.public === false)
-  const content = get(privateNote, 'content')
-  const privateNoteText =
-    content !== undefined ? `Private note: "${content}"` : ''
-  let paragraph
-  const heRecommendation = getHeRecommendation(recommendation)
-  switch (heRecommendation) {
-    case 'publish':
-      paragraph = `It is my recommendation, based on the reviews I have received for the manuscript titled "${title}" by ${authorName}, that we should proceed to publication. <br/><br/>
-      ${privateNoteText}<br/><br/>`
-      break
-    case 'reject':
-      paragraph = `It is my recommendation, based on the reviews I have received for the manuscript titled "${title}" by ${authorName}, that we should reject it for publication. <br/><br/>
-      ${privateNoteText}<br/><br/>`
-      break
-    case 'revision':
-      paragraph = `In order for the manuscript titled "${title}" by ${authorName} to proceed to publication, there needs to be a revision. <br/><br/>
-      ${privateNoteText}<br/><br/>`
-      break
-
-    default:
-      throw new Error('undefined he recommentation type')
-  }
-
-  const eic = await getEditorInChief(UserModel)
-  const toEmail = eic.email
-  await mailService.sendNotificationEmail({
-    toEmail,
-    emailType: 'eic-recommendation',
-    meta: {
-      collection: { customId: collection.customId, id: collection.id },
-      fragment: { id },
-      handlingEditorName: collection.handlingEditor.name,
-      baseUrl,
-      eicName: `${eic.firstName} ${eic.lastName}`,
-      paragraph,
-    },
-  })
-}
-
-const setupAuthorsEmailData = async ({
-  baseUrl,
-  UserModel,
-  fragment: { id, title, submittingAuthor },
-  collection,
-  comments,
-  requestToRevision = false,
-  publish = false,
-}) => {
-  const authorNote = comments.find(comm => comm.public === true)
-  const content = get(authorNote, 'content')
-  const authorNoteText =
-    content !== undefined ? `Reason & Details: "${content}"` : ''
-  let emailType = requestToRevision
-    ? 'author-request-to-revision'
-    : 'author-manuscript-rejected'
-  if (publish) emailType = 'author-manuscript-published'
-  let toAuthors = []
-  if (emailType === 'author-request-to-revision') {
-    toAuthors.push({
-      email: submittingAuthor.email,
-      name: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`,
-    })
-  } else {
-    toAuthors = collection.authors.map(author => ({
-      email: author.email,
-      name: `${author.firstName} ${author.lastName}`,
-    }))
-  }
-  toAuthors.forEach(toAuthor => {
-    mailService.sendNotificationEmail({
-      toEmail: toAuthor.email,
-      emailType,
-      meta: {
-        collection: { customId: collection.customId, id: collection.id },
-        fragment: {
-          id,
-          title,
-          authorName: toAuthor.name,
-          submittingAuthorName: `${submittingAuthor.firstName} ${
-            submittingAuthor.lastName
-          }`,
-        },
-        handlingEditorName: collection.handlingEditor.name,
-        baseUrl,
-        authorNoteText,
-      },
-    })
-  })
-}
-
-const setupManuscriptDecisionEmailForHe = async ({
-  UserModel,
-  fragment: { title, submittingAuthor },
-  collection: { customId, handlingEditor },
-  publish = false,
-}) => {
-  const eic = await getEditorInChief(UserModel)
-  const toEmail = handlingEditor.email
-  const emailType = publish
-    ? 'he-manuscript-published'
-    : 'he-manuscript-rejected'
-  await mailService.sendNotificationEmail({
-    toEmail,
-    emailType,
-    meta: {
-      emailSubject: `${customId}: Manuscript Decision`,
-      fragment: {
-        title,
-        authorName: `${submittingAuthor.firstName} ${
-          submittingAuthor.lastName
-        }`,
-      },
-      handlingEditorName: handlingEditor.name,
-      eicName: `${eic.firstName} ${eic.lastName}`,
-    },
-  })
-}
-const getSubject = recommendation =>
-  ['minor', 'major'].includes(recommendation)
-    ? 'Revision Requested'
-    : 'Recommendation Submitted'
-
-const getHeRecommendation = recommendation => {
-  let heRecommendation = recommendation === 'reject' ? 'reject' : 'publish'
-  if (['minor', 'major'].includes(recommendation)) {
-    heRecommendation = 'revision'
-  }
-  return heRecommendation
-}
-
-module.exports = {
-  getEditorInChief,
-  setupReviewSubmittedEmailData,
-  setupReviewersEmail,
-  setupEiCRecommendationEmailData,
-  setupAuthorsEmailData,
-  setupNoResponseReviewersEmailData,
-  setupManuscriptDecisionEmailForHe,
-}
diff --git a/packages/component-manuscript-manager/src/helpers/authsome.js b/packages/component-manuscript-manager/src/helpers/authsome.js
deleted file mode 100644
index 212cee2a3ea23a424b1f77dde2f7dd4cf888b2a0..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/helpers/authsome.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const config = require('config')
-const Authsome = require('authsome')
-
-const mode = require(config.get('authsome.mode'))
-
-const getAuthsome = models =>
-  new Authsome(
-    { ...config.authsome, mode },
-    {
-      // restrict methods passed to mode since these have to be shimmed on client
-      // any changes here should be reflected in the `withAuthsome` component of `pubsweet-client`
-      models: {
-        Collection: {
-          find: id => models.Collection.find(id),
-        },
-        Fragment: {
-          find: id => models.Fragment.find(id),
-        },
-        User: {
-          find: id => models.User.find(id),
-        },
-        Team: {
-          find: id => models.Team.find(id),
-        },
-      },
-    },
-  )
-
-module.exports = { getAuthsome }
diff --git a/packages/component-manuscript-manager/src/routes/fragments/patch.js b/packages/component-manuscript-manager/src/routes/fragments/patch.js
new file mode 100644
index 0000000000000000000000000000000000000000..af3511344a735f5a4d303bbfa6423a99eeb6293f
--- /dev/null
+++ b/packages/component-manuscript-manager/src/routes/fragments/patch.js
@@ -0,0 +1,147 @@
+const {
+  Team,
+  User,
+  Email,
+  services,
+  Fragment,
+  Collection,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
+const union = require('lodash/union')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Collection and fragment do not match.`,
+      })
+    const fragLength = collection.fragments.length
+    if (fragLength < 2) {
+      return res.status(400).json({
+        error: 'No previous version has been found.',
+      })
+    }
+    fragment = await models.Fragment.find(fragmentId)
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      fragment,
+      path: req.route.path,
+    }
+    const canPatch = await authsome.can(req.user, 'PATCH', target)
+    if (!canPatch)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+
+    const collectionHelper = new Collection({ collection })
+    const fragmentHelper = new Fragment({ fragment })
+    const teamHelper = new Team({
+      TeamModel: models.Team,
+      collectionId,
+      fragmentId,
+    })
+    const userHelper = new User({ UserModel: models.User })
+
+    const reviewerIds = fragment.invitations.map(inv => {
+      const { userId } = inv
+      return userId
+    })
+
+    const reviewersTeam = await teamHelper.createTeam({
+      role: 'reviewer',
+      members: reviewerIds,
+      objectType: 'fragment',
+    })
+
+    reviewerIds.forEach(id =>
+      userHelper.updateUserTeams({
+        userId: id,
+        teamId: reviewersTeam.id,
+      }),
+    )
+
+    const authorIds = fragment.authors.map(auth => {
+      const { id } = auth
+      return id
+    })
+
+    let authorsTeam = await teamHelper.getTeam({
+      role: 'author',
+      objectType: 'fragment',
+    })
+
+    if (!authorsTeam) {
+      authorsTeam = await teamHelper.createTeam({
+        role: 'author',
+        members: authorIds,
+        objectType: 'fragment',
+      })
+    } else {
+      authorsTeam.members = union(authorsTeam.members, authorIds)
+      await authorsTeam.save()
+    }
+
+    authorIds.forEach(id =>
+      userHelper.updateUserTeams({
+        userId: id,
+        teamId: reviewersTeam.id,
+      }),
+    )
+
+    const previousFragment = await models.Fragment.find(
+      collection.fragments[fragLength - 2],
+    )
+    fragmentHelper.fragment = previousFragment
+
+    const heRecommendation = fragmentHelper.getHeRequestToRevision()
+    if (!heRecommendation) {
+      return res.status(400).json({
+        error: 'No Handling Editor request to revision has been found.',
+      })
+    }
+
+    collectionHelper.updateStatusByRecommendation({
+      recommendation: heRecommendation.recommendation,
+    })
+
+    fragment.submitted = Date.now()
+    fragment = await fragment.save()
+
+    const parsedFragment = await fragmentHelper.getFragmentData({
+      handlingEditor: collection.handlingEditor,
+    })
+    const authors = await fragmentHelper.getAuthorData({
+      UserModel: models.User,
+    })
+    const email = new Email({
+      authors,
+      collection,
+      parsedFragment: { ...parsedFragment, id: fragment.id },
+      UserModel: models.User,
+      baseUrl: services.getBaseUrl(req),
+    })
+    email.setupNewVersionSubmittedEmail()
+
+    if (heRecommendation.recommendation === 'major') {
+      email.setupReviewersEmail({
+        agree: true,
+        isRevision: true,
+        isSubmitted: true,
+        FragmentModel: models.Fragment,
+        newFragmentId: fragment.id,
+      })
+    }
+
+    return res.status(200).json(fragment)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-manuscript-manager/src/routes/fragments/post.js b/packages/component-manuscript-manager/src/routes/fragments/post.js
new file mode 100644
index 0000000000000000000000000000000000000000..b54399b0cc006f469863bbfabaaecb0c8bab3150
--- /dev/null
+++ b/packages/component-manuscript-manager/src/routes/fragments/post.js
@@ -0,0 +1,61 @@
+const {
+  Email,
+  Fragment,
+  services,
+  authsome: authsomeHelper,
+} = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Collection and fragment do not match.`,
+      })
+    fragment = await models.Fragment.find(fragmentId)
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      fragment,
+      path: req.route.path,
+    }
+    const canPost = await authsome.can(req.user, 'POST', target)
+    if (!canPost)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+
+    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,
+    })
+
+    const email = new Email({
+      authors,
+      collection,
+      parsedFragment,
+      UserModel: models.User,
+      baseUrl: services.getBaseUrl(req),
+    })
+    email.setupManuscriptSubmittedEmail()
+
+    collection.status = 'submitted'
+    collection.save()
+
+    return res.status(200).json(fragment)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
index 97ee0a9f2362a6ca6665257f9cd4419ea73e172c..524276fb2d35b692a586cad981fcf7e38ab6b8a9 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js
@@ -1,7 +1,10 @@
-const helpers = require('../../helpers/helpers')
-const authsomeHelper = require('../../helpers/authsome')
-const collectionHelper = require('../../helpers/Collection')
-const userHelper = require('../../helpers/User')
+const {
+  Email,
+  services,
+  authsome: authsomeHelper,
+  Fragment,
+  Collection,
+} = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { collectionId, fragmentId, recommendationId } = req.params
@@ -12,52 +15,69 @@ module.exports = models => async (req, res) => {
       return res.status(400).json({
         error: `Collection and fragment do not match.`,
       })
-    const authsome = authsomeHelper.getAuthsome(models)
-    const target = {
-      collection,
-      path: req.route.path,
-    }
-    const user = await models.User.find(req.user)
-    const canPatch = await authsome.can(req.user, 'PATCH', target)
-    if (!canPatch)
-      return res.status(403).json({
-        error: 'Unauthorized.',
-      })
+
     fragment = await models.Fragment.find(fragmentId)
+
     const recommendation = fragment.recommendations.find(
       rec => rec.id === recommendationId,
     )
+
     if (!recommendation)
       return res.status(404).json({ error: 'Recommendation not found.' })
+
     if (recommendation.userId !== req.user)
       return res.status(403).json({
         error: 'Unauthorized.',
       })
+
+    const authsome = authsomeHelper.getAuthsome(models)
+    const target = {
+      fragment,
+      path: req.route.path,
+    }
+    const canPatch = await authsome.can(req.user, 'PATCH', target)
+    if (!canPatch)
+      return res.status(403).json({
+        error: 'Unauthorized.',
+      })
+
+    const UserModel = models.User
+    const user = await UserModel.find(req.user)
+
     Object.assign(recommendation, req.body)
     recommendation.updatedOn = Date.now()
-    if (req.body.submittedOn !== undefined) {
-      const {
-        title,
-        submittingAuthor,
-      } = await collectionHelper.getFragmentAndAuthorData({
-        UserModel: models.User,
-        fragment,
-        collection,
+    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,
       })
-      await userHelper.setupReviewSubmittedEmailData({
-        baseUrl: helpers.getBaseUrl(req),
-        UserModel: models.User,
-        fragment: { id: fragment.id, title, submittingAuthor },
+
+      const email = new Email({
+        UserModel,
         collection,
-        user,
+        parsedFragment,
+        baseUrl,
+        authors,
+      })
+
+      email.setupHandlingEditorEmail({
+        reviewSubmitted: true,
+        reviewerName: `${user.firstName} ${user.lastName}`,
       })
-      if (!['pendingApproval', 'revisionRequested'].includes(collection.status))
-        await collectionHelper.updateStatus(collection, 'reviewCompleted')
+
+      if (['underReview'].includes(collection.status))
+        collectionHelper.updateStatus({ newStatus: 'reviewCompleted' })
     }
     await fragment.save()
     return res.status(200).json(recommendation)
   } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'Item')
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
index f00c899edcddaf3339814de655fbc1d83246acc7..53ae3e90411ee519901840258d8776a40ef9aeee 100644
--- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
+++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js
@@ -1,13 +1,16 @@
-const helpers = require('../../helpers/helpers')
 const uuid = require('uuid')
-const authsomeHelper = require('../../helpers/authsome')
-const collectionHelper = require('../../helpers/Collection')
-const userHelper = require('../../helpers/User')
-const get = require('lodash/get')
+const { chain } = require('lodash')
+const {
+  Email,
+  services,
+  authsome: authsomeHelper,
+  Fragment,
+  Collection,
+} = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { recommendation, comments, recommendationType } = req.body
-  if (!helpers.checkForUndefinedParams(recommendationType))
+  if (!services.checkForUndefinedParams(recommendationType))
     return res.status(400).json({ error: 'Recommendation type is required.' })
 
   const reqUser = await models.User.find(req.user)
@@ -21,17 +24,17 @@ module.exports = models => async (req, res) => {
       return res.status(400).json({
         error: `Collection and fragment do not match.`,
       })
-
     fragment = await models.Fragment.find(fragmentId)
   } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'Item')
+    const notFoundError = await services.handleNotFoundError(e, 'Item')
     return res.status(notFoundError.status).json({
       error: notFoundError.message,
     })
   }
+
   const authsome = authsomeHelper.getAuthsome(models)
   const target = {
-    collection,
+    fragment,
     path: req.route.path,
   }
   const canPost = await authsome.can(req.user, 'POST', target)
@@ -39,6 +42,7 @@ module.exports = models => async (req, res) => {
     return res.status(403).json({
       error: 'Unauthorized.',
     })
+
   fragment.recommendations = fragment.recommendations || []
   const newRecommendation = {
     id: uuid.v4(),
@@ -51,100 +55,78 @@ module.exports = models => async (req, res) => {
   newRecommendation.recommendation = recommendation || undefined
   newRecommendation.comments = comments || undefined
   const UserModel = models.User
+  const collectionHelper = new Collection({ collection })
+  const fragmentHelper = new Fragment({ fragment })
+  const parsedFragment = await fragmentHelper.getFragmentData({
+    handlingEditor: collection.handlingEditor,
+  })
 
-  const {
-    title,
-    submittingAuthor,
-    type,
-    heRecommendation,
-  } = await collectionHelper.getFragmentAndAuthorData({
+  const baseUrl = services.getBaseUrl(req)
+  const authors = await fragmentHelper.getAuthorData({ UserModel })
+
+  const email = new Email({
     UserModel,
-    fragment,
     collection,
+    parsedFragment,
+    baseUrl,
+    authors,
   })
-  const baseUrl = helpers.getBaseUrl(req)
-  const authorName = `${submittingAuthor.firstName} ${
-    submittingAuthor.lastName
-  }`
+  const FragmentModel = models.Fragment
+
   if (reqUser.editorInChief || reqUser.admin) {
-    if (recommendation === 'return-to-handling-editor')
-      await collectionHelper.updateStatus(collection, 'reviewCompleted')
-    else {
-      await collectionHelper.updateFinalStatusByRecommendation(
-        collection,
+    if (recommendation === 'return-to-handling-editor') {
+      collectionHelper.updateStatus({ newStatus: 'reviewCompleted' })
+      const eicComments = chain(newRecommendation)
+        .get('comments')
+        .find(comm => !comm.public)
+        .get('content')
+        .value()
+      email.parsedFragment.eicComments = eicComments
+      email.setupHandlingEditorEmail({
+        returnWithComments: true,
+      })
+    } else {
+      collectionHelper.updateFinalStatusByRecommendation({
         recommendation,
-      )
-      await userHelper.setupAuthorsEmailData({
-        baseUrl,
-        UserModel,
-        collection,
-        fragment: { title, submittingAuthor },
-        comments: get(heRecommendation, 'comments'),
+      })
+      email.setupAuthorsEmail({
         requestToRevision: false,
         publish: recommendation === 'publish',
+        FragmentModel,
       })
-      await userHelper.setupManuscriptDecisionEmailForHe({
-        UserModel,
-        fragment: { title, submittingAuthor },
-        collection: {
-          customId: collection.customId,
-          handlingEditor: collection.handlingEditor,
-        },
+      email.setupHandlingEditorEmail({
         publish: recommendation === 'publish',
       })
-
-      await userHelper.setupReviewersEmail({
-        UserModel,
-        collection,
-        fragment: {
-          title,
-          authorName,
-          recommendations: fragment.recommendations,
-        },
+      email.parsedFragment.recommendations = fragment.recommendations
+      email.setupReviewersEmail({
         recommendation,
         isSubmitted: true,
+        agree: true,
+        FragmentModel,
       })
     }
   } else if (recommendationType === 'editorRecommendation') {
-    await collectionHelper.updateStatusByRecommendation(
-      collection,
-      recommendation,
-    )
-    await userHelper.setupReviewersEmail({
-      UserModel,
-      collection,
-      fragment: {
-        title,
-        authorName,
-        recommendations: fragment.recommendations,
-      },
+    collectionHelper.updateStatusByRecommendation({
       recommendation,
+      isHandlingEditor: true,
     })
-    await userHelper.setupNoResponseReviewersEmailData({
-      baseUrl,
-      UserModel,
-      collection,
-      fragment: { title, authorName, type },
+    email.setupReviewersEmail({
+      recommendation,
+      agree: true,
+      FragmentModel,
     })
-    await userHelper.setupEiCRecommendationEmailData({
-      baseUrl,
-      UserModel,
-      collection,
+    email.setupReviewersEmail({ agree: false, FragmentModel: models.Fragment })
+    email.setupEiCEmail({
       recommendation,
-      fragment: { title, id: fragment.id, authorName },
       comments: newRecommendation.comments,
     })
-    if (['minor', 'major'].includes(recommendation))
-      await userHelper.setupAuthorsEmailData({
-        baseUrl,
-        UserModel,
-        collection,
-        fragment: { title, id: fragment.id, submittingAuthor },
-        comments: newRecommendation.comments,
+    if (['minor', 'major'].includes(recommendation)) {
+      email.parsedFragment.newComments = newRecommendation.comments
+      email.setupAuthorsEmail({
         requestToRevision: true,
       })
+    }
   }
-
   fragment.recommendations.push(newRecommendation)
   await fragment.save()
   return res.status(200).json(newRecommendation)
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/collections.js b/packages/component-manuscript-manager/src/tests/fixtures/collections.js
deleted file mode 100644
index bef6132199981a79b87ea86440146eaab5891883..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/collections.js
+++ /dev/null
@@ -1,68 +0,0 @@
-const Chance = require('chance')
-const {
-  user,
-  handlingEditor,
-  author,
-  reviewer,
-  answerReviewer,
-} = require('./userData')
-const { fragment } = require('./fragments')
-
-const chance = new Chance()
-const collections = {
-  collection: {
-    id: chance.guid(),
-    title: chance.sentence(),
-    type: 'collection',
-    fragments: [fragment.id],
-    owners: [user.id],
-    save: jest.fn(),
-    authors: [
-      {
-        userId: author.id,
-        isSubmitting: true,
-        isCorresponding: false,
-      },
-    ],
-    invitations: [
-      {
-        id: chance.guid(),
-        role: 'handlingEditor',
-        hasAnswer: false,
-        isAccepted: false,
-        userId: handlingEditor.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: null,
-      },
-      {
-        id: chance.guid(),
-        role: 'reviewer',
-        hasAnswer: false,
-        isAccepted: false,
-        userId: reviewer.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: null,
-      },
-      {
-        id: chance.guid(),
-        role: 'reviewer',
-        hasAnswer: true,
-        isAccepted: false,
-        userId: answerReviewer.id,
-        invitedOn: chance.timestamp(),
-        respondedOn: chance.timestamp(),
-      },
-    ],
-    handlingEditor: {
-      id: handlingEditor.id,
-      hasAnswer: false,
-      isAccepted: false,
-      email: handlingEditor.email,
-      invitedOn: chance.timestamp(),
-      respondedOn: null,
-      name: `${handlingEditor.firstName} ${handlingEditor.lastName}`,
-    },
-  },
-}
-
-module.exports = collections
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js b/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js
deleted file mode 100644
index cbb5c85cbe056c7dc633db935bf5707764ac40bd..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/fixtures.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const fragments = require('./fragments')
-const teams = require('./teams')
-
-module.exports = {
-  users,
-  collections,
-  fragments,
-  teams,
-}
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js b/packages/component-manuscript-manager/src/tests/fixtures/fragments.js
deleted file mode 100644
index 0bc2a06b2cacf0a2a2dcb83ee5202cf9059f3618..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/fragments.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const Chance = require('chance')
-const { recReviewer } = require('./userData')
-
-const chance = new Chance()
-const fragments = {
-  fragment: {
-    id: chance.guid(),
-    metadata: {
-      title: chance.sentence(),
-      abstract: chance.paragraph(),
-    },
-    save: jest.fn(),
-    recommendations: [
-      {
-        recommendation: 'accept',
-        recommendationType: 'review',
-        comments: [
-          {
-            content: chance.paragraph(),
-            public: chance.bool(),
-            files: [
-              {
-                id: chance.guid(),
-                name: 'file.pdf',
-                size: chance.natural(),
-              },
-            ],
-          },
-        ],
-        id: chance.guid(),
-        userId: recReviewer.id,
-        createdOn: chance.timestamp(),
-        updatedOn: chance.timestamp(),
-      },
-    ],
-  },
-}
-
-module.exports = fragments
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js b/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js
deleted file mode 100644
index 607fd6661b1e7c9848bf13257a0d198f230fab00..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/teamIDs.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-const heID = chance.guid()
-const revId = chance.guid()
-
-module.exports = {
-  heTeamID: heID,
-  revTeamID: revId,
-}
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/teams.js b/packages/component-manuscript-manager/src/tests/fixtures/teams.js
deleted file mode 100644
index 1c87e804343747e1dfa7132259bc044c9f39081e..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/teams.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const { revTeamID, heTeamID } = require('./teamIDs')
-
-const { collection } = collections
-const { reviewer, handlingEditor } = users
-const teams = {
-  revTeam: {
-    teamType: {
-      name: 'reviewer',
-      permissions: 'reviewer',
-    },
-    group: 'reviewer',
-    name: 'reviewer',
-    object: {
-      type: 'collection',
-      id: collection.id,
-    },
-    members: [reviewer.id],
-    save: jest.fn(() => teams.revTeam),
-    updateProperties: jest.fn(() => teams.revTeam),
-    id: revTeamID,
-  },
-  heTeam: {
-    teamType: {
-      name: 'handlingEditor',
-      permissions: 'handlingEditor',
-    },
-    group: 'handlingEditor',
-    name: 'HandlingEditor',
-    object: {
-      type: 'collection',
-      id: collection.id,
-    },
-    members: [handlingEditor.id],
-    save: jest.fn(() => teams.heTeam),
-    updateProperties: jest.fn(() => teams.heTeam),
-    id: heTeamID,
-  },
-}
-module.exports = teams
diff --git a/packages/component-manuscript-manager/src/tests/fixtures/users.js b/packages/component-manuscript-manager/src/tests/fixtures/users.js
deleted file mode 100644
index 573c25543f255e06ffe659e1dcc5bd3feb2d38cb..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/fixtures/users.js
+++ /dev/null
@@ -1,84 +0,0 @@
-const { reviewer, author, recReviewer, handlingEditor } = require('./userData')
-const { revTeamID, heTeamID } = require('./teamIDs')
-
-const Chance = require('chance')
-
-const chance = new Chance()
-const users = {
-  reviewer: {
-    type: 'user',
-    username: chance.word(),
-    email: reviewer.email,
-    password: 'password',
-    admin: false,
-    id: reviewer.id,
-    firstName: reviewer.firstName,
-    lastName: reviewer.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.reviewer),
-    isConfirmed: true,
-    teams: [revTeamID],
-  },
-  author: {
-    type: 'user',
-    username: chance.word(),
-    email: author.email,
-    password: 'password',
-    admin: false,
-    id: author.id,
-    firstName: author.firstName,
-    lastName: author.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.author),
-    isConfirmed: true,
-  },
-  recReviewer: {
-    type: 'user',
-    username: chance.word(),
-    email: recReviewer.email,
-    password: 'password',
-    admin: false,
-    id: recReviewer.id,
-    firstName: recReviewer.firstName,
-    lastName: recReviewer.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.recReviewer),
-    isConfirmed: true,
-    teams: [revTeamID],
-  },
-  handlingEditor: {
-    type: 'user',
-    username: chance.word(),
-    email: handlingEditor.email,
-    password: 'password',
-    admin: false,
-    id: handlingEditor.id,
-    firstName: handlingEditor.firstName,
-    lastName: handlingEditor.lastName,
-    teams: [heTeamID],
-    save: jest.fn(() => users.handlingEditor),
-    editorInChief: false,
-    handlingEditor: true,
-    title: 'Mr',
-  },
-  editorInChief: {
-    type: 'user',
-    username: chance.word(),
-    email: chance.email(),
-    password: 'password',
-    admin: false,
-    id: chance.guid(),
-    firstName: chance.first(),
-    lastName: chance.last(),
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.editorInChief),
-    isConfirmed: false,
-    editorInChief: true,
-  },
-}
-
-module.exports = users
diff --git a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5afab0749a5c28f77bee042b30aeee90f179345
--- /dev/null
+++ b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js
@@ -0,0 +1,157 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
+
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendNotificationEmail: jest.fn(),
+}))
+const reqBody = {}
+
+const path = '../routes/fragments/patch'
+const route = {
+  path: '/api/collections/:collectionId/fragments/:fragmentId/submit',
+}
+describe('Patch fragments route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the parameters are correct', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { newVersion } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: newVersion.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the fragmentId does not match the collectionId', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { noParentFragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: noParentFragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Collection and fragment do not match.')
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const { user } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: 'invalid-id',
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Item not found')
+  })
+  it('should return an error when no HE recommendation exists', async () => {
+    const { user } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+    const { collection } = testFixtures.collections
+    // const collection = {
+    //   ...fCollection,
+    //   fragments: [...fCollection.fragments, '123'],
+    // }
+    fragment.recommendations.length = 0
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      'No Handling Editor request to revision has been found.',
+    )
+  })
+  it('should return an error when the request user is not the owner', async () => {
+    const { author } = testFixtures.users
+    const { newVersion } = testFixtures.fragments
+    const { collection } = testFixtures.collections
+
+    const res = await requests.sendRequest({
+      body,
+      userId: author.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: newVersion.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(403)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Unauthorized.')
+  })
+  it('should return an error when no previous version exists', async () => {
+    const { user } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+    const { collection } = testFixtures.collections
+    collection.fragments.length = 1
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('No previous version has been found.')
+  })
+})
diff --git a/packages/component-manuscript-manager/src/tests/fragments/post.test.js b/packages/component-manuscript-manager/src/tests/fragments/post.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..1da5c72f731cdc11512706c2709d014dd2ed1930
--- /dev/null
+++ b/packages/component-manuscript-manager/src/tests/fragments/post.test.js
@@ -0,0 +1,86 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
+
+const { Model, fixtures } = fixturesService
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendNotificationEmail: jest.fn(),
+}))
+const reqBody = {}
+
+const path = '../routes/fragments/post'
+const route = {
+  path: '/api/collections/:collectionId/fragments/:fragmentId/submit',
+}
+describe('Post fragments route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the parameters are correct', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the fragmentId does not match the collectionId', async () => {
+    const { user } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { noParentFragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: collection.id,
+        fragmentId: noParentFragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Collection and fragment do not match.')
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const { user } = testFixtures.users
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: user.id,
+      models,
+      route,
+      path,
+      params: {
+        collectionId: 'invalid-id',
+        fragmentId: fragment.id,
+      },
+    })
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Item not found')
+  })
+})
diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js
index 0e3c8013b981d741859bcf134f48db99e59f7e33..31755149c0d5f99a086d4360f32214953eac2765 100644
--- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js
+++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js
@@ -1,12 +1,12 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 jest.mock('pubsweet-component-mail-service', () => ({
   sendNotificationEmail: jest.fn(),
 }))
@@ -29,7 +29,7 @@ const reqBody = {
   recommendationType: 'review',
 }
 
-const path = '../../routes/fragmentsRecommendations/patch'
+const path = '../routes/fragmentsRecommendations/patch'
 const route = {
   path:
     '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId',
@@ -132,19 +132,20 @@ describe('Patch fragments recommendations route handler', () => {
     expect(data.error).toEqual('Recommendation not found.')
   })
   it('should return an error when the request user is not a reviewer', async () => {
-    const { author } = testFixtures.users
+    const { user } = testFixtures.users
     const { collection } = testFixtures.collections
     const { fragment } = testFixtures.fragments
 
     const res = await requests.sendRequest({
       body,
-      userId: author.id,
+      userId: user.id,
       models,
       route,
       path,
       params: {
         collectionId: collection.id,
         fragmentId: fragment.id,
+        recommendationId: fragment.recommendations[0].id,
       },
     })
 
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 364448eb2425e07b4f0df6ebcb9de904201d0489..c0c33bd5e78826f5f0ee2d02738b9430cb59b6f1 100644
--- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
+++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js
@@ -1,12 +1,12 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
 const cloneDeep = require('lodash/cloneDeep')
-const requests = require('./../helpers/requests')
+const fixturesService = require('pubsweet-component-fixture-service')
+const requests = require('../requests')
 
+const { Model, fixtures } = fixturesService
 const chance = new Chance()
 jest.mock('pubsweet-component-mail-service', () => ({
   sendNotificationEmail: jest.fn(),
@@ -29,7 +29,7 @@ const reqBody = {
   recommendationType: 'review',
 }
 
-const path = '../../routes/fragmentsRecommendations/post'
+const path = '../routes/fragmentsRecommendations/post'
 const route = {
   path: '/api/collections/:collectionId/fragments/:fragmentId/recommendations',
 }
@@ -57,7 +57,7 @@ describe('Post fragments recommendations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('Recommendation type is required.')
   })
-  it('should return success when the parameters are correct', async () => {
+  it('should return success when creating a recommendation as a reviewer', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
     const { fragment } = testFixtures.fragments
@@ -78,6 +78,27 @@ describe('Post fragments recommendations route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.userId).toEqual(reviewer.id)
   })
+  it('should return success when creating a recommendation as a HE', async () => {
+    const { handlingEditor } = testFixtures.users
+    const { collection } = testFixtures.collections
+    const { fragment } = testFixtures.fragments
+
+    const res = await requests.sendRequest({
+      body,
+      userId: handlingEditor.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(handlingEditor.id)
+  })
   it('should return an error when the fragmentId does not match the collectionId', async () => {
     const { reviewer } = testFixtures.users
     const { collection } = testFixtures.collections
diff --git a/packages/component-manuscript-manager/src/tests/helpers/Model.js b/packages/component-manuscript-manager/src/tests/helpers/Model.js
deleted file mode 100644
index 3e5a7364b9a7e907530d21ae9575644a69294dbc..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/helpers/Model.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// const fixtures = require('../fixtures/fixtures')
-
-const UserMock = require('../mocks/User')
-
-const notFoundError = new Error()
-notFoundError.name = 'NotFoundError'
-notFoundError.status = 404
-
-const build = fixtures => {
-  const models = {
-    User: {},
-    Collection: {
-      find: jest.fn(id => findMock(id, 'collections', fixtures)),
-    },
-    Fragment: {
-      find: jest.fn(id => findMock(id, 'fragments', fixtures)),
-    },
-    Team: {
-      find: jest.fn(id => findMock(id, 'teams', fixtures)),
-    },
-  }
-  UserMock.find = jest.fn(id => findMock(id, 'users', fixtures))
-  UserMock.all = jest.fn(() => Object.values(fixtures.users))
-  models.User = UserMock
-  return models
-}
-
-const findMock = (id, type, fixtures) => {
-  const foundObj = Object.values(fixtures[type]).find(
-    fixtureObj => fixtureObj.id === id,
-  )
-  if (foundObj === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundObj)
-}
-module.exports = { build }
diff --git a/packages/component-manuscript-manager/src/tests/mocks/User.js b/packages/component-manuscript-manager/src/tests/mocks/User.js
deleted file mode 100644
index b337c5f31ce5d71eaadd61ccb95fb1f83eef7d83..0000000000000000000000000000000000000000
--- a/packages/component-manuscript-manager/src/tests/mocks/User.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/* eslint-disable func-names-any */
-const uuid = require('uuid')
-
-function User(properties) {
-  this.type = 'user'
-  this.email = properties.email
-  this.username = properties.username
-  this.password = properties.password
-  this.roles = properties.roles
-  this.title = properties.title
-  this.affiliation = properties.affiliation
-  this.firstName = properties.firstName
-  this.lastName = properties.lastName
-  this.admin = properties.admin
-}
-
-User.prototype.save = jest.fn(function saveUser() {
-  this.id = uuid.v4()
-  return Promise.resolve(this)
-})
-
-module.exports = User
diff --git a/packages/component-manuscript-manager/src/tests/helpers/requests.js b/packages/component-manuscript-manager/src/tests/requests.js
similarity index 100%
rename from packages/component-manuscript-manager/src/tests/helpers/requests.js
rename to packages/component-manuscript-manager/src/tests/requests.js
diff --git a/packages/component-manuscript/src/components/Files.js b/packages/component-manuscript/src/components/Files.js
index 8a9c4057dcde3b1afd47989544bc7860ef6ac655..cb3315829631061e90b79fee25372d9c76507e11 100644
--- a/packages/component-manuscript/src/components/Files.js
+++ b/packages/component-manuscript/src/components/Files.js
@@ -5,7 +5,12 @@ import styled, { css } from 'styled-components'
 import { FileItem } from 'pubsweet-components-faraday/src/components/Files'
 
 const Files = ({
-  files: { manuscripts = [], coverLetter = [], supplementary = [] },
+  files: {
+    manuscripts = [],
+    coverLetter = [],
+    supplementary = [],
+    responseToReviewers = [],
+  },
 }) => (
   <Root>
     {!!manuscripts.length && (
@@ -41,6 +46,17 @@ const Files = ({
         ))}
       </Fragment>
     )}
+    {!!responseToReviewers.length && (
+      <Fragment>
+        <Header>
+          <span>Response to Reviewers</span>
+          <div />
+        </Header>
+        {responseToReviewers.map(file => (
+          <FileItem compact id={file.id} key={file.id} {...file} />
+        ))}
+      </Fragment>
+    )}
   </Root>
 )
 
@@ -60,9 +76,9 @@ const Header = styled.div`
   flex-direction: row;
 
   & span {
-    ${defaultText};
     margin-right: ${th('subGridUnit')};
     margin-top: ${th('subGridUnit')};
+    ${defaultText};
     text-transform: uppercase;
   }
 
diff --git a/packages/component-manuscript/src/components/ManuscriptDetails.js b/packages/component-manuscript/src/components/ManuscriptDetails.js
index df5e50181ab2ca80665c94378130903321deed7e..5c8545b657c15853562e220dfe4b00babe969607 100644
--- a/packages/component-manuscript/src/components/ManuscriptDetails.js
+++ b/packages/component-manuscript/src/components/ManuscriptDetails.js
@@ -7,8 +7,12 @@ import { Authors, Files } from './'
 import { Expandable } from '../molecules/'
 
 const ManuscriptDetails = ({
-  collection: { authors = [] },
-  fragment: { conflicts = {}, files = {}, metadata: { abstract = '' } },
+  fragment: {
+    files = {},
+    authors = [],
+    conflicts = {},
+    metadata: { abstract = '' },
+  },
 }) => (
   <Root>
     <Expandable label="Details" startExpanded>
diff --git a/packages/component-manuscript/src/components/ManuscriptHeader.js b/packages/component-manuscript/src/components/ManuscriptHeader.js
index caa38eb70f31265b39428a8227d36c032f68fd46..19735221355a3c92462c483faba1513c812d4403 100644
--- a/packages/component-manuscript/src/components/ManuscriptHeader.js
+++ b/packages/component-manuscript/src/components/ManuscriptHeader.js
@@ -30,10 +30,13 @@ const ManuscriptDetails = ({ version, project, journal }) => {
       <Row>
         <LeftDetails flex={3}>
           <StatusLabel>{mapStatusToLabel(project)}</StatusLabel>
-          <DateParser timestamp={get(version, 'submitted')}>
+          <DateParser
+            durationThreshold={0}
+            timestamp={get(version, 'submitted')}
+          >
             {(timestamp, days) => (
               <DateField>
-                {timestamp} ({days})
+                {timestamp} ({days} ago)
               </DateField>
             )}
           </DateParser>
diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js
index ea6b9532eb0b048a250ae750f0d4c6357c9faf44..ce3745ce0117574d4688b99c38a3480e9a14c183 100644
--- a/packages/component-manuscript/src/components/ManuscriptLayout.js
+++ b/packages/component-manuscript/src/components/ManuscriptLayout.js
@@ -26,8 +26,6 @@ const ManuscriptLayout = ({
   history,
   currentUser,
   editorInChief,
-  updateManuscript,
-  canSeeEditorialComments,
   editorialRecommendations,
   project = {},
   version = {},
@@ -53,16 +51,15 @@ const ManuscriptLayout = ({
             project={project}
             version={version}
           />
-          <ManuscriptDetails collection={project} fragment={version} />
+          <ManuscriptDetails fragment={version} />
           <ReviewsAndReports project={project} version={version} />
-          {canSeeEditorialComments &&
-            editorialRecommendations.length > 0 && (
-              <EditorialComments
-                editorInChief={editorInChief}
-                project={project}
-                recommendations={editorialRecommendations}
-              />
-            )}
+          {editorialRecommendations.length > 0 && (
+            <EditorialComments
+              editorInChief={editorInChief}
+              project={project}
+              recommendations={editorialRecommendations}
+            />
+          )}
         </Container>
         <SideBar flex={1}>
           <SideBarActions project={project} version={version} />
diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js
index df4b1eaac76cebaf609b4b5fe0d5768e25eb2979..a832022fdb722c6e9fe0dd077639eed86e449147 100644
--- a/packages/component-manuscript/src/components/ManuscriptPage.js
+++ b/packages/component-manuscript/src/components/ManuscriptPage.js
@@ -32,7 +32,6 @@ import {
 
 import ManuscriptLayout from './ManuscriptLayout'
 import { parseSearchParams, redirectToError } from './utils'
-import { canSeeEditorialComments } from '../../../component-faraday-selectors'
 
 export default compose(
   setDisplayName('ManuscriptPage'),
@@ -41,10 +40,7 @@ export default compose(
   withState('editorInChief', 'setEiC', 'N/A'),
   ConnectPage(({ match }) => [
     actions.getCollection({ id: match.params.project }),
-    actions.getFragment(
-      { id: match.params.project },
-      { id: match.params.version },
-    ),
+    actions.getFragments({ id: match.params.project }),
   ]),
   connect(
     (state, { match }) => ({
@@ -53,10 +49,9 @@ export default compose(
       hasManuscriptFailure: hasManuscriptFailure(state),
       version: selectFragment(state, match.params.version),
       project: selectCollection(state, match.params.project),
-      editorialRecommendations: selectEditorialRecommendations(state),
-      canSeeEditorialComments: canSeeEditorialComments(
+      editorialRecommendations: selectEditorialRecommendations(
         state,
-        match.params.project,
+        match.params.version,
       ),
     }),
     {
@@ -64,6 +59,7 @@ export default compose(
       getSignedUrl,
       clearCustomError,
       reviewerDecision,
+      getFragment: actions.getFragment,
       getCollection: actions.getCollection,
       updateVersion: actions.updateFragment,
     },
@@ -99,6 +95,7 @@ export default compose(
         replace,
         history,
         location,
+        getFragment,
         getCollection,
         reviewerDecision,
         setEditorInChief,
@@ -111,11 +108,15 @@ export default compose(
       }
 
       const collectionId = match.params.project
+      const fragmentId = match.params.version
       const { agree, invitationId } = parseSearchParams(location.search)
       if (agree === 'true') {
         replace(location.pathname)
-        reviewerDecision(invitationId, collectionId, true)
-          .then(() => getCollection({ id: match.params.project }))
+        reviewerDecision(invitationId, collectionId, fragmentId, true)
+          .then(() => {
+            getCollection({ id: collectionId })
+            getFragment({ id: collectionId }, { id: fragmentId })
+          })
           .catch(redirectToError(replace))
       }
 
diff --git a/packages/component-manuscript/src/components/ReviewReportCard.js b/packages/component-manuscript/src/components/ReviewReportCard.js
index ff357bec2509e18d1a2d164f1f8a26945ec0f8cf..9617ac681434f4718e7494dd11a2fba86f679724 100644
--- a/packages/component-manuscript/src/components/ReviewReportCard.js
+++ b/packages/component-manuscript/src/components/ReviewReportCard.js
@@ -12,6 +12,7 @@ import { ShowMore } from './'
 const ReviewReportCard = ({
   i = 0,
   report = {},
+  showBorder = false,
   journal: { recommendations },
 }) => {
   const hasReviewer = !isEmpty(get(report, 'user'))
@@ -24,7 +25,7 @@ const ReviewReportCard = ({
   )
 
   return (
-    <Root hasReviewer={hasReviewer}>
+    <Root showBorder={showBorder}>
       {hasReviewer && (
         <Row>
           <Text>
@@ -117,7 +118,7 @@ const Root = styled.div`
   margin: auto;
   border: none;
   padding: 0;
-  ${({ hasReviewer }) => (hasReviewer ? cardStyle : null)};
+  ${({ showBorder }) => (showBorder ? cardStyle : null)};
 `
 const Text = styled.div`
   ${defaultText};
diff --git a/packages/component-manuscript/src/components/ReviewReportsList.js b/packages/component-manuscript/src/components/ReviewReportsList.js
new file mode 100644
index 0000000000000000000000000000000000000000..ee47831817b963a6dc04a3d0361ffed6d2a98c64
--- /dev/null
+++ b/packages/component-manuscript/src/components/ReviewReportsList.js
@@ -0,0 +1,21 @@
+import React, { Fragment } from 'react'
+import { ReviewReportCard } from './'
+
+const ReviewReportsList = ({ recommendations, showBorder }) => (
+  <Fragment>
+    {recommendations.length ? (
+      recommendations.map((r, index) => (
+        <ReviewReportCard
+          i={index + 1}
+          key={r.id}
+          report={r}
+          showBorder={showBorder}
+        />
+      ))
+    ) : (
+      <div>No reports submitted yet.</div>
+    )}
+  </Fragment>
+)
+
+export default ReviewReportsList
diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js
index 946bdb61278e7c2f535cb63a450b158beff505c8..2da95ea14fcfeb5d219f7f47a57829a27facb1c6 100644
--- a/packages/component-manuscript/src/components/ReviewerReportForm.js
+++ b/packages/component-manuscript/src/components/ReviewerReportForm.js
@@ -25,7 +25,6 @@ import AutosaveIndicator from 'pubsweet-component-wizard/src/components/Autosave
 import {
   uploadFile,
   deleteFile,
-  getFileError,
   getSignedUrl,
   getRequestStatus,
 } from 'pubsweet-components-faraday/src/redux/files'
@@ -38,9 +37,8 @@ import {
   withModal,
   ConfirmationModal,
 } from 'pubsweet-component-modal/src/components'
+
 import {
-  selectError,
-  selectFetching,
   createRecommendation,
   updateRecommendation,
 } from 'pubsweet-components-faraday/src/redux/recommendations'
@@ -54,12 +52,13 @@ import {
 const guidelinesLink =
   'https://about.hindawi.com/authors/peer-review-at-hindawi/'
 
+const TextAreaField = input => <Textarea {...input} height={70} rows={6} />
+
 const ReviewerReportForm = ({
   addFile,
   fileError,
   removeFile,
   changeField,
-  errorRequest,
   isSubmitting,
   handleSubmit,
   fileFetching,
@@ -108,38 +107,32 @@ const ReviewerReportForm = ({
       )}
     </Row>
     <Row>
-      <FullWidth>
+      <FullWidth className="full-width">
         <ValidatedField
-          component={input => (
-            <Textarea
-              {...input}
-              hasError={input.validationStatus === 'error'}
-              onChange={e => changeField('public', e.target.value)}
-              readOnly={fileFetching.review}
-              rows={6}
-            />
-          )}
+          component={TextAreaField}
           name="public"
-          readOnly={fileFetching.review}
           validate={isEmpty(formValues.files) ? [required] : []}
         />
       </FullWidth>
     </Row>
     {formValues.files && (
-      <Row left>
-        {formValues.files.map(file => (
-          <FileItem
-            compact
-            id={file.id}
-            key={file.id}
-            {...file}
-            removeFile={removeFile}
-          />
-        ))}
-      </Row>
+      <Fragment>
+        <Row left>
+          {formValues.files.map(file => (
+            <FileItem
+              compact
+              id={file.id}
+              key={file.id}
+              {...file}
+              removeFile={removeFile}
+            />
+          ))}
+        </Row>
+      </Fragment>
     )}
     {formValues.hasConfidential ? (
       <Fragment>
+        <Spacing />
         <Row>
           <Label>
             Note for the editorial team <i>Not shared with the author</i>
@@ -154,17 +147,8 @@ const ReviewerReportForm = ({
         <Row>
           <FullWidth>
             <ValidatedField
-              component={input => (
-                <Textarea
-                  {...input}
-                  hasError={input.validationStatus === 'error'}
-                  onChange={e => changeField('confidential', e.target.value)}
-                  readOnly={fileFetching.review}
-                  rows={6}
-                />
-              )}
+              component={TextAreaField}
               name="confidential"
-              readOnly={fileFetching.review}
               validate={[required]}
             />
           </FullWidth>
@@ -184,11 +168,6 @@ const ReviewerReportForm = ({
         <ErrorText>{fileError}</ErrorText>
       </Row>
     )}
-    {errorRequest && (
-      <Row>
-        <ErrorText>{errorRequest}</ErrorText>
-      </Row>
-    )}
     <Row>
       <ActionButton onClick={handleSubmit}> Submit report </ActionButton>
       <AutosaveIndicator
@@ -201,8 +180,7 @@ const ReviewerReportForm = ({
 
 const ModalWrapper = compose(
   connect(state => ({
-    modalError: selectError(state),
-    fetching: selectFetching(state),
+    fetching: false,
   })),
 )(({ fetching, ...rest }) => (
   <ConfirmationModal {...rest} isFetching={fetching} />
@@ -212,8 +190,6 @@ export default compose(
   withJournal,
   connect(
     state => ({
-      fileError: getFileError(state),
-      errorRequest: selectError(state),
       fileFetching: getRequestStatus(state),
       formValues: getFormValues('reviewerReport')(state),
       isSubmitting: isSubmitting('reviewerReport')(state),
@@ -267,7 +243,7 @@ export default compose(
     form: 'reviewerReport',
     onChange: onReviewChange,
     onSubmit: onReviewSubmit,
-    enableReinitialize: true,
+    enableReinitialize: false,
     keepDirtyOnReinitialize: true,
   }),
 )(ReviewerReportForm)
@@ -328,7 +304,7 @@ const Textarea = styled.textarea`
 
 const Spacing = styled.div`
   flex: 1;
-  margin-top: ${th('gridUnit')};
+  margin-top: calc(${th('gridUnit')} / 2);
 `
 
 const FullWidth = styled.div`
@@ -347,6 +323,10 @@ const Row = styled.div`
   flex: 1;
   flex-wrap: wrap;
   justify-content: ${({ left }) => (left ? 'left' : 'space-between')};
+
+  div[role='alert'] {
+    margin-top: 0;
+  }
 `
 
 const ActionButton = styled(Button)`
diff --git a/packages/component-manuscript/src/components/ReviewsAndReports.js b/packages/component-manuscript/src/components/ReviewsAndReports.js
index b498bf92a2eab8ad79f3071057a1adf539c29efd..65b65ac5911654faa93b86737e6a840b0c5285de 100644
--- a/packages/component-manuscript/src/components/ReviewsAndReports.js
+++ b/packages/component-manuscript/src/components/ReviewsAndReports.js
@@ -1,8 +1,9 @@
 import React, { Fragment } from 'react'
-import { head } from 'lodash'
 import { th } from '@pubsweet/ui'
+import { head, get } from 'lodash'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
+import { withRouter } from 'react-router-dom'
 import { compose, withHandlers, lifecycle, withProps } from 'recompose'
 import { ReviewerBreakdown } from 'pubsweet-components-faraday/src/components/Invitations'
 import ReviewersDetailsList from 'pubsweet-components-faraday/src/components/Reviewers/ReviewersDetailsList'
@@ -12,11 +13,14 @@ import {
   getCollectionReviewers,
   selectFetchingReviewers,
 } from 'pubsweet-components-faraday/src/redux/reviewers'
-import { selectRecommendations } from 'pubsweet-components-faraday/src/redux/recommendations'
+import { selectReviewRecommendations } from 'pubsweet-components-faraday/src/redux/recommendations'
+import {
+  canSeeReviewersReports,
+  currentUserIsAuthor,
+} from 'pubsweet-component-faraday-selectors'
 
 import { Tabs, Expandable } from '../molecules'
-import { ReviewReportCard, ReviewerReportForm } from './'
-import { canSeeReviewersReports } from '../../../component-faraday-selectors'
+import { ReviewReportCard, ReviewerReportForm, ReviewReportsList } from './'
 
 const getTabSections = (collectionId, reviewers, recommendations = []) => [
   {
@@ -29,35 +33,24 @@ const getTabSections = (collectionId, reviewers, recommendations = []) => [
   {
     key: 2,
     label: 'Reviewer Reports',
-    content: (
-      <Fragment>
-        {recommendations.length ? (
-          recommendations.map((r, index) => (
-            <ReviewReportCard i={index + 1} key={r.id} report={r} />
-          ))
-        ) : (
-          <div>No reports submitted yet.</div>
-        )}
-      </Fragment>
-    ),
+    content: <ReviewReportsList recommendations={recommendations} showBorder />,
   },
 ]
 
 const ReviewsAndReports = ({
-  report,
   project,
   version,
+  isAuthor,
   isReviewer,
   mappedReviewers,
   mappedRecommendations,
   canSeeReviewersReports,
-  review = {},
-  reviewers = [],
+  reviewerRecommendation,
   recommendations = [],
 }) => (
   <Fragment>
     {canSeeReviewersReports && (
-      <Root>
+      <Root id="reviews-and-reports">
         <Expandable
           label="Reviewers & Reports"
           rightHTML={
@@ -80,38 +73,48 @@ const ReviewsAndReports = ({
       </Root>
     )}
     {isReviewer && (
-      <Root id="review-report">
+      <Root id="reviewer-report">
         <Expandable label="Your Report" startExpanded>
-          {report ? (
-            <ReviewReportCard report={report} />
+          {get(reviewerRecommendation, 'submittedOn') ? (
+            <ReviewReportCard report={reviewerRecommendation} />
           ) : (
             <ReviewerReportForm
               modalKey={`review-${project.id}`}
               project={project}
-              review={review}
+              review={reviewerRecommendation}
               version={version}
             />
           )}
         </Expandable>
       </Root>
     )}
+    {isAuthor &&
+      !!recommendations.length && (
+        <Root id="review-reports">
+          <Expandable label="Reports" startExpanded>
+            <ReviewReportsList recommendations={recommendations} showBorder />
+          </Expandable>
+        </Root>
+      )}
   </Fragment>
 )
 
 export default compose(
+  withRouter,
   connect(
-    (state, { project }) => ({
+    (state, { project, version }) => ({
       reviewers: selectReviewers(state),
-      recommendations: selectRecommendations(state),
       fetchingReviewers: selectFetchingReviewers(state),
-      isReviewer: currentUserIsReviewer(state, project.id),
+      isReviewer: currentUserIsReviewer(state, version.id),
+      isAuthor: currentUserIsAuthor(state, version.id),
+      recommendations: selectReviewRecommendations(state, version.id),
       canSeeReviewersReports: canSeeReviewersReports(state, project.id),
     }),
     { getCollectionReviewers },
   ),
   withHandlers({
-    getReviewers: ({ project, getCollectionReviewers }) => () => {
-      getCollectionReviewers(project.id)
+    getReviewers: ({ project, version, getCollectionReviewers }) => () => {
+      getCollectionReviewers(project.id, version.id)
     },
     mappedRecommendations: ({ recommendations, reviewers }) => () =>
       recommendations.filter(r => r.submittedOn).map(r => ({
@@ -127,12 +130,21 @@ export default compose(
   withProps(({ recommendations = [] }) => ({
     review: head(recommendations),
     report: head(recommendations.filter(r => r.submittedOn)),
+    reviewerRecommendation: head(recommendations),
   })),
   lifecycle({
     componentDidMount() {
       const { getReviewers, canSeeReviewersReports } = this.props
       canSeeReviewersReports && getReviewers()
     },
+    componentWillReceiveProps(nextProps) {
+      const { match, canSeeReviewersReports, getReviewers } = this.props
+      const version = get(match, 'params.version')
+      const nextVersion = get(nextProps, 'match.params.version')
+      if (version !== nextVersion) {
+        canSeeReviewersReports && getReviewers()
+      }
+    },
   }),
 )(ReviewsAndReports)
 
diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js
index 01f60511fa7ccfb30a467976c768c876c80e4fe2..76a9369e78aecb0fca7ef61a44ae14629345773a 100644
--- a/packages/component-manuscript/src/components/SideBarActions.js
+++ b/packages/component-manuscript/src/components/SideBarActions.js
@@ -2,13 +2,19 @@ import React from 'react'
 import { compose } from 'recompose'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
-import { th, Icon } from '@pubsweet/ui'
+import { th } from '@pubsweet/ui-toolkit'
+import { Icon, Button } from '@pubsweet/ui'
+import { withRouter } from 'react-router-dom'
 import ZipFiles from 'pubsweet-components-faraday/src/components/Files/ZipFiles'
 import {
   Decision,
   Recommendation,
 } from 'pubsweet-components-faraday/src/components'
+
+import { createRevision } from 'pubsweet-component-wizard/src/redux/conversion'
+
 import {
+  canMakeRevision,
   canMakeDecision,
   canMakeRecommendation,
 } from '../../../component-faraday-selectors'
@@ -16,10 +22,15 @@ import {
 const SideBarActions = ({
   project,
   version,
+  createRevision,
+  canMakeRevision,
   canMakeDecision,
   canMakeRecommendation,
 }) => (
   <Root>
+    {canMakeRevision && (
+      <DecisionButton onClick={createRevision}>Submit revision</DecisionButton>
+    )}
     {canMakeDecision && (
       <Decision
         collectionId={project.id}
@@ -48,10 +59,17 @@ const SideBarActions = ({
 )
 
 export default compose(
-  connect((state, { project }) => ({
-    canMakeDecision: canMakeDecision(state, project),
-    canMakeRecommendation: canMakeRecommendation(state, project),
-  })),
+  withRouter,
+  connect(
+    (state, { project, version }) => ({
+      canMakeDecision: canMakeDecision(state, project),
+      canMakeRevision: canMakeRevision(state, project, version),
+      canMakeRecommendation: canMakeRecommendation(state, project),
+    }),
+    (dispatch, { project, version, history }) => ({
+      createRevision: () => dispatch(createRevision(project, version, history)),
+    }),
+  ),
 )(SideBarActions)
 
 // #region styled-components
@@ -68,4 +86,17 @@ const ClickableIcon = styled.div`
     opacity: 0.7;
   }
 `
+
+const DecisionButton = styled(Button)`
+  align-items: center;
+  background-color: ${th('colorPrimary')};
+  color: ${th('colorTextReverse')};
+  display: flex;
+  font-family: ${th('fontReading')};
+  font-size: ${th('fontSizeBaseSmall')};
+  height: calc(${th('subGridUnit')} * 5);
+  padding: calc(${th('subGridUnit')} / 2) ${th('subGridUnit')};
+  text-align: center;
+  white-space: nowrap;
+`
 // #endregion
diff --git a/packages/component-manuscript/src/components/index.js b/packages/component-manuscript/src/components/index.js
index 3509c110448651a67ba4e2659d1bf1bb30abf404..dadf8d4e6ffd637ee1faed3f90c00d99978b5c20 100644
--- a/packages/component-manuscript/src/components/index.js
+++ b/packages/component-manuscript/src/components/index.js
@@ -13,3 +13,4 @@ export { default as ManuscriptVersion } from './ManuscriptVersion'
 export { default as ReviewsAndReports } from './ReviewsAndReports'
 export { default as EditorialComments } from './EditorialComments'
 export { default as ReviewerReportForm } from './ReviewerReportForm'
+export { default as ReviewReportsList } from './ReviewReportsList'
diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js
index d9785efb43eeaa1501c8f74b40d292c0d308a4c0..5b2221a561a185987d824271e9dac2fc7fa6b926 100644
--- a/packages/component-manuscript/src/components/utils.js
+++ b/packages/component-manuscript/src/components/utils.js
@@ -1,6 +1,8 @@
 import moment from 'moment'
-import { get, find, capitalize, omit, isEmpty, isEqual, debounce } from 'lodash'
+import { get, find, capitalize, omit, isEmpty, debounce } from 'lodash'
 
+import { actions } from 'pubsweet-client/src'
+import { change as changeForm } from 'redux-form'
 import {
   autosaveRequest,
   autosaveSuccess,
@@ -72,9 +74,7 @@ export const parseSearchParams = url => {
 export const parseVersionOptions = (fragments = []) =>
   fragments.map(f => ({
     value: f.id,
-    label: `Version ${f.version} - updated on ${moment(f.submitted).format(
-      'DD.MM.YYYY',
-    )}`,
+    label: `Version ${f.version}`,
   }))
 
 const alreadyAnswered = `You have already answered this invitation.`
@@ -88,7 +88,7 @@ export const redirectToError = redirectFn => err => {
 }
 
 export const parseReviewResponseToForm = (review = {}) => {
-  if (isEmpty(review)) return null
+  if (isEmpty(review)) return {}
   const comments = review.comments || []
   const publicComment = comments.find(c => c.public)
   const privateComment = comments.find(c => !c.public)
@@ -102,7 +102,7 @@ export const parseReviewResponseToForm = (review = {}) => {
 }
 
 export const parseReviewRequest = (review = {}) => {
-  if (isEmpty(review)) return null
+  if (isEmpty(review)) return {}
   const comments = [
     {
       public: true,
@@ -135,23 +135,23 @@ const onChange = (
   values,
   dispatch,
   { project, version, createRecommendation, updateRecommendation },
-  previousValues,
 ) => {
   const newValues = parseReviewRequest(values)
-  const prevValues = parseReviewRequest(previousValues)
-
-  if (!isEqual(newValues, prevValues)) {
-    dispatch(autosaveRequest())
-    if (newValues.id) {
-      updateRecommendation(project.id, version.id, newValues)
-        .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
-        .catch(e => dispatch(autosaveFailure(e)))
-    } else {
-      createRecommendation(project.id, version.id, newValues)
-        .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
-        .catch(e => dispatch(autosaveFailure(e)))
-    }
+  // if (!isEqual(newValues, prevValues)) {
+  dispatch(autosaveRequest())
+  if (newValues.id) {
+    updateRecommendation(project.id, version.id, newValues)
+      .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
+      .catch(e => dispatch(autosaveFailure(e)))
+  } else {
+    createRecommendation(project.id, version.id, newValues)
+      .then(r => {
+        dispatch(changeForm('reviewerReport', 'id', r.id))
+        return dispatch(autosaveSuccess(get(r, 'updatedOn')))
+      })
+      .catch(e => dispatch(autosaveFailure(e)))
   }
+  // }
 }
 
 export const onReviewChange = debounce(onChange, 1000, { maxWait: 5000 })
@@ -178,7 +178,10 @@ export const onReviewSubmit = (
       dispatch(autosaveRequest())
       updateRecommendation(project.id, version.id, newValues)
         .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn'))))
-        .then(hideModal)
+        .then(() => {
+          dispatch(actions.getFragments({ id: project.id }))
+          hideModal()
+        })
     },
     onCancel: hideModal,
   })
diff --git a/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js b/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js
index 79532b138eb7b9aefda592ea107e58b6273a0b22..65e3645df6b138f53668f496428c4e0c50347160 100644
--- a/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js
+++ b/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js
@@ -64,7 +64,7 @@ const AuthorsWithTooltip = ({
     {authors.map(
       (
         {
-          userId,
+          id,
           isSubmitting,
           isCorresponding,
           email = '',
@@ -83,7 +83,7 @@ const AuthorsWithTooltip = ({
           email={email}
           isCorresponding={isCorresponding}
           isSubmitting={isSubmitting}
-          key={userId}
+          key={id}
         >
           <DefaultComponent
             arr={arr}
diff --git a/packages/component-modal/src/components/withModal.js b/packages/component-modal/src/components/withModal.js
index 18f16ebcb917ffc7242b4242c6676fb8a127c771..35bb923623393be380a2af970af01ada1733a88e 100644
--- a/packages/component-modal/src/components/withModal.js
+++ b/packages/component-modal/src/components/withModal.js
@@ -23,12 +23,13 @@ const withModal = mapperFn => BaseComponent =>
   compose(connect(mapState, mapDispatch))(baseProps => {
     const { modalComponent: Component, overlayColor } = mapperFn(baseProps)
     const {
+      modalKey,
+      showModal,
       hideModal,
       modalProps,
       modalError,
+      setModalError,
       modalsVisibility,
-      modalKey,
-      showModal,
       ...rest
     } = baseProps
     return (
diff --git a/packages/component-user-manager/config/authsome-mode.js b/packages/component-user-manager/config/authsome-mode.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c663beae1962d9fc13141f8439dc0a84214ae08
--- /dev/null
+++ b/packages/component-user-manager/config/authsome-mode.js
@@ -0,0 +1,3 @@
+const authsomeMode = require('xpub-faraday/config/authsome-mode')
+
+module.exports = authsomeMode
diff --git a/packages/component-user-manager/config/default.js b/packages/component-user-manager/config/default.js
index 350b9de7761a91153fff07c37b29603005d36c33..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f 100644
--- a/packages/component-user-manager/config/default.js
+++ b/packages/component-user-manager/config/default.js
@@ -1,19 +1,3 @@
-module.exports = {
-  mailer: {
-    from: 'test@example.com',
-  },
-  'invite-reset-password': {
-    url:
-      process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL ||
-      'http://localhost:3000/invite',
-  },
-  roles: {
-    global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
-    collection: ['handlingEditor', 'reviewer'],
-    inviteRights: {
-      admin: ['admin', 'editorInChief', 'author'],
-      editorInChief: ['handlingEditor'],
-      handlingEditor: ['reviewer'],
-    },
-  },
-}
+const defaultConfig = require('xpub-faraday/config/default')
+
+module.exports = defaultConfig
diff --git a/packages/component-user-manager/config/test.js b/packages/component-user-manager/config/test.js
index a1e52fc0b730d1b5e7836ac08eb6b0188b3c13ae..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f 100644
--- a/packages/component-user-manager/config/test.js
+++ b/packages/component-user-manager/config/test.js
@@ -1,20 +1,3 @@
-module.exports = {
-  mailer: {
-    from: 'test@example.com',
-  },
-  'invite-reset-password': {
-    url:
-      process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL ||
-      'http://localhost:3000/invite',
-  },
-  roles: {
-    global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
-    collection: ['handlingEditor', 'reviewer', 'author'],
-    inviteRights: {
-      admin: ['admin', 'editorInChief', 'author', 'handlingEditor', 'author'],
-      editorInChief: ['handlingEditor'],
-      handlingEditor: ['reviewer'],
-      author: ['author'],
-    },
-  },
-}
+const defaultConfig = require('xpub-faraday/config/default')
+
+module.exports = defaultConfig
diff --git a/packages/component-user-manager/index.js b/packages/component-user-manager/index.js
index 4ba897be673285e0582abfcc2c2fabd03cc58373..86e7a8ab8fe92e4398708bcf9f077fe5cd9e804e 100644
--- a/packages/component-user-manager/index.js
+++ b/packages/component-user-manager/index.js
@@ -1,6 +1,6 @@
 module.exports = {
   backend: () => app => {
     require('./src/Users')(app)
-    require('./src/CollectionsUsers')(app)
+    require('./src/FragmentsUsers')(app)
   },
 }
diff --git a/packages/component-user-manager/package.json b/packages/component-user-manager/package.json
index 0d58cca77bc2e2b3c6162fae78e292c2d0a60e08..c65b0553de09100b8c477dfdab49531406d95536 100644
--- a/packages/component-user-manager/package.json
+++ b/packages/component-user-manager/package.json
@@ -25,7 +25,8 @@
   "peerDependencies": {
     "@pubsweet/logger": "^0.0.1",
     "pubsweet-component-mail-service": "0.0.1",
-    "pubsweet-server": "^1.0.1"
+    "pubsweet-server": "^1.0.1",
+    "pubsweet-component-helper-service": "0.0.1"
   },
   "devDependencies": {
     "apidoc": "^0.17.6",
diff --git a/packages/component-user-manager/src/CollectionsUsers.js b/packages/component-user-manager/src/FragmentsUsers.js
similarity index 51%
rename from packages/component-user-manager/src/CollectionsUsers.js
rename to packages/component-user-manager/src/FragmentsUsers.js
index 5ee6f750ff770c0b83fbaad57537206c54aa82f1..443397a6f81cb1d10c9696fb5a9d937becc2b1cb 100644
--- a/packages/component-user-manager/src/CollectionsUsers.js
+++ b/packages/component-user-manager/src/FragmentsUsers.js
@@ -1,16 +1,17 @@
 const bodyParser = require('body-parser')
 
-const CollectionsUsers = app => {
+const FragmentsUsers = app => {
   app.use(bodyParser.json())
-  const basePath = '/api/collections/:collectionId/users'
-  const routePath = './routes/collectionsUsers'
+  const basePath = '/api/collections/:collectionId/fragments/:fragmentId/users'
+  const routePath = './routes/fragmentsUsers'
   const authBearer = app.locals.passport.authenticate('bearer', {
     session: false,
   })
   /**
-   * @api {post} /api/collections/:collectionId/users Add a user to a collection
-   * @apiGroup CollectionsUsers
+   * @api {post} /api/collections/:collectionId/fragments/:fragmentId/users Add a user to a fragment
+   * @apiGroup FragmentsUsers
    * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
    * @apiParamExample {json} Body
    *    {
    *      "email": "email@example.com",
@@ -22,19 +23,12 @@ const CollectionsUsers = app => {
    *    HTTP/1.1 200 OK
    *    {
    *      "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
-   *      "type": "user",
-   *      "admin": false,
    *      "email": "email@example.com",
-   *      "teams": [
-   *        "c576695a-7cda-4e27-8e9c-31f3a0e9d592"
-   *      ],
-   *      "username": "email@example.com",
-   *      "fragments": [],
-   *      "collections": [],
-   *      "isConfirmed": false,
-   *      "editorInChief": false,
-   *      "handlingEditor": false,
-   *      "passwordResetToken": "04590a2b7f6c1f37cb84881d529e516fa6fc309c205a07f1341b2bfaa6f2b46c"
+   *      "firstName": "John",
+   *      "lastName": "Smith",
+   *      "affiliation": "MIT",
+   *      "isSubmitting": true,
+   *      "isCorresponding": false
    *    }
    * @apiErrorExample {json} Invite user errors
    *    HTTP/1.1 400 Bad Request
@@ -46,9 +40,10 @@ const CollectionsUsers = app => {
     require(`${routePath}/post`)(app.locals.models),
   )
   /**
-   * @api {delete} /api/collections/:collectionId/user/:userId Delete user from collection
-   * @apiGroup CollectionsUsers
+   * @api {delete} /api/collections/:collectionId/fragments/:fragmentId/user/:userId Delete a user from a fragment
+   * @apiGroup FragmentsUsers
    * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
    * @apiParam {userId} userId User id
    * @apiSuccessExample {json} Success
    *    HTTP/1.1 200 {}
@@ -62,9 +57,10 @@ const CollectionsUsers = app => {
     require(`${routePath}/delete`)(app.locals.models),
   )
   /**
-   * @api {get} /api/collections/:collectionId/users List collections users
-   * @apiGroup CollectionsUsers
+   * @api {get} /api/collections/:collectionId/fragments/:fragmentId/users List fragment users
+   * @apiGroup FragmentsUsers
    * @apiParam {collectionId} collectionId Collection id
+   * @apiParam {fragmentId} fragmentId Fragment id
    * @apiSuccessExample {json} Success
    *    HTTP/1.1 200 OK
    *    [{
@@ -107,62 +103,6 @@ const CollectionsUsers = app => {
    *    HTTP/1.1 404 Not Found
    */
   app.get(basePath, authBearer, require(`${routePath}/get`)(app.locals.models))
-  /**
-   * @api {patch} /api/collections/:collectionId/users/:userId Update a user on a collection
-   * @apiGroup CollectionsUsers
-   * @apiParam {collectionId} collectionId Collection id
-   * @apiParam {userId} userId User id
-   * @apiParamExample {json} Body
-   *    {
-   *      "isSubmitting": false,
-   *      "isCorresponding": true,
-   *      "firstName": "John",
-   *      "lastName": "Smith",
-   *      "affiliation": "UCLA"
-   *    }
-   * @apiSuccessExample {json} Success
-   *    HTTP/1.1 200 OK
-   *    {
-   *      "id": "7e8a77f9-8e5c-4fa3-b717-8df9932df128",
-   *      "type": "collection",
-   *      "owners": [
-   *        {
-   *           "id": "69ac1ee9-08a8-4ee6-a57c-c6c8be8d3c4f",
-   *           "username": "admin"
-   *        }
-   *      ],
-   *      "authors": [
-   *        {
-   *          "userId": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
-   *          "isSubmitting": false,
-   *          "isCorresponding": true
-   *        }
-   *      ],
-   *      "created": 1522829424474,
-   *      "customId": "9424466",
-   *      "fragments": [
-   *        "c35d0bd8-be03-4c16-b869-bd69796c5a21"
-   *      ],
-   *      "invitations": [
-   *        {
-   *          "id": "9043a836-0d49-4b8d-be0b-df39071b5c57",
-   *          "hasAnswer": false,
-   *          "timestamp": 1522831123430,
-   *          "isAccepted": false
-   *        },
-   *      ]
-   *    }
-   * @apiErrorExample {json} Update invitations errors
-   *    HTTP/1.1 403 Forbidden
-   *    HTTP/1.1 400 Bad Request
-   *    HTTP/1.1 404 Not Found
-   *    HTTP/1.1 500 Internal Server Error
-   */
-  app.patch(
-    `${basePath}/:userId`,
-    authBearer,
-    require(`${routePath}/patch`)(app.locals.models),
-  )
 }
 
-module.exports = CollectionsUsers
+module.exports = FragmentsUsers
diff --git a/packages/component-user-manager/src/Users.js b/packages/component-user-manager/src/Users.js
index 3fdfb1b60b5d7428daa93b84599c5f9024d18f12..1c5c159ddfd35f3d654dc3e593c1c74acaed283e 100644
--- a/packages/component-user-manager/src/Users.js
+++ b/packages/component-user-manager/src/Users.js
@@ -30,7 +30,7 @@ const Invite = app => {
    *      "editorInChief": false,
    *      "handlingEditor": false
    *    }
-   * @apiErrorExample {json} Invite user errors
+   * @apiErrorExample {json} Reset password errors
    *    HTTP/1.1 400 Bad Request
    *    HTTP/1.1 404 Not Found
    */
@@ -38,6 +38,56 @@ const Invite = app => {
     '/api/users/reset-password',
     require('./routes/users/resetPassword')(app.locals.models),
   )
+  /**
+   * @api {post} /api/users/confirm Confirm user
+   * @apiGroup Users
+   * @apiParamExample {json} Body
+   *    {
+   *      "userId": "valid-user-id",
+   *      "confirmationToken": "12312321"
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *    {
+   *      "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
+   *      "type": "user",
+   *      "admin": false,
+   *      "email": "email@example.com",
+   *      "teams": [],
+   *      "username": "email@example.com",
+   *      "fragments": [],
+   *      "collections": [],
+   *      "isConfirmed": true,
+   *      "editorInChief": false,
+   *      "handlingEditor": false
+   *    }
+   * @apiErrorExample {json} Forgot Password errors
+   *    HTTP/1.1 400 Bad Request
+   *    HTTP/1.1 404 Not Found
+   */
+  app.post(
+    '/api/users/confirm',
+    require('./routes/users/confirm')(app.locals.models),
+  )
+  /**
+   * @api {post} /api/users/forgot-password Forgot password
+   * @apiGroup Users
+   * @apiParamExample {json} Body
+   *    {
+   *      "email": "email@example.com",
+   *    }
+   * @apiSuccessExample {json} Success
+   *    HTTP/1.1 200 OK
+   *    {
+   *      "message": "A password reset email has been sent to email@example.com"
+   *    }
+   * @apiErrorExample {json} Forgot Password errors
+   *    HTTP/1.1 400 Bad Request
+   */
+  app.post(
+    '/api/users/forgot-password',
+    require('./routes/users/forgotPassword')(app.locals.models),
+  )
 }
 
 module.exports = Invite
diff --git a/packages/component-user-manager/src/helpers/Collection.js b/packages/component-user-manager/src/helpers/Collection.js
deleted file mode 100644
index c2079945210b19e05c5db988777b7e29d22c70b5..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/Collection.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const mailService = require('pubsweet-component-mail-service')
-const logger = require('@pubsweet/logger')
-
-module.exports = {
-  addAuthor: async (
-    collection,
-    user,
-    res,
-    url,
-    isSubmitting,
-    isCorresponding,
-  ) => {
-    collection.authors = collection.authors || []
-    const author = {
-      userId: user.id,
-      firstName: user.firstName || '',
-      lastName: user.lastName || '',
-      email: user.email,
-      title: user.title || '',
-      affiliation: user.affiliation || '',
-      isSubmitting,
-      isCorresponding,
-    }
-    collection.authors.push(author)
-    await collection.save()
-    if (collection.owners.includes(user.id)) {
-      return res.status(200).json(user)
-    }
-    try {
-      mailService.sendSimpleEmail({
-        toEmail: user.email,
-        user,
-        emailType: 'add-author',
-        dashboardUrl: url,
-      })
-
-      return res.status(200).json(user)
-    } catch (e) {
-      logger.error(e)
-      return res.status(500).json({ error: 'Email could not be sent.' })
-    }
-  },
-}
diff --git a/packages/component-user-manager/src/helpers/Team.js b/packages/component-user-manager/src/helpers/Team.js
deleted file mode 100644
index 3b700c4436f1306a7b41b07ab4b9ded4aa566944..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/Team.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const logger = require('@pubsweet/logger')
-// const get = require('lodash/get')
-
-const createNewTeam = async (collectionId, role, userId, TeamModel) => {
-  let permissions, group, name
-  switch (role) {
-    case 'handlingEditor':
-      permissions = 'handlingEditor'
-      group = 'handlingEditor'
-      name = 'Handling Editor'
-      break
-    case 'reviewer':
-      permissions = 'reviewer'
-      group = 'reviewer'
-      name = 'Reviewer'
-      break
-    case 'author':
-      permissions = 'author'
-      group = 'author'
-      name = 'author'
-      break
-    default:
-      break
-  }
-
-  const teamBody = {
-    teamType: {
-      name: role,
-      permissions,
-    },
-    group,
-    name,
-    object: {
-      type: 'collection',
-      id: collectionId,
-    },
-    members: [userId],
-  }
-  let team = new TeamModel(teamBody)
-  team = await team.save()
-  return team
-}
-
-const setupManuscriptTeam = async (models, user, collectionId, role) => {
-  const teams = await models.Team.all()
-  user.teams = user.teams || []
-  const filteredTeams = teams.filter(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-
-  if (filteredTeams.length > 0) {
-    let team = filteredTeams[0]
-    team.members.push(user.id)
-
-    try {
-      team = await team.save()
-      user.teams.push(team.id)
-      await user.save()
-      return team
-    } catch (e) {
-      logger.error(e)
-    }
-  } else {
-    const team = await createNewTeam(collectionId, role, user.id, models.Team)
-    user.teams.push(team.id)
-    await user.save()
-    return team
-  }
-}
-
-const removeTeamMember = async (teamId, userId, TeamModel) => {
-  const team = await TeamModel.find(teamId)
-  const members = team.members.filter(member => member !== userId)
-  team.members = members
-
-  await team.save()
-}
-
-const getTeamMembersByCollection = async (collectionId, role, TeamModel) => {
-  const teams = await TeamModel.all()
-  // const members = get(
-  //   teams.find(
-  //     team =>
-  //       team.group === role &&
-  //       team.object.type === 'collection' &&
-  //       team.object.id === collectionId,
-  //   ),
-  //   'members',
-  // )
-  const team = teams.find(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-
-  return team.members
-}
-
-const getTeamByGroupAndCollection = async (collectionId, role, TeamModel) => {
-  const teams = await TeamModel.all()
-  return teams.find(
-    team =>
-      team.group === role &&
-      team.object.type === 'collection' &&
-      team.object.id === collectionId,
-  )
-}
-
-module.exports = {
-  createNewTeam,
-  setupManuscriptTeam,
-  removeTeamMember,
-  getTeamMembersByCollection,
-  getTeamByGroupAndCollection,
-}
diff --git a/packages/component-user-manager/src/helpers/User.js b/packages/component-user-manager/src/helpers/User.js
deleted file mode 100644
index 41077ba17fb36fd8cf22f62998901bccd630ec58..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/User.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const helpers = require('./helpers')
-const mailService = require('pubsweet-component-mail-service')
-const logger = require('@pubsweet/logger')
-
-module.exports = {
-  setupNewUser: async (
-    body,
-    url,
-    res,
-    email,
-    role,
-    UserModel,
-    invitationType,
-  ) => {
-    const { firstName, lastName, affiliation, title } = body
-    const newUser = await helpers.createNewUser(
-      email,
-      firstName,
-      lastName,
-      affiliation,
-      title,
-      UserModel,
-      role,
-    )
-
-    try {
-      mailService.sendSimpleEmail({
-        toEmail: newUser.email,
-        user: newUser,
-        emailType: invitationType,
-        dashboardUrl: url,
-      })
-
-      return newUser
-    } catch (e) {
-      logger.error(e.message)
-      return { status: 500, error: 'Email could not be sent.' }
-    }
-  },
-}
diff --git a/packages/component-user-manager/src/helpers/helpers.js b/packages/component-user-manager/src/helpers/helpers.js
deleted file mode 100644
index 0628217ee67a8b0d6697869a7cf7beb491dfc8af..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/helpers/helpers.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const logger = require('@pubsweet/logger')
-const uuid = require('uuid')
-const crypto = require('crypto')
-
-const checkForUndefinedParams = (...params) => {
-  if (params.includes(undefined)) {
-    return false
-  }
-
-  return true
-}
-
-const validateEmailAndToken = async (email, token, userModel) => {
-  try {
-    const user = await userModel.findByEmail(email)
-    if (user) {
-      if (token !== user.passwordResetToken) {
-        logger.error(
-          `invite pw reset tokens do not match: REQ ${token} vs. DB ${
-            user.passwordResetToken
-          }`,
-        )
-        return {
-          success: false,
-          status: 400,
-          message: 'invalid request',
-        }
-      }
-      return { success: true, user }
-    }
-  } catch (e) {
-    if (e.name === 'NotFoundError') {
-      logger.error('invite pw reset on non-existing user')
-      return {
-        success: false,
-        status: 404,
-        message: 'user not found',
-      }
-    } else if (e.name === 'ValidationError') {
-      logger.error('invite pw reset validation error')
-      return {
-        success: false,
-        status: 400,
-        message: e.details[0].message,
-      }
-    }
-    logger.error('internal server error')
-    return {
-      success: false,
-      status: 500,
-      message: e.details[0].message,
-    }
-  }
-  return {
-    success: false,
-    status: 500,
-    message: 'something went wrong',
-  }
-}
-
-const handleNotFoundError = async (error, item) => {
-  const response = {
-    success: false,
-    status: 500,
-    message: 'Something went wrong',
-  }
-  if (error.name === 'NotFoundError') {
-    logger.error(`invalid ${item} id`)
-    response.status = 404
-    response.message = `${item} not found`
-    return response
-  }
-
-  logger.error(error)
-  return response
-}
-
-const createNewUser = async (
-  email,
-  firstName,
-  lastName,
-  affiliation,
-  title,
-  UserModel,
-  role,
-) => {
-  const username = email
-  const password = uuid.v4()
-  const userBody = {
-    username,
-    email,
-    password,
-    passwordResetToken: crypto.randomBytes(32).toString('hex'),
-    isConfirmed: false,
-    firstName,
-    lastName,
-    affiliation,
-    title,
-    editorInChief: role === 'editorInChief',
-    admin: role === 'admin',
-    handlingEditor: role === 'handlingEditor',
-  }
-
-  let newUser = new UserModel(userBody)
-
-  try {
-    newUser = await newUser.save()
-    return newUser
-  } catch (e) {
-    logger.error(e)
-  }
-}
-
-module.exports = {
-  checkForUndefinedParams,
-  validateEmailAndToken,
-  handleNotFoundError,
-  createNewUser,
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/delete.js b/packages/component-user-manager/src/routes/collectionsUsers/delete.js
deleted file mode 100644
index 2fae5bea551fdb6795c0fa0224a0d9f935f59e68..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/delete.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
-
-module.exports = models => async (req, res) => {
-  // TO DO: handle route  access with authsome
-  const { collectionId, userId } = req.params
-  try {
-    const collection = await models.Collection.find(collectionId)
-    const user = await models.User.find(userId)
-
-    const team = await teamHelper.getTeamByGroupAndCollection(
-      collectionId,
-      'author',
-      models.Team,
-    )
-
-    collection.authors = collection.authors.filter(
-      author => author.userId !== userId,
-    )
-    await collection.save()
-    await teamHelper.removeTeamMember(team.id, userId, models.Team)
-    user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
-    delete user.passwordResetToken
-    await user.save()
-    return res.status(200).json({})
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'item')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/get.js b/packages/component-user-manager/src/routes/collectionsUsers/get.js
deleted file mode 100644
index 1d6b395dd79dfeaa3e5b38f8c00a4b29d6c5716b..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/get.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const helpers = require('../../helpers/helpers')
-const teamHelper = require('../../helpers/Team')
-
-module.exports = models => async (req, res) => {
-  // TO DO: add authsome
-  const { collectionId } = req.params
-  try {
-    const collection = await models.Collection.find(collectionId)
-    if (collection.authors === undefined) {
-      return res.status(200).json([])
-    }
-    const members = await teamHelper.getTeamMembersByCollection(
-      collectionId,
-      'author',
-      models.Team,
-    )
-
-    if (members === undefined) {
-      res.status(400).json({
-        error: 'The requested collection does not have an author Team',
-      })
-      return
-    }
-    const membersData = members.map(async member => {
-      const user = await models.User.find(member)
-      const matchingAuthor = collection.authors.find(
-        author => author.userId === user.id,
-      )
-      return {
-        ...user,
-        isSubmitting: matchingAuthor.isSubmitting,
-        isCorresponding: matchingAuthor.isCorresponding,
-      }
-    })
-
-    const resBody = await Promise.all(membersData)
-    res.status(200).json(resBody)
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/patch.js b/packages/component-user-manager/src/routes/collectionsUsers/patch.js
deleted file mode 100644
index c973e20d44faaae59eab82afd22a5399e2321924..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/patch.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const logger = require('@pubsweet/logger')
-const helpers = require('../../helpers/helpers')
-
-module.exports = models => async (req, res) => {
-  // TO DO: add authsome
-  const { collectionId, userId } = req.params
-  const {
-    isSubmitting,
-    isCorresponding,
-    firstName,
-    lastName,
-    affiliation,
-  } = req.body
-
-  if (!helpers.checkForUndefinedParams(isSubmitting, isCorresponding)) {
-    res.status(400).json({ error: 'Missing parameters' })
-    logger.error('some parameters are missing')
-    return
-  }
-
-  try {
-    let collection = await models.Collection.find(collectionId)
-    if (collection.authors === undefined) {
-      return res.status(400).json({
-        error: 'Collection does not have any authors',
-      })
-    }
-    const user = await models.User.find(userId)
-    const matchingAuthor = collection.authors.find(
-      author => author.userId === user.id,
-    )
-    if (matchingAuthor === undefined) {
-      return res.status(400).json({
-        error: 'Collection and user do not match',
-      })
-    }
-    user.firstName = firstName
-    user.lastName = lastName
-    user.affiliation = affiliation
-    await user.save()
-    matchingAuthor.isSubmitting = isSubmitting
-    matchingAuthor.isCorresponding = isCorresponding
-    collection = await collection.save()
-    res.status(200).json(collection)
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'item')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/collectionsUsers/post.js b/packages/component-user-manager/src/routes/collectionsUsers/post.js
deleted file mode 100644
index 73957c570bea10d9dc4e1e691690e6e8170916ca..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/routes/collectionsUsers/post.js
+++ /dev/null
@@ -1,91 +0,0 @@
-const get = require('lodash/get')
-const helpers = require('../../helpers/helpers')
-const collectionHelper = require('../../helpers/Collection')
-const teamHelper = require('../../helpers/Team')
-const userHelper = require('../../helpers/User')
-
-module.exports = models => async (req, res) => {
-  const { email, role, isSubmitting, isCorresponding } = req.body
-
-  if (
-    !helpers.checkForUndefinedParams(email, role, isSubmitting, isCorresponding)
-  )
-    return res.status(400).json({ error: 'Missing parameters.' })
-
-  const collectionId = get(req, 'params.collectionId')
-  let collection
-  try {
-    collection = await models.Collection.find(collectionId)
-  } catch (e) {
-    const notFoundError = await helpers.handleNotFoundError(e, 'collection')
-    return res.status(notFoundError.status).json({
-      error: notFoundError.message,
-    })
-  }
-  const url = `${req.protocol}://${req.get('host')}`
-
-  try {
-    let user = await models.User.findByEmail(email)
-
-    if (role === 'author') {
-      await teamHelper.setupManuscriptTeam(models, user, collectionId, role)
-      // get updated user from DB
-      user = await models.User.find(user.id)
-      if (collection.authors !== undefined) {
-        const match = collection.authors.find(
-          author => author.userId === user.id,
-        )
-        if (match) {
-          return res.status(400).json({
-            error: `User ${user.email} is already an author`,
-          })
-        }
-      }
-      return await collectionHelper.addAuthor(
-        collection,
-        user,
-        res,
-        url,
-        isSubmitting,
-        isCorresponding,
-      )
-    }
-    return res.status(400).json({
-      error: `${role} is not defined`,
-    })
-  } catch (e) {
-    if (role !== 'author') {
-      return res.status(400).json({
-        error: `${role} is not defined`,
-      })
-    }
-    if (e.name === 'NotFoundError') {
-      const newUser = await userHelper.setupNewUser(
-        req.body,
-        url,
-        res,
-        email,
-        role,
-        models.User,
-        'invite-author',
-      )
-      if (newUser.error !== undefined) {
-        return res.status(newUser.status).json({
-          error: newUser.message,
-        })
-      }
-      await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role)
-      return collectionHelper.addAuthor(
-        collection,
-        newUser,
-        res,
-        url,
-        isSubmitting,
-        isCorresponding,
-      )
-    }
-    return res.status(e.status).json({
-      error: 'Something went wrong',
-    })
-  }
-}
diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/delete.js b/packages/component-user-manager/src/routes/fragmentsUsers/delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..b39893eb77b350f0e1357bda51266ff4a1654063
--- /dev/null
+++ b/packages/component-user-manager/src/routes/fragmentsUsers/delete.js
@@ -0,0 +1,37 @@
+const { Team, services } = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  // TO DO: handle route  access with authsome
+  const { collectionId, userId, fragmentId } = req.params
+  try {
+    const collection = await models.Collection.find(collectionId)
+    const user = await models.User.find(userId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+    const fragment = await models.Fragment.find(fragmentId)
+    const teamHelper = new Team({ TeamModel: models.Team, fragmentId })
+
+    const team = await teamHelper.getTeam({
+      role: 'author',
+      objectType: 'fragment',
+    })
+
+    await teamHelper.removeTeamMember({ teamId: team.id, userId })
+    user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
+    delete user.passwordResetToken
+    user.save()
+
+    fragment.authors = fragment.authors || []
+    fragment.authors = fragment.authors.filter(author => author.id !== userId)
+    fragment.save()
+
+    return res.status(200).json({})
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/get.js b/packages/component-user-manager/src/routes/fragmentsUsers/get.js
new file mode 100644
index 0000000000000000000000000000000000000000..76ef6041615e728b141e04f537ae6b67e643dd86
--- /dev/null
+++ b/packages/component-user-manager/src/routes/fragmentsUsers/get.js
@@ -0,0 +1,21 @@
+const { services } = require('pubsweet-component-helper-service')
+
+module.exports = models => async (req, res) => {
+  // TO DO: add authsome
+  const { collectionId, fragmentId } = req.params
+  try {
+    const collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+
+    const { authors = [] } = await models.Fragment.find(fragmentId)
+    return res.status(200).json(authors)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/post.js b/packages/component-user-manager/src/routes/fragmentsUsers/post.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d9b8a4ce8475e3658f5632cc4e9267e02a3198f
--- /dev/null
+++ b/packages/component-user-manager/src/routes/fragmentsUsers/post.js
@@ -0,0 +1,137 @@
+const { pick } = require('lodash')
+const mailService = require('pubsweet-component-mail-service')
+
+const {
+  User,
+  Team,
+  services,
+  Fragment,
+} = require('pubsweet-component-helper-service')
+
+const authorKeys = [
+  'id',
+  'email',
+  'title',
+  'lastName',
+  'firstName',
+  'affiliation',
+]
+
+// TODO: add authsome
+module.exports = models => async (req, res) => {
+  const { email, role, isSubmitting, isCorresponding } = req.body
+
+  if (
+    !services.checkForUndefinedParams(
+      email,
+      role,
+      isSubmitting,
+      isCorresponding,
+    )
+  )
+    return res.status(400).json({ error: 'Missing parameters.' })
+
+  const { collectionId, fragmentId } = req.params
+  let collection, fragment
+  try {
+    collection = await models.Collection.find(collectionId)
+    if (!collection.fragments.includes(fragmentId))
+      return res.status(400).json({
+        error: `Fragment ${fragmentId} does not match collection ${collectionId}`,
+      })
+    fragment = await models.Fragment.find(fragmentId)
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'item')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+  const baseUrl = services.getBaseUrl(req)
+  const UserModel = models.User
+  const teamHelper = new Team({ TeamModel: models.Team, fragmentId })
+  const fragmentHelper = new Fragment({ fragment })
+
+  try {
+    let user = await UserModel.findByEmail(email)
+
+    if (role !== 'author') {
+      return res.status(400).json({
+        error: `${role} is not defined`,
+      })
+    }
+
+    await teamHelper.setupTeam({ user, role, objectType: 'fragment' })
+    user = await UserModel.find(user.id)
+
+    fragment.authors = fragment.authors || []
+    const match = fragment.authors.find(author => author.id === user.id)
+
+    if (match) {
+      return res.status(400).json({
+        error: `User ${user.email} is already an author`,
+      })
+    }
+
+    await fragmentHelper.addAuthor({
+      user,
+      isSubmitting,
+      isCorresponding,
+    })
+
+    return res.status(200).json({
+      ...pick(user, authorKeys),
+      isSubmitting,
+      isCorresponding,
+    })
+  } catch (e) {
+    if (role !== 'author')
+      return res.status(400).json({
+        error: `${role} is not defined`,
+      })
+
+    if (e.name === 'NotFoundError') {
+      const userHelper = new User({ UserModel })
+      const newUser = await userHelper.setupNewUser({
+        url: baseUrl,
+        role,
+        invitationType: 'invite-author',
+        body: req.body,
+      })
+
+      if (newUser.error !== undefined)
+        return res.status(newUser.status).json({
+          error: newUser.message,
+        })
+
+      await teamHelper.setupTeam({
+        user: newUser,
+        role,
+        objectType: 'fragment',
+      })
+
+      await fragmentHelper.addAuthor({
+        user: newUser,
+        isSubmitting,
+        isCorresponding,
+      })
+
+      if (!collection.owners.includes(newUser.id)) {
+        mailService.sendSimpleEmail({
+          toEmail: newUser.email,
+          user: newUser,
+          emailType: 'add-author',
+          dashboardUrl: baseUrl,
+        })
+      }
+
+      return res.status(200).json({
+        ...pick(newUser, authorKeys),
+        isSubmitting,
+        isCorresponding,
+      })
+    }
+    return res.status(e.status).json({
+      error: `Something went wrong: ${e.name}`,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/users/confirm.js b/packages/component-user-manager/src/routes/users/confirm.js
new file mode 100644
index 0000000000000000000000000000000000000000..ace2592bff912eaa12bb8b6ac0a7c26fee52c61e
--- /dev/null
+++ b/packages/component-user-manager/src/routes/users/confirm.js
@@ -0,0 +1,35 @@
+const { token } = require('pubsweet-server/src/authentication')
+const { services } = require('pubsweet-component-helper-service')
+
+module.exports = ({ User }) => async (req, res) => {
+  const { userId, confirmationToken } = req.body
+
+  if (!services.checkForUndefinedParams(userId, confirmationToken))
+    return res.status(400).json({ error: 'Missing required params' })
+
+  let user
+  try {
+    user = await User.find(userId)
+
+    if (user.confirmationToken !== confirmationToken) {
+      return res.status(400).json({ error: 'Wrong confirmation token.' })
+    }
+
+    if (user.isConfirmed)
+      return res.status(400).json({ error: 'User is already confirmed.' })
+
+    user.isConfirmed = true
+    delete user.confirmationToken
+    await user.save()
+
+    return res.status(200).json({
+      ...user,
+      token: token.create(user),
+    })
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'User')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/routes/users/forgotPassword.js b/packages/component-user-manager/src/routes/users/forgotPassword.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbc5d3b686af0f74086420176bfe57bfcd00e635
--- /dev/null
+++ b/packages/component-user-manager/src/routes/users/forgotPassword.js
@@ -0,0 +1,50 @@
+const logger = require('@pubsweet/logger')
+const { services } = require('pubsweet-component-helper-service')
+const mailService = require('pubsweet-component-mail-service')
+
+module.exports = models => async (req, res) => {
+  const { email } = req.body
+  if (!services.checkForUndefinedParams(email))
+    return res.status(400).json({ error: 'Email address is required.' })
+
+  try {
+    const user = await models.User.findByEmail(email)
+    if (user.passwordResetTimestamp) {
+      const resetDate = new Date(user.passwordResetTimestamp)
+      const hoursPassed = Math.floor(
+        (new Date().getTime() - resetDate) / 3600000,
+      )
+      if (hoursPassed < 24) {
+        return res
+          .status(400)
+          .json({ error: 'A password reset has already been requested.' })
+      }
+    }
+
+    user.passwordResetToken = generatePasswordHash()
+    user.passwordResetTimestamp = Date.now()
+    await user.save()
+
+    mailService.sendSimpleEmail({
+      toEmail: user.email,
+      user,
+      emailType: 'forgot-password',
+      dashboardUrl: services.getBaseUrl(req),
+    })
+  } catch (e) {
+    logger.error(
+      `A forgot password request has been made on an non-existent email: ${email}`,
+    )
+  }
+
+  res.status(200).json({
+    message: `A password reset email has been sent to ${email}.`,
+  })
+}
+
+const generatePasswordHash = () =>
+  Array.from({ length: 4 }, () =>
+    Math.random()
+      .toString(36)
+      .slice(4),
+  ).join('')
diff --git a/packages/component-user-manager/src/routes/users/resetPassword.js b/packages/component-user-manager/src/routes/users/resetPassword.js
index 6b6dc173e2db18ac9bd666505cda16c7042cb9fd..70257a1fb7ba13bddc50c8e8043f4a533f06d2a7 100644
--- a/packages/component-user-manager/src/routes/users/resetPassword.js
+++ b/packages/component-user-manager/src/routes/users/resetPassword.js
@@ -1,8 +1,8 @@
-const helpers = require('../../helpers/helpers')
+const { services } = require('pubsweet-component-helper-service')
 
 module.exports = models => async (req, res) => {
   const { email, password, token } = req.body
-  if (!helpers.checkForUndefinedParams(email, password, token))
+  if (!services.checkForUndefinedParams(email, password, token))
     return res.status(400).json({ error: 'missing required params' })
 
   if (password.length < 7)
@@ -10,11 +10,11 @@ module.exports = models => async (req, res) => {
       .status(400)
       .json({ error: 'password needs to be at least 7 characters long' })
 
-  const validateResponse = await helpers.validateEmailAndToken(
+  const validateResponse = await services.validateEmailAndToken({
     email,
     token,
-    models.User,
-  )
+    userModel: models.User,
+  })
 
   if (validateResponse.success === false)
     return res
@@ -23,11 +23,9 @@ module.exports = models => async (req, res) => {
 
   let { user } = validateResponse
 
-  if (user.isConfirmed)
-    return res.status(400).json({ error: 'User is already confirmed' })
-
   req.body.isConfirmed = true
   delete user.passwordResetToken
+  delete user.passwordResetTimestamp
   delete req.body.token
   user = await user.updateProperties(req.body)
 
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/delete.test.js b/packages/component-user-manager/src/tests/collectionsUsers/delete.test.js
deleted file mode 100644
index 7242ac5c76c8b75ce0e265ca9ef98981490fe1a0..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/collectionsUsers/delete.test.js
+++ /dev/null
@@ -1,52 +0,0 @@
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-process.env.SUPPRESS_NO_CONFIG_WARNING = true
-
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
-
-const models = Model.build()
-
-const { author, submittingAuthor } = fixtures.users
-const { standardCollection } = fixtures.collections
-const { authorTeam } = fixtures.teams
-const deletePath = '../../routes/collectionsUsers/delete'
-
-describe('Delete collections users route handler', () => {
-  it('should return success when an author is deleted', async () => {
-    const req = httpMocks.createRequest({})
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = author.id
-    const res = httpMocks.createResponse()
-    await require(deletePath)(models)(req, res)
-
-    expect(res.statusCode).toBe(200)
-    expect(authorTeam.members).not.toContain(author.id)
-    expect(author.teams).not.toContain(authorTeam.id)
-  })
-  it('should return an error when the collection does not exist', async () => {
-    const req = httpMocks.createRequest({})
-    req.user = submittingAuthor.id
-    req.params.collectionId = 'invalid-id'
-    req.params.userId = author.id
-    const res = httpMocks.createResponse()
-    await require(deletePath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-  it('should return an error when the user does not exist', async () => {
-    const req = httpMocks.createRequest({})
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = 'invalid-id'
-    const res = httpMocks.createResponse()
-    await require(deletePath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-})
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/get.test.js b/packages/component-user-manager/src/tests/collectionsUsers/get.test.js
deleted file mode 100644
index b5975b4ed8d941cd760c22b4df9ec729c41e8ab7..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/collectionsUsers/get.test.js
+++ /dev/null
@@ -1,38 +0,0 @@
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-process.env.SUPPRESS_NO_CONFIG_WARNING = true
-
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
-
-const { standardCollection } = fixtures.collections
-const { submittingAuthor } = fixtures.users
-
-const getPath = '../../routes/collectionsUsers/get'
-describe('Get collections users route handler', () => {
-  it('should return success when the request data is correct', async () => {
-    const req = httpMocks.createRequest()
-    req.params.collectionId = standardCollection.id
-    req.user = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    const models = Model.build()
-    await require(getPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(200)
-    const data = JSON.parse(res._getData())
-    expect(data).toHaveLength(1)
-    expect(data[0].isSubmitting).toBeDefined()
-    expect(data[0].type).toBe('user')
-  })
-  it('should return an error when the collection does not exist', async () => {
-    const req = httpMocks.createRequest()
-    req.params.collectionId = 'invalid-id'
-    req.user = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    const models = Model.build()
-    await require(getPath)(models)(req, res)
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('collection not found')
-  })
-})
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/patch.test.js b/packages/component-user-manager/src/tests/collectionsUsers/patch.test.js
deleted file mode 100644
index 03c35f392d843bcf12c2b4859c656017a311c0a7..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/collectionsUsers/patch.test.js
+++ /dev/null
@@ -1,119 +0,0 @@
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
-process.env.SUPPRESS_NO_CONFIG_WARNING = true
-
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
-const Model = require('./../helpers/Model')
-const Chance = require('chance')
-
-const chance = new Chance()
-
-const models = Model.build()
-jest.mock('pubsweet-component-mail-service', () => ({
-  sendSimpleEmail: jest.fn(),
-  sendNotificationEmail: jest.fn(),
-}))
-
-const { author, submittingAuthor } = fixtures.users
-const { standardCollection, authorsCollection } = fixtures.collections
-const body = {
-  isSubmitting: false,
-  isCorresponding: true,
-  firstName: chance.first(),
-  lastName: chance.last(),
-  affiliation: chance.company(),
-}
-const patchPath = '../../routes/collectionsUsers/patch'
-describe('Patch collections users route handler', () => {
-  it('should return success when the request data is correct', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-    const data = JSON.parse(res._getData())
-    expect(res.statusCode).toBe(200)
-    const matchingAuthor = data.authors.find(
-      author => author.userId === submittingAuthor.id,
-    )
-    expect(matchingAuthor.isSubmitting).toBe(body.isSubmitting)
-    expect(matchingAuthor.isCorresponding).toBe(body.isCorresponding)
-    expect(submittingAuthor.firstName).toBe(body.firstName)
-    expect(submittingAuthor.lastName).toBe(body.lastName)
-  })
-  it('should return an error when the params are missing', async () => {
-    delete body.isSubmitting
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Missing parameters')
-    body.isSubmitting = false
-  })
-  it('should return an error if the collection does not exists', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = 'invalid-id'
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-  it('should return an error when the user does not exist', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = author.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = 'invalid-id'
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(404)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('item not found')
-  })
-  it('should return an error when the collection does not have authors', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = authorsCollection.id
-    req.params.userId = submittingAuthor.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Collection does not have any authors')
-  })
-  it('should return an error when the collection and the user do not match', async () => {
-    const req = httpMocks.createRequest({
-      body,
-    })
-    req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
-    req.params.userId = author.id
-    const res = httpMocks.createResponse()
-    await require(patchPath)(models)(req, res)
-
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('Collection and user do not match')
-  })
-})
diff --git a/packages/component-user-manager/src/tests/fixtures/collections.js b/packages/component-user-manager/src/tests/fixtures/collections.js
deleted file mode 100644
index bd50ad595d07a565ae5b799fb1a744217b92192d..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/collections.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const Chance = require('chance')
-const { submittingAuthor } = require('./userData')
-
-const chance = new Chance()
-const collections = {
-  standardCollection: {
-    id: chance.guid(),
-    title: chance.sentence(),
-    type: 'collection',
-    fragments: [],
-    owners: [submittingAuthor.id],
-    authors: [
-      {
-        userId: submittingAuthor.id,
-        isSubmitting: true,
-        isCorresponding: false,
-      },
-    ],
-    save: jest.fn(() => collections.standardCollection),
-  },
-  authorsCollection: {
-    id: chance.guid(),
-    title: chance.sentence(),
-    type: 'collection',
-    fragments: [],
-    owners: [submittingAuthor.id],
-    save: jest.fn(),
-  },
-}
-
-module.exports = collections
diff --git a/packages/component-user-manager/src/tests/fixtures/fixtures.js b/packages/component-user-manager/src/tests/fixtures/fixtures.js
deleted file mode 100644
index 0ea29e85ac3a373a20a0e82377e0b6f3dfeef51e..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/fixtures.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const teams = require('./teams')
-
-module.exports = {
-  users,
-  collections,
-  teams,
-}
diff --git a/packages/component-user-manager/src/tests/fixtures/teams.js b/packages/component-user-manager/src/tests/fixtures/teams.js
deleted file mode 100644
index 410b999e3f98f3b196eb28feb5512c744e5b61ea..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/teams.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const users = require('./users')
-const collections = require('./collections')
-const { authorTeamID } = require('./teamIDs')
-
-const { standardCollection } = collections
-const { submittingAuthor } = users
-const teams = {
-  authorTeam: {
-    teamType: {
-      name: 'author',
-      permissions: 'author',
-    },
-    group: 'author',
-    name: 'author',
-    object: {
-      type: 'collection',
-      id: standardCollection.id,
-    },
-    members: [submittingAuthor.id],
-    save: jest.fn(() => teams.authorTeam),
-    updateProperties: jest.fn(() => teams.authorTeam),
-    id: authorTeamID,
-  },
-}
-module.exports = teams
diff --git a/packages/component-user-manager/src/tests/fixtures/userData.js b/packages/component-user-manager/src/tests/fixtures/userData.js
deleted file mode 100644
index 1a962c33dc4915763c259edae6deadfab9911f98..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/userData.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const Chance = require('chance')
-
-const chance = new Chance()
-
-module.exports = {
-  author: {
-    id: chance.guid(),
-    email: chance.email(),
-    firstName: chance.first(),
-    lastName: chance.last(),
-  },
-  submittingAuthor: {
-    id: chance.guid(),
-    email: chance.email(),
-    firstName: chance.first(),
-    lastName: chance.last(),
-  },
-  admin: {
-    id: chance.guid(),
-    email: chance.email(),
-  },
-}
diff --git a/packages/component-user-manager/src/tests/fixtures/users.js b/packages/component-user-manager/src/tests/fixtures/users.js
deleted file mode 100644
index 654032921308157e12248205373970f595d0d497..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/fixtures/users.js
+++ /dev/null
@@ -1,49 +0,0 @@
-const { authorTeamID } = require('./teamIDs')
-const { author, submittingAuthor, admin } = require('./userData')
-const Chance = require('chance')
-
-const chance = new Chance()
-const users = {
-  admin: {
-    type: 'user',
-    username: 'admin',
-    email: admin.email,
-    password: 'password',
-    admin: true,
-    id: admin.id,
-  },
-  author: {
-    type: 'user',
-    username: 'author',
-    email: author.email,
-    password: 'password',
-    admin: false,
-    id: author.id,
-    passwordResetToken: chance.hash(),
-    firstName: author.firstName,
-    lastName: author.lastName,
-    affiliation: 'MIT',
-    title: 'Mr',
-    save: jest.fn(() => users.author),
-    isConfirmed: false,
-    teams: [authorTeamID],
-    updateProperties: jest.fn(() => users.author),
-  },
-  submittingAuthor: {
-    type: 'user',
-    username: 'sauthor',
-    email: submittingAuthor.email,
-    password: 'password',
-    admin: false,
-    id: submittingAuthor.id,
-    passwordResetToken: chance.hash(),
-    firstName: submittingAuthor.firstName,
-    lastName: submittingAuthor.lastName,
-    affiliation: chance.company(),
-    title: 'Mr',
-    save: jest.fn(() => users.submittingAuthor),
-    isConfirmed: false,
-  },
-}
-
-module.exports = users
diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..5bea3f85277e6cadf09fd9ff8b31ae5bbb15df7f
--- /dev/null
+++ b/packages/component-user-manager/src/tests/fragmentsUsers/delete.test.js
@@ -0,0 +1,77 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+const cloneDeep = require('lodash/cloneDeep')
+
+const httpMocks = require('node-mocks-http')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { author, submittingAuthor } = fixtures.users
+const { collection } = fixtures.collections
+const deletePath = '../../routes/fragmentsUsers/delete'
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+describe('Delete fragments users route handler', () => {
+  let testFixtures = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when an author is deleted', async () => {
+    const req = httpMocks.createRequest({})
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const req = httpMocks.createRequest({})
+    req.user = submittingAuthor.id
+    req.params.collectionId = 'invalid-id'
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('item not found')
+  })
+  it('should return an error when the user does not exist', async () => {
+    const req = httpMocks.createRequest({})
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
+    req.params.userId = 'invalid-id'
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('item not found')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const req = httpMocks.createRequest()
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-fragment-id'
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(deletePath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-fragment-id does not match collection ${collection.id}`,
+    )
+  })
+})
diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..290d26af0b1efa9ad4a9b59cf519b40c688e9787
--- /dev/null
+++ b/packages/component-user-manager/src/tests/fragmentsUsers/get.test.js
@@ -0,0 +1,64 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+const cloneDeep = require('lodash/cloneDeep')
+
+const httpMocks = require('node-mocks-http')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { collection } = fixtures.collections
+const { submittingAuthor } = fixtures.users
+
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+const getPath = '../../routes/fragmentsUsers/get'
+describe('Get fragments users route handler', () => {
+  let testFixtures = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when the request data is correct', async () => {
+    const req = httpMocks.createRequest()
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
+    req.user = submittingAuthor.id
+    const res = httpMocks.createResponse()
+    await require(getPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+
+    expect(data).toHaveLength(1)
+    expect(data[0].isSubmitting).toBeDefined()
+  })
+  it('should return an error when the collection does not exist', async () => {
+    const req = httpMocks.createRequest()
+    req.params.collectionId = 'invalid-id'
+    req.user = submittingAuthor.id
+    const res = httpMocks.createResponse()
+    await require(getPath)(models)(req, res)
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('item not found')
+  })
+  it('should return an error when the fragment does not exist', async () => {
+    const req = httpMocks.createRequest()
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-fragment-id'
+    const res = httpMocks.createResponse()
+    await require(getPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-fragment-id does not match collection ${collection.id}`,
+    )
+  })
+})
diff --git a/packages/component-user-manager/src/tests/collectionsUsers/post.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js
similarity index 61%
rename from packages/component-user-manager/src/tests/collectionsUsers/post.test.js
rename to packages/component-user-manager/src/tests/fragmentsUsers/post.test.js
index eede871a5820c4aba630a48186a8ed88a080d7ab..42ceb286f20f187bde72a6e523527f8d42b85ae9 100644
--- a/packages/component-user-manager/src/tests/collectionsUsers/post.test.js
+++ b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js
@@ -2,11 +2,12 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
 const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
 
-const models = Model.build()
 jest.mock('pubsweet-component-mail-service', () => ({
   sendSimpleEmail: jest.fn(),
   sendNotificationEmail: jest.fn(),
@@ -14,21 +15,31 @@ jest.mock('pubsweet-component-mail-service', () => ({
 const chance = new Chance()
 
 const { author, submittingAuthor } = fixtures.users
-const { standardCollection } = fixtures.collections
-const postPath = '../../routes/collectionsUsers/post'
-const body = {
+const { collection } = fixtures.collections
+const postPath = '../../routes/fragmentsUsers/post'
+const reqBody = {
   email: chance.email(),
   role: 'author',
   isSubmitting: true,
   isCorresponding: false,
 }
-describe('Post collections users route handler', () => {
-  it('should return success when an author adds a new user to a collection', async () => {
+describe('Post fragments users route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return success when an author adds a new user to a fragment', async () => {
     const req = httpMocks.createRequest({
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -36,36 +47,32 @@ describe('Post collections users route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.email).toEqual(body.email)
     expect(data.invitations).toBeUndefined()
-    const matchingAuthor = standardCollection.authors.find(
-      author => author.userId === data.id,
-    )
-    expect(matchingAuthor).toBeDefined()
   })
-  it('should return success when an author adds an existing user as co author to a collection', async () => {
+  it('should return success when an author adds an existing user as co author to a fragment', async () => {
     body.email = author.email
     const req = httpMocks.createRequest({
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
     expect(res.statusCode).toBe(200)
     const data = JSON.parse(res._getData())
     expect(data.email).toEqual(body.email)
-    const matchingAuthor = standardCollection.authors.find(
-      auth => auth.userId === author.id,
-    )
-    expect(matchingAuthor).toBeDefined()
   })
-  it('should return an error when the an author is added to the same collection', async () => {
+  it('should return an error when the an author is added to the same fragment', async () => {
     body.email = submittingAuthor.email
     const req = httpMocks.createRequest({
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -81,7 +88,9 @@ describe('Post collections users route handler', () => {
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -96,7 +105,9 @@ describe('Post collections users route handler', () => {
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -111,7 +122,9 @@ describe('Post collections users route handler', () => {
       body,
     })
     req.user = submittingAuthor.id
-    req.params.collectionId = standardCollection.id
+    req.params.collectionId = collection.id
+    const [fragmentId] = collection.fragments
+    req.params.fragmentId = fragmentId
     const res = httpMocks.createResponse()
     await require(postPath)(models)(req, res)
 
@@ -119,4 +132,21 @@ describe('Post collections users route handler', () => {
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('invalid-role is not defined')
   })
+  it('should return an error when the fragment does not exist', async () => {
+    const req = httpMocks.createRequest({
+      body,
+    })
+    req.user = submittingAuthor.id
+    req.params.collectionId = collection.id
+    req.params.fragmentId = 'invalid-fragment-id'
+    req.params.userId = author.id
+    const res = httpMocks.createResponse()
+    await require(postPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      `Fragment invalid-fragment-id does not match collection ${collection.id}`,
+    )
+  })
 })
diff --git a/packages/component-user-manager/src/tests/helpers/Model.js b/packages/component-user-manager/src/tests/helpers/Model.js
deleted file mode 100644
index e5f31fa11df30b229f84e129d41c33062f7b6d1d..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/helpers/Model.js
+++ /dev/null
@@ -1,61 +0,0 @@
-const fixtures = require('../fixtures/fixtures')
-
-const UserMock = require('../mocks/User')
-const TeamMock = require('../mocks/Team')
-
-const notFoundError = new Error()
-notFoundError.name = 'NotFoundError'
-notFoundError.status = 404
-
-const build = () => {
-  const models = {
-    User: {},
-    Collection: {
-      find: jest.fn(id => findMock(id, 'collections')),
-    },
-    Team: {},
-  }
-  UserMock.find = jest.fn(id => findMock(id, 'users'))
-  UserMock.findByEmail = jest.fn(email => findByEmailMock(email))
-  UserMock.all = jest.fn(() => Object.values(fixtures.users))
-  UserMock.updateProperties = jest.fn(user =>
-    updatePropertiesMock(user, 'users'),
-  )
-  TeamMock.find = jest.fn(id => findMock(id, 'teams'))
-  TeamMock.updateProperties = jest.fn(team =>
-    updatePropertiesMock(team, 'teams'),
-  )
-  TeamMock.all = jest.fn(() => Object.values(fixtures.teams))
-
-  models.User = UserMock
-  models.Team = TeamMock
-  return models
-}
-
-const findMock = (id, type) => {
-  const foundObj = Object.values(fixtures[type]).find(
-    fixtureObj => fixtureObj.id === id,
-  )
-
-  if (foundObj === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundObj)
-}
-
-const findByEmailMock = email => {
-  const foundUser = Object.values(fixtures.users).find(
-    fixtureUser => fixtureUser.email === email,
-  )
-
-  if (foundUser === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundUser)
-}
-
-const updatePropertiesMock = (obj, type) => {
-  const foundObj = Object.values(fixtures[type]).find(
-    fixtureObj => fixtureObj === obj,
-  )
-
-  if (foundObj === undefined) return Promise.reject(notFoundError)
-  return Promise.resolve(foundObj)
-}
-module.exports = { build }
diff --git a/packages/component-user-manager/src/tests/mocks/Team.js b/packages/component-user-manager/src/tests/mocks/Team.js
deleted file mode 100644
index f84ef4dcf8ffa0e5056c8f0eb86573f624d74c06..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/mocks/Team.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable func-names-any */
-
-function Team(properties) {
-  this.teamType = properties.teamType
-  this.group = properties.group
-  this.name = properties.name
-  this.object = properties.object
-  this.members = properties.members
-}
-
-Team.prototype.save = jest.fn(function saveTeam() {
-  this.id = '111222'
-  return Promise.resolve(this)
-})
-
-module.exports = Team
diff --git a/packages/component-user-manager/src/tests/mocks/User.js b/packages/component-user-manager/src/tests/mocks/User.js
deleted file mode 100644
index 9a7459fc5578d3aa1d5dcec8562fa8f17225b1eb..0000000000000000000000000000000000000000
--- a/packages/component-user-manager/src/tests/mocks/User.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/* eslint-disable func-names-any */
-
-function User(properties) {
-  this.type = 'user'
-  this.email = properties.email
-  this.username = properties.username
-  this.password = properties.password
-  this.roles = properties.roles
-  this.title = properties.title
-  this.affiliation = properties.affiliation
-  this.firstName = properties.firstName
-  this.lastName = properties.lastName
-  this.admin = properties.admin
-}
-
-User.prototype.save = jest.fn(function saveUser() {
-  this.id = '111222'
-  return Promise.resolve(this)
-})
-
-module.exports = User
diff --git a/packages/component-user-manager/src/tests/users/confirm.test.js b/packages/component-user-manager/src/tests/users/confirm.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f139d1f227c51ea1f8e990464921a7a1da01068
--- /dev/null
+++ b/packages/component-user-manager/src/tests/users/confirm.test.js
@@ -0,0 +1,83 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const httpMocks = require('node-mocks-http')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { user, author } = fixtures.users
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+
+const reqBody = {
+  userId: user.id,
+  confirmationToken: user.confirmationToken,
+}
+
+const notFoundError = new Error()
+notFoundError.name = 'NotFoundError'
+notFoundError.status = 404
+const forgotPasswordPath = '../../routes/users/confirm'
+
+describe('Users confirm route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+
+  it('should return an error when some parameters are missing', async () => {
+    delete body.userId
+    const req = httpMocks.createRequest({ body })
+
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Missing required params')
+  })
+  it('should return an error when the confirmation token does not match', async () => {
+    body.confirmationToken = 'invalid-token'
+
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Wrong confirmation token.')
+  })
+  it('should return an error when the user does not exist', async () => {
+    body.userId = 'invalid-user-id'
+
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('User not found')
+  })
+  it('should return an error when the user is already confirmed', async () => {
+    body.userId = author.id
+    body.confirmationToken = author.confirmationToken
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('User is already confirmed.')
+  })
+  it('should return success when the body is correct', async () => {
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    const data = JSON.parse(res._getData())
+    expect(data.token).toBeDefined()
+  })
+})
diff --git a/packages/component-user-manager/src/tests/users/forgotPassword.test.js b/packages/component-user-manager/src/tests/users/forgotPassword.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6f51c974dabb9e51ff1dc0063c43a8b99ada457
--- /dev/null
+++ b/packages/component-user-manager/src/tests/users/forgotPassword.test.js
@@ -0,0 +1,78 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const httpMocks = require('node-mocks-http')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { user, author } = fixtures.users
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+
+const reqBody = {
+  email: user.email,
+}
+
+const notFoundError = new Error()
+notFoundError.name = 'NotFoundError'
+notFoundError.status = 404
+const forgotPasswordPath = '../../routes/users/forgotPassword'
+
+describe('Users forgot password route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+
+  it('should return an error when some parameters are missing', async () => {
+    delete body.email
+    const req = httpMocks.createRequest({ body })
+
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Email address is required.')
+  })
+  it('should return an error when the user has already requested a password reset', async () => {
+    body.email = author.email
+
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('A password reset has already been requested.')
+  })
+  it('should return success when the body is correct', async () => {
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+    expect(data.message).toEqual(
+      `A password reset email has been sent to ${body.email}.`,
+    )
+  })
+  it('should return success if the email is non-existent', async () => {
+    body.email = 'email@example.com'
+    const req = httpMocks.createRequest({ body })
+    const res = httpMocks.createResponse()
+    await require(forgotPasswordPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+    const data = JSON.parse(res._getData())
+    expect(data.message).toEqual(
+      `A password reset email has been sent to ${body.email}.`,
+    )
+  })
+})
diff --git a/packages/component-user-manager/src/tests/users/resetPassword.test.js b/packages/component-user-manager/src/tests/users/resetPassword.test.js
index f2a7522d2874cd428b57896a853943fe8dcb6d9b..130526eb7f9bca6bf3cb00fd24a7e96cd3d8e2b7 100644
--- a/packages/component-user-manager/src/tests/users/resetPassword.test.js
+++ b/packages/component-user-manager/src/tests/users/resetPassword.test.js
@@ -1,25 +1,27 @@
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 process.env.SUPPRESS_NO_CONFIG_WARNING = true
 
-const httpMocks = require('node-mocks-http')
-const fixtures = require('./../fixtures/fixtures')
 const Chance = require('chance')
-const Model = require('./../helpers/Model')
-const clone = require('lodash/cloneDeep')
+const httpMocks = require('node-mocks-http')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
 
+const { Model, fixtures } = fixturesService
 const chance = new Chance()
 
-const { author } = fixtures.users
-const clonedAuthor = clone(author)
-
-const body = {
-  email: clonedAuthor.email,
-  firstName: clonedAuthor.firstName,
-  lastName: clonedAuthor.lastName,
-  title: clonedAuthor.title,
-  affiliation: clonedAuthor.affiliation,
+const { user } = fixtures.users
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+const reqBody = {
+  email: user.email,
+  firstName: user.firstName,
+  lastName: user.lastName,
+  title: user.title,
+  affiliation: user.affiliation,
   password: 'password',
-  token: clonedAuthor.passwordResetToken,
+  token: user.passwordResetToken,
   isConfirmed: false,
 }
 
@@ -28,69 +30,57 @@ notFoundError.name = 'NotFoundError'
 notFoundError.status = 404
 const resetPasswordPath = '../../routes/users/resetPassword'
 describe('Users password reset route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
   it('should return an error when some parameters are missing', async () => {
     delete body.email
     const req = httpMocks.createRequest({ body })
 
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('missing required params')
-    body.email = author.email
   })
   it('should return an error when the password is too small', async () => {
     body.password = 'small'
     const req = httpMocks.createRequest({ body })
 
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual(
       'password needs to be at least 7 characters long',
     )
-    body.password = 'password'
   })
   it('should return an error when user is not found', async () => {
     body.email = chance.email()
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(404)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('user not found')
-    body.email = author.email
   })
   it('should return an error when the tokens do not match', async () => {
     body.token = chance.hash()
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
     expect(res.statusCode).toBe(400)
     const data = JSON.parse(res._getData())
     expect(data.error).toEqual('invalid request')
-    body.token = author.passwordResetToken
-  })
-  it('should return an error when the user is already confirmed', async () => {
-    author.isConfirmed = true
-    const req = httpMocks.createRequest({ body })
-    const res = httpMocks.createResponse()
-    const models = Model.build()
-    await require(resetPasswordPath)(models)(req, res)
-    expect(res.statusCode).toBe(400)
-    const data = JSON.parse(res._getData())
-    expect(data.error).toEqual('User is already confirmed')
-    author.isConfirmed = false
   })
   it('should return success when the body is correct', async () => {
     const req = httpMocks.createRequest({ body })
     const res = httpMocks.createResponse()
-    const models = Model.build()
     await require(resetPasswordPath)(models)(req, res)
 
     expect(res.statusCode).toBe(200)
diff --git a/packages/component-wizard/package.json b/packages/component-wizard/package.json
index 72de7cfb330156155dfd5e4f1e3c4d0a04f1dc8c..7d2840160e3e7212749ca1b010b0b9ccf687a594 100644
--- a/packages/component-wizard/package.json
+++ b/packages/component-wizard/package.json
@@ -9,14 +9,15 @@
     "dist"
   ],
   "dependencies": {
-    "@pubsweet/ui": "^3.1.0",
+    "@pubsweet/ui": "4.1.3",
+    "@pubsweet/ui-toolkit": "latest",
     "moment": "^2.20.1",
     "react-dnd": "^2.5.4",
     "react-dnd-html5-backend": "^2.5.4",
     "react-dom": "^15.6.1",
     "react-router-dom": "^4.2.2",
     "redux": "^3.6.0",
-    "redux-form": "^7.0.3",
+    "redux-form": "7.0.3",
     "recompose": "^0.26.0",
     "xpub-validators": "^0.0.3",
     "xpub-connect": "^0.0.3",
diff --git a/packages/component-wizard/src/components/WizardFormStep.js b/packages/component-wizard/src/components/WizardFormStep.js
index f28662bbe5a2f8f787844e91eefa160121af7087..c4caa8b12f1bb533d416e01c1d2c716ed6908d0f 100644
--- a/packages/component-wizard/src/components/WizardFormStep.js
+++ b/packages/component-wizard/src/components/WizardFormStep.js
@@ -1,12 +1,17 @@
 import PropTypes from 'prop-types'
 import { connect } from 'react-redux'
-import { debounce, pick, get, isEqual } from 'lodash'
 import { actions } from 'pubsweet-client'
-import { compose, getContext, withProps } from 'recompose'
+import { debounce, pick, get, isEqual } from 'lodash'
+import { compose, getContext, withProps, setDisplayName } from 'recompose'
 import { reduxForm, formValueSelector, SubmissionError } from 'redux-form'
 
 import WizardStep from './WizardStep'
 import { autosaveRequest } from '../redux/autosave'
+import {
+  submitRevision,
+  isRevisionFlow,
+  submitManuscript as submitNewManuscript,
+} from '../redux/conversion'
 
 const wizardSelector = formValueSelector('wizard')
 
@@ -14,9 +19,8 @@ const onChange = (
   values,
   dispatch,
   { project, version, wizard: { formSectionKeys }, setLoader },
-  prevValues,
 ) => {
-  const prev = pick(prevValues, formSectionKeys)
+  const prev = pick(version, formSectionKeys)
   const newValues = pick(values, formSectionKeys)
   // TODO: fix this if it sucks down the road
   if (!isEqual(prev, newValues)) {
@@ -30,29 +34,36 @@ const onChange = (
   }
 }
 
-const submitManuscript = (
+const submitManuscript = ({
   values,
-  dispatch,
+  version,
   project,
+  history,
+  dispatch,
+  redirectPath = '/',
+}) => {
+  submitNewManuscript(project.id, version.id)
+    .then(() => {
+      history.push(redirectPath, {
+        project: project.id,
+        customId: project.customId,
+        version: version.id,
+      })
+    })
+    .catch(error => {
+      if (error.validationErrors) {
+        throw new SubmissionError()
+      }
+    })
+}
+
+const submitFragmentRevision = ({
   version,
+  project,
   history,
   redirectPath = '/',
-) => {
-  dispatch(
-    actions.updateFragment(project, {
-      id: version.id,
-      submitted: new Date(),
-      ...values,
-    }),
-  )
-    .then(() =>
-      dispatch(
-        actions.updateCollection({
-          id: project.id,
-          status: 'submitted',
-        }),
-      ),
-    )
+}) => {
+  submitRevision(project.id, version.id)
     .then(() => {
       history.push(redirectPath, {
         project: project.id,
@@ -71,14 +82,15 @@ const onSubmit = (
   values,
   dispatch,
   {
-    nextStep,
     isFinal,
     history,
     project,
     version,
+    nextStep,
     confirmation,
-    wizard: { confirmationModal, submissionRedirect, formSectionKeys },
+    isRevisionFlow,
     toggleConfirmation,
+    wizard: { confirmationModal, submissionRedirect, formSectionKeys },
     ...rest
   },
 ) => {
@@ -86,20 +98,27 @@ const onSubmit = (
     nextStep()
   } else if (confirmationModal && !confirmation) {
     toggleConfirmation()
-  } else {
-    const newValues = pick(values, formSectionKeys)
-    submitManuscript(
-      newValues,
-      dispatch,
+  } else if (isRevisionFlow) {
+    submitFragmentRevision({
+      version,
       project,
+      history,
+      redirectPath: submissionRedirect,
+    })
+  } else {
+    submitManuscript({
       version,
+      project,
       history,
-      submissionRedirect,
-    )
+      dispatch,
+      redirectPath: submissionRedirect,
+      values: pick(values, formSectionKeys),
+    })
   }
 }
 
 export default compose(
+  setDisplayName('SubmitWizard'),
   getContext({
     history: PropTypes.object,
     isFinal: PropTypes.bool,
@@ -111,13 +130,35 @@ export default compose(
     confirmation: PropTypes.bool,
     toggleConfirmation: PropTypes.func,
   }),
-  withProps(({ version, wizard }) => ({
-    initialValues: pick(version, wizard.formSectionKeys),
-    readonly: !!get(version, 'submitted'),
-  })),
-  connect((state, { wizard: { formSectionKeys } }) => ({
+  connect((state, { wizard: { formSectionKeys }, project, version }) => ({
     formValues: wizardSelector(state, ...formSectionKeys),
+    isRevisionFlow: isRevisionFlow(state, project, version),
   })),
+  withProps(
+    ({
+      version,
+      isFirst,
+      isFinal,
+      isRevisionFlow,
+      wizard: {
+        formSectionKeys,
+        backText = 'Back',
+        nextText = 'Next',
+        cancelText = 'Cancel',
+        submitText = 'Submit Manuscript',
+        revisionText = 'Submit Revision',
+      },
+    }) => ({
+      readonly: !!get(version, 'submitted'),
+      initialValues: pick(version, formSectionKeys),
+      buttons: {
+        backText: isFirst ? cancelText : backText,
+        nextText: !isFinal // eslint-disable-line
+          ? nextText
+          : isRevisionFlow ? revisionText : submitText,
+      },
+    }),
+  ),
   reduxForm({
     form: 'wizard',
     forceUnregisterOnUnmount: true,
diff --git a/packages/component-wizard/src/components/WizardStep.js b/packages/component-wizard/src/components/WizardStep.js
index 869bbd53c983e36b04ef7f0edc7b7689a5fb5450..a9a873575b7e77019ba0a515635dbf798ca4eb67 100644
--- a/packages/component-wizard/src/components/WizardStep.js
+++ b/packages/component-wizard/src/components/WizardStep.js
@@ -6,21 +6,22 @@ import { ValidatedField, Button, th } from '@pubsweet/ui'
 import AutosaveIndicator from './AutosaveIndicator'
 
 export default ({
-  children: stepChildren,
   title,
-  subtitle,
-  buttons,
-  nextStep,
-  prevStep,
-  handleSubmit,
+  wizard,
   isFinal,
   isFirst,
   history,
+  nextStep,
+  subtitle,
+  prevStep,
   formValues,
-  wizard,
   dispatchFns,
+  handleSubmit,
   confirmation,
+  isRevisionFlow,
   toggleConfirmation,
+  children: stepChildren,
+  buttons: { backText, nextText },
   wizard: { confirmationModal: ConfirmationModal },
   ...rest
 }) => (
@@ -39,7 +40,6 @@ export default ({
             validate,
             dependsOn,
             renderComponent: Comp,
-            format,
             parse,
             ...rest
           }) => {
@@ -50,18 +50,18 @@ export default ({
               return null
             }
             return (
-              <ValidatedField
-                component={input => (
-                  <div data-test={fieldId}>
-                    <Comp {...rest} {...input} {...dispatchFns} />{' '}
-                  </div>
-                )}
-                format={format}
-                key={fieldId}
-                name={fieldId}
-                parse={parse}
-                validate={validate}
-              />
+              <CustomValidatedField className="custom-field" key={fieldId}>
+                <ValidatedField
+                  component={input => (
+                    <div data-test={fieldId}>
+                      <Comp {...rest} {...input} {...dispatchFns} />
+                    </div>
+                  )}
+                  name={fieldId}
+                  parse={parse}
+                  validate={validate}
+                />
+              </CustomValidatedField>
             )
           },
         )}
@@ -70,14 +70,10 @@ export default ({
           data-test="button-prev"
           onClick={isFirst ? () => history.push('/') : prevStep}
         >
-          {isFirst
-            ? `${wizard.cancelText || 'Cancel'}`
-            : `${wizard.backText || 'Back'}`}
+          {backText}
         </Button>
         <Button data-test="button-next" primary type="submit">
-          {isFinal
-            ? `${wizard.submitText || 'Submit Manuscript'}`
-            : `${wizard.nextText || 'Next'}`}
+          {nextText}
         </Button>
       </ButtonContainer>
       {confirmation && (
@@ -90,6 +86,15 @@ export default ({
   </Root>
 )
 // #region styles
+
+const CustomValidatedField = styled.div`
+  div {
+    div:last-child {
+      margin-top: 0;
+    }
+  }
+`
+
 const Root = styled.div`
   align-items: stretch;
   background-color: ${th('colorTextReverse')};
diff --git a/packages/component-wizard/src/redux/conversion.js b/packages/component-wizard/src/redux/conversion.js
index a4043ca5dd365aadc19f2f7a9e7908e89a592c6a..b88ae87c8175ffbbfbcba7d92ff1286932821795 100644
--- a/packages/component-wizard/src/redux/conversion.js
+++ b/packages/component-wizard/src/redux/conversion.js
@@ -1,7 +1,7 @@
-import { pick } from 'lodash'
 import moment from 'moment'
+import { pick } from 'lodash'
 import { actions } from 'pubsweet-client'
-import { create } from 'pubsweet-client/src/helpers/api'
+import { create, update } from 'pubsweet-client/src/helpers/api'
 
 /* constants */
 export const CREATE_DRAFT_REQUEST = 'CREATE_DRAFT_REQUEST'
@@ -12,11 +12,6 @@ export const createDraftRequest = () => ({
   type: CREATE_DRAFT_REQUEST,
 })
 
-export const createDraftSuccess = draft => ({
-  type: CREATE_DRAFT_SUCCESS,
-  draft,
-})
-
 /* utils */
 const generateCustomId = () =>
   moment
@@ -24,19 +19,22 @@ const generateCustomId = () =>
     .toString()
     .slice(-7)
 
-const addSubmittingAuthor = (user, collectionId) => {
+export const isRevisionFlow = (state, collection, fragment = {}) =>
+  collection.fragments.length > 1 && !fragment.submitted
+
+/* actions */
+const addSubmittingAuthor = (user, collectionId, fragmentId) => {
   const author = {
-    ...pick(user, ['affiliation', 'email', 'firstName', 'lastName']),
+    ...pick(user, ['id', 'email', 'affiliation', 'firstName', 'lastName']),
     isSubmitting: true,
     isCorresponding: true,
   }
-  create(`/collections/${collectionId}/users`, {
+  create(`/collections/${collectionId}/fragments/${fragmentId}/users`, {
     role: 'author',
     ...author,
   })
 }
 
-/* actions */
 export const createDraftSubmission = history => (dispatch, getState) => {
   const currentUser = getState().currentUser.user
   return dispatch(
@@ -50,8 +48,11 @@ export const createDraftSubmission = history => (dispatch, getState) => {
     return dispatch(
       actions.createFragment(collection, {
         created: new Date(), // TODO: set on server
+        collectionId: collection.id,
         files: {
+          manuscripts: [],
           supplementary: [],
+          coverLetter: [],
         },
         fragmentType: 'version',
         metadata: {},
@@ -63,7 +64,7 @@ export const createDraftSubmission = history => (dispatch, getState) => {
       }
       const route = `/projects/${collection.id}/versions/${fragment.id}/submit`
       if (!currentUser.admin) {
-        addSubmittingAuthor(currentUser, collection.id)
+        addSubmittingAuthor(currentUser, collection.id, fragment.id)
       }
 
       // redirect after a short delay
@@ -74,6 +75,41 @@ export const createDraftSubmission = history => (dispatch, getState) => {
   })
 }
 
+export const submitManuscript = (collectionId, fragmentId) =>
+  create(`/collections/${collectionId}/fragments/${fragmentId}/submit`)
+
+export const createRevision = (
+  collection,
+  previousVersion,
+  history,
+) => dispatch => {
+  // copy invitations only if minor revision
+  const {
+    id,
+    submitted,
+    recommendations,
+    invitations,
+    ...prev
+  } = previousVersion
+  return dispatch(
+    actions.createFragment(collection, {
+      ...prev,
+      invitations: invitations.filter(inv => inv.isAccepted),
+      created: new Date(),
+      version: previousVersion.version + 1,
+    }),
+  ).then(({ fragment }) => {
+    const route = `/projects/${collection.id}/versions/${fragment.id}/submit`
+    window.setTimeout(() => {
+      history.push(route)
+    }, 10)
+    return fragment
+  })
+}
+
+export const submitRevision = (collId, fragId) =>
+  update(`/collections/${collId}/fragments/${fragId}/submit`)
+
 /* reducer */
 const initialState = {
   complete: undefined,
diff --git a/packages/components-faraday/package.json b/packages/components-faraday/package.json
index e6e08d40317a695c3aedff0b059a2a7a12eb7608..729e5ebdbf6769f3f0065943e96c68f12ef0e215 100644
--- a/packages/components-faraday/package.json
+++ b/packages/components-faraday/package.json
@@ -4,7 +4,8 @@
   "main": "src",
   "license": "MIT",
   "dependencies": {
-    "@pubsweet/ui": "^3.1.0",
+    "@pubsweet/ui": "4.1.3",
+    "@pubsweet/ui-toolkit": "latest",
     "moment": "^2.22.1",
     "prop-types": "^15.5.10",
     "react": "^16.1.0",
@@ -15,7 +16,14 @@
     "react-tippy": "^1.2.2",
     "recompose": "^0.26.0",
     "redux": "^3.6.0",
-    "redux-form": "^7.0.3",
+    "redux-form": "7.0.3",
     "styled-components": "^3.1.6"
+  },
+  "scripts": {
+    "test": "jest"
+  },
+  "jest": {
+    "verbose": true,
+    "testRegex": "/src/.*.test.js$"
   }
 }
diff --git a/packages/components-faraday/src/components/Admin/AddEditUser.js b/packages/components-faraday/src/components/Admin/AddEditUser.js
index 08223f1067bbcd57b9f9e4be533c21bac1654b27..2b76bde79831c4296b5bfd4c555fa5c178c538e7 100644
--- a/packages/components-faraday/src/components/Admin/AddEditUser.js
+++ b/packages/components-faraday/src/components/Admin/AddEditUser.js
@@ -49,6 +49,7 @@ const AddEditUser = ({
   user,
   history,
   error,
+  submitting,
 }) => (
   <Root>
     <FormContainer onSubmit={handleSubmit}>
@@ -68,7 +69,7 @@ const AddEditUser = ({
       )}
       <Row>
         <Button onClick={history.goBack}>Back</Button>
-        <Button primary type="submit">
+        <Button disabled={submitting} primary type="submit">
           Save user
         </Button>
       </Row>
diff --git a/packages/components-faraday/src/components/Admin/EditUserForm.js b/packages/components-faraday/src/components/Admin/EditUserForm.js
index 20971b7faf88ed16396bc5efd7c6e9d98f905c67..95025eea1ab0eaf5174df5a7abd2d5c357890daa 100644
--- a/packages/components-faraday/src/components/Admin/EditUserForm.js
+++ b/packages/components-faraday/src/components/Admin/EditUserForm.js
@@ -108,11 +108,17 @@ const Row = styled.div`
   display: flex;
   flex-direction: row;
   margin: calc(${th('subGridUnit')}*3) 0;
+  div[role='alert'] {
+    margin-top: 0;
+  }
 `
 
 const RowItem = styled.div`
   flex: 1;
   margin-right: calc(${th('subGridUnit')}*3);
+  label + div[role='alert'] {
+    margin-top: 0;
+  }
 `
 
 const Title = styled.h4`
diff --git a/packages/components-faraday/src/components/Admin/utils.js b/packages/components-faraday/src/components/Admin/utils.js
index 9d1204cbbeaf5a4d05a82b63f5e8de425ac4f2b1..1029db45e7ae91d1919fa005dbb927398badd507 100644
--- a/packages/components-faraday/src/components/Admin/utils.js
+++ b/packages/components-faraday/src/components/Admin/utils.js
@@ -48,7 +48,8 @@ export const parseUpdateUser = values => {
 export const handleFormError = error => {
   const err = get(error, 'response')
   if (err) {
-    const errorMessage = get(JSON.parse(err), 'error')
+    const errorMessage =
+      get(JSON.parse(err), 'error') || get(JSON.parse(err), 'message')
     throw new SubmissionError({
       _error: errorMessage || 'Something went wrong',
     })
diff --git a/packages/components-faraday/src/components/AppBar/AppBar.js b/packages/components-faraday/src/components/AppBar/AppBar.js
index 9c04e8949ce0a1374f74314f3b920346534ec5dd..22f1f147483990676db2bebf431ade8038b2ce4e 100644
--- a/packages/components-faraday/src/components/AppBar/AppBar.js
+++ b/packages/components-faraday/src/components/AppBar/AppBar.js
@@ -6,63 +6,107 @@ import { withRouter } from 'react-router-dom'
 import styled, { withTheme } from 'styled-components'
 import { withState, withHandlers, compose } from 'recompose'
 
+import { userNotConfirmed } from 'pubsweet-component-faraday-selectors'
+
 const AppBar = ({
+  goTo,
+  user,
+  brand,
+  theme,
   expanded,
   toggleMenu,
-  brand,
-  user,
-  goTo,
   currentUser,
   onLogoutClick,
-  theme,
+  shouldShowConfirmation,
 }) => (
-  <Root>
-    <Brand>
-      {React.cloneElement(brand, {
-        onClick: goTo('/'),
-      })}
-    </Brand>
-    {user && (
-      <User>
-        <div onClick={toggleMenu}>
-          <Icon color={theme.colorText}>user</Icon>
-          <span>
-            {get(user, 'firstName') || get(user, 'username') || 'User'}
-          </span>
-          <Icon color={theme.colorText}>chevron-down</Icon>
-        </div>
-        {expanded && (
-          <Dropdown>
-            <DropdownOption>Settings</DropdownOption>
-            {currentUser.admin && (
-              <DropdownOption onClick={goTo('/admin')}>
-                Admin dashboard
-              </DropdownOption>
-            )}
-            <DropdownOption onClick={onLogoutClick}>Logout</DropdownOption>
-          </Dropdown>
-        )}
-      </User>
+  <Root className="appbar">
+    <Row bordered className="row">
+      <Brand>
+        {React.cloneElement(brand, {
+          onClick: goTo('/'),
+        })}
+      </Brand>
+      {user && (
+        <User>
+          <div onClick={toggleMenu}>
+            <Icon color={theme.colorText}>user</Icon>
+            <span>
+              {get(user, 'firstName') || get(user, 'username') || 'User'}
+            </span>
+            <Icon color={theme.colorText}>chevron-down</Icon>
+          </div>
+          {expanded && (
+            <Dropdown>
+              <DropdownOption>Settings</DropdownOption>
+              {currentUser.admin && (
+                <DropdownOption onClick={goTo('/admin')}>
+                  Admin dashboard
+                </DropdownOption>
+              )}
+              <DropdownOption onClick={onLogoutClick}>Logout</DropdownOption>
+            </Dropdown>
+          )}
+        </User>
+      )}
+    </Row>
+    {shouldShowConfirmation && (
+      <Row bgColor="salmon" centered className="row">
+        <ConfirmationText>
+          Your author account is not confirmed. Check your email.
+        </ConfirmationText>
+      </Row>
     )}
     {expanded && <ToggleOverlay onClick={toggleMenu} />}
   </Root>
 )
 
+export default compose(
+  withRouter,
+  withTheme,
+  connect(state => ({
+    currentUser: get(state, 'currentUser.user'),
+    shouldShowConfirmation: userNotConfirmed(state),
+  })),
+  withState('expanded', 'setExpanded', false),
+  withHandlers({
+    toggleMenu: ({ setExpanded }) => () => {
+      setExpanded(v => !v)
+    },
+    goTo: ({ setExpanded, history }) => path => () => {
+      setExpanded(v => false)
+      history.push(path)
+    },
+  }),
+)(AppBar)
+
 // #region styled-components
 const Root = styled.div`
   align-items: center;
-  box-shadow: ${th('dropShadow')};
+  background-color: #ffffff;
   font-family: ${th('fontInterface')};
   display: flex;
-  justify-content: space-between;
-  height: 60px;
+  flex-direction: column;
   flex-grow: 1;
+  width: 100vw;
+
   position: fixed;
-  width: 100%;
+  top: 0;
   z-index: 10;
-  background-color: #ffffff;
 `
 
+const Row = styled.div`
+  align-items: center;
+  align-self: stretch;
+  background-color: ${({ bgColor }) => bgColor || 'transparent'};
+  box-shadow: ${({ bordered }) => (bordered ? th('dropShadow') : 'none')};
+  display: flex;
+  justify-content: ${({ centered }) =>
+    centered ? 'center' : 'space-between;'};
+`
+
+const ConfirmationText = styled.span`
+  font-size: ${th('fontSizeBaseSmall')};
+`
 const Brand = styled.div`
   padding: 10px 20px;
   cursor: pointer;
@@ -92,12 +136,12 @@ const User = styled.div`
 
 const Dropdown = styled.div`
   background-color: ${th('colorBackground')};
-  border: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBorder')};
   position: absolute;
   right: 20px;
   top: 60px;
   width: calc(${th('gridUnit')} * 8);
   z-index: 10;
+  border: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBorder')};
 `
 
 const DropdownOption = styled.div`
@@ -126,21 +170,3 @@ const ToggleOverlay = styled.div`
   opacity: 0;
 `
 // #endregion
-
-export default compose(
-  withRouter,
-  withTheme,
-  connect(state => ({
-    currentUser: get(state, 'currentUser.user'),
-  })),
-  withState('expanded', 'setExpanded', false),
-  withHandlers({
-    toggleMenu: ({ setExpanded }) => () => {
-      setExpanded(v => !v)
-    },
-    goTo: ({ setExpanded, history }) => path => () => {
-      setExpanded(v => false)
-      history.push(path)
-    },
-  }),
-)(AppBar)
diff --git a/packages/components-faraday/src/components/AuthorList/Author.js b/packages/components-faraday/src/components/AuthorList/Author.js
index 5df7654e9b1ef7277a196a6981dbff69e8c2669b..d993d10b1074579a766a60f75e880cd07a00c3cd 100644
--- a/packages/components-faraday/src/components/AuthorList/Author.js
+++ b/packages/components-faraday/src/components/AuthorList/Author.js
@@ -19,6 +19,7 @@ export default ({
   setAuthorEdit,
   isCorresponding,
   parseAuthorType,
+  ...rest
 }) => (
   <Root isOver={isOver}>
     {!isOver && dragHandle}
@@ -27,10 +28,7 @@ export default ({
         <Title>{parseAuthorType(isSubmitting, isCorresponding, index)}</Title>
         <ButtonContainer>
           {!isSubmitting && (
-            <ClickableIcon
-              onClick={removeAuthor(id, email)}
-              title="Delete author"
-            >
+            <ClickableIcon onClick={removeAuthor(id)} title="Delete author">
               <Icon size={3}>trash</Icon>
             </ClickableIcon>
           )}
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
index 47264445eab33b0bc90a9a17bc7e84fa77a05aef..023eaa2c99b3243b6b7d9a010facd6ae17f87513 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorAdder.js
@@ -1,16 +1,15 @@
 import React from 'react'
 import { get } from 'lodash'
 import { connect } from 'react-redux'
-import { reduxForm } from 'redux-form'
 import styled from 'styled-components'
 import { Button, th } from '@pubsweet/ui'
-import { compose, withProps } from 'recompose'
 import { selectCurrentUser } from 'xpub-selectors'
+import { reduxForm, change as changeForm } from 'redux-form'
+import { compose, withProps, setDisplayName } from 'recompose'
 
 import { emailValidator } from '../utils'
 import { Spinner } from '../UIComponents/'
 import {
-  getAuthors,
   authorSuccess,
   authorFailure,
   getAuthorFetching,
@@ -77,6 +76,7 @@ export default compose(
       currentUser: selectCurrentUser(state),
     }),
     {
+      changeForm,
       authorSuccess,
       authorFailure,
     },
@@ -101,38 +101,28 @@ export default compose(
   reduxForm({
     form: 'author',
     enableReinitialize: true,
+    destroyOnUnmount: false,
     onSubmit: (
       values,
       dispatch,
-      { authors = [], addAuthor, setEditMode, setFormAuthors, reset, match },
+      { reset, match, changeForm, addAuthor, setEditMode, authors = [] },
     ) => {
       const collectionId = get(match, 'params.project')
+      const fragmentId = get(match, 'params.version')
       const isFirstAuthor = authors.length === 0
-      addAuthor(
-        {
-          ...values,
-          isSubmitting: isFirstAuthor,
-          isCorresponding: isFirstAuthor,
-        },
-        collectionId,
-      ).then(
-        () => {
-          setEditMode(false)()
-          setTimeout(() => {
-            getAuthors(collectionId).then(
-              data => {
-                dispatch(authorSuccess())
-                setFormAuthors(data)
-              },
-              err => dispatch(authorFailure(err)),
-            )
-          }, 10)
-          reset()
-        },
-        err => dispatch(authorFailure(err)),
-      )
+      const newAuthor = {
+        ...values,
+        isSubmitting: isFirstAuthor,
+        isCorresponding: isFirstAuthor,
+      }
+      addAuthor(newAuthor, collectionId, fragmentId).then(author => {
+        changeForm('wizard', 'authors', [...authors, author])
+        setEditMode(false)()
+        reset()
+      })
     },
   }),
+  setDisplayName('AuthorAdder'),
 )(AuthorAdder)
 
 // #region styled-components
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorEditor.js b/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
index 38deb2e6199096c7714b31fcdbbf9ecd0db8fe15..185296d68773dbb6c6abd6cbdad0d1d5b7acbb95 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorEditor.js
@@ -3,25 +3,26 @@ import { pick } from 'lodash'
 import { connect } from 'react-redux'
 import styled, { css } from 'styled-components'
 import { Icon, Checkbox, th } from '@pubsweet/ui'
-import { compose, withHandlers, withProps } from 'recompose'
+import { compose, withProps } from 'recompose'
 import { reduxForm, Field, change as changeForm } from 'redux-form'
 
 import { Spinner } from '../UIComponents'
 
 import {
-  getAuthors,
-  editAuthor,
   authorSuccess,
   authorFailure,
   getAuthorFetching,
 } from '../../redux/authors'
 import { ValidatedTextField, Label } from './FormItems'
 
+import { authorKeys, parseEditedAuthors } from './utils'
+
 const renderCheckbox = ({ input }) => (
   <Checkbox checked={input.value} type="checkbox" {...input} />
 )
 
 const AuthorEdit = ({
+  id,
   index,
   email,
   isFetching,
@@ -30,8 +31,6 @@ const AuthorEdit = ({
   setAuthorEdit,
   parseAuthorType,
   isCorresponding,
-  changeCorresponding,
-  ...rest
 }) => (
   <Root>
     <Header>
@@ -39,11 +38,7 @@ const AuthorEdit = ({
         <span>{parseAuthorType(isSubmitting, isCorresponding, index)}</span>
         {!isSubmitting && (
           <Fragment>
-            <Field
-              component={renderCheckbox}
-              name="edit.isCorresponding"
-              onChange={changeCorresponding(email)}
-            />
+            <Field component={renderCheckbox} name="edit.isCorresponding" />
             <label>Corresponding</label>
           </Fragment>
         )}
@@ -92,47 +87,19 @@ export default compose(
   ),
   withProps(props => ({
     initialValues: {
-      edit: pick(props, [
-        'id',
-        'email',
-        'lastName',
-        'firstName',
-        'affiliation',
-        'isSubmitting',
-        'isCorresponding',
-      ]),
+      edit: pick(props, authorKeys),
     },
   })),
-  withHandlers({
-    changeCorresponding: ({ changeForm, setAsCorresponding }) => email => (
-      evt,
-      newValue,
-    ) => {
-      setAsCorresponding(email)()
-      changeForm('edit', 'edit.isCorresponding', newValue)
-    },
-  }),
   reduxForm({
     form: 'edit',
     onSubmit: (
-      values,
+      { edit: newAuthor },
       dispatch,
-      { setAuthorEdit, setAuthors, authors, index, changeForm, project },
+      { authors, changeForm, setAuthorEdit },
     ) => {
-      const newAuthor = values.edit
-      editAuthor(project.id, newAuthor.id, newAuthor).then(
-        () => {
-          getAuthors(project.id).then(
-            data => {
-              dispatch(authorSuccess())
-              setAuthorEdit(-1)()
-              setAuthors(data)
-            },
-            err => dispatch(authorFailure(err)),
-          )
-        },
-        err => dispatch(authorFailure(err)),
-      )
+      const newAuthors = parseEditedAuthors(newAuthor, authors)
+      changeForm('wizard', 'authors', newAuthors)
+      setAuthorEdit(-1)()
     },
   }),
 )(AuthorEdit)
diff --git a/packages/components-faraday/src/components/AuthorList/AuthorList.js b/packages/components-faraday/src/components/AuthorList/AuthorList.js
index c6b03ef04c98938244acd3f7731886b24b9444ab..81a2561a58d7d6d3a21b4dde5b9cd1a6da263730 100644
--- a/packages/components-faraday/src/components/AuthorList/AuthorList.js
+++ b/packages/components-faraday/src/components/AuthorList/AuthorList.js
@@ -1,40 +1,40 @@
 import React from 'react'
+import { get, isBoolean, isNumber } from 'lodash'
 import { th } from '@pubsweet/ui'
 import PropTypes from 'prop-types'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
 import { withRouter } from 'react-router-dom'
+import { selectCurrentVersion } from 'xpub-selectors'
 import {
   compose,
-  lifecycle,
   withState,
+  withProps,
   getContext,
   withHandlers,
+  setDisplayName,
 } from 'recompose'
-import { change as changeForm } from 'redux-form'
+import { change as changeForm, formValueSelector } from 'redux-form'
 import { SortableList } from 'pubsweet-component-sortable-list/src/components'
 
 import {
   addAuthor,
-  getAuthors,
   deleteAuthor,
   authorFailure,
-  getAuthorsTeam,
   getAuthorError,
-  updateAuthorsTeam,
 } from '../../redux/authors'
 
-import Author from './Author'
-import StaticList from './StaticList'
-import AuthorAdder from './AuthorAdder'
 import { DragHandle } from './FormItems'
-import AuthorEditor from './AuthorEditor'
+import { Author, StaticList, AuthorAdder, AuthorEditor } from './'
+
+const wizardSelector = formValueSelector('wizard')
 
 const Authors = ({
   match,
   error,
   authors,
   version,
+  addMode,
   editMode,
   dropItem,
   addAuthor,
@@ -50,25 +50,25 @@ const Authors = ({
       addAuthor={addAuthor}
       authors={authors}
       editAuthor={editAuthor}
-      editMode={editMode}
+      editMode={addMode}
       match={match}
       setEditMode={setEditMode}
       setFormAuthors={setFormAuthors}
     />
-    {editedAuthor > -1 ? (
+    {isNumber(editMode) && editMode > -1 ? (
       <StaticList
         authors={authors}
         editComponent={AuthorEditor}
-        editIndex={editedAuthor}
+        editIndex={editMode}
         setFormAuthors={setFormAuthors}
+        version={version}
         {...rest}
       />
     ) : (
       <SortableList
         beginDragProps={['index', 'lastName']}
         dragHandle={DragHandle}
-        dropItem={dropItem}
-        editedAuthor={editedAuthor}
+        editedAuthor={editMode}
         items={authors || []}
         listItem={Author}
         moveItem={moveAuthor}
@@ -83,9 +83,11 @@ export default compose(
   withRouter,
   getContext({ version: PropTypes.object, project: PropTypes.object }),
   connect(
-    state => ({
-      currentUser: state.currentUser.user,
+    (state, { project }) => ({
       error: getAuthorError(state),
+      currentUser: get(state, 'currentUser.user'),
+      version: selectCurrentVersion(state, project),
+      authorForm: wizardSelector(state, 'authorForm'),
     }),
     {
       addAuthor,
@@ -95,86 +97,49 @@ export default compose(
     },
   ),
   withState('authors', 'setAuthors', []),
-  withState('editMode', 'setEditMode', false),
-  withState('editedAuthor', 'setEditedAuthor', -1),
+  withProps(({ version, authorForm }) => ({
+    authors: get(version, 'authors') || [],
+    addMode: isBoolean(authorForm) && authorForm,
+    editMode: isNumber(authorForm) ? authorForm : -1,
+  })),
   withHandlers({
-    setFormAuthors: ({ setAuthors, changeForm }) => authors => {
+    setFormAuthors: ({ setAuthors, changeForm }) => (authors = []) => {
       setAuthors(authors)
       changeForm('wizard', 'authors', authors)
     },
   }),
   withHandlers({
-    setAuthorEdit: ({ setEditedAuthor, changeForm }) => editedAuthor => e => {
+    setAuthorEdit: ({ changeForm }) => authorIndex => e => {
       e && e.preventDefault && e.preventDefault()
-      changeForm('wizard', 'editMode', editedAuthor > -1)
-      setEditedAuthor(prev => editedAuthor)
+      changeForm('wizard', 'authorForm', authorIndex)
     },
-    setEditMode: ({ setEditMode, changeForm }) => mode => e => {
+    setEditMode: ({ changeForm }) => mode => e => {
       e && e.preventDefault()
-      changeForm('wizard', 'editMode', mode)
-      setEditMode(v => mode)
-    },
-    dropItem: ({ authors, setFormAuthors, project, authorFailure }) => () => {
-      setFormAuthors(authors)
-      getAuthorsTeam(project.id)
-        .then(team => {
-          const members = authors.map(a => a.id)
-          updateAuthorsTeam(team.id, { members }).catch(err => {
-            authorFailure(err)
-            getAuthors(project.id).then(setFormAuthors)
-          })
-        })
-        .catch(err => {
-          authorFailure(err)
-          getAuthors(project.id).then(setFormAuthors)
-        })
+      changeForm('wizard', 'authorForm', mode)
     },
     parseAuthorType: () => (isSubmitting, isCorresponding, index) => {
       if (isSubmitting) return `#${index + 1} Submitting author`
       if (isCorresponding) return `#${index + 1} Corresponding author`
       return `#${index + 1} Author`
     },
-    moveAuthor: ({ authors, setFormAuthors, changeForm }) => (
-      dragIndex,
-      hoverIndex,
-    ) => {
+    moveAuthor: ({ authors, setFormAuthors }) => (dragIndex, hoverIndex) => {
       const newAuthors = SortableList.moveItem(authors, dragIndex, hoverIndex)
       setFormAuthors(newAuthors)
     },
     removeAuthor: ({
       authors,
+      version,
       project,
       deleteAuthor,
-      authorFailure,
       setFormAuthors,
-    }) => (id, authorEmail) => () => {
-      deleteAuthor(project.id, id).then(
-        () => {
-          const newAuthors = authors.filter(a => a.id !== id)
-          setFormAuthors(newAuthors)
-        },
-        err => authorFailure(err),
-      )
-    },
-    setAsCorresponding: ({ authors, setFormAuthors }) => authorEmail => () => {
-      const newAuthors = authors.map(
-        a =>
-          a.email === authorEmail
-            ? {
-                ...a,
-                isCorresponding: !a.isCorresponding,
-              }
-            : { ...a, isCorresponding: false },
-      )
-      setFormAuthors(newAuthors)
-    },
-  }),
-  lifecycle({
-    componentDidMount() {
-      const { setFormAuthors, project } = this.props
-      getAuthors(project.id).then(setFormAuthors)
+    }) => id => () => {
+      deleteAuthor(project.id, version.id, id).then(() => {
+        const newAuthors = authors.filter(a => a.id !== id)
+        setFormAuthors(newAuthors)
+      })
     },
   }),
+  setDisplayName('AuthorList'),
 )(Authors)
 
 // #region styled-components
diff --git a/packages/components-faraday/src/components/AuthorList/StaticList.js b/packages/components-faraday/src/components/AuthorList/StaticList.js
index 6e00a7a456175175eb4953ddfa8a82671c82d540..f6f82fc5bd649641563777bceb170b1e161847e5 100644
--- a/packages/components-faraday/src/components/AuthorList/StaticList.js
+++ b/packages/components-faraday/src/components/AuthorList/StaticList.js
@@ -3,14 +3,15 @@ import React from 'react'
 import Author from './Author'
 
 export default ({
+  version,
+  project,
   authors,
   editIndex,
-  setFormAuthors,
   removeAuthor,
   editComponent,
   setAuthorEdit,
+  setFormAuthors,
   parseAuthorType,
-  setAsCorresponding,
   ...rest
 }) => (
   <div>
@@ -19,17 +20,17 @@ export default ({
         index === editIndex ? (
           React.createElement(editComponent, {
             key: 'author-editor',
-            authors,
             index,
             initialValues: {
               edit: author,
             },
+            authors,
             setAuthors: setFormAuthors,
             setAuthorEdit,
             parseAuthorType,
-            setAsCorresponding,
+            project,
+            version,
             ...author,
-            ...rest,
           })
         ) : (
           <Author
@@ -38,7 +39,6 @@ export default ({
             index={index}
             parseAuthorType={parseAuthorType}
             removeAuthor={removeAuthor}
-            setAsCorresponding={setAsCorresponding}
             {...rest}
           />
         ),
diff --git a/packages/components-faraday/src/components/AuthorList/index.js b/packages/components-faraday/src/components/AuthorList/index.js
index 473f04dcd0b9fba6f38dd46e866126960656c2f2..1ff2016c20ac9622dde7315255596cff24fea844 100644
--- a/packages/components-faraday/src/components/AuthorList/index.js
+++ b/packages/components-faraday/src/components/AuthorList/index.js
@@ -1 +1,9 @@
+import * as utils from './utils'
+
+export { default as Author } from './Author'
 export { default as AuthorList } from './AuthorList'
+export { default as StaticList } from './StaticList'
+export { default as AuthorAdder } from './AuthorAdder'
+export { default as AuthorEditor } from './AuthorEditor'
+
+export { utils }
diff --git a/packages/components-faraday/src/components/AuthorList/utils.js b/packages/components-faraday/src/components/AuthorList/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..e58c4abec7befcf31d2d74dc9c4ecf0ceb324ab0
--- /dev/null
+++ b/packages/components-faraday/src/components/AuthorList/utils.js
@@ -0,0 +1,40 @@
+import { isBoolean } from 'lodash'
+
+export const authorKeys = [
+  'id',
+  'email',
+  'lastName',
+  'firstName',
+  'affiliation',
+  'isSubmitting',
+  'isCorresponding',
+]
+
+export const setCorresponding = id => author =>
+  author.id === id
+    ? {
+        ...author,
+        isCorresponding: true,
+      }
+    : { ...author, isCorresponding: false }
+
+export const castToBool = author => ({
+  ...author,
+  isCorresponding: isBoolean(author.isCorresponding) && author.isCorresponding,
+})
+
+export const parseEditedAuthors = (editedAuthor, authors) => {
+  const newAuthor = castToBool(editedAuthor)
+
+  return authors.map(
+    a =>
+      a.id === newAuthor.id
+        ? newAuthor
+        : {
+            ...a,
+            isCorresponding: newAuthor.isCorresponding
+              ? false
+              : a.isCorresponding,
+          },
+  )
+}
diff --git a/packages/components-faraday/src/components/Dashboard/AssignHEModal.js b/packages/components-faraday/src/components/Dashboard/AssignHEModal.js
index eb201ac3b5ed9a70ff97f57f435f8710d1b37bd2..31422da54264f232d1cf04b0f3f4ecb5034c5f80 100644
--- a/packages/components-faraday/src/components/Dashboard/AssignHEModal.js
+++ b/packages/components-faraday/src/components/Dashboard/AssignHEModal.js
@@ -6,7 +6,7 @@ import { compose } from 'recompose'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
 import { actions } from 'pubsweet-client'
-import { th, Icon, Spinner } from '@pubsweet/ui'
+import { th, Icon, ErrorText, Spinner } from '@pubsweet/ui'
 
 import {
   selectFetching,
@@ -61,7 +61,7 @@ class AssignHEModal extends React.Component {
 
   render() {
     const { searchInput } = this.state
-    const { editors, hideModal, isFetching } = this.props
+    const { editors, hideModal, isFetching, modalError } = this.props
     const filteredEditors = this.filterEditors(editors)
     return (
       <RootModal>
@@ -105,6 +105,7 @@ class AssignHEModal extends React.Component {
             ))}
           </ModalContent>
         </ScrollContainer>
+        <CustomError>{modalError}</CustomError>
       </RootModal>
     )
   }
@@ -125,6 +126,11 @@ export default compose(
 )(AssignHEModal)
 
 // #region styled-components
+const CustomError = ErrorText.extend`
+  font-family: ${th('fontReading')};
+  margin: ${th('subGridUnit')} 0;
+`
+
 const SubtitleRow = styled.div`
   display: flex;
   justify-content: space-between;
diff --git a/packages/components-faraday/src/components/Dashboard/Dashboard.js b/packages/components-faraday/src/components/Dashboard/Dashboard.js
index c740018baa68358e10070e8457e3d53019086945..190c45f88577b15dee6746099b4db3c5ffe8da23 100644
--- a/packages/components-faraday/src/components/Dashboard/Dashboard.js
+++ b/packages/components-faraday/src/components/Dashboard/Dashboard.js
@@ -1,29 +1,26 @@
 import React from 'react'
 import styled from 'styled-components'
 import { Button, th } from '@pubsweet/ui'
-import { compose, withHandlers } from 'recompose'
+import { compose, withProps } from 'recompose'
 
 import { DashboardItems, DashboardFilters } from './'
 
 const Dashboard = ({
-  filters,
-  getItems,
-  dashboard,
-  currentUser,
-  filterItems,
-  filterValues,
   deleteProject,
+  dashboardItems,
+  canCreateDraft,
   getFilterOptions,
   changeFilterValue,
   createDraftSubmission,
-  ...rest
+  getDefaultFilterValue,
 }) => (
-  <Root>
+  <Root className="dashboard">
     <Header>
       <Heading>Manuscripts</Heading>
       <HeaderButtons>
         <Button
           data-test="new-manuscript"
+          disabled={!canCreateDraft}
           onClick={createDraftSubmission}
           primary
         >
@@ -33,26 +30,17 @@ const Dashboard = ({
     </Header>
     <DashboardFilters
       changeFilterValue={changeFilterValue}
+      getDefaultFilterValue={getDefaultFilterValue}
       getFilterOptions={getFilterOptions}
     />
-    <DashboardItems deleteProject={deleteProject} list={getItems()} />
+    <DashboardItems deleteProject={deleteProject} list={dashboardItems} />
   </Root>
 )
 
 export default compose(
-  withHandlers({
-    getItems: ({
-      filters,
-      dashboard,
-      currentUser,
-      filterItems,
-      filterValues = {},
-    }) => () =>
-      filterItems(dashboard.all).sort((a, b) => {
-        if (filterValues.order === 'newest') return a.created - b.created < 0
-        return a.created - b.created > 0
-      }),
-  }),
+  withProps(({ dashboard, filterItems }) => ({
+    dashboardItems: filterItems(dashboard.all),
+  })),
 )(Dashboard)
 
 // #region styles
@@ -61,6 +49,8 @@ const Root = styled.div`
   flex-direction: column;
   margin: auto;
   max-width: 60em;
+  min-height: 50vh;
+  overflow: auto;
 `
 
 const Header = styled.div`
diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js
index 82f9a8698c43046994924dcc4e59c40b0a313043..59638dda62cd0f0059f530d3a11105723c954d82 100644
--- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js
+++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js
@@ -1,8 +1,9 @@
 import React from 'react'
 import { get } from 'lodash'
-import { connect } from 'react-redux'
 import PropTypes from 'prop-types'
-import { Button, Icon, th } from '@pubsweet/ui'
+import { connect } from 'react-redux'
+import { th } from '@pubsweet/ui-toolkit'
+import { Button, Icon } from '@pubsweet/ui'
 import styled, { css, withTheme } from 'styled-components'
 import { compose, getContext, setDisplayName } from 'recompose'
 import { DateParser } from 'pubsweet-components-faraday/src/components'
@@ -15,8 +16,8 @@ import { selectInvitation } from '../../redux/reviewers'
 import { ReviewerDecision, HandlingEditorSection, DeleteManuscript } from './'
 import { parseVersion, parseJournalIssue, mapStatusToLabel } from './../utils'
 import {
-  currentUserIs,
   canMakeDecision,
+  isHEToManuscript,
   canInviteReviewers,
   canMakeRecommendation,
 } from '../../../../component-faraday-selectors/src'
@@ -84,7 +85,9 @@ const DashboardCard = ({
                   <Icon>download</Icon>
                 </ClickableIcon>
               </ZipFiles>
-              {!project.status && (
+              {(!project.status ||
+                project.status === 'draft' ||
+                !submittedDate) && (
                 <ActionButtons
                   data-test="button-resume-submission"
                   onClick={() =>
@@ -116,18 +119,20 @@ const DashboardCard = ({
             <ManuscriptType title={manuscriptMeta}>
               {manuscriptMeta}
             </ManuscriptType>
-            {project.status ? (
-              <Details
-                data-test="button-details"
-                onClick={() =>
-                  history.push(
-                    `/projects/${project.id}/versions/${version.id}/details`,
-                  )
-                }
-              >
-                Details
-                <Icon primary>chevron-right</Icon>
-              </Details>
+            {project.status && project.status !== 'draft' ? (
+              submittedDate && (
+                <Details
+                  data-test="button-details"
+                  onClick={() =>
+                    history.push(
+                      `/projects/${project.id}/versions/${version.id}/details`,
+                    )
+                  }
+                >
+                  Details
+                  <Icon primary>chevron-right</Icon>
+                </Details>
+              )
             ) : (
               <DeleteManuscript
                 deleteProject={() => deleteProject(project)}
@@ -137,45 +142,48 @@ const DashboardCard = ({
           </RightDetails>
         </Bottom>
       </ListView>
-      {project.status && (
-        <DetailsView>
-          <Top>
-            <AuthorsWithTooltip authors={project.authors} />
-          </Top>
-          <Bottom>
-            <LeftDetails flex={4}>
-              <HandlingEditorSection
-                currentUser={currentUser}
-                project={project}
-              />
-            </LeftDetails>
-            {canInviteReviewers && (
-              <RightDetails flex={4}>
-                <ReviewerBreakdown
-                  collectionId={project.id}
-                  compact
-                  versionId={version.id}
-                />
-                <InviteReviewers
-                  modalKey={`invite-reviewers-${project.id}`}
-                  project={project}
-                  version={version}
-                />
-              </RightDetails>
-            )}
-            {invitation && (
-              <RightDetails flex={4}>
-                <ReviewerText>Invited to review</ReviewerText>
-                <ReviewerDecision
-                  invitation={invitation}
-                  modalKey={`reviewer-decision-${project.id}`}
+      {project.status &&
+        project.status !== 'draft' && (
+          <DetailsView>
+            <Top>
+              <AuthorsWithTooltip authors={version.authors} />
+            </Top>
+            <Bottom>
+              <LeftDetails flex={4}>
+                <HandlingEditorSection
+                  currentUser={currentUser}
+                  isHE={isHE}
                   project={project}
                 />
-              </RightDetails>
-            )}
-          </Bottom>
-        </DetailsView>
-      )}
+              </LeftDetails>
+              {canInviteReviewers && (
+                <RightDetails flex={4}>
+                  <ReviewerBreakdown
+                    collectionId={project.id}
+                    compact
+                    versionId={version.id}
+                  />
+                  <InviteReviewers
+                    modalKey={`invite-reviewers-${project.id}`}
+                    project={project}
+                    version={version}
+                  />
+                </RightDetails>
+              )}
+              {invitation && (
+                <RightDetails flex={4}>
+                  <ReviewerText>Invited to review</ReviewerText>
+                  <ReviewerDecision
+                    invitation={invitation}
+                    modalKey={`reviewer-decision-${project.id}`}
+                    project={project}
+                    version={version}
+                  />
+                </RightDetails>
+              )}
+            </Bottom>
+          </DetailsView>
+        )}
     </Card>
   ) : null
 }
@@ -184,11 +192,11 @@ export default compose(
   setDisplayName('DashboardCard'),
   getContext({ journal: PropTypes.object, currentUser: PropTypes.object }),
   withTheme,
-  connect((state, { project }) => ({
-    isHE: currentUserIs(state, 'handlingEditor'),
-    invitation: selectInvitation(state, project.id),
+  connect((state, { project, version }) => ({
     canMakeDecision: canMakeDecision(state, project),
+    isHE: isHEToManuscript(state, get(project, 'id')),
     canInviteReviewers: canInviteReviewers(state, project),
+    invitation: selectInvitation(state, get(version, 'id')),
     canMakeRecommendation: canMakeRecommendation(state, project),
   })),
 )(DashboardCard)
diff --git a/packages/components-faraday/src/components/Dashboard/DashboardFilters.js b/packages/components-faraday/src/components/Dashboard/DashboardFilters.js
index 0d0cfbd37e1011ff5b9ea562abd77b16e67d8e43..6be689907a69989c4293f5e198fd9039cb894d53 100644
--- a/packages/components-faraday/src/components/Dashboard/DashboardFilters.js
+++ b/packages/components-faraday/src/components/Dashboard/DashboardFilters.js
@@ -4,44 +4,31 @@ import { Menu, th } from '@pubsweet/ui'
 import { compose, withHandlers } from 'recompose'
 
 const DashboardFilters = ({
-  // view,
-  status,
-  listView,
-  createdAt,
-  changeSort,
-  changeFilter,
   getFilterOptions,
   changeFilterValue,
+  getDefaultFilterValue,
 }) => (
-  <Root>
-    <FiltersContainer>
-      <span>Filter view:</span>
-      <FilterGroup>
-        <span>Owner</span>
-        <Menu
-          inline
-          onChange={changeFilterValue('owner')}
-          options={getFilterOptions('owner')}
-        />
-      </FilterGroup>
-      <FilterGroup>
-        <span>Status</span>
-        <Menu
-          inline
-          onChange={changeFilterValue('status')}
-          options={getFilterOptions('status')}
-        />
-      </FilterGroup>
-      <FilterGroup>
-        <span>Sort</span>
-        <Menu
-          inline
-          onChange={changeFilterValue('order')}
-          options={getFilterOptions('order')}
-        />
-      </FilterGroup>
-    </FiltersContainer>
-  </Root>
+  <FiltersContainer>
+    <span>Filter view:</span>
+    <FilterGroup>
+      <span>Priority</span>
+      <Menu
+        inline
+        onChange={changeFilterValue('priority')}
+        options={getFilterOptions('priority')}
+        value={getDefaultFilterValue('priority')}
+      />
+    </FilterGroup>
+    <FilterGroup>
+      <span>Sort</span>
+      <Menu
+        inline
+        onChange={changeFilterValue('order')}
+        options={getFilterOptions('order')}
+        value={getDefaultFilterValue('order')}
+      />
+    </FilterGroup>
+  </FiltersContainer>
 )
 
 export default compose(
@@ -53,19 +40,14 @@ export default compose(
 )(DashboardFilters)
 
 // #region styles
-
-const Root = styled.div`
+const FiltersContainer = styled.div`
+  align-items: center;
   border-bottom: ${th('borderDefault')};
   color: ${th('colorPrimary')};
   display: flex;
-  justify-content: space-between;
+  justify-content: flex-start;
   margin: calc(${th('subGridUnit')} * 2) 0;
   padding-bottom: calc(${th('subGridUnit')} * 2);
-`
-
-const FiltersContainer = styled.div`
-  align-items: center;
-  display: flex;
 
   > span {
     align-self: flex-end;
@@ -79,6 +61,8 @@ const FilterGroup = styled.div`
   display: flex;
   flex-direction: column;
   margin-left: calc(${th('subGridUnit')} * 2);
+  > div {
+    min-width: 200px;
+  }
 `
-
 // #endregion
diff --git a/packages/components-faraday/src/components/Dashboard/DashboardPage.js b/packages/components-faraday/src/components/Dashboard/DashboardPage.js
index 48dd1bfd37e6c67d4d92489197eb435a65b32a4c..dce45132340db9e88b54fd63e0f61dc1fada9446 100644
--- a/packages/components-faraday/src/components/Dashboard/DashboardPage.js
+++ b/packages/components-faraday/src/components/Dashboard/DashboardPage.js
@@ -9,9 +9,14 @@ import { compose, withContext } from 'recompose'
 import { newestFirst, selectCurrentUser } from 'xpub-selectors'
 import { createDraftSubmission } from 'pubsweet-component-wizard/src/redux/conversion'
 
+import {
+  userNotConfirmed,
+  getUserPermissions,
+} from 'pubsweet-component-faraday-selectors'
+
 import { Dashboard } from './'
-import withFilters from './withFilters'
 import { getHandlingEditors } from '../../redux/editors'
+import { priorityFilter, importanceSort, withFiltersHOC } from '../Filters'
 
 export default compose(
   ConnectPage(() => [actions.getCollections(), actions.getUsers()]),
@@ -19,7 +24,7 @@ export default compose(
     state => {
       const { collections, conversion } = state
       const currentUser = selectCurrentUser(state)
-
+      const canCreateDraft = !userNotConfirmed(state)
       const sortedCollections = newestFirst(collections)
 
       const dashboard = {
@@ -35,9 +40,18 @@ export default compose(
               reviewer => reviewer && reviewer.user === currentUser.id,
             ),
         ),
+
         all: sortedCollections,
       }
-      return { collections, conversion, currentUser, dashboard }
+      const userPermissions = getUserPermissions(state)
+      return {
+        dashboard,
+        conversion,
+        collections,
+        currentUser,
+        canCreateDraft,
+        userPermissions,
+      }
     },
     (dispatch, { history }) => ({
       deleteProject: collection =>
@@ -53,50 +67,9 @@ export default compose(
   ),
   withRouter,
   withJournal,
-  withFilters({
-    status: {
-      options: [
-        { label: 'All', value: 'all' },
-        { label: 'Submitted', value: 'submitted' },
-        { label: 'Draft', value: 'draft' },
-        { label: 'HE Invited', value: 'heInvited' },
-        { label: 'HE Assigned', value: 'heAssigned' },
-        { label: 'Reviewers Invited', value: 'reviewersInvited' },
-        { label: 'Under Review', value: 'underReview' },
-      ],
-      filterFn: filterValue => item => {
-        if (filterValue === 'all' || filterValue === '') return true
-        const itemStatus = get(item, 'status')
-        if (!itemStatus && filterValue === 'draft') {
-          return true
-        }
-        return itemStatus === filterValue
-      },
-    },
-    owner: {
-      options: [
-        { label: 'Everyone', value: 'all' },
-        { label: 'My work', value: 'me' },
-        { label: `Other's work`, value: 'other' },
-      ],
-      filterFn: (filterValue, { currentUser }) => item => {
-        if (filterValue === 'all' || filterValue === '') return true
-        const itemOwnerIds = item.owners.map(o => o.id)
-        if (filterValue === 'me') {
-          return itemOwnerIds.includes(currentUser.id)
-        } else if (filterValue === 'other') {
-          return !itemOwnerIds.includes(currentUser.id)
-        }
-        return false
-      },
-    },
-    order: {
-      options: [
-        { label: 'Newest first', value: 'newest' },
-        { label: 'Oldest first', value: 'oldest' },
-      ],
-      filterFn: () => () => true,
-    },
+  withFiltersHOC({
+    priority: priorityFilter,
+    order: importanceSort,
   }),
   withContext(
     {
diff --git a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js
index 342f530f148193ab803f78ec8898578c08f3f878..c00ce76139a9ce20065c4fe728581c06e625282e 100644
--- a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js
+++ b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js
@@ -98,6 +98,7 @@ export default compose(
         onConfirm: () =>
           assignHandlingEditor(get(editor, 'email'), project.id, true).then(
             () => {
+              getCollections()
               hideModal()
               showModal({
                 type: 'success',
@@ -170,6 +171,7 @@ const AssignButton = styled(Button)`
   background-color: ${th('colorPrimary')};
   color: ${th('colorTextReverse')};
   height: calc(${th('subGridUnit')} * 5);
+  padding: 0;
   text-align: center;
   text-transform: uppercase;
 `
diff --git a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js
index 82907e31d9d00c1fa355324265aaa4bf4b110b61..380e50fc3f4e0b23a723884097a956f2efc694d3 100644
--- a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js
+++ b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js
@@ -28,10 +28,10 @@ const DeclineModal = compose(
       value={reason}
     />
     <div data-test="he-buttons">
-      <Button onClick={hideModal}>Cancel</Button>
-      <Button onClick={onConfirm(reason)} primary>
+      <DecisionButton onClick={hideModal}>Cancel</DecisionButton>
+      <DecisionButton onClick={onConfirm(reason)} primary>
         Decline
-      </Button>
+      </DecisionButton>
     </div>
   </DeclineRoot>
 ))
@@ -119,6 +119,8 @@ const DecisionButton = styled(Button)`
   background-color: ${({ primary }) =>
     primary ? th('colorPrimary') : th('backgroundColorReverse')};
   height: calc(${th('subGridUnit')} * 5);
+  margin-left: ${th('gridUnit')};
+  padding: 0;
   text-align: center;
 `
 
@@ -158,7 +160,5 @@ const DeclineRoot = styled.div`
   }
 `
 
-const Root = styled.div`
-  margin-left: ${th('gridUnit')};
-`
+const Root = styled.div``
 // #endregion
diff --git a/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js b/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js
index b48d96eb07e3fecd249ab622ef560a4e4bce97a5..a77e2e32f7d1d83971d4b8268dc5323aefd27161 100644
--- a/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js
+++ b/packages/components-faraday/src/components/Dashboard/HandlingEditorSection.js
@@ -4,11 +4,11 @@ import { th } from '@pubsweet/ui'
 import styled, { css } from 'styled-components'
 import { EditorInChiefActions, HandlingEditorActions } from './'
 
-const renderHE = (currentUser, project) => {
+const renderHE = (currentUser, isHE, project) => {
   const status = get(project, 'status') || 'draft'
   const isAdmin = get(currentUser, 'admin')
   const isEic = get(currentUser, 'editorInChief')
-  const isHe = get(currentUser, 'handlingEditor')
+
   const handlingEditor = get(project, 'handlingEditor')
   const eicActionsStatuses = ['submitted', 'heInvited']
   const heActionsStatuses = ['heInvited']
@@ -22,7 +22,7 @@ const renderHE = (currentUser, project) => {
     )
   }
 
-  if (isHe && heActionsStatuses.includes(status)) {
+  if (isHE && heActionsStatuses.includes(status)) {
     return (
       <HandlingEditorActions
         currentUser={currentUser}
@@ -35,10 +35,10 @@ const renderHE = (currentUser, project) => {
   return <AssignedHE>{get(handlingEditor, 'name') || 'N/A'}</AssignedHE>
 }
 
-const HandlingEditorSection = ({ currentUser, project }) => (
+const HandlingEditorSection = ({ isHE, currentUser, project }) => (
   <Root>
     <HEText>Handling Editor</HEText>
-    {renderHE(currentUser, project)}
+    {renderHE(currentUser, isHE, project)}
   </Root>
 )
 
diff --git a/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js b/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js
index 6dece3361cd054c3615dd6df96e0be77ae086e92..45a32fdb91a2d83c174d77729f3c76c77bef9118 100644
--- a/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js
+++ b/packages/components-faraday/src/components/Dashboard/ReviewerDecision.js
@@ -30,20 +30,23 @@ const ModalComponent = connect(state => ({
 export default compose(
   connect(null, {
     reviewerDecision,
+    getFragments: actions.getFragments,
     getCollections: actions.getCollections,
   }),
   withModal(props => ({
     modalComponent: ModalComponent,
   })),
   withHandlers({
-    decisionSuccess: ({ getCollections, hideModal }) => () => {
+    decisionSuccess: ({ getFragments, getCollections, hideModal }) => () => {
       getCollections()
+      getFragments()
       hideModal()
     },
   }),
   withHandlers({
     showAcceptModal: ({
       project,
+      version,
       showModal,
       invitation,
       setModalError,
@@ -54,7 +57,7 @@ export default compose(
         title: 'Agree to review Manuscript?',
         confirmText: 'Agree',
         onConfirm: () => {
-          reviewerDecision(invitation.id, project.id, true).then(
+          reviewerDecision(invitation.id, project.id, version.id, true).then(
             decisionSuccess,
             handleError(setModalError),
           )
@@ -63,6 +66,7 @@ export default compose(
     },
     showDeclineModal: ({
       project,
+      version,
       showModal,
       invitation,
       setModalError,
@@ -73,7 +77,7 @@ export default compose(
         title: 'Decline to review Manuscript?',
         confirmText: 'Decline',
         onConfirm: () => {
-          reviewerDecision(invitation.id, project.id, false).then(
+          reviewerDecision(invitation.id, project.id, version.id, false).then(
             decisionSuccess,
             handleError(setModalError),
           )
@@ -95,8 +99,10 @@ const DecisionButton = styled(Button)`
   background-color: ${({ primary }) =>
     primary ? th('colorPrimary') : th('backgroundColorReverse')};
   color: ${({ primary }) =>
-    primary ? th('colorTextReverse') : th('colorPrimary')});
+    primary ? th('colorTextReverse') : th('colorPrimary')};
   height: calc(${th('subGridUnit')} * 5);
+  margin-left: ${th('gridUnit')};
+  padding: 0;
   text-align: center;
 `
 // #endregion
diff --git a/packages/components-faraday/src/components/Dashboard/withFilters.js b/packages/components-faraday/src/components/Dashboard/withFilters.js
deleted file mode 100644
index b51b7b6eba14585b15653a6481480b7c8bfa0193..0000000000000000000000000000000000000000
--- a/packages/components-faraday/src/components/Dashboard/withFilters.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { get } from 'lodash'
-import { compose, withState, withHandlers } from 'recompose'
-
-export default config => Component => {
-  const filterFns = Object.entries(config).map(([filterKey, { filterFn }]) => ({
-    key: filterKey,
-    fn: filterFn,
-  }))
-  const filterValues = Object.keys(config).reduce(
-    (acc, el) => ({ ...acc, [el]: '' }),
-    {},
-  )
-  return compose(
-    withState('filterValues', 'setFilterValues', filterValues),
-    withHandlers({
-      getFilterOptions: () => key => get(config, `${key}.options`) || [],
-      changeFilterValue: ({ setFilterValues }) => filterKey => value => {
-        setFilterValues(v => ({
-          ...v,
-          [filterKey]: value,
-        }))
-      },
-      filterItems: ({ filterValues, ...props }) => items =>
-        filterFns.reduce(
-          (acc, { key, fn }) => acc.filter(fn(filterValues[key], props)),
-          items,
-        ),
-    }),
-  )(Component)
-}
diff --git a/packages/components-faraday/src/components/Files/Files.js b/packages/components-faraday/src/components/Files/Files.js
index 3bf85519bc16e665a18fdbb36410cab86258a06c..f99ea6586e41b6af2ac0cabfefb960a3c9f48e8d 100644
--- a/packages/components-faraday/src/components/Files/Files.js
+++ b/packages/components-faraday/src/components/Files/Files.js
@@ -1,11 +1,12 @@
-import React from 'react'
-import { get } from 'lodash'
+import React, { Fragment } from 'react'
 import { th } from '@pubsweet/ui'
 import PropTypes from 'prop-types'
+import { get, isEqual } from 'lodash'
 import { connect } from 'react-redux'
 import styled from 'styled-components'
 import { withRouter } from 'react-router-dom'
 import { change as changeForm } from 'redux-form'
+import { selectCurrentVersion } from 'xpub-selectors'
 import {
   compose,
   lifecycle,
@@ -15,6 +16,7 @@ import {
   withHandlers,
 } from 'recompose'
 import { SortableList } from 'pubsweet-components-faraday/src/components'
+import { isRevisionFlow } from 'pubsweet-component-wizard/src/redux/conversion'
 
 import FileSection from './FileSection'
 import {
@@ -31,10 +33,11 @@ const Files = ({
   moveItem,
   removeFile,
   changeList,
+  isRevisionFlow,
   dropSortableFile,
 }) => (
-  <div>
-    <Error show={error}>Error uploading file, please try again.</Error>
+  <Fragment>
+    <Error show={error}> File error, please try again.</Error>
     <FileSection
       addFile={addFile('manuscripts')}
       allowedFileExtensions={['pdf', 'doc', 'docx']}
@@ -65,23 +68,43 @@ const Files = ({
       changeList={changeList}
       dropSortableFile={dropSortableFile}
       files={get(files, 'coverLetter') || []}
-      isLast
+      isLast={!isRevisionFlow}
       listId="coverLetter"
       maxFiles={1}
       moveItem={moveItem('coverLetter')}
       removeFile={removeFile('coverLetter')}
       title="Cover letter"
     />
-  </div>
+    {isRevisionFlow && (
+      <FileSection
+        addFile={addFile('responseToReviewers')}
+        allowedFileExtensions={['pdf', 'doc', 'docx']}
+        changeList={changeList}
+        dropSortableFile={dropSortableFile}
+        files={get(files, 'responseToReviewers') || []}
+        isLast={isRevisionFlow}
+        listId="responseToReviewer"
+        maxFiles={1}
+        moveItem={moveItem('responseToReviewers')}
+        removeFile={removeFile('responseToReviewers')}
+        title="Response to reviewers"
+      />
+    )}
+  </Fragment>
 )
 
 export default compose(
+  getContext({
+    project: PropTypes.object,
+    version: PropTypes.object,
+  }),
   withRouter,
-  getContext({ version: PropTypes.object, project: PropTypes.object }),
   connect(
-    state => ({
-      isFetching: getRequestStatus(state),
+    (state, { project, version }) => ({
       error: getFileError(state),
+      isFetching: getRequestStatus(state),
+      version: selectCurrentVersion(state, project),
+      isRevisionFlow: isRevisionFlow(state, project, version),
     }),
     {
       changeForm,
@@ -93,6 +116,7 @@ export default compose(
     manuscripts: [],
     coverLetter: [],
     supplementary: [],
+    responseToReviewers: [],
   }),
   lifecycle({
     componentDidMount() {
@@ -101,8 +125,16 @@ export default compose(
         manuscripts: get(files, 'manuscripts') || [],
         coverLetter: get(files, 'coverLetter') || [],
         supplementary: get(files, 'supplementary') || [],
+        responseToReviewers: get(files, 'responseToReviewers') || [],
       }))
     },
+    componentWillReceiveProps(nextProps) {
+      const { setFiles, version: { files: previousFiles } } = this.props
+      const { version: { files } } = nextProps
+      if (!isEqual(previousFiles, files)) {
+        setFiles(files)
+      }
+    },
   }),
   withHandlers({
     dropSortableFile: ({ files, setFiles, changeForm }) => (
@@ -137,10 +169,10 @@ export default compose(
     },
     addFile: ({
       files,
-      uploadFile,
+      version,
       setFiles,
+      uploadFile,
       changeForm,
-      version,
     }) => type => file => {
       uploadFile(file, type, version.id)
         .then(file => {
@@ -149,9 +181,7 @@ export default compose(
             [type]: [...files[type], file],
           }
           setFiles(newFiles)
-          setTimeout(() => {
-            changeForm('wizard', 'files', newFiles)
-          }, 1000)
+          changeForm('wizard', 'files', newFiles)
         })
         .catch(e => console.error(`Couldn't upload file.`, e))
     },
@@ -173,13 +203,16 @@ export default compose(
       version,
     }) => type => id => e => {
       e.preventDefault()
-      deleteFile(id)
-      const newFiles = {
-        ...files,
-        [type]: files[type].filter(f => f.id !== id),
-      }
-      setFiles(newFiles)
-      changeForm('wizard', 'files', files)
+      deleteFile(id, type)
+        .then(() => {
+          const newFiles = {
+            ...files,
+            [type]: files[type].filter(f => f.id !== id),
+          }
+          setFiles(newFiles)
+          changeForm('wizard', 'files', newFiles)
+        })
+        .catch(e => console.error(`Couldn't delete file.`, e))
     },
   }),
   withContext(
diff --git a/packages/components-faraday/src/components/Filters/importanceSort.js b/packages/components-faraday/src/components/Filters/importanceSort.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2491a9219a11728956995948c5c5338025715e8
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/importanceSort.js
@@ -0,0 +1,35 @@
+import { get } from 'lodash'
+
+import { utils } from './'
+import cfg from '../../../../xpub-faraday/config/default'
+
+const statuses = get(cfg, 'statuses')
+export const SORT_VALUES = {
+  MORE_IMPORTANT: 'more_important',
+  LESS_IMPORTANT: 'less_important',
+}
+
+const options = [
+  { label: 'Important first', value: SORT_VALUES.MORE_IMPORTANT },
+  { label: 'Less important first', value: SORT_VALUES.LESS_IMPORTANT },
+]
+
+const sortFn = sortValue => (item1, item2) => {
+  const item1Importance = utils.getCollectionImportance(statuses, item1)
+  const item2Importance = utils.getCollectionImportance(statuses, item2)
+
+  if (item1Importance - item2Importance === 0) {
+    return item1.created - item2.created
+  }
+
+  if (sortValue === SORT_VALUES.MORE_IMPORTANT) {
+    return item2Importance - item1Importance
+  }
+  return item1Importance - item2Importance
+}
+
+export default {
+  sortFn,
+  options,
+  type: 'sort',
+}
diff --git a/packages/components-faraday/src/components/Filters/importanceSort.test.js b/packages/components-faraday/src/components/Filters/importanceSort.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2c270dbbcdc40a45b9fb2f521714f8805c1bd29
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/importanceSort.test.js
@@ -0,0 +1,63 @@
+import fixturesService from 'pubsweet-component-fixture-service'
+
+import { importanceSort } from './'
+import { SORT_VALUES } from './importanceSort'
+
+const { sortFn } = importanceSort
+const { fixtures: { collections: { collection } } } = fixturesService
+
+describe('Importance sort', () => {
+  describe('Important items first', () => {
+    // the more important collection is already before the less important one
+    it('should return a negative value', () => {
+      const sortResult = sortFn(SORT_VALUES.MORE_IMPORTANT)(
+        { ...collection, status: 'pendingApproval' },
+        { ...collection, status: 'heAssigned' },
+      )
+      expect(sortResult).toBeLessThan(0)
+    })
+    // the more important collection is after a less important one
+    it('should return a positive value', () => {
+      const sortResult = sortFn(SORT_VALUES.MORE_IMPORTANT)(
+        { ...collection, status: 'heAssigned' },
+        { ...collection, status: 'pendingApproval' },
+      )
+      expect(sortResult).toBeGreaterThan(0)
+    })
+  })
+
+  describe('Less important items first', () => {
+    it('should return a positive value', () => {
+      const sortResult = sortFn(SORT_VALUES.LESS_IMPORTANT)(
+        { ...collection, status: 'pendingApproval' },
+        { ...collection, status: 'heAssigned' },
+      )
+      expect(sortResult).toBeGreaterThan(0)
+    })
+    it('should return a negative value', () => {
+      const sortResult = sortFn(SORT_VALUES.LESS_IMPORTANT)(
+        { ...collection, status: 'heAssigned' },
+        { ...collection, status: 'pendingApproval' },
+      )
+      expect(sortResult).toBeLessThan(0)
+    })
+  })
+
+  describe('Sort by date if both have the same', () => {
+    it('should place older item before newer item', () => {
+      const sortResult = sortFn(SORT_VALUES.LESS_IMPORTANT)(
+        { ...collection, status: 'heAssigned', created: Date.now() + 2000 },
+        { ...collection, status: 'heAssigned', created: Date.now() },
+      )
+      expect(sortResult).toBeGreaterThan(0)
+    })
+
+    it('should not move items', () => {
+      const sortResult = sortFn(SORT_VALUES.LESS_IMPORTANT)(
+        { ...collection, status: 'heAssigned', created: Date.now() },
+        { ...collection, status: 'heAssigned', created: Date.now() + 2000 },
+      )
+      expect(sortResult).toBeLessThan(0)
+    })
+  })
+})
diff --git a/packages/components-faraday/src/components/Filters/index.js b/packages/components-faraday/src/components/Filters/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..cec651407a1906360e0be3b88876740fde6ba1aa
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/index.js
@@ -0,0 +1,6 @@
+import * as utils from './utils'
+
+export { utils }
+export { default as withFiltersHOC } from './withFilters'
+export { default as priorityFilter } from './priorityFilter'
+export { default as importanceSort } from './importanceSort'
diff --git a/packages/components-faraday/src/components/Filters/priorityFilter.js b/packages/components-faraday/src/components/Filters/priorityFilter.js
new file mode 100644
index 0000000000000000000000000000000000000000..47ffbc88cb6193ba43299838d7474ff58d6337bc
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/priorityFilter.js
@@ -0,0 +1,53 @@
+import { get } from 'lodash'
+
+import { utils } from './'
+import cfg from '../../../../xpub-faraday/config/default'
+
+const statuses = get(cfg, 'statuses')
+
+export const FILTER_VALUES = {
+  ALL: 'all',
+  NEEDS_ATTENTION: 'needsAttention',
+  IN_PROGRESS: 'inProgress',
+  ARCHIVED: 'archived',
+}
+
+const options = [
+  { label: 'All', value: FILTER_VALUES.ALL },
+  { label: 'Needs Attention', value: FILTER_VALUES.NEEDS_ATTENTION },
+  { label: 'In Progress', value: FILTER_VALUES.IN_PROGRESS },
+  { label: 'Archived', value: FILTER_VALUES.ARCHIVED },
+]
+
+const archivedStatuses = ['withdrawn', 'accepted', 'rejected']
+
+const filterFn = (filterValue, { currentUser, userPermissions = [] }) => ({
+  id = '',
+  fragments = [],
+  status = 'draft',
+}) => {
+  if (filterValue === FILTER_VALUES.ARCHIVED) {
+    return archivedStatuses.includes(status)
+  }
+  const permission = userPermissions.find(
+    ({ objectId }) => objectId === id || fragments.includes(objectId),
+  )
+  const userRole = utils.getUserRole(currentUser, get(permission, 'role'))
+  switch (filterValue) {
+    case FILTER_VALUES.NEEDS_ATTENTION:
+      return get(statuses, `${status}.${userRole}.needsAttention`)
+    case FILTER_VALUES.IN_PROGRESS:
+      return (
+        !archivedStatuses.includes(status) &&
+        !get(statuses, `${status}.${userRole}.needsAttention`)
+      )
+    default:
+      return true
+  }
+}
+
+export default {
+  options,
+  filterFn,
+  type: 'filter',
+}
diff --git a/packages/components-faraday/src/components/Filters/priorityFilter.test.js b/packages/components-faraday/src/components/Filters/priorityFilter.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac2f07d2f7f5f4ffa937073fe3d96ea1bc6c88ec
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/priorityFilter.test.js
@@ -0,0 +1,436 @@
+import fixturesService from 'pubsweet-component-fixture-service'
+
+import { FILTER_VALUES } from './priorityFilter'
+import { priorityFilter, utils } from './'
+
+const {
+  fixtures: { collections: { collection }, users, teams },
+} = fixturesService
+
+const { filterFn } = priorityFilter
+
+describe('Priority filter function for reviewersInvited status', () => {
+  describe('ALL', () => {
+    it('should return true if ALL is selected', () => {
+      const filterResult = filterFn(FILTER_VALUES.ALL, { currentUser: {} })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+  })
+
+  describe('NEEDS ATTENTION', () => {
+    it('should return falsy for AUTHOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.author,
+        userPermissions: [utils.parsePermission(teams.authorTeam)],
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+
+    it('should return truthy for REVIEWER', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.reviewer,
+        userPermissions: [utils.parsePermission(teams.revTeam)],
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return truthy for HANDLING EDITOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.handlingEditor,
+        userPermissions: [utils.parsePermission(teams.heTeam)],
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return falsy for EDITOR IN CHIEF', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.editorInChief,
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return truthy for ADMIN', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+  })
+
+  describe('IN PROGRESS', () => {
+    it('should return truthy for AUTHOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.author,
+        userPermissions: [utils.parsePermission(teams.authorTeam)],
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+
+    it('should return falsy for REVIEWER', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.reviewer,
+        userPermissions: [utils.parsePermission(teams.revTeam)],
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return falsy for HANDLING EDITOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.handlingEditor,
+        userPermissions: [utils.parsePermission(teams.heTeam)],
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return truthy for EDITOR IN CHIEF', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.editorInChief,
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return falsy for ADMIN', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+  })
+
+  describe('ARCHIVED', () => {
+    it('should return falsy', () => {
+      const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'reviewersInvited',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+  })
+})
+
+describe('Priority filter function for technicalChecks status', () => {
+  describe('ALL', () => {
+    it('should return true if ALL is selected', () => {
+      const filterResult = filterFn(FILTER_VALUES.ALL, { currentUser: {} })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+  })
+
+  describe('NEEDS ATTENTION', () => {
+    it('should return falsy for AUTHOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.author,
+        userPermissions: [utils.parsePermission(teams.authorTeam)],
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+
+    it('should return truthy for REVIEWER', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.reviewer,
+        userPermissions: [utils.parsePermission(teams.revTeam)],
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return truthy for HANDLING EDITOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.handlingEditor,
+        userPermissions: [utils.parsePermission(teams.heTeam)],
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return falsy for EDITOR IN CHIEF', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.editorInChief,
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return truthy for ADMIN', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+  })
+
+  describe('IN PROGRESS', () => {
+    it('should return truthy for AUTHOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.author,
+        userPermissions: [utils.parsePermission(teams.authorTeam)],
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+
+    it('should return falsy for REVIEWER', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.reviewer,
+        userPermissions: [utils.parsePermission(teams.revTeam)],
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return falsy for HANDLING EDITOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.handlingEditor,
+        userPermissions: [utils.parsePermission(teams.heTeam)],
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return truthy for EDITOR IN CHIEF', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.editorInChief,
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return falsy for ADMIN', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+  })
+
+  describe('ARCHIVED', () => {
+    it('should return falsy', () => {
+      const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+  })
+})
+
+describe('Priority filter function for pendingApproval status', () => {
+  describe('ALL', () => {
+    it('should return true if ALL is selected', () => {
+      const filterResult = filterFn(FILTER_VALUES.ALL, { currentUser: {} })({
+        ...collection,
+        status: 'technicalChecks',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+  })
+
+  describe('NEEDS ATTENTION', () => {
+    it('should return falsy for AUTHOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.author,
+        userPermissions: [utils.parsePermission(teams.authorTeam)],
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+
+    it('should return truthy for REVIEWER', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.reviewer,
+        userPermissions: [utils.parsePermission(teams.revTeam)],
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return truthy for HANDLING EDITOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.handlingEditor,
+        userPermissions: [utils.parsePermission(teams.heTeam)],
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return falsy for EDITOR IN CHIEF', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.editorInChief,
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return truthy for ADMIN', () => {
+      const filterResult = filterFn(FILTER_VALUES.NEEDS_ATTENTION, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+  })
+
+  describe('IN PROGRESS', () => {
+    it('should return truthy for AUTHOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.author,
+        userPermissions: [utils.parsePermission(teams.authorTeam)],
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+
+    it('should return falsy for REVIEWER', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.reviewer,
+        userPermissions: [utils.parsePermission(teams.revTeam)],
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return falsy for HANDLING EDITOR', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.handlingEditor,
+        userPermissions: [utils.parsePermission(teams.heTeam)],
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeTruthy()
+    })
+    it('should return truthy for EDITOR IN CHIEF', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.editorInChief,
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+    it('should return falsy for ADMIN', () => {
+      const filterResult = filterFn(FILTER_VALUES.IN_PROGRESS, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+  })
+
+  describe('ARCHIVED', () => {
+    it('should return falsy', () => {
+      const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+        currentUser: users.admin,
+      })({
+        ...collection,
+        status: 'pendingApproval',
+      })
+      expect(filterResult).toBeFalsy()
+    })
+  })
+})
+
+describe('Priority filter function for archived statuses', () => {
+  it('should show rejected manuscripts', () => {
+    const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+      currentUser: users.admin,
+    })({
+      ...collection,
+      status: 'rejected',
+    })
+    expect(filterResult).toBeTruthy()
+  })
+
+  it('should show withdrawn manuscripts', () => {
+    const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+      currentUser: users.admin,
+    })({
+      ...collection,
+      status: 'withdrawn',
+    })
+    expect(filterResult).toBeTruthy()
+  })
+
+  it('should show accepted manuscripts', () => {
+    const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+      currentUser: users.admin,
+    })({
+      ...collection,
+      status: 'accepted',
+    })
+    expect(filterResult).toBeTruthy()
+  })
+
+  it('should not show pendingApproval manuscripts', () => {
+    const filterResult = filterFn(FILTER_VALUES.ARCHIVED, {
+      currentUser: users.admin,
+    })({
+      ...collection,
+      status: 'pendingApproval',
+    })
+    expect(filterResult).toBeFalsy()
+  })
+})
diff --git a/packages/components-faraday/src/components/Filters/utils.js b/packages/components-faraday/src/components/Filters/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..8dabf8b2f1c77b530bf761937f6a1b8fb96329a8
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/utils.js
@@ -0,0 +1,42 @@
+import { get } from 'lodash'
+
+export const hydrateFilters = defaultValues => {
+  const filterValues = localStorage.getItem('filterValues')
+  if (filterValues) return JSON.parse(filterValues)
+  return defaultValues
+}
+
+export const makeFilterFunctions = config =>
+  Object.entries(config)
+    .filter(([filterKey, { type }]) => type === 'filter')
+    .map(([filterKey, { filterFn }]) => ({
+      key: filterKey,
+      fn: filterFn,
+    }))
+
+export const makeSortFunction = config => {
+  const [sortKey, { sortFn }] = Object.entries(config).find(
+    ([filterKey, { type }]) => type !== 'filter',
+  )
+  return {
+    sortKey,
+    sortFn,
+  }
+}
+
+export const makeFilterValues = config =>
+  Object.keys(config).reduce((acc, el) => ({ ...acc, [el]: '' }), {})
+
+export const getUserRole = (user, role) => {
+  if (user.admin) return 'admin'
+  if (user.editorInChief) return 'editorInChief'
+  return role
+}
+
+export const parsePermission = permission => ({
+  objectId: permission.object.id,
+  role: permission.group,
+})
+
+export const getCollectionImportance = (statuses, item) =>
+  get(statuses, `${get(item, 'status') || 'draft'}.importance`)
diff --git a/packages/components-faraday/src/components/Filters/withFilters.js b/packages/components-faraday/src/components/Filters/withFilters.js
new file mode 100644
index 0000000000000000000000000000000000000000..d7a089f935293cb6345cfec1f1ccccca19e602fc
--- /dev/null
+++ b/packages/components-faraday/src/components/Filters/withFilters.js
@@ -0,0 +1,46 @@
+import { get } from 'lodash'
+import { compose, withState, withHandlers } from 'recompose'
+
+import { utils } from './'
+
+export default config => Component => {
+  const filterFns = utils.makeFilterFunctions(config)
+  const filterValues = utils.makeFilterValues(config)
+  const { sortKey, sortFn } = utils.makeSortFunction(config)
+
+  return compose(
+    withState(
+      'filterValues',
+      'setFilterValues',
+      utils.hydrateFilters(filterValues),
+    ),
+    withHandlers({
+      getFilterOptions: () => key => get(config, `${key}.options`) || [],
+      getDefaultFilterValue: ({ filterValues }) => key =>
+        get(filterValues, key) || '',
+      changeFilterValue: ({ setFilterValues }) => filterKey => value => {
+        // ugly but recompose doesn't pass the new state in the callback function
+        let newState = {}
+        setFilterValues(
+          v => {
+            newState = {
+              ...v,
+              [filterKey]: value,
+            }
+            return newState
+          },
+          () => {
+            localStorage.setItem('filterValues', JSON.stringify(newState))
+          },
+        )
+      },
+      filterItems: ({ filterValues, ...props }) => items =>
+        filterFns
+          .reduce(
+            (acc, { key, fn }) => acc.filter(fn(filterValues[key], props)),
+            items,
+          )
+          .sort(sortFn(filterValues[sortKey], props)),
+    }),
+  )(Component)
+}
diff --git a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js
index 6ced230390ad0b30dc5de6af1d9ff6baf53ef5be..d67820dc3764e31d5e71fd0ce0275185d662427d 100644
--- a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js
+++ b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js
@@ -31,7 +31,7 @@ export default compose(
     collection: selectCollection(state, collectionId),
   })),
   withHandlers({
-    getCompactReport: ({ collection: { invitations = [] } }) => () => {
+    getCompactReport: ({ fragment: { invitations = [] } }) => () => {
       const reviewerInvitations = invitations.filter(roleFilter('reviewer'))
       const accepted = reviewerInvitations.filter(acceptedInvitationFilter)
         .length
diff --git a/packages/components-faraday/src/components/MakeDecision/DecisionForm.js b/packages/components-faraday/src/components/MakeDecision/DecisionForm.js
index b7da20c1b2c2fcc4e7571a83a202f88150e0a1bc..0a53c9423317ac06571ebf6470b6ee16b957fb0f 100644
--- a/packages/components-faraday/src/components/MakeDecision/DecisionForm.js
+++ b/packages/components-faraday/src/components/MakeDecision/DecisionForm.js
@@ -1,39 +1,28 @@
 import React from 'react'
 import { get } from 'lodash'
 import { connect } from 'react-redux'
+import styled from 'styled-components'
 import { actions } from 'pubsweet-client'
 import { required } from 'xpub-validators'
-import styled, { css } from 'styled-components'
 import { reduxForm, formValueSelector } from 'redux-form'
 import { compose, setDisplayName, withProps } from 'recompose'
-import {
-  th,
-  Icon,
-  Button,
-  Spinner,
-  RadioGroup,
-  ValidatedField,
-} from '@pubsweet/ui'
+import { Icon, Button, RadioGroup, ValidatedField } from '@pubsweet/ui'
 
 import { FormItems } from '../UIComponents'
-import {
-  selectError,
-  selectFetching,
-  createRecommendation,
-} from '../../redux/recommendations'
+import { createRecommendation } from '../../redux/recommendations'
 import { subtitleParser, decisions, parseFormValues } from './utils'
 import { getHERecommendation } from '../../../../component-faraday-selectors'
 
 const {
-  Err,
   Row,
   Title,
   Label,
   RowItem,
-  Textarea,
   Subtitle,
   RootContainer,
   FormContainer,
+  TextAreaField,
+  CustomRadioGroup,
 } = FormItems
 const Form = RootContainer.withComponent(FormContainer)
 
@@ -41,9 +30,7 @@ const DecisionForm = ({
   aHERec,
   decision,
   hideModal,
-  isFetching,
   handleSubmit,
-  recommendationError,
   heRecommendation: { reason, message = '' },
 }) => (
   <Form onSubmit={handleSubmit}>
@@ -70,6 +57,7 @@ const DecisionForm = ({
         <ValidatedField
           component={input => (
             <CustomRadioGroup
+              className="custom-radio-group"
               justify={reason ? 'space-between' : 'space-around'}
             >
               <RadioGroup
@@ -88,32 +76,21 @@ const DecisionForm = ({
         <RowItem vertical>
           <Label>Comments for Handling Editor</Label>
           <ValidatedField
-            component={input => <Textarea {...input} height={70} />}
+            component={TextAreaField}
             name="messageToHE"
             validate={[required]}
           />
         </RowItem>
       </Row>
     )}
-    {recommendationError && (
-      <Row>
-        <RowItem centered>
-          <Err>{recommendationError}</Err>
-        </RowItem>
-      </Row>
-    )}
     <Row>
       <RowItem centered>
         <Button onClick={hideModal}>Cancel</Button>
       </RowItem>
       <RowItem centered>
-        {isFetching ? (
-          <Spinner size={3} />
-        ) : (
-          <Button primary type="submit">
-            Submit
-          </Button>
-        )}
+        <Button primary type="submit">
+          Submit
+        </Button>
       </RowItem>
     </Row>
   </Form>
@@ -124,12 +101,14 @@ export default compose(
   setDisplayName('DecisionForm'),
   connect(
     (state, { fragmentId, collectionId }) => ({
-      isFetching: selectFetching(state),
       decision: selector(state, 'decision'),
-      recommendationError: selectError(state),
       heRecommendation: getHERecommendation(state, collectionId, fragmentId),
     }),
-    { createRecommendation, getCollections: actions.getCollections },
+    {
+      createRecommendation,
+      getFragments: actions.getFragments,
+      getCollections: actions.getCollections,
+    },
   ),
   withProps(({ heRecommendation: { recommendation = '', comments = [] } }) => ({
     heRecommendation: {
@@ -144,31 +123,33 @@ export default compose(
       dispatch,
       {
         showModal,
+        hideModal,
         fragmentId,
         collectionId,
+        getFragments,
         getCollections,
         createRecommendation,
       },
     ) => {
       const recommendation = parseFormValues(values)
-      createRecommendation(collectionId, fragmentId, recommendation).then(r => {
-        getCollections()
-        showModal({
-          title: 'Decision submitted',
-          cancelText: 'OK',
-        })
-      })
+      createRecommendation(collectionId, fragmentId, recommendation).then(
+        () => {
+          showModal({
+            onCancel: () => {
+              getCollections()
+              getFragments()
+              hideModal()
+            },
+            title: 'Decision submitted',
+            cancelText: 'OK',
+          })
+        },
+      )
     },
   }),
 )(DecisionForm)
 
 // #region styled-components
-const defaultText = css`
-  color: ${th('colorText')};
-  font-family: ${th('fontReading')};
-  font-size: ${th('fontSizeBaseSmall')};
-`
-
 const IconButton = styled.div`
   align-self: flex-end;
   cursor: pointer;
@@ -185,17 +166,4 @@ const BoldSubtitle = Subtitle.extend`
   font-weight: bold;
   margin-left: 5px;
 `
-
-const CustomRadioGroup = styled.div`
-  div {
-    flex-direction: row;
-    justify-content: ${({ justify }) => justify || 'space-between'};
-    label {
-      span:last-child {
-        font-style: normal;
-        ${defaultText};
-      }
-    }
-  }
-`
 // #endregion
diff --git a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js
index 1b06f395f9e2780f5ede69aa8684bc32b81a6732..fb7452baf0ba87d8e6d3edb530511df05a9a4bfa 100644
--- a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js
+++ b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js
@@ -9,11 +9,7 @@ import { getFormValues, reset as resetForm } from 'redux-form'
 
 import { FormItems } from '../UIComponents'
 import { StepOne, StepTwo, utils } from './'
-import {
-  selectError,
-  selectFetching,
-  createRecommendation,
-} from '../../redux/recommendations'
+import { createRecommendation } from '../../redux/recommendations'
 
 const RecommendWizard = ({
   step,
@@ -22,8 +18,6 @@ const RecommendWizard = ({
   prevStep,
   closeModal,
   submitForm,
-  isFetching,
-  recommendationError,
   ...rest
 }) => (
   <FormItems.RootContainer>
@@ -39,13 +33,7 @@ const RecommendWizard = ({
       />
     )}
     {step === 1 && (
-      <StepTwo
-        decision={decision}
-        goBack={prevStep}
-        isFetching={isFetching}
-        onSubmit={submitForm}
-        recommendationError={recommendationError}
-      />
+      <StepTwo decision={decision} goBack={prevStep} onSubmit={submitForm} />
     )}
   </FormItems.RootContainer>
 )
@@ -53,13 +41,12 @@ const RecommendWizard = ({
 export default compose(
   connect(
     state => ({
-      isFetching: selectFetching(state),
-      recommendationError: selectError(state),
       decision: get(getFormValues('recommendation')(state), 'decision'),
     }),
     {
       resetForm,
       createRecommendation,
+      getFragments: actions.getFragments,
       getCollections: actions.getCollections,
     },
   ),
@@ -77,16 +64,21 @@ export default compose(
       resetForm,
       fragmentId,
       collectionId,
+      getFragments,
       getCollections,
       createRecommendation,
     }) => values => {
       const recommendation = utils.parseRecommendationValues(values)
       createRecommendation(collectionId, fragmentId, recommendation).then(r => {
         resetForm('recommendation')
-        getCollections()
         showModal({
           title: 'Recommendation sent',
           cancelText: 'OK',
+          onCancel: () => {
+            getCollections()
+            getFragments()
+            hideModal()
+          },
         })
       })
     },
diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepOne.js b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js
index fc20adb61b276af9f16aabb22e81fec792d2f026..6185a09a1677c060032875ce80ebf83c4e1139be 100644
--- a/packages/components-faraday/src/components/MakeRecommendation/StepOne.js
+++ b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js
@@ -5,7 +5,7 @@ import { RadioGroup, ValidatedField, Button } from '@pubsweet/ui'
 import { utils } from './'
 import { FormItems } from '../UIComponents'
 
-const { RootContainer, Row, RowItem, Title } = FormItems
+const { Row, Title, RowItem, RootContainer, CustomRadioGroup } = FormItems
 
 const StepOne = ({ hideModal, disabled, onSubmit }) => (
   <RootContainer>
@@ -14,11 +14,16 @@ const StepOne = ({ hideModal, disabled, onSubmit }) => (
       <RowItem>
         <ValidatedField
           component={input => (
-            <RadioGroup
-              name="decision"
-              options={utils.recommendationOptions}
-              {...input}
-            />
+            <CustomRadioGroup
+              className="custom-radio-group"
+              justify="space-between"
+            >
+              <RadioGroup
+                name="decision"
+                options={utils.recommendationOptions}
+                {...input}
+              />
+            </CustomRadioGroup>
           )}
           name="decision"
         />
diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js b/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js
index 2e554d6ccb2d8f863416d3711ebeb469c14a0109..7081d58f04ce2e3aa7ecc3ebce86a92aa99ed396 100644
--- a/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js
+++ b/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js
@@ -23,9 +23,10 @@ const {
   Label,
   Title,
   RowItem,
-  Textarea,
+  TextAreaField,
   RootContainer,
   FormContainer,
+  CustomRadioGroup,
 } = FormItems
 
 const Form = RootContainer.withComponent(FormContainer)
@@ -51,19 +52,13 @@ const StepTwo = ({
         <Row>
           <RowItem vertical>
             <Label>Message for Editor in Chief (optional)</Label>
-            <ValidatedField
-              component={input => <Textarea {...input} height={70} />}
-              name="message.eic"
-            />
+            <ValidatedField component={TextAreaField} name="message.eic" />
           </RowItem>
         </Row>
         <Row>
           <RowItem vertical>
             <Label>Message for Author (optional)</Label>
-            <ValidatedField
-              component={input => <Textarea {...input} height={70} />}
-              name="message.author"
-            />
+            <ValidatedField component={TextAreaField} name="message.author" />
           </RowItem>
         </Row>
         {recommendationError && (
@@ -76,35 +71,37 @@ const StepTwo = ({
       </Fragment>
     ) : (
       <Fragment>
-        <CustomRow>
+        <Row>
           <RowItem vertical>
             <Label>REVISION TYPE</Label>
             <ValidatedField
               component={input => (
-                <RadioGroup
-                  name="revision.revision-type"
-                  {...input}
-                  options={utils.revisionOptions}
-                />
+                <CustomRadioGroup justify="flex-start">
+                  <RadioGroup
+                    name="revision.revision-type"
+                    {...input}
+                    options={utils.revisionOptions}
+                  />
+                </CustomRadioGroup>
               )}
               name="revision.revisionType"
               validate={[required]}
             />
           </RowItem>
-        </CustomRow>
-        <CustomRow>
+        </Row>
+        <Row>
           <RowItem vertical>
             <Label>
               REASON & DETAILS
               <SubLabel>Required</SubLabel>
             </Label>
             <ValidatedField
-              component={input => <Textarea {...input} />}
+              component={TextAreaField}
               name="revision.reason"
               validate={[required]}
             />
           </RowItem>
-        </CustomRow>
+        </Row>
         {!hasNote ? (
           <Row>
             <RowItem>
@@ -114,7 +111,7 @@ const StepTwo = ({
           </Row>
         ) : (
           <Fragment>
-            <CustomRow withMargin>
+            <Row noMargin>
               <RowItem flex={2}>
                 <Label>
                   INTERNAL NOTE
@@ -127,15 +124,15 @@ const StepTwo = ({
                 </IconButton>
                 <TextButton>Remove</TextButton>
               </CustomRowItem>
-            </CustomRow>
-            <CustomRow>
+            </Row>
+            <Row noMargin>
               <RowItem>
                 <ValidatedField
-                  component={input => <Textarea {...input} height={70} />}
+                  component={TextAreaField}
                   name="revision.internal-note"
                 />
               </RowItem>
-            </CustomRow>
+            </Row>
           </Fragment>
         )}
       </Fragment>
@@ -210,9 +207,9 @@ const IconButton = styled.div`
 const CustomRowItem = RowItem.extend`
   align-items: center;
   justify-content: flex-end;
-`
 
-const CustomRow = Row.extend`
-  margin: ${({ withMargin }) => `${withMargin ? 6 : 0}px 0px`};
+  & > div {
+    justify-content: flex-end;
+  }
 `
 // #endregion
diff --git a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js
index bee6e98b5d26856b85a9192f7b27375419abc06e..b6087f87534c4a756da957e77144c91b7d026286 100644
--- a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js
+++ b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js
@@ -1,5 +1,6 @@
 import React, { Fragment } from 'react'
 import { connect } from 'react-redux'
+import { actions } from 'pubsweet-client'
 import styled, { css } from 'styled-components'
 import { Icon, Button, th, Spinner } from '@pubsweet/ui'
 import { compose, withHandlers, lifecycle } from 'recompose'
@@ -31,15 +32,20 @@ const InviteReviewersModal = compose(
       fetchingInvite: selectFetchingInvite(state),
       fetchingReviewers: selectFetchingReviewers(state),
     }),
-    { getCollectionReviewers },
+    { getCollectionReviewers, getCollections: actions.getCollections },
   ),
   withHandlers({
     getReviewers: ({
+      versionId,
       collectionId,
       setReviewers,
       getCollectionReviewers,
     }) => () => {
-      getCollectionReviewers(collectionId)
+      getCollectionReviewers(collectionId, versionId)
+    },
+    closeModal: ({ getCollections, hideModal }) => () => {
+      getCollections()
+      hideModal()
     },
   }),
   lifecycle({
@@ -50,10 +56,10 @@ const InviteReviewersModal = compose(
   }),
 )(
   ({
-    hideModal,
     onConfirm,
     showModal,
     versionId,
+    closeModal,
     collectionId,
     getReviewers,
     reviewerError,
@@ -63,7 +69,7 @@ const InviteReviewersModal = compose(
     invitations = [],
   }) => (
     <Root>
-      <CloseIcon data-test="icon-modal-hide" onClick={hideModal}>
+      <CloseIcon data-test="icon-modal-hide" onClick={closeModal}>
         <Icon primary>x</Icon>
       </CloseIcon>
 
@@ -76,6 +82,7 @@ const InviteReviewersModal = compose(
         isFetching={fetchingInvite}
         reviewerError={reviewerError}
         reviewers={reviewers}
+        versionId={versionId}
       />
 
       <Row>
@@ -193,6 +200,7 @@ const AssignButton = styled(Button)`
   background-color: ${th('colorPrimary')};
   color: ${th('colorTextReverse')};
   height: calc(${th('subGridUnit')} * 5);
+  padding: 0;
   text-align: center;
 `
 // #endregion
diff --git a/packages/components-faraday/src/components/Reviewers/ReviewerForm.js b/packages/components-faraday/src/components/Reviewers/ReviewerForm.js
index 465ed9dcb2ae9e3c37e138ca3f8e3fee5aefc84a..0c24f66472c8abf9fb6e049472776bb3966699e9 100644
--- a/packages/components-faraday/src/components/Reviewers/ReviewerForm.js
+++ b/packages/components-faraday/src/components/Reviewers/ReviewerForm.js
@@ -60,7 +60,7 @@ export default compose(
     onSubmit: (
       values,
       dispatch,
-      { inviteReviewer, collectionId, getReviewers, reset },
+      { inviteReviewer, collectionId, versionId, getReviewers, reset },
     ) => {
       const reviewerData = pick(values, [
         'email',
@@ -68,7 +68,7 @@ export default compose(
         'firstName',
         'affiliation',
       ])
-      inviteReviewer(reviewerData, collectionId).then(() => {
+      inviteReviewer(reviewerData, collectionId, versionId).then(() => {
         reset()
         getReviewers()
       })
@@ -96,6 +96,7 @@ export default compose(
 const FormButton = styled(Button)`
   height: calc(${th('subGridUnit')} * 5);
   margin: ${th('subGridUnit')};
+  padding: 0;
 `
 
 const Err = styled.span`
diff --git a/packages/components-faraday/src/components/Reviewers/ReviewerList.js b/packages/components-faraday/src/components/Reviewers/ReviewerList.js
index 5acbf18c452cd2c26371dec8d12fc88f566f7df0..4290a947cf9b6a4146a811262976a03f732f9ade 100644
--- a/packages/components-faraday/src/components/Reviewers/ReviewerList.js
+++ b/packages/components-faraday/src/components/Reviewers/ReviewerList.js
@@ -93,6 +93,7 @@ export default compose(
   withHandlers({
     showConfirmResend: ({
       showModal,
+      versionId,
       collectionId,
       inviteReviewer,
       goBackToReviewers,
@@ -104,6 +105,7 @@ export default compose(
           inviteReviewer(
             pick(reviewer, ['email', 'firstName', 'lastName', 'affiliation']),
             collectionId,
+            versionId,
           ).then(goBackToReviewers, goBackToReviewers)
         },
         onCancel: goBackToReviewers,
@@ -112,6 +114,7 @@ export default compose(
     showConfirmRevoke: ({
       showModal,
       hideModal,
+      versionId,
       collectionId,
       revokeReviewer,
       goBackToReviewers,
@@ -120,7 +123,7 @@ export default compose(
         title: 'Unassign Reviewer',
         confirmText: 'Unassign',
         onConfirm: () => {
-          revokeReviewer(invitationId, collectionId).then(
+          revokeReviewer(invitationId, collectionId, versionId).then(
             goBackToReviewers,
             goBackToReviewers,
           )
diff --git a/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js b/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js
index 3b69d0fb2a5563e425abeaf9cdd985fc910d6105..e64aa55891df9a45d62c42f54c9a37a87c11c126 100644
--- a/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js
+++ b/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js
@@ -48,18 +48,20 @@ const TR = ({
         )}
       </td>
       <DateParser timestamp={r.invitedOn}>
-        {timestamp => <td>{timestamp}</td>}
+        {timestamp => <td width="150">{timestamp}</td>}
       </DateParser>
-      <td>
+      <td width="200">
         <StatusText>
-          {`${r.status === 'accepted' ? 'Agreed: ' : `${r.status}: `}`}
+          {`${r.status === 'accepted' ? 'Agreed' : r.status}`}
         </StatusText>
-        <DateParser timestamp={r.respondedOn}>
-          {timestamp => <DateText>{timestamp}</DateText>}
-        </DateParser>
+        {r.respondedOn && (
+          <DateParser timestamp={r.respondedOn}>
+            {timestamp => <DateText>{`: ${timestamp}`}</DateText>}
+          </DateParser>
+        )}
       </td>
       <DateParser timestamp={submittedOn}>
-        {timestamp => <td>{timestamp}</td>}
+        {timestamp => <td width="150">{timestamp}</td>}
       </DateParser>
       <td width={100}>
         {r.status === 'pending' && (
@@ -81,31 +83,29 @@ const ReviewersDetailsList = ({
 }) =>
   reviewers.length > 0 ? (
     <Root>
-      <ScrollContainer>
-        <Table>
-          <thead>
-            <tr>
-              <td>Full Name</td>
-              <td>Invited On</td>
-              <td>Responded On</td>
-              <td>Submitted On</td>
-              <td />
-            </tr>
-          </thead>
-          <tbody>
-            {reviewers.map((r, index) => (
-              <TR
-                index={index}
-                key={r.email}
-                renderAcceptedLabel={renderAcceptedLabel}
-                reviewer={r}
-                showConfirmResend={showConfirmResend}
-                showConfirmRevoke={showConfirmRevoke}
-              />
-            ))}
-          </tbody>
-        </Table>
-      </ScrollContainer>
+      <Table>
+        <thead>
+          <tr>
+            <td>Full Name</td>
+            <td width="150">Invited On</td>
+            <td width="200">Responded On</td>
+            <td width="150">Submitted On</td>
+            <td width="100" />
+          </tr>
+        </thead>
+        <tbody>
+          {reviewers.map((r, index) => (
+            <TR
+              index={index}
+              key={`${`${r.email} ${index}`}`}
+              renderAcceptedLabel={renderAcceptedLabel}
+              reviewer={r}
+              showConfirmResend={showConfirmResend}
+              showConfirmRevoke={showConfirmRevoke}
+            />
+          ))}
+        </tbody>
+      </Table>
     </Root>
   ) : (
     <div> No reviewers details </div>
@@ -203,11 +203,6 @@ const StatusText = ReviewerEmail.extend`
 
 const DateText = ReviewerEmail.extend``
 
-const ScrollContainer = styled.div`
-  align-self: stretch;
-  flex: 1;
-  overflow: auto;
-`
 const Root = styled.div`
   align-items: stretch;
   align-self: stretch;
@@ -216,13 +211,28 @@ const Root = styled.div`
   flex-direction: column;
   justify-content: flex-start;
   height: 25vh;
+  display: table;
 `
 
 const Table = styled.table`
   border-spacing: 0;
   border-collapse: collapse;
   width: 100%;
-
+  thead {
+    display: table;
+    width: calc(100% - 1em);
+  }
+  tbody {
+    overflow: auto;
+    max-height: 180px;
+    margin-bottom: ${th('gridUnit')};
+    display: block;
+    tr {
+      display: table;
+      width: 100%;
+      table-layout: fixed;
+    }
+  }
   & thead tr {
     ${defaultText};
     border-bottom: ${th('borderDefault')};
diff --git a/packages/components-faraday/src/components/SignUp/AuthorSignup.js b/packages/components-faraday/src/components/SignUp/AuthorSignup.js
new file mode 100644
index 0000000000000000000000000000000000000000..40f08b2fa2a95ca0c300d26a7ede8934f607f710
--- /dev/null
+++ b/packages/components-faraday/src/components/SignUp/AuthorSignup.js
@@ -0,0 +1,197 @@
+import React, { Fragment } from 'react'
+import { reduxForm } from 'redux-form'
+import { th } from '@pubsweet/ui-toolkit'
+import { required } from 'xpub-validators'
+import { compose, withState } from 'recompose'
+import styled, { css } from 'styled-components'
+import { Icon, Button, TextField, ValidatedField } from '@pubsweet/ui'
+
+import { FormItems } from '../UIComponents'
+
+const { Row, RowItem, Label, RootContainer, FormContainer } = FormItems
+
+const Step1 = ({ handleSubmit }) => (
+  <CustomFormContainer onSubmit={handleSubmit}>
+    <Fragment>
+      <CustomRow noMargin>
+        <CustomRowItem vertical>
+          <Label>Email</Label>
+          <ValidatedField
+            component={TextField}
+            name="email"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+      <CustomRow>
+        <CustomRowItem vertical>
+          <Label>Password</Label>
+          <ValidatedField
+            component={TextField}
+            name="password"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+      <CustomRow>
+        <CustomRowItem vertical>
+          <Label>Confirm password</Label>
+          <ValidatedField
+            component={TextField}
+            name="confirmPassword"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+    </Fragment>
+    <Button primary type="submit">
+      Next
+    </Button>
+  </CustomFormContainer>
+)
+
+const AuthorSignupStep1 = reduxForm({
+  form: 'authorSignup',
+  destroyOnUnmount: false,
+  enableReinitialize: true,
+  forceUnregisterOnUnmount: true,
+})(Step1)
+
+const Step2 = ({ handleSubmit }) => (
+  <CustomFormContainer onSubmit={handleSubmit}>
+    <Fragment>
+      <CustomRow noMargin>
+        <CustomRowItem vertical>
+          <Label>First name</Label>
+          <ValidatedField
+            component={TextField}
+            name="firstName"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+      <CustomRow noMargin>
+        <CustomRowItem vertical>
+          <Label>Last name</Label>
+          <ValidatedField
+            component={TextField}
+            name="lastName"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+      <CustomRow noMargin>
+        <CustomRowItem vertical>
+          <Label>Affiliation</Label>
+          <ValidatedField
+            component={TextField}
+            name="affiliation"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+      <CustomRow noMargin>
+        <CustomRowItem vertical>
+          <Label>Title</Label>
+          <ValidatedField
+            component={TextField}
+            name="title"
+            validate={[required]}
+          />
+        </CustomRowItem>
+      </CustomRow>
+    </Fragment>
+    <Button primary type="submit">
+      Submit
+    </Button>
+  </CustomFormContainer>
+)
+
+const AuthorSignupStep2 = reduxForm({
+  form: 'authorSignup',
+  destroyOnUnmount: false,
+  forceUnregisterOnUnmount: true,
+  onSubmit: null,
+})(Step2)
+
+const AuthorWizard = ({ step, changeStep, history }) => (
+  <CustomRootContainer>
+    <IconButton onClick={history.goBack}>
+      <Icon primary size={3}>
+        x
+      </Icon>
+    </IconButton>
+    <Title>Author Signup</Title>
+    {step === 0 && <AuthorSignupStep1 onSubmit={() => changeStep(1)} />}
+    {step === 1 && <AuthorSignupStep2 />}
+  </CustomRootContainer>
+)
+
+export default compose(withState('step', 'changeStep', 0))(AuthorWizard)
+
+// #region styled-components
+const verticalPadding = css`
+  padding: ${th('subGridUnit')} 0;
+`
+
+const CustomRow = Row.extend`
+  div[role='alert'] {
+    margin-top: 0;
+  }
+`
+
+const CustomRowItem = RowItem.extend`
+  & > div {
+    flex: 1;
+
+    & > div {
+      max-width: 400px;
+      width: 400px;
+    }
+  }
+`
+
+const CustomRootContainer = RootContainer.extend`
+  align-items: center;
+  border: ${th('borderDefault')};
+  position: relative;
+`
+
+const CustomFormContainer = FormContainer.extend`
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+`
+
+const Title = styled.span`
+  font-family: ${th('fontHeading')};
+  font-size: ${th('fontSizeHeading5')};
+  ${verticalPadding};
+`
+
+const IconButton = styled.button`
+  align-items: center;
+  background-color: ${th('backgroundColorReverse')};
+  border: none;
+  color: ${th('colorPrimary')};
+  cursor: ${({ hide }) => (hide ? 'auto' : 'pointer')};
+  display: flex;
+  font-family: ${th('fontInterface')};
+  font-size: ${th('fontSizeBaseSmall')};
+  opacity: ${({ hide }) => (hide ? 0 : 1)};
+  text-align: left;
+
+  position: absolute;
+  top: ${th('subGridUnit')};
+  right: ${th('subGridUnit')};
+
+  &:active,
+  &:focus {
+    outline: none;
+  }
+  &:hover {
+    opacity: 0.7;
+  }
+`
+// #endregion
diff --git a/packages/components-faraday/src/components/SignUp/ConfirmAccount.js b/packages/components-faraday/src/components/SignUp/ConfirmAccount.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a685b927f621271162a134a24794a56b8fcfc54
--- /dev/null
+++ b/packages/components-faraday/src/components/SignUp/ConfirmAccount.js
@@ -0,0 +1,62 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { Button } from '@pubsweet/ui'
+import styled from 'styled-components'
+import { th } from '@pubsweet/ui-toolkit'
+import { compose, lifecycle, withState } from 'recompose'
+
+import { parseSearchParams } from '../utils'
+import { confirmUser } from '../../redux/users'
+
+const ConfirmAccount = ({ message, history }) => (
+  <Root>
+    <Title>{message}</Title>
+    <Button onClick={() => history.replace('/')} primary>
+      Go to Dashboard
+    </Button>
+  </Root>
+)
+
+const confirmMessage = `Your account has been successfully confirmed. Welcome to Hindawi!`
+const errorMessage = `Something went wrong with your account confirmation. Please try again.`
+
+export default compose(
+  connect(null, { confirmUser }),
+  withState('message', 'setConfirmMessage', 'Loading...'),
+  lifecycle({
+    componentDidMount() {
+      const { location, confirmUser, setConfirmMessage } = this.props
+      const { confirmationToken, userId } = parseSearchParams(location.search)
+      if (userId) {
+        confirmUser(userId, confirmationToken)
+          .then(() => {
+            setConfirmMessage(confirmMessage)
+          })
+          .catch(() => {
+            // errors are still gobbled up by pubsweet
+            setConfirmMessage(errorMessage)
+          })
+      }
+    },
+  }),
+)(ConfirmAccount)
+
+// #region styled components
+const Root = styled.div`
+  color: ${th('colorText')};
+  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;
+`
+// #endregion
diff --git a/packages/components-faraday/src/components/SignUp/ReviewerDecline.js b/packages/components-faraday/src/components/SignUp/ReviewerDecline.js
index 74f726dd83b84c55f25a7ff531081c3590aa1ca5..bb023319c639fe92ec3f17f2fc48ecc95269ad3f 100644
--- a/packages/components-faraday/src/components/SignUp/ReviewerDecline.js
+++ b/packages/components-faraday/src/components/SignUp/ReviewerDecline.js
@@ -39,10 +39,14 @@ export default compose(
         invitationToken,
         reviewerDecline,
         replace,
+        fragmentId,
       } = this.props
-      reviewerDecline(invitationId, collectionId, invitationToken).catch(
-        redirectToError(replace),
-      )
+      reviewerDecline(
+        invitationId,
+        collectionId,
+        fragmentId,
+        invitationToken,
+      ).catch(redirectToError(replace))
     },
   }),
 )(ReviewerDecline)
diff --git a/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js b/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js
index b613bc9692415a3ecf1d8ce297c4228ec0a4abfe..f55c227cc963ce9101dde08b522e239cd43c15ee 100644
--- a/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js
+++ b/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js
@@ -27,6 +27,7 @@ const {
 const agreeText = `You have been invited to review a manuscript on the Hindawi platform. Please set a password and proceed to the manuscript.`
 const declineText = `You have decline to work on a manuscript.`
 
+const PasswordField = input => <TextField {...input} type="password" />
 const min8Chars = minChars(8)
 const ReviewerInviteDecision = ({
   agree,
@@ -42,10 +43,10 @@ const ReviewerInviteDecision = ({
     {agree === 'true' && (
       <FormContainer onSubmit={handleSubmit}>
         <Row>
-          <RowItem>
+          <RowItem vertical>
             <Label> Password </Label>
             <ValidatedField
-              component={input => <TextField {...input} type="password" />}
+              component={PasswordField}
               name="password"
               validate={[required, min8Chars]}
             />
diff --git a/packages/components-faraday/src/components/SignUp/SignUpInvitationForm.js b/packages/components-faraday/src/components/SignUp/SignUpInvitationForm.js
index f7b21c17851e6a0d6dcffe32d66c3fdd11bf77c7..59644b62ba40509ce17cbac33f16fe97581ca97c 100644
--- a/packages/components-faraday/src/components/SignUp/SignUpInvitationForm.js
+++ b/packages/components-faraday/src/components/SignUp/SignUpInvitationForm.js
@@ -6,22 +6,24 @@ import { FormItems } from '../UIComponents'
 
 const { RootContainer, Title, Subtitle, Email, Err } = FormItems
 
+const defaultSubtitle = `Your details have been pre-filled, please review and confirm before set
+your password.`
+
 const SignUpInvitation = ({
   step,
-  email,
-  token,
   error,
   journal,
+  onSubmit,
   nextStep,
+  prevStep,
   initialValues,
-  submitConfirmation,
+  type,
+  subtitle = defaultSubtitle,
+  title = 'Add New Account Details',
 }) => (
   <RootContainer bordered>
-    <Title>Add New Account Details</Title>
-    <Subtitle>
-      Your details have been pre-filled, please review and confirm before set
-      your password.
-    </Subtitle>
+    <Title>{title}</Title>
+    <Subtitle>{subtitle}</Subtitle>
     <Email>{initialValues.email}</Email>
     {error && <Err>Token expired or Something went wrong.</Err>}
     {step === 0 && (
@@ -37,7 +39,9 @@ const SignUpInvitation = ({
         error={error}
         initialValues={initialValues}
         journal={journal}
-        onSubmit={submitConfirmation}
+        onSubmit={onSubmit}
+        prevStep={prevStep}
+        type={type}
       />
     )}
   </RootContainer>
diff --git a/packages/components-faraday/src/components/SignUp/SignUpInvitationPage.js b/packages/components-faraday/src/components/SignUp/SignUpInvitationPage.js
index 0ad1a8f2a6d40d1bf780c290160f0cba6fb714e9..12462b277554942af36262523997b6e070ab51b6 100644
--- a/packages/components-faraday/src/components/SignUp/SignUpInvitationPage.js
+++ b/packages/components-faraday/src/components/SignUp/SignUpInvitationPage.js
@@ -1,59 +1,29 @@
-import { get } from 'lodash'
 import { withJournal } from 'xpub-journal'
-import { SubmissionError } from 'redux-form'
-import { create } from 'pubsweet-client/src/helpers/api'
-import { loginUser } from 'pubsweet-component-login/actions'
 import { compose, withState, withProps, withHandlers } from 'recompose'
 
 import SignUpInvitation from './SignUpInvitationForm'
-
-const login = (dispatch, values, history) =>
-  dispatch(loginUser(values))
-    .then(() => {
-      history.push('/')
-    })
-    .catch(error => {
-      const err = get(error, 'response')
-      if (err) {
-        const errorMessage = get(JSON.parse(err), 'error')
-        throw new SubmissionError({
-          password: errorMessage || 'Something went wrong',
-        })
-      }
-    })
-
-const confirmUser = (email, token, history) => (values, dispatch) => {
-  const request = { ...values, email, token }
-  if (values) {
-    return create('/users/reset-password', request)
-      .then(r => {
-        const { username } = r
-        const { password } = values
-        login(dispatch, { username, password }, history)
-      })
-      .catch(error => {
-        const err = get(error, 'response')
-        if (err) {
-          const errorMessage = get(JSON.parse(err), 'error')
-          throw new SubmissionError({
-            _error: errorMessage || 'Something went wrong',
-          })
-        }
-      })
-  }
-}
+import {
+  confirmUser,
+  signUpUser,
+  resetUserPassword,
+  setNewPassword,
+} from './utils'
 
 export default compose(
   withJournal,
-  withState('step', 'changeStep', 0),
+  withState(
+    'step',
+    'changeStep',
+    ({ type }) => (type === 'forgotPassword' || type === 'setPassword' ? 1 : 0),
+  ),
   withProps(({ location }) => {
     const params = new URLSearchParams(location.search)
-    const email = params.get('email')
-    const token = params.get('token')
-    const title = params.get('title')
-    const lastName = params.get('lastName')
-    const firstName = params.get('firstName')
-    const affiliation = params.get('affiliation')
+    const email = params.get('email') || ''
+    const token = params.get('token') || ''
+    const title = params.get('title') || ''
+    const lastName = params.get('lastName') || ''
+    const firstName = params.get('firstName') || ''
+    const affiliation = params.get('affiliation') || ''
 
     return {
       initialValues: {
@@ -69,10 +39,26 @@ export default compose(
   withHandlers({
     nextStep: ({ changeStep }) => () => changeStep(step => step + 1),
     prevStep: ({ changeStep }) => () => changeStep(step => step - 1),
-    submitConfirmation: ({
-      initialValues: { email, token },
+    confirmInvitation: ({
+      initialValues: { email = '', token = '' },
       history,
-      ...rest
     }) => confirmUser(email, token, history),
+    signUp: ({ history }) => signUpUser(history),
+    forgotPassword: ({ history }) => resetUserPassword(history),
+    setNewPassword: ({ history }) => setNewPassword(history),
   }),
+  withProps(
+    ({ type, signUp, confirmInvitation, forgotPassword, setNewPassword }) => {
+      switch (type) {
+        case 'forgotPassword':
+          return { onSubmit: forgotPassword }
+        case 'signup':
+          return { onSubmit: signUp }
+        case 'setPassword':
+          return { onSubmit: setNewPassword }
+        default:
+          return { onSubmit: confirmInvitation }
+      }
+    },
+  ),
 )(SignUpInvitation)
diff --git a/packages/components-faraday/src/components/SignUp/SignUpStep0.js b/packages/components-faraday/src/components/SignUp/SignUpStep0.js
index fb3e7eb4eaf716546b66dc00647ff44148a4f6e6..f9249333ee6cd689a869366bcaf43ae1644d3602 100644
--- a/packages/components-faraday/src/components/SignUp/SignUpStep0.js
+++ b/packages/components-faraday/src/components/SignUp/SignUpStep0.js
@@ -2,26 +2,33 @@ import React from 'react'
 import { isUndefined } from 'lodash'
 import { reduxForm } from 'redux-form'
 import { required } from 'xpub-validators'
-import { Button, ValidatedField, TextField, Menu } from '@pubsweet/ui'
+import { Button, ValidatedField, TextField, Menu, Checkbox } from '@pubsweet/ui'
 
 import { FormItems } from '../UIComponents'
 
-const { FormContainer, Row, RowItem, Label } = FormItems
+const {
+  FormContainer,
+  Row,
+  RowItem,
+  Label,
+  PrivatePolicy,
+  DefaultText,
+} = FormItems
 
 const Step0 = ({ journal, handleSubmit, initialValues, error }) =>
   !isUndefined(initialValues) ? (
     <FormContainer onSubmit={handleSubmit}>
       <Row>
-        <RowItem vertical>
-          <Label> First name* </Label>
+        <RowItem vertical withRightMargin>
+          <Label>First name*</Label>
           <ValidatedField
             component={TextField}
             name="firstName"
             validate={[required]}
           />
         </RowItem>
-        <RowItem vertical>
-          <Label> Last name* </Label>
+        <RowItem vertical withRightMargin>
+          <Label>Last name*</Label>
           <ValidatedField
             component={TextField}
             name="lastName"
@@ -30,8 +37,8 @@ const Step0 = ({ journal, handleSubmit, initialValues, error }) =>
         </RowItem>
       </Row>
       <Row>
-        <RowItem vertical>
-          <Label> Affiliation* </Label>
+        <RowItem vertical withRightMargin>
+          <Label>Affiliation*</Label>
           <ValidatedField
             component={TextField}
             name="affiliation"
@@ -39,8 +46,8 @@ const Step0 = ({ journal, handleSubmit, initialValues, error }) =>
           />
         </RowItem>
 
-        <RowItem vertical>
-          <Label> Title* </Label>
+        <RowItem vertical withRightMargin>
+          <Label>Title*</Label>
           <ValidatedField
             component={input => <Menu {...input} options={journal.title} />}
             name="title"
@@ -48,10 +55,48 @@ const Step0 = ({ journal, handleSubmit, initialValues, error }) =>
           />
         </RowItem>
       </Row>
+      <Row justify="left">
+        <ValidatedField
+          component={input => (
+            <Checkbox checked={input.value} type="checkbox" {...input} />
+          )}
+          name="agreeTC"
+          validate={[required]}
+        />
+        <DefaultText>
+          By creating this account, you agree to the{' '}
+          <a
+            href="https://www.hindawi.com/terms/"
+            rel="noopener noreferrer"
+            target="_blank"
+          >
+            Terms of Service
+          </a>.
+        </DefaultText>
+      </Row>
+      <Row>
+        <PrivatePolicy>
+          This account information will be processed by us in accordance with
+          our Privacy Policy for the purpose of registering your Faraday account
+          and allowing you to use the services available via the Faraday
+          platform. Please read our{' '}
+          <a
+            href="https://www.hindawi.com/privacy/"
+            rel="noopener noreferrer"
+            target="_blank"
+          >
+            Privacy Policy
+          </a>{' '}
+          for further information.
+        </PrivatePolicy>
+      </Row>
+      <Row />
       <Row>
-        <Button primary type="submit">
-          CONFIRM & PROCEED TO SET PASSWORD
-        </Button>
+        <RowItem centered>
+          <Button primary type="submit">
+            CONFIRM & PROCEED TO SET PASSWORD
+          </Button>
+        </RowItem>
       </Row>
     </FormContainer>
   ) : (
diff --git a/packages/components-faraday/src/components/SignUp/SignUpStep1.js b/packages/components-faraday/src/components/SignUp/SignUpStep1.js
index f5469320fbd6d05098fbdd9c4f78da2005e6a39f..f221cccae62f26a8f7bbc1e0d22d9437cfb14b45 100644
--- a/packages/components-faraday/src/components/SignUp/SignUpStep1.js
+++ b/packages/components-faraday/src/components/SignUp/SignUpStep1.js
@@ -1,24 +1,104 @@
-import React from 'react'
+import React, { Fragment } from 'react'
 import { reduxForm } from 'redux-form'
 import { required } from 'xpub-validators'
 import { Button, ValidatedField, TextField } from '@pubsweet/ui'
 
 import { FormItems } from '../UIComponents'
+import { passwordValidator, emailValidator } from '../utils'
 
-const { FormContainer, Row, RowItem, Label, Err } = FormItems
+const { Row, Err, Label, RowItem, FormContainer } = FormItems
 
-const Step1 = ({ journal, handleSubmit, error }) => (
-  <FormContainer onSubmit={handleSubmit}>
+const PasswordField = input => <TextField {...input} type="password" />
+const EmailField = input => <TextField {...input} type="email" />
+
+const SignUpForm = () => (
+  <Fragment>
+    <Row>
+      <RowItem vertical>
+        <Label>Email</Label>
+        <ValidatedField
+          component={EmailField}
+          name="email"
+          validate={[required, emailValidator]}
+        />
+      </RowItem>
+    </Row>
     <Row>
-      <RowItem>
-        <Label> Password </Label>
+      <RowItem vertical>
+        <Label>Password</Label>
         <ValidatedField
-          component={input => <TextField {...input} type="password" />}
+          component={PasswordField}
           name="password"
           validate={[required]}
         />
       </RowItem>
     </Row>
+    <Row>
+      <RowItem vertical>
+        <Label>Confirm password</Label>
+        <ValidatedField
+          component={PasswordField}
+          name="confirmPassword"
+          validate={[required]}
+        />
+      </RowItem>
+    </Row>
+  </Fragment>
+)
+
+const InviteForm = () => (
+  <Fragment>
+    <Row>
+      <RowItem vertical>
+        <Label>Password</Label>
+        <ValidatedField
+          component={PasswordField}
+          name="password"
+          validate={[required]}
+        />
+      </RowItem>
+    </Row>
+    <Row>
+      <RowItem vertical>
+        <Label>Confirm password</Label>
+        <ValidatedField
+          component={PasswordField}
+          name="confirmPassword"
+          validate={[required]}
+        />
+      </RowItem>
+    </Row>
+  </Fragment>
+)
+
+const ForgotEmailForm = () => (
+  <Fragment>
+    <Row>
+      <RowItem vertical>
+        <Label>Email</Label>
+        <ValidatedField
+          component={EmailField}
+          name="email"
+          validate={[required, emailValidator]}
+        />
+      </RowItem>
+    </Row>
+  </Fragment>
+)
+
+const withoutBack = ['forgotPassword', 'setPassword']
+const Step1 = ({
+  error,
+  prevStep,
+  submitting,
+  handleSubmit,
+  type = 'invite',
+}) => (
+  <FormContainer onSubmit={handleSubmit}>
+    {type === 'signup' && <SignUpForm />}
+    {type === 'setPassword' && <InviteForm />}
+    {type === 'forgotPassword' && <ForgotEmailForm />}
+    {type === 'invite' && <InviteForm />}
     {error && (
       <Row>
         <RowItem>
@@ -26,8 +106,15 @@ const Step1 = ({ journal, handleSubmit, error }) => (
         </RowItem>
       </Row>
     )}
+    <Row />
+
     <Row>
-      <Button primary type="submit">
+      {!withoutBack.includes(type) && (
+        <Button onClick={prevStep} type="button">
+          BACK
+        </Button>
+      )}
+      <Button disabled={submitting} primary type="submit">
         CONFIRM
       </Button>
     </Row>
@@ -38,4 +125,5 @@ export default reduxForm({
   form: 'signUpInvitation',
   destroyOnUnmount: false,
   forceUnregisterOnUnmount: true,
+  validate: passwordValidator,
 })(Step1)
diff --git a/packages/components-faraday/src/components/SignUp/index.js b/packages/components-faraday/src/components/SignUp/index.js
index 8fc6a0dd3bbf70231a30792e4d78aa5594d848f4..d06b74c2b90e4e13790dc904c906288b50a4fe29 100644
--- a/packages/components-faraday/src/components/SignUp/index.js
+++ b/packages/components-faraday/src/components/SignUp/index.js
@@ -1,3 +1,5 @@
+export { default as AuthorSignup } from './AuthorSignup'
+export { default as ConfirmAccount } from './ConfirmAccount'
 export { default as ReviewerSignUp } from './ReviewerSignUp'
 export { default as ReviewerDecline } from './ReviewerDecline'
 export { default as SignUpInvitationPage } from './SignUpInvitationPage'
diff --git a/packages/components-faraday/src/components/SignUp/utils.js b/packages/components-faraday/src/components/SignUp/utils.js
index 4f264402c54df9f81ce41908b74ce063b72d3e73..a46f99096ef2210e92ffa5c292846780009635da 100644
--- a/packages/components-faraday/src/components/SignUp/utils.js
+++ b/packages/components-faraday/src/components/SignUp/utils.js
@@ -1,4 +1,26 @@
 /* eslint-disable */
+import { omit, get } from 'lodash'
+import { create } from 'pubsweet-client/src/helpers/api'
+import { loginUser } from 'pubsweet-component-login/actions'
+
+import { handleFormError } from '../utils'
+
+const generatePasswordHash = () =>
+  Array.from({ length: 4 }, () =>
+    Math.random()
+      .toString(36)
+      .slice(4),
+  ).join('')
+
+export const parseSignupAuthor = ({ token, confirmPassword, ...values }) => ({
+  ...values,
+  admin: false,
+  isConfirmed: false,
+  editorInChief: false,
+  handlingEditor: false,
+  username: values.email,
+  confirmationToken: generatePasswordHash(),
+})
 
 export const parseSearchParams = url => {
   const params = new URLSearchParams(url)
@@ -8,3 +30,60 @@ export const parseSearchParams = url => {
   }
   return parsedObject
 }
+
+export const login = (dispatch, values, history) =>
+  dispatch(loginUser(values))
+    .then(() => {
+      history.push('/')
+    })
+    .catch(handleFormError)
+
+export const confirmUser = (email, token, history) => (values, dispatch) => {
+  const request = { ...values, email, token }
+  if (values) {
+    return create('/users/reset-password', omit(request, ['confirmPassword']))
+      .then(r => {
+        const { username } = r
+        const { password } = values
+        login(dispatch, { username, password }, history)
+      })
+      .catch(handleFormError)
+  }
+}
+
+export const signUpUser = history => (values, dispatch) =>
+  create('/users', parseSignupAuthor(values))
+    .then(r => {
+      const { username } = r
+      const { password } = values
+      login(dispatch, { username, password }, history).then(() => {
+        create('/emails', {
+          email: values.email,
+          type: 'signup',
+        })
+      })
+    })
+    .catch(handleFormError)
+
+export const resetUserPassword = history => ({ email }, dispatch) =>
+  create(`/users/forgot-password`, { email })
+    .then(r => {
+      const message = get(r, 'message') || 'Password reset email has been sent.'
+      history.push('/info-page', {
+        title: 'Reset Password',
+        content: message,
+      })
+    })
+    .catch(handleFormError)
+
+export const setNewPassword = history => (
+  { email, token, password },
+  dispatch,
+) =>
+  create(`/users/reset-password`, { email, token, password })
+    .then(() => {
+      login(dispatch, { username: email, password }, history).then(() =>
+        history.push('/'),
+      )
+    })
+    .catch(handleFormError)
diff --git a/packages/components-faraday/src/components/UIComponents/ConfirmationPage.js b/packages/components-faraday/src/components/UIComponents/ConfirmationPage.js
index 3f974da8f2e7384d696cdde1db84f56aa6b6dcdc..3c893e825b13c2dfa75564ee9a3b5f67f9f5aa01 100644
--- a/packages/components-faraday/src/components/UIComponents/ConfirmationPage.js
+++ b/packages/components-faraday/src/components/UIComponents/ConfirmationPage.js
@@ -81,6 +81,6 @@ const Title = styled.div`
   color: ${th('colorPrimary')};
   font-size: ${th('fontSizeHeading5')};
   font-family: ${th('fontHeading')};
-  margin: 10px auto;
+  margin: ${th('gridUnit')} auto;
 `
 // #endregion
diff --git a/packages/components-faraday/src/components/UIComponents/FormItems.js b/packages/components-faraday/src/components/UIComponents/FormItems.js
index 844b841f2b52b3847677e0859bccd95a470f9594..8f6ebf89faf00b45808573e56a929b1635470ee5 100644
--- a/packages/components-faraday/src/components/UIComponents/FormItems.js
+++ b/packages/components-faraday/src/components/UIComponents/FormItems.js
@@ -1,5 +1,15 @@
+import React from 'react'
 import { th } from '@pubsweet/ui'
-import styled from 'styled-components'
+import styled, { css } from 'styled-components'
+
+const defaultText = css`
+  color: ${th('colorText')};
+  font-family: ${th('fontReading')};
+  font-size: ${th('fontSizeBaseSmall')};
+`
+export const DefaultText = styled.div`
+  ${defaultText};
+`
 
 export const RootContainer = styled.div`
   background-color: ${th('backgroundColorReverse')};
@@ -39,11 +49,16 @@ export const Email = styled.div`
 export const FormContainer = styled.form``
 
 export const Row = styled.div`
-  align-items: center;
+  align-items: flex-start;
   display: flex;
   flex-direction: row;
-  justify-content: space-evenly;
-  margin: calc(${th('subGridUnit')} * 2) 0;
+  justify-content: ${({ justify }) => justify || 'space-evenly'};
+  margin: ${({ noMargin }) =>
+    noMargin ? 0 : css`calc(${th('subGridUnit')} * 2) 0`};
+
+  label + div[role='alert'] {
+    margin-top: 0;
+  }
 `
 
 export const RowItem = styled.div`
@@ -51,7 +66,12 @@ export const RowItem = styled.div`
   flex: ${({ flex }) => flex || 1};
   flex-direction: ${({ vertical }) => (vertical ? 'column' : 'row')};
   justify-content: ${({ centered }) => (centered ? 'center' : 'initial')};
-  margin: 0 ${th('subGridUnit')};
+  margin-right: ${({ withRightMargin }) =>
+    withRightMargin ? th('gridUnit') : 0};
+
+  & > div {
+    flex: 1;
+  }
 `
 
 export const Label = styled.div`
@@ -66,7 +86,7 @@ export const Err = styled.span`
   color: ${th('colorError')};
   font-family: ${th('fontReading')};
   font-size: ${th('fontSizeBase')};
-  margin-top: calc(${th('gridUnit')} * -1);
+  margin-top: 0;
   text-align: center;
 `
 
@@ -76,7 +96,7 @@ export const Textarea = styled.textarea`
     hasError ? th('colorError') : th('colorPrimary')};
   font-size: ${th('fontSizeBaseSmall')};
   font-family: ${th('fontWriting')};
-  padding: calc(${th('subGridUnit')}*2);
+  padding: ${th('subGridUnit')};
   outline: none;
   transition: all 300ms linear;
 
@@ -92,3 +112,26 @@ export const Textarea = styled.textarea`
     background-color: ${th('colorBackgroundHue')};
   }
 `
+
+export const CustomRadioGroup = styled.div`
+  div {
+    flex-direction: row;
+    justify-content: ${({ justify }) => justify || 'space-between'};
+    label {
+      span:last-child {
+        font-style: normal;
+        ${defaultText};
+      }
+    }
+  }
+  & ~ div {
+    margin-top: 0;
+  }
+`
+
+export const PrivatePolicy = styled.div`
+  ${defaultText};
+  text-align: justify;
+`
+
+export const TextAreaField = input => <Textarea {...input} height={70} />
diff --git a/packages/components-faraday/src/components/UIComponents/InfoPage.js b/packages/components-faraday/src/components/UIComponents/InfoPage.js
new file mode 100644
index 0000000000000000000000000000000000000000..38e16bb14a1a49e448470ae2860cd9b87f102bdd
--- /dev/null
+++ b/packages/components-faraday/src/components/UIComponents/InfoPage.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import styled from 'styled-components'
+import { Button, th } from '@pubsweet/ui'
+
+const InfoPage = ({
+  location: {
+    state: {
+      title = 'Successfully',
+      content = '',
+      path = '/',
+      buttonText = 'Go to Dashboard',
+    },
+  },
+  history,
+}) => (
+  <Root>
+    <Title>{title}</Title>
+    <Content>{content}</Content>
+    <Button onClick={() => history.push(path)} primary>
+      {buttonText}
+    </Button>
+  </Root>
+)
+
+export default InfoPage
+
+// #region styles
+const Root = styled.div`
+  color: ${th('colorText')};
+  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: ${th('gridUnit')} auto;
+`
+
+const Content = styled.p`
+  color: ${th('colorPrimary')};
+  font-family: ${th('fontReading')};
+  font-size: ${th('fontSizeBase')};
+  margin: ${th('gridUnit')} auto;
+`
+// #endregion
diff --git a/packages/components-faraday/src/components/UIComponents/index.js b/packages/components-faraday/src/components/UIComponents/index.js
index 7ccd5732e09478b2c6463473098039f151a19754..5f22ea1c71cc274efe11bf2fdc777e0649a7377b 100644
--- a/packages/components-faraday/src/components/UIComponents/index.js
+++ b/packages/components-faraday/src/components/UIComponents/index.js
@@ -4,6 +4,7 @@ export { FormItems }
 export { default as Logo } from './Logo'
 export { default as Spinner } from './Spinner'
 export { default as NotFound } from './NotFound'
+export { default as InfoPage } from './InfoPage'
 export { default as ErrorPage } from './ErrorPage'
 export { default as DateParser } from './DateParser'
 export { default as ConfirmationPage } from './ConfirmationPage'
diff --git a/packages/components-faraday/src/components/index.js b/packages/components-faraday/src/components/index.js
index 8765d426a79c94ef859e7f154d49d56a6e59d51e..525ddeb0919f6fc43f7c3a9869b490d110e55adb 100644
--- a/packages/components-faraday/src/components/index.js
+++ b/packages/components-faraday/src/components/index.js
@@ -1,13 +1,16 @@
 import { Decision } from './MakeDecision'
+import * as Components from './UIComponents'
 import { Recommendation } from './MakeRecommendation'
 
 export { default as Steps } from './Steps/Steps'
 export { default as Files } from './Files/Files'
 export { default as AppBar } from './AppBar/AppBar'
 export { default as AuthorList } from './AuthorList/AuthorList'
+export { default as withVersion } from './Dashboard/withVersion.js'
 export { default as SortableList } from './SortableList/SortableList'
 
 export { Decision }
+export { Components }
 export { Recommendation }
 export { DragHandle } from './AuthorList/FormItems'
 export { Dropdown, DateParser, Logo, Spinner } from './UIComponents'
diff --git a/packages/components-faraday/src/components/utils.js b/packages/components-faraday/src/components/utils.js
index 1780bd7a318d551ee2fb4dfa5a03c2dbb03be8e1..60a5639770622876a45f7c913667856f5be5b5fd 100644
--- a/packages/components-faraday/src/components/utils.js
+++ b/packages/components-faraday/src/components/utils.js
@@ -1,3 +1,4 @@
+import { SubmissionError } from 'redux-form'
 import { get, find, capitalize } from 'lodash'
 
 export const parseTitle = version => {
@@ -55,6 +56,17 @@ export const handleError = fn => e => {
   fn(get(JSON.parse(e.response), 'error') || 'Oops! Something went wrong!')
 }
 
+export const handleFormError = error => {
+  const err = get(error, 'response')
+  if (err) {
+    const errorMessage =
+      get(JSON.parse(err), 'error') || get(JSON.parse(err), 'message')
+    throw new SubmissionError({
+      _error: errorMessage || 'Something went wrong',
+    })
+  }
+}
+
 const emailRegex = new RegExp(
   /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i, //eslint-disable-line
 )
@@ -66,3 +78,28 @@ export const redirectToError = redirectFn => err => {
   const errorText = get(JSON.parse(err.response), 'error')
   redirectFn('/error-page', errorText || 'Oops! Something went wrong.')
 }
+
+export const passwordValidator = values => {
+  const errors = {}
+  if (!values.password) {
+    errors.password = 'Required'
+  }
+  if (!values.confirmPassword) {
+    errors.confirmPassword = 'Required'
+  } else if (values.confirmPassword !== values.password) {
+    errors.confirmPassword = 'Password mismatched'
+  }
+
+  return errors
+}
+
+export const parseSearchParams = url => {
+  const params = new URLSearchParams(url)
+  const parsedObject = {}
+  /* eslint-disable */
+  for ([key, value] of params) {
+    parsedObject[key] = value
+  }
+  /* eslint-enable */
+  return parsedObject
+}
diff --git a/packages/components-faraday/src/index.js b/packages/components-faraday/src/index.js
index 0eb9c3b474d2da621536bec68cbe2541141b9544..b40accf6eaee709b192b849b8ab1290c47363428 100644
--- a/packages/components-faraday/src/index.js
+++ b/packages/components-faraday/src/index.js
@@ -7,7 +7,6 @@ module.exports = {
       editors: () => require('./redux/editors').default,
       files: () => require('./redux/files').default,
       reviewers: () => require('./redux/reviewers').default,
-      recommendations: () => require('./redux/recommendations').default,
     },
   },
 }
diff --git a/packages/components-faraday/src/redux/authors.js b/packages/components-faraday/src/redux/authors.js
index a348f22be32e767f5f356bbae543fc887082dd5d..324cc26adabc74a3ecc2c4d38111903757c27ab3 100644
--- a/packages/components-faraday/src/redux/authors.js
+++ b/packages/components-faraday/src/redux/authors.js
@@ -1,10 +1,7 @@
-import { get, head } from 'lodash'
-import {
-  create,
-  get as apiGet,
-  remove,
-  update,
-} from 'pubsweet-client/src/helpers/api'
+import { get } from 'lodash'
+import { create, remove, get as apiGet } from 'pubsweet-client/src/helpers/api'
+
+import { handleError } from './utils'
 
 // constants
 const REQUEST = 'authors/REQUEST'
@@ -25,33 +22,29 @@ export const authorSuccess = () => ({
   type: SUCCESS,
 })
 
-export const addAuthor = (author, collectionId) => dispatch => {
+export const getAuthors = (collectionId, fragmentId) =>
+  apiGet(`/collections/${collectionId}/fragments/${fragmentId}/users`)
+
+export const addAuthor = (author, collectionId, fragmentId) => dispatch => {
   dispatch(authorRequest())
-  return create(`/collections/${collectionId}/users`, {
+  return create(`/collections/${collectionId}/fragments/${fragmentId}/users`, {
     email: author.email,
     role: 'author',
     ...author,
-  })
+  }).then(author => {
+    dispatch(authorSuccess())
+    return author
+  }, handleError(authorFailure, dispatch))
 }
 
-export const deleteAuthor = (collectionId, userId) => dispatch => {
+export const deleteAuthor = (collectionId, fragmentId, userId) => dispatch => {
   dispatch(authorRequest())
-  return remove(`/collections/${collectionId}/users/${userId}`)
-}
-
-export const editAuthor = (collectionId, userId, body) =>
-  update(`/collections/${collectionId}/users/${userId}`, body)
-
-export const getAuthors = collectionId =>
-  apiGet(`/collections/${collectionId}/users`)
-
-export const getAuthorsTeam = collectionId =>
-  apiGet(`/teams?object.id=${collectionId}&group=author`).then(teams =>
-    head(teams),
+  return remove(
+    `/collections/${collectionId}/fragments/${fragmentId}/users/${userId}`,
   )
-
-export const updateAuthorsTeam = (teamId, body) =>
-  update(`/teams/${teamId}`, body)
+    .then(() => dispatch(authorSuccess()))
+    .catch(handleError(authorFailure, dispatch))
+}
 
 // selectors
 export const getFragmentAuthors = (state, fragmentId) =>
diff --git a/packages/components-faraday/src/redux/editors.js b/packages/components-faraday/src/redux/editors.js
index 63cb3b837d2b43150e4487bcc0cb65fd60327e9d..17984b255e19f6a3e51ac84481487980774744c1 100644
--- a/packages/components-faraday/src/redux/editors.js
+++ b/packages/components-faraday/src/redux/editors.js
@@ -38,7 +38,7 @@ export const assignHandlingEditor = (email, collectionId) => dispatch => {
     },
     err => {
       dispatch(editorsDone())
-      return err
+      throw err
     },
   )
 }
@@ -57,7 +57,7 @@ export const revokeHandlingEditor = (
     },
     err => {
       dispatch(editorsDone())
-      return err
+      throw err
     },
   )
 }
diff --git a/packages/components-faraday/src/redux/files.js b/packages/components-faraday/src/redux/files.js
index a296178e9e7e0b996ba035bdf84adf4000529b24..63a616ed58a0ccf5b58cc023cd4917b07661c484 100644
--- a/packages/components-faraday/src/redux/files.js
+++ b/packages/components-faraday/src/redux/files.js
@@ -80,14 +80,17 @@ export const uploadFile = (file, type, fragmentId) => dispatch => {
   )
 }
 
-export const deleteFile = fileId => dispatch => {
-  dispatch(removeRequest())
+export const deleteFile = (fileId, type = 'manuscripts') => dispatch => {
+  dispatch(removeRequest(type))
   return remove(`/files/${fileId}`)
     .then(r => {
       dispatch(removeSuccess())
       return r
     })
-    .catch(err => dispatch(removeFailure(err.message)))
+    .catch(err => {
+      dispatch(removeFailure(err.message))
+      throw err
+    })
 }
 
 export const getSignedUrl = fileId => dispatch => get(`/files/${fileId}`)
@@ -95,6 +98,7 @@ export const getSignedUrl = fileId => dispatch => get(`/files/${fileId}`)
 // reducer
 export default (state = initialState, action) => {
   switch (action.type) {
+    case REMOVE_REQUEST:
     case UPLOAD_REQUEST:
       return {
         ...state,
@@ -105,12 +109,14 @@ export default (state = initialState, action) => {
         },
       }
     case UPLOAD_FAILURE:
+    case REMOVE_FAILURE:
       return {
         ...state,
         isFetching: initialState.isFetching,
         error: action.error,
       }
     case UPLOAD_SUCCESS:
+    case REMOVE_SUCCESS:
       return {
         ...state,
         isFetching: initialState.isFetching,
diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js
index a85e13f8c34818166263789e08c10df7d76ff3be..4a62913c04cc48357bd9f82d65437732a36cf230 100644
--- a/packages/components-faraday/src/redux/recommendations.js
+++ b/packages/components-faraday/src/redux/recommendations.js
@@ -1,151 +1,40 @@
 import { get } from 'lodash'
 import { create, update } from 'pubsweet-client/src/helpers/api'
 
-// #region Constants
-const REQUEST = 'recommendations/REQUEST'
-const ERROR = 'recommendations/ERROR'
-
-const GET_FRAGMENT_SUCCESS = 'GET_FRAGMENT_SUCCESS'
-const GET_RECOMMENDATIONS_SUCCESS = 'recommendations/GET_SUCCESS'
-const CREATE_RECOMMENDATION_SUCCESS = 'recommendations/CREATE_SUCCESS'
-const UPDATE_RECOMMENDATION_SUCCESS = 'recommendations/UPDATE_SUCCESS'
-// #endregion
-
-// #region Action Creators
-export const recommendationsRequest = () => ({
-  type: REQUEST,
-})
-
-export const recommendationsError = error => ({
-  type: ERROR,
-  error,
-})
-
-export const getRecommendationsSuccess = recommendations => ({
-  type: GET_RECOMMENDATIONS_SUCCESS,
-  payload: { recommendations },
-})
-
-export const createRecommendationSuccess = recommendation => ({
-  type: CREATE_RECOMMENDATION_SUCCESS,
-  payload: { recommendation },
-})
-
-export const updateRecommendationSuccess = recommendation => ({
-  type: UPDATE_RECOMMENDATION_SUCCESS,
-  payload: { recommendation },
-})
-// #endregion
-
 // #region Selectors
-export const selectFetching = state =>
-  get(state, 'recommendations.fetching') || false
-export const selectError = state => get(state, 'recommendations.error')
-export const selectRecommendations = state =>
-  get(state, 'recommendations.recommendations') || []
-export const selectEditorialRecommendations = state =>
-  selectRecommendations(state).filter(
+export const selectRecommendations = (state, fragmentId) =>
+  get(state, `fragments.${fragmentId}.recommendations`) || []
+export const selectEditorialRecommendations = (state, fragmentId) =>
+  selectRecommendations(state, fragmentId).filter(
     r => r.recommendationType === 'editorRecommendation' && r.comments,
   )
+export const selectReviewRecommendations = (state, fragmentId) =>
+  selectRecommendations(state, fragmentId).filter(
+    r => r.recommendationType === 'review',
+  )
 // #endregion
 
 // #region Actions
+// error handling and fetching is handled by the autosave reducer
 export const createRecommendation = (
   collId,
   fragId,
   recommendation,
-) => dispatch => {
-  dispatch(recommendationsRequest())
-  return create(
+) => dispatch =>
+  create(
     `/collections/${collId}/fragments/${fragId}/recommendations`,
     recommendation,
-  ).then(
-    r => {
-      dispatch(getRecommendationsSuccess([r]))
-      return r
-    },
-    err => {
-      const error = get(err, 'response')
-      if (error) {
-        const errorMessage = get(JSON.parse(error), 'error')
-        dispatch(recommendationsError(errorMessage))
-      }
-      throw err
-    },
   )
-}
 
 export const updateRecommendation = (
   collId,
   fragId,
   recommendation,
-) => dispatch => {
-  dispatch(recommendationsRequest())
-  return update(
+) => dispatch =>
+  update(
     `/collections/${collId}/fragments/${fragId}/recommendations/${
       recommendation.id
     }`,
     recommendation,
-  ).then(
-    r => {
-      dispatch(getRecommendationsSuccess([r]))
-      return r
-    },
-    err => {
-      const error = get(err, 'response')
-      if (error) {
-        const errorMessage = get(JSON.parse(error), 'error')
-        dispatch(recommendationsError(errorMessage))
-      }
-    },
   )
-}
-// #endregion
-
-// #region State
-const initialState = {
-  fetching: false,
-  error: null,
-  recommendations: [],
-}
-
-export default (state = initialState, action = {}) => {
-  switch (action.type) {
-    case REQUEST:
-      return {
-        ...state,
-        fetching: true,
-      }
-    case ERROR:
-      return {
-        ...state,
-        fetching: false,
-        error: action.error,
-      }
-    case GET_FRAGMENT_SUCCESS:
-      return {
-        ...state,
-        fetching: false,
-        error: null,
-        recommendations: get(action, 'fragment.recommendations'),
-      }
-    case GET_RECOMMENDATIONS_SUCCESS:
-      return {
-        ...state,
-        fetching: false,
-        error: null,
-        recommendations: action.payload.recommendations,
-      }
-    case UPDATE_RECOMMENDATION_SUCCESS:
-    case CREATE_RECOMMENDATION_SUCCESS:
-      return {
-        ...state,
-        fetching: false,
-        error: null,
-        recommendations: [action.payload.recommendation],
-      }
-    default:
-      return state
-  }
-}
 // #endregion
diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js
index e21dae2a3fa17e369ffcae12f83dbc01b7b8c7d0..73a7501d1ac367974c29bd220b0d7787228176d2 100644
--- a/packages/components-faraday/src/redux/reviewers.js
+++ b/packages/components-faraday/src/redux/reviewers.js
@@ -81,19 +81,17 @@ export const selectFetchingInvite = state =>
 export const selectFetchingDecision = state =>
   get(state, 'reviewers.fetching.decision') || false
 
-export const selectInvitation = (state, collectionId) => {
+export const selectInvitation = (state, fragmentId) => {
   const currentUser = selectCurrentUser(state)
-  const collection = state.collections.find(c => c.id === collectionId)
-  const invitations = get(collection, 'invitations') || []
+  const invitations = get(state, `fragments.${fragmentId}.invitations`) || []
   return invitations.find(
     i => i.userId === currentUser.id && i.role === 'reviewer' && !i.hasAnswer,
   )
 }
 
-export const currentUserIsReviewer = (state, collectionId) => {
+export const currentUserIsReviewer = (state, fragmentId) => {
   const currentUser = selectCurrentUser(state)
-  const collection = state.collections.find(c => c.id === collectionId)
-  const invitations = get(collection, 'invitations') || []
+  const invitations = get(state, `fragments.${fragmentId}.invitations`) || []
   return !!invitations.find(
     i =>
       i.userId === currentUser.id &&
@@ -103,22 +101,37 @@ export const currentUserIsReviewer = (state, collectionId) => {
   )
 }
 
-export const getCollectionReviewers = collectionId => dispatch => {
+export const getCollectionReviewers = (
+  collectionId,
+  fragmentId,
+) => dispatch => {
   dispatch(getReviewersRequest())
-  return apiGet(`/collections/${collectionId}/invitations?role=reviewer`).then(
+  return apiGet(
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations?role=reviewer`,
+  ).then(
     r => dispatch(getReviewersSuccess(orderBy(r, orderReviewers))),
-    err => dispatch(getReviewersError(err)),
+    err => {
+      dispatch(getReviewersError(err))
+      throw err
+    },
   )
 }
 // #endregion
 
 // #region Actions - invitations
-export const inviteReviewer = (reviewerData, collectionId) => dispatch => {
+export const inviteReviewer = (
+  reviewerData,
+  collectionId,
+  fragmentId,
+) => dispatch => {
   dispatch(inviteRequest())
-  return create(`/collections/${collectionId}/invitations`, {
-    ...reviewerData,
-    role: 'reviewer',
-  }).then(
+  return create(
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations`,
+    {
+      ...reviewerData,
+      role: 'reviewer',
+    },
+  ).then(
     () => dispatch(inviteSuccess()),
     err => {
       dispatch(inviteError(get(JSON.parse(err.response), 'error')))
@@ -135,10 +148,14 @@ export const setReviewerPassword = reviewerBody => dispatch => {
   })
 }
 
-export const revokeReviewer = (invitationId, collectionId) => dispatch => {
+export const revokeReviewer = (
+  invitationId,
+  collectionId,
+  fragmentId,
+) => dispatch => {
   dispatch(inviteRequest())
   return remove(
-    `/collections/${collectionId}/invitations/${invitationId}`,
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`,
   ).then(
     () => dispatch(inviteSuccess()),
     err => {
@@ -153,12 +170,16 @@ export const revokeReviewer = (invitationId, collectionId) => dispatch => {
 export const reviewerDecision = (
   invitationId,
   collectionId,
+  fragmentId,
   agree = true,
 ) => dispatch => {
   dispatch(reviewerDecisionRequest())
-  return update(`/collections/${collectionId}/invitations/${invitationId}`, {
-    isAccepted: agree,
-  }).then(
+  return update(
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`,
+    {
+      isAccepted: agree,
+    },
+  ).then(
     res => {
       dispatch(reviewerDecisionSuccess())
       return res
@@ -173,11 +194,12 @@ export const reviewerDecision = (
 export const reviewerDecline = (
   invitationId,
   collectionId,
+  fragmentId,
   invitationToken,
 ) => dispatch => {
   dispatch(reviewerDecisionRequest())
   return update(
-    `/collections/${collectionId}/invitations/${invitationId}/decline`,
+    `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}/decline`,
     {
       invitationToken,
     },
diff --git a/packages/components-faraday/src/redux/users.js b/packages/components-faraday/src/redux/users.js
index dd9cd57ab2db907fbbeb5bd31a46a90afb859c06..a4e6d1a12984cde20989701eadbeddae1728779b 100644
--- a/packages/components-faraday/src/redux/users.js
+++ b/packages/components-faraday/src/redux/users.js
@@ -1,4 +1,22 @@
 import { get } from 'lodash'
+import { create } from 'pubsweet-client/src/helpers/api'
+
+const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
+
+const loginSuccess = user => ({
+  type: LOGIN_SUCCESS,
+  token: user.token,
+  user,
+})
 
 export const currentUserIs = (state, role) =>
   get(state, `currentUser.user.${role}`)
+
+export const confirmUser = (userId, confirmationToken) => dispatch =>
+  create(`/users/confirm`, {
+    userId,
+    confirmationToken,
+  }).then(user => {
+    localStorage.setItem('token', user.token)
+    return dispatch(loginSuccess(user))
+  })
diff --git a/packages/components-faraday/src/redux/utils.js b/packages/components-faraday/src/redux/utils.js
index 14560f9260f6bfeec3f768e7a47b2c2147c1d111..a84e7328c7de7431436d9cc8fc10bf6551e0f119 100644
--- a/packages/components-faraday/src/redux/utils.js
+++ b/packages/components-faraday/src/redux/utils.js
@@ -9,3 +9,12 @@ export const orderReviewers = r => {
       return 1
   }
 }
+
+export const handleError = (fn, dispatch = null) => err => {
+  if (typeof dispatch === 'function') {
+    dispatch(fn(err))
+  } else {
+    fn(err)
+  }
+  throw err
+}
diff --git a/packages/xpub-faraday/app/FaradayApp.js b/packages/xpub-faraday/app/FaradayApp.js
index 8938b75560aa3fa57aadcdc95e2ec1e6ad4cab08..05d83b3361bea32267f77fc663ef3f834ae27717 100644
--- a/packages/xpub-faraday/app/FaradayApp.js
+++ b/packages/xpub-faraday/app/FaradayApp.js
@@ -6,14 +6,21 @@ import { actions } from 'pubsweet-client'
 import { withJournal } from 'xpub-journal'
 import { AppBar } from 'pubsweet-components-faraday/src/components'
 
-const App = ({ children, currentUser, journal, logoutUser }) => (
-  <Root>
+const App = ({
+  journal,
+  children,
+  logoutUser,
+  currentUser,
+  isAuthenticated,
+}) => (
+  <Root className="faraday-root">
     <AppBar
       brand={journal.metadata.name}
+      isAuthenticated={isAuthenticated}
       onLogoutClick={logoutUser}
       user={currentUser}
     />
-    <MainContainer>{children}</MainContainer>
+    <MainContainer className="faraday-main">{children}</MainContainer>
   </Root>
 )
 
@@ -21,6 +28,7 @@ export default compose(
   connect(
     state => ({
       currentUser: state.currentUser.user,
+      isAuthenticated: state.currentUser.isAuthenticated,
     }),
     { logoutUser: actions.logoutUser },
   ),
@@ -35,7 +43,7 @@ const Root = styled.div`
 `
 
 const MainContainer = styled.div`
-  padding: 90px 10px 40px;
-  min-height: 100vh;
   background-color: ${props => props.theme.backgroundColor || '#fff'};
+  padding: 110px 10px 0 10px;
+  height: 100vh;
 `
diff --git a/packages/xpub-faraday/app/config/journal/submit-wizard.js b/packages/xpub-faraday/app/config/journal/submit-wizard.js
index 8654c6a2eb9b2222c089799fcc8501812d3b467e..a7c54767357c09f25fe2d67e4e7084471749ad02 100644
--- a/packages/xpub-faraday/app/config/journal/submit-wizard.js
+++ b/packages/xpub-faraday/app/config/journal/submit-wizard.js
@@ -2,7 +2,7 @@ import React from 'react'
 import styled from 'styled-components'
 import uploadFileFn from 'xpub-upload'
 import { AbstractEditor, TitleEditor } from 'xpub-edit'
-import { Menu, YesOrNo, TextField, CheckboxGroup } from '@pubsweet/ui'
+import { Menu, YesOrNo, CheckboxGroup } from '@pubsweet/ui'
 import { required, minChars, minSize } from 'xpub-validators'
 import { AuthorList, Files } from 'pubsweet-components-faraday/src/components'
 
@@ -10,10 +10,10 @@ import { declarations } from './'
 import issueTypes from './issues-types'
 import manuscriptTypes from './manuscript-types'
 import {
-  requiredBasedOnType,
-  editModeEnabled,
-  parseEmptyHtml,
   requiredFiles,
+  parseEmptyHtml,
+  editModeEnabled,
+  requiredBasedOnType,
 } from './wizard-validators'
 
 const min3Chars = minChars(3)
@@ -51,7 +51,13 @@ const uploadFile = input => uploadFileFn(input)
 
 export default {
   showProgress: true,
-  formSectionKeys: ['metadata', 'declarations', 'conflicts', 'files'],
+  formSectionKeys: [
+    'authors',
+    'metadata',
+    'declarations',
+    'conflicts',
+    'files',
+  ],
   submissionRedirect: '/confirmation-page',
   dispatchFunctions: [uploadFile],
   steps: [
@@ -102,7 +108,7 @@ export default {
       label: 'Manuscript & Authors Details',
       title: '3. Manuscript & Authors Details',
       subtitle:
-        'Please provide the details of all the authors of this manuscript, in the order that they appear on the manuscript. Your details are already pre-filled since, in order tu submit a manuscript you must be one of the authors',
+        'Please provide the details of all the authors of this manuscript, in the order that they appear on the manuscript. Your details are already pre-filled since, in order tu submit a manuscript you must be one of the authors.',
       children: [
         {
           fieldId: 'metadata.title',
@@ -147,7 +153,7 @@ export default {
           validate: [required],
         },
         {
-          fieldId: 'editMode',
+          fieldId: 'authorForm',
           renderComponent: Spacing,
           validate: [editModeEnabled],
         },
@@ -163,7 +169,7 @@ export default {
             condition: 'yes',
           },
           fieldId: 'conflicts.message',
-          renderComponent: TextField,
+          renderComponent: AbstractEditor,
           label: 'Conflict of interest details',
           validate: [required, min3Chars],
         },
@@ -174,7 +180,7 @@ export default {
       title: '4. Manuscript Files Upload',
       children: [
         {
-          fieldId: 'file-upload',
+          fieldId: 'files',
           renderComponent: Files,
           validate: [requiredFiles],
         },
diff --git a/packages/xpub-faraday/app/config/journal/wizard-validators.js b/packages/xpub-faraday/app/config/journal/wizard-validators.js
index bec3371d1699e67c3ef81688f26205bee7328b46..25d58d4372c51981864e9e7822e81b219ac9123e 100644
--- a/packages/xpub-faraday/app/config/journal/wizard-validators.js
+++ b/packages/xpub-faraday/app/config/journal/wizard-validators.js
@@ -23,7 +23,7 @@ export const requiredBasedOnType = (value, formValues) => {
   return undefined
 }
 
-export const editModeEnabled = value => {
+export const editModeEnabled = (value, allValues) => {
   if (value) {
     return 'You have some unsaved author details.'
   }
diff --git a/packages/xpub-faraday/app/routes.js b/packages/xpub-faraday/app/routes.js
index bf161391ab7e1d48ff2c7d2241929e5160a7e68c..98d8d460a2558e90eb0670bb9e6965a0c63f5519 100644
--- a/packages/xpub-faraday/app/routes.js
+++ b/packages/xpub-faraday/app/routes.js
@@ -3,30 +3,31 @@ import { withProps } from 'recompose'
 import { Route, Switch } from 'react-router-dom'
 import { AuthenticatedComponent } from 'pubsweet-client'
 import Login from 'pubsweet-component-login/LoginContainer'
-import Signup from 'pubsweet-component-signup/SignupContainer'
 
 import { Wizard } from 'pubsweet-component-wizard/src/components'
 import { ManuscriptPage } from 'pubsweet-component-manuscript/src/components'
 import DashboardPage from 'pubsweet-components-faraday/src/components/Dashboard'
 import {
   NotFound,
-  ConfirmationPage,
+  InfoPage,
   ErrorPage,
+  ConfirmationPage,
 } from 'pubsweet-components-faraday/src/components/UIComponents/'
 import {
-  AdminDashboard,
   AdminUsers,
   AdminRoute,
+  AdminDashboard,
 } from 'pubsweet-components-faraday/src/components/Admin'
 import AddEditUser from 'pubsweet-components-faraday/src/components/Admin/AddEditUser'
 import {
-  SignUpInvitationPage,
+  ConfirmAccount,
   ReviewerSignUp,
+  SignUpInvitationPage,
 } from 'pubsweet-components-faraday/src/components/SignUp'
 
 import FaradayApp from './FaradayApp'
 
-const LoginPage = withProps({ passwordReset: false })(Login)
+const LoginPage = withProps({ passwordReset: true })(Login)
 
 const PrivateRoute = ({ component: Component, ...rest }) => (
   <Route
@@ -43,7 +44,44 @@ const Routes = () => (
   <FaradayApp>
     <Switch>
       <Route component={LoginPage} exact path="/login" />
-      <Route component={Signup} exact path="/signup" />
+      <Route component={SignUpInvitationPage} exact path="/invite" />
+      <Route
+        component={routeParams => (
+          <SignUpInvitationPage
+            subtitle={null}
+            title="Author signup"
+            type="signup"
+            {...routeParams}
+          />
+        )}
+        exact
+        path="/signup"
+      />
+      <Route
+        component={routeParams => (
+          <SignUpInvitationPage
+            subtitle={null}
+            title="Reset password"
+            type="forgotPassword"
+            {...routeParams}
+          />
+        )}
+        exact
+        path="/password-reset"
+      />
+      <Route
+        component={routeParams => (
+          <SignUpInvitationPage
+            subtitle={null}
+            title="Set new password"
+            type="setPassword"
+            {...routeParams}
+          />
+        )}
+        exact
+        path="/forgot-password"
+      />
+      <Route component={ConfirmAccount} exact path="/confirm-signup" />
       <PrivateRoute component={DashboardPage} exact path="/" />
       <PrivateRoute
         component={ConfirmationPage}
@@ -63,7 +101,6 @@ const Routes = () => (
         exact
         path="/projects/:project/versions/:version/submit"
       />
-      <Route component={SignUpInvitationPage} exact path="/invite" />
       <Route component={ReviewerSignUp} exact path="/invite-reviewer" />
       <PrivateRoute
         component={ManuscriptPage}
@@ -72,6 +109,7 @@ const Routes = () => (
       />
 
       <Route component={ErrorPage} exact path="/error-page" />
+      <Route component={InfoPage} exact path="/info-page" />
       <Route component={NotFound} />
     </Switch>
   </FaradayApp>
diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js
index 1b7642bfca5445c4c1651d8cbfcca8aad45ef8c5..49d976c4c1491b23f5ab7d1f062ba8ee96373457 100644
--- a/packages/xpub-faraday/config/authsome-helpers.js
+++ b/packages/xpub-faraday/config/authsome-helpers.js
@@ -1,10 +1,12 @@
-const omit = require('lodash/omit')
+const { omit, get, last } = require('lodash')
+
 const config = require('config')
-const get = require('lodash/get')
 
 const statuses = config.get('statuses')
 
+const keysToOmit = ['email', 'id']
 const publicStatusesPermissions = ['author', 'reviewer']
+const authorAllowedStatuses = ['revisionRequested', 'rejected', 'accepted']
 
 const parseAuthorsData = (coll, matchingCollPerm) => {
   if (['reviewer'].includes(matchingCollPerm.permission)) {
@@ -44,7 +46,10 @@ const filterObjectData = (
           rec => rec.userId === user.id,
         )
     }
-
+    parseAuthorsData(object, matchingCollPerm)
+    if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
+      return filterRefusedInvitations(object, user)
+    }
     return object
   }
   const matchingCollPerm = collectionsPermissions.find(
@@ -52,16 +57,16 @@ const filterObjectData = (
   )
   if (matchingCollPerm === undefined) return null
   setPublicStatuses(object, matchingCollPerm)
-  parseAuthorsData(object, matchingCollPerm)
-  if (['reviewer', 'handlingEditor'].includes(matchingCollPerm.permission)) {
-    return filterRefusedInvitations(object, user)
-  }
 
   return object
 }
 
-const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => {
-  const teams = await Promise.all(
+const getTeamsByPermissions = async (
+  teamIds = [],
+  permissions = [],
+  TeamModel,
+) =>
+  (await Promise.all(
     teamIds.map(async teamId => {
       const team = await TeamModel.find(teamId)
       if (!permissions.includes(team.teamType.permissions)) {
@@ -69,15 +74,130 @@ const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => {
       }
       return team
     }),
+  )).filter(Boolean)
+
+const heIsInvitedToFragment = async ({ user, Team, collectionId }) =>
+  (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some(
+    // user is a member of the team with access to the fragment's parent collection
+    t => t.members.includes(user.id) && t.object.id === collectionId,
   )
 
-  return teams.filter(Boolean)
+const getUserPermissions = async ({
+  user,
+  Team,
+  mapFn = t => ({
+    objectId: t.object.id,
+    objectType: t.object.type,
+    role: t.teamType.permissions,
+  }),
+}) =>
+  (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn)
+
+const isOwner = ({ user: { id }, object }) => {
+  if (object.owners.includes(id)) return true
+  return !!object.owners.find(own => own.id === id)
+}
+
+const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => {
+  const userPermissions = await getUserPermissions({
+    user,
+    Team,
+  })
+
+  return !!userPermissions.find(p => {
+    const hasObject =
+      p.objectId === get(object, 'fragment.id') ||
+      p.objectId === get(object, 'fragment.collectionId')
+    if (roles.length > 0) {
+      return hasObject && roles.includes(p.role)
+    }
+    return hasObject
+  })
+}
+
+const isHandlingEditor = ({ user, object }) =>
+  get(object, 'collection.handlingEditor.id') === user.id
+
+const isInDraft = fragment => !get(fragment, 'submitted')
+
+const hasFragmentInDraft = async ({ object, Fragment }) => {
+  const lastFragmentId = last(get(object, 'fragments'))
+  const fragment = await Fragment.find(lastFragmentId)
+  return isInDraft(fragment)
+}
+
+const filterAuthorRecommendations = (recommendations, status, isLast) => {
+  const canViewRecommendations = authorAllowedStatuses.includes(status)
+  if (canViewRecommendations || !isLast) {
+    return recommendations.map(r => ({
+      ...r,
+      comments: r.comments ? r.comments.filter(c => c.public) : [],
+    }))
+  }
+  return []
+}
+
+const stripeCollectionByRole = (coll = {}, role = '') => {
+  if (role === 'author') {
+    const { handlingEditor } = coll
+
+    if (!authorAllowedStatuses.includes(coll.status)) {
+      return {
+        ...coll,
+        handlingEditor: handlingEditor &&
+          handlingEditor.isAccepted && {
+            ...omit(handlingEditor, keysToOmit),
+            name: 'Assigned',
+          },
+      }
+    }
+  }
+  return coll
+}
+
+const stripeFragmentByRole = ({
+  fragment = {},
+  role = '',
+  status = 'draft',
+  user = {},
+  isLast = false,
+}) => {
+  const { recommendations, files, authors } = fragment
+  switch (role) {
+    case 'author':
+      return {
+        ...fragment,
+        recommendations: recommendations
+          ? filterAuthorRecommendations(recommendations, status, isLast)
+          : [],
+      }
+    case 'reviewer':
+      return {
+        ...fragment,
+        files: omit(files, ['coverLetter']),
+        authors: authors.map(a => omit(a, ['email'])),
+        recommendations: recommendations
+          ? recommendations.filter(r => r.userId === user.id)
+          : [],
+      }
+    default:
+      return fragment
+  }
 }
 
 module.exports = {
+  filterObjectData,
   parseAuthorsData,
   setPublicStatuses,
-  filterRefusedInvitations,
-  filterObjectData,
   getTeamsByPermissions,
+  filterRefusedInvitations,
+  isOwner,
+  isHandlingEditor,
+  getUserPermissions,
+  heIsInvitedToFragment,
+  hasPermissionForObject,
+  isInDraft,
+  hasFragmentInDraft,
+  stripeCollectionByRole,
+  stripeFragmentByRole,
 }
diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js
index 948dd93e25476745ef93e4db143ff99f79ff8cec..143c49ac2b2ba29100d44a12c9a3352eda9f26e7 100644
--- a/packages/xpub-faraday/config/authsome-mode.js
+++ b/packages/xpub-faraday/config/authsome-mode.js
@@ -1,62 +1,8 @@
-const get = require('lodash/get')
-const pickBy = require('lodash/pickBy')
-const omit = require('lodash/omit')
-const helpers = require('./authsome-helpers')
-
-async function teamPermissions(user, operation, object, context) {
-  const permissions = ['handlingEditor', 'author', 'reviewer']
-  const teams = await helpers.getTeamsByPermissions(
-    user.teams,
-    permissions,
-    context.models.Team,
-  )
-
-  let collectionsPermissions = await Promise.all(
-    teams.map(async team => {
-      const collection = await context.models.Collection.find(team.object.id)
-      if (
-        collection.status === 'rejected' &&
-        team.teamType.permissions === 'reviewer'
-      )
-        return null
-      const collPerm = {
-        id: collection.id,
-        permission: team.teamType.permissions,
-      }
-      const objectType = get(object, 'type')
-      if (objectType === 'fragment') {
-        if (collection.fragments.includes(object.id))
-          collPerm.fragmentId = object.id
-        else return null
-      }
+const config = require('config')
+const { get, pickBy, last } = require('lodash')
 
-      if (objectType === 'collection')
-        if (object.id !== collection.id) return null
-      return collPerm
-    }),
-  )
-  collectionsPermissions = collectionsPermissions.filter(cp => cp !== null)
-  if (collectionsPermissions.length === 0) return false
-
-  return {
-    filter: filterParam => {
-      if (!filterParam.length) {
-        return helpers.filterObjectData(
-          collectionsPermissions,
-          filterParam,
-          user,
-        )
-      }
-
-      const collections = filterParam
-        .map(coll =>
-          helpers.filterObjectData(collectionsPermissions, coll, user),
-        )
-        .filter(Boolean)
-      return collections
-    },
-  }
-}
+const statuses = config.get('statuses')
+const helpers = require('./authsome-helpers')
 
 function unauthenticatedUser(operation, object) {
   // Public/unauthenticated users can GET /collections, filtered by 'published'
@@ -104,136 +50,230 @@ function unauthenticatedUser(operation, object) {
   return false
 }
 
-async function authenticatedUser(user, operation, object, context) {
-  // Allow the authenticated user to POST a collection (but not with a 'filtered' property)
-  if (operation === 'POST' && object.path === '/collections') {
-    return {
-      filter: collection => omit(collection, 'filtered'),
+const createPaths = ['/collections', '/collections/:collectionId/fragments']
+
+async function applyAuthenticatedUserPolicy(user, operation, object, context) {
+  if (operation === 'GET') {
+    if (get(object, 'path') === '/collections') {
+      return {
+        filter: async collections => {
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+          return collections.filter(collection => {
+            if (collection.owners.includes(user.id)) {
+              return true
+            }
+            const collectionPermission = userPermissions.find(
+              p => p.objectId === collection.id,
+            )
+            if (collectionPermission) {
+              return true
+            }
+
+            const fragmentPermission = userPermissions.find(p =>
+              collection.fragments.includes(p.objectId),
+            )
+            if (fragmentPermission) {
+              return true
+            }
+            return false
+          })
+        },
+      }
     }
-  }
 
-  if (
-    operation === 'POST' &&
-    object.path === '/collections/:collectionId/fragments'
-  ) {
-    return true
-  }
+    if (object === '/users') {
+      return true
+    }
 
-  // allow authenticate owners full pass for a collection
-  if (get(object, 'type') === 'collection') {
-    if (operation === 'PATCH') {
+    if (get(object, 'type') === 'collection') {
       return {
-        filter: collection => omit(collection, 'filtered'),
+        filter: async collection => {
+          const status = get(collection, 'status') || 'draft'
+          const userPermissions = await helpers.getUserPermissions({
+            user,
+            Team: context.models.Team,
+          })
+
+          const { role } = userPermissions.find(
+            p =>
+              p.objectId === collection.id ||
+              collection.fragments.includes(p.objectId),
+          )
+          const visibleStatus = get(statuses, `${status}.${role}.label`)
+          const parsedCollection = helpers.stripeCollectionByRole(
+            collection,
+            role,
+          )
+
+          return {
+            ...parsedCollection,
+            visibleStatus,
+          }
+        },
       }
     }
-    if (object.owners.includes(user.id)) return true
-    const owner = object.owners.find(own => own.id === user.id)
-    if (owner !== undefined) return true
-  }
 
-  // Allow owners of a collection to GET its teams, e.g.
-  // GET /api/collections/1/teams
-  if (operation === 'GET' && get(object, 'path') === '/teams') {
-    const collectionId = get(object, 'params.collectionId')
-    if (collectionId) {
-      const collection = await context.models.Collection.find(collectionId)
-      if (collection.owners.includes(user.id)) {
-        return true
+    if (get(object, 'type') === 'fragment') {
+      if (helpers.isInDraft(object)) {
+        return helpers.isOwner({ user, object })
+      }
+
+      const userPermissions = await helpers.getUserPermissions({
+        user,
+        Team: context.models.Team,
+      })
+
+      const permission = userPermissions.find(
+        p => p.objectId === object.id || p.objectId === object.collectionId,
+      )
+
+      if (!permission) return false
+
+      const collectionId = get(object, 'collectionId')
+      const { status, fragments } = await context.models.Collection.find(
+        collectionId,
+      )
+
+      return {
+        filter: fragment =>
+          helpers.stripeFragmentByRole({
+            fragment,
+            role: permission.role,
+            status,
+            user,
+            isLast: last(fragments) === fragment.id,
+          }),
       }
     }
-  }
 
-  if (
-    operation === 'GET' &&
-    get(object, 'type') === 'team' &&
-    get(object, 'object.type') === 'collection'
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object, 'object.id'),
-    )
-    if (collection.owners.includes(user.id)) {
+    if (get(object, 'type') === 'user') {
       return true
     }
-  }
 
-  // Advanced example
-  // Allow authenticated users to create a team based around a collection
-  // if they are one of the owners of this collection
-  if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') {
-    if (get(object, 'object.type') === 'collection') {
-      const collection = await context.models.Collection.find(
-        get(object, 'object.id'),
-      )
-      if (collection.owners.includes(user.id)) {
+    // allow HE to get reviewer invitations
+    if (get(object, 'fragment.type') === 'fragment') {
+      const collectionId = get(object, 'fragment.collectionId')
+      const collection = await context.models.Collection.find(collectionId)
+
+      if (get(collection, 'handlingEditor.id') === user.id) {
         return true
       }
     }
-  }
 
-  // only allow the HE to create, delete an invitation, or get invitation details
-  if (
-    ['POST', 'GET', 'DELETE'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('invitations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const handlingEditor = get(collection, 'handlingEditor')
-    if (!handlingEditor) return false
-    if (handlingEditor.id === user.id) return true
-    return false
+    if (get(object, 'type') === 'user') {
+      return true
+    }
   }
 
-  // only allow a reviewer and an HE to submit and to modify a recommendation
-  if (
-    ['POST', 'PATCH'].includes(operation) &&
-    get(object.collection, 'type') === 'collection' &&
-    object.path.includes('recommendations')
-  ) {
-    const collection = await context.models.Collection.find(
-      get(object.collection, 'id'),
-    )
-    const teams = await helpers.getTeamsByPermissions(
-      user.teams,
-      ['reviewer', 'handlingEditor'],
-      context.models.Team,
-    )
-    if (teams.length === 0) return false
-    const matchingTeam = teams.find(team => team.object.id === collection.id)
-    if (matchingTeam) return true
-    return false
-  }
+  if (operation === 'POST') {
+    // allow everyone to create manuscripts and versions
+    if (createPaths.includes(object.path)) {
+      return true
+    }
 
-  if (user.teams.length !== 0 && ['GET'].includes(operation)) {
-    const permissions = await teamPermissions(user, operation, object, context)
+    // allow HE to invite
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
 
-    if (permissions) {
-      return permissions
+    // allow HE or assigned reviewers to recommend
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer', 'handlingEditor'],
+      })
     }
 
-    return false
+    // allow owner to submit a manuscript
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/submit'
+    ) {
+      return helpers.isOwner({ user, object: object.fragment })
+    }
   }
 
-  if (get(object, 'type') === 'fragment') {
-    const fragment = object
+  if (operation === 'PATCH') {
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
+    }
 
-    if (fragment.owners.includes(user.id)) {
+    if (get(object, 'type') === 'fragment') {
+      return helpers.isOwner({ user, object })
+    }
+
+    // allow reviewer to patch his recommendation
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId'
+    ) {
+      return helpers.hasPermissionForObject({
+        user,
+        object,
+        Team: context.models.Team,
+        roles: ['reviewer'],
+      })
+    }
+
+    if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
       return true
     }
+
+    // allow owner to submit a revision
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/submit'
+    ) {
+      return helpers.isOwner({ user, object: object.fragment })
+    }
   }
 
-  // A user can GET, DELETE and PATCH itself
-  if (get(object, 'type') === 'user' && get(object, 'id') === user.id) {
-    if (['GET', 'DELETE', 'PATCH'].includes(operation)) {
-      return true
+  if (operation === 'DELETE') {
+    if (
+      get(object, 'path') ===
+      '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId'
+    ) {
+      return helpers.isHandlingEditor({ user, object })
+    }
+
+    if (get(object, 'type') === 'collection') {
+      return helpers.isOwner({ user, object })
     }
   }
+
   // If no individual permissions exist (above), fallback to unauthenticated
   // user's permission
   return unauthenticatedUser(operation, object)
 }
 
+async function applyEditorInChiefPolicy(user, operation, object, context) {
+  if (operation === 'GET') {
+    if (get(object, 'type') === 'collection') {
+      return {
+        filter: collection => ({
+          ...collection,
+          visibleStatus: get(
+            statuses,
+            `${collection.status}.editorInChief.label`,
+          ),
+        }),
+      }
+    }
+  }
+  return true
+}
+
 const authsomeMode = async (userId, operation, object, context) => {
   if (!userId) {
     return unauthenticatedUser(operation, object)
@@ -243,11 +283,12 @@ const authsomeMode = async (userId, operation, object, context) => {
   // authorization/authsome mode, e.g.
   const user = await context.models.User.find(userId)
 
-  // Admins and editor in chiefs can do anything
-  if (user && (user.admin || user.editorInChief)) return true
+  if (get(user, 'admin') || get(user, 'editorInChief')) {
+    return applyEditorInChiefPolicy(user, operation, object, context)
+  }
 
   if (user) {
-    return authenticatedUser(user, operation, object, context)
+    return applyAuthenticatedUserPolicy(user, operation, object, context)
   }
 
   return false
diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js
index 70c2472c57724a1ae1c711fda8b99e007c742d26..19ac346db1ac276c83197297b728bbcf89cb83cc 100644
--- a/packages/xpub-faraday/config/default.js
+++ b/packages/xpub-faraday/config/default.js
@@ -40,6 +40,7 @@ module.exports = {
     port: 3000,
     logger,
     uploads: 'uploads',
+    secret: 'SECRET',
   },
   'pubsweet-client': {
     API_ENDPOINT: '/api',
@@ -66,9 +67,15 @@ module.exports = {
   'invite-reset-password': {
     url: process.env.PUBSWEET_INVITE_PASSWORD_RESET_URL || '/invite',
   },
+  'forgot-password': {
+    url: process.env.PUBSWEET_FORGOT_PASSWORD_URL || '/forgot-password',
+  },
   'invite-reviewer': {
     url: process.env.PUBSWEET_INVITE_REVIEWER_URL || '/invite-reviewer',
   },
+  'confirm-signup': {
+    url: process.env.PUBSWEET_CONFIRM_SIGNUP_URL || '/confirm-signup',
+  },
   roles: {
     global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
     collection: ['handlingEditor', 'reviewer', 'author'],
@@ -91,48 +98,313 @@ module.exports = {
   ],
   statuses: {
     draft: {
-      public: 'Draft',
-      private: 'Draft',
+      importance: 1,
+      author: {
+        label: 'Complete Submission',
+        needsAttention: true,
+      },
+      admin: {
+        label: 'Complete Submission',
+        needsAttention: true,
+      },
+    },
+    technicalChecks: {
+      importance: 2,
+      author: {
+        label: 'Submitted',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'QA',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Approve QA',
+        needsAttention: true,
+      },
     },
     submitted: {
-      public: 'Submitted',
-      private: 'Submitted',
+      importance: 3,
+      author: {
+        label: 'Submitted',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Assign HE',
+        needsAttention: true,
+      },
+      admin: {
+        label: 'Assign HE',
+        needsAttention: true,
+      },
     },
     heInvited: {
-      public: 'Submitted',
-      private: 'Handling Editor Invited',
+      importance: 4,
+      author: {
+        label: 'HE Invited',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Respond to Invite',
+        needsAttention: true,
+      },
+      editorInChief: {
+        label: 'HE Invited',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Respond to Invite',
+        needsAttention: true,
+      },
     },
     heAssigned: {
-      public: 'Handling Editor Assigned',
-      private: 'Handling Editor Assigned',
+      importance: 5,
+      author: {
+        label: 'HE Assigned',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Invite Reviewers',
+        needsAttention: true,
+      },
+      editorInChief: {
+        label: 'HE Assigned',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Invite Reviewers',
+        needsAttention: true,
+      },
     },
     reviewersInvited: {
-      public: 'Reviewers Invited',
-      private: 'Reviewers Invited',
+      importance: 6,
+      author: {
+        label: 'Reviewers Invited',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Check Review Process',
+        needsAttention: true,
+      },
+      editorInChief: {
+        label: 'Reviewers Invited',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Respond to Invite',
+        needsAttention: true,
+      },
+      admin: {
+        label: 'Respond to Invite',
+        needsAttention: true,
+      },
     },
     underReview: {
-      public: 'Under Review',
-      private: 'Under Review',
+      importance: 7,
+      author: {
+        label: 'Under Review',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Check Review Process',
+        needsAttention: true,
+      },
+      editorInChief: {
+        label: 'Under Review',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Complete Review',
+        needsAttention: true,
+      },
+      admin: {
+        label: 'Complete Review',
+        needsAttention: true,
+      },
     },
     reviewCompleted: {
-      public: 'Under Review',
-      private: 'Review Completed',
-    },
-    pendingApproval: {
-      public: 'Under Review',
-      private: 'Pending Approval',
+      importance: 8,
+      author: {
+        label: 'Review Completed',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Make Recommendation',
+        needsAttention: true,
+      },
+      editorInChief: {
+        label: 'Review Completed',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Review Completed',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Make Recommendation',
+        needsAttention: true,
+      },
     },
     revisionRequested: {
-      public: 'Revision Requested',
-      private: 'Revision Requested',
+      importance: 9,
+      author: {
+        label: 'Submit Revision',
+        needsAttention: true,
+      },
+      handlingEditor: {
+        label: 'Revision Requested',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Revision Requested',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Revision Requested',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Submit Revision',
+        needsAttention: true,
+      },
+    },
+    pendingApproval: {
+      importance: 10,
+      author: {
+        label: 'Pending Approval',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Pending Approval',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Make Decision',
+        needsAttention: true,
+      },
+      reviewer: {
+        label: 'Pending Approval',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Make Decision',
+        needsAttention: true,
+      },
     },
     rejected: {
-      public: 'Rejected',
-      private: 'Rejected',
+      importance: 11,
+      author: {
+        label: 'Rejected',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Rejected',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Rejected',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Rejected',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Rejected',
+        needsAttention: false,
+      },
     },
-    published: {
-      public: 'Published',
-      private: 'Published',
+    inQA: {
+      importance: 12,
+      author: {
+        label: 'Pending approval',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'QA',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'QA',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'QA',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Approve QA',
+        needsAttention: true,
+      },
+    },
+    accepted: {
+      importance: 13,
+      author: {
+        label: 'Accepted',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Accepted',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Accepted',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Accepted',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Accepted',
+        needsAttention: false,
+      },
+    },
+    withdrawalRequested: {
+      importance: 14,
+      author: {
+        label: 'Withdrawal Requested',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Withdrawal Requested',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Approve Withdrawal',
+        needsAttention: true,
+      },
+      reviewer: {
+        label: 'Withdrawal Requested',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Approve Withdrawal',
+        needsAttention: true,
+      },
+    },
+    withdrawn: {
+      importance: 15,
+      author: {
+        label: 'Withdrawn',
+        needsAttention: false,
+      },
+      handlingEditor: {
+        label: 'Withdrawn',
+        needsAttention: false,
+      },
+      editorInChief: {
+        label: 'Withdrawn',
+        needsAttention: false,
+      },
+      reviewer: {
+        label: 'Withdrawn',
+        needsAttention: false,
+      },
+      admin: {
+        label: 'Withdrawn',
+        needsAttention: false,
+      },
     },
   },
   'manuscript-types': {
diff --git a/packages/xpub-faraday/config/test.js b/packages/xpub-faraday/config/test.js
new file mode 100644
index 0000000000000000000000000000000000000000..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f
--- /dev/null
+++ b/packages/xpub-faraday/config/test.js
@@ -0,0 +1,3 @@
+const defaultConfig = require('xpub-faraday/config/default')
+
+module.exports = defaultConfig
diff --git a/packages/xpub-faraday/config/upload-validations.js b/packages/xpub-faraday/config/upload-validations.js
index 21b2412ef38c1f8aea0cf3f3e98faf486a3b7107..79f682e7416efb192c78574e39b13600fd4d473d 100644
--- a/packages/xpub-faraday/config/upload-validations.js
+++ b/packages/xpub-faraday/config/upload-validations.js
@@ -16,5 +16,12 @@ module.exports = {
       'application/msword',
     ])
     .error(new Error('Only Word documents and PDFs are allowed')),
+  responseToReviewers: Joi.any()
+    .valid([
+      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      'application/pdf',
+      'application/msword',
+    ])
+    .error(new Error('Only Word documents and PDFs are allowed')),
   review: Joi.any(),
 }
diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js
index ec5a914c3268276d718abf8d87a78aeba7c7909d..fa6222be84d3181f4942373e7b06a7b6cb6f9b3d 100644
--- a/packages/xpub-faraday/config/validations.js
+++ b/packages/xpub-faraday/config/validations.js
@@ -7,16 +7,15 @@ module.exports = {
     created: Joi.date(),
     title: Joi.string(),
     status: Joi.string(),
-    reviewers: Joi.array(),
+    visibleStatus: Joi.string(),
     customId: Joi.string(),
-    authors: Joi.array(),
     invitations: Joi.array(),
     handlingEditor: Joi.object(),
-    visibleStatus: Joi.string().allow(''),
   },
   fragment: [
     {
       fragmentType: Joi.valid('version').required(),
+      collectionId: Joi.string().required(),
       created: Joi.date(),
       version: Joi.number(),
       submitted: Joi.date(),
@@ -65,6 +64,16 @@ module.exports = {
             signedUrl: Joi.string(),
           }),
         ),
+        responseToReviewers: Joi.array().items(
+          Joi.object({
+            id: Joi.string(),
+            name: Joi.string().required(),
+            type: Joi.string(),
+            size: Joi.number(),
+            url: Joi.string(),
+            signedUrl: Joi.string(),
+          }),
+        ),
       }),
       notes: Joi.object({
         fundingAcknowledgement: Joi.string(),
@@ -73,21 +82,8 @@ module.exports = {
       reviewers: Joi.array(),
       lock: Joi.object(),
       decision: Joi.object(),
-      authors: Joi.array().items(
-        Joi.object({
-          firstName: Joi.string().required(),
-          lastName: Joi.string().required(),
-          middleName: Joi.string().allow(''),
-          email: Joi.string()
-            .email()
-            .required(),
-          affiliation: Joi.string().required(),
-          country: Joi.string().allow(''),
-          isSubmitting: Joi.boolean(),
-          isCorresponding: Joi.boolean(),
-          id: Joi.string().uuid(),
-        }),
-      ),
+      authors: Joi.array(),
+      invitations: Joi.array(),
       recommendations: Joi.array().items(
         Joi.object({
           id: Joi.string().required(),
@@ -129,6 +125,8 @@ module.exports = {
     editorInChief: Joi.boolean(),
     handlingEditor: Joi.boolean(),
     invitationToken: Joi.string().allow(''),
+    confirmationToken: Joi.string().allow(''),
+    agreeTC: Joi.boolean(),
   },
   team: {
     group: Joi.string(),
diff --git a/packages/xpub-faraday/package.json b/packages/xpub-faraday/package.json
index ff555499ca38cc5796e800ee9956bdc6679b190b..e4e829deab3a05be7c02ee7c4bd237ed68d18ca0 100644
--- a/packages/xpub-faraday/package.json
+++ b/packages/xpub-faraday/package.json
@@ -8,8 +8,9 @@
     "url": "https://gitlab.coko.foundation/xpub/xpub-faraday"
   },
   "dependencies": {
-    "@pubsweet/ui": "^3.2.0",
-    "@pubsweet/component-aws-s3": "^1.0.4",
+    "@pubsweet/ui": "4.1.3",
+    "@pubsweet/ui-toolkit": "latest",
+    "@pubsweet/component-aws-s3": "^1.1.2",
     "aws-sdk": "^2.197.0",
     "babel-core": "^6.26.0",
     "config": "^1.26.2",
@@ -35,7 +36,7 @@
     "react-router-dom": "^4.2.2",
     "recompose": "^0.26.0",
     "redux": "^3.6.0",
-    "redux-form": "^7.0.3",
+    "redux-form": "7.0.3",
     "redux-logger": "^3.0.1",
     "typeface-noto-sans": "^0.0.54",
     "typeface-noto-serif": "^0.0.54",
@@ -62,6 +63,7 @@
     "file-loader": "^1.1.5",
     "html-webpack-plugin": "^2.24.0",
     "joi-browser": "^10.0.6",
+    "jest": "^22.1.1",
     "react-hot-loader": "^3.1.1",
     "string-replace-loader": "^1.3.0",
     "style-loader": "^0.19.0",
@@ -70,13 +72,20 @@
     "webpack-dev-middleware": "^1.12.0",
     "webpack-hot-middleware": "^2.20.0"
   },
+  "jest": {
+    "verbose": true,
+    "testRegex": "/tests/.*.test.js$"
+  },
   "scripts": {
     "setupdb": "pubsweet setupdb ./",
     "start": "pubsweet start",
     "start:services": "docker-compose up postgres",
     "server": "pubsweet server",
-    "start-now": "echo $secret > config/local-development.json && npm run server",
+    "start-now":
+      "echo $secret > config/local-development.json && npm run server",
     "build": "NODE_ENV=production pubsweet build",
-    "clean": "rm -rf node_modules"
+    "clean": "rm -rf node_modules",
+    "debug": "pgrep -f startup/start.js | xargs kill -sigusr1",
+    "test": "jest"
   }
 }
diff --git a/packages/xpub-faraday/static/favicon.ico b/packages/xpub-faraday/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..8a6bcd88d0d0461a41667716bc4f4390b4ec61a3
--- /dev/null
+++ b/packages/xpub-faraday/static/favicon.ico
@@ -0,0 +1 @@
+AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAgCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8CAAAAAAAAAAAAAAAAAAAAAP///wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJR0DzCAYwSphGID6ZFmANZ+bhR9IqCHhCadf9QloH7aKqKAnTCdfyoAAAAAAAAAAAAAAAAAAAAAAAAAAHleCFxkSwL/WkQB/14+AMhuZxnqP6Fu/ySpif8fpYj/JKaG/yy1jv8ion/6LJx8YgAAAAAAAAAAAAAAAHdeCVFiSgH/TzsC21U6BTAMv8wUH6GG2iKniP8xoXj/V4Q9/VqDTnwgoIV8KJt//iuviv8rmXxvAAAAAJV7ER1cQwH1TTsC41hCFhcAAAAAKJt8wymtiP8gmoPnZH01jphnAP+QZADY/wAAASuegTUpo3/kK66J/y2ffUN0WgObW0QB/08+DD0AAAAALZ1/VCqqhv8onH/+Lp9/SAAAAACEZgSxm3cA/4hqBYgAAAAANJZ4IiahePUon37Qb1oC8E03As8AAAAAAAAAACmcfKYrsYv/KZt6swAAAAAAAAAAiWkHaph1AP+GZwLFAAAAAAAAAAAbZ1G5JZl4/4RlAvlmTQLCAAAAAAAAAAAqnHywLLSO/yudfYAAAAAAAAAAAIVnBqiXdAD/hmkEtgAAAAAAAAAAFVE+1Rl/XviHaQSxkG4B/4FoDD0AAAAAK6CAaSyyjP8pnXzLAAAAAI5oB0KLZAH9k3EA/4ppCF4AAAAAG1dEUhllTP8jgmWmjGwMKI5qAvmHbQH6i2oGVAAAAAAcooi7H6+T/0mNVqaNYwDllHAA/4lnAsMAAAAAHV5NKxZSP/EVZk32NKWHIgAAAACEaQlNjm0C/5NxAf+LZACdcHsqeFqDPfmHcQv/kmkA/4hlAOJtVw4jE1JFTRZWQe8bbFP/KIhsUgAAAAAAAAAAAAAAAIVpC0GIawPkm3cA/5VuAP+TZgD/k2oA/317HP8shWP8EGlZ6BtsUv8ccFf8KopuWgAAAAAAAAAAAAAAAAAAAAAAAAAAhmsaE41tB4CKawLFimsCxZFkAnc0mXJiI6WKvCqffNMpnHuZN7SPKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAPvfAADgBwAAwAMAAIABAAAIAAAAEIgAADGMAAAxjAAAEQgAAAgQAACAAQAAwAMAAOAHAAD//wAA//8AAA==
\ No newline at end of file
diff --git a/packages/xpub-faraday/tests/authsome-helpers.test.js b/packages/xpub-faraday/tests/authsome-helpers.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ccc6e04217a55b9626a460e091fd0bf6f155850
--- /dev/null
+++ b/packages/xpub-faraday/tests/authsome-helpers.test.js
@@ -0,0 +1,164 @@
+const { cloneDeep, get } = require('lodash')
+const fixturesService = require('pubsweet-component-fixture-service')
+const ah = require('../config/authsome-helpers')
+
+describe('Authsome Helpers', () => {
+  let testFixtures = {}
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixturesService.fixtures)
+  })
+  it('stripeCollection - should return collection', () => {
+    const { collection } = testFixtures.collections
+    const result = ah.stripeCollectionByRole(collection)
+    expect(result).toBeTruthy()
+  })
+  it('stripeFragment - should return fragment', () => {
+    const { fragment } = testFixtures.fragments
+    const result = ah.stripeFragmentByRole({ fragment })
+    expect(result).toBeTruthy()
+  })
+
+  it('stripeCollection - author should not see accepted HE name before recommendation made', () => {
+    const { collection } = testFixtures.collections
+    collection.status = 'underReview'
+    collection.handlingEditor = {
+      ...collection.handlingEditor,
+      isAccepted: true,
+    }
+
+    const result = ah.stripeCollectionByRole(collection, 'author')
+    const { handlingEditor = {} } = result
+
+    expect(handlingEditor.email).toBeFalsy()
+    expect(handlingEditor.name).toEqual('Assigned')
+  })
+
+  it('stripeCollection - author should not see Assigned until HE accepted ', () => {
+    const { collection } = testFixtures.collections
+    collection.status = 'underReview'
+    collection.handlingEditor = {
+      ...collection.handlingEditor,
+      isAccepted: false,
+    }
+
+    const result = ah.stripeCollectionByRole(collection, 'author')
+    const { handlingEditor = {} } = result
+
+    expect(handlingEditor).toBeFalsy()
+    expect(handlingEditor.name).not.toEqual('Assigned')
+  })
+
+  it('stripeCollection - author should see HE name after recommendation made', () => {
+    const { collection } = testFixtures.collections
+    collection.status = 'revisionRequested'
+
+    const result = ah.stripeCollectionByRole(collection, 'author')
+    const { handlingEditor = {} } = result
+
+    expect(handlingEditor.name).not.toEqual('Assigned')
+  })
+
+  it('stripeCollection - other user than author should see HE name before recommendation made', () => {
+    const { collection } = testFixtures.collections
+    collection.status = 'underReview'
+
+    const result = ah.stripeCollectionByRole(collection, 'admin')
+    const { handlingEditor = {} } = result
+
+    expect(handlingEditor.name).not.toEqual('Assigned')
+  })
+
+  it('stripeCollection - other user than author should see HE name after recommendation made', () => {
+    const { collection } = testFixtures.collections
+    collection.status = 'revisionRequested'
+
+    const result = ah.stripeCollectionByRole(collection, 'admin')
+    const { handlingEditor = {} } = result
+
+    expect(handlingEditor.name).not.toEqual('Assigned')
+  })
+
+  it('stripeCollection - returns if collection does not have HE', () => {
+    const { collection } = testFixtures.collections
+    delete collection.handlingEditor
+
+    const result = ah.stripeCollectionByRole(collection, 'admin')
+    expect(result.handlingEditor).toBeFalsy()
+  })
+
+  it('stripeFragment - reviewer should not see authors email', () => {
+    const { fragment } = testFixtures.fragments
+    const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' })
+    const { authors = [] } = result
+    expect(authors[0].email).toBeFalsy()
+  })
+  it('stripeFragment - other roles than reviewer should see authors emails', () => {
+    const { fragment } = testFixtures.fragments
+    const result = ah.stripeFragmentByRole({ fragment, role: 'author' })
+    const { authors = [] } = result
+
+    expect(authors[0].email).toBeTruthy()
+  })
+
+  it('stripeFragment - reviewer should not see cover letter', () => {
+    const { fragment } = testFixtures.fragments
+    const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' })
+    const { files = {} } = result
+    expect(files.coverLetter).toBeFalsy()
+  })
+  it('stripeFragment - reviewer should not see others reviews', () => {
+    const { fragment } = testFixtures.fragments
+    const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' })
+    const { recommendations } = result
+    expect(recommendations).toEqual([])
+  })
+
+  it('stripeFragment - author should not see recommendations if a decision has not been made', () => {
+    const { fragment } = testFixtures.fragments
+    fragment.recommendations = [
+      {
+        comments: [
+          {
+            content: 'private',
+            public: false,
+          },
+          {
+            content: 'public',
+            public: true,
+          },
+        ],
+      },
+    ]
+    const { recommendations } = ah.stripeFragmentByRole({
+      fragment,
+      role: 'author',
+      status: 'underReview',
+      isLast: true,
+    })
+    expect(recommendations).toHaveLength(0)
+  })
+  it('stripeFragment - author should see reviews only if recommendation has been made and only public ones', () => {
+    const { fragment } = testFixtures.fragments
+    fragment.recommendations = [
+      {
+        comments: [
+          {
+            content: 'private',
+            public: false,
+          },
+          {
+            content: 'public',
+            public: true,
+          },
+        ],
+      },
+    ]
+    const result = ah.stripeFragmentByRole({
+      fragment,
+      role: 'author',
+      status: 'revisionRequested',
+    })
+    const publicComments = get(result, 'recommendations[0].comments')
+    expect(publicComments).toHaveLength(1)
+  })
+})
diff --git a/yarn.lock b/yarn.lock
index 50f540d304096e6d8380f9142adddc0dd83b7c4c..efc87c5ed07ef412dee8ed8bb153ba68180c55ab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -85,9 +85,9 @@
     lodash "^4.2.0"
     to-fast-properties "^2.0.0"
 
-"@pubsweet/component-aws-s3@^1.0.4":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@pubsweet/component-aws-s3/-/component-aws-s3-1.1.0.tgz#115c4f801bef17a214488de6bf586fe3800b1c11"
+"@pubsweet/component-aws-s3@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@pubsweet/component-aws-s3/-/component-aws-s3-1.1.2.tgz#ef7c6c7f22a19ce6f547412b73ab8de3fc81c3ee"
   dependencies:
     archiver "^2.1.1"
     aws-sdk "^2.185.0"
@@ -164,6 +164,14 @@
     typeface-fira-sans-condensed "^0.0.43"
     typeface-vollkorn "^0.0.43"
 
+"@pubsweet/ui-toolkit@latest":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@pubsweet/ui-toolkit/-/ui-toolkit-1.0.0.tgz#df05b54e7bbfabcb10c7afc2991752e1087d2298"
+  dependencies:
+    color "^3.0.0"
+    lodash "^4.17.4"
+    styled-components "^3.2.5"
+
 "@pubsweet/ui@3.0.0":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@pubsweet/ui/-/ui-3.0.0.tgz#b8915ce2b2729e66fd5628ecf7855f1d740270a5"
@@ -186,6 +194,28 @@
     redux-form "^7.0.3"
     styled-components "^2.4.0"
 
+"@pubsweet/ui@4.1.3":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@pubsweet/ui/-/ui-4.1.3.tgz#a8c65aa69618505a1e3777f4d18c3d676b800ee9"
+  dependencies:
+    babel-jest "^21.2.0"
+    classnames "^2.2.5"
+    enzyme "^3.2.0"
+    enzyme-adapter-react-16 "^1.1.1"
+    invariant "^2.2.3"
+    lodash "^4.17.4"
+    prop-types "^15.5.10"
+    react "^16.2.0"
+    react-dom "^16.2.0"
+    react-feather "^1.0.8"
+    react-redux "^5.0.2"
+    react-router-dom "^4.2.2"
+    react-tag-autocomplete "^5.5.0"
+    recompose "^0.26.0"
+    redux "^3.6.0"
+    redux-form "^7.0.3"
+    styled-components "^3.2.5"
+
 "@pubsweet/ui@^3.1.0":
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/@pubsweet/ui/-/ui-3.1.0.tgz#24c25c29fc36e34b9f654fe4378502232f8204fa"
@@ -2145,7 +2175,7 @@ collapse-white-space@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c"
 
-color-convert@^1.3.0, color-convert@^1.9.0:
+color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.1:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
   dependencies:
@@ -2161,6 +2191,13 @@ color-string@^0.3.0:
   dependencies:
     color-name "^1.0.0"
 
+color-string@^1.5.2:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9"
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
 color@^0.11.0:
   version "0.11.4"
   resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
@@ -2169,6 +2206,13 @@ color@^0.11.0:
     color-convert "^1.3.0"
     color-string "^0.3.0"
 
+color@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
+  dependencies:
+    color-convert "^1.9.1"
+    color-string "^1.5.2"
+
 colormin@^1.0.5:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
@@ -3281,7 +3325,7 @@ es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
     es6-iterator "~2.0.3"
     es6-symbol "~3.1.1"
 
-es6-error@^4.1.1:
+es6-error@^4.0.0, es6-error@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
 
@@ -4468,6 +4512,10 @@ hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.3.0, hoist-non-react-
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
 
+hoist-non-react-statics@^2.2.1:
+  version "2.5.4"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.4.tgz#fc3b1ac05d2ae3abedec84eba846511b0d4fcc4f"
+
 home-or-tmp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -4746,6 +4794,10 @@ is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
 
+is-arrayish@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd"
+
 is-binary-path@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@@ -8263,6 +8315,19 @@ reduce-function-call@^1.0.1:
   dependencies:
     balanced-match "^0.4.2"
 
+redux-form@7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.0.3.tgz#80157d01df7de6c8eb2297ad1fbbb092bafa34f5"
+  dependencies:
+    deep-equal "^1.0.1"
+    es6-error "^4.0.0"
+    hoist-non-react-statics "^2.2.1"
+    invariant "^2.2.2"
+    is-promise "^2.1.0"
+    lodash "^4.17.3"
+    lodash-es "^4.17.3"
+    prop-types "^15.5.9"
+
 redux-form@^7.0.3:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.2.3.tgz#a01111116f386f3d88451b5528dfbb180561a8b4"
@@ -8793,6 +8858,12 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  dependencies:
+    is-arrayish "^0.3.1"
+
 slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"