/** * 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')); 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 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);