codecombat/scripts/updateCloseIoLeads.js
2016-09-07 16:11:12 -07:00

989 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Upsert new lead data into Close.io
'use strict';
if (process.argv.length !== 10) {
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <Close.io mail API key4> <Close.io EU mail API key> <Intercom 'App ID:API key'> <mongo connection Url>");
process.exit();
}
// TODO: Test multiple contacts
// TODO: Support multiple emails for the same contact (i.e diff trial and coco emails)
// TODO: Update notes with new data (e.g. coco user or intercom url)
// TODO: Find/fix case-sensitive bugs
// TODO: Use generators and promises
// TODO: Reduce response data via _fields param
// TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact)
// TODO: Cleanup country/status lookup code
// TODO: Handle trial requests as individual contacts to be imported, instead of batching them into leads immediately via CocoLead objects
// Save as custom fields instead of user-specific lead notes (also saving nces_ props)
const commonTrialProperties = ['organization', 'district', 'city', 'state', 'country'];
// Old properties which are deprecated or moved
const customFieldsToRemove = [
'coco_name', 'coco_firstName', 'coco_lastName', 'coco_gender', 'coco_numClassrooms', 'coco_numStudents', 'coco_role', 'coco_schoolName', 'coco_stats', 'coco_lastLevel',
'email', 'intercom_url', 'name',
'trial_created', 'trial_educationLevel', 'trial_phoneNumber', 'trial_email', 'trial_location', 'trial_name', 'trial_numStudents', 'trial_role', 'trial_userID', 'userID', 'trial_organization', 'trial_city', 'trial_state', 'trial_country',
'demo_request_organization', 'demo_request_city', 'demo_request_state', 'demo_request_country'
];
// Skip these problematic leads
const leadsToSkip = ['6 sınıflar', 'fdsafd', 'ashtasht', 'matt+20160404teacher3 school', 'sdfdsf', 'ddddd', 'dsfadsaf', "Nolan's School of Wonders", 'asdfsadf'];
const createTeacherEmailTemplatesAuto1 = ['tmpl_i5bQ2dOlMdZTvZil21bhTx44JYoojPbFkciJ0F560mn', 'tmpl_CEZ9PuE1y4PRvlYiKB5kRbZAQcTIucxDvSeqvtQW57G'];
const demoRequestEmailTemplatesAuto1 = [
'tmpl_cGb6m4ssDvqjvYd8UaG6cacvtSXkZY3vj9b9lSmdQrf', // (Auto1) Demo Request Short
'tmpl_2hV6OdOXtsObLQK9qlRdpf0C9QKbER06T17ksGYOoUE', // (Auto1) Demo Request With Questions
'tmpl_Q0tweZ5H4xs2E489KwdYj3HET9PpzkQ7jgDQb9hOMTR', // (Auto1) Demo Request Without Questions
];
const createTeacherInternationalEmailTemplateAuto1 = 'tmpl_8vsXwcr6dWefMnAEfPEcdHaxqSfUKUY8UKq6WfReGqG';
const demoRequestInternationalEmailTemplateAuto1 = 'tmpl_nnH1p3II7G7NJYiPOIHphuj4XUaDptrZk1mGQb2d9Xa';
const createTeacherNlEmailTemplatesAuto1 = ['tmpl_yf9tAPasz8KV7L414GhWWIclU8ewclh3Z8lCx2mCoIU', 'tmpl_OgPCV2p59uq0daVuUPF6r1rcQkxJbViyZ1ZMtW45jY8'];
const demoRequestNlEmailTemplatesAuto1 = ['tmpl_XGKyZm6gcbqZ5jirt7A54Vu8p68cLxAsKZtb9QBABUE', 'tmpl_xcfgQjUHPa6LLsbPWuPvEUElFXHmIpLa4IZEybJ0b0u'];
// Prioritized Close.io lead status match list
const closeIoInitialLeadStatuses = [
{status: 'Inbound UK Auto Attempt 1', regex: /^uk$|\.uk$/},
{status: 'Inbound Canada Auto Attempt 1', regex: /^ca$|\.ca$/},
{status: 'Inbound AU Auto Attempt 1', regex: /^au$|\.au$/},
{status: 'Inbound NZ Auto Attempt 1', regex: /^nz$|\.nz$/},
{status: 'New US Schools Auto Attempt 1', regex: /^us$|\.us$|\.gov$|k12|sd/},
{status: 'Inbound International Auto Attempt 1', regex: /^[A-Za-z]{2}$|\.[A-Za-z]{2}$/},
{status: 'Auto Attempt 1', regex: /^[A-Za-z]*$/}
];
const defaultLeadStatus = 'Auto Attempt 1';
const defaultInternationalLeadStatus = 'Inbound International Auto Attempt 1';
const defaultEuLeadStatus = 'Inbound EU Auto Attempt 1';
const usSchoolStatuses = ['Auto Attempt 1', 'New US Schools Auto Attempt 1', 'New US Schools Auto Attempt 1 Low'];
const emailDelayMinutes = 27;
const closeParallelLimit = 10;
const scriptStartTime = new Date();
const closeIoApiKey = process.argv[2]; // Matt
// Automatic mails sent as API owners, first key assumed to be primary and gets 50% of the leads
// Names in comments are for reference, but Source of Truth is updateSalesLeads.sh on the analytics server
const closeIoMailApiKeys = [
{
apiKey: process.argv[3], // Lisa
weight: .8
},
{
apiKey: process.argv[4], // Elliot
weight: .15
},
{
apiKey: process.argv[5], // Nolan
weight: .05
},
{
apiKey: process.argv[6], // Sean
weight: 0
},
];
const closeIoEuMailApiKey = process.argv[7]; // Jurian
const intercomAppIdApiKey = process.argv[8];
const intercomAppId = intercomAppIdApiKey.split(':')[0];
const intercomApiKey = intercomAppIdApiKey.split(':')[1];
const mongoConnUrl = process.argv[9];
const MongoClient = require('mongodb').MongoClient;
const async = require('async');
const countryData = require('country-data');
const countryList = require('country-list')();
const parseDomain = require('parse-domain');
const request = require('request');
const earliestDate = new Date();
earliestDate.setUTCDate(earliestDate.getUTCDate() - 10);
// ** Main program
async.series([
upsertLeads
],
(err, results) => {
if (err) console.error(err);
log("Script runtime: " + (new Date() - scriptStartTime));
}
);
function upsertLeads(done) {
// log('DEBUG: Finding leads..');
findCocoLeads((err, leads) => {
if (err) return done(err);
log(`Num leads ${Object.keys(leads).length}`);
// log('DEBUG: Adding Intercom data..');
addIntercomData(leads, (err) => {
if (err) return done(err);
// log('DEBUG: Updating leads..');
updateLeads(leads, (err) => {
return done(err);
});
});
});
}
// ** Utilities
function getCountryCode(country, emails) {
// console.log(`DEBUG: getCountryCode ${country} ${emails.length}`);
if (country) {
if (country.indexOf('Nederland') >= 0) return 'NL';
let countryCode = countryList.getCode(country);
if (countryCode) return countryCode;
}
for (const email of emails) {
const domain = parseDomain(email);
if (!domain) continue;
const tld = domain.tld;
if (tld) {
const matches = /^[A-Za-z]*\.?([A-Za-z]{2})$/ig.exec(tld);
if (matches && matches.length === 2) {
return matches[1].toUpperCase();
}
}
}
}
function getInitialLeadStatusViaCountry(country, trialRequests) {
// console.log(`DEBUG: getInitialLeadStatusViaCountry ${country} ${trialRequests.length}`);
if (/^u\.s\.?(\.a)?\.?$|^us$|usa|america|united states/ig.test(country)) {
const status = 'New US Schools Auto Attempt 1'
return isLowValueUsLead(status, trialRequests) ? `${status} Low` : status;
}
const highValueLead = isHighValueLead(trialRequests);
if (/^england$|^uk$|^united kingdom$/ig.test(country) && highValueLead) {
return 'Inbound UK Auto Attempt 1';
}
if (/^ca$|^canada$/ig.test(country)) {
return 'Inbound Canada Auto Attempt 1';
}
if (/^au$|^australia$/ig.test(country)) {
return 'Inbound AU Auto Attempt 1';
}
if (/^nz$|^new zealand$/ig.test(country)) {
return 'Inbound AU Auto Attempt 1';
}
if (/bolivia|iran|korea|macedonia|taiwan|tanzania|^venezuela$/ig.test(country)) {
return defaultInternationalLeadStatus;
}
const countryCode = countryList.getCode(country);
if (countryCode) {
if (countryCode === 'NL' || countryCode === 'BE') {
return defaultEuLeadStatus;
}
if (isEuCountryCode(countryCode)) {
return highValueLead ? 'Inbound EU Auto Attempt 1 High' : defaultEuLeadStatus;
}
return defaultInternationalLeadStatus;
}
return null;
}
function getInitialLeadStatusViaEmails(emails, trialRequests) {
// console.log(`DEBUG: getInitialLeadStatusViaEmails ${emails.length} ${trialRequests.length}`);
let currentStatus = null;
let currentRank = closeIoInitialLeadStatuses.length;
for (const email of emails) {
let tld = parseDomain(email).tld;
tld = tld ? tld.toLowerCase() : '';
for (let rank = 0; rank < currentRank; rank++) {
if (closeIoInitialLeadStatuses[rank].regex.test(tld)) {
currentStatus = closeIoInitialLeadStatuses[rank].status;
currentRank = rank;
}
}
}
if (!currentStatus || [defaultLeadStatus, defaultInternationalLeadStatus].indexOf(currentStatus) >= 0) {
// Look for a better EU match
const countryCode = getCountryCode(null, emails);
if (countryCode === 'NL' || countryCode === 'BE') {
return defaultEuLeadStatus;
}
if (isEuCountryCode(countryCode)) {
return isHighValueLead(trialRequests) ? 'Inbound EU Auto Attempt 1 High' : defaultEuLeadStatus;
}
}
currentStatus = currentStatus ? currentStatus : defaultLeadStatus;
return isLowValueUsLead(currentStatus, trialRequests) ? `${currentStatus} Low` : currentStatus;
}
function isEuCountryCode(countryCode) {
if (countryData.regions.northernEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
if (countryData.regions.southernEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
if (countryData.regions.easternEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
if (countryData.regions.westernEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
return false;
}
function isLowValueUsLead(status, trialRequests) {
if (isUSSchoolStatus(status)) {
for (const trialRequest of trialRequests) {
if (parseInt(trialRequest.properties.nces_district_students) < 5000) {
return true;
}
else if (parseInt(trialRequest.properties.nces_district_students) >= 5000) {
return false;
}
}
for (const trialRequest of trialRequests) {
// Must match these values: https://github.com/codecombat/codecombat/blob/master/app/templates/teachers/request-quote-view.jade#L159
if (['1-500', '500-1,000'].indexOf(trialRequest.properties.numStudentsTotal) >= 0) {
return true;
}
}
}
return false;
}
function isHighValueLead(trialRequests) {
for (const trialRequest of trialRequests) {
// Must match these values: https://github.com/codecombat/codecombat/blob/master/app/templates/teachers/request-quote-view.jade#L159
if (['5,000-10,000', '10,000+'].indexOf(trialRequest.properties.numStudentsTotal) >= 0) {
return true;
}
}
return false;
}
function isUSSchoolStatus(status) {
return usSchoolStatuses.indexOf(status) >= 0;
}
function getEmailApiKey(leadStatus) {
if (leadStatus === defaultEuLeadStatus) return closeIoEuMailApiKey;
if (closeIoMailApiKeys.length < 0) return;
const weightedList = [];
for (let closeIoMailApiKey of closeIoMailApiKeys) {
const multiples = closeIoMailApiKey.weight * 100;
for (let i = 0; i < multiples; i++) {
weightedList.push(closeIoMailApiKey.apiKey);
}
}
return weightedList[Math.floor(Math.random() * weightedList.length)];
}
function getRandomEmailTemplate(templates) {
if (templates.length < 0) return '';
return templates[Math.floor(Math.random() * templates.length)];
}
function getEmailTemplate(siteOrigin, leadStatus, countryCode) {
// console.log(`DEBUG: getEmailTemplate ${siteOrigin} ${leadStatus} ${countryCode}`);
if (isUSSchoolStatus(leadStatus)) {
if (['create teacher', 'convert teacher'].indexOf(siteOrigin) >= 0) {
return getRandomEmailTemplate(createTeacherEmailTemplatesAuto1);
}
return getRandomEmailTemplate(demoRequestEmailTemplatesAuto1);
}
if (leadStatus === defaultEuLeadStatus && (countryCode === 'NL' || countryCode === 'BE')) {
if (['create teacher', 'convert teacher'].indexOf(siteOrigin) >= 0) {
return getRandomEmailTemplate(createTeacherNlEmailTemplatesAuto1);
}
return getRandomEmailTemplate(demoRequestNlEmailTemplatesAuto1);
}
if (['create teacher', 'convert teacher'].indexOf(siteOrigin) >= 0) {
return createTeacherInternationalEmailTemplateAuto1;
}
return demoRequestInternationalEmailTemplateAuto1;
}
function isSameEmailTemplateType(template1, template2) {
if (template1 == template2) {
return true;
}
if (createTeacherEmailTemplatesAuto1.indexOf(template1) >= 0 && createTeacherEmailTemplatesAuto1.indexOf(template2) >= 0) {
return true;
}
if (demoRequestEmailTemplatesAuto1.indexOf(template1) >= 0 && demoRequestEmailTemplatesAuto1.indexOf(template2) >= 0) {
return true;
}
return false;
}
function log(str) {
console.log(new Date().toISOString() + " " + str);
}
// ** Coco data collection methods and class
function findCocoLeads(done) {
MongoClient.connect(mongoConnUrl, (err, db) => {
if (err) return done(err);
// Recent trial requests
const query = {$and: [{created: {$gte: earliestDate}}, {type: 'course'}]};
db.collection('trial.requests').find(query).toArray((err, trialRequests) => {
if (err) {
db.close();
return done(err);
}
const leads = {};
const emailLeadMap = {};
const emails = [];
for (const trialRequest of trialRequests) {
if (!trialRequest.properties || !trialRequest.properties.email) continue;
const email = trialRequest.properties.email.toLowerCase();
emails.push(email);
const name = trialRequest.properties.nces_name || trialRequest.properties.organization || trialRequest.properties.school || trialRequest.properties.district || trialRequest.properties.nces_district || email;
if (!leads[name]) leads[name] = new CocoLead(name);
leads[name].addTrialRequest(email, trialRequest);
emailLeadMap[email] = leads[name];
}
// Users for trial requests
const query = {$and: [
{emailLower: {$in: emails}},
{anonymous: false}
]};
db.collection('users').find(query).toArray((err, users) => {
if (err) {
db.close();
return done(err);
}
const userIDs = [];
const userLeadMap = {};
const userEmailMap = {};
for (const user of users) {
const email = user.emailLower;
emailLeadMap[email].addUser(email, user);
userIDs.push(user._id);
userLeadMap[user._id.valueOf()] = emailLeadMap[email];
userEmailMap[user._id.valueOf()] = email;
}
// Classrooms for users
const query = {ownerID: {$in: userIDs}};
db.collection('classrooms').find(query).toArray((err, classrooms) => {
if (err) {
db.close();
return done(err);
}
for (const classroom of classrooms) {
userLeadMap[classroom.ownerID.valueOf()].addClassroom(userEmailMap[classroom.ownerID.valueOf()], classroom);
}
db.close();
return done(null, leads);
});
});
});
});
}
function createAddIntercomDataFn(cocoLead, email) {
return (done) => {
const options = {
url: `https://api.intercom.io/users?email=${encodeURIComponent(email)}`,
auth: {
user: intercomAppId,
pass: intercomApiKey
},
headers: {
'Accept': 'application/json'
}
};
request.get(options, (error, response, body) => {
if (error) return done(error);
try {
const user = JSON.parse(body);
cocoLead.addIntercomUser(email, user);
}
catch (err) {
console.log(err);
console.log(body);
}
return done();
});
};
}
function addIntercomData(leads, done) {
const tasks = []
for (const name in leads) {
for (const email in leads[name].contacts) {
tasks.push(createAddIntercomDataFn(leads[name], email));
}
}
async.parallel(tasks, (err, results) => {
return done(err);
});
}
class CocoLead {
constructor(name) {
this.contacts = {};
this.custom = {};
this.name = name;
this.trialRequests = [];
}
addClassroom(email, classroom) {
if (!this.contacts[email.toLowerCase()]) this.contacts[email.toLowerCase()] = {};
const contact = this.contacts[email.toLowerCase()];
contact.numClassrooms = contact.numClassrooms ? contact.numClassrooms + 1 : 1;
if (classroom.members && classroom.members.length) {
contact.numStudents = contact.numStudents ? contact.numStudents + classroom.members.length : classroom.members.length;
}
}
addIntercomUser(email, user) {
if (user && user.id) {
if (!this.contacts[email.toLowerCase()]) this.contacts[email.toLowerCase()] = {};
this.contacts[email.toLowerCase()].intercomUrl = `https://app.intercom.io/a/apps/${intercomAppId}/users/${user.id}/`;
if (user.last_request_at) {
this.contacts[email.toLowerCase()].intercomLastSeen = new Date(parseInt(user.last_request_at) * 1000);
}
if (user.session_count) {
this.contacts[email.toLowerCase()].intercomSessionCount = parseInt(user.session_count);
}
}
}
addTrialRequest(email, trial) {
if (!this.contacts[email.toLowerCase()]) this.contacts[email.toLowerCase()] = {};
if (trial.properties.firstName && trial.properties.lastName) {
this.contacts[email.toLowerCase()].name = `${trial.properties.firstName} ${trial.properties.lastName}`;
}
else if (trial.properties.name) {
this.contacts[email.toLowerCase()].name = trial.properties.name;
}
this.contacts[email.toLowerCase()].trial = trial;
this.trialRequests.push(trial);
}
addUser(email, user) {
this.contacts[email.toLowerCase()].user = user;
}
getInitialLeadStatus() {
for (const email in this.contacts) {
const props = this.contacts[email].trial.properties;
if (props && props['country']) {
const status = getInitialLeadStatusViaCountry(props['country'], this.trialRequests);
if (status) return status;
}
}
return getInitialLeadStatusViaEmails(Object.keys(this.contacts), this.trialRequests);
}
getLeadPostData() {
const postData = {
display_name: this.name,
name: this.name,
status: this.getInitialLeadStatus(),
contacts: this.getContactsPostData(),
custom: {
lastUpdated: new Date(),
'Lead Origin': this.getLeadOrigin()
}
};
for (const email in this.contacts) {
const props = this.contacts[email].trial.properties;
if (props) {
for (const prop in props) {
if (commonTrialProperties.indexOf(prop) >= 0 || /nces_/ig.test(prop)) {
postData.custom[`demo_${prop}`] = props[prop];
}
}
}
if (this.contacts[email].intercomLastSeen && (this.contacts[email].intercomLastSeen > (postData.custom['intercom_lastSeen'] || 0))) {
postData.custom['intercom_lastSeen'] = this.contacts[email].intercomLastSeen;
}
if (this.contacts[email].intercomSessionCount && (this.contacts[email].intercomSessionCount > (postData.custom['intercom_sessionCount'] || 0))) {
postData.custom['intercom_sessionCount'] = this.contacts[email].intercomSessionCount;
}
}
return postData;
}
getLeadPutData(currentLead) {
// console.log('DEBUG: getLeadPutData', currentLead.name);
const putData = {};
const currentCustom = currentLead.custom || {};
if (!currentCustom['Lead Origin']) {
putData['custom.Lead Origin'] = this.getLeadOrigin();
}
for (const email in this.contacts) {
const props = this.contacts[email].trial.properties;
if (props) {
for (const prop in props) {
if (!currentCustom[`demo_${prop}`] && (commonTrialProperties.indexOf(prop) >= 0 || /nces_/ig.test(prop))) {
putData[`custom.demo_${prop}`] = props[prop];
}
}
}
if (this.contacts[email].intercomLastSeen && (this.contacts[email].intercomLastSeen > (currentCustom['intercom_lastSeen'] || 0))) {
putData['custom.intercom_lastSeen'] = this.contacts[email].intercomLastSeen;
}
if (this.contacts[email].intercomSessionCount && (this.contacts[email].intercomSessionCount > (currentCustom['intercom_sessionCount'] || 0))) {
putData['custom.intercom_sessionCount'] = this.contacts[email].intercomSessionCount;
}
}
for (const field of customFieldsToRemove) {
if (currentCustom[field]) {
putData[`custom.${field}`] = null;
}
}
if (Object.keys(putData).length > 0) {
putData[`custom.lastUpdated`] = new Date();
}
return putData;
}
getLeadOrigin() {
for (const email in this.contacts) {
const props = this.contacts[email].trial.properties;
switch (props.siteOrigin) {
case 'create teacher':
return 'Create Teacher';
case 'convert teacher':
return 'Convert Teacher';
}
}
return 'Demo Request';
}
getContactsPostData(existingLead) {
const postData = [];
const existingEmails = {};
if (existingLead) {
const existingContacts = existingLead.contacts || [];
for (const contact of existingContacts) {
const emails = contact.emails || [];
for (const email of emails) {
existingEmails[email.email.toLowerCase()] = true;
}
}
}
for (const email in this.contacts) {
if (existingEmails[email]) continue;
const contact = this.contacts[email];
const data = {
emails: [{email: email}],
name: contact.name
}
const props = contact.trial.properties;
if (props.phoneNumber) {
data.phones = [{phone: props.phoneNumber}];
}
if (props.role) {
data.title = props.role;
}
else if (contact.user || contact.user.role) {
data.title = contact.user.role;
}
postData.push(data);
}
return postData;
}
getNotesPostData(currentNotes) {
// Post activity notes for each contact
function noteExists(email) {
if (currentNotes) {
for (const note of currentNotes) {
if (note.note.indexOf(email) >= 0) {
return true;
}
}
}
return false;
}
const notes = [];
for (const email in this.contacts) {
if (!noteExists(email)) {
const contact = this.contacts[email];
let noteData = "";
const trial = contact.trial
if (trial.properties) {
const props = trial.properties;
if (props.name) {
noteData += `${props.name}\n`;
}
if (props.email) {
noteData += `demo_email: ${props.email.toLowerCase()}\n`;
}
if (trial.created) {
noteData += `demo_request: ${trial.created}\n`;
}
if (props.educationLevel) {
noteData += `demo_educationLevel: ${props.educationLevel.join(', ')}\n`;
}
for (const prop in props) {
if (['email', 'educationLevel', 'created'].indexOf(prop) >= 0 || commonTrialProperties.indexOf(prop) >= 0) continue;
noteData += `demo_${prop}: ${props[prop]}\n`;
}
}
if (contact.intercomUrl) noteData += `intercom_url: ${contact.intercomUrl}\n`;
if (contact.intercomLastSeen) noteData += `intercom_lastSeen: ${contact.intercomLastSeen}\n`;
if (contact.intercomSessionCount) noteData += `intercom_sessionCount: ${contact.intercomSessionCount}\n`;
if (contact.user) {
const user = contact.user
noteData += `coco_userID: ${user._id}\n`;
if (user.firstName) noteData += `coco_firstName: ${user.firstName}\n`;
if (user.lastName) noteData += `coco_lastName: ${user.lastName}\n`;
if (user.name) noteData += `coco_name: ${user.name}\n`;
if (user.emaillower) noteData += `coco_email: ${user.emailLower}\n`;
if (user.gender) noteData += `coco_gender: ${user.gender}\n`;
if (user.lastLevel) noteData += `coco_lastLevel: ${user.lastLevel}\n`;
if (user.role) noteData += `coco_role: ${user.role}\n`;
if (user.schoolName) noteData += `coco_schoolName: ${user.schoolName}\n`;
if (user.stats && user.stats.gamesCompleted) noteData += `coco_gamesCompleted: ${user.stats.gamesCompleted}\n`;
noteData += `coco_preferredLanguage: ${user.preferredLanguage || 'en-US'}\n`;
}
if (contact.numClassrooms) {
noteData += `coco_numClassrooms: ${contact.numClassrooms}\n`
}
if (contact.numStudents) {
noteData += `coco_numStudents: ${contact.numStudents}\n`
}
notes.push(noteData);
}
}
return notes;
}
}
// ** Upsert Close.io methods
function updateExistingLead(lead, existingLead, userApiKeyMap, done) {
// console.log('DEBUG: updateExistingLead', existingLead.id);
const putData = lead.getLeadPutData(existingLead);
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/${existingLead.id}/`,
body: JSON.stringify(putData)
};
request.put(options, (error, response, body) => {
if (error) return done(error);
const result = JSON.parse(body);
if (result.errors || result['field-errors']) {
console.error(`Update existing lead PUT error for ${lead.name}`);
console.error(body);
// console.log(putData);
return done();
}
// Add contacts
const newContacts = lead.getContactsPostData(existingLead);
const tasks = []
for (const newContact of newContacts) {
newContact.lead_id = existingLead.id;
tasks.push(createAddContactFn(newContact, lead, existingLead, userApiKeyMap));
}
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) return done(err);
// Add notes
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/note/?lead_id=${existingLead.id}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
const currentNotes = JSON.parse(body).data;
const newNotes = lead.getNotesPostData(currentNotes);
const tasks = []
for (const newNote of newNotes) {
tasks.push(createAddNoteFn(existingLead.id, newNote));
}
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
return done(err);
});
});
});
});
}
function saveNewLead(lead, done) {
const postData = lead.getLeadPostData();
// console.log(`DEBUG: saveNewLead ${lead.name} ${postData.status}`);
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/`,
body: JSON.stringify(postData)
};
request.post(options, (error, response, body) => {
if (error) return done(error);
const existingLead = JSON.parse(body);
if (existingLead.errors || existingLead['field-errors']) {
console.error(`New lead POST error for ${lead.name}`);
console.error(body);
// console.error(JSON.stringify(postData, null, 2));
return done();
}
// Add notes
const newNotes = lead.getNotesPostData();
const tasks = []
for (const newNote of newNotes) {
tasks.push(createAddNoteFn(existingLead.id, newNote));
}
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) return done(err);
// Send emails to new contacts
const tasks = [];
for (const contact of existingLead.contacts) {
for (const email of contact.emails) {
const countryCode = getCountryCode(lead.contacts[email.email].trial.properties.country, [email.email]);
const emailTemplate = getEmailTemplate(lead.contacts[email.email].trial.properties.siteOrigin, postData.status, countryCode);
tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, emailTemplate, postData.status));
}
}
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
return done(err);
});
});
});
}
function createFindExistingLeadFn(email, name, existingLeads) {
return (done) => {
// console.log('DEBUG: findEmailLead', email);
const query = `recipient:"${email}"`;
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
try {
const data = JSON.parse(body);
if (data.total_results > 0) {
if (!existingLeads[name]) existingLeads[name] = [];
for (const lead of data.data) {
existingLeads[name].push(lead);
}
}
return done();
} catch (error) {
// console.log(url);
console.log(error);
// console.log(body);
return done(error);
}
});
};
}
function createUpdateLeadFn(lead, existingLeads, userApiKeyMap) {
// New contact lead matching algorithm:
// 1. New contact email exists
// 2. New contact NCES school id exists
// 3. New contact NCES district id and no NCES school id
// 4. New contact school name and no NCES data
// 5. New contact district name and no NCES data
return (done) => {
// console.log('DEBUG: updateLead', lead.name);
if (existingLeads[lead.name.toLowerCase()]) {
if (existingLeads[lead.name.toLowerCase()].length === 1) {
// console.log(`DEBUG: Using lead from email lookup: ${lead.name}`);
return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], userApiKeyMap, done);
}
console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`);
return done();
}
let nces_district_id;
let nces_school_id;
for (const trial of lead.trialRequests) {
if (!trial.properties) continue;
if (trial.properties.nces_district_id) {
nces_district_id = trial.properties.nces_district_id;
if (trial.properties.nces_id) {
nces_district_id = trial.properties.nces_district_id;
nces_school_id = trial.properties.nces_id;
break;
}
}
}
// console.log(`DEBUG: updateLead district ${nces_district_id} school ${nces_school_id}`);
let query = `name:"${lead.name}"`;
if (nces_school_id) {
query = `custom.demo_nces_id:"${nces_school_id}"`;
}
else if (nces_district_id) {
query = `custom.demo_nces_district_id:"${nces_district_id}" custom.demo_nces_id:""`;
}
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/?query=${encodeURIComponent(query)}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
try {
const data = JSON.parse(body);
if (data.total_results > 1) {
console.error(`ERROR: ${data.total_results} leads found for ${lead.name} nces_district_id=${nces_district_id} nces_school_id=${nces_school_id}`);
return done();
}
if (data.total_results === 1) {
return updateExistingLead(lead, data.data[0], userApiKeyMap, done);
}
return saveNewLead(lead, done);
} catch (error) {
console.log(`ERROR: updateLead ${error}`);
return done();
}
});
};
}
function createAddContactFn(postData, internalLead, closeIoLead, userApiKeyMap) {
return (done) => {
// console.log('DEBUG: addContact', postData.lead_id);
// Create new contact
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/contact/`,
body: JSON.stringify(postData)
};
request.post(options, (error, response, body) => {
if (error) return done(error);
const newContact = JSON.parse(body);
if (newContact.errors || newContact['field-errors']) {
console.error(`New Contact POST error for ${postData.lead_id}`);
console.error(body);
return done();
}
// Find previous internal user for new contact correspondence
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/email/?lead_id=${closeIoLead.id}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
const data = JSON.parse(body);
let emailApiKey = data.data && data.data.length > 0 ? userApiKeyMap[data.data[0].user_id] : getEmailApiKey(closeIoLead.status_label);
if (!emailApiKey) emailApiKey = getEmailApiKey(closeIoLead.status_label);
// Send email to new contact
const email = postData.emails[0].email;
const countryCode = getCountryCode(internalLead.contacts[email].trial.properties.country, [email]);
const emailTemplate = getEmailTemplate(internalLead.contacts[email].trial.properties.siteOrigin, closeIoLead.status_label, countryCode);
sendMail(email, closeIoLead.id, newContact.id, emailTemplate, emailApiKey, emailDelayMinutes, done);
});
});
};
}
function createAddNoteFn(leadId, newNote) {
return (done) => {
// console.log('DEBUG: addNote', leadId);
const notePostData = {
note: newNote,
lead_id: leadId
};
const options = {
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/note/`,
body: JSON.stringify(notePostData)
};
request.post(options, (error, response, body) => {
if (error) return done(error);
const result = JSON.parse(body);
if (result.errors || result['field-errors']) {
console.error(`New note POST error for ${leadId}`);
console.error(body);
// console.error(notePostData);
}
return done();
});
};
}
function createSendEmailFn(email, leadId, contactId, template, leadStatus) {
return (done) => {
return sendMail(email, leadId, contactId, template, getEmailApiKey(leadStatus), emailDelayMinutes, done);
};
}
function sendMail(toEmail, leadId, contactId, template, emailApiKey, delayMinutes, done) {
// console.log('DEBUG: sendMail', toEmail, leadId, contactId, template, emailApiKey, delayMinutes);
// Check for previously sent email
const url = `https://${closeIoApiKey}:X@app.close.io/api/v1/activity/email/?lead_id=${leadId}`;
request.get(url, (error, response, body) => {
if (error) return done(error);
try {
const data = JSON.parse(body);
for (const emailData of data.data) {
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, leadId, contactId, template, emailData.contact_id);
return done();
}
}
}
}
catch (err) {
console.log(err);
console.log(body);
return done();
}
// Send mail
const dateScheduled = new Date();
dateScheduled.setUTCMinutes(dateScheduled.getUTCMinutes() + delayMinutes);
const postData = {
to: [toEmail],
contact_id: contactId,
lead_id: leadId,
template_id: template,
status: 'scheduled',
date_scheduled: dateScheduled
};
const options = {
uri: `https://${emailApiKey}:X@app.close.io/api/v1/activity/email/`,
body: JSON.stringify(postData)
};
request.post(options, (error, response, body) => {
if (error) return done(error);
const result = JSON.parse(body);
if (result.errors || result['field-errors']) {
const errorMessage = `Send email POST error for ${toEmail} ${leadId} ${contactId}`;
console.error(errorMessage);
console.error(body);
// console.error(postData);
return done(errorMessage);
}
return done();
});
});
}
function updateLeads(leads, done) {
const userApiKeyMap = {};
let createGetUserFn = (apiKey) => {
return (done) => {
const url = `https://${apiKey}:X@app.close.io/api/v1/me/`;
request.get(url, (error, response, body) => {
if (error) return done();
const results = JSON.parse(body);
userApiKeyMap[results.id] = apiKey;
return done();
});
};
}
const tasks = [];
for (const closeIoMailApiKey of closeIoMailApiKeys) {
tasks.push(createGetUserFn(closeIoMailApiKey.apiKey));
}
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 name in leads) {
if (leadsToSkip.indexOf(name) >= 0) continue;
for (const email in leads[name].contacts) {
tasks.push(createFindExistingLeadFn(email.toLowerCase(), name.toLowerCase(), existingLeads));
}
}
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
if (err) return done(err);
const tasks = [];
for (const name in leads) {
if (leadsToSkip.indexOf(name) >= 0) continue;
tasks.push(createUpdateLeadFn(leads[name], existingLeads, userApiKeyMap));
}
async.parallelLimit(tasks, closeParallelLimit, (err, results) => {
return done(err);
});
});
});
}