-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cb762e2
commit 550afe6
Showing
4 changed files
with
618 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
module.exports = { | ||
|
||
|
||
friendlyName: 'Get enriched', | ||
|
||
|
||
description: 'Search for the contact indicated and return enriched data.', | ||
|
||
|
||
inputs: { | ||
|
||
emailAddress: { type: 'string', defaultsTo: '', }, | ||
linkedinUrl: { type: 'string', defaultsTo: '', }, | ||
firstName: { type: 'string', defaultsTo: '', }, | ||
lastName: { type: 'string', defaultsTo: '', }, | ||
organization: { type: 'string', defaultsTo: '', }, | ||
|
||
}, | ||
|
||
|
||
exits: { | ||
|
||
success: { | ||
outputFriendlyName: 'Report', | ||
outputDescription: 'All available, enriched info about this person and their current employer.', | ||
outputType: { | ||
person: { | ||
emailAddress: 'string', | ||
linkedinUrl: 'string', | ||
firstName: 'string', | ||
lastName: 'string', | ||
organization: 'string', | ||
title: 'string', | ||
phone: 'string', | ||
}, | ||
employer: { | ||
organization: 'string', | ||
numberOfEmployees: 'number', | ||
emailDomain: 'string', | ||
linkedinCompanyPageUrl: 'string', | ||
technologies: [{name: 'string', category: 'string'}] | ||
} | ||
} | ||
}, | ||
|
||
}, | ||
|
||
|
||
fn: async function ({emailAddress,linkedinUrl,firstName,lastName,organization}) { | ||
require('assert')(sails.config.custom.iqSecret); | ||
|
||
let RX_TECHNOLOGY_CATEGORIES = /(device|security|endpoint|configuration management|data management platforms|mobility management|identity|information technology|IT$|employee experience|apple)/i; | ||
|
||
// [?] https://developer.leadiq.com/#query-searchPeople | ||
// [?] https://developer.leadiq.com/#definition-SearchPeopleInput | ||
// [?] https://graphql.org/learn/serving-over-http/ | ||
let emailDomain = emailAddress.match(/@([^@]+)$/) && emailAddress.match(/@([^@]+)$/)[1] || ''; | ||
|
||
let searchExpr = `{ | ||
${emailAddress? 'email: '+ JSON.stringify(emailAddress) : ''} | ||
${linkedinUrl? 'linkedinUrl: '+ JSON.stringify(linkedinUrl) : ''} | ||
${firstName? 'firstName: '+ JSON.stringify(firstName) : ''} | ||
${lastName? 'lastName: '+ JSON.stringify(lastName) : ''} | ||
${organization || emailDomain ? (`company: { | ||
${organization? 'name: '+ JSON.stringify(organization) : ''} | ||
${emailDomain? 'domain: '+ JSON.stringify(emailDomain)+' '+'emailDomain: '+ JSON.stringify(emailDomain) : ''} | ||
searchInPastCompanies: false | ||
strict: false | ||
}`) : ''} | ||
}`; //sails.log('GraphQL query:',searchExpr); | ||
let report = await sails.helpers.http.get('https://api.leadiq.com/graphql', { | ||
query: `{ searchPeople(input: ${searchExpr}) { | ||
totalResults | ||
results { | ||
_id | ||
name { first last } | ||
linkedin { linkedinId linkedinUrl status updatedAt } | ||
profiles { network id username url status updatedAt } | ||
location { country areaLevel1 city fullAddress type status updatedAt } | ||
personalPhones { value type status verificationStatus } | ||
currentPositions { | ||
title | ||
emails { value type status } | ||
phones { value type status verificationStatus } | ||
companyInfo { | ||
name | ||
domain | ||
country | ||
address | ||
linkedinUrl | ||
numberOfEmployees | ||
technologies { name category parentCategory attributes categories } | ||
} | ||
} | ||
} | ||
} | ||
}`, | ||
}, { | ||
Authorization: `Basic ${sails.config.custom.iqSecret}`, | ||
'content-type': 'application/json' | ||
}); | ||
|
||
if (report.errors) { | ||
sails.log.warn('Errors returned from IQ API when attempting to search for a matching contact:',report.errors); | ||
} | ||
|
||
// sails.log('person search results:',require('util').inspect(report.data.searchPeople.results, {depth:null})); | ||
let foundPerson = report.data.searchPeople.results[0]; //sails.log('Found person:',foundPerson); | ||
let foundPosition = foundPerson && foundPerson.currentPositions && foundPerson.currentPositions.length >= 1 ? foundPerson.currentPositions[0] : undefined; | ||
|
||
let person; | ||
if (foundPerson) { | ||
person = { | ||
emailAddress: emailAddress? emailAddress : foundPosition && foundPosition.emails[0]? foundPosition.emails[0].value : '', | ||
linkedinUrl: linkedinUrl? linkedinUrl : foundPerson.linkedin.linkedinUrl, | ||
firstName: firstName? firstName : foundPerson.name.first, | ||
lastName: lastName? lastName : foundPerson.name.last, | ||
organization: organization? organization : foundPosition? foundPosition.companyInfo.name : '', | ||
title: foundPosition? foundPosition.title : '', | ||
phone: foundPerson.personalPhones[0] && foundPerson.personalPhones[0].status !== 'Suppressed' ? foundPerson.personalPhones[0].value : '', | ||
}; | ||
}//fi | ||
|
||
|
||
|
||
|
||
|
||
// If no person was found, then try and look up the organization by itself. | ||
let employer; | ||
if (foundPosition) { | ||
employer = { | ||
organization: organization? organization : foundPosition.companyInfo.name || '', | ||
numberOfEmployees: foundPosition.companyInfo.numberOfEmployees || 0, | ||
emailDomain:( foundPosition.companyInfo.domain? foundPosition.companyInfo.domain : emailDomain )|| '', | ||
linkedinCompanyPageUrl: foundPosition.companyInfo.linkedinUrl || '', | ||
technologies: foundPosition.companyInfo.technologies? foundPosition.companyInfo.technologies | ||
.filter((tech) => tech.category.match(RX_TECHNOLOGY_CATEGORIES)) | ||
.map((tech) => ({ name: tech.name, category: tech.category })) : [] | ||
}; | ||
} else { | ||
let report = await sails.helpers.http.get('https://api.leadiq.com/graphql', { | ||
query: `{ searchCompany(input: { | ||
${organization? 'name: '+ JSON.stringify(organization) : ''} | ||
${emailDomain? 'domain: '+ JSON.stringify(emailDomain) : ''} | ||
}) { | ||
totalResults | ||
results { | ||
name | ||
domain | ||
country | ||
address | ||
numberOfEmployees | ||
linkedinUrl | ||
technologies { name category parentCategory attributes categories } | ||
} | ||
} | ||
}` | ||
}, { | ||
Authorization: `Basic ${sails.config.custom.iqSecret}`, | ||
'content-type': 'application/json' | ||
}); | ||
// sails.log('company search report:',report); | ||
|
||
if (report.errors) { | ||
sails.log.warn('Errors returned from IQ API when attempting to search directly for a matching organization:',report.errors); | ||
} | ||
let foundEmployer = report.data.searchCompany.results[0]; //sails.log(foundEmployer); | ||
if (foundEmployer) { | ||
employer = { | ||
organization: organization? organization : foundEmployer.name || '', | ||
numberOfEmployees: foundEmployer.numberOfEmployees || 0, | ||
emailDomain: emailDomain? emailDomain : foundEmployer.domain || '', | ||
linkedinCompanyPageUrl: foundEmployer.linkedinUrl || '', | ||
technologies: foundEmployer.technologies? foundEmployer.technologies | ||
.filter((tech) => tech.category.match(RX_TECHNOLOGY_CATEGORIES)) | ||
.map((tech) => ({ name: tech.name, category: tech.category })) : [] | ||
};// process.stdout.write(JSON.stringify(employer.technologies,0,2)); | ||
} | ||
}//fi | ||
|
||
return { | ||
person, | ||
employer | ||
}; | ||
|
||
} | ||
|
||
|
||
}; | ||
|
205 changes: 205 additions & 0 deletions
205
api/helpers/salesforce/update-or-create-contact-and-account.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
module.exports = { | ||
|
||
|
||
friendlyName: 'Update or create contact and account', | ||
|
||
|
||
description: 'Upsert contact±account into Salesforce given fresh data about a particular person, and fresh IQ-enrichment data about the person and account.', | ||
|
||
|
||
inputs: { | ||
|
||
// Find by… | ||
emailAddress: { type: 'string' }, | ||
linkedinUrl: { type: 'string' }, | ||
|
||
// Set… | ||
firstName: { type: 'string', required: true }, | ||
lastName: { type: 'string', required: true }, | ||
organization: { type: 'string' }, | ||
primaryBuyingSituation: { type: 'string' }, | ||
psychologicalStage: { | ||
type: 'string', | ||
isIn: [ | ||
'1 - Unaware', | ||
'2 - Aware', | ||
'3 - Intrigued', | ||
'4 - Has use case', | ||
'5 - Personally confident', | ||
'6 - Has team buy-in' | ||
] | ||
}, | ||
}, | ||
|
||
|
||
exits: { | ||
|
||
success: { | ||
outputType: { | ||
salesforceAccountId: 'string', | ||
salesforceContactId: 'string' | ||
} | ||
}, | ||
|
||
}, | ||
|
||
|
||
fn: async function ({emailAddress, linkedinUrl, firstName, lastName, organization, primaryBuyingSituation, psychologicalStage}) { | ||
require('assert')(sails.config.custom.salesforceIntegrationUsername); | ||
require('assert')(sails.config.custom.salesforceIntegrationPasskey); | ||
require('assert')(sails.config.custom.iqSecret); | ||
|
||
|
||
if(!emailAddress && !linkedinUrl){ | ||
throw new Error('UsageError: when updating or creating a contact and account in salesforce, either an email or linkedInUrl is required.'); | ||
} | ||
// Send the information we have to the enrichment helper. | ||
let enrichmentData = await sails.helpers.iq.getEnriched(emailAddress, linkedinUrl, firstName, lastName, organization); | ||
// console.log(enrichmentData); | ||
|
||
// Log in to Salesforce. | ||
let jsforce = require('jsforce'); | ||
let salesforceConnection = new jsforce.Connection({ | ||
loginUrl : 'https://fleetdm.my.salesforce.com' | ||
}); | ||
|
||
let salesforceAccountOwnerId; | ||
|
||
await salesforceConnection.login(sails.config.custom.salesforceIntegrationUsername, sails.config.custom.salesforceIntegrationPasskey); | ||
|
||
let salesforceAccountId; | ||
if(!organization && !enrichmentData.employer){ | ||
// Special sacraficial meat cave where the contacts with no organization go. | ||
// https://fleetdm.lightning.force.com/lightning/r/Account/0014x000025JC8DAAW/view | ||
salesforceAccountId = '0014x000025JC8DAAW'; | ||
salesforceAccountOwnerId = '0054x00000735wDAAQ'; | ||
} else { | ||
let existingAccountRecord = await salesforceConnection.sobject('Account') | ||
.findOne({ | ||
'Website': enrichmentData.employer.emailDomain, | ||
// 'LinkedIn_company_URL__c': enrichmentData.employer.linkedinCompanyPageUrl // TODO: if this information is not present on an existing account, nothing will be returned. | ||
}); | ||
// console.log(existingAccountRecord); | ||
if(existingAccountRecord) { | ||
// Store the ID of the Account record we found. | ||
salesforceAccountId = existingAccountRecord.Id; | ||
salesforceAccountOwnerId = existingAccountRecord.OwnerId; | ||
// console.log('exising account found!', salesforceAccountId); | ||
} else { | ||
|
||
|
||
let roundRobinUsers = await salesforceConnection.sobject('User') | ||
.find({ | ||
AE_Round_robin__c: true,// eslint-disable-line camelcase | ||
}); | ||
let userWithEarliestAssignTimeStamp = _.sortBy(roundRobinUsers, 'AE_Account_Assignment_round_robin__c')[0]; | ||
|
||
let today = new Date(); | ||
let nowOn = today.toISOString().replace('Z', '+0000'); | ||
|
||
salesforceAccountOwnerId = userWithEarliestAssignTimeStamp.Id; | ||
|
||
// Update this user to putthem atthe bottom of the round robin list. | ||
await salesforceConnection.sobject('User') | ||
.update({ | ||
Id: salesforceAccountOwnerId, | ||
// eslint-disable-next-line camelcase | ||
AE_Account_Assignment_round_robin__c: nowOn | ||
}); | ||
// If no existing account record was found, create a new one. | ||
let newAccountRecord = await salesforceConnection.sobject('Account') | ||
.create({ | ||
OwnerId: salesforceAccountOwnerId, | ||
Account_Assigned_date__c: nowOn,// eslint-disable-line camelcase | ||
// eslint-disable-next-line camelcase | ||
Current_Assignment_Reason__c: 'Inbound Lead',// TODO verify that this matters. if not, do not set it. | ||
Prospect_Status__c: 'Assigned',// eslint-disable-line camelcase | ||
|
||
Name: enrichmentData.employer.organization,// IFWMIH: We know organization exists | ||
Website: enrichmentData.employer.emailDomain, | ||
LinkedIn_company_URL__c: enrichmentData.employer.linkedinCompanyPageUrl,// eslint-disable-line camelcase | ||
NumberOfEmployees: enrichmentData.employer.numberOfEmployees, | ||
}); | ||
salesforceAccountId = newAccountRecord.id; | ||
// console.log('New account created!', salesforceAccountId); | ||
} | ||
} | ||
|
||
|
||
|
||
// Now search for an existing Contact. | ||
// FUTURE: expand this section to improve the searches. | ||
let existingContactRecord; | ||
if(emailAddress){ | ||
// console.log('searching for existing contact by emailAddress'); | ||
existingContactRecord = await salesforceConnection.sobject('Contact') | ||
.findOne({ | ||
AccountId: salesforceAccountId, | ||
Email: emailAddress, | ||
}); | ||
} else if(linkedinUrl) { | ||
// console.log('searching for existing contact by linkedInUrl'); | ||
existingContactRecord = await salesforceConnection.sobject('Contact') | ||
.findOne({ | ||
AccountId: salesforceAccountId, | ||
LinkedIn_profile__c: linkedinUrl // eslint-disable-line camelcase | ||
}); | ||
} else { | ||
existingContactRecord = undefined; | ||
} | ||
|
||
let salesforceContactId; | ||
let valuesToSet = {}; | ||
if(emailAddress || enrichmentData.person){ | ||
valuesToSet.Email = emailAddress || enrichmentData.person.emailAddress; | ||
} | ||
if(linkedinUrl || enrichmentData.person){ | ||
valuesToSet.LinkedIn_profile__c = linkedinUrl || enrichmentData.person.linkedinUrl;// eslint-disable-line camelcase | ||
} | ||
if(enrichmentData.person){ | ||
valuesToSet.Title = enrichmentData.person.title; | ||
} | ||
if(primaryBuyingSituation) { | ||
valuesToSet.Primary_buying_situation__c = primaryBuyingSituation;// eslint-disable-line camelcase | ||
} | ||
if(psychologicalStage) { | ||
valuesToSet.Stage__c = psychologicalStage;// eslint-disable-line camelcase | ||
} | ||
|
||
|
||
if(existingContactRecord){ | ||
salesforceContactId = existingContactRecord.Id; | ||
// console.log(`existing contact record found! ${salesforceContactId}`); | ||
// Update the existing contact with the information provided. | ||
await salesforceConnection.sobject('Contact') | ||
.update({ | ||
Id: salesforceContactId, | ||
...valuesToSet, | ||
}); | ||
// console.log(`${salesforceContactId} updated!`); | ||
} else { | ||
// Otherwise create a new Contact record. | ||
let newContactRecord = await salesforceConnection.sobject('Contact') | ||
.create({ | ||
AccountId: salesforceAccountId, | ||
OwnerId: salesforceAccountOwnerId, | ||
FirstName: firstName, | ||
LastName: lastName, | ||
...valuesToSet, | ||
}); | ||
// console.log(newContactRecord); | ||
salesforceContactId = newContactRecord.id; | ||
// console.log(`New contact record created! ${salesforceContactId}`); | ||
} | ||
|
||
|
||
return { | ||
salesforceAccountId, | ||
salesforceContactId | ||
}; | ||
|
||
} | ||
|
||
|
||
}; | ||
|
Oops, something went wrong.