diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6c3dbae7..945f8788 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -51,6 +51,7 @@ jobs:
       DEPLOY_ENV: "DEV"
       LOGICAL_ENV: "dev"
       APPNAME: "challenge-api"
+      CODEARTIFACT_ENV: "PROD"
     steps: *builddeploy_steps
 
   "build-qa":
@@ -80,7 +81,8 @@ workflows:
         filters:
           branches:
             only:
-            - dev
+            - refactor/domain-challenge-dev
+            - refactor/challenge-update
 
     - "build-qa":
         context: org-global
diff --git a/app-routes.js b/app-routes.js
index fdb7db14..3632e13e 100644
--- a/app-routes.js
+++ b/app-routes.js
@@ -50,6 +50,7 @@ module.exports = (app) => {
               next(new errors.ForbiddenError("You are not allowed to perform this action!"));
             } else {
               req.authUser.handle = config.M2M_AUDIT_HANDLE;
+              req.authUser.userId = config.M2M_AUDIT_USERID;
               req.userToken = req.headers.authorization.split(" ")[1];
               next();
             }
diff --git a/config/default.js b/config/default.js
index c4867fdb..3c4f0045 100644
--- a/config/default.js
+++ b/config/default.js
@@ -42,7 +42,9 @@ module.exports = {
     // above AWS_REGION is used if we use AWS ES
     HOST: process.env.ES_HOST || "localhost:9200",
     API_VERSION: process.env.ES_API_VERSION || "6.8",
+    OPENSEARCH: process.env.OPENSEARCH || "false",
     ES_INDEX: process.env.ES_INDEX || "challenge",
+    ES_TYPE: process.env.ES_TYPE || "_doc",
     ES_REFRESH: process.env.ES_REFRESH || "true",
     TEMP_REINDEXING: process.env.TEMP_REINDEXING || true, // if true, it won't delete the existing index when reindexing data
   },
@@ -95,6 +97,7 @@ module.exports = {
   DEFAULT_CONFIDENTIALITY_TYPE: process.env.DEFAULT_CONFIDENTIALITY_TYPE || "public",
 
   M2M_AUDIT_HANDLE: process.env.M2M_AUDIT_HANDLE || "tcwebservice",
+  M2M_AUDIT_USERID: process.env.M2M_AUDIT_USERID || 22838965,
 
   FORUM_TITLE_LENGTH_LIMIT: process.env.FORUM_TITLE_LENGTH_LIMIT || 90,
 
diff --git a/package.json b/package.json
index 831428c8..0f56204a 100644
--- a/package.json
+++ b/package.json
@@ -35,14 +35,15 @@
     "chai-http": "^4.2.1",
     "mocha": "^6.1.4",
     "mocha-prepare": "^0.1.0",
+    "nodemon": "^2.0.20",
     "nyc": "^14.0.0",
-    "prettier": "^2.8.1",
-    "nodemon": "^2.0.20"
+    "prettier": "^2.8.1"
   },
   "dependencies": {
+    "@grpc/grpc-js": "^1.8.12",
     "@opensearch-project/opensearch": "^2.2.0",
-    "@topcoder-framework/domain-challenge": "^0.7.0",
-    "@topcoder-framework/lib-common": "^0.7.0",
+    "@topcoder-framework/domain-challenge": "^0.10.13",
+    "@topcoder-framework/lib-common": "^0.10.13",
     "aws-sdk": "^2.1145.0",
     "axios": "^0.19.0",
     "axios-retry": "^3.4.0",
@@ -50,12 +51,15 @@
     "body-parser": "^1.15.1",
     "config": "^3.0.1",
     "cors": "^2.7.1",
+    "deep-equal": "^2.2.0",
     "dotenv": "^8.2.0",
     "dynamoose": "^1.11.1",
+    "elasticsearch": "^16.7.3",
     "express": "^4.15.4",
     "express-fileupload": "^1.1.6",
     "express-interceptor": "^1.2.0",
     "get-parameter-names": "^0.3.0",
+    "http-aws-es": "^6.0.0",
     "http-status-codes": "^1.3.0",
     "joi": "^14.0.0",
     "jsonwebtoken": "^8.3.0",
diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js
index 52a5c346..b6a3db22 100644
--- a/src/common/challenge-helper.js
+++ b/src/common/challenge-helper.js
@@ -5,9 +5,12 @@ const HttpStatus = require("http-status-codes");
 const _ = require("lodash");
 const errors = require("./errors");
 const config = require("config");
+const helper = require("./helper");
+const constants = require("../../app-constants");
 const axios = require("axios");
 const { getM2MToken } = require("./m2m-helper");
 const { hasAdminRole } = require("./role-helper");
+const { ensureAcessibilityToModifiedGroups } = require("./group-helper");
 
 class ChallengeHelper {
   /**
@@ -43,7 +46,7 @@ class ChallengeHelper {
    * @param {String} projectId the project id
    * @param {String} currentUser the user
    */
-  async ensureProjectExist(projectId, currentUser) {
+  static async ensureProjectExist(projectId, currentUser) {
     let token = await getM2MToken();
     const url = `${config.PROJECTS_API_URL}/${projectId}`;
     try {
@@ -75,6 +78,273 @@ class ChallengeHelper {
       }
     }
   }
+
+  async validateCreateChallengeRequest(currentUser, challenge) {
+    // projectId is required for non self-service challenges
+    if (challenge.legacy.selfService == null && challenge.projectId == null) {
+      throw new errors.BadRequestError("projectId is required for non self-service challenges.");
+    }
+
+    if (challenge.status === constants.challengeStatuses.Active) {
+      throw new errors.BadRequestError(
+        "You cannot create an Active challenge. Please create a Draft challenge and then change the status to Active."
+      );
+    }
+
+    helper.ensureNoDuplicateOrNullElements(challenge.tags, "tags");
+    helper.ensureNoDuplicateOrNullElements(challenge.groups, "groups");
+    // helper.ensureNoDuplicateOrNullElements(challenge.terms, 'terms')
+    // helper.ensureNoDuplicateOrNullElements(challenge.events, 'events')
+
+    // check groups authorization
+    await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
+  }
+
+  async validateChallengeUpdateRequest(currentUser, challenge, data) {
+    if (process.env.LOCAL != "true") {
+      await helper.ensureUserCanModifyChallenge(currentUser, challenge);
+    }
+
+    helper.ensureNoDuplicateOrNullElements(data.tags, "tags");
+    helper.ensureNoDuplicateOrNullElements(data.groups, "groups");
+
+    if (data.projectId) {
+      await ChallengeHelper.ensureProjectExist(data.projectId, currentUser);
+    }
+
+    // check groups access to be updated group values
+    if (data.groups) {
+      await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
+    }
+
+    // Ensure descriptionFormat is either 'markdown' or 'html'
+    if (data.descriptionFormat && !_.includes(["markdown", "html"], data.descriptionFormat)) {
+      throw new errors.BadRequestError("The property 'descriptionFormat' must be either 'markdown' or 'html'");
+    }
+
+    // Ensure unchangeable fields are not changed
+    if (
+      _.get(challenge, "legacy.track") &&
+      _.get(data, "legacy.track") &&
+      _.get(challenge, "legacy.track") !== _.get(data, "legacy.track")
+    ) {
+      throw new errors.ForbiddenError("Cannot change legacy.track");
+    }
+
+    if (
+      _.get(challenge, "trackId") &&
+      _.get(data, "trackId") &&
+      _.get(challenge, "trackId") !== _.get(data, "trackId")
+    ) {
+      throw new errors.ForbiddenError("Cannot change trackId");
+    }
+
+    if (
+      _.get(challenge, "typeId") &&
+      _.get(data, "typeId") &&
+      _.get(challenge, "typeId") !== _.get(data, "typeId")
+    ) {
+      throw new errors.ForbiddenError("Cannot change typeId");
+    }
+
+    if (
+      _.get(challenge, "legacy.pureV5Task") &&
+      _.get(data, "legacy.pureV5Task") &&
+      _.get(challenge, "legacy.pureV5Task") !== _.get(data, "legacy.pureV5Task")
+    ) {
+      throw new errors.ForbiddenError("Cannot change legacy.pureV5Task");
+    }
+
+    if (
+      _.get(challenge, "legacy.pureV5") &&
+      _.get(data, "legacy.pureV5") &&
+      _.get(challenge, "legacy.pureV5") !== _.get(data, "legacy.pureV5")
+    ) {
+      throw new errors.ForbiddenError("Cannot change legacy.pureV5");
+    }
+
+    if (
+      _.get(challenge, "legacy.selfService") &&
+      _.get(data, "legacy.selfService") &&
+      _.get(challenge, "legacy.selfService") !== _.get(data, "legacy.selfService")
+    ) {
+      throw new errors.ForbiddenError("Cannot change legacy.selfService");
+    }
+
+    if (
+      (challenge.status === constants.challengeStatuses.Completed ||
+        challenge.status === constants.challengeStatuses.Cancelled) &&
+      data.status &&
+      data.status !== challenge.status &&
+      data.status !== constants.challengeStatuses.CancelledClientRequest
+    ) {
+      throw new errors.BadRequestError(
+        `Cannot change ${challenge.status} challenge status to ${data.status} status`
+      );
+    }
+
+    if (
+      data.winners &&
+      data.winners.length > 0 &&
+      challenge.status !== constants.challengeStatuses.Completed &&
+      data.status !== constants.challengeStatuses.Completed
+    ) {
+      throw new errors.BadRequestError(
+        `Cannot set winners for challenge with non-completed ${challenge.status} status`
+      );
+    }
+  }
+
+  sanitizeRepeatedFieldsInUpdateRequest(data) {
+    if (data.winners != null) {
+      data.winnerUpdate = {
+        winners: data.winners,
+      };
+      delete data.winners;
+    }
+
+    if (data.discussions != null) {
+      data.discussionUpdate = {
+        discussions: data.discussions,
+      };
+      delete data.discussions;
+    }
+
+    if (data.metadata != null) {
+      data.metadataUpdate = {
+        metadata: data.metadata,
+      };
+      delete data.metadata;
+    }
+
+    if (data.phases != null) {
+      data.phaseUpdate = {
+        phases: data.phases,
+      };
+      delete data.phases;
+    }
+
+    if (data.events != null) {
+      data.eventUpdate = {
+        events: data.events,
+      };
+      delete data.events;
+    }
+
+    if (data.terms != null) {
+      data.termUpdate = {
+        terms: data.terms,
+      };
+      delete data.terms;
+    }
+
+    if (data.prizeSets != null) {
+      data.prizeSetUpdate = {
+        prizeSets: data.prizeSets,
+      };
+      delete data.prizeSets;
+    }
+
+    if (data.tags != null) {
+      data.tagUpdate = {
+        tags: data.tags,
+      };
+      delete data.tags;
+    }
+
+    if (data.attachments != null) {
+      data.attachmentUpdate = {
+        attachments: data.attachments,
+      };
+      delete data.attachments;
+    }
+
+    if (data.groups != null) {
+      data.groupUpdate = {
+        groups: data.groups,
+      };
+      delete data.groups;
+    }
+
+    return data;
+  }
+
+  enrichChallengeForResponse(challenge, track, type) {
+    if (challenge.phases && challenge.phases.length > 0) {
+      const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration");
+      const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission");
+
+      challenge.currentPhase = challenge.phases
+        .slice()
+        .reverse()
+        .find((phase) => phase.isOpen);
+
+      challenge.currentPhaseNames = _.map(
+        _.filter(challenge.phases, (p) => p.isOpen === true),
+        "name"
+      );
+
+      if (registrationPhase) {
+        challenge.registrationStartDate =
+          registrationPhase.actualStartDate || registrationPhase.scheduledStartDate;
+        challenge.registrationEndDate =
+          registrationPhase.actualEndDate || registrationPhase.scheduledEndDate;
+      }
+      if (submissionPhase) {
+        challenge.submissionStartDate =
+          submissionPhase.actualStartDate || submissionPhase.scheduledStartDate;
+
+        challenge.submissionEndDate =
+          submissionPhase.actualEndDate || submissionPhase.scheduledEndDate;
+      }
+    }
+
+    challenge.created = new Date(challenge.created).toISOString();
+    challenge.updated = new Date(challenge.updated).toISOString();
+    challenge.startDate = new Date(challenge.startDate).toISOString();
+    challenge.endDate = new Date(challenge.endDate).toISOString();
+
+    if (track) {
+      challenge.track = track.name;
+    }
+
+    if (type) {
+      challenge.type = type.name;
+    }
+
+    challenge.metadata = challenge.metadata.map((m) => {
+      try {
+        m.value = JSON.stringify(JSON.parse(m.value)); // when we update how we index data, make this a JSON field
+      } catch (err) {
+        // do nothing
+      }
+      return m;
+    });
+  }
+
+  convertPrizeSetValuesToCents(prizeSets) {
+    prizeSets.forEach((prizeSet) => {
+      prizeSet.prizes.forEach((prize) => {
+        prize.amountInCents = prize.value * 100;
+        delete prize.value;
+      });
+    });
+  }
+
+  convertPrizeSetValuesToDollars(prizeSets, overview) {
+    prizeSets.forEach((prizeSet) => {
+      prizeSet.prizes.forEach((prize) => {
+        if (prize.amountInCents != null) {
+          prize.value = prize.amountInCents / 100;
+          delete prize.amountInCents;
+        }
+      });
+    });
+    if (overview && overview.totalPrizesInCents) {
+      overview.totalPrizes = overview.totalPrizesInCents / 100;
+      delete overview.totalPrizesInCents;
+    }
+  }
 }
 
 module.exports = new ChallengeHelper();
diff --git a/src/common/group-helper.js b/src/common/group-helper.js
new file mode 100644
index 00000000..7ef2911d
--- /dev/null
+++ b/src/common/group-helper.js
@@ -0,0 +1,36 @@
+const _ = require("lodash");
+const errors = require("./errors");
+const helper = require("./helper");
+
+const { hasAdminRole } = require("./role-helper");
+
+class GroupHelper {
+  /**
+   * Ensure the user can access the groups being updated to
+   * @param {Object} currentUser the user who perform operation
+   * @param {Object} data the challenge data to be updated
+   * @param {String} challenge the original challenge data
+   */
+  async ensureAcessibilityToModifiedGroups(currentUser, data, challenge) {
+    const needToCheckForGroupAccess = !currentUser
+      ? true
+      : !currentUser.isMachine && !hasAdminRole(currentUser);
+    if (!needToCheckForGroupAccess) {
+      return;
+    }
+    const userGroups = await helper.getUserGroups(currentUser.userId);
+    const userGroupsIds = _.map(userGroups, (group) => group.id);
+    const updatedGroups = _.difference(
+      _.union(challenge.groups, data.groups),
+      _.intersection(challenge.groups, data.groups)
+    );
+    const filtered = updatedGroups.filter((g) => !userGroupsIds.includes(g));
+    if (filtered.length > 0) {
+      throw new errors.ForbiddenError(
+        "ensureAcessibilityToModifiedGroups :: You don't have access to this group!"
+      );
+    }
+  }
+}
+
+module.exports = new GroupHelper();
diff --git a/src/common/helper.js b/src/common/helper.js
index 00cb3467..e5228f09 100644
--- a/src/common/helper.js
+++ b/src/common/helper.js
@@ -19,6 +19,7 @@ const xss = require("xss");
 const logger = require("./logger");
 
 const { Client: ESClient } = require("@opensearch-project/opensearch");
+const elasticsearch = require("elasticsearch");
 
 const projectHelper = require("./project-helper");
 const m2mHelper = require("./m2m-helper");
@@ -449,16 +450,19 @@ axiosRetry(axios, {
  * @param {String} token The token
  * @returns
  */
-async function createSelfServiceProject(name, description, type, token) {
+async function createSelfServiceProject(name, description, type) {
   const projectObj = {
     name,
     description,
     type,
   };
+
+  const token = await m2mHelper.getM2MToken();
   const url = `${config.PROJECTS_API_URL}`;
   const res = await axios.post(url, projectObj, {
     headers: { Authorization: `Bearer ${token}` },
   });
+
   return _.get(res, "data.id");
 }
 
@@ -838,12 +842,31 @@ function getESClient() {
   }
   const esHost = config.get("ES.HOST");
 
-  esClient = new ESClient({
-    node: esHost,
-    ssl: {
-      rejectUnauthorized: false,
-    },
-  });
+  if (config.get("ES.OPENSEARCH") == "false") {
+    if (/.*amazonaws.*/.test(esHost)) {
+      esClient = elasticsearch.Client({
+        apiVersion: config.get("ES.API_VERSION"),
+        hosts: esHost,
+        connectionClass: require("http-aws-es"), // eslint-disable-line global-require
+        amazonES: {
+          region: config.get("AMAZON.AWS_REGION"),
+          credentials: new AWS.EnvironmentCredentials("AWS"),
+        },
+      });
+    } else {
+      esClient = new elasticsearch.Client({
+        apiVersion: config.get("ES.API_VERSION"),
+        hosts: esHost,
+      });
+    }
+  } else {
+    esClient = new ESClient({
+      node: esHost,
+      ssl: {
+        rejectUnauthorized: false,
+      },
+    });
+  }
 
   return esClient;
 }
@@ -922,7 +945,7 @@ async function listChallengesByMember(memberId) {
  * @returns {Promise<Array>} an array of resources.
  */
 async function listResourcesByMemberAndChallenge(memberId, challengeId) {
-  const token = await getM2MToken();
+  const token = await m2mHelper.getM2MToken();
   let response = {};
   try {
     response = await axios.get(config.RESOURCES_API_URL, {
@@ -1103,7 +1126,7 @@ async function ensureUserCanViewChallenge(currentUser, challenge) {
  *
  * @param {Object} currentUser the user who perform operation
  * @param {Object} challenge the challenge to check
- * @returns {undefined}
+ * @returns {Promise}
  */
 async function ensureUserCanModifyChallenge(currentUser, challenge) {
   // check groups authorization
diff --git a/src/common/m2m-helper.js b/src/common/m2m-helper.js
index 6620ded6..62c51678 100644
--- a/src/common/m2m-helper.js
+++ b/src/common/m2m-helper.js
@@ -3,15 +3,17 @@ const config = require("config");
 const m2mAuth = require("tc-core-library-js").auth.m2m;
 
 class M2MHelper {
+  static m2m = null;
+
   constructor() {
-    this.m2m = m2mAuth(_.pick(config, ["AUTH0_URL", "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME"]));
+    M2MHelper.m2m = m2mAuth(_.pick(config, ["AUTH0_URL", "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME"]));
   }
   /**
    * Get M2M token.
    * @returns {Promise<String>} the M2M token
    */
-  async getM2MToken() {
-    return this.m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET);
+  getM2MToken() {
+    return M2MHelper.m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET);
   }
 }
 
diff --git a/src/common/phase-helper.js b/src/common/phase-helper.js
index b3eb6923..1ffb99a7 100644
--- a/src/common/phase-helper.js
+++ b/src/common/phase-helper.js
@@ -16,11 +16,6 @@ const errors = require("./errors");
 const phaseService = require("../services/PhaseService");
 const timelineTemplateService = require("../services/TimelineTemplateService");
 
-// const timelineTemplateDomain = new TimelineTemplateDomain(
-//   GRPC_CHALLENGE_SERVER_HOST,
-//   GRPC_CHALLENGE_SERVER_PORT
-// );
-
 const phaseDomain = new PhaseDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT);
 
 class ChallengePhaseHelper {
@@ -163,8 +158,133 @@ class ChallengePhaseHelper {
         p.constraints = [];
       }
     }
+  }
+
+  async populatePhasesForChallengeCreation(phases, startDate, timelineTemplateId) {
+    if (_.isUndefined(timelineTemplateId)) {
+      throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`);
+    }
+    const { timelineTempate } = await this.getTemplateAndTemplateMap(timelineTemplateId);
+    const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap();
+    const finalPhases = _.map(timelineTempate, (phaseFromTemplate) => {
+      const phaseDefinition = phaseDefinitionMap.get(phaseFromTemplate.phaseId);
+      const phaseFromInput = _.find(phases, (p) => p.phaseId === phaseFromTemplate.phaseId);
+      const phase = {
+        id: uuid(),
+        phaseId: phaseFromTemplate.phaseId,
+        name: phaseDefinition.name,
+        description: phaseDefinition.description,
+        duration: _.defaultTo(_.get(phaseFromInput, "duration"), phaseFromTemplate.defaultDuration),
+        isOpen: false,
+        predecessor: phaseFromTemplate.predecessor,
+        constraints: _.defaultTo(_.get(phaseFromInput, "constraints"), []),
+        scheduledStartDate: undefined,
+        scheduledEndDate: undefined,
+        actualStartDate: undefined,
+        actualEndDate: undefined,
+      };
+      if (_.isUndefined(phase.predecessor)) {
+        if (_.isUndefined(_.get(phaseFromInput, "scheduledStartDate"))) {
+          phase.scheduledStartDate = moment(startDate).toDate().toISOString();
+        } else {
+          phase.scheduledStartDate = moment(_.get(phaseFromInput, "scheduledStartDate")).toDate().toISOString();
+        }
+        phase.scheduledEndDate = moment(phase.scheduledStartDate)
+          .add(phase.duration, "seconds")
+          .toDate().toISOString();
+      }
+      return phase;
+    });
+    for (let phase of finalPhases) {
+      if (_.isUndefined(phase.predecessor)) {
+        continue;
+      }
+      const precedecessorPhase = _.find(finalPhases, {
+        phaseId: phase.predecessor,
+      });
+      if (phase.name === "Iterative Review Phase") {
+        phase.scheduledStartDate = precedecessorPhase.scheduledStartDate;
+      } else {
+        phase.scheduledStartDate = precedecessorPhase.scheduledEndDate;
+      }
+      phase.scheduledEndDate = moment(phase.scheduledStartDate)
+        .add(phase.duration, "seconds")
+        .toDate().toISOString();
+    }
+    return finalPhases;
+  }
+
+  async populatePhasesForChallengeUpdate(
+    challengePhases,
+    newPhases,
+    timelineTemplateId,
+    isBeingActivated
+  ) {
+    const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap(
+      timelineTemplateId
+    );
+    const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap();
 
-    console.log("Phases", JSON.stringify(phases, null, 2));
+    const updatedPhases = _.map(challengePhases, (phase) => {
+      const phaseFromTemplate = timelineTemplateMap.get(phase.phaseId);
+      const phaseDefinition = phaseDefinitionMap.get(phase.phaseId);
+      const updatedPhase = {
+        ...phase,
+        predecessor: phaseFromTemplate.predecessor,
+        description: phaseDefinition.description,
+      };
+      if (!_.isUndefined(phase.actualEndDate)) {
+        return updatedPhase;
+      }
+      if (updatedPhase.name === "Iterative Review Phase") {
+        return updatedPhase;
+      }
+      const newPhase = _.find(newPhases, (p) => p.phaseId === updatedPhase.phaseId);
+      if (_.isUndefined(newPhase) && !isBeingActivated) {
+        return updatedPhase;
+      }
+      updatedPhase.duration = _.defaultTo(_.get(newPhase, "duration"), updatedPhase.duration);
+      if (_.isUndefined(updatedPhase.predecessor)) {
+        if (
+          isBeingActivated &&
+          moment(
+            _.defaultTo(_.get(newPhase, "scheduledStartDate"), updatedPhase.scheduledStartDate)
+          ).isSameOrBefore(moment())
+        ) {
+          updatedPhase.isOpen = true;
+          updatedPhase.scheduledStartDate = moment().toDate().toISOString();
+          updatedPhase.actualStartDate = updatedPhase.scheduledStartDate;
+        } else if (
+          updatedPhase.isOpen === false &&
+          !_.isUndefined(_.get(newPhase, "scheduledStartDate"))
+        ) {
+          updatedPhase.scheduledStartDate = moment(newPhase.scheduledStartDate).toDate().toISOString();
+        }
+        updatedPhase.scheduledEndDate = moment(updatedPhase.scheduledStartDate)
+          .add(updatedPhase.duration, "seconds")
+          .toDate().toISOString();
+      }
+      if (!_.isUndefined(newPhase) && !_.isUndefined(newPhase.constraints)) {
+        updatedPhase.constraints = newPhase.constraints;
+      }
+      return updatedPhase;
+    });
+    for (let phase of updatedPhases) {
+      if (_.isUndefined(phase.predecessor)) {
+        continue;
+      }
+      if (phase.name === "Iterative Review Phase") {
+        continue;
+      }
+      const precedecessorPhase = _.find(updatedPhases, {
+        phaseId: phase.predecessor,
+      });
+      phase.scheduledStartDate = precedecessorPhase.scheduledEndDate;
+      phase.scheduledEndDate = moment(phase.scheduledStartDate)
+        .add(phase.duration, "seconds")
+        .toDate().toISOString();
+    }
+    return updatedPhases;
   }
 
   async validatePhases(phases) {
diff --git a/src/common/project-helper.js b/src/common/project-helper.js
index 6a966088..e344267d 100644
--- a/src/common/project-helper.js
+++ b/src/common/project-helper.js
@@ -5,6 +5,7 @@ const config = require("config");
 const HttpStatus = require("http-status-codes");
 const m2mHelper = require("./m2m-helper");
 const { hasAdminRole } = require("./role-helper");
+const errors = require("./errors");
 
 class ProjectHelper {
   /**
diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js
index 8a3aed47..a956743e 100644
--- a/src/controllers/ChallengeController.js
+++ b/src/controllers/ChallengeController.js
@@ -48,15 +48,9 @@ async function searchChallenges(req, res) {
  */
 async function createChallenge(req, res) {
   logger.debug(
-    `createChallenge User: ${JSON.stringify(
-      req.authUser
-    )} - Body: ${JSON.stringify(req.body)}`
-  );
-  const result = await service.createChallenge(
-    req.authUser,
-    req.body,
-    req.userToken
+    `createChallenge User: ${JSON.stringify(req.authUser)} - Body: ${JSON.stringify(req.body)}`
   );
+  const result = await service.createChallenge(req.authUser, req.body, req.userToken);
   res.status(HttpStatus.CREATED).send(result);
 }
 
@@ -66,10 +60,7 @@ async function createChallenge(req, res) {
  * @param {Object} res the response
  */
 async function sendNotifications(req, res) {
-  const result = await service.sendNotifications(
-    req.authUser,
-    req.params.challengeId
-  );
+  const result = await service.sendNotifications(req.authUser, req.params.challengeId);
   res.status(HttpStatus.CREATED).send(result);
 }
 
@@ -93,31 +84,7 @@ async function getChallenge(req, res) {
  * @param {Object} res the response
  */
 async function getChallengeStatistics(req, res) {
-  const result = await service.getChallengeStatistics(
-    req.authUser,
-    req.params.challengeId
-  );
-  res.send(result);
-}
-
-/**
- * Fully update challenge
- * @param {Object} req the request
- * @param {Object} res the response
- */
-async function fullyUpdateChallenge(req, res) {
-  logger.debug(
-    `fullyUpdateChallenge User: ${JSON.stringify(
-      req.authUser
-    )} - ChallengeID: ${req.params.challengeId} - Body: ${JSON.stringify(
-      req.body
-    )}`
-  );
-  const result = await service.fullyUpdateChallenge(
-    req.authUser,
-    req.params.challengeId,
-    req.body
-  );
+  const result = await service.getChallengeStatistics(req.authUser, req.params.challengeId);
   res.send(result);
 }
 
@@ -126,19 +93,13 @@ async function fullyUpdateChallenge(req, res) {
  * @param {Object} req the request
  * @param {Object} res the response
  */
-async function partiallyUpdateChallenge(req, res) {
+async function updateChallenge(req, res) {
   logger.debug(
-    `partiallyUpdateChallenge User: ${JSON.stringify(
-      req.authUser
-    )} - ChallengeID: ${req.params.challengeId} - Body: ${JSON.stringify(
-      req.body
-    )}`
-  );
-  const result = await service.partiallyUpdateChallenge(
-    req.authUser,
-    req.params.challengeId,
-    req.body
+    `updateChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${
+      req.params.challengeId
+    } - Body: ${JSON.stringify(req.body)}`
   );
+  const result = await service.updateChallenge(req.authUser, req.params.challengeId, req.body);
   res.send(result);
 }
 
@@ -149,14 +110,9 @@ async function partiallyUpdateChallenge(req, res) {
  */
 async function deleteChallenge(req, res) {
   logger.debug(
-    `deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${
-      req.params.challengeId
-    }`
-  );
-  const result = await service.deleteChallenge(
-    req.authUser,
-    req.params.challengeId
+    `deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${req.params.challengeId}`
   );
+  const result = await service.deleteChallenge(req.authUser, req.params.challengeId);
   res.send(result);
 }
 
@@ -164,8 +120,7 @@ module.exports = {
   searchChallenges,
   createChallenge,
   getChallenge,
-  fullyUpdateChallenge,
-  partiallyUpdateChallenge,
+  updateChallenge,
   deleteChallenge,
   getChallengeStatistics,
   sendNotifications,
diff --git a/src/routes.js b/src/routes.js
index 45085fbe..407b7a41 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -55,7 +55,7 @@ module.exports = {
     },
     put: {
       controller: "ChallengeController",
-      method: "fullyUpdateChallenge",
+      method: "updateChallenge",
       auth: "jwt",
       access: [
         constants.UserRoles.Admin,
@@ -68,12 +68,12 @@ module.exports = {
     },
     patch: {
       controller: "ChallengeController",
-      method: "partiallyUpdateChallenge",
+      method: "updateChallenge",
       auth: "jwt",
       access: [
         constants.UserRoles.Admin,
-        constants.UserRoles.Copilot,
         constants.UserRoles.SelfServiceCustomer,
+        constants.UserRoles.Copilot,
         constants.UserRoles.Manager,
         constants.UserRoles.User,
       ],
diff --git a/src/scripts/sync-es.js b/src/scripts/sync-es.js
index b808d155..b5ba3388 100644
--- a/src/scripts/sync-es.js
+++ b/src/scripts/sync-es.js
@@ -15,6 +15,7 @@ async function indexChallenge(challenge) {
   try {
     await esClient.update({
       index: config.get("ES.ES_INDEX"),
+      type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined,
       id: challenge.id,
       body: { doc: challenge, doc_as_upsert: true },
     });
diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js
index 041f7fb5..7d7064bd 100644
--- a/src/services/ChallengeService.js
+++ b/src/services/ChallengeService.js
@@ -30,62 +30,23 @@ const phaseHelper = require("../common/phase-helper");
 const projectHelper = require("../common/project-helper");
 const challengeHelper = require("../common/challenge-helper");
 
+const { Metadata: GrpcMetadata } = require("@grpc/grpc-js");
+
 const esClient = helper.getESClient();
 
-const { ChallengeDomain, Challenge } = require("@topcoder-framework/domain-challenge");
+const { ChallengeDomain, UpdateChallengeInput } = require("@topcoder-framework/domain-challenge");
 const { hasAdminRole } = require("../common/role-helper");
+const {
+  validateChallengeUpdateRequest,
+  enrichChallengeForResponse,
+  sanitizeRepeatedFieldsInUpdateRequest,
+  convertPrizeSetValuesToCents,
+  convertPrizeSetValuesToDollars,
+} = require("../common/challenge-helper");
+const deepEqual = require("deep-equal");
 
 const challengeDomain = new ChallengeDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT);
 
-/**
- * Validate the challenge data.
- * @param {Object} challenge the challenge data
- */
-async function validateChallengeData(challenge) {
-  let type;
-  let track;
-  if (challenge.typeId) {
-    try {
-      type = await ChallengeTypeService.getChallengeType(challenge.typeId);
-    } catch (e) {
-      if (e.name === "NotFoundError") {
-        const error = new errors.BadRequestError(
-          `No challenge type found with id: ${challenge.typeId}.`
-        );
-        throw error;
-      } else {
-        throw e;
-      }
-    }
-  }
-  if (challenge.trackId) {
-    try {
-      track = await ChallengeTrackService.getChallengeTrack(challenge.trackId);
-    } catch (e) {
-      if (e.name === "NotFoundError") {
-        const error = new errors.BadRequestError(
-          `No challenge track found with id: ${challenge.trackId}.`
-        );
-        throw error;
-      } else {
-        throw e;
-      }
-    }
-  }
-  if (challenge.timelineTemplateId) {
-    const template = await TimelineTemplateService.getTimelineTemplate(
-      challenge.timelineTemplateId
-    );
-    if (!template.isActive) {
-      const error = new errors.BadRequestError(
-        `The timeline template with id: ${challenge.timelineTemplateId} is inactive`
-      );
-      throw error;
-    }
-  }
-  return { type, track };
-}
-
 /**
  * Check if user can perform modification/deletion to a challenge
  *
@@ -175,33 +136,6 @@ async function ensureAccessibleByGroupsAccess(currentUser, challenge) {
   }
 }
 
-/**
- * Ensure the user can access the groups being updated to
- * @param {Object} currentUser the user who perform operation
- * @param {Object} data the challenge data to be updated
- * @param {String} challenge the original challenge data
- */
-async function ensureAcessibilityToModifiedGroups(currentUser, data, challenge) {
-  const needToCheckForGroupAccess = !currentUser
-    ? true
-    : !currentUser.isMachine && !hasAdminRole(currentUser);
-  if (!needToCheckForGroupAccess) {
-    return;
-  }
-  const userGroups = await helper.getUserGroups(currentUser.userId);
-  const userGroupsIds = _.map(userGroups, (group) => group.id);
-  const updatedGroups = _.difference(
-    _.union(challenge.groups, data.groups),
-    _.intersection(challenge.groups, data.groups)
-  );
-  const filtered = updatedGroups.filter((g) => !userGroupsIds.includes(g));
-  if (filtered.length > 0) {
-    throw new errors.ForbiddenError(
-      "ensureAcessibilityToModifiedGroups :: You don't have access to this group!"
-    );
-  }
-}
-
 /**
  * Search challenges by legacyId
  * @param {Object} currentUser the user who perform operation
@@ -224,7 +158,7 @@ async function searchByLegacyId(currentUser, legacyId, page, perPage) {
       },
     },
   };
-
+  1493;
   logger.debug(`es Query ${JSON.stringify(esQuery)}`);
   let docs;
   try {
@@ -916,7 +850,10 @@ async function searchChallenges(currentUser, criteria) {
   // Search with constructed query
   let docs;
   try {
-    docs = (await esClient.search(esQuery)).body;
+    docs =
+      config.get("ES.OPENSEARCH") == "false"
+        ? await esClient.search(esQuery)
+        : (await esClient.search(esQuery)).body;
   } catch (e) {
     // Catch error when the ES is fresh and has no data
     logger.error(`Query Error from ES ${JSON.stringify(e, null, 4)}`);
@@ -977,7 +914,6 @@ async function searchChallenges(currentUser, criteria) {
 
   return { total, page, perPage, result };
 }
-
 searchChallenges.schema = {
   currentUser: Joi.any(),
   criteria: Joi.object()
@@ -1048,27 +984,6 @@ searchChallenges.schema = {
     .unknown(true),
 };
 
-async function validateCreateChallengeRequest(currentUser, challenge) {
-  // projectId is required for non self-service challenges
-  if (challenge.legacy.selfService == null && challenge.projectId == null) {
-    throw new errors.BadRequestError("projectId is required for non self-service challenges.");
-  }
-
-  if (challenge.status === constants.challengeStatuses.Active) {
-    throw new errors.BadRequestError(
-      "You cannot create an Active challenge. Please create a Draft challenge and then change the status to Active."
-    );
-  }
-
-  helper.ensureNoDuplicateOrNullElements(challenge.tags, "tags");
-  helper.ensureNoDuplicateOrNullElements(challenge.groups, "groups");
-  // helper.ensureNoDuplicateOrNullElements(challenge.terms, 'terms')
-  // helper.ensureNoDuplicateOrNullElements(challenge.events, 'events')
-
-  // check groups authorization
-  await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
-}
-
 /**
  * Create challenge.
  * @param {Object} currentUser the user who perform operation
@@ -1077,7 +992,7 @@ async function validateCreateChallengeRequest(currentUser, challenge) {
  * @returns {Object} the created challenge
  */
 async function createChallenge(currentUser, challenge, userToken) {
-  await validateCreateChallengeRequest(currentUser, challenge);
+  await challengeHelper.validateCreateChallengeRequest(currentUser, challenge);
 
   if (challenge.legacy.selfService) {
     // if self-service, create a new project (what about if projectId is provided in the payload? confirm with business!)
@@ -1086,8 +1001,7 @@ async function createChallenge(currentUser, challenge, userToken) {
       challenge.projectId = await helper.createSelfServiceProject(
         selfServiceProjectName,
         "N/A",
-        config.NEW_SELF_SERVICE_PROJECT_TYPE,
-        userToken
+        config.NEW_SELF_SERVICE_PROJECT_TYPE
       );
     }
 
@@ -1121,8 +1035,10 @@ async function createChallenge(currentUser, challenge, userToken) {
   }
 
   if (!challenge.startDate) {
-    challenge.startDate = new Date();
-  } else challenge.startDate = new Date(challenge.startDate);
+    challenge.startDate = new Date().toISOString();
+  } else {
+    challenge.startDate = new Date(challenge.startDate).toISOString();
+  }
 
   const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack(challenge);
 
@@ -1162,18 +1078,11 @@ async function createChallenge(currentUser, challenge, userToken) {
       throw new errors.BadRequestError(`trackId and typeId are required to create a challenge`);
     }
   }
-
-  if (challenge.timelineTemplateId) {
-    if (!challenge.phases) {
-      challenge.phases = [];
-    }
-
-    await phaseHelper.populatePhases(
-      challenge.phases,
-      challenge.startDate,
-      challenge.timelineTemplateId
-    );
-  }
+  challenge.phases = await phaseHelper.populatePhasesForChallengeCreation(
+    challenge.phases,
+    challenge.startDate,
+    challenge.timelineTemplateId
+  );
 
   // populate challenge terms
   // const projectTerms = await helper.getProjectDefaultTerms(challenge.projectId)
@@ -1197,86 +1106,59 @@ async function createChallenge(currentUser, challenge, userToken) {
   if (challenge.metadata == null) challenge.metadata = [];
   if (challenge.groups == null) challenge.groups = [];
   if (challenge.tags == null) challenge.tags = [];
-  if (challenge.startDate != null) challenge.startDate = challenge.startDate.getTime();
-  if (challenge.endDate != null) challenge.endDate = challenge.endDate.getTime();
-
-  const ret = await challengeDomain.create(challenge);
+  if (challenge.startDate != null) challenge.startDate = challenge.startDate;
+  if (challenge.endDate != null) challenge.endDate = challenge.endDate;
+  if (challenge.discussions == null) challenge.discussions = [];
 
-  console.log("Created Challenge", JSON.stringify(ret, null, 2));
-
-  ret.numOfSubmissions = 0;
-  ret.numOfRegistrants = 0;
+  challenge.metadata = challenge.metadata.map((m) => ({
+    name: m.name,
+    value: typeof m.value === "string" ? m.value : JSON.stringify(m.value),
+  }));
 
-  if (ret.phases && ret.phases.length > 0) {
-    const registrationPhase = _.find(ret.phases, (p) => p.name === "Registration");
-    const submissionPhase = _.find(ret.phases, (p) => p.name === "Submission");
-    ret.currentPhaseNames = _.map(
-      _.filter(ret.phases, (p) => p.isOpen === true),
-      "name"
-    );
-    if (registrationPhase) {
-      ret.registrationStartDate =
-        registrationPhase.actualStartDate || registrationPhase.scheduledStartDate;
-      ret.registrationEndDate =
-        registrationPhase.actualEndDate || registrationPhase.scheduledEndDate;
-    }
-    if (submissionPhase) {
-      ret.submissionStartDate =
-        submissionPhase.actualStartDate || submissionPhase.scheduledStartDate;
-      ret.submissionEndDate = submissionPhase.actualEndDate || submissionPhase.scheduledEndDate;
-    }
-  }
+  const grpcMetadata = new GrpcMetadata();
 
-  if (track) {
-    ret.track = track.name;
-  }
+  grpcMetadata.set("handle", currentUser.handle);
+  grpcMetadata.set("userId", currentUser.userId);
 
-  if (type) {
-    ret.type = type.name;
-  }
+  convertPrizeSetValuesToCents(challenge.prizeSets);
+  const ret = await challengeDomain.create(challenge, grpcMetadata);
+  convertPrizeSetValuesToDollars(ret.prizeSets, ret.overview);
 
-  ret.metadata = ret.metadata.map((m) => {
-    try {
-      m.value = JSON.stringify(JSON.parse(m.value)); // when we update how we index data, make this a JSON field
-    } catch (err) {
-      // do nothing
-    }
-    return m;
-  });
+  ret.numOfSubmissions = 0;
+  ret.numOfRegistrants = 0;
 
-  // Create in ES
-  await esClient.create({
-    index: config.get("ES.ES_INDEX"),
-    refresh: config.get("ES.ES_REFRESH"),
-    id: ret.id,
-    body: ret,
-  });
+  enrichChallengeForResponse(ret, track, type);
+
+  const isLocal = process.env.LOCAL == "true";
+  if (!isLocal) {
+    // Create in ES
+    await esClient.create({
+      index: config.get("ES.ES_INDEX"),
+      type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined,
+      refresh: config.get("ES.ES_REFRESH"),
+      id: ret.id,
+      body: ret,
+    });
 
-  // If the challenge is self-service, add the creating user as the "client manager", *not* the manager
-  // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard
+    // If the challenge is self-service, add the creating user as the "client manager", *not* the manager
+    // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard
 
-  /** Disable Creating Resources locally (because challenge is not being indexed in ES and will result in challenge NOT FOUND error)
-  if (challenge.legacy.selfService) {
-    if (currentUser.handle) {
-      await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID);
-    }
-  } else {
-    // if created by a user, add user as a manager, but only if *not* a self-service challenge
-    if (currentUser.handle) {
-      // logger.debug(`Adding user as manager ${currentUser.handle}`)
-      await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID);
+    if (challenge.legacy.selfService) {
+      if (currentUser.handle) {
+        await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID);
+      }
     } else {
-      // logger.debug(`Not adding manager ${currentUser.sub} ${JSON.stringify(currentUser)}`)
+      if (currentUser.handle) {
+        await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID);
+      }
     }
   }
-  */
 
   // post bus event
   await helper.postBusEvent(constants.Topics.ChallengeCreated, ret);
 
   return ret;
 }
-
 createChallenge.schema = {
   currentUser: Joi.any(),
   challenge: Joi.object()
@@ -1376,7 +1258,7 @@ createChallenge.schema = {
       tags: Joi.array().items(Joi.string()), // tag names
       projectId: Joi.number().integer().positive(),
       legacyId: Joi.number().integer().positive(),
-      startDate: Joi.date(),
+      startDate: Joi.date().iso(),
       status: Joi.string().valid(_.values(constants.challengeStatuses)),
       groups: Joi.array().items(Joi.optionalId()).unique(),
       // gitRepoURLs: Joi.array().items(Joi.string().uri()),
@@ -1419,12 +1301,20 @@ async function getChallenge(currentUser, id, checkIfExists) {
   //   _id: id
   // }))
   try {
-    challenge = (
-      await esClient.getSource({
+    if (config.get("ES.OPENSEARCH") == "true") {
+      challenge = (
+        await esClient.getSource({
+          index: config.get("ES.ES_INDEX"),
+          id,
+        })
+      ).body;
+    } else {
+      challenge = await esClient.getSource({
         index: config.get("ES.ES_INDEX"),
+        type: config.get("ES.ES_TYPE"),
         id,
-      })
-    ).body;
+      });
+    }
   } catch (e) {
     if (e.statusCode === HttpStatus.NOT_FOUND) {
       throw new errors.NotFoundError(`Challenge of id ${id} is not found.`);
@@ -1485,7 +1375,6 @@ async function getChallenge(currentUser, id, checkIfExists) {
 
   return challenge;
 }
-
 getChallenge.schema = {
   currentUser: Joi.any(),
   id: Joi.id(),
@@ -1535,7 +1424,6 @@ async function getChallengeStatistics(currentUser, id) {
   }
   return _.map(_.keys(map), (userId) => map[userId]);
 }
-
 getChallengeStatistics.schema = {
   currentUser: Joi.any(),
   id: Joi.id(),
@@ -1608,103 +1496,107 @@ async function validateWinners(winners, challengeId) {
  * @param {Boolean} isFull the flag indicate it is a fully update operation.
  * @returns {Object} the updated challenge
  */
-async function update(currentUser, challengeId, data, isFull) {
-  const cancelReason = _.cloneDeep(data.cancelReason);
+async function updateChallenge(currentUser, challengeId, data) {
+  const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId));
+
+  // Remove fields from data that are not allowed to be updated and that match the existing challenge
+  data = sanitizeData(sanitizeChallenge(data), challenge);
+  console.debug("Sanitized Data:", data);
+
+  validateChallengeUpdateRequest(currentUser, challenge, data);
+
+  const projectId = _.get(challenge, "projectId");
+
   let sendActivationEmail = false;
   let sendSubmittedEmail = false;
   let sendCompletedEmail = false;
   let sendRejectedEmail = false;
-  delete data.cancelReason;
-  if (!_.isUndefined(_.get(data, "legacy.reviewType"))) {
-    _.set(data, "legacy.reviewType", _.toUpper(_.get(data, "legacy.reviewType")));
-  }
-  if (data.projectId) {
-    await challengeHelper.ensureProjectExist(data.projectId, currentUser);
-  }
 
-  helper.ensureNoDuplicateOrNullElements(data.tags, "tags");
-  helper.ensureNoDuplicateOrNullElements(data.groups, "groups");
-  // helper.ensureNoDuplicateOrNullElements(data.gitRepoURLs, 'gitRepoURLs')
+  const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation(projectId);
 
-  const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId));
-  let dynamicDescription = _.cloneDeep(data.description || challenge.description);
-  if (challenge.legacy.selfService && data.metadata && data.metadata.length > 0) {
-    for (const entry of data.metadata) {
-      const regexp = new RegExp(`{{${entry.name}}}`, "g");
-      dynamicDescription = dynamicDescription.replace(regexp, entry.value);
-    }
-    data.description = dynamicDescription;
+  if (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) {
+    _.set(data, "billing.billingAccountId", billingAccountId);
+    _.set(data, "billing.markup", markup || 0);
   }
-  if (
-    challenge.legacy.selfService &&
-    data.status === constants.challengeStatuses.Draft &&
-    challenge.status !== constants.challengeStatuses.Draft
-  ) {
-    sendSubmittedEmail = true;
+
+  // Make sure the user cannot change the direct project ID
+  if (data.legacy && data.legacy.directProjectId) {
+    _.unset(data, "legacy.directProjectId", directProjectId);
   }
-  // check if it's a self service challenge and project needs to be activated first
-  if (
-    challenge.legacy.selfService &&
-    (data.status === constants.challengeStatuses.Approved ||
-      data.status === constants.challengeStatuses.Active) &&
-    challenge.status !== constants.challengeStatuses.Active
-  ) {
-    try {
-      const selfServiceProjectName = `Self service - ${challenge.createdBy} - ${challenge.name}`;
-      const workItemSummary = _.get(
-        _.find(_.get(challenge, "metadata", []), (m) => m.name === "websitePurpose.description"),
-        "value",
-        "N/A"
-      );
-      await helper.activateProject(
-        challenge.projectId,
-        currentUser,
-        selfServiceProjectName,
-        workItemSummary
-      );
-      if (data.status === constants.challengeStatuses.Active) {
-        sendActivationEmail = true;
+
+  /* BEGIN self-service stuffs */
+
+  // TODO: At some point in the future this should be moved to a Self-Service Challenge Helper
+
+  if (challenge.legacy.selfService) {
+    // prettier-ignore
+    sendSubmittedEmail = data.status === constants.challengeStatuses.Draft && challenge.status !== constants.challengeStatuses.Draft;
+
+    if (data.metadata && data.metadata.length > 0) {
+      let dynamicDescription = _.cloneDeep(data.description || challenge.description);
+      for (const entry of data.metadata) {
+        const regexp = new RegExp(`{{${entry.name}}}`, "g");
+        dynamicDescription = dynamicDescription.replace(regexp, entry.value);
       }
-    } catch (e) {
-      await update(
-        currentUser,
-        challengeId,
-        {
-          ...data,
-          status: constants.challengeStatuses.CancelledPaymentFailed,
-          cancelReason: `Failed to activate project. Error: ${e.message}. JSON: ${JSON.stringify(
-            e
-          )}`,
-        },
-        false
-      );
-      throw new errors.BadRequestError(
-        "Failed to activate the challenge! The challenge has been canceled!"
-      );
+      data.description = dynamicDescription;
     }
-  }
 
-  const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation(
-    _.get(challenge, "projectId")
-  );
-  if (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) {
-    _.set(data, "billing.billingAccountId", billingAccountId);
-    _.set(data, "billing.markup", markup || 0);
-  }
-  if (
-    billingAccountId &&
-    _.includes(config.TOPGEAR_BILLING_ACCOUNTS_ID, _.toString(billingAccountId))
-  ) {
-    if (_.isEmpty(data.metadata)) {
-      data.metadata = [];
+    // check if it's a self service challenge and project needs to be activated first
+    if (
+      (data.status === constants.challengeStatuses.Approved ||
+        data.status === constants.challengeStatuses.Active) &&
+      challenge.status !== constants.challengeStatuses.Active
+    ) {
+      try {
+        const selfServiceProjectName = `Self service - ${challenge.createdBy} - ${challenge.name}`;
+        const workItemSummary = _.get(
+          _.find(_.get(challenge, "metadata", []), (m) => m.name === "websitePurpose.description"),
+          "value",
+          "N/A"
+        );
+        await helper.activateProject(
+          projectId,
+          currentUser,
+          selfServiceProjectName,
+          workItemSummary
+        );
+
+        sendActivationEmail = data.status === constants.challengeStatuses.Active;
+      } catch (e) {
+        await updateChallenge(
+          currentUser,
+          challengeId,
+          {
+            ...data,
+            status: constants.challengeStatuses.CancelledPaymentFailed,
+            cancelReason: `Failed to activate project. Error: ${e.message}. JSON: ${JSON.stringify(
+              e
+            )}`,
+          },
+          false
+        );
+        throw new errors.BadRequestError(
+          "Failed to activate the challenge! The challenge has been canceled!"
+        );
+      }
     }
-    if (!_.find(data.metadata, (e) => e.name === "postMortemRequired")) {
-      data.metadata.push({
-        name: "postMortemRequired",
-        value: "false",
-      });
+
+    if (data.status === constants.challengeStatuses.Draft) {
+      try {
+        await helper.updateSelfServiceProjectInfo(
+          projectId,
+          data.endDate || challenge.endDate,
+          currentUser
+        );
+      } catch (e) {
+        logger.debug(`There was an error trying to update the project: ${e.message}`);
+      }
     }
   }
+
+  /* END self-service stuffs */
+
+  let isChallengeBeingActivated = false;
   if (data.status) {
     if (data.status === constants.challengeStatuses.Active) {
       if (
@@ -1725,7 +1617,11 @@ async function update(currentUser, challengeId, data, isFull) {
           "Cannot Activate this project, it has no active billing account."
         );
       }
+      if (challenge.status === constants.challengeStatuses.Draft) {
+        isChallengeBeingActivated = true;
+      }
     }
+
     if (
       data.status === constants.challengeStatuses.CancelledRequirementsInfeasible ||
       data.status === constants.challengeStatuses.CancelledPaymentFailed
@@ -1737,6 +1633,7 @@ async function update(currentUser, challengeId, data, isFull) {
       }
       sendRejectedEmail = true;
     }
+
     if (data.status === constants.challengeStatuses.Completed) {
       if (
         !_.get(challenge, "legacy.pureV5Task") &&
@@ -1749,81 +1646,6 @@ async function update(currentUser, challengeId, data, isFull) {
     }
   }
 
-  // FIXME: Tech Debt
-  if (
-    _.get(challenge, "legacy.track") &&
-    _.get(data, "legacy.track") &&
-    _.get(challenge, "legacy.track") !== _.get(data, "legacy.track")
-  ) {
-    throw new errors.ForbiddenError("Cannot change legacy.track");
-  }
-  if (
-    _.get(challenge, "trackId") &&
-    _.get(data, "trackId") &&
-    _.get(challenge, "trackId") !== _.get(data, "trackId")
-  ) {
-    throw new errors.ForbiddenError("Cannot change trackId");
-  }
-  if (
-    _.get(challenge, "typeId") &&
-    _.get(data, "typeId") &&
-    _.get(challenge, "typeId") !== _.get(data, "typeId")
-  ) {
-    throw new errors.ForbiddenError("Cannot change typeId");
-  }
-
-  if (
-    _.get(challenge, "legacy.useSchedulingAPI") &&
-    _.get(data, "legacy.useSchedulingAPI") &&
-    _.get(challenge, "legacy.useSchedulingAPI") !== _.get(data, "legacy.useSchedulingAPI")
-  ) {
-    throw new errors.ForbiddenError("Cannot change legacy.useSchedulingAPI");
-  }
-  if (
-    _.get(challenge, "legacy.pureV5Task") &&
-    _.get(data, "legacy.pureV5Task") &&
-    _.get(challenge, "legacy.pureV5Task") !== _.get(data, "legacy.pureV5Task")
-  ) {
-    throw new errors.ForbiddenError("Cannot change legacy.pureV5Task");
-  }
-  if (
-    _.get(challenge, "legacy.pureV5") &&
-    _.get(data, "legacy.pureV5") &&
-    _.get(challenge, "legacy.pureV5") !== _.get(data, "legacy.pureV5")
-  ) {
-    throw new errors.ForbiddenError("Cannot change legacy.pureV5");
-  }
-  if (
-    _.get(challenge, "legacy.selfService") &&
-    _.get(data, "legacy.selfService") &&
-    _.get(challenge, "legacy.selfService") !== _.get(data, "legacy.selfService")
-  ) {
-    throw new errors.ForbiddenError("Cannot change legacy.selfService");
-  }
-
-  if (!_.isUndefined(challenge.legacy) && !_.isUndefined(data.legacy)) {
-    _.extend(challenge.legacy, data.legacy);
-  }
-
-  if (!_.isUndefined(challenge.billing) && !_.isUndefined(data.billing)) {
-    _.extend(challenge.billing, data.billing);
-  } else if (_.isUndefined(challenge.billing) && !_.isUndefined(data.billing)) {
-    challenge.billing = data.billing;
-  }
-
-  await helper.ensureUserCanModifyChallenge(currentUser, challenge);
-
-  // check groups access to be updated group values
-  if (data.groups) {
-    await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
-  }
-  let newAttachments;
-  if (isFull || !_.isUndefined(data.attachments)) {
-    newAttachments = data.attachments;
-  }
-
-  await ensureAccessibleForChallenge(currentUser, challenge);
-
   // Only M2M can update url and options of discussions
   if (data.discussions && data.discussions.length > 0) {
     if (challenge.discussions && challenge.discussions.length > 0) {
@@ -1858,55 +1680,10 @@ async function update(currentUser, challengeId, data, isFull) {
     }
   }
 
-  // Validate the challenge terms
-  let newTermsOfUse;
-  if (!_.isUndefined(data.terms)) {
-    // helper.ensureNoDuplicateOrNullElements(data.terms, 'terms')
-
-    // Get the project default terms
-    const defaultTerms = await helper.getProjectDefaultTerms(challenge.projectId);
-
-    if (defaultTerms) {
-      // Make sure that the default project terms were not removed
-      // TODO - there are no default terms returned by v5
-      // the terms array is objects with a roleId now, so this _.difference won't work
-      // const removedTerms = _.difference(defaultTerms, data.terms)
-      // if (removedTerms.length !== 0) {
-      //   throw new errors.BadRequestError(`Default project terms ${removedTerms} should not be removed`)
-      // }
-    }
-    // newTermsOfUse = await helper.validateChallengeTerms(_.union(data.terms, defaultTerms))
-    newTermsOfUse = await helper.validateChallengeTerms(data.terms);
-  }
-
-  await challengeHelper.validateAndGetChallengeTypeAndTrack(data);
-
-  if (
-    (challenge.status === constants.challengeStatuses.Completed ||
-      challenge.status === constants.challengeStatuses.Cancelled) &&
-    data.status &&
-    data.status !== challenge.status &&
-    data.status !== constants.challengeStatuses.CancelledClientRequest
-  ) {
-    throw new errors.BadRequestError(
-      `Cannot change ${challenge.status} challenge status to ${data.status} status`
-    );
-  }
-
-  if (
-    data.winners &&
-    data.winners.length > 0 &&
-    challenge.status !== constants.challengeStatuses.Completed &&
-    data.status !== constants.challengeStatuses.Completed
-  ) {
-    throw new errors.BadRequestError(
-      `Cannot set winners for challenge with non-completed ${challenge.status} status`
-    );
-  }
-
   // TODO: Fix this Tech Debt once legacy is turned off
   const finalStatus = data.status || challenge.status;
   const finalTimelineTemplateId = data.timelineTemplateId || challenge.timelineTemplateId;
+  const timelineTemplateChanged = false;
   if (!_.get(data, "legacy.pureV5") && !_.get(challenge, "legacy.pureV5")) {
     if (
       finalStatus !== constants.challengeStatuses.New &&
@@ -1919,6 +1696,7 @@ async function update(currentUser, challengeId, data, isFull) {
   } else if (finalTimelineTemplateId !== challenge.timelineTemplateId) {
     // make sure there are no previous phases if the timeline template has changed
     challenge.phases = [];
+    timelineTemplateChanged = true;
   }
 
   if (data.prizeSets && data.prizeSets.length > 0) {
@@ -1936,17 +1714,17 @@ async function update(currentUser, challengeId, data, isFull) {
       _.get(challenge, "overview.totalPrizes")
     ) {
       // remove the totalPrizes if challenge prizes are empty
-      challenge.overview = _.omit(challenge.overview, ["totalPrizes"]);
+      data.overview = challenge.overview = _.omit(challenge.overview, ["totalPrizes"]);
     } else {
       const totalPrizes = helper.sumOfPrizes(
         prizeSetsGroup[constants.prizeSetTypes.ChallengePrizes][0].prizes
       );
-      logger.debug(`re-calculate total prizes, current value is ${totalPrizes.value}`);
       _.assign(challenge, { overview: { totalPrizes } });
+      _.assign(data, { overview: { totalPrizes } });
     }
   }
 
-  if (data.phases || data.startDate) {
+  if (data.phases || data.startDate || timelineTemplateChanged) {
     if (
       challenge.status === constants.challengeStatuses.Completed ||
       challenge.status.indexOf(constants.challengeStatuses.Cancelled) > -1
@@ -1955,55 +1733,30 @@ async function update(currentUser, challengeId, data, isFull) {
         `Challenge phase/start date can not be modified for Completed or Cancelled challenges.`
       );
     }
-
-    if (data.phases && data.phases.length > 0) {
-      for (let i = 0; i < challenge.phases.length; i += 1) {
-        const updatedPhaseInfo = _.find(
-          data.phases,
-          (p) => p.phaseId === challenge.phases[i].phaseId
-        );
-        if (updatedPhaseInfo) {
-          _.extend(challenge.phases[i], updatedPhaseInfo);
-        }
-      }
-      if (challenge.phases.length === 0 && data.phases && data.phases.length > 0) {
-        challenge.phases = data.phases;
-      }
-    }
-
-    const newPhases = _.cloneDeep(challenge.phases) || [];
     const newStartDate = data.startDate || challenge.startDate;
+    let newPhases;
+    if (timelineTemplateChanged) {
+      newPhases = await phaseHelper.populatePhasesForChallengeCreation(
+        data.phases,
+        newStartDate,
+        finalTimelineTemplateId
+      );
+    } else if (data.startDate || (data.phases && data.phases.length > 0)) {
+      newPhases = await phaseHelper.populatePhasesForChallengeUpdate(
+        challenge.phases,
+        data.phases,
+        challenge.timelineTemplateId,
+        isChallengeBeingActivated
+      );
+    }
 
-    await PhaseService.validatePhases(newPhases);
-
-    // populate phases
-    await phaseHelper.populatePhases(
-      newPhases,
-      newStartDate,
-      data.timelineTemplateId || challenge.timelineTemplateId
-    );
     data.phases = newPhases;
-    challenge.phases = newPhases;
-    data.startDate = newStartDate;
+    data.startDate = new Date(newStartDate).toISOString();
     data.endDate = helper.calculateChallengeEndDate(challenge, data);
   }
 
-  // PUT HERE
-  if (data.status) {
-    if (challenge.legacy.selfService && data.status === constants.challengeStatuses.Draft) {
-      try {
-        await helper.updateSelfServiceProjectInfo(
-          challenge.projectId,
-          data.endDate || challenge.endDate,
-          currentUser
-        );
-      } catch (e) {
-        logger.debug(`There was an error trying to update the project: ${e.message}`);
-      }
-    }
-  }
-
   if (data.winners && data.winners.length && data.winners.length > 0) {
+    console.log("Request to validate winners", data.winners, challengeId);
     await validateWinners(data.winners, challengeId);
   }
 
@@ -2055,642 +1808,153 @@ async function update(currentUser, challengeId, data, isFull) {
     logger.info(`${challengeId} is not a pureV5 challenge or has no winners set yet.`);
   }
 
-  data.updated = moment().utc();
-  data.updatedBy = currentUser.handle || currentUser.sub;
-  const updateDetails = {};
-  let phasesHaveBeenModified = false;
-  _.each(data, (value, key) => {
-    let op;
-    if (key === "metadata") {
-      if (
-        _.isUndefined(challenge[key]) ||
-        challenge[key].length !== value.length ||
-        _.differenceWith(challenge[key], value, _.isEqual).length !== 0
-      ) {
-        op = "$PUT";
-      }
-    } else if (key === "phases") {
-      // always consider a modification if the property exists
-      phasesHaveBeenModified = true;
-      logger.info("update phases");
-      op = "$PUT";
-    } else if (key === "prizeSets") {
-      if (isDifferentPrizeSets(challenge[key], value)) {
-        logger.info("update prize sets");
-        op = "$PUT";
-      }
-    } else if (key === "tags") {
-      if (
-        _.isUndefined(challenge[key]) ||
-        challenge[key].length !== value.length ||
-        _.intersection(challenge[key], value).length !== value.length
-      ) {
-        op = "$PUT";
-      }
-    } else if (key === "attachments") {
-      const oldIds = _.map(challenge.attachments || [], (a) => a.id);
-      if (
-        oldIds.length !== value.length ||
-        _.intersection(
-          oldIds,
-          _.map(value, (a) => a.id)
-        ).length !== value.length
-      ) {
-        op = "$PUT";
-      }
-    } else if (key === "groups") {
+  const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack({
+    typeId: challenge.typeId,
+    trackId: challenge.trackId,
+    timelineTemplateId: challenge.timelineTemplateId,
+  });
+
+  if (_.get(type, "isTask")) {
+    if (!_.isEmpty(_.get(data, "task.memberId"))) {
+      const challengeResources = await helper.getChallengeResources(challengeId);
+      const registrants = _.filter(
+        challengeResources,
+        (r) => r.roleId === config.SUBMITTER_ROLE_ID
+      );
       if (
-        _.isUndefined(challenge[key]) ||
-        challenge[key].length !== value.length ||
-        _.intersection(challenge[key], value).length !== value.length
+        !_.find(
+          registrants,
+          (r) => _.toString(r.memberId) === _.toString(_.get(data, "task.memberId"))
+        )
       ) {
-        op = "$PUT";
+        throw new errors.BadRequestError(
+          `Member ${_.get(
+            data,
+            "task.memberId"
+          )} is not a submitter resource of challenge ${challengeId}`
+        );
       }
-      // } else if (key === 'gitRepoURLs') {
-      //   if (_.isUndefined(challenge[key]) || challenge[key].length !== value.length ||
-      //     _.intersection(challenge[key], value).length !== value.length) {
-      //     op = '$PUT'
-      //   }
-    } else if (key === "winners") {
-      if (
-        _.isUndefined(challenge[key]) ||
-        challenge[key].length !== value.length ||
-        _.intersectionWith(challenge[key], value, _.isEqual).length !== value.length
-      ) {
-        op = "$PUT";
-      }
-    } else if (key === "terms") {
-      const oldIds = _.map(challenge.terms || [], (t) => t.id);
-      const newIds = _.map(value || [], (t) => t.id);
-      if (
-        oldIds.length !== newIds.length ||
-        _.intersection(oldIds, newIds).length !== value.length
-      ) {
-        op = "$PUT";
-      }
-    } else if (key === "billing" || key === "legacy") {
-      // make sure that's always being udpated
-      op = "$PUT";
-    } else if (_.isUndefined(challenge[key]) || challenge[key] !== value) {
-      op = "$PUT";
-    } else if (_.get(challenge, "legacy.pureV5Task") && key === "task") {
-      // always update task for pureV5 challenges
-      op = "$PUT";
-    }
-
-    if (op) {
-      if (_.isUndefined(updateDetails[op])) {
-        updateDetails[op] = {};
-      }
-      if (key === "attachments") {
-        updateDetails[op].attachments = newAttachments;
-      } else if (key === "terms") {
-        updateDetails[op].terms = newTermsOfUse;
-      } else {
-        updateDetails[op][key] = value;
-      }
-      if (key !== "updated" && key !== "updatedBy") {
-        let oldValue;
-        let newValue;
-        if (key === "attachments") {
-          oldValue = challenge.attachments ? JSON.stringify(challenge.attachments) : "NULL";
-          newValue = JSON.stringify(newAttachments);
-        } else if (key === "terms") {
-          oldValue = challenge.terms ? JSON.stringify(challenge.terms) : "NULL";
-          newValue = JSON.stringify(newTermsOfUse);
-        } else {
-          oldValue = challenge[key] ? JSON.stringify(challenge[key]) : "NULL";
-          newValue = JSON.stringify(value);
-        }
-      }
-    }
-  });
-
-  if (isFull && _.isUndefined(data.metadata) && challenge.metadata) {
-    updateDetails["$DELETE"] = { metadata: null };
-    delete challenge.metadata;
-    // send null to Elasticsearch to clear the field
-    data.metadata = null;
-  }
-  if (isFull && _.isUndefined(data.attachments) && challenge.attachments) {
-    if (!updateDetails["$DELETE"]) {
-      updateDetails["$DELETE"] = {};
-    }
-    updateDetails["$DELETE"].attachments = null;
-    delete challenge.attachments;
-    // send null to Elasticsearch to clear the field
-    data.attachments = null;
-  }
-  if (isFull && _.isUndefined(data.groups) && challenge.groups) {
-    if (!updateDetails["$DELETE"]) {
-      updateDetails["$DELETE"] = {};
-    }
-    updateDetails["$DELETE"].groups = null;
-    delete challenge.groups;
-    // send null to Elasticsearch to clear the field
-    data.groups = null;
-  }
-  // if (isFull && _.isUndefined(data.gitRepoURLs) && challenge.gitRepoURLs) {
-  //   if (!updateDetails['$DELETE']) {
-  //     updateDetails['$DELETE'] = {}
-  //   }
-  //   updateDetails['$DELETE'].gitRepoURLs = null
-  //   auditLogs.push({
-  //     id: uuid(),
-  //     challengeId,
-  //     fieldName: 'gitRepoURLs',
-  //     oldValue: JSON.stringify(challenge.gitRepoURLs),
-  //     newValue: 'NULL',
-  //     created: moment().utc(),
-  //     createdBy: currentUser.handle || currentUser.sub,
-  //     memberId: currentUser.userId || null
-  //   })
-  //   delete challenge.gitRepoURLs
-  //   // send null to Elasticsearch to clear the field
-  //   data.gitRepoURLs = null
-  // }
-  if (isFull && _.isUndefined(data.legacyId) && challenge.legacyId) {
-    data.legacyId = challenge.legacyId;
-  }
-  if (isFull && _.isUndefined(data.winners) && challenge.winners) {
-    if (!updateDetails["$DELETE"]) {
-      updateDetails["$DELETE"] = {};
-    }
-    updateDetails["$DELETE"].winners = null;
-    delete challenge.winners;
-    // send null to Elasticsearch to clear the field
-    data.winners = null;
-  }
-
-  const { track, type } = await validateChallengeData(_.pick(challenge, ["trackId", "typeId"]));
-
-  if (_.get(type, "isTask")) {
-    if (!_.isEmpty(_.get(data, "task.memberId"))) {
-      const challengeResources = await helper.getChallengeResources(challengeId);
-      const registrants = _.filter(
-        challengeResources,
-        (r) => r.roleId === config.SUBMITTER_ROLE_ID
-      );
-      if (
-        !_.find(
-          registrants,
-          (r) => _.toString(r.memberId) === _.toString(_.get(data, "task.memberId"))
-        )
-      ) {
-        throw new errors.BadRequestError(
-          `Member ${_.get(
-            data,
-            "task.memberId"
-          )} is not a submitter resource of challenge ${challengeId}`
-        );
-      }
-    }
-  }
-
-  logger.debug(`Challenge.update id: ${challengeId} Details:  ${JSON.stringify(updateDetails)}`);
-
-  delete data.attachments;
-  delete data.terms;
-  _.assign(challenge, data);
-  if (!_.isUndefined(newAttachments)) {
-    challenge.attachments = newAttachments;
-    data.attachments = newAttachments;
-  }
-
-  if (!_.isUndefined(newTermsOfUse)) {
-    challenge.terms = newTermsOfUse;
-    data.terms = newTermsOfUse;
-  }
-
-  if (challenge.phases && challenge.phases.length > 0) {
-    await getPhasesAndPopulate(challenge);
-  }
-
-  // Populate challenge.track and challenge.type based on the track/type IDs
-
-  if (track) {
-    challenge.track = track.name;
-  }
-  if (type) {
-    challenge.type = type.name;
-  }
-
-  try {
-    logger.debug(`ChallengeDomain.update id: ${challengeId} Details:  ${JSON.stringify(challenge)}`)
-    const { items } = await challengeDomain.update({
-      filterCriteria: getScanCriteria({
-        id: challengeId,
-      }),
-      updateInput: {
-        ...challenge,
-      },
-    });
-    if (items.length > 0) {
-      if (!challenge.legacyId) {
-        challenge.legacyId = items[0].legacyId;
-      }
-    }
-  } catch (e) {
-    throw e;
-  }
-  // post bus event
-  logger.debug(`Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(challenge)}`);
-  const options = {};
-  if (challenge.status === "Completed") {
-    options.key = `${challenge.id}:${challenge.status}`;
-  }
-  await helper.postBusEvent(constants.Topics.ChallengeUpdated, challenge, options);
-  if (phasesHaveBeenModified === true && _.get(challenge, "legacy.useSchedulingAPI")) {
-    await helper.postBusEvent(config.SCHEDULING_TOPIC, { id: challengeId });
-  }
-  if (challenge.phases && challenge.phases.length > 0) {
-    challenge.currentPhase = challenge.phases
-      .slice()
-      .reverse()
-      .find((phase) => phase.isOpen);
-    challenge.endDate = helper.calculateChallengeEndDate(challenge);
-    const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration");
-    const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission");
-    challenge.currentPhaseNames = _.map(
-      _.filter(challenge.phases, (p) => p.isOpen === true),
-      "name"
-    );
-    if (registrationPhase) {
-      challenge.registrationStartDate =
-        registrationPhase.actualStartDate || registrationPhase.scheduledStartDate;
-      challenge.registrationEndDate =
-        registrationPhase.actualEndDate || registrationPhase.scheduledEndDate;
-    }
-    if (submissionPhase) {
-      challenge.submissionStartDate =
-        submissionPhase.actualStartDate || submissionPhase.scheduledStartDate;
-      challenge.submissionEndDate =
-        submissionPhase.actualEndDate || submissionPhase.scheduledEndDate;
-    }
-  }
-  // Update ES
-  await esClient.update({
-    index: config.get("ES.ES_INDEX"),
-    refresh: config.get("ES.ES_REFRESH"),
-    id: challengeId,
-    body: {
-      doc: challenge,
-    },
-  });
-
-  if (challenge.legacy.selfService) {
-    const creator = await helper.getMemberByHandle(challenge.createdBy);
-    if (sendSubmittedEmail) {
-      await helper.sendSelfServiceNotification(
-        constants.SelfServiceNotificationTypes.WORK_REQUEST_SUBMITTED,
-        [{ email: creator.email }],
-        {
-          handle: creator.handle,
-          workItemName: challenge.name,
-        }
-      );
-    }
-    if (sendActivationEmail) {
-      await helper.sendSelfServiceNotification(
-        constants.SelfServiceNotificationTypes.WORK_REQUEST_STARTED,
-        [{ email: creator.email }],
-        {
-          handle: creator.handle,
-          workItemName: challenge.name,
-          workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}`,
-        }
-      );
-    }
-    if (sendCompletedEmail) {
-      await helper.sendSelfServiceNotification(
-        constants.SelfServiceNotificationTypes.WORK_COMPLETED,
-        [{ email: creator.email }],
-        {
-          handle: creator.handle,
-          workItemName: challenge.name,
-          workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}?tab=solutions`,
-        }
-      );
-    }
-    if (sendRejectedEmail || cancelReason) {
-      logger.debug("Should send redirected email");
-      await helper.sendSelfServiceNotification(
-        constants.SelfServiceNotificationTypes.WORK_REQUEST_REDIRECTED,
-        [{ email: creator.email }],
-        {
-          handle: creator.handle,
-          workItemName: challenge.name,
-        }
-      );
-    }
-  }
-  return challenge;
-}
-
-/**
- * Send notifications
- * @param {Object} currentUser the current use
- * @param {String} challengeId the challenge id
- */
-async function sendNotifications(currentUser, challengeId) {
-  const challenge = await getChallenge(currentUser, challengeId);
-  const creator = await helper.getMemberByHandle(challenge.createdBy);
-  if (challenge.status === constants.challengeStatuses.Completed) {
-    await helper.sendSelfServiceNotification(
-      constants.SelfServiceNotificationTypes.WORK_COMPLETED,
-      [{ email: creator.email }],
-      {
-        handle: creator.handle,
-        workItemName: challenge.name,
-        workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}?tab=solutions`,
-      }
-    );
-    return { type: constants.SelfServiceNotificationTypes.WORK_COMPLETED };
-  }
-}
-
-sendNotifications.schema = {
-  currentUser: Joi.any(),
-  challengeId: Joi.id(),
-};
-
-/**
- * Remove unwanted properties from the challenge object
- * @param {Object} challenge the challenge object
- */
-function sanitizeChallenge(challenge) {
-  const sanitized = _.pick(challenge, [
-    "trackId",
-    "typeId",
-    "name",
-    "description",
-    "privateDescription",
-    "descriptionFormat",
-    "timelineTemplateId",
-    "tags",
-    "projectId",
-    "legacyId",
-    "startDate",
-    "status",
-    "task",
-    "groups",
-    "cancelReason",
-  ]);
-  if (!_.isUndefined(sanitized.name)) {
-    sanitized.name = xss(sanitized.name);
-  }
-  if (!_.isUndefined(sanitized.description)) {
-    sanitized.description = xss(sanitized.description);
-  }
-  if (challenge.legacy) {
-    sanitized.legacy = _.pick(challenge.legacy, [
-      "track",
-      "subTrack",
-      "reviewType",
-      "confidentialityType",
-      "forumId",
-      "directProjectId",
-      "screeningScorecardId",
-      "reviewScorecardId",
-      "isTask",
-      "useSchedulingAPI",
-      "pureV5Task",
-      "pureV5",
-      "selfService",
-      "selfServiceCopilot",
-    ]);
-  }
-  if (challenge.billing) {
-    sanitized.billing = _.pick(challenge.billing, ["billingAccountId", "markup"]);
-  }
-  if (challenge.metadata) {
-    sanitized.metadata = _.map(challenge.metadata, (meta) => _.pick(meta, ["name", "value"]));
-  }
-  if (challenge.phases) {
-    sanitized.phases = _.map(challenge.phases, (phase) =>
-      _.pick(phase, [
-        "phaseId",
-        "duration",
-        "isOpen",
-        "actualEndDate",
-        "scheduledStartDate",
-        "constraints",
-      ])
-    );
-  }
-  if (challenge.prizeSets) {
-    sanitized.prizeSets = _.map(challenge.prizeSets, (prizeSet) => ({
-      ..._.pick(prizeSet, ["type", "description"]),
-      prizes: _.map(prizeSet.prizes, (prize) => _.pick(prize, ["description", "type", "value"])),
-    }));
-  }
-  if (challenge.events) {
-    sanitized.events = _.map(challenge.events, (event) => _.pick(event, ["id", "name", "key"]));
-  }
-  if (challenge.winners) {
-    sanitized.winners = _.map(challenge.winners, (winner) =>
-      _.pick(winner, ["userId", "handle", "placement", "type"])
-    );
-  }
-  if (challenge.discussions) {
-    sanitized.discussions = _.map(challenge.discussions, (discussion) => ({
-      ..._.pick(discussion, ["id", "provider", "name", "type", "url", "options"]),
-      name: _.get(discussion, "name", "").substring(0, config.FORUM_TITLE_LENGTH_LIMIT),
-    }));
-  }
-  if (challenge.terms) {
-    sanitized.terms = _.map(challenge.terms, (term) => _.pick(term, ["id", "roleId"]));
-  }
-  if (challenge.attachments) {
-    sanitized.attachments = _.map(challenge.attachments, (attachment) =>
-      _.pick(attachment, ["id", "name", "url", "fileSize", "description", "challengeId"])
-    );
-  }
-  return sanitized;
-}
-
-/**
- * Fully update challenge.
- * @param {Object} currentUser the user who perform operation
- * @param {String} challengeId the challenge id
- * @param {Object} data the challenge data to be updated
- * @returns {Object} the updated challenge
- */
-async function fullyUpdateChallenge(currentUser, challengeId, data) {
-  return update(currentUser, challengeId, sanitizeChallenge(data), true);
-}
-
-fullyUpdateChallenge.schema = {
-  currentUser: Joi.any(),
-  challengeId: Joi.id(),
-  data: Joi.object()
-    .keys({
-      legacy: Joi.object()
-        .keys({
-          reviewType: Joi.string()
-            .valid(_.values(constants.reviewTypes))
-            .insensitive()
-            .default(constants.reviewTypes.Internal),
-          confidentialityType: Joi.string().default(config.DEFAULT_CONFIDENTIALITY_TYPE),
-          forumId: Joi.number().integer(),
-          directProjectId: Joi.number().integer(),
-          screeningScorecardId: Joi.number().integer(),
-          reviewScorecardId: Joi.number().integer(),
-          isTask: Joi.boolean(),
-          useSchedulingAPI: Joi.boolean(),
-          pureV5Task: Joi.boolean(),
-          pureV5: Joi.boolean(),
-          selfService: Joi.boolean(),
-          selfServiceCopilot: Joi.string().allow(null),
-        })
-        .unknown(true),
-      cancelReason: Joi.string(),
-      billing: Joi.object()
-        .keys({
-          billingAccountId: Joi.string(),
-          markup: Joi.number().min(0).max(100),
-        })
-        .unknown(true),
-      task: Joi.object().keys({
-        isTask: Joi.boolean().default(false),
-        isAssigned: Joi.boolean().default(false),
-        memberId: Joi.string().allow(null),
-      }),
-      trackId: Joi.optionalId(),
-      typeId: Joi.optionalId(),
-      name: Joi.string().required(),
-      description: Joi.string(),
-      privateDescription: Joi.string(),
-      descriptionFormat: Joi.string(),
-      metadata: Joi.array()
-        .items(
-          Joi.object()
-            .keys({
-              name: Joi.string().required(),
-              value: Joi.required(),
-            })
-            .unknown(true)
-        )
-        .unique((a, b) => a.name === b.name),
-      timelineTemplateId: Joi.string(), // Joi.optionalId(),
-      phases: Joi.array().items(
-        Joi.object()
-          .keys({
-            phaseId: Joi.id(),
-            duration: Joi.number().integer().min(0),
-            isOpen: Joi.boolean(),
-            actualEndDate: Joi.date().allow(null),
-            scheduledStartDate: Joi.date().allow(null),
-            constraints: Joi.array()
-              .items(
-                Joi.object()
-                  .keys({
-                    name: Joi.string(),
-                    value: Joi.number().integer().min(0),
-                  })
-                  .optional()
-              )
-              .optional(),
-          })
-          .unknown(true)
-      ),
-      prizeSets: Joi.array().items(
-        Joi.object()
-          .keys({
-            type: Joi.string().valid(_.values(constants.prizeSetTypes)).required(),
-            description: Joi.string(),
-            prizes: Joi.array()
-              .items(
-                Joi.object().keys({
-                  description: Joi.string(),
-                  type: Joi.string().required(),
-                  value: Joi.number().min(0).required(),
-                })
-              )
-              .min(1)
-              .required(),
-          })
-          .unknown(true)
-      ),
-      events: Joi.array().items(
-        Joi.object()
-          .keys({
-            id: Joi.number().required(),
-            name: Joi.string(),
-            key: Joi.string(),
-          })
-          .unknown(true)
-      ),
-      discussions: Joi.array().items(
-        Joi.object().keys({
-          id: Joi.optionalId(),
-          name: Joi.string().required(),
-          type: Joi.string().required().valid(_.values(constants.DiscussionTypes)),
-          provider: Joi.string().required(),
-          url: Joi.string(),
-          options: Joi.array().items(Joi.object()),
-        })
-      ),
-      tags: Joi.array().items(Joi.string()), // tag names
-      projectId: Joi.number().integer().positive().required(),
-      legacyId: Joi.number().integer().positive(),
-      startDate: Joi.date(),
-      status: Joi.string().valid(_.values(constants.challengeStatuses)).required(),
-      attachments: Joi.array().items(
-        Joi.object().keys({
-          id: Joi.id(),
-          challengeId: Joi.id(),
-          name: Joi.string().required(),
-          url: Joi.string().uri().required(),
-          fileSize: Joi.fileSize(),
-          description: Joi.string(),
-        })
-      ),
-      groups: Joi.array().items(Joi.optionalId()),
-      // gitRepoURLs: Joi.array().items(Joi.string().uri()),
-      winners: Joi.array()
-        .items(
-          Joi.object()
-            .keys({
-              userId: Joi.number().integer().positive().required(),
-              handle: Joi.string().required(),
-              placement: Joi.number().integer().positive().required(),
-              type: Joi.string()
-                .valid(_.values(constants.prizeSetTypes))
-                .default(constants.prizeSetTypes.ChallengePrizes),
-            })
-            .unknown(true)
-        )
-        .min(1),
-      terms: Joi.array()
-        .items(
-          Joi.object()
-            .keys({
-              id: Joi.id(),
-              roleId: Joi.id(),
-            })
-            .unknown(true)
-        )
-        .optional()
-        .allow([]),
-      overview: Joi.any().forbidden(),
-    })
-    .unknown(true)
-    .required(),
-};
+    }
+  }
 
-/**
- * Partially update challenge.
- * @param {Object} currentUser the user who perform operation
- * @param {String} challengeId the challenge id
- * @param {Object} data the challenge data to be updated
- * @returns {Object} the updated challenge
- */
-async function partiallyUpdateChallenge(currentUser, challengeId, data) {
-  return update(currentUser, challengeId, sanitizeChallenge(data));
+  if (!_.isUndefined(data.terms)) {
+    await helper.validateChallengeTerms(data.terms.map((t) => t.id));
+  }
+
+  if (data.phases && data.phases.length > 0) {
+    await getPhasesAndPopulate(data);
+
+    if (deepEqual(data.phases, challenge.phases)) {
+      delete data.phases;
+    }
+  }
+
+  try {
+    const updateInput = sanitizeRepeatedFieldsInUpdateRequest(data);
+
+    if (!_.isEmpty(updateInput)) {
+      const grpcMetadata = new GrpcMetadata();
+
+      grpcMetadata.set("handle", currentUser.handle);
+      grpcMetadata.set("userId", currentUser.userId);
+
+      if (updateInput.prizeSetUpdate != null) {
+        convertPrizeSetValuesToCents(updateInput.prizeSetUpdate.prizeSets);
+      }
+      await challengeDomain.update(
+        {
+          filterCriteria: getScanCriteria({ id: challengeId }),
+          updateInput,
+        },
+        grpcMetadata
+      );
+    }
+  } catch (e) {
+    throw e;
+  }
+
+  const updatedChallenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId));
+  convertPrizeSetValuesToDollars(updatedChallenge.prizeSets, updatedChallenge.overview);
+
+  // post bus event
+  logger.debug(
+    `Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(updatedChallenge)}`
+  );
+
+  enrichChallengeForResponse(updatedChallenge, track, type);
+
+  await helper.postBusEvent(constants.Topics.ChallengeUpdated, updatedChallenge, {
+    key:
+      updatedChallenge.status === "Completed"
+        ? `${updatedChallenge.id}:${updatedChallenge.status}`
+        : undefined,
+  });
+
+  const isLocal = process.env.LOCAL == "true";
+  if (!isLocal) {
+    // Update ES
+    await esClient.update({
+      index: config.get("ES.ES_INDEX"),
+      type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined,
+      refresh: config.get("ES.ES_REFRESH"),
+      id: challengeId,
+      body: {
+        doc: updatedChallenge,
+      },
+    });
+
+    if (updatedChallenge.legacy.selfService) {
+      const creator = await helper.getMemberByHandle(updatedChallenge.createdBy);
+      if (sendSubmittedEmail) {
+        await helper.sendSelfServiceNotification(
+          constants.SelfServiceNotificationTypes.WORK_REQUEST_SUBMITTED,
+          [{ email: creator.email }],
+          {
+            handle: creator.handle,
+            workItemName: updatedChallenge.name,
+          }
+        );
+      }
+      if (sendActivationEmail) {
+        await helper.sendSelfServiceNotification(
+          constants.SelfServiceNotificationTypes.WORK_REQUEST_STARTED,
+          [{ email: creator.email }],
+          {
+            handle: creator.handle,
+            workItemName: updatedChallenge.name,
+            workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}`,
+          }
+        );
+      }
+      if (sendCompletedEmail) {
+        await helper.sendSelfServiceNotification(
+          constants.SelfServiceNotificationTypes.WORK_COMPLETED,
+          [{ email: creator.email }],
+          {
+            handle: creator.handle,
+            workItemName: updatedChallenge.name,
+            workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}?tab=solutions`,
+          }
+        );
+      }
+      if (sendRejectedEmail || cancelReason) {
+        logger.debug("Should send redirected email");
+        await helper.sendSelfServiceNotification(
+          constants.SelfServiceNotificationTypes.WORK_REQUEST_REDIRECTED,
+          [{ email: creator.email }],
+          {
+            handle: creator.handle,
+            workItemName: updatedChallenge.name,
+          }
+        );
+      }
+    }
+  }
+
+  return updatedChallenge;
 }
 
-partiallyUpdateChallenge.schema = {
+updateChallenge.schema = {
   currentUser: Joi.any(),
   challengeId: Joi.id(),
   data: Joi.object()
@@ -2714,12 +1978,14 @@ partiallyUpdateChallenge.schema = {
           selfServiceCopilot: Joi.string().allow(null),
         })
         .unknown(true),
-      cancelReason: Joi.string(),
-      task: Joi.object().keys({
-        isTask: Joi.boolean().default(false),
-        isAssigned: Joi.boolean().default(false),
-        memberId: Joi.string().allow(null),
-      }),
+      cancelReason: Joi.string().optional(),
+      task: Joi.object()
+        .keys({
+          isTask: Joi.boolean().default(false),
+          isAssigned: Joi.boolean().default(false),
+          memberId: Joi.alternatives().try(Joi.string().allow(null), Joi.number().allow(null)),
+        })
+        .optional(),
       billing: Joi.object()
         .keys({
           billingAccountId: Joi.string(),
@@ -2728,10 +1994,10 @@ partiallyUpdateChallenge.schema = {
         .unknown(true),
       trackId: Joi.optionalId(),
       typeId: Joi.optionalId(),
-      name: Joi.string(),
-      description: Joi.string(),
-      privateDescription: Joi.string(),
-      descriptionFormat: Joi.string(),
+      name: Joi.string().optional(),
+      description: Joi.string().optional(),
+      privateDescription: Joi.string().allow("").optional(),
+      descriptionFormat: Joi.string().optional(),
       metadata: Joi.array()
         .items(
           Joi.object()
@@ -2742,7 +2008,7 @@ partiallyUpdateChallenge.schema = {
             .unknown(true)
         )
         .unique((a, b) => a.name === b.name),
-      timelineTemplateId: Joi.string(), // changing this to update migrated challenges
+      timelineTemplateId: Joi.string().optional(), // changing this to update migrated challenges
       phases: Joi.array()
         .items(
           Joi.object()
@@ -2765,7 +2031,8 @@ partiallyUpdateChallenge.schema = {
             })
             .unknown(true)
         )
-        .min(1),
+        .min(1)
+        .optional(),
       events: Joi.array().items(
         Joi.object()
           .keys({
@@ -2774,18 +2041,21 @@ partiallyUpdateChallenge.schema = {
             key: Joi.string(),
           })
           .unknown(true)
+          .optional()
       ),
-      discussions: Joi.array().items(
-        Joi.object().keys({
-          id: Joi.optionalId(),
-          name: Joi.string().required(),
-          type: Joi.string().required().valid(_.values(constants.DiscussionTypes)),
-          provider: Joi.string().required(),
-          url: Joi.string(),
-          options: Joi.array().items(Joi.object()),
-        })
-      ),
-      startDate: Joi.date(),
+      discussions: Joi.array()
+        .items(
+          Joi.object().keys({
+            id: Joi.optionalId(),
+            name: Joi.string().required(),
+            type: Joi.string().required().valid(_.values(constants.DiscussionTypes)),
+            provider: Joi.string().required(),
+            url: Joi.string(),
+            options: Joi.array().items(Joi.object()),
+          })
+        )
+        .optional(),
+      startDate: Joi.date().iso(),
       prizeSets: Joi.array()
         .items(
           Joi.object()
@@ -2820,7 +2090,7 @@ partiallyUpdateChallenge.schema = {
           description: Joi.string(),
         })
       ),
-      groups: Joi.array().items(Joi.id()), // group names
+      groups: Joi.array().items(Joi.optionalId()).unique(),
       // gitRepoURLs: Joi.array().items(Joi.string().uri()),
       winners: Joi.array()
         .items(
@@ -2835,14 +2105,157 @@ partiallyUpdateChallenge.schema = {
             })
             .unknown(true)
         )
-        .min(1),
-      terms: Joi.array().items(Joi.id().optional()).optional().allow([]),
+        .optional(),
+      terms: Joi.array().items(
+        Joi.object().keys({
+          id: Joi.id(),
+          roleId: Joi.id(),
+        })
+      ),
       overview: Joi.any().forbidden(),
     })
     .unknown(true)
     .required(),
 };
 
+/**
+ * Send notifications
+ * @param {Object} currentUser the current use
+ * @param {String} challengeId the challenge id
+ */
+async function sendNotifications(currentUser, challengeId) {
+  const challenge = await getChallenge(currentUser, challengeId);
+  const creator = await helper.getMemberByHandle(challenge.createdBy);
+  if (challenge.status === constants.challengeStatuses.Completed) {
+    await helper.sendSelfServiceNotification(
+      constants.SelfServiceNotificationTypes.WORK_COMPLETED,
+      [{ email: creator.email }],
+      {
+        handle: creator.handle,
+        workItemName: challenge.name,
+        workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}?tab=solutions`,
+      }
+    );
+    return { type: constants.SelfServiceNotificationTypes.WORK_COMPLETED };
+  }
+}
+
+sendNotifications.schema = {
+  currentUser: Joi.any(),
+  challengeId: Joi.id(),
+};
+
+/**
+ * Remove unwanted properties from the challenge object
+ * @param {Object} challenge the challenge object
+ */
+function sanitizeChallenge(challenge) {
+  const sanitized = _.pick(challenge, [
+    "trackId",
+    "typeId",
+    "name",
+    "description",
+    "privateDescription",
+    "descriptionFormat",
+    "timelineTemplateId",
+    "tags",
+    "projectId",
+    "legacyId",
+    "startDate",
+    "status",
+    "task",
+    "groups",
+    "cancelReason",
+  ]);
+  if (!_.isUndefined(sanitized.name)) {
+    sanitized.name = xss(sanitized.name);
+  }
+  if (!_.isUndefined(sanitized.description)) {
+    sanitized.description = xss(sanitized.description);
+  }
+  if (challenge.legacy) {
+    sanitized.legacy = _.pick(challenge.legacy, [
+      "track",
+      "subTrack",
+      "reviewType",
+      "confidentialityType",
+      "forumId",
+      "directProjectId",
+      "screeningScorecardId",
+      "reviewScorecardId",
+      "isTask",
+      "useSchedulingAPI",
+      "pureV5Task",
+      "pureV5",
+      "selfService",
+      "selfServiceCopilot",
+    ]);
+  }
+  if (challenge.billing) {
+    sanitized.billing = _.pick(challenge.billing, ["billingAccountId", "markup"]);
+  }
+  if (challenge.metadata) {
+    sanitized.metadata = _.map(challenge.metadata, (meta) => _.pick(meta, ["name", "value"]));
+  }
+  if (challenge.phases) {
+    sanitized.phases = _.map(challenge.phases, (phase) =>
+      _.pick(phase, ["phaseId", "duration", "scheduledStartDate", "constraints"])
+    );
+  }
+  if (challenge.prizeSets) {
+    sanitized.prizeSets = _.map(challenge.prizeSets, (prizeSet) => ({
+      ..._.pick(prizeSet, ["type", "description"]),
+      prizes: _.map(prizeSet.prizes, (prize) => _.pick(prize, ["description", "type", "value"])),
+    }));
+  }
+  if (challenge.events) {
+    sanitized.events = _.map(challenge.events, (event) => _.pick(event, ["id", "name", "key"]));
+  }
+  if (challenge.winners) {
+    sanitized.winners = _.map(challenge.winners, (winner) =>
+      _.pick(winner, ["userId", "handle", "placement", "type"])
+    );
+  }
+  if (challenge.discussions) {
+    sanitized.discussions = _.map(challenge.discussions, (discussion) => ({
+      ..._.pick(discussion, ["id", "provider", "name", "type", "url", "options"]),
+      name: _.get(discussion, "name", "").substring(0, config.FORUM_TITLE_LENGTH_LIMIT),
+    }));
+  }
+  if (challenge.terms) {
+    sanitized.terms = _.map(challenge.terms, (term) => _.pick(term, ["id", "roleId"]));
+  }
+  if (challenge.attachments) {
+    sanitized.attachments = _.map(challenge.attachments, (attachment) =>
+      _.pick(attachment, ["id", "name", "url", "fileSize", "description", "challengeId"])
+    );
+  }
+
+  return sanitized;
+}
+
+function sanitizeData(data, challenge) {
+  for (const key in data) {
+    if (key === "phases") continue;
+
+    if (challenge.hasOwnProperty(key)) {
+      if (
+        (typeof data[key] === "object" || Array.isArray(data[key])) &&
+        deepEqual(data[key], challenge[key])
+      ) {
+        delete data[key];
+      } else if (
+        typeof data[key] !== "object" &&
+        !Array.isArray(data[key]) &&
+        data[key] === challenge[key]
+      ) {
+        delete data[key];
+      }
+    }
+  }
+  return data;
+}
+
 /**
  * Delete challenge.
  * @param {Object} currentUser the user who perform operation
@@ -2883,10 +2296,10 @@ module.exports = {
   searchChallenges,
   createChallenge,
   getChallenge,
-  fullyUpdateChallenge,
-  partiallyUpdateChallenge,
+  updateChallenge,
   deleteChallenge,
   getChallengeStatistics,
   sendNotifications,
 };
+
 logger.buildService(module.exports);
diff --git a/src/services/ChallengeTimelineTemplateService.js b/src/services/ChallengeTimelineTemplateService.js
index 3f20a688..c0c1fd24 100644
--- a/src/services/ChallengeTimelineTemplateService.js
+++ b/src/services/ChallengeTimelineTemplateService.js
@@ -56,6 +56,8 @@ searchChallengeTimelineTemplates.schema = {
     trackId: Joi.optionalId(),
     timelineTemplateId: Joi.optionalId(),
     isDefault: Joi.boolean(),
+    page: Joi.page(),
+    perPage: Joi.perPage(),
   }),
 };
 
diff --git a/yarn.lock b/yarn.lock
index 302108a1..938144b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -125,18 +125,18 @@
     enabled "2.0.x"
     kuler "^2.0.0"
 
-"@grpc/grpc-js@^1.8.0":
-  version "1.8.12"
-  resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.12.tgz#bc0120859e8b153db764b473cc019ddf6bb2b414"
-  integrity sha512-MbUMvpVvakeKhdYux6gbSIPJaFMLNSY8jw4PqLI+FFztGrQRrYYAnHlR94+ncBQQewkpXQaW449m3tpH/B/ZnQ==
+"@grpc/grpc-js@^1.8.0", "@grpc/grpc-js@^1.8.12":
+  version "1.8.13"
+  resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.13.tgz#e775685962909b76f8d4b813833c3d123867165b"
+  integrity sha512-iY3jsdfbc0ARoCLFvbvUB8optgyb0r1XLPb142u+QtgBcKJYkCIFt3Fd/881KqjLYWjsBJF57N3b8Eop9NDfUA==
   dependencies:
     "@grpc/proto-loader" "^0.7.0"
     "@types/node" ">=12.12.47"
 
 "@grpc/proto-loader@^0.7.0":
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.5.tgz#ee9e7488fa585dc6b0f7fe88cd39723a3e64c906"
-  integrity sha512-mfcTuMbFowq1wh/Rn5KQl6qb95M21Prej3bewD9dUQMurYGVckGO/Pbe2Ocwto6sD05b/mxZLspvqwx60xO2Rg==
+  version "0.7.6"
+  resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.6.tgz#b71fdf92b184af184b668c4e9395a5ddc23d61de"
+  integrity sha512-QyAXR8Hyh7uMDmveWxDSUcJr9NAWaZ2I6IXgAYvQmfflwouTM+rArE2eEaCtLlRqO81j7pRLCt81IefUei6Zbw==
   dependencies:
     "@types/long" "^4.0.1"
     lodash.camelcase "^4.3.0"
@@ -245,35 +245,35 @@
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
   integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
 
-"@topcoder-framework/client-relational@^0.7.0":
-  version "0.7.0"
-  resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.7.0.tgz#bd219fb466ce2d436ca393b1f1bb4bdd0f05be80"
-  integrity sha512-AXkKyzmKfQap+eib9FehQZbZ7oAYGW+41gMXNFpxmqrZ0/TMgh8znnaw6uPmwyalVPh1bZdvxIGadCQxgi3jWw==
+"@topcoder-framework/client-relational@^0.10.13":
+  version "0.10.13"
+  resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.10.13.tgz#84293cd265328d5f770c28ffd690fbb434ac936b"
+  integrity sha512-p4ygOE0K2xrz/wmTSS5/3DX2lEH/bmiWsW+sL8RVstAhilWSQmdyJb49sI/QzbFqhHGS/aQnkKPt8gaNtIaVWQ==
   dependencies:
     "@grpc/grpc-js" "^1.8.0"
-    "@topcoder-framework/lib-common" "^0.7.0"
-    topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.29"
+    "@topcoder-framework/lib-common" "^0.10.13"
+    topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46"
     tslib "^2.4.1"
 
-"@topcoder-framework/domain-challenge@^0.7.0":
-  version "0.7.0"
-  resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.7.0.tgz#318acc9fb7cfdd1c837d69eee43f94ae17e30840"
-  integrity sha512-ekg2oplRLc0UXxzHzm3Eb6YX4iWqJkcg0Nye6g+k93vkGiWZaccW4cujBKZq/39JVx6+Sc1uiAlLbi6gbRY7Jg==
+"@topcoder-framework/domain-challenge@^0.10.13":
+  version "0.10.13"
+  resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.10.13.tgz#dede4cd01054e56eb4e4486eeb99cfd9ab4d75f1"
+  integrity sha512-srkncIcHaD1aGYD6DSHGzZDORjPZkTN9qNgZSNNYXx3Q6pNc4z3dUQqv79bEv472af4zkXmemMcmHqPTRilVtQ==
   dependencies:
     "@grpc/grpc-js" "^1.8.0"
-    "@topcoder-framework/client-relational" "^0.7.0"
-    "@topcoder-framework/lib-common" "^0.7.0"
-    topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.29"
+    "@topcoder-framework/client-relational" "^0.10.13"
+    "@topcoder-framework/lib-common" "^0.10.13"
+    topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46"
     tslib "^2.4.1"
 
-"@topcoder-framework/lib-common@^0.7.0":
-  version "0.7.0"
-  resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.7.0.tgz#557900413fe2e0b67d233f04c63db2e81eac5dbc"
-  integrity sha512-3qjcRYGHqRiBWPbOM2C/BwpZEswIqCbc+scskIHmtY/FYn52lTT1w7Cm/KOcgBpE3S/mmWq0YwtZKNNzbRwglA==
+"@topcoder-framework/lib-common@^0.10.13":
+  version "0.10.13"
+  resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.10.13.tgz#69a0c70d601cc37821ece1b13d300dbe8e6ddc10"
+  integrity sha512-LXaoLQma+7cs7ly6McXmhO3YWNF27MzqiR3fgtlefVU1XbfVfWhSfDLitTUSw08PMgv+VC6nTfyo0t4202ZVcg==
   dependencies:
     "@grpc/grpc-js" "^1.8.0"
     rimraf "^3.0.2"
-    topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.29"
+    topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46"
     tslib "^2.4.1"
 
 "@types/body-parser@*":
@@ -346,9 +346,9 @@
   integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
 
 "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
-  version "18.15.3"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
-  integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==
+  version "18.15.6"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.6.tgz#af98ef4a36e7ac5f2d03040f3109fcce972bf6cb"
+  integrity sha512-YErOafCZpK4g+Rp3Q/PBgZNAsWKGunQTm9FA3/Pbcm0VCriTEzcrutQ/SxSc0rytAp0NoFWue669jmKhEtd0sA==
 
 "@types/node@11.11.0":
   version "11.11.0"
@@ -406,6 +406,13 @@ agent-base@6:
   dependencies:
     debug "4"
 
+agentkeepalive@^3.4.1:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67"
+  integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==
+  dependencies:
+    humanize-ms "^1.2.1"
+
 ajv@^6.12.3:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -421,6 +428,11 @@ ansi-colors@3.2.3:
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813"
   integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==
 
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==
+
 ansi-regex@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
@@ -436,6 +448,11 @@ ansi-regex@^5.0.1:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
+ansi-styles@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+  integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==
+
 ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -549,9 +566,9 @@ aws-sdk@2.395.0:
     xml2js "0.4.19"
 
 aws-sdk@^2.1145.0:
-  version "2.1338.0"
-  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1338.0.tgz#e9272a0563940ceebed5910d271c944f31dbae00"
-  integrity sha512-apxv53ABuvi87UQHAUqRrJOaGNMiPXAe6bizzJhOnsaNqasg2KjDDit7QSCi6HlLNG44n1ApIvMtR/k+NnxU4Q==
+  version "2.1342.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1342.0.tgz#2ddb60e7480b6f3a3b1ec5cfba4c6beed7cfc024"
+  integrity sha512-RknStRPY+ohgOhuuDYEkAWuBcU9841EjtelZn4J2VubhaS7ZFQ2lmiYqm4P5Tw8Kwq6GuUqISBB8RCp8cO2qfA==
   dependencies:
     buffer "4.9.2"
     events "1.1.1"
@@ -792,6 +809,17 @@ chai@^4.2.0:
     pathval "^1.1.1"
     type-detect "^4.0.5"
 
+chalk@^1.0.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+  integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==
+  dependencies:
+    ansi-styles "^2.2.1"
+    escape-string-regexp "^1.0.2"
+    has-ansi "^2.0.0"
+    strip-ansi "^3.0.0"
+    supports-color "^2.0.0"
+
 chalk@^2.0.0, chalk@^2.0.1:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -1078,6 +1106,29 @@ deep-equal@1.0.1:
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
   integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==
 
+deep-equal@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6"
+  integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==
+  dependencies:
+    call-bind "^1.0.2"
+    es-get-iterator "^1.1.2"
+    get-intrinsic "^1.1.3"
+    is-arguments "^1.1.1"
+    is-array-buffer "^3.0.1"
+    is-date-object "^1.0.5"
+    is-regex "^1.1.4"
+    is-shared-array-buffer "^1.0.2"
+    isarray "^2.0.5"
+    object-is "^1.1.5"
+    object-keys "^1.1.1"
+    object.assign "^4.1.4"
+    regexp.prototype.flags "^1.4.3"
+    side-channel "^1.0.4"
+    which-boxed-primitive "^1.0.2"
+    which-collection "^1.0.1"
+    which-typed-array "^1.1.9"
+
 default-require-extensions@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"
@@ -1158,6 +1209,15 @@ ee-first@1.1.1:
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
 
+elasticsearch@^16.7.3:
+  version "16.7.3"
+  resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-16.7.3.tgz#bf0e1cc129ab2e0f06911953a1b1f3c740715fab"
+  integrity sha512-e9kUNhwnIlu47fGAr4W6yZJbkpsgQJB0TqNK8rCANe1J4P65B1sGnbCFTgcKY3/dRgCWnuP1AJ4obvzW604xEQ==
+  dependencies:
+    agentkeepalive "^3.4.1"
+    chalk "^1.0.0"
+    lodash "^4.17.10"
+
 emoji-regex@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -1230,6 +1290,21 @@ es-array-method-boxes-properly@^1.0.0:
   resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
   integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
 
+es-get-iterator@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
+  integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.3"
+    has-symbols "^1.0.3"
+    is-arguments "^1.1.1"
+    is-map "^2.0.2"
+    is-set "^2.0.2"
+    is-string "^1.0.7"
+    isarray "^2.0.5"
+    stop-iteration-iterator "^1.0.0"
+
 es-set-tostringtag@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8"
@@ -1263,7 +1338,7 @@ escape-html@~1.0.3:
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
 
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
@@ -1635,6 +1710,13 @@ har-validator@~5.1.3:
     ajv "^6.12.3"
     har-schema "^2.0.0"
 
+has-ansi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+  integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==
+  dependencies:
+    ansi-regex "^2.0.0"
+
 has-bigints@^1.0.1, has-bigints@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@@ -1718,6 +1800,11 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
+http-aws-es@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/http-aws-es/-/http-aws-es-6.0.0.tgz#1528978d2bee718b8732dcdced0856efa747aeff"
+  integrity sha512-g+qp7J110/m4aHrR3iit4akAlnW0UljZ6oTq/rCcbsI8KP9x+95vqUtx49M2XQ2JMpwJio3B6gDYx+E8WDxqiA==
+
 http-errors@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
@@ -1760,6 +1847,13 @@ https-proxy-agent@^5.0.0:
     agent-base "6"
     debug "4"
 
+humanize-ms@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+  integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
+  dependencies:
+    ms "^2.0.0"
+
 iconv-lite@0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -1805,7 +1899,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-internal-slot@^1.0.5:
+internal-slot@^1.0.4, internal-slot@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986"
   integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==
@@ -1824,7 +1918,7 @@ ipaddr.js@1.9.1:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
-is-arguments@^1.0.4:
+is-arguments@^1.0.4, is-arguments@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
   integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
@@ -1890,7 +1984,7 @@ is-core-module@^2.9.0:
   dependencies:
     has "^1.0.3"
 
-is-date-object@^1.0.1:
+is-date-object@^1.0.1, is-date-object@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
   integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
@@ -1933,6 +2027,11 @@ is-ip@^2.0.0:
   dependencies:
     ip-regex "^2.0.0"
 
+is-map@^2.0.1, is-map@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
+  integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
+
 is-negative-zero@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@@ -1963,6 +2062,11 @@ is-retry-allowed@^2.2.0:
   resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d"
   integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==
 
+is-set@^2.0.1, is-set@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
+  integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
+
 is-shared-array-buffer@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
@@ -2010,6 +2114,11 @@ is-typedarray@~1.0.0:
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
 
+is-weakmap@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
+  integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
+
 is-weakref@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
@@ -2017,11 +2126,24 @@ is-weakref@^1.0.2:
   dependencies:
     call-bind "^1.0.2"
 
+is-weakset@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d"
+  integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.1"
+
 isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
 
+isarray@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
 isemail@3.x.x:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c"
@@ -2317,7 +2439,7 @@ lodash@4.17.15:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
-lodash@^4.17.15, lodash@^4.17.19:
+lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -2521,7 +2643,7 @@ ms@2.1.2:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@2.1.3, ms@^2.1.1, ms@^2.1.2, ms@^2.1.3:
+ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.2, ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -2571,9 +2693,9 @@ node-environment-flags@1.0.5:
     semver "^5.7.0"
 
 nodemon@^2.0.20:
-  version "2.0.21"
-  resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.21.tgz#267edff25578da91075d6aa54346ef77ecb7b302"
-  integrity sha512-djN/n2549DUtY33S7o1djRCd7dEm0kBnj9c7S9XVXqRUbuggN1MZH/Nqa+5RFQr63Fbefq37nFXAE9VU86yL1A==
+  version "2.0.22"
+  resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258"
+  integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==
   dependencies:
     chokidar "^3.5.2"
     debug "^3.2.7"
@@ -2654,6 +2776,14 @@ object-inspect@^1.12.3, object-inspect@^1.9.0:
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
   integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
 
+object-is@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
+  integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
 object-keys@^1.0.11, object-keys@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@@ -2827,9 +2957,9 @@ precond@0.2:
   integrity sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==
 
 prettier@^2.8.1:
-  version "2.8.4"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
-  integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
+  version "2.8.6"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.6.tgz#5c174b29befd507f14b83e3c19f83fdc0e974b71"
+  integrity sha512-mtuzdiBbHwPEgl7NxWlqOkithPyp4VN93V7VeHVWBF+ad3I5avc0RVDT4oImXQy9H/AqxA2NSQH8pSxHW6FYbQ==
 
 process-nextick-args@~2.0.0:
   version "2.0.1"
@@ -3317,6 +3447,13 @@ statuses@2.0.1:
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
   integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
 
+stop-iteration-iterator@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
+  integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==
+  dependencies:
+    internal-slot "^1.0.4"
+
 streamsearch@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
@@ -3389,6 +3526,13 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
+strip-ansi@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
+  dependencies:
+    ansi-regex "^2.0.0"
+
 strip-ansi@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
@@ -3443,6 +3587,11 @@ supports-color@6.0.0:
   dependencies:
     has-flag "^3.0.0"
 
+supports-color@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+  integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==
+
 supports-color@^5.3.0, supports-color@^5.5.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -3528,9 +3677,9 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git:
     superagent "^3.8.3"
     tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4
 
-"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.29":
+"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.46":
   version "1.0.0"
-  resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/6ad366c0dc28a8452bd71ed87d718ac559bee62b"
+  resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8ed5b7686125a17209c85c33f69c92476625e3c1"
 
 topo@3.x.x:
   version "3.0.3"
@@ -3702,6 +3851,16 @@ which-boxed-primitive@^1.0.2:
     is-string "^1.0.5"
     is-symbol "^1.0.3"
 
+which-collection@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
+  integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
+  dependencies:
+    is-map "^2.0.1"
+    is-set "^2.0.1"
+    is-weakmap "^2.0.1"
+    is-weakset "^2.0.1"
+
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"