@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.
- 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/index.js
CHANGED
|
@@ -17,12 +17,7 @@ var bootstrap = function(mongoose, config) {
|
|
|
17
17
|
var configHas_CUSTOMER_ID = typeof config['CUSTOMER_ID'] != 'undefined';
|
|
18
18
|
if (!configHas_CUSTOMER_ID) throw new Error('Config is missing CUSTOMER_ID setting');
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
require('./lib/Activity.js')(mongoose, config);
|
|
22
|
-
require('./lib/CalendarEvent.js')(mongoose, config);
|
|
23
|
-
require('./lib/Fund.js')(mongoose, config);
|
|
24
|
-
require('./lib/List.js')(mongoose, config);
|
|
25
|
-
require('./lib/News.js')(mongoose, config);
|
|
20
|
+
// todo - find a solution to load without worrying about order
|
|
26
21
|
|
|
27
22
|
// note must be loaded before limited partner
|
|
28
23
|
// note must be loaded before organization
|
|
@@ -30,11 +25,28 @@ var bootstrap = function(mongoose, config) {
|
|
|
30
25
|
require('./lib/Note.js')(mongoose, config);
|
|
31
26
|
|
|
32
27
|
// organization must be loaded before limited partner
|
|
28
|
+
// organization must be loaded before round
|
|
29
|
+
// organization must be loaded before cap table
|
|
33
30
|
require('./lib/Organization.js')(mongoose, config);
|
|
34
31
|
|
|
32
|
+
// round must be loaded before investment
|
|
33
|
+
// round must be loaded before fund
|
|
34
|
+
require('./lib/Round.js')(mongoose, config);
|
|
35
|
+
|
|
36
|
+
// fund must be loaded before limited partner
|
|
37
|
+
require('./lib/Fund.js')(mongoose, config);
|
|
38
|
+
|
|
35
39
|
// limited partner must be loaded before person
|
|
36
40
|
require('./lib/LimitedPartner.js')(mongoose, config);
|
|
37
41
|
|
|
42
|
+
// none of these need to be loaded before anything
|
|
43
|
+
require('./lib/Account.js')(mongoose, config);
|
|
44
|
+
require('./lib/Activity.js')(mongoose, config);
|
|
45
|
+
require('./lib/CalendarEvent.js')(mongoose, config);
|
|
46
|
+
require('./lib/CapTable.js')(mongoose, config);
|
|
47
|
+
require('./lib/Investment.js')(mongoose, config);
|
|
48
|
+
require('./lib/List.js')(mongoose, config);
|
|
49
|
+
require('./lib/News.js')(mongoose, config);
|
|
38
50
|
require('./lib/Person.js')(mongoose, config);
|
|
39
51
|
|
|
40
52
|
};
|
package/lib/Account.js
CHANGED
|
@@ -16,36 +16,35 @@ module.exports = function(mongoose, config) {
|
|
|
16
16
|
username: { type: String, required: true, unique: true },
|
|
17
17
|
password: { type: String, default: '' },
|
|
18
18
|
email: { type: String, required: true, unique: true },
|
|
19
|
-
avatar: { type: String, default: '/media/abstract-cubes.png' },
|
|
20
19
|
reset: {
|
|
21
|
-
token: { type: String
|
|
20
|
+
token: { type: String },
|
|
22
21
|
expiresOn: { type: Date }
|
|
23
22
|
},
|
|
24
23
|
setup: {
|
|
25
|
-
token: { type: String
|
|
24
|
+
token: { type: String },
|
|
26
25
|
expiresOn: { type: Date },
|
|
27
|
-
complete: { type: Boolean
|
|
26
|
+
complete: { type: Boolean},
|
|
28
27
|
terms: {
|
|
29
28
|
raw: { type: String },
|
|
30
29
|
hash: { type: String }
|
|
31
30
|
}
|
|
32
31
|
},
|
|
33
32
|
twoFactorAuth: {
|
|
34
|
-
enabled: { type: Boolean
|
|
35
|
-
phone: { type: String
|
|
33
|
+
enabled: { type: Boolean },
|
|
34
|
+
phone: { type: String },
|
|
36
35
|
country: {
|
|
37
|
-
callingCode: { type: String
|
|
38
|
-
abbreviation: { type: String
|
|
36
|
+
callingCode: { type: String },
|
|
37
|
+
abbreviation: { type: String }
|
|
39
38
|
},
|
|
40
|
-
verified: { type: Boolean
|
|
39
|
+
verified: { type: Boolean }
|
|
41
40
|
},
|
|
42
|
-
admin: { type: Boolean
|
|
43
|
-
active: { type: Boolean
|
|
44
|
-
subscriber: { type: Boolean
|
|
45
|
-
role: { type: String, enum: ['admin', 'lp-full', 'lp-names', 'none']
|
|
41
|
+
admin: { type: Boolean },
|
|
42
|
+
active: { type: Boolean },
|
|
43
|
+
subscriber: { type: Boolean },
|
|
44
|
+
role: { type: String, enum: ['admin', 'lp-full', 'lp-names', 'none'] },
|
|
46
45
|
|
|
47
46
|
// Enable this and iLevel links will show on portfolio pages
|
|
48
|
-
iLevelUser: { type: Boolean
|
|
47
|
+
iLevelUser: { type: Boolean },
|
|
49
48
|
|
|
50
49
|
person: {
|
|
51
50
|
type: Schema.ObjectId,
|
|
@@ -252,42 +251,6 @@ module.exports = function(mongoose, config) {
|
|
|
252
251
|
|
|
253
252
|
};
|
|
254
253
|
|
|
255
|
-
//Account.pre('save', function(next) {
|
|
256
|
-
//
|
|
257
|
-
// // Encrypt sensitive data
|
|
258
|
-
//
|
|
259
|
-
// if (this.auth) {
|
|
260
|
-
// if (this.auth.google) {
|
|
261
|
-
// if (this.auth.google.accessToken) this.auth.google.accessToken = crypto.encrypt(this.auth.google.accessToken);
|
|
262
|
-
// if (this.auth.google.refreshToken) this.auth.google.refreshToken = crypto.encrypt(this.auth.google.refreshToken);
|
|
263
|
-
// if (this.auth.google.params) {
|
|
264
|
-
// if (this.auth.google.params.access_token) this.auth.google.params.access_token = crypto.encrypt(this.auth.google.params.access_token);
|
|
265
|
-
// if (this.auth.google.params.id_token) this.auth.google.params.id_token = crypto.encrypt(this.auth.google.params.id_token);
|
|
266
|
-
// }
|
|
267
|
-
// }
|
|
268
|
-
// }
|
|
269
|
-
//
|
|
270
|
-
// next();
|
|
271
|
-
//
|
|
272
|
-
//});
|
|
273
|
-
//
|
|
274
|
-
//Account.post('init', function() {
|
|
275
|
-
//
|
|
276
|
-
// // Decrypt sensitive data
|
|
277
|
-
//
|
|
278
|
-
// if (this.auth) {
|
|
279
|
-
// if (this.auth.google) {
|
|
280
|
-
// if (this.auth.google.accessToken) this.auth.google.accessToken = crypto.decrypt(this.auth.google.accessToken);
|
|
281
|
-
// if (this.auth.google.refreshToken) this.auth.google.refreshToken = crypto.decrypt(this.auth.google.refreshToken);
|
|
282
|
-
// if (this.auth.google.params) {
|
|
283
|
-
// if (this.auth.google.params.access_token) this.auth.google.params.access_token = crypto.decrypt(this.auth.google.params.access_token);
|
|
284
|
-
// if (this.auth.google.params.id_token) this.auth.google.params.id_token = crypto.decrypt(this.auth.google.params.id_token);
|
|
285
|
-
// }
|
|
286
|
-
// }
|
|
287
|
-
// }
|
|
288
|
-
//
|
|
289
|
-
//});
|
|
290
|
-
|
|
291
254
|
Account.post('remove', function(doc) {
|
|
292
255
|
// CalendarEvent.account
|
|
293
256
|
// Person.account
|
package/lib/CapTable.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
4
|
+
// NOTES
|
|
5
|
+
//
|
|
6
|
+
// This is private data
|
|
7
|
+
//
|
|
8
|
+
// Unique-keyed on organization and customer combo
|
|
9
|
+
//
|
|
10
|
+
// These are not referenced in orgs. If you need the cap table for an org, query this
|
|
11
|
+
// collection separately and stitch together as necessary
|
|
12
|
+
//
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
|
|
15
|
+
module.exports = function(mongoose, config) {
|
|
16
|
+
|
|
17
|
+
var Schema = mongoose.Schema;
|
|
18
|
+
var Organization = mongoose.model('Organization');
|
|
19
|
+
var _ = require('underscore');
|
|
20
|
+
var async = require('async');
|
|
21
|
+
|
|
22
|
+
var CapTable = new Schema({
|
|
23
|
+
|
|
24
|
+
// the org this cap table is for
|
|
25
|
+
organization: { type: Schema.ObjectId, ref: 'Organization', required: true },
|
|
26
|
+
|
|
27
|
+
// each customer has their own cap table
|
|
28
|
+
customer: { type: Schema.ObjectId, ref: 'Organization', required: true },
|
|
29
|
+
|
|
30
|
+
// preferably the date in the cap table document, not the date added to our system
|
|
31
|
+
asOfDate: { type: Date, required: true, default: Date.now },
|
|
32
|
+
|
|
33
|
+
// so customers can download full file
|
|
34
|
+
originalDocumentUrl: { type: String, trim: true },
|
|
35
|
+
|
|
36
|
+
// single pool of unissued options for the org
|
|
37
|
+
unissuedOptions: { type: Number, default: 0 },
|
|
38
|
+
|
|
39
|
+
// computed on save (via pre-save hook); don't set directly;
|
|
40
|
+
fullyDiluted: { type: Number, default: 0 },
|
|
41
|
+
|
|
42
|
+
stakeholders: [{
|
|
43
|
+
|
|
44
|
+
// Note that name will always be pulled from sheet and used by default when rendering a cap table.
|
|
45
|
+
// The stakeholder will be programmatically linked to a person or fund in the event of an exact text match on person.name.full or fund.name.
|
|
46
|
+
// Manual creation and linking of stakeholder and entity will happen within Admin.
|
|
47
|
+
// The corollary of this is that neither person nor fund is required and the name from the sheet will be used in their absence.
|
|
48
|
+
|
|
49
|
+
// verbatim text from the cap table
|
|
50
|
+
name: { type: String, trim: true, required: true },
|
|
51
|
+
|
|
52
|
+
// link stakeholder to a person in our system
|
|
53
|
+
person: {
|
|
54
|
+
type: Schema.ObjectId,
|
|
55
|
+
ref: 'Person',
|
|
56
|
+
default: null,
|
|
57
|
+
required: false,
|
|
58
|
+
validate: {
|
|
59
|
+
validator: function(v) {
|
|
60
|
+
if (v && this.fund) return false;
|
|
61
|
+
else return true;
|
|
62
|
+
},
|
|
63
|
+
message: 'A stakeholder can be linked to a person or a fund, but not both'
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// link stakeholder to a fund in our system
|
|
68
|
+
fund: {
|
|
69
|
+
type: Schema.ObjectId,
|
|
70
|
+
ref: 'Fund',
|
|
71
|
+
default: null,
|
|
72
|
+
required: false,
|
|
73
|
+
validate: {
|
|
74
|
+
validator: function(v) {
|
|
75
|
+
if (v && this.person) return false;
|
|
76
|
+
else return true;
|
|
77
|
+
},
|
|
78
|
+
message: 'A stakeholder can be linked to a person or a fund, but not both'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Put stakes in a bucket for their type.
|
|
83
|
+
// Note we are grouping all stakes for the current stakeholder.
|
|
84
|
+
|
|
85
|
+
common: [{
|
|
86
|
+
round: { type: String, trim: true, required: true },
|
|
87
|
+
shares: { type: Number, default: 0 }
|
|
88
|
+
}],
|
|
89
|
+
|
|
90
|
+
options: [{
|
|
91
|
+
round: { type: String, trim: true, required: true },
|
|
92
|
+
shares: { type: Number, default: 0 }
|
|
93
|
+
}],
|
|
94
|
+
|
|
95
|
+
preferred: [{
|
|
96
|
+
round: { type: String, trim: true, required: true },
|
|
97
|
+
shares: { type: Number, default: 0 }
|
|
98
|
+
}],
|
|
99
|
+
|
|
100
|
+
warrants: [{
|
|
101
|
+
round: { type: String, trim: true, required: true },
|
|
102
|
+
shares: { type: Number, default: 0 }
|
|
103
|
+
}],
|
|
104
|
+
|
|
105
|
+
// computed on save (via pre-save hook); don't set directly;
|
|
106
|
+
fullyDiluted: { type: Number, default: 0 },
|
|
107
|
+
|
|
108
|
+
// computed on save (via pre-save hook); don't set directly;
|
|
109
|
+
fullyDilutedPercentage: { type: Number, default: 0 }
|
|
110
|
+
|
|
111
|
+
}],
|
|
112
|
+
|
|
113
|
+
entered: {
|
|
114
|
+
by: { type: String, trim: true, required: true },
|
|
115
|
+
on: { type: Date, default: Date.now, required: true }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
123
|
+
// VIRTUALS
|
|
124
|
+
// Properties that are not persisted to the database
|
|
125
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
126
|
+
|
|
127
|
+
CapTable.virtual('stakeholdersByRound').get(function () {
|
|
128
|
+
|
|
129
|
+
// Returns list of tuples, one for each round on this cap table along with participants in each round
|
|
130
|
+
|
|
131
|
+
var self = this;
|
|
132
|
+
|
|
133
|
+
var buildRoundList = function buildRoundList(stakeholders) {
|
|
134
|
+
|
|
135
|
+
var roundList = [];
|
|
136
|
+
|
|
137
|
+
_.each(stakeholders, function(stakeholder) {
|
|
138
|
+
roundList = roundList.concat(_.pluck(stakeholder.common, 'round'));
|
|
139
|
+
roundList = roundList.concat(_.pluck(stakeholder.options, 'round'));
|
|
140
|
+
roundList = roundList.concat(_.pluck(stakeholder.preferred, 'round'));
|
|
141
|
+
roundList = roundList.concat(_.pluck(stakeholder.warrants, 'round'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
roundList = _.uniq(roundList);
|
|
145
|
+
|
|
146
|
+
return roundList;
|
|
147
|
+
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
var matchStakeholderToRound = function matchStakeholderToRound(arr, participants, name, round) {
|
|
151
|
+
var match = _.find(arr, function(i) { return round == i.round; });
|
|
152
|
+
if (match) { participants.push(name); }
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
var roundList = buildRoundList(self.stakeholders);
|
|
156
|
+
|
|
157
|
+
var stakeholders = [];
|
|
158
|
+
|
|
159
|
+
_.each(roundList, function(round) {
|
|
160
|
+
|
|
161
|
+
// List of each participant in this round
|
|
162
|
+
var participants = [];
|
|
163
|
+
|
|
164
|
+
_.each(self.stakeholders, function(stakeholder) {
|
|
165
|
+
matchStakeholderToRound(stakeholder.common, participants, stakeholder.name, round);
|
|
166
|
+
matchStakeholderToRound(stakeholder.options, participants, stakeholder.name, round);
|
|
167
|
+
matchStakeholderToRound(stakeholder.preferred, participants, stakeholder.name, round);
|
|
168
|
+
matchStakeholderToRound(stakeholder.warrants, participants, stakeholder.name, round);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
participants = _.uniq(participants);
|
|
172
|
+
|
|
173
|
+
// return tuple of round name and participants in the round
|
|
174
|
+
stakeholders.push({
|
|
175
|
+
round: round,
|
|
176
|
+
participants: participants
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return stakeholders;
|
|
182
|
+
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
188
|
+
// METHODS
|
|
189
|
+
// Methods operate on a single document that has already been returned
|
|
190
|
+
// Note that running a method on a document does not save it
|
|
191
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
192
|
+
|
|
193
|
+
CapTable.methods.mapFundsToOrgs = function mapFundsToOrgs(cb) {
|
|
194
|
+
|
|
195
|
+
// Check each stakeholder to see if it is a fund, and if so, get the org the fund belongs to
|
|
196
|
+
|
|
197
|
+
var self = this;
|
|
198
|
+
var map = [];
|
|
199
|
+
|
|
200
|
+
var getOrg = function getOrg(stakeholder, callback) {
|
|
201
|
+
|
|
202
|
+
if (!stakeholder.fund) return callback();
|
|
203
|
+
|
|
204
|
+
var fundid = stakeholder.fund._id || stakeholder.fund;
|
|
205
|
+
|
|
206
|
+
Organization.getByFund(fundid, function(err, result) {
|
|
207
|
+
|
|
208
|
+
if (err) return callback(err);
|
|
209
|
+
else if (!result) return callback();
|
|
210
|
+
|
|
211
|
+
map.push({
|
|
212
|
+
fund: fundid,
|
|
213
|
+
org: result
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return callback();
|
|
217
|
+
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
async.each(self.stakeholders, getOrg, function(err) {
|
|
223
|
+
if (err) return cb(err, null);
|
|
224
|
+
else return cb(null, map);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
//////////////////////////////////////////////////////
|
|
232
|
+
// STATICS
|
|
233
|
+
// Statics operate on the entire collection
|
|
234
|
+
//////////////////////////////////////////////////////
|
|
235
|
+
|
|
236
|
+
CapTable.statics.delete = function(id, cb) {
|
|
237
|
+
var self = this;
|
|
238
|
+
self.findByIdAndRemove(id, cb);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
CapTable.statics.deleteAllForCustomer = function(customerId, cb) {
|
|
242
|
+
var self = this;
|
|
243
|
+
self.remove({ customer: customerId }, cb);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
CapTable.statics.getAllForCustomer = function getAllForCustomer(customerId, cb) {
|
|
247
|
+
|
|
248
|
+
var self = this;
|
|
249
|
+
|
|
250
|
+
self
|
|
251
|
+
.find({customer: customerId })
|
|
252
|
+
.populate('organization', 'name logoUrl')
|
|
253
|
+
.exec(cb);
|
|
254
|
+
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
CapTable.statics.getForCustomer = function getForCustomer(orgId, customerId, cb) {
|
|
258
|
+
|
|
259
|
+
var self = this;
|
|
260
|
+
|
|
261
|
+
self
|
|
262
|
+
.findOne({customer: customerId, organization: orgId })
|
|
263
|
+
.populate('stakeholders.person', 'name avatarUrl title')
|
|
264
|
+
.populate('stakeholders.fund', 'name')
|
|
265
|
+
.exec(cb);
|
|
266
|
+
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
CapTable.statics.insert = function(capTable, username, cb) {
|
|
270
|
+
|
|
271
|
+
var self = this;
|
|
272
|
+
|
|
273
|
+
if (!capTable) { return cb(new Error('capTable is required'), null); }
|
|
274
|
+
if (!username) { return cb(new Error('username is required'), null); }
|
|
275
|
+
|
|
276
|
+
capTable.constructor.getForCustomer(capTable.organization, capTable.customer, function(err, result) {
|
|
277
|
+
|
|
278
|
+
if (err) return cb(err, null);
|
|
279
|
+
if (result) return cb(new Error('A cap table already exists for this customer/org combo'), null);
|
|
280
|
+
|
|
281
|
+
capTable.entered.by = username;
|
|
282
|
+
capTable.entered.on = new Date();
|
|
283
|
+
|
|
284
|
+
self.create(capTable, cb);
|
|
285
|
+
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
CapTable.statics.replace = function(capTable, username, cb) {
|
|
291
|
+
|
|
292
|
+
var self = this;
|
|
293
|
+
|
|
294
|
+
if (!capTable) { return cb(new Error('capTable is required'), null); }
|
|
295
|
+
if (!username) { return cb(new Error('username is required'), null); }
|
|
296
|
+
|
|
297
|
+
capTable.entered.by = username;
|
|
298
|
+
capTable.entered.on = new Date();
|
|
299
|
+
|
|
300
|
+
self.update(
|
|
301
|
+
{ '_id': capTable._id },
|
|
302
|
+
capTable,
|
|
303
|
+
{ upsert: false, multi: false, overwrite: true },
|
|
304
|
+
cb
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
CapTable.statics.updateOriginalDocumentUrl = function updateOriginalDocumentUrl(id, originalDocumentUrl, cb) {
|
|
310
|
+
|
|
311
|
+
var self = this;
|
|
312
|
+
|
|
313
|
+
self
|
|
314
|
+
.findByIdAndUpdate(
|
|
315
|
+
id,
|
|
316
|
+
{ $set: { originalDocumentUrl: originalDocumentUrl } },
|
|
317
|
+
{ new: true, upsert: false },
|
|
318
|
+
cb
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
326
|
+
// HOOKS
|
|
327
|
+
// Pre-save: fired on every document before it is saved
|
|
328
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
329
|
+
|
|
330
|
+
CapTable.pre('save', function(next) {
|
|
331
|
+
|
|
332
|
+
var self = this;
|
|
333
|
+
|
|
334
|
+
// Sum each stakeholders fully diluted shares
|
|
335
|
+
_.each(self.stakeholders, function(stakeholder) {
|
|
336
|
+
|
|
337
|
+
var common = _.reduce(stakeholder.common, function(memo, i) {
|
|
338
|
+
return memo + i.shares;
|
|
339
|
+
}, 0);
|
|
340
|
+
|
|
341
|
+
var options = _.reduce(stakeholder.options, function(memo, i) {
|
|
342
|
+
return memo + i.shares;
|
|
343
|
+
}, 0);
|
|
344
|
+
|
|
345
|
+
var preferred = _.reduce(stakeholder.preferred, function(memo, i) {
|
|
346
|
+
return memo + i.shares;
|
|
347
|
+
}, 0);
|
|
348
|
+
|
|
349
|
+
var warrants = _.reduce(stakeholder.warrants, function(memo, i) {
|
|
350
|
+
return memo + i.shares;
|
|
351
|
+
}, 0);
|
|
352
|
+
|
|
353
|
+
stakeholder.fullyDiluted = common + options + preferred + warrants;
|
|
354
|
+
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Sum total fully diluted
|
|
358
|
+
self.fullyDiluted = _.reduce(self.stakeholders, function(memo, stakeholder) {
|
|
359
|
+
return memo + stakeholder.fullyDiluted;
|
|
360
|
+
}, 0);
|
|
361
|
+
self.fullyDiluted = self.fullyDiluted + self.unissuedOptions;
|
|
362
|
+
|
|
363
|
+
// Calculate each stakeholders fully diluted percentage
|
|
364
|
+
_.each(self.stakeholders, function(stakeholder) {
|
|
365
|
+
if (self.fullyDiluted == 0) { stakeholder.fullyDiluted = 0; }
|
|
366
|
+
else { stakeholder.fullyDilutedPercentage = ((stakeholder.fullyDiluted / self.fullyDiluted) * 100).toFixed(2); }
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return next();
|
|
370
|
+
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
CapTable.post('init', function(doc, next) {
|
|
374
|
+
|
|
375
|
+
doc.stakeholders = _.sortBy(doc.stakeholders, 'fullyDilutedPercentage').reverse();
|
|
376
|
+
|
|
377
|
+
return next();
|
|
378
|
+
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
384
|
+
// CONFIG
|
|
385
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
386
|
+
|
|
387
|
+
CapTable.set('toJSON', { virtuals: true });
|
|
388
|
+
CapTable.set('autoIndex', true);
|
|
389
|
+
|
|
390
|
+
CapTable.index({ organization: 1, customer: 1 }, { unique: true, sparse: true });
|
|
391
|
+
CapTable.on('index', function(err) { console.log('error building CapTable indexes: ' + err); });
|
|
392
|
+
|
|
393
|
+
mongoose.model('CapTable', CapTable);
|
|
394
|
+
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
|
package/lib/Fund.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = function(mongoose, config) {
|
|
4
4
|
|
|
5
|
-
var Schema = mongoose.Schema
|
|
5
|
+
var Schema = mongoose.Schema;
|
|
6
|
+
var Round = mongoose.model('Round');
|
|
7
|
+
var _ = require('underscore');
|
|
6
8
|
var env = process.env.NODE_ENV || 'development';
|
|
7
9
|
|
|
8
10
|
var Fund = new Schema({
|
|
9
11
|
|
|
10
|
-
name: { type: String, required: true },
|
|
12
|
+
name: { type: String, required: true, unique: true },
|
|
11
13
|
slug: { type: String, required: true, unique: true },
|
|
12
14
|
shortName: { type: String, required: true },
|
|
13
15
|
spv: { type: Boolean, default: false },
|
|
@@ -68,37 +70,79 @@ module.exports = function(mongoose, config) {
|
|
|
68
70
|
|
|
69
71
|
///////////////////////////////////////
|
|
70
72
|
|
|
71
|
-
Fund.statics.
|
|
73
|
+
Fund.statics.findBySlugs = function findBySlugs(slugs, cb) {
|
|
72
74
|
this
|
|
73
|
-
.
|
|
75
|
+
.find({ 'slug': { $in : slugs }})
|
|
74
76
|
.exec(cb);
|
|
75
77
|
};
|
|
76
78
|
|
|
77
|
-
Fund.statics.
|
|
79
|
+
Fund.statics.getById = function getById(id, cb) {
|
|
78
80
|
this
|
|
79
|
-
.
|
|
81
|
+
.findById(id)
|
|
80
82
|
.exec(cb);
|
|
81
83
|
};
|
|
82
84
|
|
|
83
|
-
Fund.statics.
|
|
85
|
+
Fund.statics.getBySlug = function getBySlug(slug, cb) {
|
|
84
86
|
this
|
|
85
|
-
.find({
|
|
87
|
+
.find({slug: slug})
|
|
86
88
|
.exec(cb);
|
|
87
89
|
};
|
|
88
90
|
|
|
89
|
-
Fund.statics.getCount = function (cb) {
|
|
91
|
+
Fund.statics.getCount = function getCount(cb) {
|
|
90
92
|
this
|
|
91
93
|
.count({})
|
|
92
94
|
.exec(cb);
|
|
93
95
|
};
|
|
94
96
|
|
|
95
|
-
Fund.statics.
|
|
97
|
+
Fund.statics.getPerformance = function getPerformance(id, cb) {
|
|
98
|
+
|
|
99
|
+
var getLatestVehicle = function(rounds) {
|
|
100
|
+
|
|
101
|
+
var latestVehicles = [];
|
|
102
|
+
|
|
103
|
+
_.each(rounds, function (r) {
|
|
104
|
+
latestVehicles.push(_.max(r.vehicles, function (v) {
|
|
105
|
+
return v.valuationDate;
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
latestVehicles = _.compact(latestVehicles);
|
|
110
|
+
|
|
111
|
+
return _.max(latestVehicles, function (v) { return v.valuationDate; });
|
|
112
|
+
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
Round.getByFund(id, function(err, rounds) {
|
|
116
|
+
|
|
117
|
+
if (err) return cb(err, null);
|
|
118
|
+
if (!rounds || rounds.length == 0) return cb(null, []);
|
|
119
|
+
|
|
120
|
+
// filter vehicles down to this fund
|
|
121
|
+
_.each(rounds, function (r) {
|
|
122
|
+
r.vehicles = _.filter(r.vehicles, function (v) {
|
|
123
|
+
return v.fund._id.toString() == id.toString();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
var latestVehicle = getLatestVehicle(rounds);
|
|
128
|
+
|
|
129
|
+
return cb(null, {
|
|
130
|
+
cost: latestVehicle.cost,
|
|
131
|
+
fairValue: latestVehicle.fairValue,
|
|
132
|
+
multiple: latestVehicle.multiple
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
Fund.statics.list = function list(cb) {
|
|
96
140
|
this
|
|
97
141
|
.find()
|
|
98
142
|
.exec(cb);
|
|
99
143
|
};
|
|
100
144
|
|
|
101
|
-
Fund.statics.search = function (query, options, cb) {
|
|
145
|
+
Fund.statics.search = function search(query, options, cb) {
|
|
102
146
|
|
|
103
147
|
if (!cb) throw new Error('cb is required');
|
|
104
148
|
if (!query) return cb(null, []);
|
|
@@ -115,7 +159,7 @@ module.exports = function(mongoose, config) {
|
|
|
115
159
|
|
|
116
160
|
};
|
|
117
161
|
|
|
118
|
-
Fund.statics.upsert = function(fund, username, cb) {
|
|
162
|
+
Fund.statics.upsert = function upsert(fund, username, cb) {
|
|
119
163
|
|
|
120
164
|
if (!fund) { return cb(new Error('Fund is required'), null); }
|
|
121
165
|
if (!username) { return cb(new Error('Username is required'), null); }
|