@dhyasama/totem-models 1.0.1 → 1.6.1
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/index.js +18 -6
- package/lib/Account.js +13 -50
- package/lib/CapTable.js +397 -0
- package/lib/Fund.js +56 -12
- package/lib/Investment.js +143 -0
- package/lib/LimitedPartner.js +0 -10
- package/lib/News.js +4 -0
- package/lib/Organization.js +570 -391
- package/lib/Person.js +47 -62
- package/lib/Round.js +805 -0
- package/package.json +4 -3
- package/test/CapTable.js +359 -0
- package/test/Investment.js +180 -0
- package/test/Organization.js +99 -2
- package/test/Person.js +0 -48
- package/test/Round.js +684 -0
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
|
+
|