From d36530a63a4c3e90d3f1d0a40753cd4319ad96a2 Mon Sep 17 00:00:00 2001 From: Gianfranco Paoloni Date: Wed, 6 Sep 2023 19:44:00 -0300 Subject: [PATCH] Mimic DeactivateConversation Orchestration in taskrouter listeners (#507) * Added logic to mimic DeactivateConversationOrchestration in the taskrouter listeners --- .../helpers/chatChannelJanitor.private.ts | 4 +- .../janitorListener.private.ts | 92 +++++++++++--- .../postSurveyListener.private.ts | 2 - .../janitorListener.test.ts | 116 +++++++++++++++++- 4 files changed, 193 insertions(+), 21 deletions(-) diff --git a/functions/helpers/chatChannelJanitor.private.ts b/functions/helpers/chatChannelJanitor.private.ts index 6f8ac49c..e5403796 100644 --- a/functions/helpers/chatChannelJanitor.private.ts +++ b/functions/helpers/chatChannelJanitor.private.ts @@ -44,7 +44,7 @@ const deleteProxySession = async (context: Context, 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; } @@ -53,7 +53,7 @@ const deleteProxySession = async (context: Context, proxySession: strin return removed; } catch (err) { // eslint-disable-next-line no-console - console.warn('deleteProxySession error: ', err); + console.log('deleteProxySession error: ', err); return false; } }; diff --git a/functions/taskrouterListeners/janitorListener.private.ts b/functions/taskrouterListeners/janitorListener.private.ts index 87de5492..f2909ed0 100644 --- a/functions/taskrouterListeners/janitorListener.private.ts +++ b/functions/taskrouterListeners/janitorListener.private.ts @@ -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'; @@ -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, ]; @@ -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; } @@ -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 => new Promise((resolve) => { setTimeout(resolve, ms); @@ -114,7 +147,15 @@ export const shouldHandle = (event: EventFields) => eventTypes.includes(event.Ev export const handleEvent = async (context: Context, 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} =====`); @@ -143,6 +184,25 @@ export const handleEvent = async (context: Context, 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 ====='); @@ -155,9 +215,9 @@ export const handleEvent = async (context: Context, 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; diff --git a/functions/taskrouterListeners/postSurveyListener.private.ts b/functions/taskrouterListeners/postSurveyListener.private.ts index 00d9281d..dcc87d89 100644 --- a/functions/taskrouterListeners/postSurveyListener.private.ts +++ b/functions/taskrouterListeners/postSurveyListener.private.ts @@ -102,8 +102,6 @@ export const handleEvent = async (context: Context, 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' diff --git a/tests/taskrouterListeners/janitorListener.test.ts b/tests/taskrouterListeners/janitorListener.test.ts index 2e7b62ba..dc9ccdea 100644 --- a/tests/taskrouterListeners/janitorListener.test.ts +++ b/tests/taskrouterListeners/janitorListener.test.ts @@ -19,6 +19,7 @@ import { EventFields, EventType, TASK_WRAPUP, + TASK_COMPLETED, TASK_CANCELED, TASK_DELETED, TASK_SYSTEM_DELETED, @@ -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>(), + getTwilioClient: (): any => ({ + flexApi: { configuration: { get: () => ({ fetch: mockFetchFlexApiConfig }) } }, + }), CHAT_SERVICE_SID: 'CHxxx', FLEX_PROXY_SERVICE_SID: 'KCxxx', }; @@ -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(), EventType: TASK_CANCELED as EventType, TaskAttributes: JSON.stringify({ ...captureControlTaskAttributes, channelType }), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -114,6 +127,7 @@ describe('isCleanupBotCapture', () => { ...mock(), EventType: eventType, TaskAttributes: JSON.stringify(captureControlTaskAttributes), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -126,6 +140,7 @@ describe('isCleanupBotCapture', () => { ...mock(), EventType: TASK_CANCELED as EventType, TaskAttributes: JSON.stringify(nonPostSurveyTaskAttributes), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -145,6 +160,7 @@ describe('isCleanupCustomChannel', () => { ...mock(), EventType: eventType as EventType, TaskAttributes: JSON.stringify({ ...customChannelTaskAttributes, channelType }), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -160,6 +176,7 @@ describe('isCleanupCustomChannel', () => { ...mock(), EventType: eventType as EventType, TaskAttributes: JSON.stringify(nonCustomChannelTaskAttributes), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -190,6 +207,7 @@ describe('isCleanupCustomChannel', () => { ...mock(), EventType: TASK_CANCELED as EventType, TaskAttributes: JSON.stringify(taskAttributes), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -211,6 +229,7 @@ describe('isCleanupCustomChannel', () => { ...mock(), EventType: eventType as EventType, TaskAttributes: JSON.stringify(taskAttributes), + TaskChannelUniqueName: 'chat', }; await janitorListener.handleEvent(context, event); @@ -218,3 +237,98 @@ describe('isCleanupCustomChannel', () => { }, ); }); + +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(), + 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(), + 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(), + EventType: eventType as EventType, + TaskAttributes: JSON.stringify({ ...customChannelTaskAttributes, channelType }), + TaskChannelUniqueName: 'chat', + }; + await janitorListener.handleEvent(context, event); + + const { channelSid } = customChannelTaskAttributes; + expect(mockChannelJanitor).toHaveBeenCalledWith(context, { channelSid }); + }, + ); +});