Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CHI-3069: Backend contact create #730

Merged
merged 18 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to set capacity to 1 here? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always set it back to 1 on certain events. With backend manual pulling, instead of trying to track capacity, we just always have it set to 1, them set it to the value required to take an extra task when manually pulling or accepting transfers, then set it back to 1 again when we're done.

There is no issue with being 'over capacity' other than not being able to accept more tasks, so this is simpler than trying to track capacity against tasks

Also this logic isn't changed in this PR

} 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could reuse the hasTransferStarted method (functions/transfer/helpers.private.ts)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not worth pulling it in for IMHO, when it's a lambda and we can import stuff properly then sure

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
Loading