Skip to content

Commit

Permalink
Merge pull request #743 from techmatters/CHI-3173-add_external_id_mor…
Browse files Browse the repository at this point in the history
…e_places

CHI-3173: Attempt to add external id on task updated if it hasn't already been added on task.created
  • Loading branch information
stephenhand authored Jan 23, 2025
2 parents 4d1e48d + 862ae73 commit a1f9fdc
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 26 deletions.
15 changes: 3 additions & 12 deletions functions/helpers/addCustomerExternalId.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,14 @@ type Response = {
updatedTask?: TaskInstance;
};

const TASK_CREATED_EVENT = 'task.created';

const logAndReturnError = (
taskSid: TaskInstance['sid'],
workspaceSid: EnvVars['TWILIO_WORKSPACE_SID'],
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 };
};

Expand All @@ -51,14 +49,7 @@ export const addCustomerExternalId = async (
): Promise<Response> => {
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;
Expand Down
30 changes: 26 additions & 4 deletions functions/taskrouterListeners/createContactListener.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -50,15 +59,28 @@ export const shouldHandle = (event: EventFields) => eventTypes.includes(event.Ev
export const handleEvent = async (context: Context<EnvVars>, event: EventFields) => {
const { EventType: eventType, TaskAttributes: taskAttributesString } = event;
const taskAttributes = JSON.parse(taskAttributesString);

if (isCreateContactTask(eventType, taskAttributes)) {
console.log('Handling create contact...');
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(
`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)
Expand Down
2 changes: 1 addition & 1 deletion functions/webhooks/taskrouterCallback.protected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,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',
Expand Down
1 change: 0 additions & 1 deletion tech-matters-serverless-helpers-lib/src/tokenValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand Down
14 changes: 9 additions & 5 deletions tests/helpers/addCustomerExternalId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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' },
});
});
44 changes: 41 additions & 3 deletions tests/taskrouterListeners/createContactListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand All @@ -35,6 +37,18 @@ const functions = {
};
global.Runtime.getFunctions = () => functions;

const mockFetchFlexApiConfig = jest.fn();

const mockClient: RecursivePartial<Twilio> = {
flexApi: {
v1: {
configuration: {
get: () => ({ fetch: mockFetchFlexApiConfig }),
},
},
},
};

const facebookTaskAttributes = {
isContactlessTask: false,
channelType: 'facebook',
Expand All @@ -55,10 +69,11 @@ type EnvVars = {
CHAT_SERVICE_SID: string;
};

const context = {
const context: Context<EnvVars> = {
...mock<Context<EnvVars>>(),
TWILIO_WORKSPACE_SID: 'WSxxx',
CHAT_SERVICE_SID: 'CHxxx',
getTwilioClient: () => mockClient as Twilio,
};

const addCustomerExternalIdMock = jest.fn();
Expand All @@ -77,6 +92,11 @@ beforeEach(() => {
virtual: true,
},
);
mockFetchFlexApiConfig.mockResolvedValue({
attributes: {
feature_flags: {},
},
});
});

afterEach(() => {
Expand Down Expand Up @@ -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<EventFields>(),
EventType: TASK_WRAPUP as EventType,
Expand All @@ -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<EventFields>(),
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).not.toHaveBeenCalledWith(context, event);
expect(addTaskSidToChannelAttributesMock).not.toHaveBeenCalledWith(context, event);
});
});

0 comments on commit a1f9fdc

Please sign in to comment.