@dhyasama/totem-models 1.0.1 → 1.6.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.
@@ -2,8 +2,6 @@
2
2
 
3
3
  module.exports = function(mongoose, config) {
4
4
 
5
- var LPS_ENABLED = config.LPS_ENABLED || false;
6
-
7
5
  var
8
6
 
9
7
  _ = require('underscore'),
@@ -12,6 +10,8 @@ module.exports = function(mongoose, config) {
12
10
  Schema = mongoose.Schema,
13
11
  Note = mongoose.model('Note'),
14
12
 
13
+ customerPortfolio = [],
14
+
15
15
  Organization = new Schema({
16
16
 
17
17
  name: { type: String, required: true, index: true },
@@ -64,6 +64,7 @@ module.exports = function(mongoose, config) {
64
64
  title: { type: String, default: null, trim: true },
65
65
  current: { type: Boolean, default: false, index: true },
66
66
  leader: { type: Boolean, default: false, index: true },
67
+ public: { type: Boolean, default: true },
67
68
  board: { type: String, enum: ['chairman', 'director', 'observer', 'none'], default: 'none', index: true }
68
69
  }],
69
70
 
@@ -114,10 +115,6 @@ module.exports = function(mongoose, config) {
114
115
  url: { type: String, unique: true, sparse: true, trim: true }
115
116
  },
116
117
 
117
- eshares: {
118
- companyid: { type: String, unique: true, sparse: true, trim: true }
119
- },
120
-
121
118
  related: [{ type: Schema.ObjectId, ref: 'Organization' }],
122
119
 
123
120
  // customer specific (customer id is stored on note)
@@ -172,171 +169,238 @@ module.exports = function(mongoose, config) {
172
169
  // Funds belonging to this organization (which is typically, but not necessarily, what we'd call an investor)
173
170
  // An example of a non-"investor" with funds would be Slack with it's Slack Fund
174
171
  // Another way to put it, these are funds from which this organization makes investments
175
- funds: [{ type: Schema.ObjectId, ref: 'Fund' }],
172
+ funds: [{ type: Schema.ObjectId, ref: 'Fund' }]
176
173
 
177
- // Private
178
- capTable: [{
174
+ });
179
175
 
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
176
 
185
- // todo - ??? - clarify how to handle this
186
- optionsAndWarrants: {
187
- unissued: { type: Number },
188
- issuedToUnspecified: { type: Number }
189
- }
190
177
 
191
- }],
178
+ ///////////////////////////////////////////////////////////////////////////////////////
179
+ // HELPERS
180
+ ///////////////////////////////////////////////////////////////////////////////////////
192
181
 
193
- // Equity or debt this organization has taken from other organizations
194
- rounds: [{
182
+ var getSources = function getSources(people) {
195
183
 
196
- roundName: { type: String, trim: true, default: null },
184
+ var result = _.compact(_.pluck(people, 'person'));
185
+ if (result.length == 0) return [];
197
186
 
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 },
187
+ result = _.compact(_.pluck(result, 'sources'));
188
+ if (result.length == 0) return [];
200
189
 
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 },
190
+ result = _.flatten(result);
191
+ result = _.compact(result);
205
192
 
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: [{
193
+ if (result.length == 0) return [];
194
+ if (mongoose.Types.ObjectId.isValid(result[0].person)) return []; // not populated
210
195
 
211
- fund: { type: Schema.ObjectId, ref: 'Fund', required: true },
196
+ result = _.uniq(result, function(item) { return item.person._id.toString(); });
197
+ result = _.sortBy(result, 'interactions.count');
198
+ result.reverse();
199
+ result = _.sortBy(result, 'name.full');
212
200
 
213
- // the lead on this round from the fund/org
214
- partner: { type: Schema.ObjectId, ref: 'Person', required: false },
201
+ return result;
215
202
 
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 },
203
+ };
219
204
 
220
- // Investments in this round from this fund
221
- // this is all private data
222
- investments: [{
223
205
 
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
206
 
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 },
207
+ ///////////////////////////////////////////////////////////////////////////////////////
208
+ // VIRTUALS
209
+ // Properties that are not persisted to the database
210
+ ///////////////////////////////////////////////////////////////////////////////////////
237
211
 
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
- }]
212
+ Organization.virtual('address.primary').get(function () {
213
+ var primary = _.find(this.contact.address, function(address) {
214
+ return address.primary;
215
+ });
216
+ return primary ? primary.address : null;
217
+ });
246
218
 
247
- }]
219
+ Organization.virtual('board.all').get(function () {
248
220
 
249
- }],
221
+ var self = this;
222
+ var members = [];
250
223
 
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
- }]
224
+ _.each(self.people, function(p) {
225
+ var boardRole = p.board;
226
+ if (boardRole && boardRole != 'none') members.push(p.person);
227
+ });
256
228
 
257
- }],
229
+ members = _.sortBy(members, function(member) {
230
+ if (!member || !member.name) return null;
231
+ return member.name.last + member.name.first;
232
+ });
258
233
 
234
+ return members;
235
+
236
+ });
237
+
238
+ Organization.virtual('board.current').get(function () {
239
+
240
+ var self = this;
241
+ var members = [];
242
+
243
+ _.each(self.people, function(p) {
244
+ var boardRole = p.board;
245
+ if (p.current && boardRole && boardRole != 'none') members.push(p.person);
259
246
  });
260
247
 
261
- ///////////////////////////////
248
+ members = _.sortBy(members, function(member) {
249
+ if (!member || !member.name) return null;
250
+ return member.name.last + member.name.first;
251
+ });
262
252
 
263
- Organization.virtual('status').get(function () {
253
+ return members;
254
+
255
+ });
256
+
257
+ Organization.virtual('board.past').get(function () {
264
258
 
265
259
  var self = this;
260
+ var members = [];
266
261
 
267
- // infer from data
262
+ _.each(self.people, function(p) {
263
+ var boardRole = p.board;
264
+ if (!p.current && boardRole && boardRole != 'none') members.push(p.person);
265
+ });
268
266
 
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';
267
+ members = _.sortBy(members, function(member) {
268
+ if (!member || !member.name) return null;
269
+ return member.name.last + member.name.first;
270
+ });
271
+
272
+ return members;
273
273
 
274
274
  });
275
275
 
276
- Organization.virtual('address.primary').get(function () {
277
- var primary = _.find(this.contact.address, function(address) {
278
- return address.primary;
276
+ Organization.virtual('employees.all').get(function () {
277
+
278
+ var self = this;
279
+ var result = [];
280
+
281
+ _.each(self.people, function(p) {
282
+ if (!p.leader && (!p.board || p.board == 'none')) result.push(p);
279
283
  });
280
- return primary ? primary.address : null;
284
+
285
+ result = _.sortBy(result, function(item) {
286
+ if (!item.person || !item.person.name) return null;
287
+ return item.person.name.last + item.person.name.first;
288
+ });
289
+
290
+ return result;
291
+
281
292
  });
282
293
 
283
- Organization.virtual('sources').get(function () {
294
+ Organization.virtual('employees.current').get(function () {
295
+
296
+ var self = this;
297
+ var result = [];
298
+
299
+ _.each(self.people, function(p) {
300
+ if (p.current && !p.leader && (!p.board || p.board == 'none')) result.push(p);
301
+ });
302
+
303
+ result = _.sortBy(result, function(item) {
304
+ if (!item.person || !item.person.name) return null;
305
+ return item.person.name.last + item.person.name.first;
306
+ });
307
+
308
+ return result;
309
+
310
+ });
284
311
 
285
- // Aggregate sources from all people at org
286
- // Sort by interactions descending, full name ascending
312
+ Organization.virtual('employees.past').get(function () {
287
313
 
288
314
  var self = this;
315
+ var result = [];
289
316
 
290
- var result = _.compact(_.pluck(self.people, 'person'));
291
- if (result.length == 0) return [];
317
+ _.each(self.people, function(p) {
318
+ if (!p.current && !p.leader && (!p.board || p.board == 'none')) result.push(p);
319
+ });
292
320
 
293
- result = _.compact(_.pluck(result, 'sources'));
294
- if (result.length == 0) return [];
321
+ result = _.sortBy(result, function(item) {
322
+ if (!item.person || !item.person.name) return null;
323
+ return item.person.name.last + item.person.name.first;
324
+ });
295
325
 
296
- result = _.flatten(result);
297
- result = _.compact(result);
326
+ return result;
298
327
 
299
- if (result.length == 0) return [];
300
- if (mongoose.Types.ObjectId.isValid(result[0].person)) return []; // not populated
328
+ });
301
329
 
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');
330
+ Organization.virtual('leaders.all').get(function () {
331
+
332
+ var self = this;
333
+ var result = [];
334
+
335
+ _.each(self.people, function(p) {
336
+ if (p.leader) result.push(p);
337
+ });
338
+
339
+ result = _.sortBy(result, function(item) {
340
+ if (!item.person || !item.person.name) return null;
341
+ return item.person.name.last + item.person.name.first;
342
+ });
306
343
 
307
344
  return result;
308
345
 
309
346
  });
310
347
 
311
- ///////////////////////////////
348
+ Organization.virtual('leaders.current').get(function () {
312
349
 
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
- };
350
+ var self = this;
351
+ var result = [];
322
352
 
323
- Organization.methods.deleteOrg = function() {
324
- // Soft delete
325
- this.deleted = true;
326
- };
353
+ _.each(self.people, function(p) {
354
+ if (p.leader && p.current) result.push(p);
355
+ });
327
356
 
328
- Organization.methods.relateOrg = function(id) {
357
+ result = _.sortBy(result, function(item) {
358
+ if (!item.person || !item.person.name) return null;
359
+ return item.person.name.last + item.person.name.first;
360
+ });
361
+
362
+ return result;
363
+
364
+ });
365
+
366
+ Organization.virtual('leaders.past').get(function () {
329
367
 
330
368
  var self = this;
369
+ var result = [];
331
370
 
332
- var match = _.find(self.related, function(org) {
333
- var orgId = utils.isValidObjectId(org) ? org : org.id;
334
- return orgId.toString() == id.toString();
371
+ _.each(self.people, function(p) {
372
+ if (p.leader && !p.current) result.push(p);
335
373
  });
336
374
 
337
- if (!match) { self.related.push(id); }
375
+ result = _.sortBy(result, function(item) {
376
+ if (!item.person || !item.person.name) return null;
377
+ return item.person.name.last + item.person.name.first;
378
+ });
379
+
380
+ return result;
381
+
382
+ });
383
+
384
+ Organization.virtual('status').get(function () {
385
+
386
+ var self = this;
387
+
388
+ // infer from data
389
+
390
+ if (self.closedOn) return 'Dead';
391
+ else if (self.ipo && self.ipo.year) return 'IPO';
392
+ else if (self.acquisition && self.acquisition.year) return 'Acquired';
393
+ else return 'Active';
394
+
395
+ });
396
+
338
397
 
339
- };
398
+
399
+ ///////////////////////////////////////////////////////////////////////////////////////
400
+ // METHODS
401
+ // Methods operate on a single document that has already been returned
402
+ // Note that running a method on a document does not save it
403
+ ///////////////////////////////////////////////////////////////////////////////////////
340
404
 
341
405
  Organization.methods.addPerson = function(personToAdd) {
342
406
 
@@ -358,165 +422,149 @@ module.exports = function(mongoose, config) {
358
422
 
359
423
  };
360
424
 
361
- Organization.methods.removePerson = function(personIdToRemove) {
362
-
363
- this.people = _.reject(this.people, function(existingPerson) {
364
- var id = utils.isValidObjectId(existingPerson.person) ? existingPerson.person : existingPerson.person._id;
365
- return id.toString() == personIdToRemove.toString();
366
- });
367
-
425
+ Organization.methods.deleteOrg = function() {
426
+ // Soft delete
427
+ this.deleted = true;
368
428
  };
369
429
 
370
- ///////////////////////
371
-
372
- // search for orgs based on fields provided
373
- // data format, each field is optional
374
- // {
375
- // domain: ''
376
- // name: ''
377
- // }
378
- // options:
379
- // fuzzy: searches within strings, e.g., searching for "venture" will match "ff Venture Capital"
380
- // non-fuzzy does a full-string match, e.g., "ffvc" will match "ffvc" but "ff" will not
381
- // both fuzzy and non are case-insensitive
382
- // queryFirstWord: tokenizes first word and searches for it
383
- // removeRelatedFromTopLevel: related people are removed from top-level results
384
- Organization.statics.search = function search(data, options, cb) {
430
+ Organization.methods.merge = function(id) {
431
+ if (this.merged.indexOf(id) == -1) {
432
+ this.merged.push(id);
433
+ return true;
434
+ }
435
+ else {
436
+ return false;
437
+ }
438
+ };
385
439
 
386
- // validate input
387
- if (!cb) throw new Error('cb is required');
388
- if (!data || (!data.name && !data.domain)) return cb(null, []);
440
+ Organization.methods.relateOrg = function(id) {
389
441
 
390
- var defaultOptions = {
391
- fuzzy: true,
392
- queryFirstWord: false,
393
- removeRelatedFromTopLevel: true
394
- };
442
+ var self = this;
395
443
 
396
- // combine provided and default options
397
- options = utils.setDefaults(options, defaultOptions);
444
+ var match = _.find(self.related, function(org) {
445
+ var orgId = utils.isValidObjectId(org) ? org : org.id;
446
+ return orgId.toString() == id.toString();
447
+ });
398
448
 
399
- // need to be admin or provide investor id
400
- if (!options.admin && !options.investor) return cb(null, []);
449
+ if (!match) { self.related.push(id); }
401
450
 
402
- data.name = data.name || '';
403
- data.domain = data.domain || '';
451
+ };
404
452
 
405
- var removeRelatedFromTopLevel = function(items) {
453
+ Organization.methods.removePerson = function(personIdToRemove) {
406
454
 
407
- // All entities are searched initially, including entities that are listed as related to other entities
408
- // Remove related entities from top-level results so they only come up as related
455
+ this.people = _.reject(this.people, function(existingPerson) {
456
+ var id = utils.isValidObjectId(existingPerson.person) ? existingPerson.person : existingPerson.person._id;
457
+ return id.toString() == personIdToRemove.toString();
458
+ });
409
459
 
410
- var getRelatedIds = function getRelatedIds(items) {
460
+ };
411
461
 
412
- var result = [];
413
462
 
414
- _.each(items, function(item) {
415
- if (item.related && item.related.length >= 1) {
416
- var ids = _.pluck(item.related, 'id');
417
- result = result.concat(ids);
418
- }
419
- });
420
463
 
421
- return _.uniq(result);
464
+ //////////////////////////////////////////////////////
465
+ // STATICS
466
+ // Statics query the entire collection
467
+ //////////////////////////////////////////////////////
422
468
 
423
- };
469
+ Organization.statics.addNote = function(organizationId, creatorPersonId, text, cb) {
424
470
 
425
- var relatedIds = getRelatedIds(items);
426
- items = _.reject(items, function(item) {
427
- return relatedIds.indexOf(item.id) >= 0;
428
- });
471
+ Note.createForModel({
472
+ createdBy: creatorPersonId,
473
+ text: text
474
+ }, this, organizationId, cb);
429
475
 
430
- return items;
476
+ };
431
477
 
432
- };
478
+ Organization.statics.deleteNote = function(noteId, cb) {
433
479
 
434
- // use first word in org name to look for fuzzy matches which is a simple
435
- // but effective technique as the first word is commonly the most important
436
- var tokenizeName = function tokenizeName(name, query) {
480
+ // Delete the note itself along with any references
437
481
 
438
- if (!name) throw new Error('name is required');
439
- if (!query) throw new Error('query is required');
482
+ var self = this;
440
483
 
441
- var parts = [];
484
+ var removeReferences = function removeReferences(callback) {
485
+ self.update({}, {
486
+ $pull: { 'notes' : [noteId] }
487
+ }, callback);
488
+ };
442
489
 
443
- try { parts = name.toString().split(' '); }
444
- catch (err) { throw err; }
490
+ async.series([
491
+ Note.delete.bind(Note, noteId),
492
+ removeReferences
493
+ ], function(err, results) {
494
+ return cb(err, null);
495
+ });
445
496
 
446
- // fuzzy search first word if more than one word in phrase
447
- if (parts.length >= 2) {
448
- query['$or'].push({ 'name': new RegExp(parts[0], 'i') });
449
- }
497
+ };
450
498
 
451
- };
499
+ Organization.statics.findByDomains = function findByDomains(domains, cb) {
500
+ var query = this.find({ $or: [ {'website': { $in : domains }}, {'websiteAliases': { $in : domains }} ], 'deleted': {$ne: true} });
501
+ query.exec(cb);
502
+ };
452
503
 
453
- var buildQuery = function buildQuery(data, options) {
504
+ Organization.statics.findByFunds = function findByFunds(fundids, cb) {
505
+ var self = this;
506
+ self
507
+ .find({ 'funds': { $in : fundids }, 'deleted': {$ne: true} })
508
+ .select('name logoUrl')
509
+ .exec(cb);
510
+ };
454
511
 
455
- // prepare query
456
- var query = {};
457
- query['$or'] = []; // these will be added dynamically from data parameter
458
- query['$and'] = []; // these will be added dynamically from data parameter
459
- query['deleted'] = { $ne: true }; // always include this
512
+ Organization.statics.findByIds = function findByIds(ids, cb) {
460
513
 
461
- // conditionally include name
462
- if (data.name) {
514
+ var self = this;
463
515
 
464
- // an exact match is always a good thing
465
- query['$or'].push({ 'name': new RegExp('^' + data.name + '$', 'i') });
466
-
467
- if (options.fuzzy) {
468
- // fuzzy search full phrase
469
- query['$or'].push({'name': new RegExp(data.name, 'i')});
470
- }
471
-
472
- if (options.queryFirstWord) {
473
- // fuzzy search first word
474
- tokenizeName(data.name, query);
516
+ self
517
+ .find({ '_id': { $in : ids }})
518
+ .populate('people.person', 'name avatarUrl title calendarEventSummaries sources')
519
+ .populate('related', 'name logoUrl')
520
+ .populate('notes')
521
+ .populate('chairs.first', 'name avatarUrl title')
522
+ .populate('chairs.second', 'name avatarUrl title')
523
+ .populate('funds', 'name hexColorCode')
524
+ .deepPopulate([
525
+ 'people.person.sources.person',
526
+ 'people.person.calendarEventSummaries.attendees.internal',
527
+ 'people.person.calendarEventSummaries.attendees.external',
528
+ 'people.person.calendarEventSummaries.notes.createdBy',
529
+ 'notes.createdBy'
530
+ ], {
531
+ populate: {
532
+ 'people.person.sources.person': { select: 'name avatarUrl title' },
533
+ 'people.person.calendarEventSummaries.attendees.internal': { select: 'name avatarUrl title' },
534
+ 'people.person.calendarEventSummaries.attendees.external': { select: 'name avatarUrl title' },
535
+ 'people.person.calendarEventSummaries.notes.createdBy': { select: 'name avatarUrl title' },
536
+ 'notes.createdBy': { select: 'name avatarUrl title' }
475
537
  }
538
+ })
539
+ .exec(cb);
476
540
 
477
- var aliasRegExp = options.fuzzy ? new RegExp(data.name, 'i') : new RegExp('^' + data.name + '$', 'i');
478
- query['$or'].push({ 'aliases': aliasRegExp });
479
-
480
- }
481
-
482
- // conditionally include domain
483
- if (data.domain) {
484
- var domainRegExp = options.fuzzy ? new RegExp(data.domain, 'i') : new RegExp('^' + data.domain + '$', 'i');
485
- query['$or'].push({ 'website': domainRegExp });
486
- }
487
-
488
- // remove if unused
489
- if (query['$or'].length == 0) delete query['$or'];
490
- if (query['$and'].length == 0) delete query['$and'];
491
-
492
- return query;
493
-
494
- };
495
-
496
- var query = buildQuery(data, options);
497
-
498
- this
499
- .find(query)
500
- .populate('merged')
501
- .select('logoUrl name description related')
502
- .exec(function(err, orgs) {
541
+ };
503
542
 
504
- if (err) return cb(err, null);
505
- else if (!orgs) return cb(null, null);
506
- else {
543
+ Organization.statics.findByPerson = function findByPerson(personId, cb) {
507
544
 
508
- // remove related from top level
509
- if (options.removeRelatedFromTopLevel) orgs = removeRelatedFromTopLevel(orgs);
545
+ // Given a person id, get all organizations with that person
510
546
 
511
- // sort by name
512
- orgs = _.sortBy(orgs, function(orgs) { return orgs.name; });
547
+ var self = this;
513
548
 
514
- return cb(null, orgs);
549
+ self
550
+ .find({ $or:
551
+ [
552
+ { 'people.person': personId },
553
+ { 'chairs.first': personId },
554
+ { 'chairs.second': personId }
555
+ ],
556
+ 'deleted': { $ne: true }
557
+ })
558
+ .exec(cb);
515
559
 
516
- }
560
+ };
517
561
 
518
- });
562
+ Organization.statics.findBySlug = function findBySlug(slug, cb) {
563
+ this.findOne({ slug: slug, 'deleted': {$ne: true} }).exec(cb);
564
+ };
519
565
 
566
+ Organization.statics.findBySlugs = function findBySlugs(slugs, cb) {
567
+ this.find({ 'slug': { $in : slugs }, 'deleted': {$ne: true} }).exec(cb);
520
568
  };
521
569
 
522
570
  Organization.statics.findDupes = function findDupes(org, options, cb) {
@@ -543,43 +591,60 @@ module.exports = function(mongoose, config) {
543
591
 
544
592
  };
545
593
 
546
- Organization.statics.stats = function stats(cb) {
547
- this.collection.stats(cb);
548
- };
594
+ Organization.statics.flag = function(orgId, resolved, text, username, cb) {
549
595
 
550
- Organization.statics.findByPerson = function findByPerson(personId, cb) {
551
- // Given a person id, get all organizations with that person
552
- this.find({ 'people.person': personId, 'deleted': {$ne: true} }).exec(cb);
553
- };
596
+ this.findById(orgId).exec(function(err, org) {
597
+
598
+ org.flagged.resolved = resolved;
599
+ org.flagged.text = text;
600
+ org.flagged.by = username;
601
+ org.flagged.on = Date.now();
602
+
603
+ org.save(cb);
604
+
605
+ });
554
606
 
555
- Organization.statics.findBySlug = function findBySlug(slug, cb) {
556
- this.findOne({ slug: slug, 'deleted': {$ne: true} }).exec(cb);
557
607
  };
558
608
 
559
- Organization.statics.findBySlugs = function findBySlugs(slugs, cb) {
560
- this.find({ 'slug': { $in : slugs }, 'deleted': {$ne: true} }).exec(cb);
609
+ Organization.statics.getBoardsOfPerson = function getBoardsOfPerson(personId, cb) {
610
+
611
+ var self = this;
612
+
613
+ self
614
+ .find({
615
+ 'deleted': { $ne: true },
616
+ 'people': {
617
+ $elemMatch: {
618
+ 'person': personId,
619
+ 'board': { $in: ['chairman', 'director', 'observer'] }
620
+ }
621
+ }
622
+ })
623
+ .select('name avatarUrl')
624
+ .exec(cb);
625
+
561
626
  };
562
627
 
563
- Organization.statics.findByDomains = function findByDomains(domains, cb) {
564
- var query = this.find({ $or: [ {'website': { $in : domains }}, {'websiteAliases': { $in : domains }} ], 'deleted': {$ne: true} });
565
- query.exec(cb);
628
+ Organization.statics.getByFund = function getByFund(id, cb) {
629
+ var self = this;
630
+ self
631
+ .findOne({ 'funds': id, 'deleted': {$ne: true} })
632
+ .select('name logoUrl')
633
+ .exec(cb);
566
634
  };
567
635
 
568
- Organization.statics.findByIds = function findByIds(ids, cb) {
636
+ Organization.statics.getById = function getById(id, cb) {
569
637
 
570
638
  var self = this;
571
639
 
572
640
  self
573
- .find({ '_id': { $in : ids }})
641
+ .findById(id)
574
642
  .populate('people.person', 'name avatarUrl title calendarEventSummaries sources')
575
643
  .populate('related', 'name logoUrl')
576
644
  .populate('notes')
577
645
  .populate('chairs.first', 'name avatarUrl title')
578
646
  .populate('chairs.second', 'name avatarUrl title')
579
- .populate('funds', 'name hexColorCode')
580
- .populate('rounds.funds.fund', 'name hexColorCode')
581
- .populate('rounds.funds.partner', 'name avatarUrl title')
582
- .populate('rounds.people.person', 'name avatarUrl title')
647
+ .populate('funds', 'name hexColorCode closeDate')
583
648
  .deepPopulate([
584
649
  'people.person.sources.person',
585
650
  'people.person.calendarEventSummaries.attendees.internal',
@@ -599,51 +664,94 @@ module.exports = function(mongoose, config) {
599
664
 
600
665
  };
601
666
 
602
- Organization.statics.getById = function getById(id, cb) {
667
+ Organization.statics.getChairsOfPerson = function getChairsOfPerson(personId, cb) {
668
+
669
+ var self = this;
670
+
671
+ self
672
+ .find({ $or:
673
+ [
674
+ { 'chairs.first': personId },
675
+ { 'chairs.second': personId }
676
+ ],
677
+ 'deleted': { $ne: true }
678
+ })
679
+ .exec(cb);
680
+
681
+ };
682
+
683
+ Organization.statics.getSources = function(id, cb) {
603
684
 
604
685
  var self = this;
605
686
 
606
687
  self
607
688
  .findById(id)
608
689
  .populate('people.person', 'name avatarUrl title calendarEventSummaries sources')
609
- .populate('related', 'name logoUrl')
610
- .populate('notes')
611
- .populate('chairs.first', 'name avatarUrl title')
612
- .populate('chairs.second', 'name avatarUrl title')
613
- .populate('funds', 'name hexColorCode')
614
- .populate('rounds.funds.fund', 'name hexColorCode')
615
- .populate('rounds.funds.partner', 'name avatarUrl title')
616
- .populate('rounds.people.person', 'name avatarUrl title')
617
690
  .deepPopulate([
618
- 'people.person.sources.person',
619
- 'people.person.calendarEventSummaries.attendees.internal',
620
- 'people.person.calendarEventSummaries.attendees.external',
621
- 'people.person.calendarEventSummaries.notes.createdBy',
622
- 'notes.createdBy'
691
+ 'people.person.sources.person'
623
692
  ], {
624
693
  populate: {
625
- 'people.person.sources.person': { select: 'name avatarUrl title' },
626
- 'people.person.calendarEventSummaries.attendees.internal': { select: 'name avatarUrl title' },
627
- 'people.person.calendarEventSummaries.attendees.external': { select: 'name avatarUrl title' },
628
- 'people.person.calendarEventSummaries.notes.createdBy': { select: 'name avatarUrl title' },
629
- 'notes.createdBy': { select: 'name avatarUrl title' }
694
+ 'people.person.sources.person': { select: 'name avatarUrl title' }
630
695
  }
631
696
  })
632
- .exec(cb);
697
+ .exec(function(err, org) {
698
+
699
+ if (err) return cb(err, null);
700
+ if (!org) return cb(null, null);
701
+
702
+ self.findById(config.CUSTOMER_ID, function(err, customer) {
703
+
704
+ if (err) return cb(err, null);
705
+ if (!customer) return cb(null, null);
706
+
707
+ var current = [];
708
+ var past = [];
709
+ var all = [];
710
+ var orgSources = getSources(org.people);
711
+
712
+ _.each(customer.people, function(p) {
713
+
714
+ var source = _.find(orgSources, function(os) {
715
+ return os.person._id.toString() == p.person.toString();
716
+ });
717
+
718
+ if (!source) return;
719
+
720
+ if (p.current) current.push(source);
721
+ if (!p.current) past.push(source);
722
+ all.push(source);
723
+
724
+ });
725
+
726
+ return cb(null, {
727
+ all: all,
728
+ current: current,
729
+ past: past
730
+ });
731
+
732
+ });
733
+
734
+ });
633
735
 
634
736
  };
635
737
 
636
- Organization.statics.getPortfolio = function getPortfolio(orgId, cb) {
738
+ Organization.statics.listAllFlagged = function (cb) {
637
739
 
638
- // Get all organizations invested in by the given org id
740
+ var query = this.find({'flagged.resolved': false, 'flagged.text': { $ne: '' }, 'deleted': {$ne: true}});
741
+ query.exec(function (err, orgs) {
639
742
 
640
- var self = this;
743
+ if (err) return cb(err, null);
641
744
 
642
- self
643
- .find({ 'rounds.funds.customer': orgId })
644
- .select('name logoUrl description exit filters contact.addresses')
645
- .sort('name')
646
- .exec(cb);
745
+ orgs = _.sortBy(orgs, function(org){
746
+ var timestamp = new Date(org.flagged.on);
747
+ timestamp = timestamp.getTime();
748
+ return timestamp;
749
+ });
750
+ orgs = orgs.reverse();
751
+
752
+ return cb(null, orgs);
753
+
754
+ });
647
755
 
648
756
  };
649
757
 
@@ -658,93 +766,195 @@ module.exports = function(mongoose, config) {
658
766
 
659
767
  };
660
768
 
661
- Organization.statics.listAllFlagged = function (cb) {
769
+ Organization.statics.removeById = function(id, cb) {
662
770
 
663
- var query = this.find({'flagged.resolved': false, 'flagged.text': { $ne: '' }, 'deleted': {$ne: true}});
664
- query.exec(function (err, orgs) {
771
+ if (!id) { return cb(new Error('id is required'), null); }
772
+ if (!cb) { throw new Error('cb is required'); }
665
773
 
666
- if (err) return cb(err, null);
774
+ this.remove({_id: id}, cb);
775
+
776
+ };
667
777
 
668
- orgs = _.sortBy(orgs, function(org){
669
- var timestamp = new Date(org.flagged.on);
670
- timestamp = timestamp.getTime();
671
- return timestamp;
778
+ Organization.statics.search = function search(data, options, cb) {
779
+
780
+ // search for orgs based on fields provided
781
+ // data format, each field is optional
782
+ // {
783
+ // domain: ''
784
+ // name: ''
785
+ // }
786
+ // options:
787
+ // fuzzy: searches within strings, e.g., searching for "venture" will match "ff Venture Capital"
788
+ // non-fuzzy does a full-string match, e.g., "ffvc" will match "ffvc" but "ff" will not
789
+ // both fuzzy and non are case-insensitive
790
+ // queryFirstWord: tokenizes first word and searches for it
791
+ // removeRelatedFromTopLevel: related people are removed from top-level results
792
+
793
+ // validate input
794
+ if (!cb) throw new Error('cb is required');
795
+ if (!data || (!data.name && !data.domain)) return cb(null, []);
796
+
797
+ var defaultOptions = {
798
+ fuzzy: true,
799
+ queryFirstWord: false,
800
+ removeRelatedFromTopLevel: true
801
+ };
802
+
803
+ // combine provided and default options
804
+ options = utils.setDefaults(options, defaultOptions);
805
+
806
+ // need to be admin or provide investor id
807
+ if (!options.admin && !options.investor) return cb(null, []);
808
+
809
+ data.name = data.name || '';
810
+ data.domain = data.domain || '';
811
+
812
+ var removeRelatedFromTopLevel = function(items) {
813
+
814
+ // All entities are searched initially, including entities that are listed as related to other entities
815
+ // Remove related entities from top-level results so they only come up as related
816
+
817
+ var getRelatedIds = function getRelatedIds(items) {
818
+
819
+ var result = [];
820
+
821
+ _.each(items, function(item) {
822
+ if (item.related && item.related.length >= 1) {
823
+ var ids = _.pluck(item.related, 'id');
824
+ result = result.concat(ids);
825
+ }
672
826
  });
673
- orgs = orgs.reverse();
674
827
 
675
- return cb(null, orgs);
828
+ return _.uniq(result);
829
+
830
+ };
676
831
 
832
+ var relatedIds = getRelatedIds(items);
833
+ items = _.reject(items, function(item) {
834
+ return relatedIds.indexOf(item.id) >= 0;
677
835
  });
678
836
 
679
- };
837
+ return items;
680
838
 
681
- Organization.statics.upsert = function(org, username, cb) {
839
+ };
682
840
 
683
- if (!org) { return cb(new Error('Organization is required'), null); }
684
- if (!username) { return cb(new Error('Username is required'), null); }
841
+ // use first word in org name to look for fuzzy matches which is a simple
842
+ // but effective technique as the first word is commonly the most important
843
+ var tokenizeName = function tokenizeName(name, query) {
685
844
 
686
- org.entered.by = username;
687
- org.entered.on = new Date();
845
+ if (!name) throw new Error('name is required');
846
+ if (!query) throw new Error('query is required');
688
847
 
689
- org.save(cb);
848
+ var parts = [];
690
849
 
691
- };
850
+ try { parts = name.toString().split(' '); }
851
+ catch (err) { throw err; }
692
852
 
693
- Organization.statics.removeById = function(id, cb) {
853
+ // fuzzy search first word if more than one word in phrase
854
+ if (parts.length >= 2) {
855
+ query['$or'].push({ 'name': new RegExp(parts[0], 'i') });
856
+ }
694
857
 
695
- if (!id) { return cb(new Error('id is required'), null); }
696
- if (!cb) { throw new Error('cb is required'); }
858
+ };
697
859
 
698
- this.remove({_id: id}, cb);
860
+ var buildQuery = function buildQuery(data, options) {
699
861
 
700
- };
862
+ // prepare query
863
+ var query = {};
864
+ query['$or'] = []; // these will be added dynamically from data parameter
865
+ query['$and'] = []; // these will be added dynamically from data parameter
866
+ query['deleted'] = { $ne: true }; // always include this
701
867
 
702
- Organization.statics.flag = function(orgId, resolved, text, username, cb) {
868
+ // conditionally include name
869
+ if (data.name) {
703
870
 
704
- this.findById(orgId).exec(function(err, org) {
871
+ // an exact match is always a good thing
872
+ query['$or'].push({ 'name': new RegExp('^' + data.name + '$', 'i') });
705
873
 
706
- org.flagged.resolved = resolved;
707
- org.flagged.text = text;
708
- org.flagged.by = username;
709
- org.flagged.on = Date.now();
874
+ if (options.fuzzy) {
875
+ // fuzzy search full phrase
876
+ query['$or'].push({'name': new RegExp(data.name, 'i')});
877
+ }
710
878
 
711
- org.save(cb);
879
+ if (options.queryFirstWord) {
880
+ // fuzzy search first word
881
+ tokenizeName(data.name, query);
882
+ }
712
883
 
713
- });
884
+ var aliasRegExp = options.fuzzy ? new RegExp(data.name, 'i') : new RegExp('^' + data.name + '$', 'i');
885
+ query['$or'].push({ 'aliases': aliasRegExp });
714
886
 
715
- };
887
+ }
716
888
 
717
- Organization.statics.addNote = function(organizationId, creatorPersonId, text, cb) {
889
+ // conditionally include domain
890
+ if (data.domain) {
891
+ var domainRegExp = options.fuzzy ? new RegExp(data.domain, 'i') : new RegExp('^' + data.domain + '$', 'i');
892
+ query['$or'].push({ 'website': domainRegExp });
893
+ }
718
894
 
719
- Note.createForModel({
720
- createdBy: creatorPersonId,
721
- text: text
722
- }, this, organizationId, cb);
895
+ // remove if unused
896
+ if (query['$or'].length == 0) delete query['$or'];
897
+ if (query['$and'].length == 0) delete query['$and'];
723
898
 
724
- };
899
+ return query;
725
900
 
726
- Organization.statics.deleteNote = function(noteId, cb) {
901
+ };
727
902
 
728
- // Delete the note itself along with any references
903
+ var query = buildQuery(data, options);
904
+
905
+ this
906
+ .find(query)
907
+ .populate('merged')
908
+ .select('logoUrl name description related')
909
+ .exec(function(err, orgs) {
910
+
911
+ if (err) return cb(err, null);
912
+ else if (!orgs) return cb(null, null);
913
+ else {
914
+
915
+ // remove related from top level
916
+ if (options.removeRelatedFromTopLevel) orgs = removeRelatedFromTopLevel(orgs);
917
+
918
+ // sort by name
919
+ orgs = _.sortBy(orgs, function(orgs) { return orgs.name; });
920
+
921
+ return cb(null, orgs);
922
+
923
+ }
924
+
925
+ });
729
926
 
927
+ };
928
+
929
+ Organization.statics.setCustomerPortfolio = function setCustomerPortfolio(portfolio) {
730
930
  var self = this;
931
+ self.customerPortfolio = portfolio;
932
+ return self.customerPortfolio;
933
+ };
731
934
 
732
- var removeReferences = function removeReferences(callback) {
733
- self.update({}, {
734
- $pull: { 'notes' : [noteId] }
735
- }, callback);
736
- };
935
+ Organization.statics.stats = function stats(cb) {
936
+ this.collection.stats(cb);
937
+ };
737
938
 
738
- async.series([
739
- Note.delete.bind(Note, noteId),
740
- removeReferences
741
- ], function(err, results) {
742
- return cb(err, null);
743
- });
939
+ Organization.statics.upsert = function(org, username, cb) {
940
+
941
+ if (!org) { return cb(new Error('Organization is required'), null); }
942
+ if (!username) { return cb(new Error('Username is required'), null); }
943
+
944
+ org.entered.by = username;
945
+ org.entered.on = new Date();
946
+
947
+ org.save(cb);
744
948
 
745
949
  };
746
950
 
747
- ///////////////////////////////
951
+
952
+
953
+ ///////////////////////////////////////////////////////////////////////////////////////
954
+ // HOOKS
955
+ // Pre-save: fired on every document before it is saved
956
+ // Post-init: fired on every document when it's returned from a query
957
+ ///////////////////////////////////////////////////////////////////////////////////////
748
958
 
749
959
  Organization.pre('save', function(next) {
750
960
 
@@ -764,75 +974,44 @@ module.exports = function(mongoose, config) {
764
974
 
765
975
  });
766
976
 
767
- Organization.post('init', function() {
977
+ Organization.post('init', function(doc, next) {
768
978
 
769
979
  // Protect private data
770
980
  // Merge public and private data
771
981
 
772
- var self = this;
773
982
  var CUSTOMER_ID = config.CUSTOMER_ID;
774
983
 
775
- if (CUSTOMER_ID == 'GLOBAL_PROCESS') return;
984
+ if (CUSTOMER_ID == 'GLOBAL_PROCESS') return next();
776
985
 
777
- var protectCustomerData = function protectCustomerData() {
986
+ // remove customer details if not viewing self
987
+ if (doc._id.toString() != CUSTOMER_ID) doc.customer = {};
778
988
 
779
- // remove customer details if not viewing self
780
- if (self._id.toString() != CUSTOMER_ID) self.customer = {};
781
-
782
- self.filters = _.reject(self.filters, function(item) { return item.customer != CUSTOMER_ID; });
783
- self.docs = _.reject(self.docs, function(item) { return item.customer != CUSTOMER_ID; });
784
- self.iLevel = _.reject(self.iLevel, function(item) { return item.customer != CUSTOMER_ID; });
785
- self.chairs = _.reject(self.chairs, function(item) { return item.customer != CUSTOMER_ID; });
786
- self.capTable = _.reject(self.capTable, function(item) { return item.customer != CUSTOMER_ID; });
787
-
788
- // investment data
789
- _.each(self.rounds, function(round) {
790
- _.each(round.funds, function(fund) {
791
- if (fund.customer != CUSTOMER_ID) fund.investments = [];
792
- });
793
- });
794
-
795
- };
989
+ doc.filters = _.reject(doc.filters, function(item) { return item.customer != CUSTOMER_ID; });
990
+ doc.docs = _.reject(doc.docs, function(item) { return item.customer != CUSTOMER_ID; });
991
+ doc.iLevel = _.reject(doc.iLevel, function(item) { return item.customer != CUSTOMER_ID; });
992
+ doc.chairs = _.reject(doc.chairs, function(item) { return item.customer != CUSTOMER_ID; });
796
993
 
797
- var mergePublicAndPrivateData = function mergePublicAndPrivateData() {
994
+ // people
995
+ _.each(doc.people, function(person) {
798
996
 
799
- // rounds are public
800
- _.each(self.rounds, function(round) {
997
+ // always include if explicitly public
998
+ if (person.public) return;
801
999
 
802
- // note that public versions of the following data exist at this level:
803
- // preMoneyValuation
804
- // amountRaised
805
- // closingDate
806
- // this data will be overwritten if private data belonging to the current customer is present
1000
+ // if not public, keep it in if org is in the current customer's portfolio. otherwise remove it.
1001
+ // todo
1002
+ console.log(customerPortfolio);
807
1003
 
808
- // participating funds are public
809
- _.each(round.funds, function(fund) {
810
-
811
- if (fund.customer != CUSTOMER_ID) return;
812
- if (!fund.investments || fund.investments.length == 0) return;
813
-
814
- // there can be multiple investments in a single round from the same fund
815
- // use the most recent
816
- var mostRecentInvestment = _.max(fund.investments, function(investment) { return investment.date; });
817
-
818
- // this fund belongs to the current customer
819
- // if we have private data, use it rather than the public data
820
- if (mostRecentInvestment) {
821
- if (mostRecentInvestment.preMoneyValuation) round.preMoneyValuation = mostRecentInvestment.preMoneyValuation;
822
- if (mostRecentInvestment.amountRaised) round.amountRaised = mostRecentInvestment.amountRaised;
823
- if (mostRecentInvestment.closingDate) round.closingDate = mostRecentInvestment.closingDate;
824
- }
1004
+ });
825
1005
 
826
- });
1006
+ return next();
827
1007
 
828
- });
1008
+ });
829
1009
 
830
- };
831
1010
 
832
- protectCustomerData();
833
- mergePublicAndPrivateData();
834
1011
 
835
- });
1012
+ ///////////////////////////////////////////////////////////////////////////////////////
1013
+ // CONFIG
1014
+ ///////////////////////////////////////////////////////////////////////////////////////
836
1015
 
837
1016
  Organization.set('toJSON', { virtuals: true });
838
1017
  Organization.set('autoIndex', true);
@@ -843,6 +1022,6 @@ module.exports = function(mongoose, config) {
843
1022
  var deepPopulate = require('mongoose-deep-populate')(mongoose);
844
1023
  Organization.plugin(deepPopulate);
845
1024
 
846
- mongoose.model('Organization', Organization, 'Organization_new');
1025
+ mongoose.model('Organization', Organization, 'organization_new');
847
1026
 
848
1027
  };