mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 00:40:56 -05:00
🐛Fix sales lead assignments
Lead assignments can be incorrect when the user requests licenses needed via the contact form before the related emails have been added to the Close.io lead. We’ll use a custom field for automation sales contact if there’s no email activity.
This commit is contained in:
parent
86d54d78c0
commit
12baae7acd
2 changed files with 128 additions and 57 deletions
|
@ -93,7 +93,11 @@ const parseDomain = require('parse-domain');
|
|||
const request = require('request');
|
||||
|
||||
const earliestDate = new Date();
|
||||
earliestDate.setUTCDate(earliestDate.getUTCDate() - 10);
|
||||
earliestDate.setUTCDate(earliestDate.getUTCDate() - 3);
|
||||
|
||||
const apiKeyEmailMap = {};
|
||||
const emailApiKeyMap = {};
|
||||
const userApiKeyMap = {};
|
||||
|
||||
// ** Main program
|
||||
|
||||
|
@ -116,8 +120,13 @@ function upsertLeads(done) {
|
|||
addIntercomData(contacts, (err) => {
|
||||
if (err) return done(err);
|
||||
|
||||
// log('DEBUG: Updating contacts..');
|
||||
updateCloseLeads(contacts, (err) => {
|
||||
updateCloseApiKeyMaps((err) => {
|
||||
|
||||
// log('DEBUG: Updating contacts..');
|
||||
updateCloseLeads(contacts, (err) => {
|
||||
return done(err);
|
||||
});
|
||||
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
|
@ -467,6 +476,8 @@ class CocoContact {
|
|||
'Lead Origin': this.getLeadOrigin()
|
||||
}
|
||||
};
|
||||
const emailApiKey = getEmailApiKey(postData.status);
|
||||
if (apiKeyEmailMap[emailApiKey]) postData.custom['auto_sales_email'] = apiKeyEmailMap[emailApiKey];
|
||||
const props = this.trialRequest.properties;
|
||||
if (props) {
|
||||
for (const prop in props) {
|
||||
|
@ -603,7 +614,7 @@ class CocoContact {
|
|||
|
||||
// ** Upsert Close.io methods
|
||||
|
||||
function updateCloseLead(cocoContact, closeLead, userApiKeyMap, done) {
|
||||
function updateCloseLead(cocoContact, closeLead, done) {
|
||||
// console.log('DEBUG: updateCloseLead', cocoContact.email, closeLead.id);
|
||||
|
||||
const putData = cocoContact.getLeadPutData(closeLead);
|
||||
|
@ -632,7 +643,7 @@ function updateCloseLead(cocoContact, closeLead, userApiKeyMap, done) {
|
|||
}
|
||||
|
||||
// Add Close contact
|
||||
addContact(cocoContact, closeLead, userApiKeyMap, (err, results) => {
|
||||
addContact(cocoContact, closeLead, (err, results) => {
|
||||
if (err) return done(err);
|
||||
|
||||
// Add Close note
|
||||
|
@ -646,7 +657,7 @@ function updateCloseLead(cocoContact, closeLead, userApiKeyMap, done) {
|
|||
});
|
||||
}
|
||||
|
||||
function saveNewCloseLead(cocoContact, userApiKeyMap, done) {
|
||||
function saveNewCloseLead(cocoContact, done) {
|
||||
const postData = cocoContact.getLeadPostData();
|
||||
// console.log(`DEBUG: saveNewCloseLead ${cocoContact.email} ${postData.status}`);
|
||||
const options = {
|
||||
|
@ -683,7 +694,7 @@ function saveNewCloseLead(cocoContact, userApiKeyMap, done) {
|
|||
}
|
||||
const countryCode = getCountryCode(cocoContact.trialRequest.properties.country, [cocoContact.email]);
|
||||
const emailTemplate = getEmailTemplate(cocoContact.trialRequest.properties.siteOrigin, postData.status, countryCode);
|
||||
sendMail(cocoContact.email, newCloseLead, newContact.id, emailTemplate, userApiKeyMap, emailDelayMinutes, done);
|
||||
sendMail(cocoContact.email, newCloseLead, newContact.id, emailTemplate, emailDelayMinutes, done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -713,7 +724,7 @@ function createFindExistingLeadFn(email, existingLeads) {
|
|||
};
|
||||
}
|
||||
|
||||
function createUpdateCloseLeadFn(cocoContact, existingLeads, userApiKeyMap) {
|
||||
function createUpdateCloseLeadFn(cocoContact, existingLeads) {
|
||||
// New contact lead matching algorithm:
|
||||
// 1. New contact email exists
|
||||
// 2. New contact NCES school id exists
|
||||
|
@ -726,7 +737,7 @@ function createUpdateCloseLeadFn(cocoContact, existingLeads, userApiKeyMap) {
|
|||
if (existingLeads[cocoContact.email]) {
|
||||
if (existingLeads[cocoContact.email].length === 1) {
|
||||
// console.log(`DEBUG: Using lead from email lookup: ${cocoContact.email}`);
|
||||
return updateCloseLead(cocoContact, existingLeads[cocoContact.email][0], userApiKeyMap, done);
|
||||
return updateCloseLead(cocoContact, existingLeads[cocoContact.email][0], done);
|
||||
}
|
||||
console.error(`ERROR: ${existingLeads[cocoContact.email].length} email leads found for ${cocoContact.email}`);
|
||||
return done();
|
||||
|
@ -758,9 +769,9 @@ function createUpdateCloseLeadFn(cocoContact, existingLeads, userApiKeyMap) {
|
|||
return done();
|
||||
}
|
||||
if (data.total_results === 1) {
|
||||
return updateCloseLead(cocoContact, data.data[0], userApiKeyMap, done);
|
||||
return updateCloseLead(cocoContact, data.data[0], done);
|
||||
}
|
||||
return saveNewCloseLead(cocoContact, userApiKeyMap, done);
|
||||
return saveNewCloseLead(cocoContact, done);
|
||||
} catch (error) {
|
||||
console.log(`ERROR: createUpdateCloseLeadFn ${cocoContact.email}`);
|
||||
console.log(error);
|
||||
|
@ -770,7 +781,7 @@ function createUpdateCloseLeadFn(cocoContact, existingLeads, userApiKeyMap) {
|
|||
};
|
||||
}
|
||||
|
||||
function addContact(cocoContact, closeLead, userApiKeyMap, done) {
|
||||
function addContact(cocoContact, closeLead, done) {
|
||||
// console.log('DEBUG: addContact', closeLead.id, cocoContact.email);
|
||||
const postData = cocoContact.getContactPostData(closeLead);
|
||||
const options = {
|
||||
|
@ -787,7 +798,7 @@ function addContact(cocoContact, closeLead, userApiKeyMap, done) {
|
|||
|
||||
const countryCode = getCountryCode(cocoContact.trialRequest.properties.country, [cocoContact.email]);
|
||||
const emailTemplate = getEmailTemplate(cocoContact.trialRequest.properties.siteOrigin, closeLead.status_label, countryCode);
|
||||
sendMail(cocoContact.email, closeLead, newContact.id, emailTemplate, userApiKeyMap, emailDelayMinutes, done);
|
||||
sendMail(cocoContact.email, closeLead, newContact.id, emailTemplate, emailDelayMinutes, done);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -812,10 +823,12 @@ function addNote(cocoContact, closeLead, currentNotes, done) {
|
|||
});
|
||||
}
|
||||
|
||||
function sendMail(toEmail, closeLead, contactId, template, userApiKeyMap, delayMinutes, done) {
|
||||
// console.log('DEBUG: sendMail', toEmail, leadId, contactId, template, emailApiKey, delayMinutes);
|
||||
function sendMail(toEmail, closeLead, contactId, template, delayMinutes, done) {
|
||||
// console.log('DEBUG: sendMail', toEmail, leadId, contactId, template, delayMinutes);
|
||||
|
||||
let emailApiKey = getEmailApiKey(closeLead.status_label);
|
||||
// Sales contact email precedence: previous email to contact, previous email to lead, lead custom field, lead status default
|
||||
let emailApiKey = null;
|
||||
let emailDiffContactApiKey = null;
|
||||
|
||||
// Check for previously sent email
|
||||
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/email/?lead_id=${closeLead.id}`;
|
||||
|
@ -824,12 +837,29 @@ function sendMail(toEmail, closeLead, contactId, template, userApiKeyMap, delayM
|
|||
try {
|
||||
const data = JSON.parse(body);
|
||||
for (const emailData of data.data) {
|
||||
emailApiKey = userApiKeyMap[emailData.user_id] || emailApiKey;
|
||||
if (!isSameEmailTemplateType(emailData.template_id, template)) continue;
|
||||
for (const email of emailData.to) {
|
||||
if (email.toLowerCase() === toEmail.toLowerCase()) {
|
||||
console.error("ERROR: sending duplicate email:", toEmail, closeLead.id, contactId, template, emailData.contact_id);
|
||||
return done();
|
||||
|
||||
// Check for previous email to this contact
|
||||
if (!emailApiKey && userApiKeyMap[emailData.user_id]) {
|
||||
for (const email of emailData.to) {
|
||||
if (email.toLowerCase() === toEmail.toLowerCase()) {
|
||||
emailApiKey = userApiKeyMap[emailData.user_id];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save previous lead email to this lead
|
||||
if (!emailDiffContactApiKey && !emailApiKey && userApiKeyMap[emailData.user_id]) {
|
||||
emailDiffContactApiKey = userApiKeyMap[emailData.user_id];
|
||||
}
|
||||
|
||||
// Never send this email template to this contact again
|
||||
if (isSameEmailTemplateType(emailData.template_id, template)) {
|
||||
for (const email of emailData.to) {
|
||||
if (email.toLowerCase() === toEmail.toLowerCase()) {
|
||||
console.error("ERROR: sending duplicate email:", toEmail, closeLead.id, contactId, template, emailData.contact_id);
|
||||
return done();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -840,6 +870,16 @@ function sendMail(toEmail, closeLead, contactId, template, userApiKeyMap, delayM
|
|||
return done();
|
||||
}
|
||||
|
||||
if (!emailApiKey && emailDiffContactApiKey) emailApiKey = emailDiffContactApiKey;
|
||||
if (!emailApiKey) {
|
||||
if (closeLead.custom && closeLead.custom['auto_sales_email'] && emailApiKeyMap[closeLead.custom['auto_sales_email']]) {
|
||||
emailApiKey = emailApiKeyMap[closeLead.custom['auto_sales_email']];
|
||||
}
|
||||
else {
|
||||
emailApiKey = getEmailApiKey(closeLead.status_label);
|
||||
}
|
||||
}
|
||||
|
||||
// Send mail
|
||||
const dateScheduled = new Date();
|
||||
dateScheduled.setUTCMinutes(dateScheduled.getUTCMinutes() + delayMinutes);
|
||||
|
@ -869,39 +909,40 @@ function sendMail(toEmail, closeLead, contactId, template, userApiKeyMap, delayM
|
|||
}
|
||||
|
||||
function updateCloseLeads(cocoContacts, done) {
|
||||
const userApiKeyMap = {};
|
||||
// Lookup existing leads via email to protect against direct lead name querying later
|
||||
// Querying via lead name is unreliable
|
||||
const existingLeads = {};
|
||||
const tasks = [];
|
||||
for (const email in cocoContacts) {
|
||||
tasks.push(createFindExistingLeadFn(email, existingLeads));
|
||||
}
|
||||
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||
if (err) return done(err);
|
||||
const tasks = [];
|
||||
for (const email in cocoContacts) {
|
||||
tasks.push(createUpdateCloseLeadFn(cocoContacts[email], existingLeads));
|
||||
}
|
||||
async.series(tasks, done);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCloseApiKeyMaps(done) {
|
||||
let createGetUserFn = (apiKey) => {
|
||||
return (done) => {
|
||||
const url = `https://${apiKey}:X@app.close.io/api/v1/me/?_fields=id`;
|
||||
const url = `https://${apiKey}:X@app.close.io/api/v1/me/?_fields=id,email`;
|
||||
request.get(url, (error, response, body) => {
|
||||
if (error) return done();
|
||||
const results = JSON.parse(body);
|
||||
apiKeyEmailMap[apiKey] = results.email;
|
||||
emailApiKeyMap[results.email] = apiKey;
|
||||
userApiKeyMap[results.id] = apiKey;
|
||||
return done();
|
||||
});
|
||||
};
|
||||
}
|
||||
const tasks = [];
|
||||
const tasks = [createGetUserFn(closeIoEuMailApiKey)];
|
||||
for (const closeIoMailApiKey of closeIoMailApiKeys) {
|
||||
tasks.push(createGetUserFn(closeIoMailApiKey.apiKey));
|
||||
}
|
||||
tasks.push(createGetUserFn(closeIoEuMailApiKey));
|
||||
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||
if (err) console.log(err);
|
||||
// Lookup existing leads via email to protect against direct lead name querying later
|
||||
// Querying via lead name is unreliable
|
||||
const existingLeads = {};
|
||||
const tasks = [];
|
||||
for (const email in cocoContacts) {
|
||||
tasks.push(createFindExistingLeadFn(email, existingLeads));
|
||||
}
|
||||
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
|
||||
if (err) return done(err);
|
||||
const tasks = [];
|
||||
for (const email in cocoContacts) {
|
||||
tasks.push(createUpdateCloseLeadFn(cocoContacts[email], existingLeads, userApiKeyMap));
|
||||
}
|
||||
async.series(tasks, done);
|
||||
});
|
||||
});
|
||||
async.parallelLimit(tasks, closeParallelLimit, done);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ log = require 'winston'
|
|||
request = require 'request'
|
||||
|
||||
apiKey = config.closeIO?.apiKey
|
||||
defaultSalesContactUser = 'user_Fh0uLUkRIKMk2to61ISq8PneyQonuD2i7hes6RhZgDX'
|
||||
defaultSalesContactUserID = 'user_Fh0uLUkRIKMk2to61ISq8PneyQonuD2i7hes6RhZgDX'
|
||||
|
||||
module.exports =
|
||||
logError: (msg) ->
|
||||
|
@ -50,27 +50,57 @@ module.exports =
|
|||
return done('ERROR: multiple leads returned for ' + email + ' ' + leads.data.length)
|
||||
return done()
|
||||
|
||||
getSalesContactEmail: (email, done) ->
|
||||
getSalesContactEmail: (userEmail, done) ->
|
||||
# Sales contact email precedence: previous email to contact, previous email to lead, lead custom field, lead status default
|
||||
try
|
||||
# NOTE: does not work on + email addresses due to Close.io API bug
|
||||
uri = "https://#{apiKey}:X@app.close.io/api/v1/lead/?query=email_address:#{email}"
|
||||
uri = "https://#{apiKey}:X@app.close.io/api/v1/lead/?query=email_address:#{userEmail}"
|
||||
request.get uri, (error, response, body) =>
|
||||
return done(error) if error
|
||||
leads = JSON.parse(body)
|
||||
return done("Unexpected leads format: " + body) unless leads.data?
|
||||
return done("No existing Close.IO lead found for #{email}") unless leads.data?.length > 0
|
||||
return done("Unexpected Close leads format: " + body) unless leads.data?
|
||||
return done("No existing Close.IO lead found for #{userEmail}") unless leads.data?.length > 0
|
||||
lead = leads.data[0]
|
||||
uri = "https://#{apiKey}:X@app.close.io/api/v1/activity/?lead_id=#{lead.id}"
|
||||
request.get uri, (error, response, body) =>
|
||||
return done(error) if error
|
||||
activities = JSON.parse(body)
|
||||
return done("Unexpected activities format: " + body) unless activities.data?
|
||||
for activity in activities.data when activity._type is 'Email'
|
||||
if /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender) and not (activity.sender?.indexOf(config.mail.username) >= 0) and not (activity.sender?.indexOf('brian@codecombat.com') >= 0)
|
||||
return done(null, activity.sender, activity.user_id, lead.id)
|
||||
return done(null, config.mail.supportSchools, defaultSalesContactUser, lead.id)
|
||||
activityForThisContact = null
|
||||
activityForThisLead = null
|
||||
for activity in activities.data when activity?._type is 'Email'
|
||||
continue unless /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender)
|
||||
continue if activity.sender.indexOf('brian@codecombat.com') >= 0
|
||||
continue if activity.sender.indexOf(config.mail.username) >= 0
|
||||
activityForThisLead ?= activity
|
||||
for email in activity.to or [] when email?.toLowerCase() is userEmail?.toLowerCase()
|
||||
activityForThisContact ?= activity
|
||||
|
||||
if activityForThisContact
|
||||
return done(null, activityForThisContact.sender, activityForThisContact.user_id, lead.id)
|
||||
else if activityForThisLead
|
||||
return done(null, activityForThisLead.sender, activityForThisLead.user_id, lead.id)
|
||||
|
||||
if email = lead.custom?['auto_sales_email']
|
||||
# Have to lookup Close user Id if email from lead custom field
|
||||
uri = "https://#{apiKey}:X@app.close.io/api/v1/user/?_fields=id,email"
|
||||
request.get uri, (error, response, body) =>
|
||||
return done(error) if error
|
||||
users = JSON.parse(body)
|
||||
return done("Unexpected Close users format: " + body) unless users.data?
|
||||
userID = null
|
||||
for user in users.data or [] when user.email?.toLowerCase() is email.toLowerCase()
|
||||
userID = user.id
|
||||
break
|
||||
if userID
|
||||
return done(null, email, userID, lead.id)
|
||||
else
|
||||
@logError("No user found for leadID=#{lead.id} user=#{userEmail} auto_sales_email=#{lead.custom?['auto_sales_email']}")
|
||||
return done(null, config.mail.supportSchools, defaultSalesContactUserID, lead.id)
|
||||
else
|
||||
return done(null, config.mail.supportSchools, defaultSalesContactUserID, lead.id)
|
||||
catch error
|
||||
log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}")
|
||||
log.error("closeIO.getSalesContactEmail Error for #{userEmail}: #{JSON.stringify(error)}")
|
||||
return done(error)
|
||||
|
||||
sendMail: (fromAddress, subject, content, salesContactEmail, leadID, done) ->
|
||||
|
@ -106,7 +136,7 @@ module.exports =
|
|||
uri: "https://#{apiKey}:X@app.close.io/api/v1/lead/#{leadID}/"
|
||||
body: JSON.stringify(putData)
|
||||
request.put options, (error, response, body) =>
|
||||
return done(error) if error
|
||||
return done(error) if error
|
||||
result = JSON.parse(body)
|
||||
if result.errors or result['field-errors']
|
||||
errorMessage = "Update Close.io lead PUT error for #{teacherEmail} #{leadID}"
|
||||
|
@ -123,7 +153,7 @@ module.exports =
|
|||
uri: "https://#{apiKey}:X@app.close.io/api/v1/task/"
|
||||
body: JSON.stringify(postData)
|
||||
request.post options, (error, response, body) =>
|
||||
return done(error) if error
|
||||
return done(error) if error
|
||||
result = JSON.parse(body)
|
||||
if result.errors or result['field-errors']
|
||||
errorMessage = "Create Close.io call task POST error for #{teacherEmail} #{leadID}"
|
||||
|
@ -144,7 +174,7 @@ module.exports =
|
|||
uri: "https://#{apiKey}:X@app.close.io/api/v1/opportunity/"
|
||||
body: JSON.stringify(postData)
|
||||
request.post options, (error, response, body) =>
|
||||
return done(error) if error
|
||||
return done(error) if error
|
||||
result = JSON.parse(body)
|
||||
if result.errors or result['field-errors']
|
||||
errorMessage = "Create Close.io opportunity POST error for #{teacherEmail} #{leadID}"
|
||||
|
|
Loading…
Reference in a new issue