Skip to content

Contact match #97

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

Merged
merged 11 commits into from
May 27, 2025
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
64 changes: 62 additions & 2 deletions src/adapters/clio/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,66 @@ async function findContact({ user, authHeader, phoneNumber, overridingFormat })
};
}

async function findContactWithName({ user, authHeader, name }) {
const matchedContactInfo = [];
const personInfo = await axios.get(`https://${user.hostname}/api/v4/contacts.json?type=Person&query=${name}&fields=id,name,title,company,primary_phone_number`, {
headers: { 'Authorization': authHeader }
});
extraDataTracking = {
ratelimitRemaining: personInfo.headers['x-ratelimit-remaining'],
ratelimitAmount: personInfo.headers['x-ratelimit-limit'],
ratelimitReset: personInfo.headers['x-ratelimit-reset']
};
if (personInfo.data.data.length > 0) {
for (var result of personInfo.data.data) {
const matterInfo = await axios.get(
`https://${user.hostname}/api/v4/matters.json?client_id=${result.id}&fields=id,display_number,description,status`,
{
headers: { 'Authorization': authHeader }
});
let matters = matterInfo.data.data.length > 0 ? matterInfo.data.data.map(m => { return { const: m.id, title: m.display_number, description: m.description, status: m.status } }) : null;
matters = matters?.filter(m => m.status !== 'Closed');
let associatedMatterInfo = await axios.get(
`https://${user.hostname}/api/v4/relationships.json?contact_id=${result.id}&fields=matter{id,display_number,description,status}`,
{
headers: { 'Authorization': authHeader }
});
extraDataTracking = {
ratelimitRemaining: associatedMatterInfo.headers['x-ratelimit-remaining'],
ratelimitAmount: associatedMatterInfo.headers['x-ratelimit-limit'],
ratelimitReset: associatedMatterInfo.headers['x-ratelimit-reset']
};
let associatedMatters = associatedMatterInfo.data.data.length > 0 ? associatedMatterInfo.data.data.map(m => { return { const: m.matter.id, title: m.matter.display_number, description: m.matter.description, status: m.matter.status } }) : null;
associatedMatters = associatedMatters?.filter(m => m.status !== 'Closed');
let returnedMatters = [];
returnedMatters = returnedMatters.concat(matters ?? []);
returnedMatters = returnedMatters.concat(associatedMatters ?? []);
matchedContactInfo.push({
id: result.id,
name: result.name,
title: result.title ?? "",
type: 'contact',
company: result.company?.name ?? "",
phone: result.primary_phone_number ?? "",
additionalInfo: returnedMatters.length > 0 ?
{
matters: returnedMatters,
logTimeEntry: user.userSettings?.clioDefaultTimeEntryTick ?? true,
nonBillable: user.userSettings?.clioDefaultNonBillableTick ?? false
} :
{
logTimeEntry: user.userSettings?.clioDefaultTimeEntryTick ?? true
}
})
}
}

return {
successful: true,
matchedContactInfo
}
}

async function createContact({ user, authHeader, phoneNumber, newContactName }) {
let extraDataTracking = {};
const personInfo = await axios.post(
Expand Down Expand Up @@ -744,7 +804,6 @@ function upsertTranscript({ body, transcript }) {
}
return body;
}

exports.getAuthType = getAuthType;
exports.getOauthInfo = getOauthInfo;
exports.getUserInfo = getUserInfo;
Expand All @@ -756,4 +815,5 @@ exports.createMessageLog = createMessageLog;
exports.updateMessageLog = updateMessageLog;
exports.findContact = findContact;
exports.createContact = createContact;
exports.unAuthorize = unAuthorize;
exports.unAuthorize = unAuthorize;
exports.findContactWithName = findContactWithName;
6 changes: 4 additions & 2 deletions src/adapters/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,8 @@
"required": true
}
]
}
},
"useContactSearch": true
},
"embeddedOnCrmPage": {
"welcomePage": {
Expand Down Expand Up @@ -1752,7 +1753,8 @@
"required": true
}
]
}
},
"useContactSearch": true
}
},
"googleSheets": {
Expand Down
98 changes: 96 additions & 2 deletions src/adapters/netsuite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ async function upsertCallDisposition({ user, existingCallLog, authHeader, dispos
}
}
async function findContact({ user, authHeader, phoneNumber, overridingFormat }) {
// const requestStartTime = new Date().getTime();
// const requestStartTime = new Date().getTime();
try {
const phoneNumberObj = parsePhoneNumber(phoneNumber.replace(' ', '+'));
const phoneNumberWithoutCountryCode = phoneNumberObj.number.significant;
Expand Down Expand Up @@ -344,6 +344,13 @@ async function findContact({ user, authHeader, phoneNumber, overridingFormat })
additionalInfo: null,
isNewContact: true
});
//Enable this after testing
// matchedContactInfo.push({
// id: 'searchContact',
// name: 'Search NetSuite',
// additionalInfo: null,
// isFindContact: true
// });
// const requestEndTime = new Date().getTime();
// console.log({ message: "Time taken to find contact", time: (requestEndTime - requestStartTime) / 1000 });
return {
Expand Down Expand Up @@ -376,8 +383,94 @@ async function findContact({ user, authHeader, phoneNumber, overridingFormat })
}
}
}
async function findContactWithName({ user, authHeader, name }) {
const matchedContactInfo = [];
const contactSearch = user.userSettings?.contactsSearchId?.value ?? [];
if (contactSearch.length === 0) {
contactSearch.push('contact', 'customer');
}
const { enableSalesOrderLogging = false } = user.userSettings;
// const contactQuery = `SELECT id,firstName,middleName,lastName,entitytitle,phone FROM contact WHERE firstname ='${name}' OR lastname ='${name}' OR (firstname || ' ' || lastname) ='${name}'`;
const contactQuery = `SELECT * FROM contact WHERE LOWER(firstname) =LOWER('${name}') OR LOWER(lastname) =LOWER('${name}') OR LOWER(entitytitle) =LOWER('${name}')`;
const customerQuery = `SELECT * FROM customer WHERE LOWER(firstname) =LOWER('${name}') OR LOWER(lastname) =LOWER('${name}') OR LOWER(entitytitle) =LOWER('${name}')`;
if (contactSearch.includes('contact')) {
const personInfo = await axios.post(
`https://${user.hostname.split(".")[0]}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql`,
{
q: contactQuery
},
{
headers: { 'Authorization': authHeader, 'Content-Type': 'application/json', 'Prefer': 'transient' }
});
if (personInfo.data.items.length > 0) {
for (var result of personInfo.data.items) {
let firstName = result.firstname ?? '';
let middleName = result.middlename ?? '';
let lastName = result.lastname ?? '';
const contactName = (firstName + middleName + lastName).length > 0 ? `${firstName} ${middleName} ${lastName}` : result.entitytitle;
matchedContactInfo.push({
id: result.id,
name: contactName,
phone: result.phone ?? '',
homephone: result.homephone ?? '',
mobilephone: result.mobilephone ?? '',
officephone: result.officephone ?? '',
additionalInfo: null,
type: 'contact'
})
}
}
}
if (contactSearch.includes('customer')) {
const customerInfo = await axios.post(
`https://${user.hostname.split(".")[0]}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql`,
{
q: customerQuery
},
{
headers: { 'Authorization': authHeader, 'Content-Type': 'application/json', 'Prefer': 'transient' }
});
if (customerInfo.data.items.length > 0) {
for (const result of customerInfo.data.items) {
let salesOrders = [];
try {
if (enableSalesOrderLogging.value) {
const salesOrderResponse = await findSalesOrdersAgainstContact({ user, authHeader, contactId: result.id });
for (const salesOrder of salesOrderResponse?.data?.items) {
salesOrders.push({
const: salesOrder?.id,
title: salesOrder?.trandisplayname
});
}
}
} catch (e) {
console.log({ message: "Error in SalesOrder search" });
}
let firstName = result.firstname ?? '';
let middleName = result.middlename ?? '';
let lastName = result.lastname ?? '';
const customerName = (firstName + middleName + lastName).length > 0 ? `${firstName} ${middleName} ${lastName}` : result.entitytitle;
matchedContactInfo.push({
id: result.id,
name: customerName,
phone: result.phone ?? '',
homephone: result.homephone ?? '',
mobilephone: result.mobilephone ?? '',
altphone: result.altphone ?? '',
additionalInfo: salesOrders.length > 0 ? { salesorder: salesOrders } : {},
type: 'custjob'
})
}
}
}
return {
successful: true,
matchedContactInfo
}
}

async function createCallLog({ user, contactInfo, authHeader, callLog, note, additionalSubmission, aiNote, transcript }) {
console.log({ Duration: callLog.duration });
try {
const title = callLog.customSubject ?? `${callLog.direction} Call ${callLog.direction === 'Outbound' ? 'to' : 'from'} ${contactInfo.name}`;
const oneWorldEnabled = user?.platformAdditionalInfo?.oneWorldEnabled;
Expand Down Expand Up @@ -1234,4 +1327,5 @@ exports.updateMessageLog = updateMessageLog;
exports.findContact = findContact;
exports.createContact = createContact;
exports.unAuthorize = unAuthorize;
exports.upsertCallDisposition = upsertCallDisposition;
exports.upsertCallDisposition = upsertCallDisposition;
exports.findContactWithName = findContactWithName;
84 changes: 83 additions & 1 deletion src/core/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,87 @@ async function createContact({ platform, userId, phoneNumber, newContactName, ne
}
}

async function findContactWithName({ platform, userId, name }) {
try {
let user = await UserModel.findOne({
where: {
id: userId,
platform
}
});
if (!user || !user.accessToken) {
return {
successful: false,
returnMessage: {
message: `No contact found with name ${name}`,
messageType: 'warning',
ttl: 5000
}
};
}
const platformModule = require(`../adapters/${platform}`);
const authType = platformModule.getAuthType();
let authHeader = '';
switch (authType) {
case 'oauth':
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname })));
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
authHeader = `Bearer ${user.accessToken}`;
break;
case 'apiKey':
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
authHeader = `Basic ${basicAuth}`;
break;
}
const { successful, matchedContactInfo, returnMessage } = await platformModule.findContactWithName({ user, authHeader, name });
if (matchedContactInfo != null && matchedContactInfo?.filter(c => !c.isNewContact)?.length > 0) {
return { successful, returnMessage, contact: matchedContactInfo };
}
else {
if (returnMessage) {
return {
successful,
returnMessage,
contact: matchedContactInfo,
}
}
return {
successful,
returnMessage:
{
message: `No contact found with name ${name} `,
messageType: 'warning',
ttl: 5000
},
contact: matchedContactInfo
};
}
} catch (e) {
console.error(`platform: ${platform} \n${e.stack} \n${JSON.stringify(e.response?.data)}`);
if (e.response?.status === 429) {
return {
successful: false,
returnMessage: errorMessage.rateLimitErrorMessage({ platform })
};
}
else if (e.response?.status >= 400 && e.response?.status < 410) {
return {
successful: false,
returnMessage: errorMessage.authorizationErrorMessage({ platform }),
};
}
return {
successful: false,
returnMessage:
{
message: `Error finding contacts`,
messageType: 'warning',
ttl: 5000
}
};
}
}

exports.findContact = findContact;
exports.createContact = createContact;
exports.createContact = createContact;
exports.findContactWithName = findContactWithName;
47 changes: 47 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const analytics = require('./lib/analytics');
const util = require('./lib/util');
const dynamoose = require('dynamoose');
const googleSheetsExtra = require('./adapters/googleSheets/extra.js');
const { truncate } = require('fs');
let packageJson = null;
try {
packageJson = require('./package.json');
Expand Down Expand Up @@ -1019,6 +1020,52 @@ app.post('/messageLog', async function (req, res) {
});
});

app.get('/custom/contact/search', async function (req, res) {
const requestStartTime = new Date().getTime();
let platformName = null;
let success = false;
let resultCount = 0;
let statusCode = 200;
const { hashedExtensionId, hashedAccountId, userAgent, ip, author } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
try {
const jwtToken = req.query.jwtToken;
if (jwtToken) {
const { id: userId, platform } = jwt.decodeJwt(jwtToken);
platformName = platform;
const { successful, returnMessage, contact } = await contactCore.findContactWithName({ platform, userId, name: req.query.name });
res.status(200).send({ successful, returnMessage, contact });
success = successful;
}
else {
res.status(400).send('Please go to Settings and authorize CRM platform');
success = false;
}

}
catch (e) {
console.log(`platform: ${platformName} \n${e.stack}`);
statusCode = e.response?.status ?? 'unknown';
res.status(400).send(e);
success = false;
}
const requestEndTime = new Date().getTime();
analytics.track({
eventName: 'Contact Search by Name',
interfaceName: 'contactSearchByName',
adapterName: platformName,
rcAccountId: hashedAccountId,
extensionId: hashedExtensionId,
success,
requestDuration: (requestEndTime - requestStartTime) / 1000,
userAgent,
ip,
author,
extras: {
statusCode
}
});

});
if (process.env.IS_PROD === 'false') {
app.post('/registerMockUser', async function (req, res) {
const secretKey = req.query.secretKey;
Expand Down