-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from 17 commits
aca3973
a283767
f6e9c5c
8cffc60
ee0c6b9
c335fc2
0546b75
93b1dde
a8d4b84
217c503
91f0d91
034b12e
bcc1583
c59e530
316abb2
62f76d7
8c76aa3
9fb53c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/** | ||
* 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/prepopulateForm'; | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this could reuse the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 contactForApi: 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/prepopulateForm'].path; | ||
const { prepopulateForm } = require(prepopulatePath) as PrepopulateForm; | ||
await prepopulateForm(taskAttributes, contactForApi, formDefinitionsVersionUrl); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think is harder to follow what is really happening here, cause There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I won't make it a pure function because that makes it significantly uglier for deep objects and there's no practical benefit here since it's a brand new contact anyway. I thought using 'populate' in the method name would have made the mutation effect clear, bad assumption on my part. So the function still mutates but we reassign the output to a new variable to be clear. I also changed the method name to be more descriptive There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reassignment is good enough, I don't mind it being pure as much as being explicit that the contact was transformed. Thanks for the change! |
||
const options: RequestInit = { | ||
method: 'POST', | ||
body: JSON.stringify(contactForApi), | ||
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; |
There was a problem hiding this comment.
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? 🤔
There was a problem hiding this comment.
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