From f7ef066ee91821154eb7c5cf472d9a6609243d2c Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Wed, 15 Jan 2025 16:42:45 +0000 Subject: [PATCH 1/5] Attempt to add external id on task updated if it hasn't already been added on task.created --- .../createContactListener.private.ts | 27 ++++++++++++++++--- .../webhooks/taskrouterCallback.protected.ts | 2 +- .../src/tokenValidator.ts | 1 - 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/functions/taskrouterListeners/createContactListener.private.ts b/functions/taskrouterListeners/createContactListener.private.ts index 135bf156..7d1d3682 100644 --- a/functions/taskrouterListeners/createContactListener.private.ts +++ b/functions/taskrouterListeners/createContactListener.private.ts @@ -24,12 +24,13 @@ import { EventFields, EventType, TASK_CREATED, + TASK_UPDATED, } from '@tech-matters/serverless-helpers/taskrouter'; import type { AddCustomerExternalId } from '../helpers/addCustomerExternalId.private'; import type { AddTaskSidToChannelAttributes } from '../helpers/addTaskSidToChannelAttributes.private'; -export const eventTypes: EventType[] = [TASK_CREATED]; +export const eventTypes: EventType[] = [TASK_CREATED, TASK_UPDATED]; type EnvVars = { TWILIO_WORKSPACE_SID: string; @@ -41,6 +42,14 @@ const isCreateContactTask = ( taskAttributes: { isContactlessTask?: boolean }, ) => eventType === TASK_CREATED && !taskAttributes.isContactlessTask; +const isTaskRequiringExternalId = ({ + isContactlessTask, + customers, +}: { + isContactlessTask?: boolean; + customers?: { external_id?: string }; +}) => !isContactlessTask && !customers?.external_id; + /** * Checks the event type to determine if the listener should handle the event or not. * If it returns true, the taskrouter will invoke this listener. @@ -50,15 +59,25 @@ export const shouldHandle = (event: EventFields) => eventTypes.includes(event.Ev export const handleEvent = async (context: Context, event: EventFields) => { const { EventType: eventType, TaskAttributes: taskAttributesString } = event; const taskAttributes = JSON.parse(taskAttributesString); - - if (isCreateContactTask(eventType, taskAttributes)) { - console.log('Handling create contact...'); + if (isTaskRequiringExternalId(taskAttributes)) { + if (eventType === TASK_CREATED) { + console.debug( + `Task ${event.TaskSid} requires an external_id but doesn't have one on event: ${eventType}`, + ); + } else { + console.warn( + `Task ${event.TaskSid} still requires an external_id but doesn't have one on event: ${eventType}, meaning one wasn't assigned on creation. Attempting to assign now`, + ); + } // For offline contacts, this is already handled when the task is created in /assignOfflineContact function const handlerPath = Runtime.getFunctions()['helpers/addCustomerExternalId'].path; const addCustomerExternalId = require(handlerPath) .addCustomerExternalId as AddCustomerExternalId; await addCustomerExternalId(context, event); + } + if (isCreateContactTask(eventType, taskAttributes)) { + console.log('Handling create contact...'); if ((taskAttributes.customChannelType || taskAttributes.channelType) === 'web') { // Add task sid to tasksSids channel attr so we can end the chat from webchat client (see endChat function) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 15cf4fa2..c3a23be4 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -68,7 +68,7 @@ const runTaskrouterListeners = async ( }; console.info('Forwarding to delegate webhook:', delegateUrl); console.info('event:', event); - console.debug('headers:', delegateHeaders); + console.debug('headers:', JSON.stringify(request.headers)); // Fire and forget delegatePromise = fetch(delegateUrl, { method: 'POST', diff --git a/tech-matters-serverless-helpers-lib/src/tokenValidator.ts b/tech-matters-serverless-helpers-lib/src/tokenValidator.ts index 69f1c4d9..83a8ec04 100644 --- a/tech-matters-serverless-helpers-lib/src/tokenValidator.ts +++ b/tech-matters-serverless-helpers-lib/src/tokenValidator.ts @@ -64,7 +64,6 @@ export const functionValidator = < try { const tokenResult: TokenValidatorResponse = await validator(token, accountSid, authToken) const isGuestToken = !isWorker(tokenResult) || isGuest(tokenResult); - if (isGuestToken && !options.allowGuestToken) { return failedResponse('Unauthorized: endpoint not open to guest tokens.'); } From bc3b85d66a6afb9b95e3116fd4dd56d72404a011 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Wed, 15 Jan 2025 19:51:40 +0000 Subject: [PATCH 2/5] Add feature flag to createContactListener in preparation for migration. Fixed and extended tests --- .../createContactListener.private.ts | 3 ++ .../createContactListener.test.ts | 44 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/functions/taskrouterListeners/createContactListener.private.ts b/functions/taskrouterListeners/createContactListener.private.ts index 7d1d3682..75c341af 100644 --- a/functions/taskrouterListeners/createContactListener.private.ts +++ b/functions/taskrouterListeners/createContactListener.private.ts @@ -59,6 +59,9 @@ export const shouldHandle = (event: EventFields) => eventTypes.includes(event.Ev export const handleEvent = async (context: Context, event: EventFields) => { const { EventType: eventType, TaskAttributes: taskAttributesString } = event; const taskAttributes = JSON.parse(taskAttributesString); + const flexConfig = await context.getTwilioClient().flexApi.v1.configuration.get().fetch(); + const { feature_flags: featureFlags } = flexConfig.attributes; + if (featureFlags.lambda_task_created_handler) return; if (isTaskRequiringExternalId(taskAttributes)) { if (eventType === TASK_CREATED) { console.debug( diff --git a/tests/taskrouterListeners/createContactListener.test.ts b/tests/taskrouterListeners/createContactListener.test.ts index 4e7935c1..80428e38 100644 --- a/tests/taskrouterListeners/createContactListener.test.ts +++ b/tests/taskrouterListeners/createContactListener.test.ts @@ -23,7 +23,9 @@ import { import { Context } from '@twilio-labs/serverless-runtime-types/types'; import { mock } from 'jest-mock-extended'; +import { Twilio } from 'twilio'; import * as contactListener from '../../functions/taskrouterListeners/createContactListener.private'; +import { RecursivePartial } from '../helpers'; const functions = { 'helpers/addCustomerExternalId': { @@ -35,6 +37,18 @@ const functions = { }; global.Runtime.getFunctions = () => functions; +const mockFetchFlexApiConfig = jest.fn(); + +const mockClient: RecursivePartial = { + flexApi: { + v1: { + configuration: { + get: () => ({ fetch: mockFetchFlexApiConfig }), + }, + }, + }, +}; + const facebookTaskAttributes = { isContactlessTask: false, channelType: 'facebook', @@ -55,10 +69,11 @@ type EnvVars = { CHAT_SERVICE_SID: string; }; -const context = { +const context: Context = { ...mock>(), TWILIO_WORKSPACE_SID: 'WSxxx', CHAT_SERVICE_SID: 'CHxxx', + getTwilioClient: () => mockClient as Twilio, }; const addCustomerExternalIdMock = jest.fn(); @@ -77,6 +92,11 @@ beforeEach(() => { virtual: true, }, ); + mockFetchFlexApiConfig.mockResolvedValue({ + attributes: { + feature_flags: {}, + }, + }); }); afterEach(() => { @@ -123,7 +143,7 @@ describe('Create contact', () => { expect(addTaskSidToChannelAttributesMock).not.toHaveBeenCalled(); }); - test('task wrapup do not add customerExternalId', async () => { + test('other event than task created adds customerExternalId but does not add task SID to channel attributes', async () => { const event = { ...mock(), EventType: TASK_WRAPUP as EventType, @@ -132,7 +152,25 @@ describe('Create contact', () => { await contactListener.handleEvent(context, event); - expect(addCustomerExternalIdMock).not.toHaveBeenCalled(); + expect(addCustomerExternalIdMock).toHaveBeenCalled(); expect(addTaskSidToChannelAttributesMock).not.toHaveBeenCalled(); }); + + test('does nothing if lambda_task_created_handler is set', async () => { + const event = { + ...mock(), + EventType: TASK_CREATED as EventType, + TaskAttributes: JSON.stringify(webTaskAttributes), + }; + mockFetchFlexApiConfig.mockResolvedValue({ + attributes: { + feature_flags: { lambda_task_created_handler: true }, + }, + }); + + await contactListener.handleEvent(context, event); + + expect(addCustomerExternalIdMock).toHaveBeenCalledWith(context, event); + expect(addTaskSidToChannelAttributesMock).toHaveBeenCalledWith(context, event); + }); }); From 3b08ac183b488868dfd528a4c2a3825d3f16402c Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Wed, 15 Jan 2025 20:32:32 +0000 Subject: [PATCH 3/5] Fix test --- tests/taskrouterListeners/createContactListener.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/taskrouterListeners/createContactListener.test.ts b/tests/taskrouterListeners/createContactListener.test.ts index 80428e38..fd76a724 100644 --- a/tests/taskrouterListeners/createContactListener.test.ts +++ b/tests/taskrouterListeners/createContactListener.test.ts @@ -170,7 +170,7 @@ describe('Create contact', () => { await contactListener.handleEvent(context, event); - expect(addCustomerExternalIdMock).toHaveBeenCalledWith(context, event); - expect(addTaskSidToChannelAttributesMock).toHaveBeenCalledWith(context, event); + expect(addCustomerExternalIdMock).not.toHaveBeenCalledWith(context, event); + expect(addTaskSidToChannelAttributesMock).not.toHaveBeenCalledWith(context, event); }); }); From e85bd80dbe09f954d83cc46692e99e3202dcb142 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Wed, 15 Jan 2025 21:26:04 +0000 Subject: [PATCH 4/5] Remove check for created event in addCustomerExternalId.private.ts --- functions/helpers/addCustomerExternalId.private.ts | 11 +---------- tests/helpers/addCustomerExternalId.test.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/functions/helpers/addCustomerExternalId.private.ts b/functions/helpers/addCustomerExternalId.private.ts index 78d65ffc..f1e4f56f 100644 --- a/functions/helpers/addCustomerExternalId.private.ts +++ b/functions/helpers/addCustomerExternalId.private.ts @@ -32,8 +32,6 @@ type Response = { updatedTask?: TaskInstance; }; -const TASK_CREATED_EVENT = 'task.created'; - const logAndReturnError = ( taskSid: TaskInstance['sid'], workspaceSid: EnvVars['TWILIO_WORKSPACE_SID'], @@ -51,14 +49,7 @@ export const addCustomerExternalId = async ( ): Promise => { console.log('-------- addCustomerExternalId execution --------'); - const { EventType, TaskSid } = event; - - const isNewTask = EventType === TASK_CREATED_EVENT; - - if (!isNewTask) { - return { message: `Event is not ${TASK_CREATED_EVENT}` }; - } - + const { TaskSid } = event; if (!event.TaskSid) throw new Error('TaskSid missing in event object'); let task: TaskInstance; diff --git a/tests/helpers/addCustomerExternalId.test.ts b/tests/helpers/addCustomerExternalId.test.ts index aecee065..4a44f2a4 100644 --- a/tests/helpers/addCustomerExternalId.test.ts +++ b/tests/helpers/addCustomerExternalId.test.ts @@ -126,12 +126,16 @@ test('Should return OK (modify live contact)', async () => { }); }); -test('Should return status 200 (ignores other events)', async () => { +test('Should return status 200 (accepts other events)', async () => { const event: Body = { EventType: 'other.event', TaskSid: 'live-contact', }; const result = await addCustomerExternalId(baseContext, event); - expect(result.message).toBe('Event is not task.created'); + expect(result.message).toBe('Task updated'); + expect(JSON.parse(result.updatedTask!.attributes)).toEqual({ + ...liveAttributes, + customers: { ...liveAttributes.customers, external_id: 'live-contact' }, + }); }); From 862ae736683bd4be4b71cdaa2bb225562fe6103c Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Thu, 16 Jan 2025 13:25:45 +0000 Subject: [PATCH 5/5] Bump error logs for failing to add customer external id to error --- functions/helpers/addCustomerExternalId.private.ts | 4 ++-- tests/helpers/addCustomerExternalId.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/functions/helpers/addCustomerExternalId.private.ts b/functions/helpers/addCustomerExternalId.private.ts index f1e4f56f..3b1935a0 100644 --- a/functions/helpers/addCustomerExternalId.private.ts +++ b/functions/helpers/addCustomerExternalId.private.ts @@ -38,8 +38,8 @@ const logAndReturnError = ( step: 'fetch' | 'update', errorInstance: unknown, ) => { - const errorMessage = `Error at addCustomerExternalId: task with sid ${taskSid} does not exists in workspace ${workspaceSid} when trying to ${step} it.`; - console.info(errorMessage, errorInstance); + const errorMessage = `Error at addCustomerExternalId: task with sid ${taskSid} in workspace ${workspaceSid} when trying to ${step} it.`; + console.error(errorMessage, errorInstance); return { message: errorMessage }; }; diff --git a/tests/helpers/addCustomerExternalId.test.ts b/tests/helpers/addCustomerExternalId.test.ts index 4a44f2a4..bc05f536 100644 --- a/tests/helpers/addCustomerExternalId.test.ts +++ b/tests/helpers/addCustomerExternalId.test.ts @@ -71,7 +71,7 @@ const baseContext = { const liveAttributes = { some: 'some', customers: { other: 1 } }; -const logSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); +const logSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); beforeAll(() => { helpers.setup({}); @@ -91,7 +91,7 @@ test("Should log and return error (can't fetch task)", async () => { }; const expectedError = - 'Error at addCustomerExternalId: task with sid non-existing does not exists in workspace WSxxx when trying to fetch it.'; + 'Error at addCustomerExternalId: task with sid non-existing in workspace WSxxx when trying to fetch it.'; const result = await addCustomerExternalId(baseContext, event); expect(result.message).toBe(expectedError); @@ -105,7 +105,7 @@ test("Should log and return error (can't update task)", async () => { }; const expectedError = - 'Error at addCustomerExternalId: task with sid non-updateable does not exists in workspace WSxxx when trying to update it.'; + 'Error at addCustomerExternalId: task with sid non-updateable in workspace WSxxx when trying to update it.'; const result = await addCustomerExternalId(baseContext, event); expect(result.message).toBe(expectedError);