Files
Converge/scripts/test-finance.mjs
Chris Olson 93007a1053 Sprint 2 UI and brand refresh
Workbench: ramp-adjusted headline (TCO, net run-rate, NPV, IRR, payback),
financial model panel with internal-only investment, year-by-year cash flow,
scenario panel (presets + custom, presented scenario, report comparison),
overlap heat map, shelfware and expansion panel, editable implementation
timeline (drag, resize, row editor, dependencies, reset), version history
with compare and restore, deal status, customer logo.

Deliverables: report, Word and new PowerPoint deck present the chosen
scenario with net figures only; investment and dossier entitlements never
itemized (covered by the leak suite). Exports and printing snapshot
automatically.

New pages: Portfolio dashboard, Settings (company name, logo, accent).
Knowledge base: duplicate queue with merge, confidence and sourcing,
TCO defaults, needs-review filter.

Brand: VMware by Broadcom palette per Brand Guidelines.pdf; Darkmode navy
dark theme; AA-safe accent ink; business units remapped to palette.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:41:15 -04:00

134 lines
7.8 KiB
JavaScript

/**
* Financial engine, planner, scenarios, shelfware and knowledge-base curation.
* Pure logic against the compiled libs — no server needed.
*/
import { readFileSync } from 'node:fs';
import { rollup, lineCost } from '../.test-build/scoring.js';
import { evaluate, allScenarios, cashFlow } from '../.test-build/finance.js';
import { buildPlan } from '../.test-build/timeline.js';
import { shelfware } from '../.test-build/shelfware.js';
import { findDuplicates, mergeEntries, learnFromAssessment, newEntry, confidenceOf } from '../.test-build/kb.js';
let pass = 0, fail = 0;
const t = (n, ok, x = '') => { ok ? (pass++, console.log(' PASS', n)) : (fail++, console.log(' FAIL', n, x)); };
const near = (a, b, tol = 1) => Math.abs(a - b) <= tol;
const catalog = JSON.parse(readFileSync(new URL('../data/catalog.json', import.meta.url)));
const TODAY = new Date('2026-09-24T12:00:00Z'); // plan month 0 = Oct 2026
const line = (id, over = {}) => ({
id, incumbentVendor: `Vendor${id}`, incumbentProduct: 'Tool', domain: 'observability',
productIds: ['aod-dx-oi'], annualSpend: 1_000_000, units: 0, unitLabel: '', renewalDate: '',
capability: 4, switchEase: 4, timeToRoiMonths: 6, savingsPct: 50, migrationCost: 100_000,
strategicValue: 3, risks: '', notes: '', included: true, ...over,
});
const base = (candidates, extra = {}) => ({
id: 't', version: 1, customerName: 'T', industry: '', preparedBy: '', preparedFor: '', opportunityRef: '',
executiveSummary: '', horizonYears: 3, weights: { capability: 35, switchEase: 25, timeToRoi: 25, spend: 15 },
candidates, createdAt: '', updatedAt: '', ...extra,
});
console.log('TCO');
{
const c = lineCost(line('a'), 150_000);
t('default TCO = licence + 15% infra + 0.25 FTE + 5% other', near(c.tco, 1_000_000 + 150_000 + 37_500 + 50_000), c.tco);
t('unknown spend carries no phantom admin', lineCost(line('b', { annualSpend: 0 }), 150_000).tco === 0);
const o = lineCost(line('c', { tco: { infra: 10, adminFte: 2, other: null } }), 100_000);
t('overrides win per category', o.infra === 10 && o.admin === 200_000 && near(o.other, 50_000));
}
console.log('Savings model');
{
const r = rollup(base([line('a')]), catalog);
const m = r.metrics.get('a');
t('no investment: net = savings % of TCO', near(m.annualSavings, m.tco * 0.5) && !r.hasInvestment);
const inv = base([line('a'), line('b', { annualSpend: 3_000_000 }), line('c', { investment: { annual: 50_000, services: null } }), line('d', { alreadyEntitled: 'X' })],
{ finance: { discountRate: 10, fteCost: 150_000, investment: { annual: 1_000_000, services: 200_000 } } });
const ri = rollup(inv, catalog);
const [ma, mb, mc, md] = ['a', 'b', 'c', 'd'].map((k) => ri.metrics.get(k));
t('with investment: gross = full TCO', near(ma.grossRemoved, ma.tco));
t('per-line price wins', mc.subscription === 50_000);
t('entitled line takes no subscription share', md.subscription === 0);
t('pool shared by TCO', near(ma.subscription / mb.subscription, ma.tco / mb.tco, 0.001));
t('subscriptions sum to the deal figure', near(ri.totalSubscription, 1_000_000, 2), ri.totalSubscription);
t('services pool fully attributed', near(ri.totalMigrationCost, 4 * 100_000 + 200_000, 2), ri.totalMigrationCost);
}
console.log('Cash flow');
{
const a = base([line('a'), line('b', { renewalDate: '2027-06-30', switchEase: 3 })],
{ finance: { discountRate: 10, fteCost: 150_000, investment: { annual: 400_000, services: 0 } } });
const e = evaluate(a, catalog, undefined, TODAY);
const sumYears = e.cash.years.reduce((s, y) => s + y.net, 0);
t('years add up to the horizon net', near(sumYears, e.cash.horizonNet, 2), `${sumYears} vs ${e.cash.horizonNet}`);
t('subscription billed from month 0', e.cash.months[0] < 0);
t('NPV below undiscounted net', e.cash.npv < e.cash.horizonNet);
t('payback found', e.cash.paybackMonth !== null && e.cash.paybackMonth > 0, e.cash.paybackMonth);
t('IRR solved', e.cash.irr !== null && e.cash.irr > 0, e.cash.irr);
const b = e.plan.items.find((i) => i.id === 'b');
t('contract line cuts over by contract end (Jun 2027 = month 8)', b.end <= 9 && b.savingsFrom === 9, JSON.stringify(b));
}
console.log('Planner overrides');
{
const a = base([
line('p', { plan: { start: '2027-04', duration: 4 } }),
line('q', { plan: { after: ['p'] } }),
line('x', { plan: { after: ['y'] } }), line('y', { plan: { after: ['x'] } }),
]);
const r = rollup(a, catalog);
const plan = buildPlan(a, r, catalog, TODAY);
const get = (id) => plan.items.find((i) => i.id === id);
t('pinned start and duration honoured', get('p').start === 6 && get('p').end === 10 && get('p').anchor === 'pinned');
t('dependency waits for predecessor', get('q').start >= get('p').end, `${get('q').start}`);
t('dependency cycle still schedules', !!get('x') && !!get('y'));
const pinnedEarly = base([line('p'), line('q', { plan: { start: '2026-10', after: ['p'] } })]);
const pe = buildPlan(pinnedEarly, rollup(pinnedEarly, catalog), catalog, TODAY);
t('pinned before its predecessor is flagged', pe.items.find((i) => i.id === 'q').conflict && pe.conflictCount === 1);
}
console.log('Scenarios');
{
const a = base([line('a'), line('b'), line('c')], { scenarios: [{ id: 's1', name: 'Only A', kind: 'custom', valueFactor: 1, excluded: ['b', 'c'] }] });
const net = Object.fromEntries(allScenarios(a).map((s) => [s.id, evaluate(a, catalog, s, TODAY).cash.runRateNet]));
t('conservative < expected < aggressive', net.conservative < net.expected && net.expected < net.aggressive, JSON.stringify(net));
t('custom scope excludes lines', near(net.s1 * 3, net.expected, 5), `${net.s1} vs ${net.expected}`);
t('scenario does not mutate the assessment', a.candidates.every((c) => c.included));
}
console.log('Shelfware');
{
const a = base([line('lm', { productIds: ['aod-dx-uim'] })], { accountIntel: { consumption: [
{ product: 'UIM', entitled: 100, deployed: 40, adoption: '' },
{ product: 'VCF OPERATIONS', entitled: 10, deployed: 0, adoption: '' },
] } });
const s = shelfware(a, catalog);
t('acronym prefers the product in the plan', s[0].match?.id === 'aod-dx-uim' && s[0].linked[0] === 'lm', s[0].match?.name);
t('exact name is decisive', s[1].match?.name === 'VCF Operations', s[1].match?.name);
}
console.log('Knowledge base curation');
{
const kb = { version: 1, updatedAt: '', entries: [newEntry('F5 BIG-IP', 'manual'), newEntry('F5 BIG-IP LTM', 'manual'), newEntry('Microsoft Entra ID', 'manual')] };
kb.entries.forEach((e) => { e.domain = 'adc'; e.productIds = ['ans-avi']; });
const dups = findDuplicates(kb);
t('finds F5 BIG-IP / F5 BIG-IP LTM', dups.some((d) => [d.a.label, d.b.label].sort().join('|') === 'F5 BIG-IP|F5 BIG-IP LTM'));
const fam = { entries: [newEntry('Datadog', 'manual'), newEntry('Datadog Logs', 'manual')] };
fam.entries[0].productIds = ['aod-dx-oi']; fam.entries[1].productIds = ['vcf-ops-logs'];
t('product family with different replacements is not a duplicate', findDuplicates(fam).length === 0);
const a = base([line('f5', { incumbentVendor: 'F5', incumbentProduct: 'BIG-IP', domain: 'adc', productIds: ['ans-avi'] })]);
const learned = learnFromAssessment(kb, a);
const merged = mergeEntries(learned, 'f5-big-ip-ltm', 'f5-big-ip');
const ltm = merged.entries.find((e) => e.id === 'f5-big-ip-ltm');
t('merge moves deal history and adds alias', ltm.observations.length === 1 && ltm.aliases.includes('F5 BIG-IP'));
t('merged entry is removed', !merged.entries.some((e) => e.id === 'f5-big-ip'));
const relearn = learnFromAssessment(merged, { ...a, updatedAt: 'x' }) ?? merged;
t('learning after merge lands on the survivor', !relearn.entries.some((e) => e.id === 'f5-big-ip')
&& relearn.entries.find((e) => e.id === 'f5-big-ip-ltm').observations.length === 1);
t('one deal = low confidence', confidenceOf(ltm).level === 'low');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);