@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 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
- require('./lib/Account.js')(mongoose, config);
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, default: '' },
20
+ token: { type: String },
22
21
  expiresOn: { type: Date }
23
22
  },
24
23
  setup: {
25
- token: { type: String, default: '' },
24
+ token: { type: String },
26
25
  expiresOn: { type: Date },
27
- complete: { type: Boolean, default: false },
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, default: true },
35
- phone: { type: String, default: '' },
33
+ enabled: { type: Boolean },
34
+ phone: { type: String },
36
35
  country: {
37
- callingCode: { type: String, default: '' },
38
- abbreviation: { type: String, default: '' }
36
+ callingCode: { type: String },
37
+ abbreviation: { type: String }
39
38
  },
40
- verified: { type: Boolean, default: false }
39
+ verified: { type: Boolean }
41
40
  },
42
- admin: { type: Boolean, default: false },
43
- active: { type: Boolean, default: false },
44
- subscriber: { type: Boolean, default: false },
45
- role: { type: String, enum: ['admin', 'lp-full', 'lp-names', 'none'], default: '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, default: false },
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
@@ -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 shortName')
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.getById = function (id, cb) {
73
+ Fund.statics.findBySlugs = function findBySlugs(slugs, cb) {
72
74
  this
73
- .findById(id)
75
+ .find({ 'slug': { $in : slugs }})
74
76
  .exec(cb);
75
77
  };
76
78
 
77
- Fund.statics.getBySlug = function (slug, cb) {
79
+ Fund.statics.getById = function getById(id, cb) {
78
80
  this
79
- .find({slug: slug})
81
+ .findById(id)
80
82
  .exec(cb);
81
83
  };
82
84
 
83
- Fund.statics.findBySlugs = function (slugs, cb) {
85
+ Fund.statics.getBySlug = function getBySlug(slug, cb) {
84
86
  this
85
- .find({ 'slug': { $in : slugs }})
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.list = function (cb) {
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); }