@dhyasama/totem-models 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Person.js ADDED
@@ -0,0 +1,1163 @@
1
+ "use strict";
2
+
3
+ module.exports = function(mongoose, config) {
4
+
5
+ var customerCompanyIds = [];
6
+
7
+ var
8
+
9
+ Schema = mongoose.Schema,
10
+ LimitedPartner = mongoose.model('LimitedPartner'),
11
+ Note = mongoose.model('Note'),
12
+ Crypto = require('@dhyasama/ffvc-crypto'),
13
+ crypto = new Crypto({'key': config.crypto.key}),
14
+ domain = config.domain,
15
+ utils = require('@dhyasama/ffvc-utilities'),
16
+ async = require('async'),
17
+ env = process.env.NODE_ENV || 'development',
18
+ bcrypt = require('bcrypt'),
19
+ _ = require('underscore'),
20
+ phoneFormatter = require('phone-formatter'),
21
+ PhoneNumber = require( 'awesome-phonenumber'),
22
+ validator = require('validator'),
23
+ escapeStringRegexp = require('escape-string-regexp');
24
+
25
+ var Person = new Schema({
26
+
27
+ name: {
28
+ first: { type: String, default: '', index: true },
29
+ last: { type: String, default: '', index: true }
30
+ },
31
+
32
+ slug: { type: String, required: true, unique: true, sparse: false, trim: true, lowercase: true },
33
+
34
+ contact: {
35
+
36
+ phone: [{
37
+ type: { type: String, enum: ['home', 'work', 'assistant', 'mobile', 'other'] },
38
+ number: { type: String, default: '' },
39
+ primary: { type: Boolean, default: false }
40
+ }],
41
+
42
+ email: [{
43
+ type: { type: String, enum: ['personal', 'work', 'assistant', 'other'] },
44
+ email: { type: String, default: '', es_type: 'text' },
45
+ primary: { type: Boolean, default: false }
46
+ }],
47
+
48
+ address: [{
49
+ type: { type: String, default: '' },
50
+ street: { type: String, default: '' },
51
+ suite: { type: String, default: '' },
52
+ city: { type: String, default: '' },
53
+ state: { type: String, default: '' },
54
+ country: { type: String, default: '' },
55
+ postalCode: { type: String, default: '' },
56
+ primary: { type: Boolean, default: false }
57
+ }]
58
+
59
+ },
60
+
61
+ title: { type: String, default: null, trim: true },
62
+
63
+ birthday: { type: String, default: null, trim: true },
64
+
65
+ bio: { type: String, default: null, trim: true },
66
+
67
+ website: { type: String, default: null, required: false, trim: true, lowercase: true },
68
+
69
+ crunchbase: {
70
+ uuid: { type: String, unique: true, required: false, sparse: true, trim: true }
71
+ },
72
+
73
+ vCardUrl: { type: String, default: null, trim: true },
74
+
75
+ social: {
76
+ twitter: { type: String, trim: true, unique: true, sparse: true },
77
+ facebook: { type: String, trim: true, unique: true, sparse: true },
78
+ linkedin: { type: String, trim: true, unique: true, sparse: true }
79
+ },
80
+
81
+ avatarUrl: { type: String, default: null, trim: true },
82
+
83
+ sources: [{
84
+ _id: false,
85
+ person: { type: Schema.ObjectId, ref: 'Person' },
86
+ customer: { type: Schema.ObjectId, ref: 'Organization' },
87
+ interactions: {
88
+ calendar: { type: Number, required: false, default: 0 },
89
+ email: { type: Number, required: false, default: 0 }
90
+ }
91
+ }],
92
+
93
+ notes: [ { type: Schema.ObjectId, ref: 'Note' } ],
94
+
95
+ // this only relates to access for lps
96
+ syndication: {
97
+ notify: { type: Boolean, default: false }
98
+ },
99
+
100
+ // this only relates to access for lps
101
+ subscriptions: [{ id: { type: Schema.ObjectId, required: false, default: null }}],
102
+
103
+ account: { type: Schema.ObjectId, ref: 'Account', required: false },
104
+
105
+ entered: {
106
+ by: { type: String, default: null, trim: true },
107
+ on: { type: Date, default: Date.now }
108
+ },
109
+
110
+ flagged: {
111
+ by: { type: String, default: null, trim: true },
112
+ on: { type: Date, default: Date.now },
113
+ text: { type: String, default: null, trim: true },
114
+ resolved: { type: Boolean, default: false }
115
+ },
116
+
117
+ merged: [{ type: Schema.ObjectId, ref: 'Person' }],
118
+
119
+ deleted: { type: Boolean, default: false },
120
+
121
+ aliases: [ { type: String, trim: true } ],
122
+
123
+ related: [{ type: Schema.ObjectId, ref: 'Person' }],
124
+
125
+ calendarEventSummaries: [
126
+ {
127
+ calendarEventId: { type: Schema.ObjectId, ref: 'CalendarEvent', required: true },
128
+ providerEventId: { type: String, required: true, index: true, trim: true },
129
+ totemCustomerId: { type: Schema.ObjectId, ref: 'Organization', required: true },
130
+ summary: { type: String, required: true, trim: true },
131
+ startTime: { type: Date },
132
+ attendees: {
133
+ internal: [{ type: Schema.ObjectId, ref: 'Person' }],
134
+ external: [{ type: Schema.ObjectId, ref: 'Person' }]
135
+ },
136
+ notes: [ { type: Schema.ObjectId, ref: 'Note' } ]
137
+ }
138
+ ]
139
+
140
+ });
141
+
142
+ /////////////
143
+
144
+ Person.virtual('name.full').get(function () {
145
+ return this.name.first + ' ' + this.name.last;
146
+ });
147
+
148
+ Person.virtual('avatar').get(function() {
149
+ return this.avatarUrl || domain + '/media/moustache.jpg';
150
+ });
151
+
152
+ Person.virtual('email.primary').get(function () {
153
+ var primary = _.find(this.contact.email, function(email) {
154
+ return email.primary;
155
+ });
156
+ return primary ? primary.email : '';
157
+ });
158
+
159
+ Person.virtual('phone.primary').get(function () {
160
+ var primary = _.find(this.contact.phone, function(phone) {
161
+ return phone.primary;
162
+ });
163
+ return primary ? primary.number : '';
164
+ });
165
+
166
+ Person.virtual('interactions.count').get(function () {
167
+
168
+ // handle missing doc data
169
+ this.sources = this.sources || [];
170
+
171
+ var calendarCount = 0;
172
+ var emailCount = 0;
173
+
174
+ _.each(this.sources, function(source) {
175
+
176
+ // Only count interactions for the current customer
177
+ // A universal interaction count isn't useful
178
+ var cid = mongoose.Types.ObjectId.isValid(source.customer) ? source.customer : source.customer._id;
179
+
180
+ if (cid.toString() == config.CUSTOMER_ID) {
181
+ source.interactions = source.interactions || {};
182
+ calendarCount += source.interactions.calendar || 0;
183
+ emailCount += source.interactions.email || 0;
184
+ }
185
+
186
+ });
187
+
188
+ return calendarCount + emailCount;
189
+
190
+ });
191
+
192
+ Person.virtual('allNotes').get(function () {
193
+
194
+ // combine direct person notes and notes from events this person attended
195
+ // sort reverse chronologically
196
+
197
+ var personNotes = this.notes || [];
198
+ personNotes = _.compact(personNotes);
199
+
200
+ var eventNotes = _.pluck(this.calendarEventSummaries || [], 'notes');
201
+ eventNotes = _.flatten(eventNotes);
202
+ eventNotes = _.compact(eventNotes);
203
+
204
+ var result = _.sortBy(personNotes.concat(eventNotes), 'createdOn');
205
+
206
+ return result.reverse();
207
+
208
+ });
209
+
210
+ /////////////
211
+
212
+ Person.methods.addMerged = function(id) {
213
+ if (this.merged.indexOf(id) == -1) {
214
+ this.merged.push(id);
215
+ return true;
216
+ }
217
+ else {
218
+ return false;
219
+ }
220
+ };
221
+
222
+ Person.methods.deletePerson = function() {
223
+ this.deleted = true;
224
+ };
225
+
226
+ Person.methods.relatePerson = function(id) {
227
+ this.related.push(id);
228
+ };
229
+
230
+ Person.methods.addChairPos = function(chairObj, orgId) {
231
+
232
+ var chair = {
233
+ company: orgId,
234
+ start: ''
235
+ };
236
+
237
+ switch(chairObj.num) {
238
+ case 'chair1':
239
+ this.chairs.first.push(chair);
240
+ break;
241
+
242
+ case 'chair2':
243
+ this.chairs.second.push(chair);
244
+ break;
245
+ }
246
+
247
+ };
248
+
249
+ Person.methods.removeChair = function(chairObj, orgId) {
250
+
251
+ var field = 'chairs.first';
252
+ switch(chairObj.num) {
253
+ case 'chair1':
254
+ this.chairs.first = _.filter(this.chairs.first, function(chair) {
255
+ var id = utils.isValidObjectId(chair.company.toString()) ? chair.company.toString() : chair.company.id.toString();
256
+ return id != orgId;
257
+ });
258
+ break;
259
+
260
+ case 'chair2':
261
+ field = 'chairs.second';
262
+ this.chairs.second = _.filter(this.chairs.second, function(chair) {
263
+ var id = utils.isValidObjectId(chair.company.toString()) ? chair.company.toString() : chair.company.id.toString();
264
+ return chair.company.id != orgId;
265
+ });
266
+ break;
267
+ }
268
+
269
+ if(chairObj.val && chairObj.val == "") {
270
+ this.previous.push({
271
+ field: field,
272
+ value: {
273
+ company: orgId
274
+ }
275
+ });
276
+ }
277
+
278
+ };
279
+
280
+ /************* ACCESS **********************/
281
+
282
+ Person.methods.funds = function() {
283
+ return getActiveFunds(this);
284
+ };
285
+
286
+ Person.methods.isSubscriber = function() {
287
+ return this.subscriptions && this.subscriptions.length >= 1;
288
+ };
289
+
290
+ ////////////
291
+
292
+ // search for people based on fields provided
293
+ // Data format, everything is optional
294
+ //{
295
+ // emails: [],
296
+ // phones: [],
297
+ // name: {
298
+ // first: '',
299
+ // last: '',
300
+ // full: ''
301
+ // },
302
+ // rawQuery: ''
303
+ //}
304
+ // first and last names are prioritized over full
305
+ // full, if used, is tokenized into first and last
306
+ // fuzzy searches within strings, e.g., searching for "Drew" will match "Andrew"
307
+ // non-fuzzy does a full-string match, e.g., "Andrew" will match "Andrew" but "Drew" will not
308
+ // both fuzzy and non are case-insensitive
309
+ // fuzzy only applies to name, not email or phone
310
+ // related people are removed from top-level results
311
+ Person.statics.search = function (data, options, cb) {
312
+
313
+ // validate input
314
+ if (!cb) throw new Error('cb is required');
315
+ if (!data || (!data.emails && !data.phones && !data.name)) return cb(null, []);
316
+
317
+ var self = this;
318
+ var query;
319
+
320
+ var buildQuery = function buildQuery(data, options) {
321
+
322
+ var cleanQuery = function cleanQuery(query) {
323
+
324
+ // "and" isn't used, so remove it
325
+ if (query['$and'].length == 0) {
326
+ delete query['$and'];
327
+ }
328
+
329
+ // "and" is only used for name, which is one more thing to "or" so move it to "or"
330
+ // note that changes may be needed if "and" is used for other things as well
331
+ else {
332
+ query['$or'].push({ '$and': query['$and'] });
333
+ delete query['$and'];
334
+ }
335
+
336
+ return query;
337
+
338
+ };
339
+
340
+ var getNicknames = function getNicknames(word, dictionary) {
341
+ var synonyms = dictionary[word.toLowerCase()];
342
+ if (!synonyms) return [word];
343
+ return synonyms;
344
+ };
345
+
346
+ var oneWordFullNameQuery = function oneWordFullNameQuery(data) {
347
+ var fullNameContainsSpace = data.name.full.indexOf(' ') >= 0;
348
+ var result = data.name.full.length >= 1 && fullNameContainsSpace == false && data.original.name.first.length == 0 && data.original.name.last.length == 0;
349
+ return result;
350
+ };
351
+
352
+ var prepareDictionary = function prepareDictionary() {
353
+
354
+ var dictionary = require('../dictionaries/nicknames.json');
355
+
356
+ var dict = {};
357
+
358
+ // Expand synonyms dictionary for caching
359
+ _.each(dictionary, function(synonyms, word) {
360
+ var key = word.toLowerCase();
361
+ dict[key] = [word].concat(synonyms);
362
+ // Add values as keys for back-matching
363
+ dict[key].forEach(function(synonym) {
364
+ var synonymKey = synonym.toLowerCase();
365
+ if (!dict[synonymKey]) dict[synonymKey] = [word];
366
+ dict[synonymKey] = _.uniq(dict[synonymKey].concat(dict[key]));
367
+ });
368
+ });
369
+
370
+ return dict;
371
+
372
+ };
373
+
374
+ // prepare query
375
+ var query = {};
376
+ query['$or'] = []; // these will be added dynamically from data parameter
377
+ query['$and'] = []; // these will be added dynamically from data parameter
378
+ query['deleted'] = { $ne: true }; // always include this
379
+
380
+ var dictionary = prepareDictionary();
381
+ var nicknames = getNicknames(data.name.first, dictionary);
382
+ var firstEscaped = escapeStringRegexp(data.name.first);
383
+ var firstRegExp = options.fuzzy ? new RegExp(firstEscaped, 'i') : new RegExp('^' + firstEscaped + '$', 'i');
384
+ var lastEscaped = escapeStringRegexp(data.name.last);
385
+ var lastRegExp = options.fuzzy ? new RegExp(lastEscaped, 'i') : new RegExp('^' + lastEscaped + '$', 'i');
386
+ var key = options.andFirstLast && data.name.first && data.name.last ? '$and' : '$or';
387
+
388
+ // if we only have one word, search both first and last
389
+ if (oneWordFullNameQuery(data)) {
390
+
391
+ query['$or'].push({ 'name.first': firstRegExp });
392
+ query['$or'].push({ 'name.last': firstRegExp });
393
+
394
+ }
395
+
396
+ // options say to "and", rather than "or", first and last names
397
+ else if (options.andFirstLast && data.name.first && data.name.last) {
398
+
399
+ _.each(nicknames, function(nickname) {
400
+ var escapedNickname = escapeStringRegexp(nickname);
401
+ var nicknameRegExp = options.fuzzy ? new RegExp(escapedNickname, 'i') : new RegExp('^' + escapedNickname + '$', 'i');
402
+ query['$or'].push({ 'name.first': nicknameRegExp, 'name.last': lastRegExp });
403
+ });
404
+
405
+ }
406
+ else {
407
+
408
+ // include first unless options say not to
409
+ if (data.name.first && options.orLastOnly == false) {
410
+
411
+ _.each(nicknames, function (nickname) {
412
+ var escapedNickname = escapeStringRegexp(nickname);
413
+ var nicknameRegExp = options.fuzzy ? new RegExp(escapedNickname, 'i') : new RegExp('^' + escapedNickname + '$', 'i');
414
+ query[key].push({'name.first': nicknameRegExp});
415
+ });
416
+
417
+ }
418
+
419
+ // always include last if it's provided
420
+ if (data.name.last) {
421
+ query[key].push({ 'name.last': lastRegExp });
422
+ }
423
+
424
+ }
425
+
426
+ // search alias as well
427
+ if (data.name.first || data.name.last) {
428
+ var fullName = escapeStringRegexp((data.name.first + ' ' + data.name.last).trim());
429
+ var aliasRegExp = options.fuzzy ? new RegExp(fullName, 'i') : new RegExp('^' + fullName + '$', 'i');
430
+ query['$or'].push({ 'aliases': aliasRegExp });
431
+ }
432
+
433
+ // conditionally include emails and phones
434
+ if (data.emails) query['$or'].push({'contact.email.email': { $in : data.emails }});
435
+ if (data.phones) query['$or'].push({'contact.phone.number': { $in : data.phones }});
436
+
437
+ query = cleanQuery(query);
438
+
439
+ return query;
440
+
441
+ };
442
+
443
+ var prepData = function prepData(data) {
444
+
445
+ // add raw query to email query if it is an email
446
+ var addIfEmail = function addIfEmail(data) {
447
+
448
+ if (validator.isEmail(data.rawQuery)) {
449
+ if (data.emails.indexOf(data.rawQuery) == -1) {
450
+ data.emails.push(data.rawQuery);
451
+ }
452
+ }
453
+
454
+ return data;
455
+
456
+ };
457
+
458
+ // add raw query to phone query if it is a phone
459
+ var addIfPhone = function addIfPhone(data) {
460
+
461
+ var pn = new PhoneNumber(data.rawQuery, 'US');
462
+
463
+ if (pn.isValid()) {
464
+ if (data.phones.indexOf(data.rawQuery) == -1) {
465
+ data.phones.push(data.rawQuery);
466
+ }
467
+ }
468
+
469
+ return data;
470
+
471
+ };
472
+
473
+ // remove name from data if it's actually an email
474
+ var removeIfEmail = function removeIfEmail(data) {
475
+
476
+ if (validator.isEmail(data.name.full)) data.name.full = '';
477
+ if (validator.isEmail(data.name.first)) data.name.first = '';
478
+ if (validator.isEmail(data.name.last)) data.name.last = '';
479
+
480
+ return data;
481
+
482
+ };
483
+
484
+ // remove name from data if it's actually a phone number
485
+ var removeIfPhone = function removeIfPhone(data) {
486
+
487
+ var pnFull = new PhoneNumber(data.name.full, 'US');
488
+ if (pnFull.isValid()) data.name.full = '';
489
+
490
+ var pnFirst = new PhoneNumber(data.name.first, 'US');
491
+ if (pnFirst.isValid()) data.name.first = '';
492
+
493
+ var pnLast = new PhoneNumber(data.name.last, 'US');
494
+ if (pnLast.isValid()) data.name.last = '';
495
+
496
+ return data;
497
+
498
+ };
499
+
500
+ // break full name into parts
501
+ var tokenizeName = function tokenizeName(data) {
502
+
503
+ var parts = [];
504
+
505
+ try { parts = data.name.full.toString().split(' '); }
506
+ catch (err) { return data; }
507
+
508
+ // LIMITATION: Doesn't handle 2+ word first names, i.e., Bobbi Sue is fucked
509
+
510
+ // if there is at least one part, use the first, but don't overwrite
511
+ if (parts.length >= 1 && !data.name.first) data.name.first = parts[0].trim();
512
+
513
+ // if there are at least two parts, join all but the first, but don't overwrite
514
+ if (parts.length >= 2 && !data.name.last) data.name.last = parts.slice(1).join(' ');
515
+
516
+ return data;
517
+
518
+ };
519
+
520
+ // save the original name
521
+ data.original =
522
+ data.original = {
523
+ name: {
524
+ first: data.name && data.name.first || '',
525
+ last: data.name && data.name.last || ''
526
+ }
527
+ };
528
+
529
+ // make sure each field exists, even if empty, so we don't have to constantly check
530
+ data.emails = data.emails || [];
531
+ data.phones = data.phones || [];
532
+ data.name = data.name || {};
533
+ data.name.first = data.name.first || '';
534
+ data.name.last = data.name.last || '';
535
+ data.name.full = data.name.full || '';
536
+ data.rawQuery = data.rawQuery || '';
537
+
538
+ data = addIfEmail(data);
539
+ data = removeIfEmail(data);
540
+ data = addIfPhone(data);
541
+ data = removeIfPhone(data);
542
+ data = tokenizeName(data);
543
+
544
+ return data;
545
+
546
+ };
547
+
548
+ // All entities are searched initially, including entities that are listed as related to other entities
549
+ // Remove related entities from top-level results so they only come up as related
550
+ var removeRelatedFromTopLevel = function(items) {
551
+
552
+ var getRelatedIds = function getRelatedIds(items) {
553
+
554
+ var result = [];
555
+
556
+ _.each(items, function(item) {
557
+ if (item.related.length >= 1) {
558
+ var ids = _.pluck(item.related, 'id');
559
+ result = result.concat(ids);
560
+ }
561
+ });
562
+
563
+ return _.uniq(result);
564
+
565
+ };
566
+
567
+ var relatedIds = getRelatedIds(items);
568
+ items = _.reject(items, function(item) {
569
+ return relatedIds.indexOf(item.id) >= 0;
570
+ });
571
+
572
+ return items;
573
+
574
+ };
575
+
576
+ var defaultOptions = {
577
+
578
+ // accept partial matches
579
+ fuzzy: true,
580
+
581
+ // use 'and' clause, rather than 'or', between first and last
582
+ // note this trumps orLastOnly
583
+ andFirstLast: false,
584
+
585
+ // when not 'anding' first and last, query on last name but not first
586
+ // note this is trumped by andFirstLast
587
+ orLastOnly: true,
588
+
589
+ removeRelatedFromTopLevel: true
590
+
591
+ };
592
+
593
+ // combine provided and default options
594
+ options = utils.setDefaults(options, defaultOptions);
595
+
596
+ // need to be admin or provide investor id
597
+ if (!options.admin && !options.investor) return cb(null, []);
598
+
599
+ data = prepData(data);
600
+
601
+ query = buildQuery(data, options);
602
+
603
+ self
604
+ .find(query)
605
+ .select('avatarUrl name title related')
606
+ .exec(function(err, people) {
607
+
608
+ if (err) return cb(err, null);
609
+ else if (!people) return cb(null, null);
610
+ else {
611
+
612
+ if (options.removeRelatedFromTopLevel) people = removeRelatedFromTopLevel(people);
613
+
614
+ people = _.sortBy(people, function(person) { return person.name.full; });
615
+
616
+ return cb(null, people);
617
+
618
+ }
619
+
620
+ });
621
+
622
+ };
623
+
624
+ Person.statics.findDupes = function(person, options, cb) {
625
+
626
+ if (!cb) throw new Error('cb is required');
627
+ if (!person) return cb(null, []);
628
+
629
+ var emails = [];
630
+ if (person.contact && person.contact.email) {
631
+ // pluck emails
632
+ emails = _.map(person.contact.email, function(email) {
633
+ return email.email;
634
+ });
635
+ }
636
+
637
+ var phones = [];
638
+ if (person.contact && person.contact.phone) {
639
+ // pluck and normalize phone numbers
640
+ phones = _.map(person.contact.phone, function(phone) {
641
+ return phoneFormatter.normalize(phone.number);
642
+ });
643
+ }
644
+
645
+ // Sometimes clients pass in something like { name: 'Jason Reynolds' } rather than { name: { first: 'Jason', last: 'Reynolds' } }
646
+ // Parse it out as a courtesy
647
+ if (!person.name.first && !person.name.last) {
648
+ var fullName = person.name;
649
+ person = {
650
+ name: {
651
+ first: fullName.split(" ")[0],
652
+ last: fullName.split(" ")[1]
653
+ }
654
+ };
655
+ }
656
+
657
+ // Require names that are at least two characters long
658
+ if (person.name.first.length < 2) person.name.first = '';
659
+ if (person.name.last.length < 2) person.name.last = '';
660
+
661
+ this.search({
662
+ emails: emails,
663
+ phones: phones,
664
+ name: person.name
665
+ }, options, function(err, people) {
666
+
667
+ if (err) return cb(err, null);
668
+ if (!people) return cb(null, null);
669
+
670
+ // exclude passed in person from results
671
+ people = _.reject(people, function(p) {
672
+ return p.id == person.id;
673
+ });
674
+
675
+ return cb(null, people);
676
+
677
+ });
678
+
679
+ };
680
+
681
+ Person.statics.stats = function (cb) {
682
+ this.collection.stats(cb);
683
+ };
684
+
685
+ Person.statics.getById = function (id, cb) {
686
+
687
+ this
688
+ .findById(id)
689
+ .populate('account')
690
+ .populate('chairs.first.company')
691
+ .populate('chairs.second.company')
692
+ .populate('notes')
693
+ .populate('related')
694
+ .populate('sources.person')
695
+ .exec(function (err, person) {
696
+
697
+ if (err) return cb(err, null);
698
+ else if (!person) return cb(null, null);
699
+
700
+ person.sources = _.sortBy(person.sources, function(source) {
701
+ return source.interactions.calendar + source.interactions.email;
702
+ }).reverse();
703
+
704
+ return cb(null, person);
705
+
706
+ });
707
+
708
+ };
709
+
710
+ Person.statics.getByIdCustomer = function (id, customer, cb) {
711
+
712
+ // this would speed it up
713
+ // still need a way to optionally return all though
714
+ //.findById(id, { 'calendarEventSummaries': { $slice: 5 } })
715
+
716
+ this
717
+ .findById(id)
718
+ .select('-previous')
719
+ .populate('calendarEventSummaries.attendees.external', 'avatarUrl name title')
720
+ .populate('calendarEventSummaries.attendees.internal', 'avatarUrl name title')
721
+ .populate('calendarEventSummaries.notes')
722
+ .populate('chairs.first.company', 'logoUrl name')
723
+ .populate('chairs.second.company', 'logoUrl name')
724
+ .populate('sources.person', 'avatarUrl name title')
725
+ .deepPopulate([
726
+ 'notes.createdBy',
727
+ 'calendarEventSummaries.notes.createdBy'
728
+ ], {
729
+ populate: {
730
+ 'notes.createdBy': {select: 'avatarUrl name'},
731
+ 'calendarEventSummaries.notes.createdBy': { select: 'avatarUrl name' }
732
+ }
733
+ })
734
+ .exec(function (err, person) {
735
+
736
+ if (err) return cb(err, null);
737
+ else if (!person) return cb(null, null);
738
+
739
+ person.sources = _.sortBy(person.sources, function(source) {
740
+ return source.interactions.calendar + source.interactions.email;
741
+ }).reverse();
742
+
743
+ // note this gets filtered by customer in post init hook
744
+ // sort descending, i.e., reverse chronologically
745
+ person.calendarEventSummaries = _.sortBy(person.calendarEventSummaries, function(summary) {
746
+ return summary.startTime;
747
+ }).reverse();
748
+
749
+ return cb(null, person);
750
+
751
+ });
752
+
753
+ };
754
+
755
+ Person.statics.getByIdNoPopulate = function (id, cb) {
756
+
757
+ this
758
+ .findById(id)
759
+ .exec(cb);
760
+
761
+ };
762
+
763
+ Person.statics.getByIds = function (ids, cb) {
764
+ this
765
+ .find({ '_id': { $in : ids }, 'deleted': {$ne: true} })
766
+ .exec(cb);
767
+ };
768
+
769
+ // Retrieve any person matching an email in the emails array parameter
770
+ Person.statics.findByEmails = function (emails, cb) {
771
+
772
+ // sort is done post retrieval because query sort is done pre-decryption
773
+
774
+ this
775
+ .find({ 'contact.email.email': { $in : emails }, 'deleted': {$ne: true}})
776
+ .select('name avatarUrl')
777
+ .exec(function (err, people) {
778
+ cb(err, _.sortBy(people, function(person) { return person.name.full; }));
779
+ });
780
+
781
+ };
782
+
783
+ // Retrieve any person matching an email in the emails array parameter
784
+ Person.statics.findByEmailsSkinny = function (emails, cb) {
785
+ this
786
+ .find({ 'contact.email.email': { $in : emails }, 'deleted': {$ne: true}})
787
+ .exec(cb);
788
+ };
789
+
790
+ // TODO - make dynamic
791
+ Person.statics.getPartners = function (cb) {
792
+
793
+ var partnerEmails = [
794
+ 'john@ffvc.com',
795
+ 'adam@ffvc.com',
796
+ 'alex@ffvc.com',
797
+ 'david@ffvc.com',
798
+ 'michael@ffvc.com',
799
+ 'ryan@ffvc.com',
800
+ 'herb@ffvc.com',
801
+ 'paul@ffvc.com'
802
+ ];
803
+
804
+ this
805
+ .find({ 'contact.email.email': { $in: partnerEmails }, 'deleted': { $ne: true } } )
806
+ .populate('account boards.company chairs.first.company chairs.second.company')
807
+ .exec(cb);
808
+
809
+ };
810
+
811
+ Person.statics.findByProviderEventId = function (id, cb) {
812
+ this
813
+ .find({ 'calendarEventSummaries.providerEventId': id })
814
+ .exec(cb);
815
+ };
816
+
817
+ Person.statics.findBySlug = function (slug, cb) {
818
+ this
819
+ .find({ slug: new RegExp(slug, 'i'), 'deleted': {$ne: true} })
820
+ .exec(cb);
821
+ };
822
+
823
+ Person.statics.findBySlugs = function (slugs, cb) {
824
+ this
825
+ .find({ 'slug': { $in : slugs }, 'deleted': {$ne: true} })
826
+ .exec(cb);
827
+ };
828
+
829
+ Person.statics.findByUniqueInfo = function (email, cb) {
830
+
831
+ // useful for not inserting dupes
832
+ // todo: expand this to check other fields
833
+
834
+ this.findOne({
835
+ $or: [
836
+ { 'contact.email': { $elemMatch: {'email': crypto.encrypt(email)} }},
837
+ { 'contact.email': { $elemMatch: {'email': email} }}
838
+ ]})
839
+ .populate('account boards.company leadership.company chairs.first.company chairs.second.company')
840
+ .exec(cb);
841
+
842
+ };
843
+
844
+ Person.statics.listAllPeople = function (cb) {
845
+
846
+ // sort is done post retrieval because query sort is done pre-decryption
847
+
848
+ var query = this.find({'deleted': {$ne: true}});
849
+ query.populate('account chairs.first.company chairs.second.company');
850
+ query.exec(function (err, people) {
851
+ cb(err, _.sortBy(people, function(person) { return person.name.full; }));
852
+ });
853
+
854
+ };
855
+
856
+ Person.statics.listMostRecentPeople = function (cb) {
857
+
858
+ // sort is done post retrieval because query sort is done pre-decryption
859
+
860
+ this
861
+ .find({'deleted': {$ne: true}})
862
+ .sort({'_id': 1})
863
+ .limit(100)
864
+ .exec(function (err, people) {
865
+ cb(err, _.sortBy(people, function(person) { return person.name.full; }));
866
+ });
867
+
868
+ };
869
+
870
+ Person.statics.listPage = function (skip, cb) {
871
+
872
+ this
873
+ .find({'deleted': {$ne: true}})
874
+ .sort({'name.first':1, 'name.last':1})
875
+ .skip(skip)
876
+ .limit(100)
877
+ .exec(cb);
878
+
879
+ };
880
+
881
+ Person.statics.listAllPeopleNoPop = function (cb) {
882
+
883
+ // sort is done post retrieval because query sort is done pre-decryption
884
+
885
+ var query = this.find({'deleted': {$ne: true}});
886
+ query.exec(function (err, people) {
887
+ cb(err, _.sortBy(people, function(person) { return person.name.full; }));
888
+ });
889
+
890
+ };
891
+
892
+ Person.statics.listAllUnverifiedPeople = function (cb) {
893
+
894
+ this
895
+ .find({'status.verified': false, 'deleted': {$ne: true}})
896
+ .sort({'_id': 1})
897
+ .limit(100)
898
+ .exec(function (err, people) {
899
+ cb(err, _.sortBy(people, function(person) { return person.name.full; }));
900
+ });
901
+
902
+ };
903
+
904
+ Person.statics.listAllFlaggedPeople = function (cb) {
905
+
906
+ this
907
+ .find({'flagged.resolved': false, 'flagged.text': { $ne: '' }, 'deleted': {$ne: true}})
908
+ .exec(function (err, people) {
909
+ people = _.sortBy(people, function(person){
910
+ var timestamp = new Date(person.flagged.on);
911
+ timestamp = timestamp.getTime();
912
+ return timestamp;
913
+ });
914
+ people = people.reverse();
915
+
916
+ cb(err, people);
917
+ });
918
+
919
+ };
920
+
921
+ Person.statics.listAllPeopleWithoutAccounts = function (cb) {
922
+
923
+ // sort is done post retrieval because query sort is done pre-decryption
924
+
925
+ this
926
+ .find({account: {$exists: false}, 'deleted': {$ne: true}})
927
+ .exec(function (err, people) {
928
+ cb(err, _.sortBy(people, function(person) { return person.name.full; }));
929
+ });
930
+
931
+ };
932
+
933
+ Person.statics.listForSubscription = function (subscriptionId, cb) {
934
+
935
+ this
936
+ .find({ 'subscriptions.id': subscriptionId, 'deleted': {$ne: true}})
937
+ .exec(function(err, people) {
938
+ cb(err, people);
939
+ });
940
+
941
+ };
942
+
943
+ Person.statics.addChair = function(personId, chairPos, companyId, cb) {
944
+
945
+ this.findById(personId).exec(function(err, person) {
946
+
947
+ var chairObj = {
948
+ company: companyId,
949
+ start: ''
950
+ };
951
+
952
+ switch(chairPos) {
953
+ case 'first':
954
+ person.chairs.first.push(chairObj);
955
+ break;
956
+
957
+ case 'second':
958
+ person.chairs.second.push(chairObj);
959
+ break;
960
+ }
961
+
962
+ person.save(cb);
963
+
964
+ });
965
+
966
+ };
967
+
968
+ //same function for flag resolved and unresolved... all based on true|false resolved arg
969
+ Person.statics.flag = function(personId, resolved, text, username, cb) {
970
+
971
+ this.findById(personId).exec(function(err, person) {
972
+
973
+ if (!person) return cb(new Error('person not found'), null);
974
+
975
+ person.flagged.resolved = resolved;
976
+ person.flagged.text = text;
977
+ person.flagged.by = username;
978
+ person.flagged.on = Date.now();
979
+
980
+ person.save(cb);
981
+
982
+ });
983
+
984
+ };
985
+
986
+ Person.statics.setCustomerCompanyIds = function(ids) {
987
+ customerCompanyIds = ids;
988
+ };
989
+
990
+ Person.statics.upsert = function(currentPerson, username, cb) {
991
+
992
+ if (!currentPerson) { return cb(new Error('Current Person is required'), null); }
993
+ if (!username) { return cb(new Error('Username is required'), null); }
994
+
995
+ currentPerson.entered = {
996
+ by: username,
997
+ on: new Date()
998
+ };
999
+
1000
+ currentPerson.save(cb);
1001
+
1002
+ };
1003
+
1004
+ Person.statics.addNote = function(personId, creatorPersonId, text, cb) {
1005
+
1006
+ Note.createForModel({
1007
+ createdBy: creatorPersonId,
1008
+ text: text
1009
+ }, this, personId, cb);
1010
+
1011
+ };
1012
+
1013
+ Person.statics.deleteNote = function(noteId, cb) {
1014
+
1015
+ // Delete the note itself along with any references on people
1016
+
1017
+ var self = this;
1018
+
1019
+ var removePersonNoteReferences = function removePersonNoteReferences(callback) {
1020
+ self.update({}, {
1021
+ $pull: { 'notes' : [noteId] }
1022
+ }, callback);
1023
+ };
1024
+
1025
+ var removeEventNoteReferences = function removeEventNoteReferences(callback) {
1026
+ self.update({
1027
+ 'calendarEventSummaries.notes': noteId
1028
+ }, {
1029
+ $pull: { 'calendarEventSummaries.$.notes' : [noteId] }
1030
+ }, callback);
1031
+ };
1032
+
1033
+ async.series([
1034
+ Note.delete.bind(Note, noteId),
1035
+ removePersonNoteReferences,
1036
+ removeEventNoteReferences
1037
+ ], function(err, results) {
1038
+ return cb(err, null);
1039
+ });
1040
+
1041
+ };
1042
+
1043
+ ///////////////
1044
+
1045
+ // Person.pre('save', function(next) {
1046
+ // var self = this;
1047
+ // var PersonHistory = mongoose.model('PersonHistory');
1048
+ // var snapshot = new PersonHistory();
1049
+ // snapshot.create(self, 'jason', function(err, result) {
1050
+ // if (err) {} // todo: alert admin to fix; ok to proceed;
1051
+ // return next();
1052
+ // });
1053
+ // });
1054
+
1055
+ Person.post('remove', function(doc) {
1056
+ // Account.person
1057
+ // Investor.people
1058
+ // Investor.formerPeople
1059
+ // LimitedPartner.person
1060
+ // Organization.people.person
1061
+ // Person.merged
1062
+ // Person.related
1063
+ // Person.calendarEventSummaries.attendees.internal
1064
+ // Person.calendarEventSummaries.attendees.external
1065
+ // PortfolioCompany.chairs.first
1066
+ // PortfolioCompany.chairs.second
1067
+ // PortfolioCompany.boards
1068
+ // PortfolioCompany.leadership.person
1069
+ // PortfolioCompany.people.person
1070
+ // PortfolioCompany.formerPeople
1071
+ });
1072
+
1073
+ var protectCustomerData = function(context) {
1074
+
1075
+ var CUSTOMER_ID = config.CUSTOMER_ID;
1076
+
1077
+ var protectCalendarEventData = function protectCalendarEventData(person) {
1078
+
1079
+ if (CUSTOMER_ID == 'GLOBAL_PROCESS') return;
1080
+
1081
+ person.calendarEventSummaries = _.reject(person.calendarEventSummaries, function(event) {
1082
+ return event.totemCustomerId != CUSTOMER_ID;
1083
+ });
1084
+
1085
+ };
1086
+
1087
+ var protectSources = function protectSources(person) {
1088
+
1089
+ if (CUSTOMER_ID == 'GLOBAL_PROCESS') return;
1090
+
1091
+ person.sources = _.reject(person.sources, function(source) {
1092
+ var customerId = mongoose.Types.ObjectId.isValid(source.customer) ? source.customer : source.customer._id;
1093
+ return customerId.toString() != CUSTOMER_ID.toString();
1094
+ });
1095
+
1096
+ };
1097
+
1098
+ protectCalendarEventData(context);
1099
+ protectSources(context);
1100
+
1101
+ };
1102
+
1103
+ Person.pre('save', function(next) {
1104
+
1105
+ var self = this;
1106
+
1107
+ // Reset counters so we can calc from scratch
1108
+ _.each(self.sources, function(source) {
1109
+ source.interactions.calendar = 0;
1110
+ });
1111
+
1112
+ _.each(self.calendarEventSummaries, function(ces) {
1113
+
1114
+ var customerId = ces.totemCustomerId;
1115
+
1116
+ // Each attendee from the customer is a source
1117
+ _.each(ces.attendees.internal, function(attendee) {
1118
+
1119
+ var aid = mongoose.Types.ObjectId.isValid(attendee) ? attendee : attendee._id;
1120
+
1121
+ // Check if this attendee has already been added as a source
1122
+ var match = _.find(self.sources, function(source) {
1123
+ // It's a match if both person and customer ids are a match
1124
+ var pid = mongoose.Types.ObjectId.isValid(source.person) ? source.person : source.person._id;
1125
+ var cid = mongoose.Types.ObjectId.isValid(source.customer) ? source.customer : source.customer._id;
1126
+ return pid.toString() == aid.toString() && cid.toString() == customerId.toString();
1127
+ });
1128
+
1129
+ // If source already added, increment counter
1130
+ if (match) { match.interactions.calendar += 1; }
1131
+
1132
+ // Otherwise add the source
1133
+ else {
1134
+ self.sources.push({
1135
+ person: aid,
1136
+ customer: customerId,
1137
+ interactions: { calendar: 1 }
1138
+ });
1139
+ }
1140
+
1141
+ });
1142
+
1143
+ });
1144
+
1145
+ next();
1146
+
1147
+ });
1148
+
1149
+ Person.post('init', function() {
1150
+ protectCustomerData(this);
1151
+ });
1152
+
1153
+ var deepPopulate = require('mongoose-deep-populate')(mongoose);
1154
+ Person.plugin(deepPopulate);
1155
+
1156
+ Person.set('autoIndex', true);
1157
+ Person.on('index', function(err) { console.log('error building person indexes: ' + err); });
1158
+
1159
+ Person.set('toJSON', { virtuals: true });
1160
+
1161
+ mongoose.model('Person', Person);
1162
+
1163
+ };