Skip to content

Commit

Permalink
Merge pull request #730 from techmatters/CHI-3069-backend_contact_create
Browse files Browse the repository at this point in the history
CHI-3069: Backend contact create
  • Loading branch information
stephenhand authored Dec 19, 2024
2 parents d99617d + 9fb53c4 commit b62d9c0
Show file tree
Hide file tree
Showing 8 changed files with 835 additions and 113 deletions.
479 changes: 479 additions & 0 deletions functions/hrm/populateHrmContactFormFromTask.ts

Large diffs are not rendered by default.

53 changes: 21 additions & 32 deletions functions/taskrouterListeners/adjustCapacityListener.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,41 +45,30 @@ export const shouldHandle = (event: EventFields) => eventTypes.includes(event.Ev
* @param event
*/
export const handleEvent = async (context: Context<EnvVars>, event: EventFields) => {
try {
const {
EventType: eventType,
WorkerSid: workerSid,
TaskChannelUniqueName: taskChannelUniqueName,
} = event;
if (taskChannelUniqueName !== 'chat') return;
console.log(`===== Executing AdjustCapacityListener for event: ${eventType} =====`);
const serviceConfig = await context.getTwilioClient().flexApi.configuration.get().fetch();
const {
feature_flags: {
enable_manual_pulling: enabledManualPulling,
enable_backend_manual_pulling: enableBackendManualPulling,
},
} = serviceConfig.attributes;
const { WorkerSid: workerSid, TaskChannelUniqueName: taskChannelUniqueName } = event;
if (taskChannelUniqueName !== 'chat') return;
const serviceConfig = await context.getTwilioClient().flexApi.configuration.get().fetch();
const {
feature_flags: {
enable_manual_pulling: enabledManualPulling,
enable_backend_manual_pulling: enableBackendManualPulling,
},
} = serviceConfig.attributes;

if (enabledManualPulling && enableBackendManualPulling) {
const { path } = Runtime.getFunctions().adjustChatCapacity;
if (enabledManualPulling && enableBackendManualPulling) {
const { path } = Runtime.getFunctions().adjustChatCapacity;

// eslint-disable-next-line global-require,import/no-dynamic-require,prefer-destructuring
const adjustChatCapacity: AdjustChatCapacityType = require(path).adjustChatCapacity;
const body = {
workerSid,
adjustment: 'setTo1',
} as const;
// eslint-disable-next-line global-require,import/no-dynamic-require,prefer-destructuring
const adjustChatCapacity: AdjustChatCapacityType = require(path).adjustChatCapacity;
const body = {
workerSid,
adjustment: 'setTo1',
} as const;

await adjustChatCapacity(context, body);
console.log('===== AdjustCapacityListener successful =====');
} else {
console.log('===== AdjustCapacityListener skipped - flag not enabled =====');
}
} catch (err) {
console.log('===== AdjustCapacityListener has failed =====');
console.log(String(err));
throw err;
const { status, message } = await adjustChatCapacity(context, body);
console.log(`===== AdjustCapacityListener completed: status ${status}, '${message}' =====`);
} else {
console.log('===== AdjustCapacityListener skipped - flag not enabled =====');
}
};

Expand Down
51 changes: 20 additions & 31 deletions functions/taskrouterListeners/createContactListener.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,39 +48,28 @@ const isCreateContactTask = (
export const shouldHandle = (event: EventFields) => eventTypes.includes(event.EventType);

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

console.log(`===== Executing CreateContactListener for event: ${eventType} =====`);
const taskAttributes = JSON.parse(taskAttributesString);

if (isCreateContactTask(eventType, taskAttributes)) {
console.log('Handling create contact...');

// 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 ((taskAttributes.customChannelType || taskAttributes.channelType) === 'web') {
// Add task sid to tasksSids channel attr so we can end the chat from webchat client (see endChat function)
const addTaskHandlerPath =
Runtime.getFunctions()['helpers/addTaskSidToChannelAttributes'].path;
const addTaskSidToChannelAttributes = require(addTaskHandlerPath)
.addTaskSidToChannelAttributes as AddTaskSidToChannelAttributes;
await addTaskSidToChannelAttributes(context, event);
}

console.log('Finished handling create contact.');
return;
const { EventType: eventType, TaskAttributes: taskAttributesString } = event;
const taskAttributes = JSON.parse(taskAttributesString);

if (isCreateContactTask(eventType, taskAttributes)) {
console.log('Handling create contact...');

// 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 ((taskAttributes.customChannelType || taskAttributes.channelType) === 'web') {
// Add task sid to tasksSids channel attr so we can end the chat from webchat client (see endChat function)
const addTaskHandlerPath =
Runtime.getFunctions()['helpers/addTaskSidToChannelAttributes'].path;
const addTaskSidToChannelAttributes = require(addTaskHandlerPath)
.addTaskSidToChannelAttributes as AddTaskSidToChannelAttributes;
await addTaskSidToChannelAttributes(context, event);
}

console.log('===== CreateContactListener finished successfully =====');
} catch (err) {
console.log('===== CreateContactListener has failed =====');
console.log(String(err));
throw err;
console.log('Finished handling create contact.');
}
};

Expand Down
191 changes: 191 additions & 0 deletions functions/taskrouterListeners/createHrmContactListener.private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

/* eslint-disable global-require */
/* eslint-disable import/no-dynamic-require */
import '@twilio-labs/serverless-runtime-types';
import { Context } from '@twilio-labs/serverless-runtime-types/types';

import {
EventFields,
EventType,
RESERVATION_ACCEPTED,
TaskrouterListener,
} from '@tech-matters/serverless-helpers/taskrouter';
import { HrmContact, PrepopulateForm } from '../hrm/populateHrmContactFormFromTask';

export const eventTypes: EventType[] = [RESERVATION_ACCEPTED];

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

// Temporarily copied to this repo, will share the flex types when we move them into the same repo

const BLANK_CONTACT: HrmContact = {
id: '',
timeOfContact: new Date().toISOString(),
taskId: null,
helpline: '',
rawJson: {
childInformation: {},
callerInformation: {},
caseInformation: {},
callType: '',
contactlessTask: {
channel: 'web',
date: '',
time: '',
createdOnBehalfOf: '',
helpline: '',
},
categories: {},
},
channelSid: '',
serviceSid: '',
channel: 'default',
createdBy: '',
createdAt: '',
updatedBy: '',
updatedAt: '',
queueName: '',
number: '',
conversationDuration: 0,
csamReports: [],
conversationMedia: [],
};

/**
* 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.
*/
export const shouldHandle = ({
TaskAttributes: taskAttributesString,
TaskSid: taskSid,
EventType: eventType,
}: EventFields) => {
if (!eventTypes.includes(eventType)) return false;

const { isContactlessTask, transferTargetType } = JSON.parse(taskAttributesString ?? '{}');

if (isContactlessTask) {
console.debug(`Task ${taskSid} is a contactless task, contact was already created in Flex.`);
return false;
}

if (transferTargetType) {
console.debug(
`Task ${taskSid} was created to receive a ${transferTargetType} transfer. The original contact will be used so a new one will not be created.`,
);
return false;
}
return true;
};

export const handleEvent = async (
{ getTwilioClient, HRM_STATIC_KEY, TWILIO_WORKSPACE_SID }: Context<EnvVars>,
{ TaskAttributes: taskAttributesString, TaskSid: taskSid, WorkerSid: workerSid }: EventFields,
) => {
const taskAttributes = taskAttributesString ? JSON.parse(taskAttributesString) : {};
const { channelSid } = taskAttributes;

const client = getTwilioClient();
const serviceConfig = await client.flexApi.configuration.get().fetch();

const {
definitionVersion,
hrm_base_url: hrmBaseUrl,
hrm_api_version: hrmApiVersion,
form_definitions_version_url: configFormDefinitionsVersionUrl,
assets_bucket_url: assetsBucketUrl,
helpline_code: helplineCode,
channelType,
customChannelType,
feature_flags: { enable_backend_hrm_contact_creation: enableBackendHrmContactCreation },
} = serviceConfig.attributes;
const formDefinitionsVersionUrl =
configFormDefinitionsVersionUrl || `${assetsBucketUrl}/form-definitions/${helplineCode}/v1`;
if (!enableBackendHrmContactCreation) {
console.debug(
`enable_backend_hrm_contact_creation is not set, the contact associated with task ${taskSid} will be created from Flex.`,
);
return;
}
console.debug('Creating HRM contact for task', taskSid);
const hrmBaseAccountUrl = `${hrmBaseUrl}/${hrmApiVersion}/accounts/${serviceConfig.accountSid}`;

const newContact: HrmContact = {
...BLANK_CONTACT,
channel: (customChannelType || channelType) as HrmContact['channel'],
rawJson: {
definitionVersion,
...BLANK_CONTACT.rawJson,
},
twilioWorkerId: workerSid as HrmContact['twilioWorkerId'],
taskId: taskSid as HrmContact['taskId'],
channelSid: channelSid ?? '',
serviceSid: (channelSid && serviceConfig.chatServiceInstanceSid) ?? '',
// We set createdBy to the workerSid because the contact is 'created' by the worker who accepts the task
createdBy: workerSid as HrmContact['createdBy'],
};

const prepopulatePath = Runtime.getFunctions()['hrm/populateHrmContactFormFromTask'].path;
const { populateHrmContactFormFromTask } = require(prepopulatePath) as PrepopulateForm;
const populatedContact = await populateHrmContactFormFromTask(
taskAttributes,
newContact,
formDefinitionsVersionUrl,
);
const options: RequestInit = {
method: 'POST',
body: JSON.stringify(populatedContact),
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${HRM_STATIC_KEY}`,
},
};
const response = await fetch(`${hrmBaseAccountUrl}/contacts`, options);
if (!response.ok) {
console.error(
`Failed to create HRM contact for task ${taskSid} - status: ${response.status} - ${response.statusText}`,
await response.text(),
);
return;
}
const { id }: HrmContact = await response.json();
console.info(`Created HRM contact with id ${id} for task ${taskSid}`);

const taskContext = client.taskrouter.v1.workspaces.get(TWILIO_WORKSPACE_SID).tasks.get(taskSid);
const currentTaskAttributes = (await taskContext.fetch()).attributes; // Less chance of race conditions if we fetch the task attributes again, still not the best...
const updatedAttributes = {
...JSON.parse(currentTaskAttributes),
contactId: id.toString(),
};
await taskContext.update({ attributes: JSON.stringify(updatedAttributes) });
};

/**
* The taskrouter callback expects that all taskrouter listeners return
* a default object of type TaskrouterListener.
*/
const createHrmContactListener: TaskrouterListener = {
shouldHandle,
handleEvent,
};

export default createHrmContactListener;
Loading

0 comments on commit b62d9c0

Please sign in to comment.