@dhyasama/totem-models 12.24.0 → 12.25.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/Account.js CHANGED
@@ -77,14 +77,6 @@ module.exports = function(mongoose, config) {
77
77
  sendSystemEmails: { type: Boolean, default: false },
78
78
  },
79
79
 
80
- // Granola native REST API integration. Keys are per-user, so they live on
81
- // the account: when active, webhooks polls this user's Granola notes using
82
- // apiKey (see webhooks/src/granola). Opt-in — only synced when a key is set.
83
- granola: {
84
- active: { type: Boolean, default: false },
85
- apiKey: { type: String, trim: true, default: '' }
86
- },
87
-
88
80
  summary: {
89
81
  active: { type: Boolean, default: false }
90
82
  }
package/lib/Financials.js CHANGED
@@ -76,6 +76,9 @@ module.exports = function(mongoose, config) {
76
76
 
77
77
  uuid: { type: String },
78
78
 
79
+ // DEPRECATED: currency/scale now live per-metric (see metrics[].currency / metrics[].scale).
80
+ // Retained during the dual-write transition as the fallback default for un-migrated metrics;
81
+ // remove once all read paths resolve currency/scale per metric.
79
82
  currency: { type: String, default: 'USD' },
80
83
  scale: { type: Number, enum: [1, 1000, 1000000, 1000000000, 1000000000000], default: 1 },
81
84
 
@@ -145,6 +148,11 @@ module.exports = function(mongoose, config) {
145
148
  }
146
149
  }],
147
150
  verified: { type: Boolean, default: false },
151
+ // Per-metric currency/scale. Intentionally NO defaults: an undefined value means
152
+ // "not yet migrated" and read paths fall back to the (deprecated) snapshot-level
153
+ // currency/scale. See scripts/migrate-financials-schema.js for the backfill.
154
+ currency: { type: String, trim: true },
155
+ scale: { type: Number, enum: [1, 1000, 1000000, 1000000000, 1000000000000] },
148
156
  history: [{
149
157
  _id: false,
150
158
  timestamp: { type: Date, required: true, default: Date.now },
@@ -156,6 +164,8 @@ module.exports = function(mongoose, config) {
156
164
  value: { type: Number },
157
165
  breakdown: { type: Schema.Types.Mixed },
158
166
  calculation: { type: String, trim: true },
167
+ currency: { type: String, trim: true },
168
+ scale: { type: Number, enum: [1, 1000, 1000000, 1000000000, 1000000000000] },
159
169
  sources: [{
160
170
  _id: false,
161
171
  model: { type: String, enum: ['Document', 'Meeting', 'Message', 'Note'] },
@@ -443,6 +453,8 @@ module.exports = function(mongoose, config) {
443
453
  value: currentMetric.value != null ? currentMetric.value : null,
444
454
  breakdown: currentMetric.breakdown != null ? currentMetric.breakdown : null,
445
455
  calculation: currentMetric.calculation || null,
456
+ currency: currentMetric.currency != null ? currentMetric.currency : null,
457
+ scale: currentMetric.scale != null ? currentMetric.scale : null,
446
458
  sources: currentMetric.sources || [],
447
459
  verified: currentMetric.verified != null ? currentMetric.verified : null,
448
460
  history: currentMetric.history || []
@@ -459,6 +471,11 @@ module.exports = function(mongoose, config) {
459
471
  'snapshots.$.metrics.$[metric].verified': metric.verified
460
472
  };
461
473
 
474
+ // Persist per-metric currency/scale only when the caller actually supplies a value, so a
475
+ // caller that omits them (or passes null) never clobbers an already-migrated metric.
476
+ if (metric.currency != null) { $set['snapshots.$.metrics.$[metric].currency'] = metric.currency; }
477
+ if (metric.scale != null) { $set['snapshots.$.metrics.$[metric].scale'] = metric.scale; }
478
+
462
479
  // Save previous state if the metric already exists
463
480
  if (previousState) {
464
481
  $set['snapshots.$.metrics.$[metric].previous'] = previousState;
@@ -564,7 +581,7 @@ module.exports = function(mongoose, config) {
564
581
  const $set = {};
565
582
  const $unset = { [prefix + '.previous']: 1 };
566
583
 
567
- const fields = ['value', 'breakdown', 'calculation', 'sources', 'verified'];
584
+ const fields = ['value', 'breakdown', 'calculation', 'currency', 'scale', 'sources', 'verified'];
568
585
  fields.forEach(field => {
569
586
  if (prev[field] != null) {
570
587
  $set[prefix + '.' + field] = prev[field];
@@ -225,6 +225,13 @@ module.exports = function(mongoose, config) {
225
225
  active: { type: Boolean, default: false }
226
226
  },
227
227
 
228
+ // Granola native REST API integration. When active, webhooks polls this
229
+ // customer's Granola notes using apiKey (see webhooks/src/granola).
230
+ granola: {
231
+ active: { type: Boolean, default: false },
232
+ apiKey: { type: String, trim: true }
233
+ },
234
+
228
235
  sheet: {
229
236
  id: { type: String, trim: true, unique: true, partialFilterExpression : { type :"string" } },
230
237
  format: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhyasama/totem-models",
3
- "version": "12.24.0",
3
+ "version": "12.25.0",
4
4
  "author": "Jason Reynolds",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Models for Totem platform",
@@ -21,6 +21,10 @@
21
21
  * 10. Link metrics to their source financial statement documents (Income Statement,
22
22
  * Balance Sheet, Cash Flow Statement) when available
23
23
  * 11. Remove projections array from root level
24
+ * 12. Backfill per-metric currency/scale from the (now-deprecated) snapshot-level
25
+ * currency/scale. Idempotent: metrics that already carry their own are left untouched.
26
+ * Defaults to 'USD' / 1 when the snapshot has neither. Part of the snapshot→metric
27
+ * currency migration (dual-write transition; snapshot fields retained as fallback).
24
28
  *
25
29
  * Usage:
26
30
  * node scripts/migrate-financials-schema.js <customerId> [--config <configPath>] [--dry-run]
@@ -81,6 +85,14 @@ if (configPath) {
81
85
  dbUri = config.db.uri;
82
86
  }
83
87
 
88
+ // Valid metric scales (must match the enum in lib/Financials.js). The backfill sanitizes the
89
+ // snapshot's stored scale against this set so a corrupt legacy value can't propagate into a
90
+ // metric and fail a future validated write. Unknown/absent values fall back to 1.
91
+ const VALID_METRIC_SCALES = [1, 1000, 1000000, 1000000000, 1000000000000];
92
+ function sanitizeScale(scale) {
93
+ return VALID_METRIC_SCALES.includes(scale) ? scale : 1;
94
+ }
95
+
84
96
  console.log('Migration Configuration:');
85
97
  console.log(' Customer ID:', CUSTOMER_ID);
86
98
  console.log(' Database URI:', dbUri.replace(/\/\/[^:]+:[^@]+@/, '//*****:*****@')); // Mask credentials
@@ -312,12 +324,18 @@ async function migrateFinancials() {
312
324
  const source = getSourceForMetric(metric.name);
313
325
 
314
326
  // Create separate metric entries for each scenario that has a value
327
+ // Backfill per-metric currency/scale from the (now-deprecated) snapshot-level values
328
+ const metricCurrency = snapshot.currency || 'USD';
329
+ const metricScale = sanitizeScale(snapshot.scale);
330
+
315
331
  if ('actual' in metric && metric.actual !== undefined && metric.actual !== null) {
316
332
  const newMetric = {
317
333
  name: metric.name,
318
334
  scenario: 'actual',
319
335
  value: metric.actual,
320
336
  breakdown: metric.breakdown || {},
337
+ currency: metricCurrency,
338
+ scale: metricScale,
321
339
  verified: true,
322
340
  history: historyArrayForMetrics
323
341
  };
@@ -331,6 +349,8 @@ async function migrateFinancials() {
331
349
  scenario: 'budget',
332
350
  value: metric.budget,
333
351
  breakdown: {},
352
+ currency: metricCurrency,
353
+ scale: metricScale,
334
354
  verified: true,
335
355
  history: historyArrayForMetrics
336
356
  };
@@ -344,6 +364,8 @@ async function migrateFinancials() {
344
364
  scenario: 'forecast',
345
365
  value: metric.forecast,
346
366
  breakdown: {},
367
+ currency: metricCurrency,
368
+ scale: metricScale,
347
369
  verified: true,
348
370
  history: historyArrayForMetrics
349
371
  };
@@ -356,7 +378,7 @@ async function migrateFinancials() {
356
378
  updates[`snapshots.${index}.metrics`] = newMetrics;
357
379
  changes.push(` - Snapshot ${index}: Converted ${oldMetrics.length} metric(s) to new structure (${newMetrics.length} entries, ${metricsWithSources} with sources)`);
358
380
  } else {
359
- // Already in new structure, just add verified, history, and source if missing
381
+ // Already in new structure, just add verified, history, source, and currency/scale if missing
360
382
  oldMetrics.forEach((metric, mIndex) => {
361
383
  if (!('verified' in metric)) {
362
384
  updates[`snapshots.${index}.metrics.${mIndex}.verified`] = true;
@@ -373,6 +395,17 @@ async function migrateFinancials() {
373
395
  changes.push(` - Snapshot ${index}: Added sources to metric ${mIndex} (${metric.name})`);
374
396
  }
375
397
  }
398
+ // Backfill per-metric currency/scale from the (now-deprecated) snapshot-level values.
399
+ // Idempotent: only set when the metric doesn't already carry its own.
400
+ if (!('currency' in metric)) {
401
+ updates[`snapshots.${index}.metrics.${mIndex}.currency`] = snapshot.currency || 'USD';
402
+ changes.push(` - Snapshot ${index}: Backfilled currency (${snapshot.currency || 'USD'}) to metric ${mIndex} (${metric.name})`);
403
+ }
404
+ if (!('scale' in metric)) {
405
+ const backfillScale = sanitizeScale(snapshot.scale);
406
+ updates[`snapshots.${index}.metrics.${mIndex}.scale`] = backfillScale;
407
+ changes.push(` - Snapshot ${index}: Backfilled scale (${backfillScale}) to metric ${mIndex} (${metric.name})`);
408
+ }
376
409
  });
377
410
  }
378
411
  }