Skip to content

Commit

Permalink
Add transfer task functionality (#10)
Browse files Browse the repository at this point in the history
* Added transferChatStart

* Added transferChatResolve
  • Loading branch information
GPaoloni authored Jun 8, 2020
1 parent b15d539 commit c3c28b4
Show file tree
Hide file tree
Showing 5 changed files with 879 additions and 1 deletion.
170 changes: 170 additions & 0 deletions functions/transferChatResolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import '@twilio-labs/serverless-runtime-types';
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types';
import {
responseWithCors,
bindResolve,
error400,
error500,
success,
} from 'tech-matters-serverless-helpers';

const TokenValidator = require('twilio-flex-token-validator').functionValidator;

type EnvVars = {
TWILIO_WORKSPACE_SID: string;
TWILIO_CHAT_TRANSFER_WORKFLOW_SID: string;
CHAT_SERVICE_SID: string;
};

export type Body = {
closeSid?: string;
keepSid?: string;
memberToKick?: string;
newStatus?: string;
};

async function closeTask(
context: Context<EnvVars>,
sid: string,
taskToCloseAttributes: any,
newStatus: string,
) {
// set the channelSid and ProxySessionSID to a dummy value. This keeps the session alive
const newTaskToCloseAttributes = {
...taskToCloseAttributes,
channelSid: 'CH00000000000000000000000000000000',
proxySessionSID: 'KC00000000000000000000000000000000',
};

const client = context.getTwilioClient();

await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(sid)
.update({ attributes: JSON.stringify(newTaskToCloseAttributes) });

// close the Task and set the proper status
const closedTask = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(sid)
.update({
assignmentStatus: 'completed',
reason: 'task transferred',
attributes: JSON.stringify({
...newTaskToCloseAttributes,
transferMeta: {
...newTaskToCloseAttributes.transferMeta,
transferStatus: newStatus,
},
}),
});

return closedTask;
}

async function kickMember(context: Context<EnvVars>, memberToKick: string, chatChannel: string) {
const client = context.getTwilioClient();

// kick out the counselor that is not required anymore
if (memberToKick) {
const memberKicked = await client.chat
.services(context.CHAT_SERVICE_SID)
.channels(chatChannel)
.members(memberToKick)
.remove();

return memberKicked;
}

return false;
}

async function closeTaskAndKick(context: Context<EnvVars>, body: Required<Body>) {
const client = context.getTwilioClient();

// retrieve attributes of the task to close
const taskToClose = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(body.closeSid)
.fetch();
const taskToCloseAttributes = JSON.parse(taskToClose.attributes);
const { channelSid } = taskToCloseAttributes;

const [closedTask] = await Promise.all([
closeTask(context, body.closeSid, taskToCloseAttributes, body.newStatus),
kickMember(context, body.memberToKick, channelSid),
]);

return closedTask;
}

async function updateTaskToKeep(context: Context<EnvVars>, body: Required<Body>) {
const client = context.getTwilioClient();

// retrieve attributes of the preserved task
const taskToKeep = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(body.keepSid)
.fetch();

const taskToKeepAttributes = JSON.parse(taskToKeep.attributes);

// update the status of the task that is preserved
const newTaskToKeepAttributes = {
...taskToKeepAttributes,
transferMeta: {
...taskToKeepAttributes.transferMeta,
transferStatus: body.newStatus,
},
};

const keptTask = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(body.keepSid)
.update({ attributes: JSON.stringify(newTaskToKeepAttributes) });

return keptTask;
}

export const handler: ServerlessFunctionSignature = TokenValidator(
async (context: Context<EnvVars>, event: Body, callback: ServerlessCallback) => {
const response = responseWithCors();
const resolve = bindResolve(callback)(response);

const { closeSid, keepSid, memberToKick, newStatus } = event;

try {
if (closeSid === undefined) {
resolve(error400('closeSid'));
return;
}
if (keepSid === undefined) {
resolve(error400('keepSid'));
return;
}
if (memberToKick === undefined) {
resolve(error400('memberToKick'));
return;
}
if (newStatus === undefined) {
resolve(error400('newStatus'));
return;
}

const validBody = { closeSid, keepSid, memberToKick, newStatus };

const [closedTask, keptTask] = await Promise.all([
closeTaskAndKick(context, validBody),
updateTaskToKeep(context, validBody),
]);

resolve(success({ closed: closedTask.sid, kept: keptTask.sid }));
} catch (err) {
resolve(error500(err));
}
},
);
163 changes: 163 additions & 0 deletions functions/transferChatStart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import '@twilio-labs/serverless-runtime-types';
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types';
import {
responseWithCors,
bindResolve,
error400,
error500,
success,
} from 'tech-matters-serverless-helpers';

const TokenValidator = require('twilio-flex-token-validator').functionValidator;

type EnvVars = {
TWILIO_WORKSPACE_SID: string;
TWILIO_CHAT_TRANSFER_WORKFLOW_SID: string;
CHAT_SERVICE_SID: string;
};

export type Body = {
taskSid?: string;
targetSid?: string;
workerName?: string;
mode?: string;
memberToKick?: string;
};

async function closeTask(context: Context<EnvVars>, sid: string, taskToCloseAttributes: any) {
// set the channelSid and ProxySessionSID to a dummy value. This keeps the session alive
const newTaskToCloseAttributes = {
...taskToCloseAttributes,
channelSid: 'CH00000000000000000000000000000000',
proxySessionSID: 'KC00000000000000000000000000000000',
};

const client = context.getTwilioClient();

await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(sid)
.update({ attributes: JSON.stringify(newTaskToCloseAttributes) });

// close the Task and set the proper status
const closedTask = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(sid)
.update({
assignmentStatus: 'completed',
reason: 'task transferred',
attributes: JSON.stringify(newTaskToCloseAttributes),
});

return closedTask;
}

async function kickMember(context: Context<EnvVars>, memberToKick: string, chatChannel: string) {
const client = context.getTwilioClient();

// kick out the counselor that is not required anymore
if (memberToKick) {
const memberKicked = await client.chat
.services(context.CHAT_SERVICE_SID)
.channels(chatChannel)
.members(memberToKick)
.remove();

return memberKicked;
}

return false;
}

async function closeTaskAndKick(context: Context<EnvVars>, body: Required<Body>) {
const client = context.getTwilioClient();

// retrieve attributes of the task to close
const taskToClose = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(body.taskSid)
.fetch();
const taskToCloseAttributes = JSON.parse(taskToClose.attributes);
const { channelSid } = taskToCloseAttributes;

const [closedTask] = await Promise.all([
closeTask(context, body.taskSid, taskToCloseAttributes),
kickMember(context, body.memberToKick, channelSid),
]);

return closedTask;
}

export const handler: ServerlessFunctionSignature = TokenValidator(
async (context: Context<EnvVars>, event: Body, callback: ServerlessCallback) => {
const client = context.getTwilioClient();

const response = responseWithCors();
const resolve = bindResolve(callback)(response);

const { taskSid, targetSid, workerName, mode, memberToKick } = event;

try {
if (taskSid === undefined) {
resolve(error400('taskSid'));
return;
}
if (targetSid === undefined) {
resolve(error400('targetSid'));
return;
}
if (workerName === undefined) {
resolve(error400('workerName'));
return;
}
if (mode === undefined) {
resolve(error400('mode'));
return;
}
if (memberToKick === undefined) {
resolve(error400('memberToKick'));
return;
}

// retrieve attributes of the original task
const originalTask = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks(taskSid)
.fetch();

const originalAttributes = JSON.parse(originalTask.attributes);

const newAttributes = {
...originalAttributes,
conversations: originalAttributes.conversation // set up attributes of the new task to link them to the original task in Flex Insights
? originalAttributes.conversation
: { conversation_id: taskSid },
ignoreAgent: workerName, // update task attributes to ignore the agent who transferred the task
targetSid, // update task attributes to include the required targetSid on the task (workerSid or a queueSid)
transferTargetType: targetSid.startsWith('WK') ? 'worker' : 'queue',
};

// create New task
const newTask = await client.taskrouter
.workspaces(context.TWILIO_WORKSPACE_SID)
.tasks.create({
workflowSid: context.TWILIO_CHAT_TRANSFER_WORKFLOW_SID,
taskChannel: originalTask.taskChannelUniqueName,
attributes: JSON.stringify(newAttributes),
});

if (mode === 'COLD') {
const validBody = { taskSid, targetSid, workerName, mode, memberToKick };
await closeTaskAndKick(context, validBody);
}

resolve(success({ taskSid: newTask.sid }));
} catch (err) {
resolve(error500(err));
}
},
);
3 changes: 2 additions & 1 deletion tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ export default {
MockRuntime,
backupEnv,
restoreEnv
};
};

Loading

0 comments on commit c3c28b4

Please sign in to comment.