@quotivity/cpq-inventory-core 0.1.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.
Files changed (45) hide show
  1. package/assets/report/assets/index-CKPi--yh.css +1 -0
  2. package/assets/report/assets/index-CqUjsqpS.js +9 -0
  3. package/assets/report/fonts/LexendDeca-VariableFont_wght.ttf +0 -0
  4. package/assets/report/index.html +14 -0
  5. package/lib/banner.d.ts +7 -0
  6. package/lib/banner.js +13 -0
  7. package/lib/classify/advancedCondition.d.ts +34 -0
  8. package/lib/classify/advancedCondition.js +112 -0
  9. package/lib/classify/formula.d.ts +29 -0
  10. package/lib/classify/formula.js +125 -0
  11. package/lib/classify/index.d.ts +80 -0
  12. package/lib/classify/index.js +662 -0
  13. package/lib/classify/rules.d.ts +31 -0
  14. package/lib/classify/rules.js +868 -0
  15. package/lib/config/outbound.d.ts +31 -0
  16. package/lib/config/outbound.js +35 -0
  17. package/lib/connection.d.ts +42 -0
  18. package/lib/connection.js +52 -0
  19. package/lib/extract/index.d.ts +112 -0
  20. package/lib/extract/index.js +373 -0
  21. package/lib/extract/records.d.ts +421 -0
  22. package/lib/extract/records.js +327 -0
  23. package/lib/index.d.ts +30 -0
  24. package/lib/index.js +51 -0
  25. package/lib/liveness.d.ts +19 -0
  26. package/lib/liveness.js +34 -0
  27. package/lib/mapping.d.ts +14 -0
  28. package/lib/mapping.js +272 -0
  29. package/lib/report/assemble.d.ts +16 -0
  30. package/lib/report/assemble.js +499 -0
  31. package/lib/report/printDoc.d.ts +13 -0
  32. package/lib/report/printDoc.js +82 -0
  33. package/lib/report/shareSummary.d.ts +8 -0
  34. package/lib/report/shareSummary.js +54 -0
  35. package/lib/report/types.d.ts +146 -0
  36. package/lib/report/types.js +2 -0
  37. package/lib/server.d.ts +32 -0
  38. package/lib/server.js +185 -0
  39. package/lib/soql.d.ts +50 -0
  40. package/lib/soql.js +76 -0
  41. package/lib/summary.d.ts +14 -0
  42. package/lib/summary.js +143 -0
  43. package/lib/types.d.ts +22 -0
  44. package/lib/types.js +85 -0
  45. package/package.json +35 -0
@@ -0,0 +1,662 @@
1
+ import { detectBulkTouch, isAlive } from '../liveness.js';
2
+ import { validateAgainstIndexes } from './advancedCondition.js';
3
+ import { checkFormula } from './formula.js';
4
+ import { ruleRow } from './rules.js';
5
+ const PRICE_FIELDS = new Set([
6
+ 'SBQQ__ListPrice__c',
7
+ 'SBQQ__SpecialPrice__c',
8
+ 'SBQQ__RegularPrice__c',
9
+ 'SBQQ__CustomerPrice__c',
10
+ 'SBQQ__NetPrice__c',
11
+ 'SBQQ__PartnerPrice__c',
12
+ 'SBQQ__UnitCost__c',
13
+ 'SBQQ__ProratedListPrice__c',
14
+ 'SBQQ__ProratedPrice__c',
15
+ 'SBQQ__OriginalPrice__c',
16
+ 'SBQQ__SpecialPriceType__c',
17
+ 'SBQQ__PricingMethod__c',
18
+ ].map((f) => f.toLowerCase()));
19
+ const DISCOUNT_FIELDS = new Set([
20
+ 'SBQQ__Discount__c',
21
+ 'SBQQ__AdditionalDiscount__c',
22
+ 'SBQQ__AdditionalDiscountAmount__c',
23
+ 'SBQQ__DistributorDiscount__c',
24
+ 'SBQQ__PartnerDiscount__c',
25
+ 'SBQQ__Markup__c',
26
+ 'SBQQ__MarkupRate__c',
27
+ 'SBQQ__MarkupAmount__c',
28
+ 'SBQQ__VolumeDiscount__c',
29
+ 'SBQQ__CompoundDiscountRate__c',
30
+ 'SBQQ__DiscountSchedule__c',
31
+ 'SBQQ__CustomerDiscount__c',
32
+ 'SBQQ__DiscountAmount__c',
33
+ 'SBQQ__AdditionalDiscountRate__c',
34
+ ].map((f) => f.toLowerCase()));
35
+ const QCP_HOOKS = [
36
+ 'onInit',
37
+ 'onBeforeCalculate',
38
+ 'onBeforePriceRules',
39
+ 'onAfterPriceRules',
40
+ 'onAfterCalculate',
41
+ 'isFieldVisible',
42
+ 'isFieldEditable',
43
+ 'isFieldVisibleForObject',
44
+ 'isFieldEditableForObject',
45
+ ];
46
+ const bare = (field) => (field ?? '').split('.').pop()?.toLowerCase() ?? '';
47
+ export function classifyAll(ex) {
48
+ const since = ex.since;
49
+ const records = [];
50
+ const tallies = [];
51
+ const residue = [];
52
+ const objects = [];
53
+ const ruleConditionProblems = [];
54
+ const add = (read, rec, primary, secondary = [], details = [], aliveOverride) => {
55
+ records.push({
56
+ id: rec.Id,
57
+ object: read.object,
58
+ name: rec.Name ?? rec.Id,
59
+ primary,
60
+ secondary,
61
+ alive: aliveOverride ?? isAlive(rec, since),
62
+ details,
63
+ });
64
+ };
65
+ const stat = (read, classified, residueN) => {
66
+ objects.push({
67
+ object: read.object,
68
+ status: read.status,
69
+ count: read.count,
70
+ retrieved: read.records.length,
71
+ classified,
72
+ residue: residueN,
73
+ partial: read.partial,
74
+ bulkTouch: read.status === 'ok' ? detectBulkTouch(read.object, read.records) : null,
75
+ });
76
+ };
77
+ const L = (rowId, bucket) => {
78
+ const r = ruleRow(rowId);
79
+ const b = bucket ?? (r.bucket === 'follows' ? 'price' : r.bucket);
80
+ return { rowId, bucket: b };
81
+ };
82
+ const cross = (rowId) => {
83
+ const r = ruleRow(rowId);
84
+ return r.crossBucket ? [{ rowId, bucket: r.crossBucket }] : [];
85
+ };
86
+ // ── Catalog ─────────────────────────────────────────────────────────────
87
+ const bundleParents = new Set(ex.productFeature.records.map((f) => f.SBQQ__ConfiguredSKU__c));
88
+ for (const o of ex.productOption.records)
89
+ bundleParents.add(o.SBQQ__ConfiguredSKU__c);
90
+ const optionProducts = new Set(ex.productOption.records.map((o) => o.SBQQ__OptionalSKU__c));
91
+ const blockPriced = new Set(ex.blockPrice.records.map((b) => b.SBQQ__Product__c));
92
+ for (const p of ex.product2.records) {
93
+ const secondary = [];
94
+ const method = (p.SBQQ__PricingMethod__c ?? '').toLowerCase();
95
+ if (method === 'cost')
96
+ secondary.push(L('price.costPlus'));
97
+ else if (method === 'percent of total')
98
+ secondary.push(L('price.percentOfTotal'));
99
+ else if (method === 'block' || blockPriced.has(p.Id))
100
+ secondary.push(L('price.block'));
101
+ if ((p.SBQQ__ChargeType__c ?? '').toLowerCase() === 'usage')
102
+ secondary.push(L('price.usage'));
103
+ if (p.SBQQ__SubscriptionPricing__c) {
104
+ const isBundleHeader = bundleParents.has(p.Id) ||
105
+ (p.SBQQ__ConfigurationType__c && p.SBQQ__ConfigurationType__c !== 'None');
106
+ const rowId = isBundleHeader || optionProducts.has(p.Id)
107
+ ? 'price.subscriptionBundled'
108
+ : 'price.subscriptionUnbundled';
109
+ secondary.push(L(rowId), ...cross(rowId));
110
+ }
111
+ if (p.SBQQ__NonDiscountable__c || p.SBQQ__PriceEditable__c === false)
112
+ secondary.push(L('grd.productFlags'));
113
+ add(ex.product2, p, L('cat.products'), secondary);
114
+ }
115
+ stat(ex.product2, ex.product2.records.length, 0);
116
+ for (const pb of ex.pricebook2.records)
117
+ add(ex.pricebook2, pb, L('cat.pricebooks'));
118
+ stat(ex.pricebook2, ex.pricebook2.records.length, 0);
119
+ const currencies = ex.currencyType.records.filter((c) => c.IsActive).map((c) => c.IsoCode);
120
+ const multiCurrency = currencies.length > 1;
121
+ if (multiCurrency)
122
+ tallies.push({
123
+ rowId: 'cat.currencies',
124
+ bucket: 'catalog',
125
+ count: currencies.length,
126
+ alive: currencies.length,
127
+ details: currencies,
128
+ });
129
+ const periods = new Map();
130
+ for (const r of ex.datedConversionRate.records)
131
+ periods.set(r.IsoCode, (periods.get(r.IsoCode) ?? 0) + 1);
132
+ const staticCur = [...periods].filter(([, n]) => n <= 1).map(([c]) => c);
133
+ const tableCur = [...periods].filter(([, n]) => n > 1).map(([c]) => c);
134
+ if (staticCur.length)
135
+ tallies.push({
136
+ rowId: 'cat.dcrStatic',
137
+ bucket: 'catalog',
138
+ count: staticCur.length,
139
+ alive: staticCur.length,
140
+ details: staticCur,
141
+ });
142
+ if (tableCur.length)
143
+ tallies.push({
144
+ rowId: 'cat.dcrTable',
145
+ bucket: 'catalog',
146
+ count: tableCur.length,
147
+ alive: tableCur.length,
148
+ details: tableCur.map((c) => `${c}: ${periods.get(c)} rate periods`),
149
+ });
150
+ // ── Configuration ───────────────────────────────────────────────────────
151
+ const featureShape = new Map();
152
+ for (const f of ex.productFeature.records) {
153
+ const min = f.SBQQ__MinOptionCount__c ?? null;
154
+ const max = f.SBQQ__MaxOptionCount__c ?? null;
155
+ let rowId = 'cfg.features';
156
+ if (min === 1 && max === 1)
157
+ rowId = 'cfg.featurePickOne';
158
+ else if (min === 1 && max == null)
159
+ rowId = 'cfg.featurePickMany';
160
+ else if ((min != null && min > 1) || (max != null && max > 1))
161
+ rowId = 'cfg.featureCap';
162
+ featureShape.set(f.Id, rowId);
163
+ add(ex.productFeature, f, L(rowId));
164
+ }
165
+ stat(ex.productFeature, ex.productFeature.records.length, 0);
166
+ const optionById = new Map(ex.productOption.records.map((o) => [o.Id, o]));
167
+ for (const o of ex.productOption.records) {
168
+ const rowId = bundleParents.has(o.SBQQ__OptionalSKU__c)
169
+ ? 'cfg.optionsNested'
170
+ : 'cfg.optionsPlain';
171
+ add(ex.productOption, { ...o, Name: o.Id }, L(rowId));
172
+ }
173
+ stat(ex.productOption, ex.productOption.records.length, 0);
174
+ // Nesting depth: follow option → bundle chains.
175
+ const childrenOf = new Map();
176
+ for (const o of ex.productOption.records) {
177
+ const list = childrenOf.get(o.SBQQ__ConfiguredSKU__c) ?? [];
178
+ list.push(o.SBQQ__OptionalSKU__c);
179
+ childrenOf.set(o.SBQQ__ConfiguredSKU__c, list);
180
+ }
181
+ const depthOf = (id, seen) => {
182
+ if (seen.has(id))
183
+ return 0;
184
+ seen.add(id);
185
+ const kids = childrenOf.get(id) ?? [];
186
+ let d = 0;
187
+ for (const k of kids)
188
+ d = Math.max(d, depthOf(k, seen));
189
+ seen.delete(id);
190
+ return kids.length ? 1 + d : 0;
191
+ };
192
+ let nestingDepth = 0;
193
+ for (const parent of childrenOf.keys())
194
+ nestingDepth = Math.max(nestingDepth, depthOf(parent, new Set()));
195
+ let cfgRes = 0;
196
+ for (const c of ex.optionConstraint.records) {
197
+ const type = (c.SBQQ__Type__c ?? '').toLowerCase();
198
+ if (type === 'exclusion')
199
+ add(ex.optionConstraint, c, L('cfg.constraintExclusion'));
200
+ else if (type === 'dependency') {
201
+ const req = c.SBQQ__ConstrainingOption__c
202
+ ? optionById.get(c.SBQQ__ConstrainingOption__c)
203
+ : undefined;
204
+ const satisfied = !!req &&
205
+ !!req.SBQQ__Selected__c &&
206
+ featureShape.get(req.SBQQ__Feature__c ?? '') === 'cfg.featurePickOne';
207
+ add(ex.optionConstraint, c, L(satisfied ? 'cfg.constraintDepSatisfied' : 'cfg.constraintDepOther'));
208
+ }
209
+ else {
210
+ cfgRes += 1;
211
+ residue.push({ object: ex.optionConstraint.object, id: c.Id, name: c.Name });
212
+ }
213
+ }
214
+ stat(ex.optionConstraint, ex.optionConstraint.records.length - cfgRes, cfgRes);
215
+ for (const r of ex.configurationRule.records)
216
+ add(ex.configurationRule, { ...r, Name: r.Id }, L('cfg.configRules'));
217
+ stat(ex.configurationRule, ex.configurationRule.records.length, 0);
218
+ for (const a of ex.configurationAttribute.records) {
219
+ const rowId = a.SBQQ__ApplyToProductOptions__c
220
+ ? 'disc.attributesOnOptions'
221
+ : 'disc.attributesGuided';
222
+ add(ex.configurationAttribute, a, L(rowId), cross(rowId));
223
+ }
224
+ stat(ex.configurationAttribute, ex.configurationAttribute.records.length, 0);
225
+ // ── Product rules (classifier input) ────────────────────────────────────
226
+ const errorCondsByRule = groupBy(ex.errorCondition.records, (c) => c.SBQQ__Rule__c);
227
+ const actionsByRule = groupBy(ex.productAction.records, (a) => a.SBQQ__Rule__c);
228
+ const testedByValidation = new Set();
229
+ for (const c of ex.errorCondition.records)
230
+ if (c.SBQQ__TestedField__c)
231
+ testedByValidation.add(bare(c.SBQQ__TestedField__c));
232
+ const productRuleBucket = new Map();
233
+ let prRes = 0;
234
+ for (const r of ex.productRule.records) {
235
+ const type = (r.SBQQ__Type__c ?? '').toLowerCase();
236
+ const scope = (r.SBQQ__Scope__c ?? '').toLowerCase();
237
+ let rowId = null;
238
+ if (type === 'selection')
239
+ rowId = 'cfg.selectionRules';
240
+ else if (type === 'filter')
241
+ rowId = 'cfg.filterRules';
242
+ else if ((type === 'validation' || type === 'alert') && scope === 'quote')
243
+ rowId = 'grd.quoteScope';
244
+ else if (type === 'validation')
245
+ rowId = 'grd.validation';
246
+ else if (type === 'alert')
247
+ rowId = 'grd.alert';
248
+ if (!rowId) {
249
+ prRes += 1;
250
+ residue.push({ object: ex.productRule.object, id: r.Id, name: r.Name });
251
+ continue;
252
+ }
253
+ const secondary = [];
254
+ const details = [];
255
+ if ((r.SBQQ__ConditionsMet__c ?? '').toLowerCase() === 'custom') {
256
+ secondary.push(L('grd.customLogic'));
257
+ const present = (errorCondsByRule.get(r.Id) ?? []).map((c) => c.SBQQ__Index__c ?? -1);
258
+ const v = validateAgainstIndexes(r.SBQQ__AdvancedCondition__c, present);
259
+ if (v.parsed)
260
+ details.push(`custom logic ${r.SBQQ__AdvancedCondition__c} → ${v.parsed.dnf.map((g) => g.join(' AND ')).join(' OR ')}`);
261
+ if (!v.ok)
262
+ ruleConditionProblems.push({
263
+ rule: r.Name,
264
+ problem: v.error ?? `references conditions ${v.missing.join(', ')} that do not exist`,
265
+ });
266
+ }
267
+ const listing = L(rowId);
268
+ productRuleBucket.set(r.Id, listing.bucket);
269
+ add(ex.productRule, r, listing, secondary, details);
270
+ }
271
+ stat(ex.productRule, ex.productRule.records.length - prRes, prRes);
272
+ // Product actions: only Remove/Disable carry their own discriminator; the rest are children.
273
+ let countedActions = 0;
274
+ for (const [ruleId, actions] of actionsByRule) {
275
+ const hasReplacement = actions.some((a) => /add|enable|default/i.test(a.SBQQ__Type__c ?? '') &&
276
+ !/remove|disable/i.test(a.SBQQ__Type__c ?? ''));
277
+ for (const a of actions) {
278
+ if (!/remove|disable/i.test(a.SBQQ__Type__c ?? ''))
279
+ continue;
280
+ countedActions += 1;
281
+ add(ex.productAction, { ...a, Name: `${a.SBQQ__Type__c} on rule ${ruleId}` }, L(hasReplacement ? 'cfg.actionSwap' : 'cfg.actionRemoveNoReplacement'));
282
+ }
283
+ }
284
+ objects.push({
285
+ object: ex.productAction.object,
286
+ status: ex.productAction.status,
287
+ count: ex.productAction.count,
288
+ retrieved: ex.productAction.records.length,
289
+ classified: countedActions,
290
+ residue: 0,
291
+ partial: ex.productAction.partial,
292
+ bulkTouch: null,
293
+ });
294
+ // ── Discovery ───────────────────────────────────────────────────────────
295
+ for (const q of ex.quoteProcess.records)
296
+ add(ex.quoteProcess, q, L('disc.quoteProcesses'));
297
+ stat(ex.quoteProcess, ex.quoteProcess.records.length, 0);
298
+ for (const p of ex.processInput.records)
299
+ add(ex.processInput, p, L('disc.processInputs'));
300
+ stat(ex.processInput, ex.processInput.records.length, 0);
301
+ // ── Price rules (classifier input) ──────────────────────────────────────
302
+ const priceActionsByRule = groupBy(ex.priceAction.records, (a) => a.SBQQ__Rule__c);
303
+ const priceCondsByRule = groupBy(ex.priceCondition.records, (c) => c.SBQQ__Rule__c);
304
+ const attributeTargets = new Set(ex.configurationAttribute.records.map((a) => bare(a.SBQQ__TargetField__c)).filter(Boolean));
305
+ const priceRuleBucket = new Map();
306
+ let priceRes = 0;
307
+ for (const r of ex.priceRule.records) {
308
+ const actions = priceActionsByRule.get(r.Id) ?? [];
309
+ const conds = priceCondsByRule.get(r.Id) ?? [];
310
+ const scope = (r.SBQQ__TargetObject__c ?? '').toLowerCase();
311
+ const writes = actions.map((a) => bare(a.SBQQ__Field__c)).filter(Boolean);
312
+ const writesPrice = actions.some((a) => PRICE_FIELDS.has(bare(a.SBQQ__Field__c)) ||
313
+ (!!a.SBQQ__Formula__c && PRICE_FIELDS.has(bare(a.SBQQ__Field__c))));
314
+ const writesDiscount = writes.some((f) => DISCOUNT_FIELDS.has(f));
315
+ const writesTested = writes.some((f) => testedByValidation.has(f));
316
+ const testsAttribute = conds.some((c) => attributeTargets.has(bare(c.SBQQ__Field__c)));
317
+ let rowId = null;
318
+ if (scope === 'configurator')
319
+ rowId = 'cfg.priceRuleConfigurator';
320
+ else if (writesTested)
321
+ rowId = 'grd.floorCeiling';
322
+ else if (writesPrice)
323
+ rowId = testsAttribute ? 'price.attributeDriven' : 'price.priceRulesPrice';
324
+ else if (writesDiscount)
325
+ rowId = 'dsc.priceRulesDiscount';
326
+ else if (writes.length)
327
+ rowId = 'disc.priceRulesDefaulting';
328
+ if (!rowId) {
329
+ priceRes += 1;
330
+ residue.push({ object: ex.priceRule.object, id: r.Id, name: r.Name });
331
+ continue;
332
+ }
333
+ const details = [];
334
+ const event = scope === 'configurator'
335
+ ? r.SBQQ__ConfiguratorEvaluationEvent__c
336
+ : r.SBQQ__EvaluationEvent__c;
337
+ if (event)
338
+ details.push(`${r.SBQQ__TargetObject__c ?? 'Calculator'} · ${event}`);
339
+ const secondary = [];
340
+ if ((r.SBQQ__ConditionsMet__c ?? '').toLowerCase() === 'custom') {
341
+ secondary.push(L('grd.customLogic'));
342
+ const present = conds.map((c) => c.SBQQ__Index__c ?? -1);
343
+ const v = validateAgainstIndexes(r.SBQQ__AdvancedCondition__c, present);
344
+ if (v.parsed)
345
+ details.push(`custom logic ${r.SBQQ__AdvancedCondition__c} → ${v.parsed.dnf.map((g) => g.join(' AND ')).join(' OR ')}`);
346
+ if (!v.ok)
347
+ ruleConditionProblems.push({
348
+ rule: r.Name,
349
+ problem: v.error ?? `references conditions ${v.missing.join(', ')} that do not exist`,
350
+ });
351
+ }
352
+ const listing = L(rowId);
353
+ priceRuleBucket.set(r.Id, listing.bucket);
354
+ add(ex.priceRule, r, listing, secondary, details);
355
+ }
356
+ stat(ex.priceRule, ex.priceRule.records.length - priceRes, priceRes);
357
+ // Price actions with their own discriminator.
358
+ let countedPriceActions = 0;
359
+ for (const a of ex.priceAction.records) {
360
+ const target = (a.SBQQ__TargetObject__c ?? '').toLowerCase();
361
+ const check = checkFormula(a.SBQQ__Formula__c);
362
+ let rowId = null;
363
+ const details = [];
364
+ if (!check.ok) {
365
+ rowId = 'price.actionsOverLimit';
366
+ details.push(...check.breaches);
367
+ }
368
+ else if (target === 'quote')
369
+ rowId = a.SBQQ__SourceVariable__c
370
+ ? 'price.actionsQuoteRollup'
371
+ : 'price.actionsQuoteArbitrary';
372
+ if (!rowId)
373
+ continue;
374
+ countedPriceActions += 1;
375
+ add(ex.priceAction, { ...a, Name: `${a.SBQQ__Field__c ?? 'action'} on rule ${a.SBQQ__Rule__c}` }, L(rowId), [], details);
376
+ }
377
+ objects.push({
378
+ object: ex.priceAction.object,
379
+ status: ex.priceAction.status,
380
+ count: ex.priceAction.count,
381
+ retrieved: ex.priceAction.records.length,
382
+ classified: countedPriceActions,
383
+ residue: 0,
384
+ partial: ex.priceAction.partial,
385
+ bulkTouch: null,
386
+ });
387
+ // ── Summary variables follow the rule that consumes them ────────────────
388
+ const svConsumer = new Map();
389
+ const note = (id, bucket) => {
390
+ if (id && bucket && !svConsumer.has(id))
391
+ svConsumer.set(id, bucket);
392
+ };
393
+ for (const a of ex.priceAction.records)
394
+ note(a.SBQQ__SourceVariable__c, priceRuleBucket.get(a.SBQQ__Rule__c));
395
+ for (const c of ex.priceCondition.records) {
396
+ note(c.SBQQ__TestedVariable__c, priceRuleBucket.get(c.SBQQ__Rule__c));
397
+ note(c.SBQQ__FilterVariable__c, priceRuleBucket.get(c.SBQQ__Rule__c));
398
+ }
399
+ for (const c of ex.errorCondition.records) {
400
+ note(c.SBQQ__TestedVariable__c, productRuleBucket.get(c.SBQQ__Rule__c));
401
+ note(c.SBQQ__FilterVariable__c, productRuleBucket.get(c.SBQQ__Rule__c));
402
+ }
403
+ for (const t of ex.termCondition.records)
404
+ note(t.SBQQ__TestedVariable__c, 'output');
405
+ let deadSummaryVariables = 0;
406
+ for (const s of ex.summaryVariable.records) {
407
+ const target = (s.SBQQ__TargetObject__c ?? '').toLowerCase();
408
+ const scope = (s.SBQQ__Scope__c ?? '').toLowerCase();
409
+ let rowId = 'sv.plain';
410
+ if (target === 'asset' || target === 'subscription' || scope === 'assets')
411
+ rowId = 'sv.asset';
412
+ else if (target === 'product option')
413
+ rowId = 'sv.productOption';
414
+ else if (s.SBQQ__ConstraintField__c)
415
+ rowId = 'sv.constraint';
416
+ else if (s.SBQQ__CombineWith__c || s.SBQQ__ValueElement__c != null)
417
+ rowId = 'sv.composite';
418
+ const consumer = svConsumer.get(s.Id);
419
+ const details = [];
420
+ let aliveOverride;
421
+ if (!consumer) {
422
+ deadSummaryVariables += 1;
423
+ details.push('no rule, condition or term references this variable — dead configuration');
424
+ aliveOverride = false;
425
+ }
426
+ add(ex.summaryVariable, s, L(rowId, consumer ?? 'price'), [], details, aliveOverride);
427
+ }
428
+ stat(ex.summaryVariable, ex.summaryVariable.records.length, 0);
429
+ let lqRes = 0;
430
+ for (const q of ex.lookupQuery.records) {
431
+ if (q.SBQQ__ProductRule__c)
432
+ add(ex.lookupQuery, q, L('cfg.lookupProductRule'), cross('cfg.lookupProductRule'));
433
+ else if (q.SBQQ__PriceRule__c)
434
+ add(ex.lookupQuery, q, L('price.lookupPriceRule'), cross('price.lookupPriceRule'));
435
+ else {
436
+ lqRes += 1;
437
+ residue.push({ object: ex.lookupQuery.object, id: q.Id, name: q.Name });
438
+ }
439
+ }
440
+ stat(ex.lookupQuery, ex.lookupQuery.records.length - lqRes, lqRes);
441
+ // Dimensions: differing quantities across segments sharing a segment key.
442
+ const differingDimensions = new Set();
443
+ const byKey = groupBy(ex.segmentedLines.records.filter((l) => l.SBQQ__SegmentKey__c), (l) => l.SBQQ__SegmentKey__c);
444
+ for (const lines of byKey.values()) {
445
+ const qty = new Set(lines.map((l) => l.SBQQ__Quantity__c ?? 0));
446
+ if (qty.size > 1)
447
+ for (const l of lines)
448
+ if (l.SBQQ__Dimension__c)
449
+ differingDimensions.add(l.SBQQ__Dimension__c);
450
+ }
451
+ for (const d of ex.dimension.records)
452
+ add(ex.dimension, d, L(differingDimensions.has(d.Id) ? 'price.mdqDiffering' : 'price.mdqSame'));
453
+ stat(ex.dimension, ex.dimension.records.length, 0);
454
+ // ── Discounting ─────────────────────────────────────────────────────────
455
+ for (const s of ex.discountSchedule.records) {
456
+ const type = (s.SBQQ__Type__c ?? '').toLowerCase();
457
+ const scope = (s.SBQQ__AggregationScope__c ?? '').toLowerCase();
458
+ let rowId = 'dsc.perLine';
459
+ const secondary = [];
460
+ if (type === 'slab')
461
+ rowId = 'dsc.slab';
462
+ else if (scope === 'group')
463
+ rowId = 'dsc.groupScope';
464
+ else if (scope === 'quote')
465
+ rowId = 'dsc.quoteScope';
466
+ if (scope === 'group')
467
+ secondary.push(L('grd.groupGated'));
468
+ add(ex.discountSchedule, s, L(rowId), secondary);
469
+ }
470
+ stat(ex.discountSchedule, ex.discountSchedule.records.length, 0);
471
+ const accounts = new Set();
472
+ for (const c of ex.contractedPrice.records) {
473
+ if (c.SBQQ__Account__c)
474
+ accounts.add(c.SBQQ__Account__c);
475
+ add(ex.contractedPrice, c, L('dsc.contractedPrices'));
476
+ }
477
+ stat(ex.contractedPrice, ex.contractedPrice.records.length, 0);
478
+ // ── Guardrails: platform validation rules ───────────────────────────────
479
+ for (const v of ex.validationRule.records) {
480
+ add(ex.validationRule, {
481
+ Id: v.Id,
482
+ Name: `${v.EntityDefinition?.QualifiedApiName ?? ''}.${v.ValidationName}`,
483
+ LastModifiedDate: undefined,
484
+ }, L('grd.platformValidation'), cross('grd.platformValidation'), [], true);
485
+ }
486
+ stat(ex.validationRule, ex.validationRule.records.length, 0);
487
+ // Conditions gating on a quote line group (product rules / approvals) cross-list to grd.groupGated.
488
+ for (const c of ex.errorCondition.records) {
489
+ if (/group/i.test(c.SBQQ__TestedObject__c ?? '')) {
490
+ const rec = records.find((r) => r.id === c.SBQQ__Rule__c);
491
+ if (rec && !rec.secondary.some((s) => s.rowId === 'grd.groupGated'))
492
+ rec.secondary.push(L('grd.groupGated'));
493
+ }
494
+ }
495
+ // ── Approvals ───────────────────────────────────────────────────────────
496
+ for (const r of ex.approvalRule.records)
497
+ add(ex.approvalRule, r, L(r.sbaa__ApproverField__c ? 'apr.derivedApprover' : 'apr.rules'));
498
+ stat(ex.approvalRule, ex.approvalRule.records.length, 0);
499
+ for (const c of ex.approvalChain.records)
500
+ add(ex.approvalChain, c, L('apr.chains'));
501
+ stat(ex.approvalChain, ex.approvalChain.records.length, 0);
502
+ for (const t of ex.trackedField.records)
503
+ add(ex.trackedField, t, L('apr.trackedFields'));
504
+ stat(ex.trackedField, ex.trackedField.records.length, 0);
505
+ for (const v of ex.approvalVariable.records)
506
+ add(ex.approvalVariable, v, L('apr.variables'));
507
+ stat(ex.approvalVariable, ex.approvalVariable.records.length, 0);
508
+ for (const p of ex.processDefinition.records)
509
+ add(ex.processDefinition, { Id: p.Id, Name: p.Name, LastModifiedDate: p.LastModifiedDate }, L('apr.native'), [], [`${p.TableEnumOrId} · ${p.State}`]);
510
+ stat(ex.processDefinition, ex.processDefinition.records.length, 0);
511
+ for (const c of ex.approvalCondition.records) {
512
+ if (/group/i.test(c.sbaa__TestedField__c ?? '')) {
513
+ const rec = records.find((r) => r.id === c.sbaa__ApprovalRule__c);
514
+ if (rec && !rec.secondary.some((s) => s.rowId === 'grd.groupGated'))
515
+ rec.secondary.push(L('grd.groupGated'));
516
+ }
517
+ }
518
+ // Approval history → median cycle time and rules fired.
519
+ const cycles = [];
520
+ const firedRules = new Set();
521
+ for (const h of ex.approvalHistory.records) {
522
+ if (h.sbaa__Rule__c)
523
+ firedRules.add(h.sbaa__Rule__c);
524
+ if (/approved|rejected/i.test(h.sbaa__Status__c ?? ''))
525
+ cycles.push((Date.parse(h.LastModifiedDate) - Date.parse(h.CreatedDate)) / 86_400_000);
526
+ }
527
+ for (const p of ex.processInstance.records) {
528
+ if (p.CompletedDate)
529
+ cycles.push((Date.parse(p.CompletedDate) - Date.parse(p.CreatedDate)) / 86_400_000);
530
+ }
531
+ const medianCycle = median(cycles.filter((d) => d >= 0));
532
+ // ── Quote output ────────────────────────────────────────────────────────
533
+ for (const t of ex.quoteTemplate.records)
534
+ add(ex.quoteTemplate, t, L('out.templates'));
535
+ stat(ex.quoteTemplate, ex.quoteTemplate.records.length, 0);
536
+ for (const c of ex.lineColumn.records)
537
+ add(ex.lineColumn, c, L('out.lineColumns'));
538
+ stat(ex.lineColumn, ex.lineColumn.records.length, 0);
539
+ for (const t of ex.quoteTerm.records)
540
+ add(ex.quoteTerm, t, L('out.terms'));
541
+ stat(ex.quoteTerm, ex.quoteTerm.records.length, 0);
542
+ for (const t of ex.termCondition.records)
543
+ add(ex.termCondition, {
544
+ Id: t.Id,
545
+ Name: `${t.SBQQ__Field__c ?? ''} ${t.SBQQ__Operator__c ?? ''} ${t.SBQQ__Value__c ?? ''}`.trim(),
546
+ LastModifiedDate: undefined,
547
+ }, L('out.termConditions'), [], [], true);
548
+ stat(ex.termCondition, ex.termCondition.records.length, 0);
549
+ if (ex.quoteLineGroupCount > 0)
550
+ tallies.push({
551
+ rowId: 'out.groups',
552
+ bucket: 'output',
553
+ count: ex.quoteLineGroupCount,
554
+ alive: ex.quoteLineGroupAlive,
555
+ });
556
+ // ── Contract lifecycle (count-only rows) ────────────────────────────────
557
+ tallies.push({
558
+ rowId: 'life.contracts',
559
+ bucket: 'lifecycle',
560
+ count: ex.contractActive,
561
+ alive: ex.contractAlive,
562
+ details: [`${ex.subscriptionCount} subscriptions`],
563
+ });
564
+ const sinceYear = Number(ex.since.slice(0, 4));
565
+ const amendRenew = ex.quotesByYear
566
+ .filter((q) => q.year >= sinceYear && /amendment|renewal/i.test(q.type))
567
+ .reduce((a, q) => a + q.count, 0);
568
+ tallies.push({
569
+ rowId: 'life.amendRenew',
570
+ bucket: 'lifecycle',
571
+ count: amendRenew,
572
+ alive: amendRenew,
573
+ });
574
+ if (ex.orderCount + ex.assetCount > 0)
575
+ tallies.push({
576
+ rowId: 'life.ordersAssets',
577
+ bucket: 'lifecycle',
578
+ count: ex.orderCount + ex.assetCount,
579
+ alive: ex.orderCount + ex.assetCount,
580
+ details: [`${ex.orderCount} orders`, `${ex.assetCount} assets`],
581
+ });
582
+ const nowYear = Number(ex.now.slice(0, 4));
583
+ const lastYear = nowYear - 1;
584
+ const lastYearRows = ex.quotesByYear.filter((q) => q.year === lastYear);
585
+ const lastYearTotal = lastYearRows.reduce((a, q) => a + q.count, 0);
586
+ const lastYearAR = lastYearRows
587
+ .filter((q) => /amendment|renewal/i.test(q.type))
588
+ .reduce((a, q) => a + q.count, 0);
589
+ // ── Custom code & UI ────────────────────────────────────────────────────
590
+ const qcpHooks = {};
591
+ for (const s of ex.customScript.records) {
592
+ const code = s.SBQQ__Code__c ?? '';
593
+ const hooks = QCP_HOOKS.filter((h) => new RegExp(`\\b${h}\\s*[:(=]`).test(code) || new RegExp(`function\\s+${h}\\b`).test(code));
594
+ qcpHooks[s.Name] = hooks;
595
+ add(ex.customScript, s, L('code.qcp'), [], hooks.length ? [`hooks: ${hooks.join(', ')}`] : ['no calculator hook detected']);
596
+ }
597
+ stat(ex.customScript, ex.customScript.records.length, 0);
598
+ for (const t of ex.apexTrigger.records)
599
+ add(ex.apexTrigger, { Id: t.Id, Name: t.Name, LastModifiedDate: t.LastModifiedDate }, L('code.triggers'), [], [`${t.TableEnumOrId} · ${t.Status ?? ''}`]);
600
+ stat(ex.apexTrigger, ex.apexTrigger.records.length, 0);
601
+ for (const a of ex.customAction.records)
602
+ add(ex.customAction, a, L('code.customActions'), [], [a.SBQQ__Location__c ?? '', a.SBQQ__Type__c ?? ''].filter(Boolean));
603
+ stat(ex.customAction, ex.customAction.records.length, 0);
604
+ for (const l of ex.layout.records)
605
+ add(ex.layout, { Id: l.Id, Name: `Layout · ${l.Name}` }, L('code.ui'), [], [l.TableEnumOrId ?? ''], true);
606
+ stat(ex.layout, ex.layout.records.length, 0);
607
+ for (const w of ex.webLink.records)
608
+ add(ex.webLink, { Id: w.Id, Name: `Button · ${w.Name}` }, L('code.ui'), [], [w.PageOrSobjectType ?? ''], true);
609
+ stat(ex.webLink, ex.webLink.records.length, 0);
610
+ for (const f of ex.flexiPage.records)
611
+ add(ex.flexiPage, { Id: f.Id, Name: `Record page · ${f.DeveloperName}` }, L('code.ui'), [], [f.EntityDefinitionId ?? ''], true);
612
+ stat(ex.flexiPage, ex.flexiPage.records.length, 0);
613
+ const facts = {
614
+ bundles: bundleParents.size,
615
+ nestingDepth,
616
+ featureCount: ex.productFeature.count,
617
+ optionCount: ex.productOption.count,
618
+ approverCount: ex.approver.count,
619
+ medianApprovalCycleDays: medianCycle,
620
+ approvalHistoryCount: ex.approvalHistory.count + ex.processInstance.count,
621
+ approvalRulesFired: firedRules.size,
622
+ contractedPriceAccounts: accounts.size,
623
+ discountTierCount: ex.discountTier.count,
624
+ templateSectionCount: ex.templateSection.count,
625
+ templateContentCount: ex.templateContent.count,
626
+ deadSummaryVariables,
627
+ qcpHooks,
628
+ ruleConditionProblems,
629
+ attributeCount: ex.configurationAttribute.count,
630
+ currencies,
631
+ multiCurrency,
632
+ advancedApprovalsInstalled: ex.approvalRule.status !== 'absent',
633
+ nativeApprovalProcesses: ex.processDefinition.count,
634
+ subscriptionCount: ex.subscriptionCount,
635
+ productsNeverQuoted: Math.max(0, ex.product2.count - ex.distinctProductsQuoted),
636
+ amendRenewShareLastYear: lastYearTotal ? lastYearAR / lastYearTotal : null,
637
+ lastYear: lastYearTotal ? lastYear : null,
638
+ };
639
+ return { records, tallies, residue, objects, facts };
640
+ }
641
+ function groupBy(items, key) {
642
+ const m = new Map();
643
+ for (const it of items) {
644
+ const k = key(it);
645
+ const list = m.get(k);
646
+ if (list)
647
+ list.push(it);
648
+ else
649
+ m.set(k, [it]);
650
+ }
651
+ return m;
652
+ }
653
+ export function median(values) {
654
+ if (!values.length)
655
+ return null;
656
+ const sorted = [...values].sort((a, b) => a - b);
657
+ const mid = Math.floor(sorted.length / 2);
658
+ const a = sorted[mid];
659
+ const b = sorted[mid - 1];
660
+ return sorted.length % 2 ? a : (a + b) / 2;
661
+ }
662
+ //# sourceMappingURL=index.js.map