@dhyasama/totem-models 1.0.0 → 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.
package/lib/Round.js ADDED
@@ -0,0 +1,805 @@
1
+ "use strict";
2
+
3
+ ///////////////////////////////////////////////////////////////////////////////////////
4
+ // ROUNDS
5
+ //
6
+ // This is public data
7
+ //
8
+ // These are not referenced in orgs. If you need the rounds for an org, query this
9
+ // collection separately and stitch together as necessary
10
+ //
11
+ // Each round has zero to many investments which are private. They are conditionally
12
+ // populated based on the current customer.
13
+ //
14
+ // Rounds are rarely deleted. Compare that to cap tables and investments which are
15
+ // wiped on each sheet pull.
16
+ //
17
+ ///////////////////////////////////////////////////////////////////////////////////////
18
+
19
+
20
+
21
+ module.exports = function(mongoose, config) {
22
+
23
+ var Schema = mongoose.Schema;
24
+ var Organization = mongoose.model('Organization');
25
+ var utils = require('@dhyasama/ffvc-utilities');
26
+ var async = require('async');
27
+ var _ = require('underscore');
28
+
29
+ var Round = new Schema({
30
+
31
+ organization: { type: Schema.ObjectId, ref: 'Organization', required: true, index: true },
32
+
33
+ roundName: { type: String, trim: true, required: true },
34
+
35
+ // used when cap table round names are changed and need to be matched to investment data
36
+ oldRoundName: { type: String, trim: true, default: null },
37
+
38
+ // these three values are public and will be used in the absence of overriding private data
39
+ // if private data exists (in the form of investments), the most recent investment data will be used
40
+ preMoneyValuation: { type: Number, default: 0 },
41
+ dollarsRaised: { type: Number, default: 0 },
42
+ closingDate: { type: Date, default: null },
43
+
44
+ // People that participated in this round
45
+ // This is a public list of people that invested
46
+ // Use the addPerson method and removePerson static
47
+ people: [{
48
+ person: { type: Schema.ObjectId, ref: 'Person', index: true },
49
+ _id: false
50
+ }],
51
+
52
+ // Investment vehicles that participated in this round
53
+ // Currently only funds. May expand to SPVs.
54
+ // Recall that a fund belongs to an organization
55
+ // This is a public list of funds, i.e., orgs that invested
56
+ // Use the addVehicle method and removeVehicle static
57
+ vehicles: [{
58
+
59
+ // this is how an investor is referenced, indirectly through the investor's fund
60
+ // note we can have a fund here without a corresponding investment which is how we can show round participants that aren't customers
61
+ fund: { type: Schema.ObjectId, ref: 'Fund', required: true, index: true },
62
+
63
+ // Investments in this round, from this vehicle, which is all private data
64
+ // This data is conditionally populated for the current customer
65
+ // Use the addInvestment method
66
+ // These references are removed by the Investment post-remove hook calling Round.removeInvestment
67
+ investments: [{ type: Schema.ObjectId, ref: 'Investment', required: false }],
68
+
69
+ // computed from investments
70
+ cost: { type: Number, default: 0 },
71
+
72
+ // set at the vehicle level during investment pull
73
+ fairValue: { type: Number, default: 0 },
74
+ valuationDate: { type: Date, default: null },
75
+ unrealized: { type: Number, default: 0 },
76
+ realized: { type: Number, default: 0 },
77
+
78
+ _id: false
79
+
80
+ }]
81
+
82
+ });
83
+
84
+
85
+
86
+ ///////////////////////////////////////////////////////////////////////////////////////
87
+ // HELPERS
88
+ ///////////////////////////////////////////////////////////////////////////////////////
89
+
90
+ var buildPortfolio = function buildPortfolio(rounds, fundIds) {
91
+
92
+ // Given a collection of rounds and funds, construct a portfolio of organizations
93
+
94
+ var portfolio = [];
95
+
96
+ var calculateOrgPerformance = function calculateOrgPerformance(org, rounds) {
97
+
98
+ // sum the cost of each vehicle
99
+ org.cost = _.reduce(rounds, function(memo, round) {
100
+ var sum = _.reduce(round.vehicles, function(memo, vehicle) { return memo + vehicle.cost; }, 0);
101
+ return memo + sum;
102
+ }, 0);
103
+
104
+ // sum the fair value of each vehicle, i.e., the unrealized value
105
+ org.unrealized = _.reduce(rounds, function(memo, round) {
106
+ var sum = _.reduce(round.vehicles, function(memo, vehicle) { return memo + vehicle.fairValue; }, 0);
107
+ return memo + sum;
108
+ }, 0);
109
+
110
+ // sum the realized value (typically from proceeds)
111
+ org.realized = _.reduce(rounds, function(memo, round) {
112
+ var sum = _.reduce(round.vehicles, function(memo, vehicle) { return memo + vehicle.realized; }, 0);
113
+ return memo + sum;
114
+ }, 0);
115
+
116
+ // finally, calc the multiple
117
+ org.multiple = org.cost > 0 ? (org.unrealized + org.realized) / org.cost : 0;
118
+
119
+ return org;
120
+
121
+ };
122
+
123
+ rounds = filterVehicles(rounds, fundIds);
124
+
125
+ // we now have all the rounds the funds have participated in with other fund information removed from each round
126
+
127
+ // Group rounds by org, for two reasons:
128
+ // 1. We're ultimately returning a collection of orgs
129
+ // 2. Each orgs collection of rounds is used to calculate performance
130
+ var grouped = _.groupBy(rounds, function(r) { return r.organization._id; });
131
+
132
+ _.each(grouped, function(rounds) {
133
+
134
+ // construct the org
135
+ var org = {
136
+ _id: rounds[0].organization._id,
137
+ name: rounds[0].organization.name,
138
+ logoUrl: rounds[0].organization.logoUrl,
139
+ description: rounds[0].organization.description,
140
+ status: rounds[0].organization.status,
141
+ preMoneyValuation: rounds[0].preMoneyValuation
142
+ };
143
+
144
+ org = calculateOrgPerformance(org, rounds);
145
+
146
+ portfolio.push(org);
147
+
148
+ });
149
+
150
+ // sort by org name, ascending
151
+ portfolio = _.sortBy(portfolio, function(p) { return p.name.toLowerCase(); });
152
+
153
+ return portfolio;
154
+
155
+ };
156
+
157
+ var checkForUnpopulatedData = function checkForUnpopulatedData(rounds, fnName, cb) {
158
+
159
+ // If one of these things isn't populated, that means we have bad data
160
+ // Fuck shit up so it gets taken care of
161
+
162
+ var vehicles = _.compact(_.flatten(_.pluck(rounds, 'vehicles')));
163
+ var people = _.compact(_.flatten(_.pluck(rounds, 'people')));
164
+ var badData = null;
165
+
166
+ badData = _.find(rounds, function(item) { return !item.organization; });
167
+ if (badData) return new Error(fnName + ' has one or more orgs that is not populated which means there is bad data');
168
+
169
+ badData = _.find(people, function(p) { return !p.person; });
170
+ if (badData) return new Error(fnName + ' has one or more people that is not populated which means there is bad data');
171
+
172
+ badData = _.find(vehicles, function(item) { return !item.fund; });
173
+ if (badData) return new Error(fnName + ' has one or more funds that is not populated which means there is bad data');
174
+
175
+ badData = _.find(vehicles, function(item) { return _.find(item.investments, function(i) { return !i; } ); });
176
+ if (badData) return new Error(fnName + ' has one or more investments that is not populated which means there is bad data');
177
+
178
+ return null;
179
+
180
+ };
181
+
182
+ var filterVehicles = function filterVehicles(rounds, fundIds) {
183
+
184
+ // Given a collection of rounds, each with a collection of vehicles,
185
+ // remove all vehicles that aren't for the given fund ids
186
+
187
+ fundIds = _.map(fundIds, function(id) { return id.toString(); });
188
+
189
+ _.each(rounds, function(r) {
190
+ r.vehicles = _.filter(r.vehicles, function(v) {
191
+ var fundid = utils.isValidObjectId(v.fund) ? v.fund : v.fund._id;
192
+ return fundIds.indexOf(fundid.toString()) >= 0;
193
+ });
194
+ });
195
+
196
+ return rounds;
197
+
198
+ };
199
+
200
+
201
+
202
+ ///////////////////////////////////////////////////////////////////////////////////////
203
+ // VIRTUALS
204
+ // Properties that are not persisted to the database
205
+ ///////////////////////////////////////////////////////////////////////////////////////
206
+
207
+ Round.virtual('cost').get(function () {
208
+ // sums the cost of all vehicles in the round
209
+ var self = this;
210
+ return _.reduce(self.vehicles, function(memo, vehicle) { return memo + vehicle.cost; }, 0);
211
+ });
212
+
213
+ Round.virtual('month').get(function () {
214
+ var self = this;
215
+ var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
216
+ if (!self.closingDate) return 'Unknown';
217
+ else return monthNames[self.closingDate.getMonth()];
218
+ });
219
+
220
+ Round.virtual('value').get(function () {
221
+ // sums the value (fair or realized) of all vehicles in the round
222
+ var self = this;
223
+ return _.reduce(self.vehicles, function(memo, vehicle) {
224
+ if (self.roundName == 'Proceeds') return memo + vehicle.realized; // proceeds are realized and do not have a fair value
225
+ else return memo + vehicle.fairValue;
226
+ }, 0);
227
+ });
228
+
229
+ Round.virtual('year').get(function () {
230
+ var self = this;
231
+ if (!self.closingDate) return 'Unknown';
232
+ else return self.closingDate.getFullYear();
233
+ });
234
+
235
+
236
+
237
+ ///////////////////////////////////////////////////////////////////////////////////////
238
+ // METHODS
239
+ //
240
+ // Methods operate on a single document that has already been returned
241
+ //
242
+ // Note that running a method on a document does not save it
243
+ //
244
+ // I considered making these statics which would be good for one off additions, from
245
+ // admin for example, but the predominant use case is adding via sheet pulls which
246
+ // will add a bunch of things and then just get saved once.
247
+ ///////////////////////////////////////////////////////////////////////////////////////
248
+
249
+ Round.methods.addInvestment = function addInvestment(investment, fund, data) {
250
+
251
+ // Add an investment reference
252
+ // Investment data is private
253
+ // The vehicle off which the investment hangs will be created if it doesn't already exist
254
+ // Note data will overwrite values for an existing vehicle.
255
+
256
+ var self = this;
257
+
258
+ if (!investment) throw new Error('Must supply investment to add');
259
+ if (!fund) throw new Error('Must supply fund to add');
260
+
261
+ // Do our best to accept an object or an objectid
262
+ if (!utils.isValidObjectId(investment)) {
263
+ investment = investment._id;
264
+ if (!utils.isValidObjectId(investment)) throw new Error('Need a valid investment objectid!');
265
+ }
266
+
267
+ // Do our best to accept an object or an objectid
268
+ if (!utils.isValidObjectId(fund)) {
269
+ fund = fund._id;
270
+ if (!utils.isValidObjectId(fund)) throw new Error('Need a valid fund objectid!');
271
+ }
272
+
273
+ // Create or update
274
+ var vehicle = self.addVehicle(fund, data);
275
+ var fid = utils.isValidObjectId(vehicle.fund) ? vehicle.fund : vehicle.fund._id;
276
+
277
+ // The vehicle returned by addVehicle, appears to be a copy not a reference which is odd to me.
278
+ // We need a reference to add the investment, so go get it.
279
+ vehicle = _.find(self.vehicles, function(v) {
280
+ var fundid = utils.isValidObjectId(v.fund) ? v.fund : v.fund._id;
281
+ return fundid.toString() == fid.toString();
282
+ });
283
+
284
+ // bail if not found
285
+ if (!vehicle) throw new Error('Vehicle does not exist and unable to create it');
286
+
287
+ // Check if investment is already added
288
+ var match = _.find(vehicle.investments, function(i) {
289
+ var id = utils.isValidObjectId(i) ? i : i._id;
290
+ return id.toString() == investment.toString();
291
+ });
292
+
293
+ // If not, add it
294
+ if (!match) vehicle.investments.push(investment);
295
+ // note there is no need to compute vehicle cost as it is done during upsert
296
+
297
+ };
298
+
299
+ Round.methods.addPerson = function addPerson(person) {
300
+
301
+ // Add a reference to a person that participated in this round
302
+
303
+ var self = this;
304
+
305
+ if (!person) throw new Error('Must supply person to add');
306
+
307
+ // Do our best to accept an object or an objectid
308
+ if (!utils.isValidObjectId(person)) {
309
+ person = person._id;
310
+ if (!utils.isValidObjectId(person)) throw new Error('Need a valid objectid!');
311
+ }
312
+
313
+ // Check if person is already added
314
+ var match = _.find(self.people, function(existingPerson) {
315
+ var p = existingPerson.person || existingPerson;
316
+ var id = utils.isValidObjectId(p) ? p : p._id;
317
+ return id.toString() == person.toString();
318
+ });
319
+
320
+ // If not, add it
321
+ if (!match) self.people.push({
322
+ person: person
323
+ });
324
+
325
+ };
326
+
327
+ Round.methods.addVehicle = function addVehicle(fund, data) {
328
+
329
+ // Add a vehicle, which is a fund
330
+ // This data is public
331
+ // Private investment data will hang off it
332
+ // Note investment data is not added here
333
+ // Note data will overwrite values for an existing vehicle.
334
+
335
+ var self = this;
336
+
337
+ if (!fund) throw new Error('Must supply fund to add');
338
+
339
+ // Do our best to accept an object or an objectid
340
+ if (!utils.isValidObjectId(fund)) {
341
+ fund = fund._id;
342
+ if (!utils.isValidObjectId(fund)) throw new Error('Need a valid fund objectid!');
343
+ }
344
+
345
+ data = data || {};
346
+ data.fairValue = data.fairValue || 0;
347
+ data.realized = data.realized || 0;
348
+ data.valuationDate = data.valuationDate || null;
349
+
350
+ // Check if vehicle is already added
351
+ var vehicle = _.find(self.vehicles, function(v) {
352
+ var fundid = utils.isValidObjectId(v.fund) ? v.fund : v.fund._id;
353
+ return fundid.toString() == fund;
354
+ });
355
+
356
+ // update data on existing vehicle
357
+ if (vehicle) {
358
+ vehicle.fairValue = data.fairValue;
359
+ vehicle.realized = data.realized;
360
+ vehicle.valuationDate = data.valuationDate;
361
+ }
362
+ // create vehicle
363
+ else {
364
+ vehicle = {
365
+ fund: fund,
366
+ investments: [],
367
+ fairValue: data.fairValue,
368
+ realized: data.realized,
369
+ valuationDate: data.valuationDate
370
+ };
371
+ self.vehicles.push(vehicle);
372
+ }
373
+
374
+ return vehicle;
375
+
376
+ };
377
+
378
+
379
+
380
+ //////////////////////////////////////////////////////
381
+ // STATICS
382
+ // Statics operate on the entire collection
383
+ //////////////////////////////////////////////////////
384
+
385
+ Round.statics.getByFund = function getByFund(fundId, cb) {
386
+
387
+ var self = this;
388
+
389
+ self
390
+ .find({ 'vehicles.fund': fundId })
391
+ .exec(cb);
392
+
393
+ };
394
+
395
+ Round.statics.getByOrg = function getByOrg(orgId, cb) {
396
+
397
+ // Gets all the rounds an org has raised
398
+
399
+ var self = this;
400
+
401
+ self
402
+ .find({
403
+ 'deleted': { $ne: true },
404
+ 'organization': orgId
405
+ })
406
+ .populate('organization', 'name logoUrl')
407
+ .populate('people.person', 'name avatarUrl title')
408
+ .populate('vehicles.fund', 'name shortName')
409
+ .populate({
410
+ path: 'vehicles.investments',
411
+ match: { customer: config.CUSTOMER_ID }
412
+ })
413
+ .exec(function(err, rounds) {
414
+
415
+ if (err) return cb(err, null);
416
+ if (!rounds || rounds.length == 0) return cb(null, []);
417
+
418
+ var error = checkForUnpopulatedData(rounds, 'Round.getByOrg');
419
+ if (error) return cb(error, null);
420
+
421
+ // reverse chronological
422
+ rounds = _.sortBy(rounds, function(r) {
423
+
424
+ var date = r.closingDate;
425
+
426
+ // use investment date in absence of closing date
427
+ if (!date && r.vehicles.length && r.vehicles[0].investments.length) {
428
+ date = r.vehicles[0].investments[0].investmentDate;
429
+ }
430
+
431
+ return date;
432
+
433
+ }).reverse();
434
+
435
+ return cb(null, rounds);
436
+
437
+ });
438
+
439
+ };
440
+
441
+ Round.statics.getByOrgs = function getByOrg(orgIds, cb) {
442
+
443
+ var self = this;
444
+
445
+ self
446
+ .find({
447
+ 'deleted': { $ne: true },
448
+ 'organization': { $in: orgIds }
449
+ })
450
+ .populate('organization', 'name logoUrl')
451
+ .populate('people.person', 'name avatarUrl')
452
+ .populate('vehicles.fund')
453
+ .exec(function(err, rounds) {
454
+
455
+ if (err) return cb(err, null);
456
+ if (!rounds || rounds.length == 0) return cb(null, []);
457
+
458
+ var error = checkForUnpopulatedData(rounds, 'Round.getByOrgs');
459
+ if (error) return cb(error, null);
460
+
461
+ // reverse chronological
462
+ rounds = _.sortBy(rounds, function(r) {
463
+
464
+ var date = r.closingDate;
465
+
466
+ // use investment date in absence of closing date
467
+ if (!date && r.vehicles.length && r.vehicles[0].investments.length) {
468
+ date = r.vehicles[0].investments[0].investmentDate;
469
+ }
470
+
471
+ return date;
472
+
473
+ }).reverse();
474
+
475
+ return cb(null, rounds);
476
+
477
+ });
478
+
479
+ };
480
+
481
+ Round.statics.getFundPerformance = function getFundPerformance(fundId, cb) {
482
+
483
+ var self = this;
484
+
485
+ self
486
+ .find({ 'vehicles.fund': fundId })
487
+ .exec(function(err, rounds) {
488
+
489
+ if (err) return cb(err, null);
490
+ if (!rounds || rounds.length == 0) return cb(null, null);
491
+
492
+ rounds = filterVehicles(rounds, [fundId]);
493
+
494
+ // we now have all the rounds the fund has participated in with other fund information removed from each round
495
+
496
+ var totalFundCost = _.reduce(rounds, function(memo, round) {
497
+ var roundCost = _.reduce(round.vehicles, function(memo, vehicle) { return memo + vehicle.cost; }, 0);
498
+ return memo + roundCost;
499
+ }, 0);
500
+
501
+ var totalUnrealized = _.reduce(rounds, function(memo, round) {
502
+ var roundValue = _.reduce(round.vehicles, function(memo, vehicle) { return memo + vehicle.fairValue; }, 0);
503
+ return memo + roundValue;
504
+ }, 0);
505
+
506
+ var totalRealized = _.reduce(rounds, function(memo, round) {
507
+ var roundValue = _.reduce(round.vehicles, function(memo, vehicle) { return memo + vehicle.realized; }, 0);
508
+ return memo + roundValue;
509
+ }, 0);
510
+
511
+ return cb(null, {
512
+ cost: totalFundCost,
513
+ unrealized: totalUnrealized,
514
+ realized: totalRealized,
515
+ value: totalUnrealized + totalRealized,
516
+ multiple: totalFundCost > 0 ? (totalUnrealized + totalRealized) / totalFundCost : 0
517
+ });
518
+
519
+ });
520
+
521
+ };
522
+
523
+ Round.statics.getPortfolioByFund = function getPortfolioByFund(fundId, cb) {
524
+
525
+ var self = this;
526
+
527
+ self
528
+ .find({ 'vehicles.fund': fundId })
529
+ .select('organization vehicles preMoneyValuation')
530
+ .populate('organization', 'name logoUrl description')
531
+ .populate('vehicles.fund')
532
+ .populate('vehicles.investments')
533
+ .exec(function(err, rounds) {
534
+
535
+ if (err) return cb(err, null);
536
+ if (!rounds || rounds.length == 0) return cb(null, []);
537
+
538
+ var error = checkForUnpopulatedData(rounds, 'Round.getPortfolioByFund');
539
+ if (error) return cb(error, null);
540
+
541
+ return cb(null, buildPortfolio(rounds, [fundId]));
542
+
543
+ });
544
+
545
+ };
546
+
547
+ Round.statics.getPortfolioByFunds = function getPortfolioByFunds(fundIds, cb) {
548
+
549
+ // Returns a list of orgs that have rounds participated in by the given fund ids
550
+ // Note retrieving multiple funds does not calculate fund stats. Use getPortfolioByFund for that.
551
+
552
+ var self = this;
553
+
554
+ self
555
+ .find({ 'vehicles.fund': { $in: fundIds } })
556
+ .select('organization vehicles preMoneyValuation')
557
+ .populate('organization', 'name logoUrl description')
558
+ .populate('vehicles.fund')
559
+ .populate('vehicles.investments')
560
+ .exec(function(err, rounds) {
561
+
562
+ if (err) return cb(err, null);
563
+ if (!rounds || rounds.length == 0) return cb(null, []);
564
+
565
+ var error = checkForUnpopulatedData(rounds, 'Round.getPortfolioByFunds');
566
+ if (error) return cb(error, null);
567
+
568
+ return cb(null, buildPortfolio(rounds, fundIds));
569
+
570
+ });
571
+
572
+ };
573
+
574
+ Round.statics.getPortfolioByPerson = function getPortfolioByPerson(personId, cb) {
575
+
576
+ // Returns a list of orgs that have rounds participated in by the given person id
577
+
578
+ var self = this;
579
+
580
+ self
581
+ .find({ 'people.person': personId })
582
+ .select('organization')
583
+ .populate('organization', 'name logoUrl description')
584
+ .exec(function(err, rounds) {
585
+
586
+ if (err) return cb(err, null);
587
+ if (!rounds || rounds.length == 0) return cb(null, []);
588
+
589
+ var error = checkForUnpopulatedData(rounds, 'Round.getPortfolioByPerson');
590
+ if (error) return cb(error, null);
591
+
592
+ // Return a list of orgs, not a list of rounds
593
+ var portfolio = _.pluck(rounds, 'organization');
594
+ portfolio = _.uniq(portfolio, function(org) { return org.name; });
595
+ portfolio = _.sortBy(portfolio, 'name');
596
+
597
+ return cb(null, portfolio);
598
+
599
+ });
600
+
601
+ };
602
+
603
+ Round.statics.removeInvestment = function(investmentId, cb) {
604
+
605
+ // Do not call this directly. It will leave an orphaned investment document.
606
+ // The preferred way to delete an investment is to delete it directly and this
607
+ // reference will be cleaned up by the investment post-remove hook.
608
+
609
+ // Note that hooks don't work on updates so that makes this more complicated because our pre-save hook to compute
610
+ // cost won't work on a straight update. The workaround is to pull a list of round ids that will have the investment
611
+ // removed, call update to remove the investments from rounds, retrieve the updated rounds and save them which will
612
+ // trigger the pre-save hook and update the cost.
613
+
614
+ var self = this;
615
+
616
+ var getRounds = function getRounds(callback) {
617
+ var query = self.find({ 'vehicles.investments': investmentId });
618
+ query.select('_id');
619
+ query.exec(callback);
620
+ };
621
+
622
+ var updateRounds = function updateRounds(previous, callback) {
623
+ var query = self.update(
624
+ { 'vehicles.investments': investmentId },
625
+ { $pull : { 'vehicles.$.investments': investmentId } },
626
+ { multi : true }
627
+ );
628
+ query.exec(callback);
629
+ };
630
+
631
+ var updateCost = function updateCost(previous, callback) {
632
+
633
+ var ids = _.pluck(previous, '_id');
634
+ var query = self.find({_id: { $in: ids }});
635
+
636
+ query.exec(function (err, docs) {
637
+ async.map(docs, function (doc, saveCallback) {
638
+ doc.save(function (err) {
639
+ saveCallback(err, doc);
640
+ });
641
+ }, callback);
642
+ });
643
+
644
+ };
645
+
646
+ async.waterfall([
647
+ getRounds,
648
+ updateRounds,
649
+ updateCost
650
+ ], cb);
651
+
652
+ };
653
+
654
+ Round.statics.removePerson = function(roundId, personId, cb) {
655
+
656
+ var self = this;
657
+
658
+ self
659
+ .findOneAndUpdate(
660
+ { '_id': roundId },
661
+ { $pull : { 'people': { 'person': personId } } },
662
+ { multi : false }
663
+ )
664
+ .exec(cb);
665
+
666
+ };
667
+
668
+ Round.statics.removeVehicle = function(roundId, fundId, cb) {
669
+
670
+ var self = this;
671
+
672
+ self
673
+ .findOneAndUpdate(
674
+ { '_id': roundId },
675
+ { $pull : { 'vehicles': { 'fund': fundId } } },
676
+ { multi : false }
677
+ )
678
+ .exec(cb);
679
+
680
+ };
681
+
682
+ Round.statics.upsert = function(round, cb) {
683
+
684
+ if (!round) { return cb(new Error('round is required'), null); }
685
+
686
+ var compute = function compute(doc) {
687
+
688
+ // compute for each vehicle
689
+
690
+ _.each(doc.vehicles, function(vehicle) {
691
+
692
+ var investments = vehicle.investments;
693
+
694
+ if (investments.length >= 1) {
695
+ // sum cost of each investment
696
+ vehicle.cost = _.reduce(investments, function(memo, investment){ return memo + investment.cost; }, 0);
697
+ }
698
+ else {
699
+ // no investments = no cost
700
+ vehicle.cost = 0;
701
+ }
702
+
703
+ // proceeds are special in that the cost is actually a realized gain
704
+ if (doc.roundName == 'Proceeds') {
705
+ vehicle.realized = vehicle.cost;
706
+ vehicle.cost = 0;
707
+ vehicle.unrealized = 0;
708
+ vehicle.fairValue = 0;
709
+ }
710
+
711
+ // escrow is also special, except the gains are unrealized
712
+ else if (doc.roundName == 'Escrow') {
713
+ vehicle.unrealized = vehicle.fairValue - vehicle.cost;
714
+ vehicle.realized = 0;
715
+ }
716
+
717
+ });
718
+
719
+ };
720
+
721
+ round.save(function(err, result) {
722
+
723
+ if (err) return cb(err, null);
724
+
725
+ result.populate({
726
+ path: 'vehicles.investments',
727
+ select: 'cost'
728
+ }, function(err, doc) {
729
+
730
+ if (err) return cb(err, null);
731
+ else if (!doc) return cb(null, null);
732
+
733
+ compute(doc);
734
+
735
+ round.save(cb);
736
+
737
+ });
738
+
739
+ });
740
+
741
+ };
742
+
743
+
744
+
745
+ ///////////////////////////////////////////////////////////////////////////////////////
746
+ // HOOKS
747
+ // Pre-save: fired on every document before it is saved
748
+ // Post-init: fired on every document when it's returned from a query
749
+ // Post-remove: fired on every document after it's deleted
750
+ ///////////////////////////////////////////////////////////////////////////////////////
751
+
752
+ Round.post('init', function(doc, next) {
753
+
754
+ var CUSTOMER_ID = config.CUSTOMER_ID;
755
+
756
+ if (CUSTOMER_ID == 'GLOBAL_PROCESS') return next();
757
+
758
+ // note participating people are public as there is no investment, i.e., private, data attached to them
759
+
760
+ // note that public versions of the following data exist at this level:
761
+ // preMoneyValuation
762
+ // amountRaised
763
+ // closingDate
764
+ // this data will be overwritten if private data belonging to the current customer is present
765
+
766
+ // participating vehicles are public
767
+ // merge private data that was matched for customer
768
+ _.each(doc.vehicles, function(vehicle) {
769
+
770
+ if (!vehicle.investments || vehicle.investments.length == 0) return;
771
+
772
+ // there can be multiple investments in a single round from the same fund
773
+ // use the most recent
774
+ var mostRecentInvestment = _.max(vehicle.investments, function(investment) { return investment.investmentDate; });
775
+
776
+ // if we have private data, use it rather than the public data
777
+ if (mostRecentInvestment) {
778
+ if (mostRecentInvestment.preMoneyValuation) doc.preMoneyValuation = mostRecentInvestment.preMoneyValuation;
779
+ if (mostRecentInvestment.dollarsRaised) doc.dollarsRaised = mostRecentInvestment.dollarsRaised;
780
+ if (mostRecentInvestment.closingDate) doc.closingDate = mostRecentInvestment.closingDate;
781
+ }
782
+
783
+ });
784
+
785
+ return next();
786
+
787
+ });
788
+
789
+
790
+
791
+ ///////////////////////////////////////////////////////////////////////////////////////
792
+ // CONFIG
793
+ ///////////////////////////////////////////////////////////////////////////////////////
794
+
795
+ Round.set('toJSON', { virtuals: true });
796
+ Round.set('autoIndex', true);
797
+
798
+ Round.index({ organization: 1, roundName: 1 }, { unique: true, sparse: true });
799
+ Round.on('index', function(err) { console.log('error building Round indexes: ' + err); });
800
+
801
+ mongoose.model('Round', Round);
802
+
803
+ };
804
+
805
+