Skip to content

Commit

Permalink
Mimic DeactivateConversation Orchestration in taskrouter listeners (#507
Browse files Browse the repository at this point in the history
)

* Added logic to mimic DeactivateConversationOrchestration in the taskrouter listeners
  • Loading branch information
GPaoloni authored Sep 6, 2023
1 parent f9ceb19 commit d36530a
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 21 deletions.
4 changes: 2 additions & 2 deletions functions/helpers/chatChannelJanitor.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const deleteProxySession = async (context: Context<EnvVars>, proxySession: strin

if (!ps) {
// eslint-disable-next-line no-console
console.warn(`Tried to remove proxy session ${proxySession} but couldn't find it.`);
console.log(`Tried to remove proxy session ${proxySession} but couldn't find it.`);
return false;
}

Expand All @@ -53,7 +53,7 @@ const deleteProxySession = async (context: Context<EnvVars>, proxySession: strin
return removed;
} catch (err) {
// eslint-disable-next-line no-console
console.warn('deleteProxySession error: ', err);
console.log('deleteProxySession error: ', err);
return false;
}
};
Expand Down
92 changes: 76 additions & 16 deletions functions/taskrouterListeners/janitorListener.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
TASK_WRAPUP,
TASK_DELETED,
TASK_SYSTEM_DELETED,
TASK_COMPLETED,
} from '@tech-matters/serverless-helpers/taskrouter';

import type { ChatChannelJanitor } from '../helpers/chatChannelJanitor.private';
Expand All @@ -37,6 +38,7 @@ import type { ChatTransferTaskAttributes, TransferHelpers } from '../transfer/he
export const eventTypes: EventType[] = [
TASK_CANCELED,
TASK_WRAPUP,
TASK_COMPLETED,
TASK_DELETED,
TASK_SYSTEM_DELETED,
];
Expand All @@ -62,36 +64,44 @@ const isCleanupBotCapture = (
return channelCaptureHandlers.isChatCaptureControlTask(taskAttributes);
};

const isCleanupCustomChannel = (
eventType: EventType,
const isHandledByOtherListener = (
taskSid: string,
taskAttributes: {
channelType?: string;
isChatCaptureControl?: boolean;
} & ChatTransferTaskAttributes,
) => {
if (
!(
eventType === TASK_DELETED ||
eventType === TASK_SYSTEM_DELETED ||
eventType === TASK_CANCELED
)
) {
return false;
}

const channelCaptureHandlers = require(Runtime.getFunctions()[
'channelCapture/channelCaptureHandlers'
].path) as ChannelCaptureHandlers;

if (channelCaptureHandlers.isChatCaptureControlTask(taskAttributes)) {
return false;
return true;
}

const transferHelers = require(Runtime.getFunctions()['transfer/helpers']
.path) as TransferHelpers;

if (!transferHelers.hasTaskControl(taskSid, taskAttributes)) {
return true;
}

return false;
};

const isCleanupCustomChannel = (
eventType: EventType,
taskSid: string,
taskAttributes: {
channelType?: string;
isChatCaptureControl?: boolean;
} & ChatTransferTaskAttributes,
) => {
if (![TASK_DELETED, TASK_SYSTEM_DELETED, TASK_CANCELED].includes(eventType)) {
return false;
}

if (isHandledByOtherListener(taskSid, taskAttributes)) {
return false;
}

Expand All @@ -101,6 +111,29 @@ const isCleanupCustomChannel = (
return channelToFlex.isAseloCustomChannel(taskAttributes.channelType);
};

const isDeactivateConversationOrchestration = (
eventType: EventType,
taskSid: string,
taskAttributes: {
channelType?: string;
isChatCaptureControl?: boolean;
} & ChatTransferTaskAttributes,
) => {
if (
![TASK_WRAPUP, TASK_COMPLETED, TASK_DELETED, TASK_SYSTEM_DELETED, TASK_CANCELED].includes(
eventType,
)
) {
return false;
}

if (isHandledByOtherListener(taskSid, taskAttributes)) {
return false;
}

return true;
};

const wait = (ms: number): Promise<void> =>
new Promise((resolve) => {
setTimeout(resolve, ms);
Expand All @@ -114,7 +147,15 @@ export const shouldHandle = (event: EventFields) => eventTypes.includes(event.Ev

export const handleEvent = async (context: Context<EnvVars>, event: EventFields) => {
try {
const { EventType: eventType, TaskAttributes: taskAttributesString, TaskSid: taskSid } = event;
const {
EventType: eventType,
TaskAttributes: taskAttributesString,
TaskSid: taskSid,
TaskChannelUniqueName: taskChannelUniqueName,
} = event;

// The janitor is only be executed for chat based tasks
if (taskChannelUniqueName !== 'chat') return;

console.log(`===== Executing JanitorListener for event: ${eventType} =====`);

Expand Down Expand Up @@ -143,6 +184,25 @@ export const handleEvent = async (context: Context<EnvVars>, event: EventFields)
return;
}

if (isDeactivateConversationOrchestration(eventType, taskSid, taskAttributes)) {
// This task has reached a point where the channel should be deactivated, unless post survey is enabled
const client = context.getTwilioClient();
const serviceConfig = await client.flexApi.configuration.get().fetch();
const { feature_flags: featureFlags } = serviceConfig.attributes;

// TODO: remove featureFlags.backend_handled_chat_janitor condition once all accounts are updated, since we want this code to be executed in all Flex instances once CHI-2202 is implemented and in place
if (!featureFlags.enable_post_survey && featureFlags.backend_handled_chat_janitor) {
console.log('Handling DeactivateConversationOrchestration...');

const chatChannelJanitor = require(Runtime.getFunctions()['helpers/chatChannelJanitor']
.path).chatChannelJanitor as ChatChannelJanitor;
await chatChannelJanitor(context, { channelSid: taskAttributes.channelSid });

console.log('Finished DeactivateConversationOrchestration.');
return;
}
}

console.log('===== JanitorListener finished successfully =====');
} catch (err) {
console.log('===== JanitorListener has failed =====');
Expand All @@ -155,9 +215,9 @@ export const handleEvent = async (context: Context<EnvVars>, event: EventFields)
* The taskrouter callback expects that all taskrouter listeners return
* a default object of type TaskrouterListener.
*/
const transfersListener: TaskrouterListener = {
const janitorListener: TaskrouterListener = {
shouldHandle,
handleEvent,
};

export default transfersListener;
export default janitorListener;
2 changes: 0 additions & 2 deletions functions/taskrouterListeners/postSurveyListener.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ export const handleEvent = async (context: Context<EnvVars>, event: EventFields)
const serviceConfig = await client.flexApi.configuration.get().fetch();
const { feature_flags: featureFlags, helplineLanguage } = serviceConfig.attributes;

/** ==================== */
// TODO: Once all accounts are ready to manage triggering post survey on task wrap within taskRouterCallback, the check on post_survey_serverless_handled can be removed
if (featureFlags.enable_post_survey) {
const channelToFlex = require(Runtime.getFunctions()[
'helpers/customChannels/customChannelToFlex'
Expand Down
116 changes: 115 additions & 1 deletion tests/taskrouterListeners/janitorListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
EventFields,
EventType,
TASK_WRAPUP,
TASK_COMPLETED,
TASK_CANCELED,
TASK_DELETED,
TASK_SYSTEM_DELETED,
Expand Down Expand Up @@ -60,8 +61,19 @@ type EnvVars = {
FLEX_PROXY_SERVICE_SID: string;
};

const mockFetchFlexApiConfig = jest.fn(() => ({
attributes: {
feature_flags: {
enable_post_survey: true,
backend_handled_chat_janitor: true,
},
},
}));
const context = {
...mock<Context<EnvVars>>(),
getTwilioClient: (): any => ({
flexApi: { configuration: { get: () => ({ fetch: mockFetchFlexApiConfig }) } },
}),
CHAT_SERVICE_SID: 'CHxxx',
FLEX_PROXY_SERVICE_SID: 'KCxxx',
};
Expand Down Expand Up @@ -92,13 +104,14 @@ afterEach(() => {
});

describe('isCleanupBotCapture', () => {
each(['web', ...Object.values(AseloCustomChannels)]).test(
each(['web', ...Object.values(AseloCustomChannels)].map((channelType) => ({ channelType }))).test(
'capture control task canceled with channelType $channelType, should trigger janitor',
async ({ channelType }) => {
const event = {
...mock<EventFields>(),
EventType: TASK_CANCELED as EventType,
TaskAttributes: JSON.stringify({ ...captureControlTaskAttributes, channelType }),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

Expand All @@ -114,6 +127,7 @@ describe('isCleanupBotCapture', () => {
...mock<EventFields>(),
EventType: eventType,
TaskAttributes: JSON.stringify(captureControlTaskAttributes),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

Expand All @@ -126,6 +140,7 @@ describe('isCleanupBotCapture', () => {
...mock<EventFields>(),
EventType: TASK_CANCELED as EventType,
TaskAttributes: JSON.stringify(nonPostSurveyTaskAttributes),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

Expand All @@ -145,6 +160,7 @@ describe('isCleanupCustomChannel', () => {
...mock<EventFields>(),
EventType: eventType as EventType,
TaskAttributes: JSON.stringify({ ...customChannelTaskAttributes, channelType }),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

Expand All @@ -160,6 +176,7 @@ describe('isCleanupCustomChannel', () => {
...mock<EventFields>(),
EventType: eventType as EventType,
TaskAttributes: JSON.stringify(nonCustomChannelTaskAttributes),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

Expand Down Expand Up @@ -190,6 +207,7 @@ describe('isCleanupCustomChannel', () => {
...mock<EventFields>(),
EventType: TASK_CANCELED as EventType,
TaskAttributes: JSON.stringify(taskAttributes),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

Expand All @@ -211,10 +229,106 @@ describe('isCleanupCustomChannel', () => {
...mock<EventFields>(),
EventType: eventType as EventType,
TaskAttributes: JSON.stringify(taskAttributes),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

expect(mockChannelJanitor).not.toHaveBeenCalled();
},
);
});

describe('isDeactivateConversationOrchestration', () => {
each(
// [TASK_WRAPUP, TASK_COMPLETED, TASK_DELETED, TASK_SYSTEM_DELETED, TASK_CANCELED].flatMap(
[TASK_WRAPUP, TASK_COMPLETED].flatMap((eventType) =>
[...Object.values(AseloCustomChannels), 'web', 'sms', 'whatsapp', 'facebook'].map(
(channelType) => ({ channelType, eventType }),
),
),
).test(
'when enable_post_survey=false & backend_handled_chat_janitor=false, eventType $eventType with channelType $channelType, should not trigger janitor',
async ({ channelType, eventType }) => {
mockFetchFlexApiConfig.mockImplementationOnce(() => ({
attributes: {
feature_flags: {
enable_post_survey: true,
backend_handled_chat_janitor: true,
},
},
}));
const event = {
...mock<EventFields>(),
EventType: eventType as EventType,
TaskAttributes: JSON.stringify({ ...customChannelTaskAttributes, channelType }),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

const { channelSid } = customChannelTaskAttributes;
expect(mockChannelJanitor).not.toHaveBeenCalledWith(context, { channelSid });
},
);

each(
// [TASK_WRAPUP, TASK_COMPLETED, TASK_DELETED, TASK_SYSTEM_DELETED, TASK_CANCELED].flatMap(
[TASK_WRAPUP, TASK_COMPLETED].flatMap((eventType) =>
[...Object.values(AseloCustomChannels), 'web', 'sms', 'whatsapp', 'facebook'].map(
(channelType) => ({ channelType, eventType }),
),
),
).test(
'when enable_post_survey=true & backend_handled_chat_janitor=true, eventType $eventType with channelType $channelType, should not trigger janitor',
async ({ channelType, eventType }) => {
mockFetchFlexApiConfig.mockImplementationOnce(() => ({
attributes: {
feature_flags: {
enable_post_survey: true,
backend_handled_chat_janitor: true,
},
},
}));
const event = {
...mock<EventFields>(),
EventType: eventType as EventType,
TaskAttributes: JSON.stringify({ ...customChannelTaskAttributes, channelType }),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

const { channelSid } = customChannelTaskAttributes;
expect(mockChannelJanitor).not.toHaveBeenCalledWith(context, { channelSid });
},
);

each(
[TASK_WRAPUP, TASK_COMPLETED, TASK_DELETED, TASK_SYSTEM_DELETED, TASK_CANCELED].flatMap(
(eventType) =>
[...Object.values(AseloCustomChannels), 'web', 'sms', 'whatsapp', 'facebook'].map(
(channelType) => ({ channelType, eventType }),
),
),
).test(
'when enable_post_survey=false & backend_handled_chat_janitor=true, eventType $eventType with channelType $channelType, should trigger janitor',
async ({ channelType, eventType }) => {
mockFetchFlexApiConfig.mockImplementationOnce(() => ({
attributes: {
feature_flags: {
enable_post_survey: false,
backend_handled_chat_janitor: true,
},
},
}));
const event = {
...mock<EventFields>(),
EventType: eventType as EventType,
TaskAttributes: JSON.stringify({ ...customChannelTaskAttributes, channelType }),
TaskChannelUniqueName: 'chat',
};
await janitorListener.handleEvent(context, event);

const { channelSid } = customChannelTaskAttributes;
expect(mockChannelJanitor).toHaveBeenCalledWith(context, { channelSid });
},
);
});

0 comments on commit d36530a

Please sign in to comment.