From bb7896edf2c8d879c6198a71b74105ec6590a3cc Mon Sep 17 00:00:00 2001
From: Rakib Ansary <rakibansary@topcoder.com>
Date: Wed, 10 Apr 2024 17:29:55 +0600
Subject: [PATCH 1/4] fix: add endpoint to update legacy payment records

* this endpiont allows wallet a way to keep legacy payment tables
* in sync with edits that happen in wallet. Since legacy payment
* tables are not in any actual use - the only purpose this serves
* is keeping data in sync so looker is upto date - there is no other
* impact to using this endpoint

Signed-off-by: Rakib Ansary <rakibansary@topcoder.com>
---
 config/default.js                      |   6 +-
 src/controllers/ChallengeController.js |  21 ++++
 src/routes.js                          |  10 +-
 src/services/ChallengeService.js       | 134 +++++++++++++++++++++++++
 4 files changed, 168 insertions(+), 3 deletions(-)

diff --git a/config/default.js b/config/default.js
index e23770d8..4727e58e 100644
--- a/config/default.js
+++ b/config/default.js
@@ -92,6 +92,7 @@ module.exports = {
     UPDATE: process.env.SCOPE_CHALLENGES_UPDATE || "update:challenges",
     DELETE: process.env.SCOPE_CHALLENGES_DELETE || "delete:challenges",
     ALL: process.env.SCOPE_CHALLENGES_ALL || "all:challenges",
+    PAYMENT: process.env.SCOPE_PAYMENT || "create:payments",
   },
 
   DEFAULT_CONFIDENTIALITY_TYPE: process.env.DEFAULT_CONFIDENTIALITY_TYPE || "public",
@@ -129,7 +130,8 @@ module.exports = {
   GRPC_CHALLENGE_SERVER_HOST: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_HOST || "localhost",
   GRPC_CHALLENGE_SERVER_PORT: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_PORT || 8888,
   GRPC_ACL_SERVER_HOST: process.env.GRPC_ACL_SERVER_HOST || "localhost",
-  GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_PORT || 8889,
+  GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_PORT || 40020,
 
-  SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID: process.env.SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID || '517e76b0-8824-4e72-9b48-a1ebde1793a8'
+  SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID:
+    process.env.SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID || "517e76b0-8824-4e72-9b48-a1ebde1793a8",
 };
diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js
index 38768dc1..2d726578 100644
--- a/src/controllers/ChallengeController.js
+++ b/src/controllers/ChallengeController.js
@@ -104,6 +104,26 @@ async function updateChallenge(req, res) {
   res.send(result);
 }
 
+/**
+ * Update Legacy Payout (Updates informixoltp:payment_detail)
+ * This has no effect other than to keep DW in sync for looker with
+ * Updates that happen in Wallet
+ */
+async function updateLegacyPayout(req, res) {
+  logger.debug(
+    `updateLegacyPayout User: ${JSON.stringify(req.authUser)} - ChallengeID: ${
+      req.params.challengeId
+    } - Body: ${JSON.stringify(req.body)}`
+  );
+  const result = await service.updateLegacyPayout(
+    req,
+    req.authUser,
+    req.params.challengeId,
+    req.body
+  );
+  res.send(result);
+}
+
 /**
  * Delete challenge
  * @param {Object} req the request
@@ -152,6 +172,7 @@ module.exports = {
   createChallenge,
   getChallenge,
   updateChallenge,
+  updateLegacyPayout,
   deleteChallenge,
   getChallengeStatistics,
   sendNotifications,
diff --git a/src/routes.js b/src/routes.js
index 8d46e69d..8adb7d2a 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -4,7 +4,7 @@
 
 const constants = require("../app-constants");
 const {
-  SCOPES: { READ, CREATE, UPDATE, DELETE, ALL },
+  SCOPES: { PAYMENT, READ, CREATE, UPDATE, DELETE, ALL },
 } = require("config");
 
 module.exports = {
@@ -112,6 +112,14 @@ module.exports = {
       scopes: [UPDATE, ALL],
     },
   },
+  "/challenges/:challengeId/legacy-payment": {
+    patch: {
+      controller: "ChallengeController",
+      method: "updateLegacyPayout",
+      auth: "jwt",
+      scopes: [PAYMENT],
+    },
+  },
   "/challenges/:challengeId/statistics": {
     get: {
       controller: "ChallengeController",
diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js
index 46a30f7c..d0d9ef24 100644
--- a/src/services/ChallengeService.js
+++ b/src/services/ChallengeService.js
@@ -2172,6 +2172,13 @@ updateChallenge.schema = {
         )
         .optional(),
       overview: Joi.any().forbidden(),
+      v5Payout: Joi.object().keys({
+        userId: Joi.number().integer().positive().required(),
+        amount: Joi.number().allow(null),
+        status: Joi.string().allow(null),
+        datePaid: Joi.string().allow(null),
+        releaseDate: Joi.string().allow(null),
+      }),
     })
     .unknown(true)
     .required(),
@@ -2498,6 +2505,132 @@ async function indexChallengeAndPostToKafka(updatedChallenge, track, type) {
   });
 }
 
+async function updateLegacyPayout(currentUser, challengeId, data) {
+  const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId));
+  const { v5Payout } = data;
+
+  // SQL qurey to fetch the payment and payment_detail record
+  let sql = `SELECT * FROM informixoltp:payment p
+    INNER JOIN informixoltp:payment_detail pd ON p.most_recent_detail_id = pd.payment_detail_id
+    WHERE p.user_id = ${v5Payout.userId} AND`;
+
+  if (challenge.legacyId != null) {
+    sql += ` pd.component_project_id = ${challenge.legacyId}`;
+  } else {
+    sql += ` pd.jira_issue_id = \'${challengeId}\'`;
+  }
+
+  sql += " ORDER BY pd.payment_detail_id ASC";
+
+  console.log("Fetch legacy payment detail: ", sql);
+
+  const result = await aclQueryDomain.rawQuery({ sql });
+  let updateClauses = [`date_modified = current`];
+
+  const statusMap = {
+    Paid: 53,
+    OnHold: 55,
+    OnHoldAdmin: 55,
+    Owed: 56,
+    Cancelled: 65,
+    EnteredIntoPaymentSystem: 70,
+  };
+
+  if (v5Payout.status != null) {
+    updateClauses.push(`payment_status_id = ${statusMap[v5Payout.status]}`);
+    if (v5Payout.status === "Paid") {
+      updateClauses.push(`date_paid = '${v5Payout.datePaid}'`);
+    } else {
+      updateClauses.push("date_paid = null");
+    }
+  }
+
+  if (v5Payout.releaseDate != null) {
+    updateClauses.push(`date_due = '${v5Payout.releaseDate}'`);
+  }
+
+  const paymentDetailIds = result.rows.map(
+    (row) => row.fields.find((field) => field.key === "payment_detail_id").value
+  );
+
+  if (v5Payout.amount != null) {
+    updateClauses.push(`total_amount = ${v5Payout.amount}`);
+    if (paymentDetailIds.length === 1) {
+      updateClauses.push(`net_amount = ${v5Payout.amount}`);
+      updateClauses.push(`gross_amount = ${v5Payout.amount}`);
+    }
+  }
+
+  if (paymentDetailIds.length === 0) {
+    return {
+      success: false,
+      message: "No payment detail record found",
+    };
+  }
+
+  const whereClause = [`payment_detail_id IN (${paymentDetailIds.join(",")})`];
+
+  const updateQuery = `UPDATE informixoltp:payment_detail SET ${updateClauses.join(
+    ", "
+  )} WHERE ${whereClause.join(" AND ")}`;
+
+  console.log("Update Clauses", updateClauses);
+  console.log("Update Query", updateQuery);
+
+  await aclQueryDomain.rawQuery({ sql: updateQuery });
+
+  if (v5Payout.amount != null) {
+    if (paymentDetailIds.length > 1) {
+      const amountInCents = v5Payout.amount * 100;
+
+      const split1Cents = Math.round(amountInCents * 0.75);
+      const split2Cents = amountInCents - split1Cents;
+
+      const split1Dollars = Number((split1Cents / 100).toFixed(2));
+      const split2Dollars = Number((split2Cents / 100).toFixed(2));
+
+      const paymentUpdateQueries = paymentDetailIds.map((paymentDetailId, index) => {
+        let amt = 0;
+        if (index === 0) {
+          amt = split1Dollars;
+        }
+        if (index === 1) {
+          amt = split2Dollars;
+        }
+
+        return `UPDATE informixoltp:payment_detail SET date_modified = CURRENT, net_amount = ${amt}, gross_amount = ${amt} WHERE payment_detail_id = ${paymentDetailId}`;
+      });
+
+      console.log("Payment Update Queries", paymentUpdateQueries);
+
+      await Promise.all(
+        paymentUpdateQueries.map((query) => aclQueryDomain.rawQuery({ sql: query }))
+      );
+    }
+  }
+
+  return {
+    success: true,
+    message: "Successfully updated legacy payout",
+  };
+}
+updateLegacyPayout.schema = {
+  currentUser: Joi.any(),
+  challengeId: Joi.id(),
+  data: Joi.object()
+    .keys({
+      v5Payout: Joi.object().keys({
+        userId: Joi.number().integer().positive().required(),
+        amount: Joi.number().allow(null),
+        status: Joi.string().allow(null),
+        datePaid: Joi.string().allow(null),
+        releaseDate: Joi.string().allow(null),
+      }),
+    })
+    .unknown(true)
+    .required(),
+};
+
 /**
  * Get SRM Schedule
  * @param {Object} criteria the criteria
@@ -2562,6 +2695,7 @@ module.exports = {
   getChallenge,
   updateChallenge,
   deleteChallenge,
+  updateLegacyPayout,
   getChallengeStatistics,
   sendNotifications,
   advancePhase,

From be1309946efc02953ca4358c749bac939952cc64 Mon Sep 17 00:00:00 2001
From: Kiril Kartunov <kkartunov@users.noreply.github.com>
Date: Wed, 10 Apr 2024 19:18:25 +0300
Subject: [PATCH 2/4] Revert "fix: add endpoint to update legacy payment
 records"

---
 config/default.js                      |   6 +-
 src/controllers/ChallengeController.js |  21 ----
 src/routes.js                          |  10 +-
 src/services/ChallengeService.js       | 134 -------------------------
 4 files changed, 3 insertions(+), 168 deletions(-)

diff --git a/config/default.js b/config/default.js
index 4727e58e..e23770d8 100644
--- a/config/default.js
+++ b/config/default.js
@@ -92,7 +92,6 @@ module.exports = {
     UPDATE: process.env.SCOPE_CHALLENGES_UPDATE || "update:challenges",
     DELETE: process.env.SCOPE_CHALLENGES_DELETE || "delete:challenges",
     ALL: process.env.SCOPE_CHALLENGES_ALL || "all:challenges",
-    PAYMENT: process.env.SCOPE_PAYMENT || "create:payments",
   },
 
   DEFAULT_CONFIDENTIALITY_TYPE: process.env.DEFAULT_CONFIDENTIALITY_TYPE || "public",
@@ -130,8 +129,7 @@ module.exports = {
   GRPC_CHALLENGE_SERVER_HOST: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_HOST || "localhost",
   GRPC_CHALLENGE_SERVER_PORT: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_PORT || 8888,
   GRPC_ACL_SERVER_HOST: process.env.GRPC_ACL_SERVER_HOST || "localhost",
-  GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_PORT || 40020,
+  GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_PORT || 8889,
 
-  SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID:
-    process.env.SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID || "517e76b0-8824-4e72-9b48-a1ebde1793a8",
+  SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID: process.env.SKIP_PROJECT_ID_BY_TIMLINE_TEMPLATE_ID || '517e76b0-8824-4e72-9b48-a1ebde1793a8'
 };
diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js
index 2d726578..38768dc1 100644
--- a/src/controllers/ChallengeController.js
+++ b/src/controllers/ChallengeController.js
@@ -104,26 +104,6 @@ async function updateChallenge(req, res) {
   res.send(result);
 }
 
-/**
- * Update Legacy Payout (Updates informixoltp:payment_detail)
- * This has no effect other than to keep DW in sync for looker with
- * Updates that happen in Wallet
- */
-async function updateLegacyPayout(req, res) {
-  logger.debug(
-    `updateLegacyPayout User: ${JSON.stringify(req.authUser)} - ChallengeID: ${
-      req.params.challengeId
-    } - Body: ${JSON.stringify(req.body)}`
-  );
-  const result = await service.updateLegacyPayout(
-    req,
-    req.authUser,
-    req.params.challengeId,
-    req.body
-  );
-  res.send(result);
-}
-
 /**
  * Delete challenge
  * @param {Object} req the request
@@ -172,7 +152,6 @@ module.exports = {
   createChallenge,
   getChallenge,
   updateChallenge,
-  updateLegacyPayout,
   deleteChallenge,
   getChallengeStatistics,
   sendNotifications,
diff --git a/src/routes.js b/src/routes.js
index 8adb7d2a..8d46e69d 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -4,7 +4,7 @@
 
 const constants = require("../app-constants");
 const {
-  SCOPES: { PAYMENT, READ, CREATE, UPDATE, DELETE, ALL },
+  SCOPES: { READ, CREATE, UPDATE, DELETE, ALL },
 } = require("config");
 
 module.exports = {
@@ -112,14 +112,6 @@ module.exports = {
       scopes: [UPDATE, ALL],
     },
   },
-  "/challenges/:challengeId/legacy-payment": {
-    patch: {
-      controller: "ChallengeController",
-      method: "updateLegacyPayout",
-      auth: "jwt",
-      scopes: [PAYMENT],
-    },
-  },
   "/challenges/:challengeId/statistics": {
     get: {
       controller: "ChallengeController",
diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js
index d0d9ef24..46a30f7c 100644
--- a/src/services/ChallengeService.js
+++ b/src/services/ChallengeService.js
@@ -2172,13 +2172,6 @@ updateChallenge.schema = {
         )
         .optional(),
       overview: Joi.any().forbidden(),
-      v5Payout: Joi.object().keys({
-        userId: Joi.number().integer().positive().required(),
-        amount: Joi.number().allow(null),
-        status: Joi.string().allow(null),
-        datePaid: Joi.string().allow(null),
-        releaseDate: Joi.string().allow(null),
-      }),
     })
     .unknown(true)
     .required(),
@@ -2505,132 +2498,6 @@ async function indexChallengeAndPostToKafka(updatedChallenge, track, type) {
   });
 }
 
-async function updateLegacyPayout(currentUser, challengeId, data) {
-  const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId));
-  const { v5Payout } = data;
-
-  // SQL qurey to fetch the payment and payment_detail record
-  let sql = `SELECT * FROM informixoltp:payment p
-    INNER JOIN informixoltp:payment_detail pd ON p.most_recent_detail_id = pd.payment_detail_id
-    WHERE p.user_id = ${v5Payout.userId} AND`;
-
-  if (challenge.legacyId != null) {
-    sql += ` pd.component_project_id = ${challenge.legacyId}`;
-  } else {
-    sql += ` pd.jira_issue_id = \'${challengeId}\'`;
-  }
-
-  sql += " ORDER BY pd.payment_detail_id ASC";
-
-  console.log("Fetch legacy payment detail: ", sql);
-
-  const result = await aclQueryDomain.rawQuery({ sql });
-  let updateClauses = [`date_modified = current`];
-
-  const statusMap = {
-    Paid: 53,
-    OnHold: 55,
-    OnHoldAdmin: 55,
-    Owed: 56,
-    Cancelled: 65,
-    EnteredIntoPaymentSystem: 70,
-  };
-
-  if (v5Payout.status != null) {
-    updateClauses.push(`payment_status_id = ${statusMap[v5Payout.status]}`);
-    if (v5Payout.status === "Paid") {
-      updateClauses.push(`date_paid = '${v5Payout.datePaid}'`);
-    } else {
-      updateClauses.push("date_paid = null");
-    }
-  }
-
-  if (v5Payout.releaseDate != null) {
-    updateClauses.push(`date_due = '${v5Payout.releaseDate}'`);
-  }
-
-  const paymentDetailIds = result.rows.map(
-    (row) => row.fields.find((field) => field.key === "payment_detail_id").value
-  );
-
-  if (v5Payout.amount != null) {
-    updateClauses.push(`total_amount = ${v5Payout.amount}`);
-    if (paymentDetailIds.length === 1) {
-      updateClauses.push(`net_amount = ${v5Payout.amount}`);
-      updateClauses.push(`gross_amount = ${v5Payout.amount}`);
-    }
-  }
-
-  if (paymentDetailIds.length === 0) {
-    return {
-      success: false,
-      message: "No payment detail record found",
-    };
-  }
-
-  const whereClause = [`payment_detail_id IN (${paymentDetailIds.join(",")})`];
-
-  const updateQuery = `UPDATE informixoltp:payment_detail SET ${updateClauses.join(
-    ", "
-  )} WHERE ${whereClause.join(" AND ")}`;
-
-  console.log("Update Clauses", updateClauses);
-  console.log("Update Query", updateQuery);
-
-  await aclQueryDomain.rawQuery({ sql: updateQuery });
-
-  if (v5Payout.amount != null) {
-    if (paymentDetailIds.length > 1) {
-      const amountInCents = v5Payout.amount * 100;
-
-      const split1Cents = Math.round(amountInCents * 0.75);
-      const split2Cents = amountInCents - split1Cents;
-
-      const split1Dollars = Number((split1Cents / 100).toFixed(2));
-      const split2Dollars = Number((split2Cents / 100).toFixed(2));
-
-      const paymentUpdateQueries = paymentDetailIds.map((paymentDetailId, index) => {
-        let amt = 0;
-        if (index === 0) {
-          amt = split1Dollars;
-        }
-        if (index === 1) {
-          amt = split2Dollars;
-        }
-
-        return `UPDATE informixoltp:payment_detail SET date_modified = CURRENT, net_amount = ${amt}, gross_amount = ${amt} WHERE payment_detail_id = ${paymentDetailId}`;
-      });
-
-      console.log("Payment Update Queries", paymentUpdateQueries);
-
-      await Promise.all(
-        paymentUpdateQueries.map((query) => aclQueryDomain.rawQuery({ sql: query }))
-      );
-    }
-  }
-
-  return {
-    success: true,
-    message: "Successfully updated legacy payout",
-  };
-}
-updateLegacyPayout.schema = {
-  currentUser: Joi.any(),
-  challengeId: Joi.id(),
-  data: Joi.object()
-    .keys({
-      v5Payout: Joi.object().keys({
-        userId: Joi.number().integer().positive().required(),
-        amount: Joi.number().allow(null),
-        status: Joi.string().allow(null),
-        datePaid: Joi.string().allow(null),
-        releaseDate: Joi.string().allow(null),
-      }),
-    })
-    .unknown(true)
-    .required(),
-};
-
 /**
  * Get SRM Schedule
  * @param {Object} criteria the criteria
@@ -2695,7 +2562,6 @@ module.exports = {
   getChallenge,
   updateChallenge,
   deleteChallenge,
-  updateLegacyPayout,
   getChallengeStatistics,
   sendNotifications,
   advancePhase,

From 321bc4db037c86296c4ee46e27506cef7c67bd11 Mon Sep 17 00:00:00 2001
From: Justin Gasper <jmgasper@gmail.com>
Date: Thu, 11 Apr 2024 07:48:33 +1000
Subject: [PATCH 3/4] Better 401/403 debugging

---
 app-routes.js | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/app-routes.js b/app-routes.js
index 4a943233..73bbc45f 100644
--- a/app-routes.js
+++ b/app-routes.js
@@ -53,7 +53,9 @@ module.exports = (app) => {
           if (req.authUser.isMachine) {
             // M2M
             if (!req.authUser.scopes || !helper.checkIfExists(def.scopes, req.authUser.scopes)) {
-              next(new errors.ForbiddenError("You are not allowed to perform this action!"));
+              next(new errors.ForbiddenError(`You are not allowed to perform this action, because the scopes are incorrect. \
+                                              Required scopes: ${JSON.stringify(def.scopes)} \
+                                              Provided scopes: ${JSON.stringify(req.authUser.scopes)}`));
             } else {
               req.authUser.handle = config.M2M_AUDIT_HANDLE;
               req.authUser.userId = config.M2M_AUDIT_USERID;
@@ -71,14 +73,17 @@ module.exports = (app) => {
                   _.map(req.authUser.roles, (r) => r.toLowerCase())
                 )
               ) {
-                next(new errors.ForbiddenError("You are not allowed to perform this action!"));
+                next(new errors.ForbiddenError(`You are not allowed to perform this action, because the roles are incorrect. \
+                                                Required scopes: ${JSON.stringify(def.access)} \
+                                                Provided scopes: ${JSON.stringify(req.authUser.roles)}`));
               } else {
                 // user token is used in create/update challenge to ensure user can create/update challenge under specific project
                 req.userToken = req.headers.authorization.split(" ")[1];
                 next();
               }
             } else {
-              next(new errors.ForbiddenError("You are not authorized to perform this action"));
+              next(new errors.ForbiddenError("You are not authorized to perform this action, \
+                                             because no roles were provided"));
             }
           }
         });

From ae5284c96848cff16de8320df5c3f243a4a2a5f9 Mon Sep 17 00:00:00 2001
From: Justin Gasper <jmgasper@gmail.com>
Date: Thu, 11 Apr 2024 07:58:43 +1000
Subject: [PATCH 4/4] Typo

---
 app-routes.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app-routes.js b/app-routes.js
index 73bbc45f..824b04d0 100644
--- a/app-routes.js
+++ b/app-routes.js
@@ -74,8 +74,8 @@ module.exports = (app) => {
                 )
               ) {
                 next(new errors.ForbiddenError(`You are not allowed to perform this action, because the roles are incorrect. \
-                                                Required scopes: ${JSON.stringify(def.access)} \
-                                                Provided scopes: ${JSON.stringify(req.authUser.roles)}`));
+                                                Required roles: ${JSON.stringify(def.access)} \
+                                                Provided roles: ${JSON.stringify(req.authUser.roles)}`));
               } else {
                 // user token is used in create/update challenge to ensure user can create/update challenge under specific project
                 req.userToken = req.headers.authorization.split(" ")[1];