@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.
@@ -0,0 +1,1011 @@
1
+ "use strict";
2
+
3
+ module.exports = function(mongoose, config) {
4
+
5
+ var LPS_ENABLED = config.LPS_ENABLED || false;
6
+
7
+ var
8
+
9
+ _ = require('underscore'),
10
+ async = require('async'),
11
+ utils = require('@dhyasama/ffvc-utilities'),
12
+ Schema = mongoose.Schema,
13
+ Note = mongoose.model('Note'),
14
+
15
+ Organization = new Schema({
16
+
17
+ name: { type: String, required: true, index: true },
18
+
19
+ slug: { type: String, required: true, unique: true, sparse: false, trim: true, lowercase: true },
20
+
21
+ logoUrl: { type: String, trim: true },
22
+
23
+ description: { type: String, trim: true },
24
+
25
+ website: { type: String, unique: true, sparse: true, required: false, trim: true, lowercase: true },
26
+
27
+ websiteAliases: [ { type: String, unique: true, sparse: true, trim: true, lowercase: true } ],
28
+
29
+ social: {
30
+ twitter: { type: String, trim: true, unique: true, sparse: true },
31
+ facebook: { type: String, trim: true, unique: true, sparse: true },
32
+ linkedin: { type: String, trim: true, unique: true, sparse: true }
33
+ },
34
+
35
+ contact: {
36
+
37
+ phone: [{
38
+ type: { type: String, default: null, trim: true },
39
+ number: { type: String, default: null, trim: true },
40
+ primary: { type: Boolean, default: false }
41
+ }],
42
+
43
+ email: [{
44
+ type: { type: String, default: null, trim: true },
45
+ email: { type: String, default: null, trim: true, lowercase: true },
46
+ primary: { type: Boolean, default: false }
47
+ }],
48
+
49
+ address: [{
50
+ type: { type: String, default: '', trim: true },
51
+ street: { type: String, default: '', trim: true },
52
+ suite: { type: String, default: '', trim: true },
53
+ city: { type: String, default: '', trim: true },
54
+ state: { type: String, default: '', trim: true },
55
+ country: { type: String, default: '', trim: true },
56
+ postalCode: { type: String, default: '', trim: true },
57
+ primary: { type: Boolean, default: false }
58
+ }]
59
+
60
+ },
61
+
62
+ people: [{
63
+ person: { type: Schema.ObjectId, ref: 'Person', required: true, index: true },
64
+ title: { type: String, default: null, trim: true },
65
+ current: { type: Boolean, default: false, index: true },
66
+ leader: { type: Boolean, default: false, index: true },
67
+ board: { type: String, enum: ['chairman', 'director', 'observer', 'none'], default: 'none', index: true }
68
+ }],
69
+
70
+ entered: {
71
+ by: { type: String, default: '', trim: true },
72
+ on: { type: Date, default: Date.now }
73
+ },
74
+
75
+ flagged: {
76
+ by: { type: String, default: '', trim: true },
77
+ on: { type: Date, default: Date.now },
78
+ text: { type: String, default: '', trim: true },
79
+ resolved: { type: Boolean, default: false }
80
+ },
81
+
82
+ merged: [{ type: Schema.ObjectId, ref: 'Organization' }],
83
+
84
+ deleted: { type: Boolean, default: false },
85
+
86
+ aliases: [ { type: String, trim: true } ],
87
+
88
+ foundedOn: { type: Date, default: null },
89
+ closedOn: { type: Date, default: null },
90
+
91
+ exit: {
92
+
93
+ acquisition: {
94
+ by: { type: Schema.ObjectId, ref: 'Organization' },
95
+ year: { type: Date, default: null },
96
+ amount: { type: Number, default: 0 }
97
+ },
98
+
99
+ ipo: {
100
+ ticker: {type: String, default: null, trim: true},
101
+ year: {type: Date, default: null}
102
+ }
103
+
104
+ },
105
+
106
+ filters: [{
107
+ group: { type: String, trim: true },
108
+ items: [{ type: String, trim: true }],
109
+ customer: { type: Schema.ObjectId, ref: 'Organization' }
110
+ }],
111
+
112
+ crunchbase: {
113
+ uuid: { type: String, unique: true, sparse: true, trim: true },
114
+ url: { type: String, unique: true, sparse: true, trim: true }
115
+ },
116
+
117
+ eshares: {
118
+ companyid: { type: String, unique: true, sparse: true, trim: true }
119
+ },
120
+
121
+ related: [{ type: Schema.ObjectId, ref: 'Organization' }],
122
+
123
+ // customer specific (customer id is stored on note)
124
+ notes: [ { type: Schema.ObjectId, ref: 'Note' } ],
125
+
126
+ customer: {
127
+
128
+ active: { type: Boolean, default: false },
129
+
130
+ totemUrl: { type: String, trim: true, unique: true, sparse: true },
131
+
132
+ style: {
133
+ backgroundColor: { type: String, default: '#666666', trim: true },
134
+ textColor: { type: String, default: '#ffffff', trim: true }
135
+ },
136
+
137
+ investmentsSheet: {
138
+ sheet: { type: String, trim: true, unique: true, sparse: true },
139
+ googleSheetCreds: {
140
+ private_key_id: { type: String, trim: true, unique: true, sparse: true },
141
+ private_key: { type: String, trim: true, unique: true, sparse: true },
142
+ client_email: { type: String, trim: true, unique: true, sparse: true },
143
+ client_id: { type: String, trim: true, unique: true, sparse: true },
144
+ type: { type: String, trim: true }
145
+ }
146
+ },
147
+
148
+ eshares: {
149
+ firmid: { type: String, unique: true, sparse: true, trim: true }
150
+ }
151
+
152
+ },
153
+
154
+ docs: [{
155
+ customer: { type: Schema.ObjectId, ref: 'Organization' },
156
+ url: { type: String, trim: true, unique: true, sparse: true }
157
+ }],
158
+
159
+ // Link to deeper data on iLevel
160
+ // iLevelUser must be true at the account level
161
+ iLevel: [{
162
+ customer: { type: Schema.ObjectId, ref: 'Organization' },
163
+ url: { type: String, trim: true, unique: true, sparse: true }
164
+ }],
165
+
166
+ chairs: [{
167
+ customer: { type: Schema.ObjectId, ref: 'Organization' },
168
+ first: { type: Schema.ObjectId, ref: 'Person' },
169
+ second: { type: Schema.ObjectId, ref: 'Person' }
170
+ }],
171
+
172
+ // Funds belonging to this organization (which is typically, but not necessarily, what we'd call an investor)
173
+ // An example of a non-"investor" with funds would be Slack with it's Slack Fund
174
+ // Another way to put it, these are funds from which this organization makes investments
175
+ funds: [{ type: Schema.ObjectId, ref: 'Fund' }],
176
+
177
+ // Private
178
+ capTable: [{
179
+
180
+ customer: { type: Schema.ObjectId, ref: 'Organization', required: true },
181
+ shares: { type: Number, default: 0 },
182
+ fullyDiluted: { type: Number, default: 0 },
183
+ asOfDate: { type: Date },
184
+
185
+ // todo - ???
186
+ optionsAndWarrants: {
187
+ unissued: { type: Number },
188
+ issuedToUnspecified: { type: Number }
189
+ }
190
+
191
+ }],
192
+
193
+ // Equity or debt this organization has taken from other organizations
194
+ rounds: [{
195
+
196
+ roundName: { type: String, trim: true, default: null },
197
+
198
+ // used when cap table round names are changed and need to be matched to investment data
199
+ oldRoundName: { type: String, trim: true, default: null },
200
+
201
+ // these three values are public and will be used in the absence of overriding private data
202
+ preMoneyValuation: { type: Number, default: 0 },
203
+ amountRaised: { type: Number, default: 0 },
204
+ closingDate: { type: Date, default: null },
205
+
206
+ // Funds that participated in this round
207
+ // Recall that a fund belongs to an organization
208
+ // This is a public list of funds/orgs that invested
209
+ funds: [{
210
+
211
+ fund: { type: Schema.ObjectId, ref: 'Fund', required: true },
212
+
213
+ // the lead on this round from the fund/org
214
+ partner: { type: Schema.ObjectId, ref: 'Person', required: false },
215
+
216
+ // not strictly necessary since we have fund
217
+ // include because it makes filtering investments data, which is private, much easier
218
+ customer: { type: Schema.ObjectId, ref: 'Organization', required: false },
219
+
220
+ // Investments in this round from this fund
221
+ // this is all private data
222
+ investments: [{
223
+
224
+ amount: { type: Number, default: 0 },
225
+ date: { type: Date, default: null },
226
+ lead: { type: Boolean, default: false },
227
+ participatingPreferred: { type: Boolean, default: false },
228
+ pricePerShare: { type: Number, default: 0 },
229
+ fairMarketValue: { type: Number, default: 0 },
230
+ notes: { type: String, trim: true, default: null },
231
+
232
+ // note these fields are duplicative at the round level
233
+ // this is private data and will override the round level numbers where appropriate
234
+ preMoneyValuation: { type: Number, default: 0 },
235
+ amountRaised: { type: Number, default: 0 },
236
+ closingDate: { type: Date, default: null },
237
+
238
+ convertibleNotes: [{
239
+ cap: { type: Number, default: 0 },
240
+ discount: { type: Number, default: 0 },
241
+ interestRate: { type: Number, default: 0 },
242
+ maturityDate: { type: Date, default: null },
243
+ fairMarketValue: { type: Number, default: 0 },
244
+ notes: { type: String, trim: true, default: null }
245
+ }]
246
+
247
+ }]
248
+
249
+ }],
250
+
251
+ // People that participated in this round
252
+ // This is a public list of people that invested
253
+ people: [{
254
+ person: {type: Schema.ObjectId, ref: 'Person'}
255
+ }]
256
+
257
+ }],
258
+
259
+ });
260
+
261
+ ///////////////////////////////
262
+
263
+ Organization.virtual('status').get(function () {
264
+
265
+ var self = this;
266
+
267
+ // infer from data
268
+
269
+ if (self.closedOn) return 'Dead';
270
+ else if (self.ipo.year) return 'IPO';
271
+ else if (self.acquisition.year) return 'Acquired';
272
+ else return 'Active';
273
+
274
+ });
275
+
276
+ Organization.virtual('address.primary').get(function () {
277
+ var primary = _.find(this.contact.address, function(address) {
278
+ return address.primary;
279
+ });
280
+ return primary ? primary.address : null;
281
+ });
282
+
283
+ Organization.virtual('sources').get(function () {
284
+
285
+ // Aggregate sources from all people at org
286
+ // Sort by interactions descending, full name ascending
287
+
288
+ var self = this;
289
+
290
+ var result = _.compact(_.pluck(self.people, 'person'));
291
+ if (result.length == 0) return [];
292
+
293
+ result = _.compact(_.pluck(result, 'sources'));
294
+ if (result.length == 0) return [];
295
+
296
+ result = _.flatten(result);
297
+ result = _.compact(result);
298
+
299
+ if (result.length == 0) return [];
300
+ if (mongoose.Types.ObjectId.isValid(result[0].person)) return []; // not populated
301
+
302
+ result = _.uniq(result, function(item) { return item.person._id.toString(); });
303
+ result = _.sortBy(result, 'interactions.count');
304
+ result.reverse();
305
+ result = _.sortBy(result, 'name.full');
306
+
307
+ return result;
308
+
309
+ });
310
+
311
+ ///////////////////////////////
312
+
313
+ Organization.methods.merge = function(id) {
314
+ if (this.merged.indexOf(id) == -1) {
315
+ this.merged.push(id);
316
+ return true;
317
+ }
318
+ else {
319
+ return false;
320
+ }
321
+ };
322
+
323
+ Organization.methods.deleteOrg = function() {
324
+ // Soft delete
325
+ this.deleted = true;
326
+ };
327
+
328
+ Organization.methods.relateOrg = function(id) {
329
+
330
+ var self = this;
331
+
332
+ var match = _.find(self.related, function(org) {
333
+ var orgId = utils.isValidObjectId(org) ? org : org.id;
334
+ return orgId.toString() == id.toString();
335
+ });
336
+
337
+ if (!match) { self.related.push(id); }
338
+
339
+ };
340
+
341
+ // todo - what are "basic" fields and how are they used?
342
+ Organization.methods.newBasic = function(field, value) {
343
+
344
+ var compoundField = false;
345
+ if(field.split(".").length > 1) {
346
+ compoundField = true;
347
+ }
348
+
349
+ if(!compoundField) {
350
+ this[field] = value;
351
+ } else {
352
+ var parentField = field.split('.')[0];
353
+ var childField = field.split('.')[1];
354
+
355
+ this[parentField][childField] = value;
356
+ }
357
+
358
+ };
359
+
360
+ Organization.methods.updateBasic = function(field, value) {
361
+
362
+ var compoundField = false;
363
+ if(field.split(".").length > 1) {
364
+ compoundField = true;
365
+ }
366
+
367
+ if(!compoundField) {
368
+ var currentVal = this[field];
369
+ this[field] = value;
370
+ } else {
371
+ var parentField = field.split('.')[0];
372
+ var childField = field.split('.')[1];
373
+
374
+ var currentVal = this[parentField][childField];
375
+ this[parentField][childField] = value;
376
+ }
377
+
378
+ };
379
+
380
+ Organization.methods.updateContact = function(field, value) {
381
+
382
+ var parentField = field.split('.')[0];
383
+ var childField = field.split('.')[1];
384
+
385
+ this[parentField][childField] = value;
386
+
387
+ };
388
+
389
+ Organization.methods.addPerson = function(personToAdd) {
390
+
391
+ var self = this;
392
+
393
+ // Some simple validation
394
+ if (!personToAdd) throw new Error('Must supply person to add');
395
+ if (!personToAdd.person) throw new Error('Person to add must have person reference');
396
+ if (!utils.isValidObjectId(personToAdd.person)) throw new Error('Person reference must be a valid objectid');
397
+
398
+ // Check if person is already added
399
+ var match = _.find(self.people, function(existingPerson) {
400
+ var id = utils.isValidObjectId(existingPerson.person) ? existingPerson.person : existingPerson.person.id;
401
+ return id.toString() == personToAdd.person.toString();
402
+ });
403
+
404
+ // If not, add it
405
+ if (!match) self.people.push(personToAdd);
406
+
407
+ };
408
+
409
+ Organization.methods.removePerson = function(personIdToRemove) {
410
+
411
+ this.people = _.reject(this.people, function(existingPerson) {
412
+ var id = utils.isValidObjectId(existingPerson.person) ? existingPerson.person : existingPerson.person._id;
413
+ return id.toString() == personIdToRemove.toString();
414
+ });
415
+
416
+ };
417
+
418
+ Organization.methods.updateFunds = function(funds) {
419
+
420
+ var self = this;
421
+
422
+ // todo - ??? - funds invested into this org or funds this org invests from?
423
+
424
+ if (!self.portfolioCompany) return;
425
+
426
+ // wipe funds and start fresh
427
+ self.portfolioCompany.fundsInvested = funds;
428
+
429
+ // Checking if it's a valid object id is a shortcut
430
+ // TODO: The alternative is to get a list of funds and verify against that
431
+ // TODO: also would be good to check for dupes
432
+ // This is used internally, in admin, so let's punt for now
433
+
434
+ //_.each(fundIds, function(fundId) {
435
+ // if (utilities.isValidObjectId(fundId.id)) {
436
+ // self.fundsInvested.push(fundId);
437
+ // };
438
+ //});
439
+
440
+ };
441
+
442
+ Organization.methods.updateChair = function(chairObj) {
443
+
444
+ var self = this;
445
+
446
+ if (!self.portfolioCompany) return;
447
+
448
+ // todo - needs customer id
449
+
450
+ switch(chairObj.num) {
451
+ case 'chair1':
452
+ if(chairObj.val && chairObj.val != "") {
453
+ self.portfolioCompany.chairs.first = chairObj.val;
454
+ } else {
455
+ self.portfolioCompany.chairs.first = {};
456
+ }
457
+ break;
458
+
459
+ case 'chair2':
460
+ if(chairObj.val && chairObj.val != "") {
461
+ self.portfolioCompany.chairs.second = chairObj.val;
462
+ } else {
463
+ self.portfolioCompany.chairs.second = {}
464
+ }
465
+ break;
466
+ }
467
+
468
+ };
469
+
470
+ // todo - change this to exit (with acquisition or ipo)
471
+ Organization.methods.updateAcquisition = function(aObj) {
472
+
473
+ var self = this;
474
+
475
+ if (!self.portfolioCompany) return;
476
+
477
+ self.portfolioCompany.acquisition = aObj;
478
+
479
+ };
480
+
481
+ Organization.methods.removeInvestmentsAndDebtsByCustomer = function(investor) {
482
+
483
+ var self = this;
484
+
485
+ // todo - change to round.funds and capTable.customer
486
+
487
+ if (!self.portfolioCompany) return;
488
+
489
+ if(self.portfolioCompany.investments.length > 0) {
490
+ self.portfolioCompany.investments = _.reject(self.portfolioCompany.investments, function(i) {
491
+ return String(i.source) == String(investor._id);
492
+ });
493
+ }
494
+
495
+ if(self.portfolioCompany.debts.length > 0) {
496
+ self.portfolioCompany.debts = _.reject(self.portfolioCompany.debts, function(d) {
497
+ return String(d.source) == String(investor._id);
498
+ });
499
+ }
500
+
501
+ if(self.portfolioCompany.fundsInvested.length > 0) {
502
+ self.portfolioCompany.fundsInvested = _.reject(self.portfolioCompany.fundsInvested, function(f) {
503
+ return String(f.investor._id || f.investor) == String(investor._id);
504
+ });
505
+ }
506
+
507
+ };
508
+
509
+ Organization.methods.addCompanyToInvestor = function(idOfCompanyToAdd) {
510
+
511
+ var self = this;
512
+ if (!self.investor) return;
513
+
514
+ var match = _.find(self.investor.companies, function(company) {
515
+ var companyId = utils.isValidObjectId(company) ? company : company.id;
516
+ return companyId.toString() == idOfCompanyToAdd.toString();
517
+ });
518
+
519
+ if (!match) self.investor.companies.push(idOfCompanyToAdd);
520
+
521
+ };
522
+
523
+ Organization.methods.addFundToInvestor = function(idOfFundToAdd) {
524
+
525
+ var self = this;
526
+ if (!self.investor) return;
527
+
528
+ var match = _.find(self.investor.funds, function(fund) {
529
+ var fundId = utils.isValidObjectId(fund) ? fund : fund.id;
530
+ return fundId.toString() == idOfFundToAdd.toString();
531
+ });
532
+
533
+ if (!match) self.investor.funds.push(idOfFundToAdd);
534
+
535
+ };
536
+
537
+ ///////////////////////
538
+
539
+ // search for orgs based on fields provided
540
+ // data format, each field is optional
541
+ // {
542
+ // domain: ''
543
+ // name: ''
544
+ // }
545
+ // options:
546
+ // fuzzy: searches within strings, e.g., searching for "venture" will match "ff Venture Capital"
547
+ // non-fuzzy does a full-string match, e.g., "ffvc" will match "ffvc" but "ff" will not
548
+ // both fuzzy and non are case-insensitive
549
+ // queryFirstWord: tokenizes first word and searches for it
550
+ // removeRelatedFromTopLevel: related people are removed from top-level results
551
+ Organization.statics.search = function search(data, options, cb) {
552
+
553
+ // validate input
554
+ if (!cb) throw new Error('cb is required');
555
+ if (!data || (!data.name && !data.domain)) return cb(null, []);
556
+
557
+ var defaultOptions = {
558
+ fuzzy: true,
559
+ queryFirstWord: false,
560
+ removeRelatedFromTopLevel: true
561
+ };
562
+
563
+ // combine provided and default options
564
+ options = utils.setDefaults(options, defaultOptions);
565
+
566
+ // need to be admin or provide investor id
567
+ if (!options.admin && !options.investor) return cb(null, []);
568
+
569
+ data.name = data.name || '';
570
+ data.domain = data.domain || '';
571
+
572
+ var removeRelatedFromTopLevel = function(items) {
573
+
574
+ // All entities are searched initially, including entities that are listed as related to other entities
575
+ // Remove related entities from top-level results so they only come up as related
576
+
577
+ var getRelatedIds = function getRelatedIds(items) {
578
+
579
+ var result = [];
580
+
581
+ _.each(items, function(item) {
582
+ if (item.related && item.related.length >= 1) {
583
+ var ids = _.pluck(item.related, 'id');
584
+ result = result.concat(ids);
585
+ }
586
+ });
587
+
588
+ return _.uniq(result);
589
+
590
+ };
591
+
592
+ var relatedIds = getRelatedIds(items);
593
+ items = _.reject(items, function(item) {
594
+ return relatedIds.indexOf(item.id) >= 0;
595
+ });
596
+
597
+ return items;
598
+
599
+ };
600
+
601
+ // use first word in org name to look for fuzzy matches which is a simple
602
+ // but effective technique as the first word is commonly the most important
603
+ var tokenizeName = function tokenizeName(name, query) {
604
+
605
+ if (!name) throw new Error('name is required');
606
+ if (!query) throw new Error('query is required');
607
+
608
+ var parts = [];
609
+
610
+ try { parts = name.toString().split(' '); }
611
+ catch (err) { throw err; }
612
+
613
+ // fuzzy search first word if more than one word in phrase
614
+ if (parts.length >= 2) {
615
+ query['$or'].push({ 'name': new RegExp(parts[0], 'i') });
616
+ }
617
+
618
+ };
619
+
620
+ var buildQuery = function buildQuery(data, options) {
621
+
622
+ // prepare query
623
+ var query = {};
624
+ query['$or'] = []; // these will be added dynamically from data parameter
625
+ query['$and'] = []; // these will be added dynamically from data parameter
626
+ query['deleted'] = { $ne: true }; // always include this
627
+
628
+ // conditionally include name
629
+ if (data.name) {
630
+
631
+ // an exact match is always a good thing
632
+ query['$or'].push({ 'name': new RegExp('^' + data.name + '$', 'i') });
633
+
634
+ if (options.fuzzy) {
635
+ // fuzzy search full phrase
636
+ query['$or'].push({'name': new RegExp(data.name, 'i')});
637
+ }
638
+
639
+ if (options.queryFirstWord) {
640
+ // fuzzy search first word
641
+ tokenizeName(data.name, query);
642
+ }
643
+
644
+ var aliasRegExp = options.fuzzy ? new RegExp(data.name, 'i') : new RegExp('^' + data.name + '$', 'i');
645
+ query['$or'].push({ 'aliases': aliasRegExp });
646
+
647
+ }
648
+
649
+ // conditionally include domain
650
+ if (data.domain) {
651
+ var domainRegExp = options.fuzzy ? new RegExp(data.domain, 'i') : new RegExp('^' + data.domain + '$', 'i');
652
+ query['$or'].push({ 'website': domainRegExp });
653
+ }
654
+
655
+ // remove if unused
656
+ if (query['$or'].length == 0) delete query['$or'];
657
+ if (query['$and'].length == 0) delete query['$and'];
658
+
659
+ return query;
660
+
661
+ };
662
+
663
+ var query = buildQuery(data, options);
664
+
665
+ this
666
+ .find(query)
667
+ .populate('merged')
668
+ .select('logoUrl name description related')
669
+ .exec(function(err, orgs) {
670
+
671
+ if (err) return cb(err, null);
672
+ else if (!orgs) return cb(null, null);
673
+ else {
674
+
675
+ // remove related from top level
676
+ if (options.removeRelatedFromTopLevel) orgs = removeRelatedFromTopLevel(orgs);
677
+
678
+ // sort by name
679
+ orgs = _.sortBy(orgs, function(orgs) { return orgs.name; });
680
+
681
+ return cb(null, orgs);
682
+
683
+ }
684
+
685
+ });
686
+
687
+ };
688
+
689
+ Organization.statics.findDupes = function findDupes(org, options, cb) {
690
+
691
+ if (!cb) throw new Error('cb is required');
692
+ if (!org || (!org.name && !org.website)) return cb(null, []);
693
+
694
+ this.search({
695
+ name: org.name,
696
+ domain: utils.getDomain(org.website)
697
+ }, options, function(err, orgs) {
698
+
699
+ if (err) return cb(err, null);
700
+ if (!orgs) return cb(null, null);
701
+
702
+ // exclude passed in org from results
703
+ orgs = _.reject(orgs, function(o) {
704
+ return o.id == org.id;
705
+ });
706
+
707
+ return cb(null, orgs);
708
+
709
+ });
710
+
711
+ };
712
+
713
+ Organization.statics.stats = function stats(cb) {
714
+ this.collection.stats(cb);
715
+ };
716
+
717
+ Organization.statics.findByPerson = function findByPerson(personId, cb) {
718
+ this.find({ 'people.person': personId, 'deleted': {$ne: true} }).exec(cb);
719
+ };
720
+
721
+ Organization.statics.findBySlug = function findBySlug(slug, cb) {
722
+ this.find({ slug: slug, 'deleted': {$ne: true} }).exec(cb);
723
+ };
724
+
725
+ Organization.statics.findBySlugs = function findBySlugs(slugs, cb) {
726
+ this.find({ 'slug': { $in : slugs }, 'deleted': {$ne: true} }).exec(cb);
727
+ };
728
+
729
+ Organization.statics.findByDomains = function findByDomains(domains, cb) {
730
+ var query = this.find({ $or: [ {'website': { $in : domains }}, {'websiteAliases': { $in : domains }} ], 'deleted': {$ne: true} });
731
+ query.exec(cb);
732
+ };
733
+
734
+ Organization.statics.findByIds = function findByIds(ids, cb) {
735
+
736
+ var self = this;
737
+
738
+ self
739
+ .find({ '_id': { $in : ids }})
740
+ .populate('people.person', 'name avatarUrl title calendarEventSummaries sources')
741
+ .populate('related', 'name logoUrl')
742
+ .populate('notes')
743
+ .populate('chairs.first', 'name avatarUrl title')
744
+ .populate('chairs.second', 'name avatarUrl title')
745
+ .populate('funds', 'name hexColorCode')
746
+ .populate('rounds.funds.fund', 'name hexColorCode')
747
+ .populate('rounds.funds.partner', 'name avatarUrl title')
748
+ .populate('rounds.people.person', 'name avatarUrl title')
749
+ .deepPopulate([
750
+ 'people.person.sources.person',
751
+ 'people.person.calendarEventSummaries.attendees.internal',
752
+ 'people.person.calendarEventSummaries.attendees.external',
753
+ 'people.person.calendarEventSummaries.notes.createdBy',
754
+ 'notes.createdBy'
755
+ ], {
756
+ populate: {
757
+ 'people.person.sources.person': { select: 'name avatarUrl title' },
758
+ 'people.person.calendarEventSummaries.attendees.internal': { select: 'name avatarUrl title' },
759
+ 'people.person.calendarEventSummaries.attendees.external': { select: 'name avatarUrl title' },
760
+ 'people.person.calendarEventSummaries.notes.createdBy': { select: 'name avatarUrl title' },
761
+ 'notes.createdBy': { select: 'name avatarUrl title' }
762
+ }
763
+ })
764
+ .exec(cb);
765
+
766
+ };
767
+
768
+ Organization.statics.getById = function getById(id, cb) {
769
+
770
+ var self = this;
771
+
772
+ self
773
+ .findById(id)
774
+ .populate('people.person', 'name avatarUrl title calendarEventSummaries sources')
775
+ .populate('related', 'name logoUrl')
776
+ .populate('notes')
777
+ .populate('chairs.first', 'name avatarUrl title')
778
+ .populate('chairs.second', 'name avatarUrl title')
779
+ .populate('funds', 'name hexColorCode')
780
+ .populate('rounds.funds.fund', 'name hexColorCode')
781
+ .populate('rounds.funds.partner', 'name avatarUrl title')
782
+ .populate('rounds.people.person', 'name avatarUrl title')
783
+ .deepPopulate([
784
+ 'people.person.sources.person',
785
+ 'people.person.calendarEventSummaries.attendees.internal',
786
+ 'people.person.calendarEventSummaries.attendees.external',
787
+ 'people.person.calendarEventSummaries.notes.createdBy',
788
+ 'notes.createdBy'
789
+ ], {
790
+ populate: {
791
+ 'people.person.sources.person': { select: 'name avatarUrl title' },
792
+ 'people.person.calendarEventSummaries.attendees.internal': { select: 'name avatarUrl title' },
793
+ 'people.person.calendarEventSummaries.attendees.external': { select: 'name avatarUrl title' },
794
+ 'people.person.calendarEventSummaries.notes.createdBy': { select: 'name avatarUrl title' },
795
+ 'notes.createdBy': { select: 'name avatarUrl title' }
796
+ }
797
+ })
798
+ .exec(cb);
799
+
800
+ };
801
+
802
+ Organization.statics.getPortfolio = function getPortfolio(orgId, cb) {
803
+
804
+ var self = this;
805
+
806
+ self
807
+ .find({ 'rounds.funds.customer': orgId })
808
+ .select('name logoUrl description exit filters contact.addresses')
809
+ .sort('name')
810
+ .exec(cb);
811
+
812
+ };
813
+
814
+ Organization.statics.listPage = function (skip, cb) {
815
+
816
+ this
817
+ .find({'deleted': {$ne: true}})
818
+ .sort('name')
819
+ .skip(skip)
820
+ .limit(100)
821
+ .exec(cb)
822
+
823
+ };
824
+
825
+ Organization.statics.listAllFlagged = function (cb) {
826
+
827
+ var query = this.find({'flagged.resolved': false, 'flagged.text': { $ne: '' }, 'deleted': {$ne: true}});
828
+ query.exec(function (err, orgs) {
829
+
830
+ if (err) return cb(err, null);
831
+
832
+ orgs = _.sortBy(orgs, function(org){
833
+ var timestamp = new Date(org.flagged.on);
834
+ timestamp = timestamp.getTime();
835
+ return timestamp;
836
+ });
837
+ orgs = orgs.reverse();
838
+
839
+ return cb(null, orgs);
840
+
841
+ });
842
+
843
+ };
844
+
845
+ Organization.statics.upsert = function(org, username, cb) {
846
+
847
+ if (!org) { return cb(new Error('Organization is required'), null); }
848
+ if (!username) { return cb(new Error('Username is required'), null); }
849
+
850
+ org.entered.by = username;
851
+ org.entered.on = new Date();
852
+
853
+ org.save(cb);
854
+
855
+ };
856
+
857
+ Organization.statics.removeById = function(id, cb) {
858
+
859
+ if (!id) { return cb(new Error('id is required'), null); }
860
+ if (!cb) { throw new Error('cb is required'); }
861
+
862
+ this.remove({_id: id}, cb);
863
+
864
+ };
865
+
866
+ Organization.statics.flag = function(orgId, resolved, text, username, cb) {
867
+
868
+ this.findById(orgId).exec(function(err, org) {
869
+
870
+ org.flagged.resolved = resolved;
871
+ org.flagged.text = text;
872
+ org.flagged.by = username;
873
+ org.flagged.on = Date.now();
874
+
875
+ org.save(cb);
876
+
877
+ });
878
+
879
+ };
880
+
881
+ Organization.statics.addNote = function(organizationId, creatorPersonId, text, cb) {
882
+
883
+ Note.createForModel({
884
+ createdBy: creatorPersonId,
885
+ text: text
886
+ }, this, organizationId, cb);
887
+
888
+ };
889
+
890
+ Organization.statics.deleteNote = function(noteId, cb) {
891
+
892
+ // Delete the note itself along with any references
893
+
894
+ var self = this;
895
+
896
+ var removeReferences = function removeReferences(callback) {
897
+ self.update({}, {
898
+ $pull: { 'notes' : [noteId] }
899
+ }, callback);
900
+ };
901
+
902
+ async.series([
903
+ Note.delete.bind(Note, noteId),
904
+ removeReferences
905
+ ], function(err, results) {
906
+ return cb(err, null);
907
+ });
908
+
909
+ };
910
+
911
+ ///////////////////////////////
912
+
913
+ Organization.pre('save', function(next) {
914
+
915
+ var self = this;
916
+
917
+ // Format website
918
+ if (self.website) self.website = utils.getDomain(self.website);
919
+
920
+ // Format website aliases
921
+ if (self.websiteAliases && self.websiteAliases.length >= 1) {
922
+ self.websiteAliases = _.map(self.websiteAliases, function(alias) {
923
+ return utils.getDomain(alias);
924
+ });
925
+ }
926
+
927
+ return next();
928
+
929
+ });
930
+
931
+ Organization.post('init', function() {
932
+
933
+ // Protect private data and merge public and private data
934
+
935
+ var self = this;
936
+ var CUSTOMER_ID = config.CUSTOMER_ID;
937
+
938
+ if (CUSTOMER_ID == 'GLOBAL_PROCESS') return;
939
+
940
+ var protectCustomerData = function protectCustomerData() {
941
+
942
+ // remove customer details if not viewing self
943
+ if (self._id.toString() != CUSTOMER_ID) self.customer = {};
944
+
945
+ self.filters = _.reject(self.filters, function(item) { return item.customer != CUSTOMER_ID; });
946
+ self.docs = _.reject(self.docs, function(item) { return item.customer != CUSTOMER_ID; });
947
+ self.iLevel = _.reject(self.iLevel, function(item) { return item.customer != CUSTOMER_ID; });
948
+ self.chairs = _.reject(self.chairs, function(item) { return item.customer != CUSTOMER_ID; });
949
+ self.capTable = _.reject(self.capTable, function(item) { return item.customer != CUSTOMER_ID; });
950
+
951
+ // investment data
952
+ _.each(self.rounds, function(round) {
953
+ _.each(round.funds, function(fund) {
954
+ if (fund.customer != CUSTOMER_ID) fund.investments = [];
955
+ });
956
+ });
957
+
958
+ };
959
+
960
+ var mergePublicAndPrivateData = function mergePublicAndPrivateData() {
961
+
962
+ // rounds are public
963
+ _.each(self.rounds, function(round) {
964
+
965
+ // note that public versions of the following data exist at this level:
966
+ // preMoneyValuation
967
+ // amountRaised
968
+ // closingDate
969
+ // this data will be overwritten if private data belonging to the current customer is present
970
+
971
+ // participating funds are public
972
+ _.each(round.funds, function(fund) {
973
+
974
+ if (fund.customer != CUSTOMER_ID) return;
975
+ if (!fund.investments || fund.investments.length == 0) return;
976
+
977
+ // there can be multiple investments in a single round from the same fund
978
+ // use the most recent
979
+ var mostRecentInvestment = _.max(fund.investments, function(investment) { return investment.date; });
980
+
981
+ // this fund belongs to the current customer
982
+ // if we have private data, use it rather than the public data
983
+ if (mostRecentInvestment) {
984
+ if (mostRecentInvestment.preMoneyValuation) round.preMoneyValuation = mostRecentInvestment.preMoneyValuation;
985
+ if (mostRecentInvestment.amountRaised) round.amountRaised = mostRecentInvestment.amountRaised;
986
+ if (mostRecentInvestment.closingDate) round.closingDate = mostRecentInvestment.closingDate;
987
+ }
988
+
989
+ });
990
+
991
+ });
992
+
993
+ };
994
+
995
+ protectCustomerData();
996
+ mergePublicAndPrivateData();
997
+
998
+ });
999
+
1000
+ Organization.set('toJSON', { virtuals: true });
1001
+ Organization.set('autoIndex', true);
1002
+
1003
+ Organization.index({ website: 1, websiteAliases: 1 }, { unique: true, sparse: true });
1004
+ Organization.on('index', function(err) { console.log('error building organization_new indexes: ' + err); });
1005
+
1006
+ var deepPopulate = require('mongoose-deep-populate')(mongoose);
1007
+ Organization.plugin(deepPopulate);
1008
+
1009
+ mongoose.model('Organization', Organization, 'Organization_new');
1010
+
1011
+ };