diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index bcd07c5a..3db3956c 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -379,3 +379,49 @@ Validation: Follow-ups: - Optional next big slice: bring remaining proxy/message routes using raw axios promise chains (`createcaseinvolvement_api.js`, `createrepinvolvement_api.js`, `updatecase_api.js`) onto the same async/await + explicit contract pattern. + +--- + +### CL-010: TASK22224 involvement/update route async contract hardening bundle + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/{createcaseinvolvement_api,createrepinvolvement_api,updatecase_api}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Execute the next requested slice by modernizing remaining relay-backed involvement/update handlers that still used axios promise chains and legacy dead imports. +impact: Improves reliability/readability and preserves existing response contracts, including 412 "record exists" semantics for involvement creation flows. +status: completed + +Summary: + +- `createcaseinvolvement_api.js` + - removed unused `CryptoJS` import + - refactored axios `.then/.catch` to explicit `try/catch` + - preserved conflict behavior: status 412 -> success `{ record: "exists" }` + - preserved failure contract: `CREATE_CASE_INVOLVEMENT_FAILED` +- `createrepinvolvement_api.js` + - removed unused `CryptoJS` import + - refactored axios `.then/.catch` to explicit `try/catch` + - preserved conflict behavior: status 412 -> success `{ record: "exists" }` + - preserved failure contract: `CREATE_REP_INVOLVEMENT_FAILED` +- `updatecase_api.js` + - removed unused `CryptoJS` import + - refactored axios `.then/.catch` to explicit `try/catch` + - preserved failure contract: `UPDATE_CASE_FAILED` +- Phase21 tests expanded: + - createcaseinvolvement 412 conflict success contract + - createrepinvolvement dependency failure contract + - updatecase dependency failure contract + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (49/49) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 49/49 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional: apply equivalent modernization to any remaining relay-backed handlers outside `pages/api/file/` that still use raw axios promise chains and have no explicit phase21 contract assertions. diff --git a/pages/api/file/createcaseinvolvement_api.js b/pages/api/file/createcaseinvolvement_api.js index af6b7848..a1761dc9 100644 --- a/pages/api/file/createcaseinvolvement_api.js +++ b/pages/api/file/createcaseinvolvement_api.js @@ -11,7 +11,6 @@ */ import axios from "axios"; -import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -36,20 +35,20 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); - var contactid = req.body.contactid; - var queryUrl = + const token = await getToken(); + const contactid = req.body.contactid; + const queryUrl = "incidents(" + req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - var crmUrl = "https://" + process.env.CRMURL; + const crmUrl = "https://" + process.env.CRMURL; - var data = { + const data = { "@odata.id": crmUrl + "/api/data/v8.2/contacts(" + contactid + ")" }; - var config = { + const config = { method: "post", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { @@ -64,18 +63,20 @@ export default async function ApiProxy(req, res) { data: data }; - return axios(config) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - return error.status == 412 - ? respondSuccess(res, { "record": "exists" }) - : (consoleLogger(Object.assign(error, data)), - respondError(res, { - status: 400, - code: "CREATE_CASE_INVOLVEMENT_FAILED", - message: "Failed to create case involvement" - })); + try { + const { data: responseData } = await axios(config); + return respondSuccess(res, responseData); + } catch (error) { + const errorStatus = error?.status || error?.response?.status; + if (errorStatus == 412) { + return respondSuccess(res, { record: "exists" }); + } + + consoleLogger(Object.assign(error, data)); + return respondError(res, { + status: 400, + code: "CREATE_CASE_INVOLVEMENT_FAILED", + message: "Failed to create case involvement" }); + } } diff --git a/pages/api/file/createrepinvolvement_api.js b/pages/api/file/createrepinvolvement_api.js index 4f40349f..856185ec 100644 --- a/pages/api/file/createrepinvolvement_api.js +++ b/pages/api/file/createrepinvolvement_api.js @@ -11,7 +11,6 @@ */ import axios from "axios"; -import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -36,22 +35,22 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); - var contactid = req.body.contactid; - var queryUrl = + const token = await getToken(); + const contactid = req.body.contactid; + const queryUrl = "incidents(" + req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - var crmUrl = "https://" + process.env.CRMURL; - var crmVersion = process.env.CRMURL_VERSION; + const crmUrl = "https://" + process.env.CRMURL; + const crmVersion = process.env.CRMURL_VERSION; - var data = { + const data = { "@odata.id": crmUrl + "/api/data/" + crmVersion + "/contacts(" + contactid + ")" }; - var config = { + const config = { method: "post", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { @@ -66,18 +65,20 @@ export default async function ApiProxy(req, res) { data: data }; - return axios(config) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - return error.status == 412 - ? respondSuccess(res, { "record": "exists" }) - : (consoleLogger(Object.assign(error, data)), - respondError(res, { - status: 400, - code: "CREATE_REP_INVOLVEMENT_FAILED", - message: "Failed to create representation involvement" - })); + try { + const { data: responseData } = await axios(config); + return respondSuccess(res, responseData); + } catch (error) { + const errorStatus = error?.status || error?.response?.status; + if (errorStatus == 412) { + return respondSuccess(res, { record: "exists" }); + } + + consoleLogger(Object.assign(error, data)); + return respondError(res, { + status: 400, + code: "CREATE_REP_INVOLVEMENT_FAILED", + message: "Failed to create representation involvement" }); + } } diff --git a/pages/api/file/updatecase_api.js b/pages/api/file/updatecase_api.js index c458b8dd..ccb51318 100644 --- a/pages/api/file/updatecase_api.js +++ b/pages/api/file/updatecase_api.js @@ -1,5 +1,4 @@ import axios from "axios"; -import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -10,12 +9,12 @@ const WEBAPI_URL = "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; export default async function ApiProxy(req, res) { - var data = JSON.stringify(req.body); - var appealObj = req.query.appealObj; - var incidentID = req.query.incident; + const data = JSON.stringify(req.body); + const appealObj = req.query.appealObj; + const incidentID = req.query.incident; - var updateFormCollection = req.query.updateFormCollection; - var queryUrl = updateFormCollection + "(" + appealObj + ")"; + const updateFormCollection = req.query.updateFormCollection; + const queryUrl = updateFormCollection + "(" + appealObj + ")"; if ( typeof updateFormCollection === "undefined" || @@ -35,9 +34,9 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var config = { + const config = { method: "patch", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { @@ -53,16 +52,15 @@ export default async function ApiProxy(req, res) { //console.log("update case:", data); - return axios(config) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "UPDATE_CASE_FAILED", - message: "Failed to update case" - }); + try { + const { data: responseData } = await axios(config); + return respondSuccess(res, responseData); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "UPDATE_CASE_FAILED", + message: "Failed to update case" }); + } } diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index 6aa84eb9..b22ff772 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -694,6 +694,95 @@ test("createappealcompletemessageproxy dependency failure returns CREATE_APPEAL_ ); }); +test("createcaseinvolvement returns record exists on 412 conflict", async () => { + const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + hashAPIPath: () => "?hash=abc", + consoleLogger: () => {}, + process: { + env: { CRMURL: "crm.example", RELAY_ROOT: "https://relay/" } + }, + axios: async () => { + const error = new Error("exists"); + error.status = 412; + throw error; + } + }); + + const req = { + body: { contactid: "c1", incidentid: "i1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + record: "exists" + }); +}); + +test("createrepinvolvement dependency failure returns CREATE_REP_INVOLVEMENT_FAILED", async () => { + const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + hashAPIPath: () => "?hash=abc", + consoleLogger: () => {}, + process: { + env: { + CRMURL: "crm.example", + CRMURL_VERSION: "v9.2", + RELAY_ROOT: "https://relay/" + } + }, + axios: async () => { + throw new Error("rep involvement failed"); + } + }); + + const req = { + body: { contactid: "c1", incidentid: "i1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "CREATE_REP_INVOLVEMENT_FAILED" + ); +}); + +test("updatecase dependency failure returns UPDATE_CASE_FAILED", async () => { + const mod = loadModule("pages/api/file/updatecase_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + hashAPIPath: () => "?hash=abc", + consoleLogger: () => {}, + process: { env: { RELAY_ROOT: "https://relay/" } }, + axios: async () => { + throw new Error("update failed"); + } + }); + + const req = { + query: { + updateFormCollection: "pinswg_forms", + appealObj: "obj1", + incident: "inc1" + }, + body: { field: "value" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "UPDATE_CASE_FAILED"); +}); + test("getawaitingsubmissionfromblob handler success returns payload", async () => { const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { nextConnect: createNextConnectMock(),