From 8b60a9fa410f1986f1b8a8aa6f33588217dbd2da Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Mon, 25 Nov 2024 10:38:04 -0500 Subject: [PATCH 01/21] Create ngpvan-optout message handler. This message handler sends a request to VAN to update a given record if the contact opts out via our auto-optout feature. --- .../message-handlers/ngpvan-optout/index.js | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/extensions/message-handlers/ngpvan-optout/index.js diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js new file mode 100644 index 000000000..a8952ee6b --- /dev/null +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -0,0 +1,76 @@ +import { hasConfig } from "../../../server/api/lib/config"; +const Van = require("../../../extensions/action-handlers/ngpvan-action"); + +export const serverAdministratorInstructions = () => { + return { + description: ` + Update the contact in VAN with an opt out status + if and only if the internal Spoke auto-optout triggers. + `, + setupInstructions: ` + This message handler is dependent on the ngpvan-action Action Handler. + Follow it's setup instructions. + Additionally, "ngpvan-optout" must be adde dot the message handler + environment variable. + `, + environmentVariables: [] + }; +} + +export const available = organization => + (hasConfig("NGP_VAN_API_KEY", organization) || + hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) && + hasConfig("NGP_VAN_APP_NAME", organization); +// + +/* Sends a request to VAN to place an opt out tag to an individual. + * Ideally, we would want to query VAN to see if anyone else is attached + * to the cell, but we are unable to do a blind query based soley on + * the number. + */ +export const postMessageSave = async ({ + message, + contact, + handlerContext, + organization +}) => { + if (!exports.available(organization)) return {}; + + // TODO: Check VAN ID as well. + // Maybe be good to check that we also got this contact + // from a VAN upload? + if ( + message.is_from_contact || + !contact || + !handlerContext.autoOptOutReason + ) return {}; + + // Testing shows that "-" , "(", and ")" break this request + const cell = contact.cell.replace(/\D/g,'') + + // https://docs.ngpvan.com/reference/peoplevanidcanvassresponses + const body = { + "canvassContext": { + "inputTypeId": 11, // API input + "phone": { + "dialingPrefix": "1", + "phoneNumber": cell, + "smsOptInStatus": "O" // opt out status + } + }, + "resultCodeId": 205 + }; + + return Van.postCanvassResponse(contact, organization, body) + .then(() => ({})) + .catch(caughtError => { + // eslint-disable-next-line no-console + console.log( + "Encountered exception in ngpvan-optout.postMessageSave", + caughtError + ) + return {}; + }) +} + + From f6fb04e535c8d15aab81ac4277d97545ddb0b3f9 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Mon, 25 Nov 2024 10:43:17 -0500 Subject: [PATCH 02/21] Add auto-optout Message Handler to ngpvan-optout requirments --- .../message-handlers/ngpvan-optout/index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index a8952ee6b..b568bb053 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -1,4 +1,4 @@ -import { hasConfig } from "../../../server/api/lib/config"; +import { hasConfig, getConfig } from "../../../server/api/lib/config"; const Van = require("../../../extensions/action-handlers/ngpvan-action"); export const serverAdministratorInstructions = () => { @@ -8,9 +8,10 @@ export const serverAdministratorInstructions = () => { if and only if the internal Spoke auto-optout triggers. `, setupInstructions: ` - This message handler is dependent on the ngpvan-action Action Handler. - Follow it's setup instructions. - Additionally, "ngpvan-optout" must be adde dot the message handler + This message handler is dependent on the ngpvan-action Action Handler, + and the auto-optout Message Handler. + Follow their setup instructions. + Additionally, "ngpvan-optout" must be added to the message handler environment variable. `, environmentVariables: [] @@ -18,9 +19,10 @@ export const serverAdministratorInstructions = () => { } export const available = organization => - (hasConfig("NGP_VAN_API_KEY", organization) || + (hasConfig("NGP_VAN_API_KEY", organization) || hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) && - hasConfig("NGP_VAN_APP_NAME", organization); + hasConfig("NGP_VAN_APP_NAME", organization) && + getConfig("MESSAGE_HANDLER").inlcudes("auto-optout"); // /* Sends a request to VAN to place an opt out tag to an individual. From 361e16f8bb70a1ee91fa9e3ad04a25f88c0f577d Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Mon, 25 Nov 2024 13:40:14 -0500 Subject: [PATCH 03/21] Basic introduction to opting out a contact in VAN when manually opted out by the texter. Runs similar to the proposed ngp-optout message handler. May need to add additional checks to ensure that the contact has a vanid --- src/server/api/schema.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 86730b90a..afe8bd0f6 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -6,7 +6,6 @@ import _ from "lodash"; import { gzip, makeTree, getHighestRole } from "../../lib"; import { capitalizeWord, groupCannedResponses } from "./lib/utils"; import httpRequest from "../lib/http-request"; -import ownedPhoneNumber from "./lib/owned-phone-number"; import { getIngestMethod } from "../../extensions/contact-loaders"; import { @@ -82,6 +81,7 @@ import { Jobs } from "../../workers/job-processes"; import { Tasks } from "../../workers/tasks"; const uuidv4 = require("uuid").v4; +const Van = require("../../extensions/action-handlers/ngpvan-action.js"); // This function determines whether a field was requested // in a graphql query. Each graphql resolver receives a fourth parameter, @@ -1292,7 +1292,7 @@ const rootMutations = { } } return finalContacts; - }, + }, // createOptOut: async ( _, { optOut, campaignContactId, noReply }, @@ -1342,7 +1342,35 @@ const rootMutations = { const newContact = cacheableData.campaignContact.updateCacheForOptOut( contact ); - return newContact; + + if (!await Van.available(organization)) return newContact; + + console.log( + "createOptOut VAN" + ); + + const body = { + "canvassContext": { + "inputTypeId": 11, + "phone": { + "dialingPrefix": "1", + "phoneNumber": contact.cell, + "smsOptInStatus": "O" + } + }, + "resultCodeId": 205 + } + + try { + // I am assuming here that contact has vanID. + // will need to test + await Van.postCanvassResponse(contact, organization, body); + console.log(`canvasOptOut VAN success ${contact}`) + } catch (e) { + console.log(`Error opting out ${contact.cell}: ${e}`); + } finally { + return newContact; + } }, deleteQuestionResponses: async ( _, From 2c0994837b98e18ef2e49c77cb7bc1ed413250a8 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Tue, 3 Dec 2024 08:19:29 -0500 Subject: [PATCH 04/21] check for VAN ID in custom fields before attempting to send a request to VAN. --- .../message-handlers/ngpvan-optout/index.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index b568bb053..ebb954688 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -22,13 +22,9 @@ export const available = organization => (hasConfig("NGP_VAN_API_KEY", organization) || hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) && hasConfig("NGP_VAN_APP_NAME", organization) && - getConfig("MESSAGE_HANDLER").inlcudes("auto-optout"); -// + getConfig("MESSAGE_HANDLERS").inlcudes("auto-optout"); /* Sends a request to VAN to place an opt out tag to an individual. - * Ideally, we would want to query VAN to see if anyone else is attached - * to the cell, but we are unable to do a blind query based soley on - * the number. */ export const postMessageSave = async ({ message, @@ -38,9 +34,15 @@ export const postMessageSave = async ({ }) => { if (!exports.available(organization)) return {}; - // TODO: Check VAN ID as well. - // Maybe be good to check that we also got this contact - // from a VAN upload? + // customFields is a JSON object + // Checking for vanID in contact + try { + const c = JSON.stringify(contact.customFields); + if ("vanId" in c) return {} + } catch (exception) { + console.log("message-handlers | ngpvan-optout ERROR", exception); + } + if ( message.is_from_contact || !contact || From 8a0a3986535a6fa38e27d9316dbd3a891b71aa4f Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Tue, 3 Dec 2024 08:19:40 -0500 Subject: [PATCH 05/21] Add additional note that manual opt outs are handled by a different process. --- src/extensions/message-handlers/ngpvan-optout/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index ebb954688..d5d5fdbf4 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -6,6 +6,7 @@ export const serverAdministratorInstructions = () => { description: ` Update the contact in VAN with an opt out status if and only if the internal Spoke auto-optout triggers. + Manual opt outs are handled by a different process. `, setupInstructions: ` This message handler is dependent on the ngpvan-action Action Handler, From a54cdb04491d2c01dc0b69fd307082034dd6f3bc Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Tue, 3 Dec 2024 11:10:18 -0500 Subject: [PATCH 06/21] Test for vanId when manually opting someone out --- src/server/api/schema.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/server/api/schema.js b/src/server/api/schema.js index afe8bd0f6..583ee780e 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -1345,6 +1345,15 @@ const rootMutations = { if (!await Van.available(organization)) return newContact; + // Checking that contact contains a vanId + // If not, return and skip next steps + try { + const c = JSON.stringify(contact.customFiels); + if (!"vanId" in c) return newContact; + } catch (exception) { + console.log(exception) + } + console.log( "createOptOut VAN" ); From b4d391b7d3821a7d8910225dc8a02747ab51f7d9 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Thu, 5 Dec 2024 11:00:53 -0500 Subject: [PATCH 07/21] Fixed two minor bugs in ngpvan-optout. --- src/extensions/message-handlers/ngpvan-optout/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index d5d5fdbf4..dde72db00 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -23,7 +23,7 @@ export const available = organization => (hasConfig("NGP_VAN_API_KEY", organization) || hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) && hasConfig("NGP_VAN_APP_NAME", organization) && - getConfig("MESSAGE_HANDLERS").inlcudes("auto-optout"); + getConfig("MESSAGE_HANDLERS", organization).indexOf("auto-optout") !== -1; /* Sends a request to VAN to place an opt out tag to an individual. */ @@ -39,7 +39,7 @@ export const postMessageSave = async ({ // Checking for vanID in contact try { const c = JSON.stringify(contact.customFields); - if ("vanId" in c) return {} + if (c?.VanID === undefined) return {} } catch (exception) { console.log("message-handlers | ngpvan-optout ERROR", exception); } From e2a3df959bc33c56fb597da5152cae2b79ee4232 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Thu, 5 Dec 2024 11:17:09 -0500 Subject: [PATCH 08/21] access VanID properly; return in case of failure; prettify --- src/server/api/schema.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 583ee780e..8a6822578 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -1349,13 +1349,14 @@ const rootMutations = { // If not, return and skip next steps try { const c = JSON.stringify(contact.customFiels); - if (!"vanId" in c) return newContact; + if (c?.VanID === undefined) return newContact; } catch (exception) { - console.log(exception) + console.log(exception); + return newContact; } console.log( - "createOptOut VAN" + `createOptOut VAN ${contact.cell}` ); const body = { @@ -1368,13 +1369,13 @@ const rootMutations = { } }, "resultCodeId": 205 - } + }; try { // I am assuming here that contact has vanID. // will need to test await Van.postCanvassResponse(contact, organization, body); - console.log(`canvasOptOut VAN success ${contact}`) + console.log(`canvasOptOut VAN success ${contact.cell}`) } catch (e) { console.log(`Error opting out ${contact.cell}: ${e}`); } finally { From cd4a8ac7bb2bdfd60350cd41608f9afd86fa9c3a Mon Sep 17 00:00:00 2001 From: Ruby Engelhart Date: Fri, 6 Dec 2024 11:39:05 -0500 Subject: [PATCH 09/21] clear up faulty logic --- .../message-handlers/ngpvan-optout/index.js | 16 +++++++++++----- src/server/api/schema.js | 18 +++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index dde72db00..3fe654e76 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -28,24 +28,30 @@ export const available = organization => /* Sends a request to VAN to place an opt out tag to an individual. */ export const postMessageSave = async ({ - message, + _, // message contact, handlerContext, organization }) => { if (!exports.available(organization)) return {}; - // customFields is a JSON object + let customField; + let vanId; + // Checking for vanID in contact + // While Van.postCanvassResponse will check the customFields, + // we don't want to call that function every time if a vanid + // was never provided in the first place. Example: CSV try { - const c = JSON.stringify(contact.customFields); - if (c?.VanID === undefined) return {} + customField = JSON.parse(contact.customFields); + vanId = customField.VanID || customField.vanid; + if (!vanId) return {} } catch (exception) { console.log("message-handlers | ngpvan-optout ERROR", exception); + return {}; } if ( - message.is_from_contact || !contact || !handlerContext.autoOptOutReason ) return {}; diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 8a6822578..a3a176ff2 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -1348,8 +1348,9 @@ const rootMutations = { // Checking that contact contains a vanId // If not, return and skip next steps try { - const c = JSON.stringify(contact.customFiels); - if (c?.VanID === undefined) return newContact; + const c = JSON.parse(contact.customFields); + const vanId = c.VanId || c.vanid + if (!vanId) return newContact; } catch (exception) { console.log(exception); return newContact; @@ -1359,25 +1360,24 @@ const rootMutations = { `createOptOut VAN ${contact.cell}` ); + const cell = contact.cell.replace(/\D/g,''); + const body = { "canvassContext": { - "inputTypeId": 11, + "inputTypeId": 11, // API Input "phone": { "dialingPrefix": "1", - "phoneNumber": contact.cell, - "smsOptInStatus": "O" + "phoneNumber": cell, + "smsOptInStatus": "O" // opt out status } }, "resultCodeId": 205 }; try { - // I am assuming here that contact has vanID. - // will need to test await Van.postCanvassResponse(contact, organization, body); - console.log(`canvasOptOut VAN success ${contact.cell}`) } catch (e) { - console.log(`Error opting out ${contact.cell}: ${e}`); + console.log(`Error manually opting out ${contact.cell}: ${e}`); } finally { return newContact; } From e73268ccfbe75b83373688b737f3676286439756 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Mon, 9 Dec 2024 17:44:15 -0500 Subject: [PATCH 10/21] add test for ngpvan-optout message-handler --- .../message-handlers/ngpvan-optout.test.js | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 __test__/extensions/message-handlers/ngpvan-optout.test.js diff --git a/__test__/extensions/message-handlers/ngpvan-optout.test.js b/__test__/extensions/message-handlers/ngpvan-optout.test.js new file mode 100644 index 000000000..8bca37ea3 --- /dev/null +++ b/__test__/extensions/message-handlers/ngpvan-optout.test.js @@ -0,0 +1,143 @@ +const Config = require("../../../src/server/api/lib/config"); +const Van = require("../../../src/extensions/message-handlers/ngpvan-optout"); +const VanAction = require("../../../src/extensions/action-handlers/ngpvan-action"); + +describe("extensions.message-handlers.ngpvan-optout", () => { + afterEach(async () => { + jest.restoreAllMocks(); + }); + + describe("postMessageSave", () => { + let message; + let contact; + let organization; + let handlerContext; + let body; + + beforeEach(async () => { + message = { + is_from_contact: false + }; + + contact = { + message_status: "needsMessage", + customFields: '{"vanid": 12345}', + cell: "(123)-456-7891" + }; + + organization = { + id: 1 + }; + + handlerContext = { + autoOptOutReason: "stop" + } + + // Custom body for this call - this is the expected structure + body = { + "canvassContext": { + "inputTypeId": 11, // API input + "phone": { + "dialingPrefix": "1", + "phoneNumber": "1234567891", + "smsOptInStatus": "O" // opt out status + } + }, + "resultCodeId": 205 + }; + + jest.spyOn(Config, "getConfig").mockReturnValue(undefined); + jest.spyOn(Van, "available").mockReturnValue(true); + + jest.spyOn(VanAction, "postCanvassResponse").mockResolvedValue(null); + }); + + it("delegates to its dependencies and DOES call postCanvassResponse", async () => { + const result = await Van.postMessageSave({ + contact, + handlerContext, + organization + }); + + expect(result).toEqual({}); + + // This also verifies that postCanvassResponse was only called once + expect(VanAction.postCanvassResponse.mock.calls).toEqual([ + [ + contact, + organization, + body + ] + ]); + }); + + describe("when the handler is not available", () => { + beforeEach(async () => { + Van.available.mockReturnValue(false); + }); + + it("returns an empty object and DOES NOT call postCanvassResponse", async () => { + const result = await Van.postMessageSave({ + message, + contact, + organization + }); + expect(result).toEqual({}); + expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(0); + }); + }); + + describe("when contact is null or undefined", () => { + it("returns an empty object and DOES NOT call postCanvassResponse", async () => { + const result = await Van.postMessageSave({ + message, + organization + }); + expect(result).toEqual({}); + expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(0); + }); + }); + + describe("when no VAN Id is inclued", () => { + beforeEach(async () => { + contact = { + ...contact, + customFields: '{}' + }; + }); + + it("returns an empty object and DOES NOT call postCanvassResponse", async () => { + const result = await Van.postMessageSave({ + message, + contact, + organization, + handlerContext + }); + + expect(result).toEqual({}); + expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(0); + }) + }) + + describe("when alternate VAN ID is included", () => { + beforeEach(async () => { + contact = { + ...contact, + customFields: '{"VanID": 54321}' + }; + }); + + it("still works and DOES call postCanvassResponse", async () => { + const result = await Van.postMessageSave({ + message, + contact, + organization, + handlerContext + }); + + expect(result).toEqual({}); + expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(1); + }) + }) + }); +}); From c76587f0921dd5a7fcf69003f65e3267df90a89c Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Mon, 9 Dec 2024 17:44:35 -0500 Subject: [PATCH 11/21] adjust when contact and handler context are checked --- .../message-handlers/ngpvan-optout/index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index 3fe654e76..4d092cd1f 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -28,7 +28,6 @@ export const available = organization => /* Sends a request to VAN to place an opt out tag to an individual. */ export const postMessageSave = async ({ - _, // message contact, handlerContext, organization @@ -38,6 +37,11 @@ export const postMessageSave = async ({ let customField; let vanId; + if ( + !contact || + !handlerContext.autoOptOutReason + ) return {}; + // Checking for vanID in contact // While Van.postCanvassResponse will check the customFields, // we don't want to call that function every time if a vanid @@ -51,11 +55,6 @@ export const postMessageSave = async ({ return {}; } - if ( - !contact || - !handlerContext.autoOptOutReason - ) return {}; - // Testing shows that "-" , "(", and ")" break this request const cell = contact.cell.replace(/\D/g,'') From 7d0bbf4cdc66aca4480d98b45beadf1ff0c05cc0 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Tue, 10 Dec 2024 11:11:34 -0500 Subject: [PATCH 12/21] After some brief testing, there were some bad assumptions made about how to update a users opt in status. Result code id changed from 205 to 130 (Do not text result code). Cell is checked later down the call chain, so don't need to worry here. customFields >> custom_fields. Test is updated to reflect these changes. --- .../extensions/message-handlers/ngpvan-optout.test.js | 10 +++++----- src/extensions/message-handlers/ngpvan-optout/index.js | 9 +++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/__test__/extensions/message-handlers/ngpvan-optout.test.js b/__test__/extensions/message-handlers/ngpvan-optout.test.js index 8bca37ea3..ea3fd9422 100644 --- a/__test__/extensions/message-handlers/ngpvan-optout.test.js +++ b/__test__/extensions/message-handlers/ngpvan-optout.test.js @@ -21,8 +21,8 @@ describe("extensions.message-handlers.ngpvan-optout", () => { contact = { message_status: "needsMessage", - customFields: '{"vanid": 12345}', - cell: "(123)-456-7891" + custom_fields: '{"vanid": 12345}', + cell: "123-456-7891" }; organization = { @@ -39,11 +39,11 @@ describe("extensions.message-handlers.ngpvan-optout", () => { "inputTypeId": 11, // API input "phone": { "dialingPrefix": "1", - "phoneNumber": "1234567891", + "phoneNumber": "123-456-7891", "smsOptInStatus": "O" // opt out status } }, - "resultCodeId": 205 + "resultCodeId": 130 }; jest.spyOn(Config, "getConfig").mockReturnValue(undefined); @@ -102,7 +102,7 @@ describe("extensions.message-handlers.ngpvan-optout", () => { beforeEach(async () => { contact = { ...contact, - customFields: '{}' + custom_fields: '{}' }; }); diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index 4d092cd1f..aab843689 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -47,7 +47,7 @@ export const postMessageSave = async ({ // we don't want to call that function every time if a vanid // was never provided in the first place. Example: CSV try { - customField = JSON.parse(contact.customFields); + customField = JSON.parse(contact.custom_fields); vanId = customField.VanID || customField.vanid; if (!vanId) return {} } catch (exception) { @@ -55,20 +55,17 @@ export const postMessageSave = async ({ return {}; } - // Testing shows that "-" , "(", and ")" break this request - const cell = contact.cell.replace(/\D/g,'') - // https://docs.ngpvan.com/reference/peoplevanidcanvassresponses const body = { "canvassContext": { "inputTypeId": 11, // API input "phone": { "dialingPrefix": "1", - "phoneNumber": cell, + "phoneNumber": contact.cell, "smsOptInStatus": "O" // opt out status } }, - "resultCodeId": 205 + "resultCodeId": 130 // Do Not Text result code }; return Van.postCanvassResponse(contact, organization, body) From f2e098c2b676ec5d1c80f6a50d1412b64d61a1b7 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Wed, 11 Dec 2024 13:13:33 -0500 Subject: [PATCH 13/21] Slight refactor from previous. "contact" is not passed to postMessageSave (even though its passed in cacheable_queries.message). This queries the DB for the custom fields table which include van id, and uses that to update the VAN records. --- .../message-handlers/ngpvan-optout/index.js | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index aab843689..47c2e5e9a 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -1,5 +1,6 @@ import { hasConfig, getConfig } from "../../../server/api/lib/config"; const Van = require("../../../extensions/action-handlers/ngpvan-action"); +import { r } from "../../../server/models"; export const serverAdministratorInstructions = () => { return { @@ -19,41 +20,50 @@ export const serverAdministratorInstructions = () => { }; } +const dbQuery = async (campaignContactId) => { + return await r + .knex("campaign_contact") + .select("custom_fields") + .where("id", campaignContactId) +} + export const available = organization => (hasConfig("NGP_VAN_API_KEY", organization) || hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) && hasConfig("NGP_VAN_APP_NAME", organization) && getConfig("MESSAGE_HANDLERS", organization).indexOf("auto-optout") !== -1; -/* Sends a request to VAN to place an opt out tag to an individual. +/* + * Sends a request to VAN to place an opt out tag to an individual. */ export const postMessageSave = async ({ - contact, handlerContext, - organization + organization, + message }) => { if (!exports.available(organization)) return {}; + let query; let customField; let vanId; + let cell; + // If no message or optOut, return if ( - !contact || + !message || !handlerContext.autoOptOutReason ) return {}; - // Checking for vanID in contact - // While Van.postCanvassResponse will check the customFields, - // we don't want to call that function every time if a vanid - // was never provided in the first place. Example: CSV - try { - customField = JSON.parse(contact.custom_fields); - vanId = customField.VanID || customField.vanid; - if (!vanId) return {} - } catch (exception) { - console.log("message-handlers | ngpvan-optout ERROR", exception); - return {}; - } + // Grabs van id and phone number + // While there may be multiple phone numbers, + // we want to use the # we originally texted + query = await dbQuery(message.campaign_campaign_id); + customField = JSON.parse(query[0]["custom_fields"] || "{}"); + vanId = customField["VanID"] || customField["vanid"]; + cell = message["contact_number"] || ""; // Phone number + + // if no van id or cell #, return + if (!vanId || !cell) return {}; // https://docs.ngpvan.com/reference/peoplevanidcanvassresponses const body = { @@ -61,7 +71,7 @@ export const postMessageSave = async ({ "inputTypeId": 11, // API input "phone": { "dialingPrefix": "1", - "phoneNumber": contact.cell, + "phoneNumber": cell, "smsOptInStatus": "O" // opt out status } }, From df04c67a7d14dded5668a68e691d228c9f1d1bca Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 09:58:13 -0500 Subject: [PATCH 14/21] refactor away from using the van action-handler post request. Same concept before, but building this POST request from scratch as the contact object is not always passed to postMessageSave. --- .../message-handlers/ngpvan-optout/index.js | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index 47c2e5e9a..8c09d3793 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -1,6 +1,11 @@ import { hasConfig, getConfig } from "../../../server/api/lib/config"; -const Van = require("../../../extensions/action-handlers/ngpvan-action"); import { r } from "../../../server/models"; +import httpRequest from "../../../server/lib/http-request"; +import { + getCountryCode, + getDashedPhoneNumberDisplay +} from "../../../lib/phone-format"; +import Van from "../../contact-loaders/ngpvan/util"; export const serverAdministratorInstructions = () => { return { @@ -16,6 +21,7 @@ export const serverAdministratorInstructions = () => { Additionally, "ngpvan-optout" must be added to the message handler environment variable. `, + // Does this include NGP_VAN env variables and what not? environmentVariables: [] }; } @@ -41,12 +47,15 @@ export const postMessageSave = async ({ organization, message }) => { + // Redundent, but other message-handlers check this first as well if (!exports.available(organization)) return {}; - let query; + let query; // store custom_fields of the contact let customField; - let vanId; - let cell; + let vanId; // vanid of contact + let cell; // phone number that sent opt out message + let phoneCountry; // The coutnry code + let url; // url of VAN api // If no message or optOut, return if ( @@ -54,40 +63,56 @@ export const postMessageSave = async ({ !handlerContext.autoOptOutReason ) return {}; - // Grabs van id and phone number - // While there may be multiple phone numbers, - // we want to use the # we originally texted - query = await dbQuery(message.campaign_campaign_id); - customField = JSON.parse(query[0]["custom_fields"] || "{}"); - vanId = customField["VanID"] || customField["vanid"]; - cell = message["contact_number"] || ""; // Phone number + try { + query = await dbQuery(message.campaign_contact_id); + customField = JSON.parse(query[0]["custom_fields"] || "{}"); + + vanId = customField["VanID"] || customField["vanid"]; + cell = message["contact_number"] || ""; + } catch (exception) { + console.error( + `postMessageSave.ngpvan-optout ERROR finding contact or ` + + `parsing custom fields for contact ${message.campaign_contact_id}` + ) + } + // if no van id or cell #, return if (!vanId || !cell) return {}; + phoneCountry = process.env.PHONE_NUMBER_COUNTRY || "US"; + cell = getDashedPhoneNumberDisplay(cell, phoneCountry); + + url = Van.makeUrl(`v4/people/${vanId}/canvassResponses`, organization); + // https://docs.ngpvan.com/reference/peoplevanidcanvassresponses const body = { "canvassContext": { "inputTypeId": 11, // API input "phone": { - "dialingPrefix": "1", + "dialingPrefix": getCountryCode(cell, phoneCountry).toString(), "phoneNumber": cell, "smsOptInStatus": "O" // opt out status } }, - "resultCodeId": 130 // Do Not Text result code + // Do Not Text result code + // Unsure if this is specfic to each VAN committe ? + "resultCodeId": 130 }; - return Van.postCanvassResponse(contact, organization, body) - .then(() => ({})) - .catch(caughtError => { - // eslint-disable-next-line no-console - console.log( - "Encountered exception in ngpvan-optout.postMessageSave", - caughtError - ) - return {}; - }) + return httpRequest(url, { + method: "POST", + retries: 1, + timeout: Van.getVanTimeout(organization), + headers: { + Authorization: await Van.getAuth(organization), + "accept": "text/plain", + "Content-Type": "application/json" + }, + body: JSON.stringify(body), + validStatuses: [204], + compress: false + }) } From c347bb533e5aafacfda85fbe7ba3f2769da620db Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 13:28:57 -0500 Subject: [PATCH 15/21] tests and ironing out the final logic --- .../message-handlers/ngpvan-optout.test.js | 196 +++++++++++------- .../message-handlers/ngpvan-optout/index.js | 13 +- 2 files changed, 135 insertions(+), 74 deletions(-) diff --git a/__test__/extensions/message-handlers/ngpvan-optout.test.js b/__test__/extensions/message-handlers/ngpvan-optout.test.js index ea3fd9422..ac2473742 100644 --- a/__test__/extensions/message-handlers/ngpvan-optout.test.js +++ b/__test__/extensions/message-handlers/ngpvan-optout.test.js @@ -1,6 +1,6 @@ -const Config = require("../../../src/server/api/lib/config"); -const Van = require("../../../src/extensions/message-handlers/ngpvan-optout"); -const VanAction = require("../../../src/extensions/action-handlers/ngpvan-action"); +const VanOptOut = require("../../../src/extensions/message-handlers/ngpvan-optout"); +const VanUtil = require("../../../src/extensions/contact-loaders/ngpvan/util"); +const HttpRequest = require("../../../src/server/lib/http-request"); describe("extensions.message-handlers.ngpvan-optout", () => { afterEach(async () => { @@ -12,17 +12,12 @@ describe("extensions.message-handlers.ngpvan-optout", () => { let contact; let organization; let handlerContext; - let body; beforeEach(async () => { message = { - is_from_contact: false - }; - - contact = { - message_status: "needsMessage", - custom_fields: '{"vanid": 12345}', - cell: "123-456-7891" + campaign_contact_id: "1234", + contact_number: "(123)-456-7890", + is_from_contact: true }; organization = { @@ -33,111 +28,172 @@ describe("extensions.message-handlers.ngpvan-optout", () => { autoOptOutReason: "stop" } - // Custom body for this call - this is the expected structure - body = { - "canvassContext": { - "inputTypeId": 11, // API input - "phone": { - "dialingPrefix": "1", - "phoneNumber": "123-456-7891", - "smsOptInStatus": "O" // opt out status - } - }, - "resultCodeId": 130 - }; + jest.spyOn(VanOptOut, "available").mockReturnValue(true); + jest.spyOn(VanOptOut, "dbQuery").mockReturnValue([{custom_fields: '{"VanID": 1234}'}]); - jest.spyOn(Config, "getConfig").mockReturnValue(undefined); - jest.spyOn(Van, "available").mockReturnValue(true); + jest.spyOn(VanUtil.default, "getAuth").mockReturnValue("*****"); - jest.spyOn(VanAction, "postCanvassResponse").mockResolvedValue(null); + jest.spyOn(HttpRequest, "default").mockReturnValue(null); }); - it("delegates to its dependencies and DOES call postCanvassResponse", async () => { - const result = await Van.postMessageSave({ - contact, + it("delegates to its dependencies and DOES post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ handlerContext, - organization + organization, + message }); expect(result).toEqual({}); - // This also verifies that postCanvassResponse was only called once - expect(VanAction.postCanvassResponse.mock.calls).toEqual([ + expect(HttpRequest.default.mock.calls).toEqual( [ - contact, - organization, - body + [ + "https://api.securevan.com/v4/people/1234/canvassResponses", + { + "method": "POST", + "retries": 1, + "timeout": 32000, + "headers": { + "Authorization": "*****", + "accept": "text/plain", + "Content-Type": "application/json" + }, + "body": `{"canvassContext":{"inputTypeId":11,"phone":{"dialingPrefix":"1"`+ + `,"phoneNumber":"123-456-7890","smsOptInStatus":"O"}},"resultCodeId":130}`, + "validStatuses": [204], + "compress": false + } + ] ] - ]); + ); }); describe("when the handler is not available", () => { beforeEach(async () => { - Van.available.mockReturnValue(false); + VanOptOut.available.mockReturnValue(false); }); - it("returns an empty object and DOES NOT call postCanvassResponse", async () => { - const result = await Van.postMessageSave({ - message, - contact, - organization + it("returns an empty object and DOES NOT post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, + organization, + message }); + expect(result).toEqual({}); - expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(0); + expect(HttpRequest.default.mock.calls).toHaveLength(0); }); }); - describe("when contact is null or undefined", () => { - it("returns an empty object and DOES NOT call postCanvassResponse", async () => { - const result = await Van.postMessageSave({ - message, - organization + describe("when message is null or undefined", () => { + beforeEach(async () => { + handlerContext = {} + }); + + it("returns an empty object and DOES NOT post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, + organization, + message }); + expect(result).toEqual({}); - expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(0); + expect(HttpRequest.default.mock.calls).toHaveLength(0); }); }); describe("when no VAN Id is inclued", () => { beforeEach(async () => { - contact = { - ...contact, - custom_fields: '{}' - }; + VanOptOut.dbQuery.mockReturnValue({}); }); - it("returns an empty object and DOES NOT call postCanvassResponse", async () => { - const result = await Van.postMessageSave({ - message, - contact, + it("returns an empty object and DOES NOT post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, organization, - handlerContext + message }); expect(result).toEqual({}); - expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(0); + expect(HttpRequest.default.mock.calls).toHaveLength(0); }) }) describe("when alternate VAN ID is included", () => { beforeEach(async () => { - contact = { - ...contact, - customFields: '{"VanID": 54321}' - }; + VanOptOut.dbQuery.mockReturnValue([{custom_fields: '{"vanid": 1234}'}]) }); - it("still works and DOES call postCanvassResponse", async () => { - const result = await Van.postMessageSave({ - message, - contact, + it("still works and DOES post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, organization, - handlerContext + message }); expect(result).toEqual({}); - expect(VanAction.postCanvassResponse.mock.calls).toHaveLength(1); + expect(HttpRequest.default.mock.calls).toEqual( + [ + [ + "https://api.securevan.com/v4/people/1234/canvassResponses", + { + "method": "POST", + "retries": 1, + "timeout": 32000, + "headers": { + "Authorization": "*****", + "accept": "text/plain", + "Content-Type": "application/json" + }, + "body": `{"canvassContext":{"inputTypeId":11,"phone":{"dialingPrefix":"1"`+ + `,"phoneNumber":"123-456-7890","smsOptInStatus":"O"}},"resultCodeId":130}`, + "validStatuses": [204], + "compress": false + } + ] + ] + ); }) - }) + }); + + describe("when no contact number is included in the message object", () => { + beforeEach(async () => { + message = { + ...message, + contact_number: "" + }; + }); + + it("returns an object and DOES NOT post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, + organization, + message + }); + + expect(result).toEqual({}); + expect(HttpRequest.default.mock.calls).toHaveLength(0); + }); + }); + + describe("when the message is not from the contact", () => { + beforeEach(async () => { + message = { + ...message, + is_from_contact: false + }; + }); + + it("returns and empty obejct and DOES NOT post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, + organization, + message + }); + + expect(result).toEqual({}); + expect(HttpRequest.default.mock.calls).toHaveLength(0); + }); + }); }); }); diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index 8c09d3793..5163c34d3 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -26,7 +26,7 @@ export const serverAdministratorInstructions = () => { }; } -const dbQuery = async (campaignContactId) => { +export const dbQuery = async campaignContactId => { return await r .knex("campaign_contact") .select("custom_fields") @@ -60,12 +60,13 @@ export const postMessageSave = async ({ // If no message or optOut, return if ( !message || + !message.is_from_contact || !handlerContext.autoOptOutReason ) return {}; try { - query = await dbQuery(message.campaign_contact_id); + query = await exports.dbQuery(message.campaign_contact_id); customField = JSON.parse(query[0]["custom_fields"] || "{}"); vanId = customField["VanID"] || customField["vanid"]; @@ -100,10 +101,12 @@ export const postMessageSave = async ({ "resultCodeId": 130 }; - return httpRequest(url, { + console.log(`ngpvan-optout.postMessageSave VAN ID : ${vanId}`); + + await httpRequest(url, { method: "POST", retries: 1, - timeout: Van.getVanTimeout(organization), + timeout: Van.getNgpVanTimeout(organization), headers: { Authorization: await Van.getAuth(organization), "accept": "text/plain", @@ -113,6 +116,8 @@ export const postMessageSave = async ({ validStatuses: [204], compress: false }) + + return {}; } From 5e627ce64e6b572860e13e56547ec15f32484e89 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 14:17:16 -0500 Subject: [PATCH 16/21] adjust how manually opting out someone works by reusing the ngpvan-optout message handler code --- .../message-handlers/ngpvan-optout/index.js | 8 +-- src/server/api/schema.js | 52 ++++++------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/src/extensions/message-handlers/ngpvan-optout/index.js b/src/extensions/message-handlers/ngpvan-optout/index.js index 5163c34d3..b6bcad328 100644 --- a/src/extensions/message-handlers/ngpvan-optout/index.js +++ b/src/extensions/message-handlers/ngpvan-optout/index.js @@ -60,8 +60,7 @@ export const postMessageSave = async ({ // If no message or optOut, return if ( !message || - !message.is_from_contact || - !handlerContext.autoOptOutReason + !(handlerContext.autoOptOutReason || handlerContext.optOutReason) ) return {}; @@ -101,7 +100,10 @@ export const postMessageSave = async ({ "resultCodeId": 130 }; - console.log(`ngpvan-optout.postMessageSave VAN ID : ${vanId}`); + console.log( + `ngpvan-optout.postMessageSave VAN ID : ${vanId} ` + + `: ${handlerContext.autoOptOutReason || handlerContext.optOutReason}` + ); await httpRequest(url, { method: "POST", diff --git a/src/server/api/schema.js b/src/server/api/schema.js index a3a176ff2..af2d75894 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -83,6 +83,11 @@ import { Tasks } from "../../workers/tasks"; const uuidv4 = require("uuid").v4; const Van = require("../../extensions/action-handlers/ngpvan-action.js"); +import { + available, + postMessageSave as optOutInVan +} from "../../extensions/message-handlers/ngpvan-optout"; + // This function determines whether a field was requested // in a graphql query. Each graphql resolver receives a fourth parameter, // which contains information about the current request and the execution @@ -1343,44 +1348,21 @@ const rootMutations = { contact ); - if (!await Van.available(organization)) return newContact; - - // Checking that contact contains a vanId - // If not, return and skip next steps - try { - const c = JSON.parse(contact.customFields); - const vanId = c.VanId || c.vanid - if (!vanId) return newContact; - } catch (exception) { - console.log(exception); - return newContact; - } - - console.log( - `createOptOut VAN ${contact.cell}` - ); + if (!available(organization)) return newContact; - const cell = contact.cell.replace(/\D/g,''); - - const body = { - "canvassContext": { - "inputTypeId": 11, // API Input - "phone": { - "dialingPrefix": "1", - "phoneNumber": cell, - "smsOptInStatus": "O" // opt out status - } + // Reusing VAN opt out message-handler + await optOutInVan({ + handlerContext: { + optOutReason: reason }, - "resultCodeId": 205 - }; + organization, + message: { + campaign_contact_id: campaignContactId, + contact_number: contact.cell + } + }) - try { - await Van.postCanvassResponse(contact, organization, body); - } catch (e) { - console.log(`Error manually opting out ${contact.cell}: ${e}`); - } finally { - return newContact; - } + return newContact; }, deleteQuestionResponses: async ( _, From cfc39282e8eb817c1fe50da44821fb7894964c73 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 14:17:28 -0500 Subject: [PATCH 17/21] add documentation for ngpvan-optout --- docs/HOWTO-use-message-handlers.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/HOWTO-use-message-handlers.md b/docs/HOWTO-use-message-handlers.md index 53a98960e..2abb73ad5 100644 --- a/docs/HOWTO-use-message-handlers.md +++ b/docs/HOWTO-use-message-handlers.md @@ -33,6 +33,14 @@ This is especially useful to auto-optout hostile contact replies so texters do n need to see them. Additionally the JSON object can encode a "reason_code" that will be logged in the opt_out table record. +### ngpvan-optout + +Sends a POST request to NGP VAN to update a record that they are now opted out from +future campaigns. + +Requires that the contact have `vanid` or `VanID` in their custom_feilds. +Requires auto-optout to be enabled. + ### profanity-tagger Before you enable a custom regular expression with auto-optout, we recommend strongly From a375f07527adf30c58207d2f2852c3a3b6f85bc8 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 14:38:27 -0500 Subject: [PATCH 18/21] skip test which checks if message is from contact. May be a world where we opt someone out after we send them a message --- __test__/extensions/message-handlers/ngpvan-optout.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__test__/extensions/message-handlers/ngpvan-optout.test.js b/__test__/extensions/message-handlers/ngpvan-optout.test.js index ac2473742..388e5be4c 100644 --- a/__test__/extensions/message-handlers/ngpvan-optout.test.js +++ b/__test__/extensions/message-handlers/ngpvan-optout.test.js @@ -176,7 +176,9 @@ describe("extensions.message-handlers.ngpvan-optout", () => { }); }); - describe("when the message is not from the contact", () => { + // Skipping as there is a world where we opt out someone + // even when the message is not from them originally + describe.skip("when the message is not from the contact", () => { beforeEach(async () => { message = { ...message, From fa96e286cda73e6f3ba60147e64ddb5f9f08c4d2 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 14:39:06 -0500 Subject: [PATCH 19/21] reason is undefined, set optOutReason to manual --- src/server/api/schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/schema.js b/src/server/api/schema.js index af2d75894..a00df67f0 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -1353,7 +1353,7 @@ const rootMutations = { // Reusing VAN opt out message-handler await optOutInVan({ handlerContext: { - optOutReason: reason + optOutReason: "manual" }, organization, message: { From 122f0c92578e54a26ad5f737af9d81804443860d Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 14:42:39 -0500 Subject: [PATCH 20/21] add test to check for alternate opt out reason in handlerContext object --- .../message-handlers/ngpvan-optout.test.js | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/__test__/extensions/message-handlers/ngpvan-optout.test.js b/__test__/extensions/message-handlers/ngpvan-optout.test.js index 388e5be4c..ec2770a29 100644 --- a/__test__/extensions/message-handlers/ngpvan-optout.test.js +++ b/__test__/extensions/message-handlers/ngpvan-optout.test.js @@ -176,6 +176,25 @@ describe("extensions.message-handlers.ngpvan-optout", () => { }); }); + describe("when the alternate optOutReason is passed in the handlerContext object", () => { + beforeEach(async () => { + handlerContext = { + optOutReason: "manual" + } + }); + + it("returns an empty object and DOES post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, + organization, + message + }) + + expect(result).toEqual({}); + expect(HttpRequest.default.mock.calls) + }) + }) + // Skipping as there is a world where we opt out someone // even when the message is not from them originally describe.skip("when the message is not from the contact", () => { @@ -194,7 +213,27 @@ describe("extensions.message-handlers.ngpvan-optout", () => { }); expect(result).toEqual({}); - expect(HttpRequest.default.mock.calls).toHaveLength(0); + expect(HttpRequest.default.mock.calls).toEqual( + [ + [ + "https://api.securevan.com/v4/people/1234/canvassResponses", + { + "method": "POST", + "retries": 1, + "timeout": 32000, + "headers": { + "Authorization": "*****", + "accept": "text/plain", + "Content-Type": "application/json" + }, + "body": `{"canvassContext":{"inputTypeId":11,"phone":{"dialingPrefix":"1"`+ + `,"phoneNumber":"123-456-7890","smsOptInStatus":"O"}},"resultCodeId":130}`, + "validStatuses": [204], + "compress": false + } + ] + ] + ); }); }); }); From c463b35d8e7d81818cf5716f97ebff34fcb2b4d7 Mon Sep 17 00:00:00 2001 From: engelhartrueben Date: Fri, 13 Dec 2024 14:50:02 -0500 Subject: [PATCH 21/21] bad copy paste --- .../message-handlers/ngpvan-optout.test.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/__test__/extensions/message-handlers/ngpvan-optout.test.js b/__test__/extensions/message-handlers/ngpvan-optout.test.js index ec2770a29..7c7ff9dac 100644 --- a/__test__/extensions/message-handlers/ngpvan-optout.test.js +++ b/__test__/extensions/message-handlers/ngpvan-optout.test.js @@ -190,28 +190,6 @@ describe("extensions.message-handlers.ngpvan-optout", () => { message }) - expect(result).toEqual({}); - expect(HttpRequest.default.mock.calls) - }) - }) - - // Skipping as there is a world where we opt out someone - // even when the message is not from them originally - describe.skip("when the message is not from the contact", () => { - beforeEach(async () => { - message = { - ...message, - is_from_contact: false - }; - }); - - it("returns and empty obejct and DOES NOT post to NGP VAN", async () => { - const result = await VanOptOut.postMessageSave({ - handlerContext, - organization, - message - }); - expect(result).toEqual({}); expect(HttpRequest.default.mock.calls).toEqual( [ @@ -234,6 +212,28 @@ describe("extensions.message-handlers.ngpvan-optout", () => { ] ] ); + }) + }) + + // Skipping as there is a world where we opt out someone + // even when the message is not from them originally + describe.skip("when the message is not from the contact", () => { + beforeEach(async () => { + message = { + ...message, + is_from_contact: false + }; + }); + + it("returns and empty obejct and DOES NOT post to NGP VAN", async () => { + const result = await VanOptOut.postMessageSave({ + handlerContext, + organization, + message + }); + + expect(result).toEqual({}); + expect(HttpRequest.default.mock.calls).toHaveLength(0); }); }); });