mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 16:51:35 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
72804ffc3e
4 changed files with 112 additions and 87 deletions
|
@ -100,6 +100,7 @@
|
|||
"coffeelint-brunch": "^1.7.1",
|
||||
"commonjs-require-definition": "0.2.0",
|
||||
"compressible": "~1.0.1",
|
||||
"country-data": "0.0.24",
|
||||
"country-list": "0.0.3",
|
||||
"css-brunch": "^1.7.0",
|
||||
"fs-extra": "^0.26.2",
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// Migrate users from coursePrepaidID to coursePrepaid
|
||||
|
||||
startDate = new Date(Date.UTC(2016,4,22)).toISOString(); // NOTE: Month is 0 indexed...
|
||||
endDate = new Date(Date.UTC(2017,4,22)).toISOString();
|
||||
print('Setting start/end', startDate, endDate);
|
||||
startDate = new Date(Date.UTC(2016,4,15)).toISOString(); // NOTE: Month is 0 indexed...
|
||||
endDate = new Date(Date.UTC(2017,5,1)).toISOString();
|
||||
|
||||
db.prepaids.find({type: 'course'}).limit(10).forEach(function (prepaid) {
|
||||
cutoffDate = new Date(2015,11,11);
|
||||
cutoffID = ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000');
|
||||
|
||||
print('Setting start/end', startDate, endDate, cutoffID);
|
||||
|
||||
var cursor = db.prepaids.find({type: 'course', _id: { $gt: cutoffID }})
|
||||
|
||||
cursor.forEach(function (prepaid) {
|
||||
var properties = prepaid.properties || {};
|
||||
if (!(prepaid.endDate && prepaid.startDate)) {
|
||||
if (!prepaid.endDate) {
|
||||
|
@ -19,21 +25,21 @@ db.prepaids.find({type: 'course'}).limit(10).forEach(function (prepaid) {
|
|||
if (!prepaid.startDate) {
|
||||
prepaid.startDate = startDate;
|
||||
}
|
||||
print('updating prepaid', JSON.stringify(prepaid, null, '\t'));
|
||||
//print(db.prepaids.save(prepaid));
|
||||
print('updating prepaid', prepaid._id, 'creator', prepaid.creator, 'start/end', prepaid.startDate, prepaid.endDate);
|
||||
print(' -', db.prepaids.save(prepaid));
|
||||
}
|
||||
|
||||
var redeemers = prepaid.redeemers || [];
|
||||
for (var index in redeemers) {
|
||||
var redeemer = redeemers[index];
|
||||
var user = db.users.findOne({ _id: redeemer.userID }, { coursePrepaid: 1, coursePrepaidID: 1 });
|
||||
var user = db.users.findOne({ _id: redeemer.userID }, { coursePrepaid: 1, coursePrepaidID: 1, email:1, name:1, permissions: 1 });
|
||||
if (user.coursePrepaidID && !user.coursePrepaid) {
|
||||
var update = {
|
||||
$set: { coursePrepaid: { _id: user.coursePrepaidID, startDate: prepaid.startDate, endDate: prepaid.endDate } },
|
||||
$unset: { coursePrepaidID: '' }
|
||||
}
|
||||
print('updating user', JSON.stringify(user, null, ' '), JSON.stringify(update, null, ' '));
|
||||
//print(db.users.update({_id: user._id}, update));
|
||||
print('\t updating user', user._id, user.name, user.email, user.permissions, JSON.stringify(update));
|
||||
print('\t', db.users.update({_id: user._id}, update));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// Upsert new lead data into Close.io
|
||||
|
||||
'use strict';
|
||||
if (process.argv.length !== 8) {
|
||||
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <Intercom 'App ID:API key'> <mongo connection Url>");
|
||||
if (process.argv.length !== 9) {
|
||||
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 EU mail API key> <Intercom 'App ID:API key'> <mongo connection Url>");
|
||||
process.exit();
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ if (process.argv.length !== 8) {
|
|||
// 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
|
||||
|
||||
// Save as custom fields instead of user-specific lead notes (also saving nces_ props)
|
||||
const commonTrialProperties = ['organization', 'city', 'state', 'country'];
|
||||
|
@ -32,6 +33,8 @@ const createTeacherEmailTemplatesAuto1 = ['tmpl_i5bQ2dOlMdZTvZil21bhTx44JYoojPbF
|
|||
const demoRequestEmailTemplatesAuto1 = ['tmpl_s7BZiydyCHOMMeXAcqRZzqn0fOtk0yOFlXSZ412MSGm', 'tmpl_cGb6m4ssDvqjvYd8UaG6cacvtSXkZY3vj9b9lSmdQrf'];
|
||||
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 = [
|
||||
|
@ -45,8 +48,9 @@ const closeIoInitialLeadStatuses = [
|
|||
];
|
||||
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'];
|
||||
const usSchoolStatuses = ['Auto Attempt 1', 'New US Schools Auto Attempt 1', 'New US Schools Auto Attempt 1 Low'];
|
||||
|
||||
const emailDelayMinutes = 27;
|
||||
|
||||
|
@ -54,12 +58,14 @@ const scriptStartTime = new Date();
|
|||
const closeIoApiKey = process.argv[2];
|
||||
// Automatic mails sent as API owners, first key assumed to be primary and gets 50% of the leads
|
||||
const closeIoMailApiKeys = [process.argv[3], process.argv[3], process.argv[4], process.argv[5]];
|
||||
const intercomAppIdApiKey = process.argv[6];
|
||||
const closeIoEuMailApiKey = process.argv[6];
|
||||
const intercomAppIdApiKey = process.argv[7];
|
||||
const intercomAppId = intercomAppIdApiKey.split(':')[0];
|
||||
const intercomApiKey = intercomAppIdApiKey.split(':')[1];
|
||||
const mongoConnUrl = process.argv[7];
|
||||
const mongoConnUrl = process.argv[8];
|
||||
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');
|
||||
|
@ -98,12 +104,31 @@ function upsertLeads(done) {
|
|||
|
||||
// ** Utilities
|
||||
|
||||
function getCountryCode(country, emails) {
|
||||
// console.log(`DEBUG: getCountryCode ${country} ${emails.length}`);
|
||||
if (country) {
|
||||
let countryCode = countryList.getCode(country);
|
||||
if (countryCode) return countryCode;
|
||||
}
|
||||
for (const email of emails) {
|
||||
const tld = parseDomain(email).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 isLowValueLead(status, trialRequests) ? `${status} Low` : status;
|
||||
return isLowValueUsLead(status, trialRequests) ? `${status} Low` : status;
|
||||
}
|
||||
if (/^england$|^uk$|^united kingdom$/ig.test(country)) {
|
||||
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)) {
|
||||
|
@ -118,13 +143,18 @@ function getInitialLeadStatusViaCountry(country, trialRequests) {
|
|||
if (/bolivia|iran|korea|macedonia|taiwan|tanzania|^venezuela$/ig.test(country)) {
|
||||
return defaultInternationalLeadStatus;
|
||||
}
|
||||
if (countryList.getCode(country)) {
|
||||
const countryCode = countryList.getCode(country);
|
||||
if (countryCode) {
|
||||
if (isEuCountryCode(countryCode)) {
|
||||
return highValueLead ? 'Inbound EU Auto Attempt 1 High' : 'Inbound EU Auto Attempt 1';
|
||||
}
|
||||
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) {
|
||||
|
@ -137,16 +167,42 @@ function getInitialLeadStatusViaEmails(emails, trialRequests) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!currentStatus || [defaultLeadStatus, defaultInternationalLeadStatus].indexOf(currentStatus) >= 0) {
|
||||
// Look for a better EU match
|
||||
const countryCode = getCountryCode(null, emails);
|
||||
if (isEuCountryCode(countryCode)) {
|
||||
return isHighValueLead(trialRequests) ? 'Inbound EU Auto Attempt 1 High' : 'Inbound EU Auto Attempt 1';
|
||||
}
|
||||
}
|
||||
currentStatus = currentStatus ? currentStatus : defaultLeadStatus;
|
||||
return isLowValueLead(currentStatus, trialRequests) ? `${currentStatus} Low` : currentStatus;
|
||||
return isLowValueUsLead(currentStatus, trialRequests) ? `${currentStatus} Low` : currentStatus;
|
||||
}
|
||||
|
||||
function isLowValueLead(status, trialRequests) {
|
||||
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
|
||||
|
@ -158,11 +214,22 @@ function isLowValueLead(status, trialRequests) {
|
|||
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 getRandomEmailApiKey() {
|
||||
function getEmailApiKey(leadStatus) {
|
||||
if (leadStatus === 'Inbound EU Auto Attempt 1') return closeIoEuMailApiKey;
|
||||
if (closeIoMailApiKeys.length < 0) return;
|
||||
return closeIoMailApiKeys[Math.floor(Math.random() * closeIoMailApiKeys.length)];
|
||||
}
|
||||
|
@ -172,14 +239,20 @@ function getRandomEmailTemplate(templates) {
|
|||
return templates[Math.floor(Math.random() * templates.length)];
|
||||
}
|
||||
|
||||
function getEmailTemplate(siteOrigin, leadStatus) {
|
||||
// console.log(`DEBUG: getEmailTemplate ${siteOrigin} ${leadStatus}`);
|
||||
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;
|
||||
}
|
||||
|
@ -599,8 +672,9 @@ function saveNewLead(lead, done) {
|
|||
const tasks = [];
|
||||
for (const contact of existingLead.contacts) {
|
||||
for (const email of contact.emails) {
|
||||
const emailTemplate = getEmailTemplate(lead.contacts[email.email].trial.properties.siteOrigin, postData.status);
|
||||
tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, emailTemplate));
|
||||
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.parallel(tasks, (err, results) => {
|
||||
|
@ -689,8 +763,9 @@ function createAddContactFn(postData, internalLead, externalLead) {
|
|||
|
||||
// Send emails 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, externalLead.status_label);
|
||||
sendMail(email, externalLead.id, newContact.id, emailTemplate, getRandomEmailApiKey(), emailDelayMinutes, done);
|
||||
sendMail(email, externalLead.id, newContact.id, emailTemplate, getEmailApiKey(externalLead.status_label), emailDelayMinutes, done);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -719,9 +794,9 @@ function createAddNoteFn(leadId, newNote) {
|
|||
};
|
||||
}
|
||||
|
||||
function createSendEmailFn(email, leadId, contactId, template) {
|
||||
function createSendEmailFn(email, leadId, contactId, template, leadStatus) {
|
||||
return (done) => {
|
||||
return sendMail(email, leadId, contactId, template, getRandomEmailApiKey(), emailDelayMinutes, done);
|
||||
return sendMail(email, leadId, contactId, template, getEmailApiKey(leadStatus), emailDelayMinutes, done);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -161,63 +161,6 @@ describe 'POST /db/prepaid/:handle/redeemers', ->
|
|||
done()
|
||||
|
||||
|
||||
describe 'DELETE /db/prepaid/:handle/redeemers', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, CourseInstance, Payment, Prepaid, User])
|
||||
@teacher = yield utils.initUser({role: 'teacher'})
|
||||
@admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(@admin)
|
||||
@prepaid = yield utils.makePrepaid({ creator: @teacher.id })
|
||||
yield utils.loginUser(@teacher)
|
||||
@student = yield utils.initUser()
|
||||
@url = getURL("/db/prepaid/#{@prepaid.id}/redeemers")
|
||||
[res, body] = yield request.postAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(res.statusCode).toBe(201)
|
||||
done()
|
||||
|
||||
it 'removes a given user to the redeemers property', utils.wrap (done) ->
|
||||
prepaid = yield Prepaid.findById(@prepaid.id)
|
||||
expect(prepaid.get('redeemers').length).toBe(1)
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(body.redeemers.length).toBe(0)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = yield Prepaid.findById(body._id)
|
||||
expect(prepaid.get('redeemers').length).toBe(0)
|
||||
student = yield User.findById(@student.id)
|
||||
expect(student.get('coursePrepaid')).toBeUndefined()
|
||||
done()
|
||||
|
||||
it 'works if the user has not migrated from coursePrepaidID to coursePrepaid', utils.wrap (done) ->
|
||||
yield @student.update({
|
||||
$set: { coursePrepaidID: @prepaid._id }
|
||||
$unset: { coursePrepaid: '' }
|
||||
})
|
||||
yield @student.save()
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(body.redeemers.length).toBe(0)
|
||||
expect(res.statusCode).toBe(200)
|
||||
prepaid = yield Prepaid.findById(body._id)
|
||||
expect(prepaid.get('redeemers').length).toBe(0)
|
||||
student = yield User.findById(@student.id)
|
||||
expect(student.get('coursePrepaid')).toBeUndefined()
|
||||
expect(student.get('coursePrepaidID')).toBeUndefined()
|
||||
done()
|
||||
|
||||
it 'returns 403 unless the user is the "creator"', utils.wrap (done) ->
|
||||
otherTeacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(otherTeacher)
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: @student.id } }
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'returns 422 unless the target user is in "redeemers"', utils.wrap (done) ->
|
||||
otherStudent = yield utils.initUser({role: 'student'})
|
||||
[res, body] = yield request.delAsync {uri: @url, json: { userID: otherStudent.id } }
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/prepaid?creator=:id', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, CourseInstance, Payment, Prepaid, User])
|
||||
|
@ -246,7 +189,7 @@ describe 'GET /db/prepaid?creator=:id', ->
|
|||
done()
|
||||
|
||||
it 'returns 403 if the user tries to view another user\'s prepaids', utils.wrap (done) ->
|
||||
anotherUser = yield utils.initUser()
|
||||
anotherUser = yield utils.initUser()
|
||||
url = getURL("/db/prepaid?creator=#{anotherUser.id}")
|
||||
[res, body] = yield request.getAsync({uri: url, json: true})
|
||||
expect(res.statusCode).toBe(403)
|
||||
|
@ -755,14 +698,14 @@ describe '/db/prepaid', ->
|
|||
|
||||
# Spawn all requests at once!
|
||||
requests = []
|
||||
options = {
|
||||
options = {
|
||||
url: getURL('/db/subscription/-/subscribe_prepaid')
|
||||
json: { ppc: prepaid.code }
|
||||
}
|
||||
for thread in threads
|
||||
requests.push(thread.request.postAsync(options))
|
||||
|
||||
# Wait until all requests finish, make sure all but one succeeded
|
||||
# Wait until all requests finish, make sure all but one succeeded
|
||||
responses = yield requests
|
||||
redeemed = _.size(_.where(responses, {statusCode: 200}))
|
||||
errors = _.size(_.where(responses, {statusCode: 403}))
|
||||
|
|
Loading…
Reference in a new issue