2016-06-18 00:05:46 -04:00
// Copy ZenProspect contacts with email replies into Close.io leads
'use strict' ;
if ( process . argv . length !== 4 ) {
console . log ( "Usage: node <script> <Close.io general API key> <ZenProspect auth token>" ) ;
process . exit ( ) ;
}
2016-08-16 08:27:56 -04:00
// NOTE: last_activity_date_range is the contacted at date, UTC
// TODO: Only looking at contacts created in last 30 days. How do we catch replies for contacts older than 30 days?
2016-06-18 00:05:46 -04:00
const closeIoApiKey = process . argv [ 2 ] ;
const zpAuthToken = process . argv [ 3 ] ;
const scriptStartTime = new Date ( ) ;
const async = require ( 'async' ) ;
const request = require ( 'request' ) ;
const zpPageSize = 100 ;
2016-08-16 08:27:56 -04:00
let zpMinActivityDate = new Date ( ) ;
zpMinActivityDate . setUTCDate ( zpMinActivityDate . getUTCDate ( ) - 30 ) ;
zpMinActivityDate = zpMinActivityDate . toISOString ( ) . substring ( 0 , 10 ) ;
2016-06-18 00:05:46 -04:00
2016-08-25 13:21:36 -04:00
const closeParallelLimit = 100 ;
2016-07-06 18:48:46 -04:00
getZPContacts ( ( err , emailContactMap ) => {
2016-06-18 00:05:46 -04:00
if ( err ) {
console . log ( err ) ;
return ;
}
const tasks = [ ] ;
for ( const email in emailContactMap ) {
const contact = emailContactMap [ email ] ;
tasks . push ( createUpsertCloseLeadFn ( contact ) ) ;
}
2016-08-25 13:21:36 -04:00
async . parallelLimit ( tasks , closeParallelLimit , ( err , results ) => {
2016-06-18 00:05:46 -04:00
if ( err ) console . log ( err ) ;
2016-06-19 02:21:06 -04:00
log ( "Script runtime: " + ( new Date ( ) - scriptStartTime ) ) ;
2016-06-18 00:05:46 -04:00
} ) ;
} ) ;
function createCloseLead ( zpContact , done ) {
const postData = {
name : zpContact . organization ,
status : 'Contacted' ,
contacts : [
{
name : zpContact . name ,
emails : [ { email : zpContact . email } ]
}
] ,
custom : {
lastUpdated : new Date ( ) ,
'Lead Origin' : 'outbound campaign'
}
} ;
if ( zpContact . phone ) {
postData . contacts [ 0 ] . phones = [ { phone : zpContact . phone } ] ;
}
2016-08-16 08:27:56 -04:00
if ( zpContact . title ) {
postData . contacts [ 0 ] . title = zpContact . title ;
}
2016-08-10 13:12:57 -04:00
if ( zpContact . district ) {
postData . custom [ 'demo_nces_district' ] = zpContact . district ;
postData . custom [ 'demo_nces_name' ] = zpContact . organization ;
}
if ( zpContact . nces _district _id ) {
postData . custom [ 'demo_nces_district_id' ] = zpContact . nces _district _id ;
}
if ( zpContact . nces _school _id ) {
postData . custom [ 'demo_nces_id' ] = zpContact . nces _school _id ;
}
2016-06-18 00:05:46 -04:00
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 newLead = JSON . parse ( body ) ;
if ( newLead . errors || newLead [ 'field-errors' ] ) {
console . error ( ` New lead POST error for ${ zpContact . name } ${ zpContact . organization } ` ) ;
return done ( newLead . errors || newLead [ 'field-errors' ] ) ;
}
return done ( ) ;
} ) ;
}
function updateCloseLead ( zpContact , existingLead , done ) {
2016-07-06 18:48:46 -04:00
// console.log(`DEBUG: updateCloseLead ${existingLead.id} ${zpContact.email}`);
2016-06-18 00:05:46 -04:00
const putData = {
status : 'Contacted' ,
'custom.lastUpdated' : new Date ( ) ,
'custom.Lead Origin' : 'outbound campaign'
} ;
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' ] ) {
return done ( ` Update existing lead PUT error for ${ existingLead . id } ${ zpContact . email } ${ result . errors || result [ 'field-errors' ] } ` ) ;
}
const postData = {
lead _id : existingLead . id ,
name : zpContact . name ,
title : zpContact . title ,
emails : [ { email : zpContact . email } ]
} ;
2016-07-06 18:48:46 -04:00
if ( zpContact . phone ) {
postData . phones = [ { phone : zpContact . phone } ] ;
}
2016-06-18 00:05:46 -04:00
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 result = JSON . parse ( body ) ;
if ( result . errors || result [ 'field-errors' ] ) {
return done ( ` New Contact POST error for ${ existingLead . id } ${ zpContact . email } ${ result . errors || result [ 'field-errors' ] } ` ) ;
}
return done ( ) ;
} ) ;
} ) ;
}
function createUpsertCloseLeadFn ( zpContact ) {
2016-08-25 13:21:36 -04:00
// 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
2016-06-18 00:05:46 -04:00
return ( done ) => {
// console.log(`DEBUG: createUpsertCloseLeadFn ${zpContact.organization} ${zpContact.email}`);
2016-08-25 13:21:36 -04:00
let query = ` email: ${ zpContact . email } ` ;
let url = ` https:// ${ closeIoApiKey } :X@app.close.io/api/v1/lead/?query= ${ encodeURIComponent ( query ) } ` ;
2016-06-18 00:05:46 -04:00
request . get ( url , ( error , response , body ) => {
if ( error ) return done ( error ) ;
const data = JSON . parse ( body ) ;
if ( data . total _results != 0 ) return done ( ) ;
2016-08-25 13:21:36 -04:00
query = ` name: ${ zpContact . organization } ` ;
if ( zpContact . nces _school _id ) {
query = ` custom.demo_nces_id:" ${ zpContact . nces _school _id } " ` ;
}
else if ( zpContact . nces _district _id ) {
query = ` custom.demo_nces_district_id:" ${ zpContact . nces _district _id } " custom.demo_nces_id:"" ` ;
}
url = ` https:// ${ closeIoApiKey } :X@app.close.io/api/v1/lead/?query= ${ encodeURIComponent ( query ) } ` ;
2016-06-18 00:05:46 -04:00
request . get ( url , ( error , response , body ) => {
if ( error ) return done ( error ) ;
const data = JSON . parse ( body ) ;
if ( data . total _results === 0 ) {
2016-08-25 13:21:36 -04:00
console . log ( ` DEBUG: Creating lead for ${ zpContact . organization } ${ zpContact . email } nces_district_id= ${ zpContact . nces _district _id } nces_school_id= ${ zpContact . nces _school _id } ` ) ;
2016-06-18 00:05:46 -04:00
return createCloseLead ( zpContact , done ) ;
}
else {
const existingLead = data . data [ 0 ] ;
2016-08-25 13:21:36 -04:00
console . log ( ` DEBUG: Adding to ${ existingLead . id } ${ zpContact . organization } ${ zpContact . email } nces_district_id= ${ zpContact . nces _district _id } nces_school_id= ${ zpContact . nces _school _id } ` ) ;
2016-06-18 00:05:46 -04:00
return updateCloseLead ( zpContact , existingLead , done ) ;
}
} ) ;
} ) ;
} ;
}
2016-07-06 18:48:46 -04:00
function getZPContactsPage ( contacts , searchQuery , done ) {
2016-06-18 00:05:46 -04:00
const options = {
2016-07-06 18:48:46 -04:00
url : ` https://www.zenprospect.com/api/v1/contacts/search? ${ searchQuery } ` ,
2016-06-18 00:05:46 -04:00
headers : {
'Accept' : 'application/json'
}
} ;
request . get ( options , ( err , response , body ) => {
if ( err ) return done ( err ) ;
const data = JSON . parse ( body ) ;
for ( let contact of data . contacts ) {
2016-08-10 13:12:57 -04:00
const newContact = {
organization : contact . organization _name ,
2016-07-06 18:48:46 -04:00
name : contact . name ,
title : contact . title ,
email : contact . email ,
phone : contact . phone ,
data : contact
2016-08-10 13:12:57 -04:00
} ;
// Check custom fields, school_name set means organization_name is district name
if ( contact . custom _fields ) {
if ( contact . custom _fields . school _name ) {
newContact . district = contact . organization _name ;
newContact . organization = contact . custom _fields . school _name ;
// console.log(`DEBUG: found contact with school name ${newContact.email} ${contact.custom_fields.school_name}`);
}
if ( contact . custom _fields . nces _district _id ) {
newContact . nces _district _id = contact . custom _fields . nces _district _id ;
// console.log(`DEBUG: found contact with district id ${newContact.email} ${newContact.nces_district_id}`);
}
if ( contact . custom _fields . nces _school _id ) {
newContact . nces _school _id = contact . custom _fields . nces _school _id ;
// console.log(`DEBUG: found contact with school id ${newContact.email} ${newContact.nces_school_id}`);
}
}
contacts . push ( newContact ) ;
2016-06-18 00:05:46 -04:00
}
return done ( null , data . pipeline _total ) ;
} ) ;
}
2016-07-06 18:48:46 -04:00
function createGetZPAutoResponderContactsPage ( contacts , page ) {
return ( done ) => {
// console.log(`DEBUG: Fetching autoresponder page ${page} ${zpPageSize}...`);
2016-08-16 08:27:56 -04:00
let searchQuery = ` codecombat_special_auth_token= ${ zpAuthToken } &page= ${ page } &per_page= ${ zpPageSize } &last_activity_date_range[min]= ${ zpMinActivityDate } &contact_email_autoresponder=true ` ;
2016-07-06 18:48:46 -04:00
getZPContactsPage ( contacts , searchQuery , done ) ;
} ;
}
function createGetZPRepliedContactsPage ( contacts , page ) {
return ( done ) => {
// console.log(`DEBUG: Fetching email reply page ${page} ${zpPageSize}...`);
2016-08-16 08:27:56 -04:00
let searchQuery = ` codecombat_special_auth_token= ${ zpAuthToken } &page= ${ page } &per_page= ${ zpPageSize } &last_activity_date_range[min]= ${ zpMinActivityDate } &contact_email_replied=true ` ;
2016-07-06 18:48:46 -04:00
getZPContactsPage ( contacts , searchQuery , done ) ;
} ;
}
function getZPContacts ( done ) {
// Get first page to get total contact count for future parallized page fetches
2016-06-18 00:05:46 -04:00
const contacts = [ ] ;
2016-07-06 18:48:46 -04:00
createGetZPAutoResponderContactsPage ( contacts , 0 ) ( ( err , autoResponderTotal ) => {
2016-06-18 00:05:46 -04:00
if ( err ) return done ( err ) ;
2016-07-06 18:48:46 -04:00
createGetZPRepliedContactsPage ( contacts , 0 ) ( ( err , repliedTotal ) => {
2016-06-18 00:05:46 -04:00
if ( err ) return done ( err ) ;
2016-07-06 18:48:46 -04:00
const tasks = [ ] ;
for ( let i = 1 ; ( i - 1 ) * zpPageSize < autoResponderTotal ; i ++ ) {
tasks . push ( createGetZPAutoResponderContactsPage ( contacts , i ) ) ;
2016-06-18 00:05:46 -04:00
}
2016-07-06 18:48:46 -04:00
for ( let i = 1 ; ( i - 1 ) * zpPageSize < repliedTotal ; i ++ ) {
tasks . push ( createGetZPRepliedContactsPage ( contacts , i ) ) ;
}
async . series ( tasks , ( err , results ) => {
if ( err ) return done ( err ) ;
const emailContactMap = { } ;
for ( const contact of contacts ) {
2016-08-16 08:27:56 -04:00
if ( ! contact . organization || ! contact . name || ! contact . email ) {
2016-08-24 17:33:47 -04:00
console . log ( ` DEBUG: missing data for zp contact ${ contact . email } : ` ) ;
// console.log(JSON.stringify(contact, null, 2));
2016-07-06 18:48:46 -04:00
}
2016-08-24 17:33:47 -04:00
else if ( ! emailContactMap [ contact . email ] ) {
2016-07-06 18:48:46 -04:00
emailContactMap [ contact . email ] = contact ;
}
// else {
// console.log(`DEBUG: already have contact ${contact.email}`);
// }
}
log ( ` ( ${ autoResponderTotal + repliedTotal } ) ${ autoResponderTotal } autoresponder ZP contacts ${ repliedTotal } ZP contacts ${ Object . keys ( emailContactMap ) . length } contacts mapped ` ) ;
return done ( null , emailContactMap ) ;
} ) ;
2016-06-18 00:05:46 -04:00
} ) ;
} ) ;
}
2016-06-19 02:21:06 -04:00
function log ( str ) {
console . log ( new Date ( ) . toISOString ( ) + " " + str ) ;
}