1149 lines
48 KiB
JavaScript
1149 lines
48 KiB
JavaScript
/**
|
|
* SVS MSP Calculator — Quote Engine Tests
|
|
*
|
|
* Run: node svsmspcalc/tests/test-quote-engine.js
|
|
*
|
|
* Zero dependencies. Uses a minimal test harness built-in.
|
|
* Tests the pure calculateQuote(state, pricing) function against
|
|
* known-good expected values using default pricing.
|
|
*/
|
|
|
|
// ── Minimal test harness ───────────────────────────────────────
|
|
let _passed = 0;
|
|
let _failed = 0;
|
|
let _group = '';
|
|
|
|
function describe(name, fn) {
|
|
_group = name;
|
|
console.log(`\n ${name}`);
|
|
fn();
|
|
}
|
|
|
|
function it(name, fn) {
|
|
try {
|
|
fn();
|
|
_passed++;
|
|
console.log(` \x1b[32m✓\x1b[0m ${name}`);
|
|
} catch (e) {
|
|
_failed++;
|
|
console.log(` \x1b[31m✗\x1b[0m ${name}`);
|
|
console.log(` \x1b[31m${e.message}\x1b[0m`);
|
|
}
|
|
}
|
|
|
|
function assert(condition, msg) {
|
|
if (!condition) throw new Error(msg || 'Assertion failed');
|
|
}
|
|
|
|
function eq(actual, expected, label) {
|
|
if (actual !== expected) {
|
|
throw new Error(`${label || 'Value'}: expected ${expected}, got ${actual}`);
|
|
}
|
|
}
|
|
|
|
function near(actual, expected, tolerance, label) {
|
|
if (Math.abs(actual - expected) > tolerance) {
|
|
throw new Error(`${label || 'Value'}: expected ~${expected} (±${tolerance}), got ${actual}`);
|
|
}
|
|
}
|
|
|
|
// ── Load engine (mock window as global) ────────────────────────
|
|
const _global = {};
|
|
(function loadModules() {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const dir = path.resolve(__dirname, '..');
|
|
|
|
// Load quote-pricing.js — sets DEFAULTS on _global
|
|
const pricingSrc = fs.readFileSync(path.join(dir, 'quote-pricing.js'), 'utf8')
|
|
.replace(/\(window\)\s*;?\s*$/, '(_global);');
|
|
new Function('_global', pricingSrc)(_global);
|
|
|
|
// Load quote-engine.js — sets calculateQuote on _global
|
|
const engineSrc = fs.readFileSync(path.join(dir, 'quote-engine.js'), 'utf8')
|
|
.replace(/\(window\)\s*;?\s*$/, '(_global);');
|
|
new Function('_global', engineSrc)(_global);
|
|
})();
|
|
|
|
const calculateQuote = _global.calculateQuote;
|
|
const DEFAULTS = _global.SVSQuotePricing.DEFAULTS;
|
|
|
|
// Helper: build a state object with sensible defaults
|
|
function makeState(overrides) {
|
|
return Object.assign({
|
|
byol: false,
|
|
users: 0,
|
|
endpoints: 0,
|
|
servers: 0,
|
|
addExtHours: false,
|
|
addPWM: false,
|
|
addINKY: false,
|
|
addZT: false,
|
|
addUSB: false,
|
|
addBMB: false,
|
|
ztSeats: 0,
|
|
ztRouters: 0,
|
|
voipTier: 'basic',
|
|
voipSeats: 0,
|
|
addVoipPhone: false,
|
|
addVoipFax: false,
|
|
clientName: '',
|
|
contractTerm: 'm2m',
|
|
hstEnabled: false,
|
|
oneTimeFee: 0,
|
|
adminWaived: false
|
|
}, overrides);
|
|
}
|
|
|
|
// ── Tests ──────────────────────────────────────────────────────
|
|
console.log('\n\x1b[1mSVS Quote Engine — Test Suite\x1b[0m');
|
|
|
|
// ── 1. Basic pricing ──────────────────────────────────────────
|
|
describe('Basic M365 quote (1 user, 1 endpoint, m2m)', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1 }), DEFAULTS);
|
|
|
|
it('userBase = 140 (m2m rate)', () => eq(q.userBase, 140, 'userBase'));
|
|
it('userTotal = 140', () => eq(q.userTotal, 140, 'userTotal'));
|
|
it('endpointBase = 35', () => eq(q.endpointBase, 35, 'endpointBase'));
|
|
it('endpointTotal = 35', () => eq(q.endpointTotal, 35, 'endpointTotal'));
|
|
it('baseSubtotal = 175', () => eq(q.baseSubtotal, 175, 'baseSubtotal'));
|
|
it('admin fee scales: max(150, 650-175) = 475', () => eq(q.siteAdminBase, 475, 'siteAdminBase'));
|
|
it('adminFeeNet = 475', () => eq(q.adminFeeNet, 475, 'adminFeeNet'));
|
|
it('MRR = 650', () => eq(q.MRR, 650, 'MRR'));
|
|
it('annual = 7800', () => eq(q.annual, 7800, 'annual'));
|
|
it('no discount at m2m', () => eq(q.discountAmt, 0, 'discountAmt'));
|
|
it('effectiveMrr = MRR', () => eq(q.effectiveMrr, 650, 'effectiveMrr'));
|
|
});
|
|
|
|
// ── 2. BYOL rate ──────────────────────────────────────────────
|
|
describe('BYOL rate (1 user, 1 endpoint)', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, byol: true }), DEFAULTS);
|
|
|
|
it('baseUserRate = 110 (BYOL)', () => eq(q.baseUserRate, 110, 'baseUserRate'));
|
|
it('userBase = 110', () => eq(q.userBase, 110, 'userBase'));
|
|
it('baseSubtotal = 145', () => eq(q.baseSubtotal, 145, 'baseSubtotal'));
|
|
it('admin fee scales: max(150, 650-145) = 505', () => eq(q.siteAdminBase, 505, 'siteAdminBase'));
|
|
it('MRR still = 650 (admin absorbs difference)', () => eq(q.MRR, 650, 'MRR'));
|
|
});
|
|
|
|
// ── 3. Admin fee floor ────────────────────────────────────────
|
|
describe('Admin fee floor (high service volume)', () => {
|
|
// 5 users + 1 endpoint: baseSubtotal = 675+35 = 710 > 650
|
|
const q = calculateQuote(makeState({ users: 5, endpoints: 1 }), DEFAULTS);
|
|
|
|
it('baseSubtotal = 735', () => eq(q.baseSubtotal, 735, 'baseSubtotal'));
|
|
it('admin hits floor: max(150, 650-710) = 150', () => eq(q.siteAdminBase, 150, 'siteAdminBase'));
|
|
it('adminFeeNet = 150', () => eq(q.adminFeeNet, 150, 'adminFeeNet'));
|
|
});
|
|
|
|
// ── 4. Admin fee ZT premium ───────────────────────────────────
|
|
describe('Admin fee with Zero Trust premium', () => {
|
|
const q = calculateQuote(makeState({ users: 5, endpoints: 1, addZT: true }), DEFAULTS);
|
|
|
|
it('ztActive = true', () => assert(q.ztActive, 'ztActive'));
|
|
it('admin includes ZT premium: 150 + 250 = 400', () => eq(q.adminFeeNet, 400, 'adminFeeNet'));
|
|
});
|
|
|
|
describe('Admin fee ZT premium via ztSeats (no addZT checkbox)', () => {
|
|
const q = calculateQuote(makeState({ users: 5, endpoints: 1, ztSeats: 2 }), DEFAULTS);
|
|
|
|
it('ztActive = true (from ztSeats > 0)', () => assert(q.ztActive, 'ztActive'));
|
|
it('admin includes ZT premium', () => eq(q.adminFeeNet, 400, 'adminFeeNet'));
|
|
});
|
|
|
|
// ── 5. Admin fee 1Password markup ─────────────────────────────
|
|
describe('Admin fee with 1Password markup', () => {
|
|
// 5 users, 1 endpoint, addPWM
|
|
// userPWM = 5 * 9 = 45, admin1PWM = round(45 * 0.10) = 5
|
|
const q = calculateQuote(makeState({ users: 5, endpoints: 1, addPWM: true }), DEFAULTS);
|
|
|
|
it('userPWM = 45', () => eq(q.userPWM, 45, 'userPWM'));
|
|
it('admin1PWM = 5 (10% of 45, rounded)', () => eq(q.admin1PWM, 5, 'admin1PWM'));
|
|
it('adminFeeNet = 150 + 5 = 155', () => eq(q.adminFeeNet, 155, 'adminFeeNet'));
|
|
});
|
|
|
|
// ── 6. Admin waived ───────────────────────────────────────────
|
|
describe('Admin fee waived', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, adminWaived: true }), DEFAULTS);
|
|
|
|
it('adminFeeNet still calculated = 475', () => eq(q.adminFeeNet, 475, 'adminFeeNet'));
|
|
it('admin does not contribute to MRR', () => {
|
|
eq(q.MRR, 140 + 35, 'MRR without admin'); // 175
|
|
});
|
|
});
|
|
|
|
// ── 7. User add-ons ──────────────────────────────────────────
|
|
describe('All user add-ons (10 users)', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 10, endpoints: 1,
|
|
addExtHours: true, addPWM: true, addINKY: true, addZT: true
|
|
}), DEFAULTS);
|
|
|
|
it('userExt = 250', () => eq(q.userExt, 250, 'userExt'));
|
|
it('userPWM = 90', () => eq(q.userPWM, 90, 'userPWM'));
|
|
it('userINKY = 80', () => eq(q.userINKY, 80, 'userINKY'));
|
|
it('userZT = 550', () => eq(q.userZT, 550, 'userZT'));
|
|
it('addonRate = 25+9+8+55 = 97', () => {
|
|
eq(q.totalUserRate - q.baseUserRate, 97, 'addonRate');
|
|
});
|
|
it('userTotal = 10 * (140+97) = 2370', () => eq(q.userTotal, 2370, 'userTotal'));
|
|
});
|
|
|
|
// ── 8. Endpoint add-ons ──────────────────────────────────────
|
|
describe('Endpoint add-ons (10 endpoints)', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 1, endpoints: 10, addUSB: true, addBMB: true
|
|
}), DEFAULTS);
|
|
|
|
it('endpointBase = 350', () => eq(q.endpointBase, 350, 'endpointBase'));
|
|
it('endpointUSB = 40', () => eq(q.endpointUSB, 40, 'endpointUSB'));
|
|
it('endpointBMB = 250', () => eq(q.endpointBMB, 250, 'endpointBMB'));
|
|
it('endpointTotal = 640 (includes no servers)', () => eq(q.endpointTotal, 640, 'endpointTotal'));
|
|
});
|
|
|
|
// ── 9. Server pricing ────────────────────────────────────────
|
|
describe('Server pricing', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, servers: 3 }), DEFAULTS);
|
|
|
|
it('serverBase = 360', () => eq(q.serverBase, 360, 'serverBase'));
|
|
it('servers included in endpointTotal', () => eq(q.endpointTotal, 35 + 360, 'endpointTotal'));
|
|
it('servers in baseSubtotal', () => eq(q.baseSubtotal, 140 + 35 + 360, 'baseSubtotal'));
|
|
});
|
|
|
|
// ── 10. Contract discounts ───────────────────────────────────
|
|
describe('Contract term discounts', () => {
|
|
const base = { users: 10, endpoints: 10 };
|
|
|
|
it('m2m = 0% discount', () => {
|
|
const q = calculateQuote(makeState({ ...base, contractTerm: 'm2m' }), DEFAULTS);
|
|
eq(q.discountPct, 0, 'discountPct');
|
|
eq(q.discountAmt, 0, 'discountAmt');
|
|
});
|
|
|
|
it('12mo = 3% discount', () => {
|
|
const q = calculateQuote(makeState({ ...base, contractTerm: '12mo' }), DEFAULTS);
|
|
eq(q.discountPct, 0.03, 'discountPct');
|
|
eq(q.discountAmt, Math.round(q.MRR * 0.03), 'discountAmt');
|
|
eq(q.effectiveMrr, q.MRR - q.discountAmt, 'effectiveMrr');
|
|
});
|
|
|
|
it('24mo = 5% discount', () => {
|
|
const q = calculateQuote(makeState({ ...base, contractTerm: '24mo' }), DEFAULTS);
|
|
eq(q.discountPct, 0.05, 'discountPct');
|
|
eq(q.discountAmt, Math.round(q.MRR * 0.05), 'discountAmt');
|
|
});
|
|
});
|
|
|
|
// ── 10b. M365 term-aware rate ────────────────────────────────
|
|
describe('M365 rate varies by contract term', () => {
|
|
it('m2m uses RATE_M365_M2M ($140)', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, contractTerm: 'm2m' }), DEFAULTS);
|
|
eq(q.m365Rate, 140, 'm365Rate');
|
|
eq(q.baseUserRate, 140, 'baseUserRate');
|
|
});
|
|
it('12mo uses RATE_M365 ($130)', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, contractTerm: '12mo' }), DEFAULTS);
|
|
eq(q.m365Rate, 130, 'm365Rate');
|
|
eq(q.baseUserRate, 130, 'baseUserRate');
|
|
});
|
|
it('24mo uses RATE_M365 ($130)', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, contractTerm: '24mo' }), DEFAULTS);
|
|
eq(q.m365Rate, 130, 'm365Rate');
|
|
eq(q.baseUserRate, 130, 'baseUserRate');
|
|
});
|
|
it('BYOL rate unaffected by term', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, byol: true, contractTerm: '12mo' }), DEFAULTS);
|
|
eq(q.baseUserRate, 110, 'baseUserRate');
|
|
});
|
|
});
|
|
|
|
// ── 11. HST calculation ──────────────────────────────────────
|
|
describe('HST calculation', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 1, endpoints: 1, hstEnabled: true
|
|
}), DEFAULTS);
|
|
|
|
it('hstAmt = round(650 * 0.13) = 85', () => eq(q.hstAmt, 85, 'hstAmt'));
|
|
it('mrrWithHst = 650 + 85 = 735', () => eq(q.mrrWithHst, 735, 'mrrWithHst'));
|
|
});
|
|
|
|
describe('HST disabled = 0', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 1, endpoints: 1, hstEnabled: false
|
|
}), DEFAULTS);
|
|
|
|
it('hstAmt = 0', () => eq(q.hstAmt, 0, 'hstAmt'));
|
|
it('mrrWithHst = effectiveMrr', () => eq(q.mrrWithHst, q.effectiveMrr, 'mrrWithHst'));
|
|
});
|
|
|
|
// ── 12. VoIP tiers ───────────────────────────────────────────
|
|
describe('VoIP tiers', () => {
|
|
it('basic = $28/seat', () => {
|
|
const q = calculateQuote(makeState({ voipSeats: 5, voipTier: 'basic' }), DEFAULTS);
|
|
eq(q.voipSeatRate, 28, 'voipSeatRate');
|
|
eq(q.voipSeatsAmt, 140, 'voipSeatsAmt');
|
|
});
|
|
|
|
it('standard = $35/seat', () => {
|
|
const q = calculateQuote(makeState({ voipSeats: 5, voipTier: 'standard' }), DEFAULTS);
|
|
eq(q.voipSeatRate, 35, 'voipSeatRate');
|
|
eq(q.voipSeatsAmt, 175, 'voipSeatsAmt');
|
|
});
|
|
|
|
it('premium = $45/seat', () => {
|
|
const q = calculateQuote(makeState({ voipSeats: 5, voipTier: 'premium' }), DEFAULTS);
|
|
eq(q.voipSeatRate, 45, 'voipSeatRate');
|
|
eq(q.voipSeatsAmt, 225, 'voipSeatsAmt');
|
|
});
|
|
});
|
|
|
|
// ── 13. VoIP add-ons ─────────────────────────────────────────
|
|
describe('VoIP phone + fax add-ons', () => {
|
|
const q = calculateQuote(makeState({
|
|
voipSeats: 10, voipTier: 'standard',
|
|
addVoipPhone: true, addVoipFax: true
|
|
}), DEFAULTS);
|
|
|
|
it('voipPhoneAmt = 150', () => eq(q.voipPhoneAmt, 150, 'voipPhoneAmt'));
|
|
it('voipFaxAmt = 100', () => eq(q.voipFaxAmt, 100, 'voipFaxAmt'));
|
|
it('voipTotal = 350+150+100 = 600', () => eq(q.voipTotal, 600, 'voipTotal'));
|
|
});
|
|
|
|
// ── 14. Zero Trust networking ────────────────────────────────
|
|
describe('Zero Trust networking', () => {
|
|
const q = calculateQuote(makeState({ ztSeats: 10, ztRouters: 2 }), DEFAULTS);
|
|
|
|
it('ztNetSeats = 250', () => eq(q.ztNetSeats, 250, 'ztNetSeats'));
|
|
it('ztNetRouters = 200', () => eq(q.ztNetRouters, 200, 'ztNetRouters'));
|
|
it('ztNetTotal = 450', () => eq(q.ztNetTotal, 450, 'ztNetTotal'));
|
|
it('ztActive = true', () => assert(q.ztActive, 'ztActive'));
|
|
});
|
|
|
|
// ── 15. Zero users edge case ─────────────────────────────────
|
|
describe('Zero users edge case', () => {
|
|
const q = calculateQuote(makeState({ users: 0, endpoints: 0 }), DEFAULTS);
|
|
|
|
it('perUserAllin = 0', () => eq(q.perUserAllin, 0, 'perUserAllin'));
|
|
it('perUserServices = 0', () => eq(q.perUserServices, 0, 'perUserServices'));
|
|
it('userTotal = 0', () => eq(q.userTotal, 0, 'userTotal'));
|
|
it('admin still applies (floor)', () => {
|
|
// baseSubtotal = 0, max(150, 650-0) = 650
|
|
eq(q.siteAdminBase, 650, 'siteAdminBase');
|
|
});
|
|
});
|
|
|
|
// ── 16. Per-user breakdown ───────────────────────────────────
|
|
describe('Per-user cost breakdown', () => {
|
|
const q = calculateQuote(makeState({ users: 10, endpoints: 10 }), DEFAULTS);
|
|
|
|
it('perUserServices = round(userTotal / users)', () => {
|
|
eq(q.perUserServices, Math.round(q.userTotal / 10), 'perUserServices');
|
|
});
|
|
it('perUserSiteOvhd = round((effectiveMrr - userTotal) / users)', () => {
|
|
eq(q.perUserSiteOvhd, Math.round((q.effectiveMrr - q.userTotal) / 10), 'perUserSiteOvhd');
|
|
});
|
|
it('perUserAllin = MRR / users', () => {
|
|
eq(q.perUserAllin, q.MRR / 10, 'perUserAllin');
|
|
});
|
|
});
|
|
|
|
// ── 17. HST + discount combo ─────────────────────────────────
|
|
describe('HST applied AFTER contract discount', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 10, endpoints: 10,
|
|
contractTerm: '12mo', hstEnabled: true
|
|
}), DEFAULTS);
|
|
|
|
it('discount applied first', () => {
|
|
eq(q.effectiveMrr, q.MRR - q.discountAmt, 'effectiveMrr');
|
|
});
|
|
it('HST on discounted amount', () => {
|
|
eq(q.hstAmt, Math.round(q.effectiveMrr * 0.13), 'hstAmt');
|
|
});
|
|
it('mrrWithHst = effectiveMrr + hstAmt', () => {
|
|
eq(q.mrrWithHst, q.effectiveMrr + q.hstAmt, 'mrrWithHst');
|
|
});
|
|
});
|
|
|
|
// ── 18. Realistic full quote ─────────────────────────────────
|
|
describe('Realistic 22-user full quote', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 22, endpoints: 22, servers: 2,
|
|
addPWM: true, addINKY: true,
|
|
ztSeats: 5, ztRouters: 1,
|
|
voipSeats: 15, voipTier: 'standard', addVoipPhone: true,
|
|
contractTerm: '12mo', hstEnabled: true
|
|
}), DEFAULTS);
|
|
|
|
it('userTotal = 22 * (130+9+8) = 3234', () => eq(q.userTotal, 3234, 'userTotal'));
|
|
it('endpointBase = 770', () => eq(q.endpointBase, 770, 'endpointBase'));
|
|
it('serverBase = 240', () => eq(q.serverBase, 240, 'serverBase'));
|
|
it('ztNetTotal = 5*25 + 1*100 = 225', () => eq(q.ztNetTotal, 225, 'ztNetTotal'));
|
|
it('voipTotal = 15*35 + 15*15 = 750', () => eq(q.voipTotal, 750, 'voipTotal'));
|
|
|
|
it('admin: baseSubtotal = 2860+770+240 = 3870, floor applies', () => {
|
|
// baseSubtotal = 22*130 + 22*35 + 2*120 = 2860+770+240 = 3870
|
|
// Wait: baseSubtotal = userBase + endpointBase + serverBase
|
|
// userBase = 22*130 = 2860, endpointBase = 770, serverBase = 240
|
|
eq(q.baseSubtotal, 3870, 'baseSubtotal');
|
|
eq(q.siteAdminBase, 150, 'siteAdminBase (floor)');
|
|
});
|
|
|
|
it('admin includes ZT premium + 1PWM', () => {
|
|
// ztActive because ztSeats > 0
|
|
// admin1PWM = round(22*9 * 0.10) = round(19.8) = 20
|
|
eq(q.admin1PWM, 20, 'admin1PWM');
|
|
eq(q.adminFeeNet, 150 + 250 + 20, 'adminFeeNet'); // 420
|
|
});
|
|
|
|
it('MRR components sum correctly', () => {
|
|
const expected = q.userTotal + q.endpointTotal + q.adminFeeNet + q.ztNetTotal + q.voipTotal;
|
|
eq(q.MRR, expected, 'MRR');
|
|
});
|
|
|
|
it('12mo discount = 3%', () => {
|
|
eq(q.discountAmt, Math.round(q.MRR * 0.03), 'discountAmt');
|
|
});
|
|
|
|
it('HST on discounted total', () => {
|
|
eq(q.hstAmt, Math.round(q.effectiveMrr * 0.13), 'hstAmt');
|
|
});
|
|
|
|
it('effectiveAnnual = effectiveMrr * 12', () => {
|
|
eq(q.effectiveAnnual, q.effectiveMrr * 12, 'effectiveAnnual');
|
|
});
|
|
});
|
|
|
|
// ── 19. MRR components always sum ────────────────────────────
|
|
describe('MRR = sum of all components (various configs)', () => {
|
|
const configs = [
|
|
{ users: 1, endpoints: 1 },
|
|
{ users: 50, endpoints: 50, servers: 5, addPWM: true, addZT: true },
|
|
{ users: 0, endpoints: 0, voipSeats: 20, voipTier: 'premium' },
|
|
{ users: 10, endpoints: 10, adminWaived: true },
|
|
{ users: 3, endpoints: 3, ztSeats: 10, ztRouters: 3 }
|
|
];
|
|
|
|
configs.forEach((cfg, i) => {
|
|
it(`config #${i + 1}: MRR = user + endpoint + admin + zt + voip`, () => {
|
|
const q = calculateQuote(makeState(cfg), DEFAULTS);
|
|
const adminEff = q.adminWaived ? 0 : q.adminFeeNet;
|
|
const expected = q.userTotal + q.endpointTotal + adminEff + q.ztNetTotal + q.voipTotal;
|
|
eq(q.MRR, expected, 'MRR sum');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ── EXPANSION: Pricing Defaults Integrity ────────────────────
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
describe('Pricing DEFAULTS integrity', () => {
|
|
const requiredKeys = [
|
|
'RATE_M365', 'RATE_M365_M2M', 'RATE_BYOL',
|
|
'ADDON_EXT_HOURS', 'ADDON_1PASSWORD', 'ADDON_INKY', 'ADDON_ZERO_TRUST_USER',
|
|
'RATE_ENDPOINT', 'RATE_SERVER',
|
|
'ADDON_USB_BLOCKING', 'ADDON_BARE_METAL_BACKUP',
|
|
'ZT_SEAT_RATE', 'ZT_ROUTER_RATE',
|
|
'ADMIN_FEE_FLOOR', 'ADMIN_FEE_MINIMUM', 'ADMIN_FEE_ZT', 'ADMIN_1PWM_PCT',
|
|
'VOIP_RATE_BASIC', 'VOIP_RATE_STANDARD', 'VOIP_RATE_PREMIUM',
|
|
'VOIP_PHONE_RATE', 'VOIP_FAX_RATE',
|
|
'DISCOUNT_M2M', 'DISCOUNT_12MO', 'DISCOUNT_24MO',
|
|
'HST_RATE'
|
|
];
|
|
|
|
requiredKeys.forEach(key => {
|
|
it(`DEFAULTS.${key} exists and is a number`, () => {
|
|
assert(key in DEFAULTS, `${key} missing from DEFAULTS`);
|
|
assert(typeof DEFAULTS[key] === 'number', `${key} is not a number`);
|
|
});
|
|
});
|
|
|
|
it('DEFAULTS is frozen (immutable)', () => {
|
|
assert(Object.isFrozen(DEFAULTS), 'DEFAULTS should be frozen');
|
|
});
|
|
|
|
it('M365 m2m rate > annual rate', () => {
|
|
assert(DEFAULTS.RATE_M365_M2M > DEFAULTS.RATE_M365, 'm2m should exceed annual');
|
|
});
|
|
|
|
it('BYOL rate < M365 annual rate', () => {
|
|
assert(DEFAULTS.RATE_BYOL < DEFAULTS.RATE_M365, 'BYOL should be less than M365');
|
|
});
|
|
|
|
it('VoIP tiers in ascending order', () => {
|
|
assert(DEFAULTS.VOIP_RATE_BASIC < DEFAULTS.VOIP_RATE_STANDARD, 'basic < standard');
|
|
assert(DEFAULTS.VOIP_RATE_STANDARD < DEFAULTS.VOIP_RATE_PREMIUM, 'standard < premium');
|
|
});
|
|
|
|
it('Discount rates in ascending order (m2m < 12mo < 24mo)', () => {
|
|
assert(DEFAULTS.DISCOUNT_M2M < DEFAULTS.DISCOUNT_12MO, 'm2m < 12mo');
|
|
assert(DEFAULTS.DISCOUNT_12MO < DEFAULTS.DISCOUNT_24MO, '12mo < 24mo');
|
|
});
|
|
|
|
it('HST_RATE is 13%', () => {
|
|
eq(DEFAULTS.HST_RATE, 0.13, 'HST_RATE');
|
|
});
|
|
|
|
it('ADMIN_FEE_FLOOR < ADMIN_FEE_MINIMUM', () => {
|
|
assert(DEFAULTS.ADMIN_FEE_FLOOR < DEFAULTS.ADMIN_FEE_MINIMUM, 'floor < minimum');
|
|
});
|
|
|
|
it('Known pricing values match spec', () => {
|
|
eq(DEFAULTS.RATE_M365, 130, 'M365 annual');
|
|
eq(DEFAULTS.RATE_M365_M2M, 140, 'M365 m2m');
|
|
eq(DEFAULTS.RATE_BYOL, 110, 'BYOL');
|
|
eq(DEFAULTS.ADDON_EXT_HOURS, 25, 'ExtHours');
|
|
eq(DEFAULTS.ADDON_1PASSWORD, 9, '1PWM');
|
|
eq(DEFAULTS.ADDON_INKY, 8, 'INKY');
|
|
eq(DEFAULTS.ADDON_ZERO_TRUST_USER, 55, 'ZT user');
|
|
eq(DEFAULTS.RATE_ENDPOINT, 35, 'endpoint');
|
|
eq(DEFAULTS.RATE_SERVER, 120, 'server');
|
|
eq(DEFAULTS.ADMIN_FEE_FLOOR, 150, 'admin floor');
|
|
eq(DEFAULTS.ADMIN_FEE_MINIMUM, 650, 'admin minimum');
|
|
eq(DEFAULTS.ADMIN_FEE_ZT, 250, 'admin ZT');
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ── EXPANSION: Engine Edge Cases & Boundaries ────────────────
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
describe('Admin fee exact threshold (baseSubtotal = 500)', () => {
|
|
// baseSubtotal = 500 → max(150, 650-500) = max(150, 150) = 150
|
|
const q = calculateQuote(makeState({ users: 3, endpoints: 1, byol: true }), DEFAULTS);
|
|
// 3 * 110 = 330 + 35 = 365 … not 500. Let me use custom pricing.
|
|
|
|
it('at threshold: admin = max(floor, minimum - subtotal)', () => {
|
|
// 3 users BYOL + 1 endpoint: 330 + 35 = 365
|
|
eq(q.baseSubtotal, 365, 'baseSubtotal');
|
|
eq(q.siteAdminBase, Math.max(150, 650 - 365), 'siteAdminBase');
|
|
});
|
|
});
|
|
|
|
describe('Admin fee exactly at minimum (baseSubtotal = 650)', () => {
|
|
// Need baseSubtotal = 650. 4 users M365 m2m = 560, + ~3 endpoints = 105 → 665 too much
|
|
// 4 users m2m = 560 + 2 endpoints = 70 → 630; + 0 servers → 630
|
|
// Use: 4 users BYOL = 440 + 6 endpoints = 210 → 650
|
|
const q = calculateQuote(makeState({ users: 4, endpoints: 6, byol: true }), DEFAULTS);
|
|
|
|
it('baseSubtotal = 650 exactly', () => eq(q.baseSubtotal, 650, 'baseSubtotal'));
|
|
it('admin = floor (650-650=0, floor wins)', () => eq(q.siteAdminBase, 150, 'siteAdminBase'));
|
|
});
|
|
|
|
describe('Admin fee when baseSubtotal just under minimum', () => {
|
|
// 4 users BYOL = 440, 5 endpoints = 175 → 615
|
|
const q = calculateQuote(makeState({ users: 4, endpoints: 5, byol: true }), DEFAULTS);
|
|
|
|
it('baseSubtotal = 615', () => eq(q.baseSubtotal, 615, 'baseSubtotal'));
|
|
it('admin = max(150, 650-615) = max(150, 35) = 150', () => eq(q.siteAdminBase, 150, 'siteAdminBase'));
|
|
});
|
|
|
|
describe('Large user count (100 users, 100 endpoints)', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 100, endpoints: 100, servers: 10,
|
|
addExtHours: true, addPWM: true, addINKY: true, addZT: true,
|
|
addUSB: true, addBMB: true,
|
|
contractTerm: '24mo', hstEnabled: true
|
|
}), DEFAULTS);
|
|
|
|
it('userTotal = 100 * (130+25+9+8+55) = 22700', () => eq(q.userTotal, 22700, 'userTotal'));
|
|
it('endpointBase = 3500', () => eq(q.endpointBase, 3500, 'endpointBase'));
|
|
it('endpointUSB = 400', () => eq(q.endpointUSB, 400, 'endpointUSB'));
|
|
it('endpointBMB = 2500', () => eq(q.endpointBMB, 2500, 'endpointBMB'));
|
|
it('serverBase = 1200', () => eq(q.serverBase, 1200, 'serverBase'));
|
|
it('admin hits floor', () => eq(q.siteAdminBase, 150, 'siteAdminBase'));
|
|
it('MRR is positive integer', () => assert(q.MRR > 0 && Number.isInteger(q.MRR), 'MRR'));
|
|
it('24mo discount = 5%', () => eq(q.discountPct, 0.05, 'discountPct'));
|
|
it('HST calculated on discounted MRR', () => {
|
|
eq(q.hstAmt, Math.round(q.effectiveMrr * 0.13), 'hstAmt');
|
|
});
|
|
it('annual = effectiveMrr * 12', () => eq(q.effectiveAnnual, q.effectiveMrr * 12, 'effectiveAnnual'));
|
|
});
|
|
|
|
describe('String coercion for numeric inputs', () => {
|
|
const q = calculateQuote(makeState({ users: '5', endpoints: '3', servers: '2' }), DEFAULTS);
|
|
|
|
it('users parsed from string', () => eq(q.users, 5, 'users'));
|
|
it('endpoints parsed from string', () => eq(q.endpoints, 3, 'endpoints'));
|
|
it('servers parsed from string', () => eq(q.servers, 2, 'servers'));
|
|
it('calculations correct with string inputs', () => {
|
|
eq(q.userBase, 5 * 140, 'userBase');
|
|
eq(q.endpointBase, 3 * 35, 'endpointBase');
|
|
eq(q.serverBase, 2 * 120, 'serverBase');
|
|
});
|
|
});
|
|
|
|
describe('Invalid inputs handled gracefully', () => {
|
|
it('NaN endpoints → 0', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: NaN }), DEFAULTS);
|
|
eq(q.endpoints, 0, 'endpoints');
|
|
});
|
|
it('undefined servers → 0', () => {
|
|
const q = calculateQuote(makeState({ users: 1, servers: undefined }), DEFAULTS);
|
|
eq(q.servers, 0, 'servers');
|
|
});
|
|
it('null users → 0', () => {
|
|
const q = calculateQuote(makeState({ users: null, endpoints: 1 }), DEFAULTS);
|
|
eq(q.users, 0, 'users');
|
|
});
|
|
it('empty string users → 0', () => {
|
|
const q = calculateQuote(makeState({ users: '', endpoints: 1 }), DEFAULTS);
|
|
eq(q.users, 0, 'users');
|
|
});
|
|
});
|
|
|
|
describe('Single endpoint, no users', () => {
|
|
const q = calculateQuote(makeState({ users: 0, endpoints: 1 }), DEFAULTS);
|
|
|
|
it('endpointBase = 35', () => eq(q.endpointBase, 35, 'endpointBase'));
|
|
it('userTotal = 0', () => eq(q.userTotal, 0, 'userTotal'));
|
|
it('admin still scales', () => {
|
|
// baseSubtotal = 0 + 35 + 0 = 35, max(150, 650-35) = 615
|
|
eq(q.siteAdminBase, 615, 'siteAdminBase');
|
|
});
|
|
});
|
|
|
|
describe('Only servers, no users or endpoints', () => {
|
|
const q = calculateQuote(makeState({ users: 0, endpoints: 0, servers: 5 }), DEFAULTS);
|
|
|
|
it('serverBase = 600', () => eq(q.serverBase, 600, 'serverBase'));
|
|
it('endpointTotal includes servers', () => eq(q.endpointTotal, 600, 'endpointTotal'));
|
|
it('baseSubtotal = 600', () => eq(q.baseSubtotal, 600, 'baseSubtotal'));
|
|
it('admin = max(150, 650-600) = 150', () => eq(q.siteAdminBase, 150, 'siteAdminBase'));
|
|
});
|
|
|
|
describe('VoIP only quote (no users, no endpoints)', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 0, endpoints: 0,
|
|
voipSeats: 20, voipTier: 'premium', addVoipPhone: true, addVoipFax: true
|
|
}), DEFAULTS);
|
|
|
|
it('voipSeatsAmt = 900', () => eq(q.voipSeatsAmt, 900, 'voipSeatsAmt'));
|
|
it('voipPhoneAmt = 300', () => eq(q.voipPhoneAmt, 300, 'voipPhoneAmt'));
|
|
it('voipFaxAmt = 200', () => eq(q.voipFaxAmt, 200, 'voipFaxAmt'));
|
|
it('voipTotal = 1400', () => eq(q.voipTotal, 1400, 'voipTotal'));
|
|
it('admin is full (no user/endpoint base)', () => eq(q.siteAdminBase, 650, 'siteAdminBase'));
|
|
});
|
|
|
|
describe('VoIP fax without phone', () => {
|
|
const q = calculateQuote(makeState({
|
|
voipSeats: 5, voipTier: 'basic', addVoipFax: true, addVoipPhone: false
|
|
}), DEFAULTS);
|
|
|
|
it('voipFaxAmt = 50', () => eq(q.voipFaxAmt, 50, 'voipFaxAmt'));
|
|
it('voipPhoneAmt = 0', () => eq(q.voipPhoneAmt, 0, 'voipPhoneAmt'));
|
|
it('voipTotal = 140 + 50 = 190', () => eq(q.voipTotal, 190, 'voipTotal'));
|
|
});
|
|
|
|
describe('VoIP addons with zero seats', () => {
|
|
const q = calculateQuote(makeState({
|
|
voipSeats: 0, voipTier: 'standard', addVoipPhone: true, addVoipFax: true
|
|
}), DEFAULTS);
|
|
|
|
it('voipPhoneAmt = 0 (no seats)', () => eq(q.voipPhoneAmt, 0, 'voipPhoneAmt'));
|
|
it('voipFaxAmt = 0 (no seats)', () => eq(q.voipFaxAmt, 0, 'voipFaxAmt'));
|
|
it('voipTotal = 0', () => eq(q.voipTotal, 0, 'voipTotal'));
|
|
});
|
|
|
|
describe('ZT networking without ZT user addon', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 5, endpoints: 5, addZT: false, ztSeats: 10, ztRouters: 1
|
|
}), DEFAULTS);
|
|
|
|
it('ztActive = true (from ztSeats)', () => assert(q.ztActive, 'ztActive'));
|
|
it('userZT = 0 (addZT is false)', () => eq(q.userZT, 0, 'userZT'));
|
|
it('ztNetTotal = 250 + 100 = 350', () => eq(q.ztNetTotal, 350, 'ztNetTotal'));
|
|
it('admin includes ZT supplement', () => {
|
|
assert(q.adminFeeNet >= q.siteAdminBase + 250, 'admin includes ZT');
|
|
});
|
|
});
|
|
|
|
describe('Admin waived still calculates adminFeeNet', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 10, endpoints: 10, addPWM: true, addZT: true, adminWaived: true
|
|
}), DEFAULTS);
|
|
|
|
it('adminWaived flag is true', () => assert(q.adminWaived, 'adminWaived'));
|
|
it('adminFeeNet is still calculated (non-zero)', () => assert(q.adminFeeNet > 0, 'adminFeeNet > 0'));
|
|
it('MRR excludes admin', () => {
|
|
const expected = q.userTotal + q.endpointTotal + q.ztNetTotal + q.voipTotal;
|
|
eq(q.MRR, expected, 'MRR without admin');
|
|
});
|
|
});
|
|
|
|
describe('All addons combined with 12mo discount', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 15, endpoints: 15, servers: 3,
|
|
addExtHours: true, addPWM: true, addINKY: true, addZT: true,
|
|
addUSB: true, addBMB: true,
|
|
ztSeats: 8, ztRouters: 2,
|
|
voipSeats: 10, voipTier: 'standard', addVoipPhone: true, addVoipFax: true,
|
|
contractTerm: '12mo', hstEnabled: true
|
|
}), DEFAULTS);
|
|
|
|
it('all user addons active', () => {
|
|
assert(q.userExt > 0, 'userExt');
|
|
assert(q.userPWM > 0, 'userPWM');
|
|
assert(q.userINKY > 0, 'userINKY');
|
|
assert(q.userZT > 0, 'userZT');
|
|
});
|
|
|
|
it('all endpoint addons active', () => {
|
|
assert(q.endpointUSB > 0, 'endpointUSB');
|
|
assert(q.endpointBMB > 0, 'endpointBMB');
|
|
});
|
|
|
|
it('ztNetTotal > 0', () => assert(q.ztNetTotal > 0, 'ztNetTotal'));
|
|
it('voipTotal > 0', () => assert(q.voipTotal > 0, 'voipTotal'));
|
|
it('12mo discount applied', () => eq(q.discountPct, 0.03, 'discountPct'));
|
|
it('HST on discounted total', () => {
|
|
eq(q.hstAmt, Math.round(q.effectiveMrr * 0.13), 'hstAmt');
|
|
});
|
|
it('MRR components sum', () => {
|
|
const expected = q.userTotal + q.endpointTotal + q.adminFeeNet + q.ztNetTotal + q.voipTotal;
|
|
eq(q.MRR, expected, 'MRR sum');
|
|
});
|
|
});
|
|
|
|
describe('Per-user breakdown with admin waived', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 10, endpoints: 10, adminWaived: true
|
|
}), DEFAULTS);
|
|
|
|
it('perUserAllin = MRR / users', () => {
|
|
eq(q.perUserAllin, q.MRR / 10, 'perUserAllin');
|
|
});
|
|
it('perUserSiteOvhd reflects waived admin', () => {
|
|
eq(q.perUserSiteOvhd, Math.round((q.effectiveMrr - q.userTotal) / 10), 'perUserSiteOvhd');
|
|
});
|
|
});
|
|
|
|
describe('1Password admin markup scales with users', () => {
|
|
it('1 user: admin1PWM = round(9 * 0.10) = 1', () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, addPWM: true }), DEFAULTS);
|
|
eq(q.admin1PWM, 1, 'admin1PWM');
|
|
});
|
|
|
|
it('50 users: admin1PWM = round(450 * 0.10) = 45', () => {
|
|
const q = calculateQuote(makeState({ users: 50, endpoints: 1, addPWM: true }), DEFAULTS);
|
|
eq(q.admin1PWM, 45, 'admin1PWM');
|
|
});
|
|
|
|
it('no 1PWM addon: admin1PWM = 0', () => {
|
|
const q = calculateQuote(makeState({ users: 50, endpoints: 1, addPWM: false }), DEFAULTS);
|
|
eq(q.admin1PWM, 0, 'admin1PWM');
|
|
});
|
|
});
|
|
|
|
describe('HST with admin waived', () => {
|
|
const q = calculateQuote(makeState({
|
|
users: 10, endpoints: 10, hstEnabled: true, adminWaived: true
|
|
}), DEFAULTS);
|
|
|
|
it('HST computed on MRR without admin', () => {
|
|
eq(q.hstAmt, Math.round(q.effectiveMrr * 0.13), 'hstAmt');
|
|
});
|
|
it('mrrWithHst = effectiveMrr + hstAmt', () => {
|
|
eq(q.mrrWithHst, q.effectiveMrr + q.hstAmt, 'mrrWithHst');
|
|
});
|
|
});
|
|
|
|
describe('Contract term does not affect BYOL rate', () => {
|
|
['m2m', '12mo', '24mo'].forEach(term => {
|
|
it(`${term}: BYOL rate = 110`, () => {
|
|
const q = calculateQuote(makeState({ users: 1, endpoints: 1, byol: true, contractTerm: term }), DEFAULTS);
|
|
eq(q.baseUserRate, 110, 'baseUserRate');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Discount amounts are rounded integers', () => {
|
|
['12mo', '24mo'].forEach(term => {
|
|
it(`${term} discount is integer`, () => {
|
|
const q = calculateQuote(makeState({ users: 7, endpoints: 7, contractTerm: term }), DEFAULTS);
|
|
assert(Number.isInteger(q.discountAmt), `discountAmt should be integer, got ${q.discountAmt}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ── EXPANSION: Export JSON Schema Validation ─────────────────
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
describe('Export JSON payload schema (simulated)', () => {
|
|
// Simulate the payload that exportQuoteJSON() builds from a quote result
|
|
function buildExportPayload(q, overrides) {
|
|
const o = overrides || {};
|
|
const termMap = { m2m: 'Month-to-Month', '12mo': '12-Month', '24mo': '24-Month' };
|
|
return {
|
|
version: '1.1',
|
|
quoteRef: o.quoteRef || 'SVS-20260315-1234',
|
|
quoteDate: o.quoteDate || '2026-03-15',
|
|
clientName: q.clientName || '',
|
|
repName: o.repName || '',
|
|
quoteNotes: o.quoteNotes || '',
|
|
contractTerm: termMap[q.contractTerm] || 'Month-to-Month',
|
|
licensing: q.byol ? 'BYOL' : 'M365-Included',
|
|
users: q.users,
|
|
endpoints: q.endpoints,
|
|
servers: q.servers,
|
|
addons: {
|
|
extendedHours: q.addExtHours,
|
|
passwordManager: q.addPWM,
|
|
inkyPro: q.addINKY,
|
|
zeroTrustUsers: q.addZT,
|
|
baremetalBackup: q.addBMB,
|
|
usbBlocking: q.addUSB
|
|
},
|
|
zeroTrustNetwork: {
|
|
seats: q.ztSeats,
|
|
routers: q.ztRouters
|
|
},
|
|
voip: {
|
|
tier: q.voipSeats > 0 ? q.voipTier : null,
|
|
seats: q.voipSeats,
|
|
phoneHardware: q.addVoipPhone,
|
|
faxLine: q.addVoipFax,
|
|
currentPhoneBill: 0
|
|
},
|
|
adminWaived: q.adminWaived || false,
|
|
onboardingWaived: false,
|
|
onboardingManual: false,
|
|
pricing: {
|
|
baseMrr: q.MRR,
|
|
discountPct: Math.round(q.discountPct * 100),
|
|
discountAmt: q.discountAmt,
|
|
effectiveMrr: q.effectiveMrr,
|
|
annual: q.effectiveAnnual,
|
|
oneTimeFee: q.oneTimeFee,
|
|
hstIncluded: q.hstEnabled,
|
|
hstAmt: q.hstAmt,
|
|
mrrWithHst: q.mrrWithHst
|
|
}
|
|
};
|
|
}
|
|
|
|
const q = calculateQuote(makeState({
|
|
users: 10, endpoints: 10, servers: 1,
|
|
addPWM: true, addINKY: true, addZT: true,
|
|
ztSeats: 5, ztRouters: 1,
|
|
voipSeats: 8, voipTier: 'standard', addVoipPhone: true,
|
|
contractTerm: '12mo', hstEnabled: true,
|
|
clientName: 'Acme Corp'
|
|
}), DEFAULTS);
|
|
|
|
const payload = buildExportPayload(q);
|
|
|
|
it('has version field = "1.1"', () => eq(payload.version, '1.1', 'version'));
|
|
it('has quoteRef string', () => assert(typeof payload.quoteRef === 'string' && payload.quoteRef.length > 0, 'quoteRef'));
|
|
it('has quoteDate string', () => assert(typeof payload.quoteDate === 'string', 'quoteDate'));
|
|
it('clientName matches', () => eq(payload.clientName, 'Acme Corp', 'clientName'));
|
|
it('contractTerm is display label', () => eq(payload.contractTerm, '12-Month', 'contractTerm'));
|
|
it('licensing is M365-Included', () => eq(payload.licensing, 'M365-Included', 'licensing'));
|
|
|
|
it('users/endpoints/servers are numbers', () => {
|
|
assert(typeof payload.users === 'number', 'users');
|
|
assert(typeof payload.endpoints === 'number', 'endpoints');
|
|
assert(typeof payload.servers === 'number', 'servers');
|
|
});
|
|
|
|
it('addons object has all 6 boolean keys', () => {
|
|
const keys = ['extendedHours', 'passwordManager', 'inkyPro', 'zeroTrustUsers', 'baremetalBackup', 'usbBlocking'];
|
|
keys.forEach(k => {
|
|
assert(typeof payload.addons[k] === 'boolean', `addons.${k} should be boolean`);
|
|
});
|
|
});
|
|
|
|
it('zeroTrustNetwork has seats and routers', () => {
|
|
assert(typeof payload.zeroTrustNetwork.seats === 'number', 'zt seats');
|
|
assert(typeof payload.zeroTrustNetwork.routers === 'number', 'zt routers');
|
|
});
|
|
|
|
it('voip object has required fields', () => {
|
|
assert(typeof payload.voip.seats === 'number', 'voip seats');
|
|
assert(typeof payload.voip.phoneHardware === 'boolean', 'voip phone');
|
|
assert(typeof payload.voip.faxLine === 'boolean', 'voip fax');
|
|
assert(payload.voip.tier === 'standard', 'voip tier');
|
|
});
|
|
|
|
it('voip tier is null when no seats', () => {
|
|
const q2 = calculateQuote(makeState({ users: 1, endpoints: 1, voipSeats: 0 }), DEFAULTS);
|
|
const p2 = buildExportPayload(q2);
|
|
eq(p2.voip.tier, null, 'voip tier null');
|
|
});
|
|
|
|
it('pricing object has all required fields', () => {
|
|
const pricingKeys = ['baseMrr', 'discountPct', 'discountAmt', 'effectiveMrr', 'annual', 'oneTimeFee', 'hstIncluded', 'hstAmt', 'mrrWithHst'];
|
|
pricingKeys.forEach(k => {
|
|
assert(k in payload.pricing, `pricing.${k} missing`);
|
|
});
|
|
});
|
|
|
|
it('pricing.discountPct is integer percentage', () => {
|
|
eq(payload.pricing.discountPct, 3, 'discountPct as integer %');
|
|
});
|
|
|
|
it('pricing.baseMrr matches engine MRR', () => {
|
|
eq(payload.pricing.baseMrr, q.MRR, 'baseMrr');
|
|
});
|
|
|
|
it('pricing.effectiveMrr matches engine', () => {
|
|
eq(payload.pricing.effectiveMrr, q.effectiveMrr, 'effectiveMrr');
|
|
});
|
|
|
|
it('pricing.mrrWithHst matches engine', () => {
|
|
eq(payload.pricing.mrrWithHst, q.mrrWithHst, 'mrrWithHst');
|
|
});
|
|
|
|
it('BYOL licensing label', () => {
|
|
const q2 = calculateQuote(makeState({ users: 1, endpoints: 1, byol: true }), DEFAULTS);
|
|
const p2 = buildExportPayload(q2);
|
|
eq(p2.licensing, 'BYOL', 'licensing');
|
|
});
|
|
|
|
it('m2m contractTerm label', () => {
|
|
const q2 = calculateQuote(makeState({ users: 1, endpoints: 1, contractTerm: 'm2m' }), DEFAULTS);
|
|
const p2 = buildExportPayload(q2);
|
|
eq(p2.contractTerm, 'Month-to-Month', 'contractTerm');
|
|
});
|
|
|
|
it('24mo contractTerm label', () => {
|
|
const q2 = calculateQuote(makeState({ users: 1, endpoints: 1, contractTerm: '24mo' }), DEFAULTS);
|
|
const p2 = buildExportPayload(q2);
|
|
eq(p2.contractTerm, '24-Month', 'contractTerm');
|
|
});
|
|
|
|
it('repName is string', () => {
|
|
assert(typeof payload.repName === 'string', 'repName should be string');
|
|
});
|
|
|
|
it('quoteNotes is string', () => {
|
|
assert(typeof payload.quoteNotes === 'string', 'quoteNotes should be string');
|
|
});
|
|
|
|
it('repName populates from override', () => {
|
|
const p2 = buildExportPayload(q, { repName: 'Jane Doe' });
|
|
eq(p2.repName, 'Jane Doe', 'repName');
|
|
});
|
|
|
|
it('quoteNotes populates from override', () => {
|
|
const p2 = buildExportPayload(q, { quoteNotes: 'Special pricing approved.' });
|
|
eq(p2.quoteNotes, 'Special pricing approved.', 'quoteNotes');
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ── EXPANSION: Persistence State Shape ───────────────────────
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
describe('Persistence state shape (save round-trip)', () => {
|
|
// Simulate the state shape that saveState() produces
|
|
function buildPersistenceState(overrides) {
|
|
return Object.assign({
|
|
clientName: '',
|
|
users: 0,
|
|
endpoints: 0,
|
|
servers: 0,
|
|
byol: false,
|
|
addExtHours: false,
|
|
addPWM: false,
|
|
addINKY: false,
|
|
addZT: false,
|
|
addUSB: false,
|
|
addBMB: false,
|
|
ztSeats: 0,
|
|
ztRouters: 0,
|
|
voipTier: 'basic',
|
|
voipSeats: 0,
|
|
addVoipPhone: false,
|
|
addVoipFax: false,
|
|
phoneBill: 0,
|
|
contractTerm: 'm2m',
|
|
hstEnabled: false,
|
|
oneTimeFee: 0,
|
|
adminWaived: false,
|
|
onboardingWaived: false,
|
|
onboardingManual: false,
|
|
repName: '',
|
|
quoteNotes: ''
|
|
}, overrides);
|
|
}
|
|
|
|
const fullState = buildPersistenceState({
|
|
clientName: 'Test Client',
|
|
users: 15, endpoints: 12, servers: 2,
|
|
byol: true, addExtHours: true, addPWM: true, addINKY: true,
|
|
addZT: true, addUSB: true, addBMB: true,
|
|
ztSeats: 5, ztRouters: 1,
|
|
voipTier: 'premium', voipSeats: 10,
|
|
addVoipPhone: true, addVoipFax: true, phoneBill: 250,
|
|
contractTerm: '24mo', hstEnabled: true,
|
|
oneTimeFee: 500, adminWaived: true,
|
|
onboardingWaived: true, onboardingManual: false
|
|
});
|
|
|
|
it('serializes to valid JSON', () => {
|
|
const json = JSON.stringify(fullState);
|
|
const parsed = JSON.parse(json);
|
|
assert(typeof parsed === 'object', 'valid JSON object');
|
|
});
|
|
|
|
it('round-trip preserves all string fields', () => {
|
|
const parsed = JSON.parse(JSON.stringify(fullState));
|
|
eq(parsed.clientName, 'Test Client', 'clientName');
|
|
eq(parsed.voipTier, 'premium', 'voipTier');
|
|
eq(parsed.contractTerm, '24mo', 'contractTerm');
|
|
});
|
|
|
|
it('round-trip preserves all numeric fields', () => {
|
|
const parsed = JSON.parse(JSON.stringify(fullState));
|
|
eq(parsed.users, 15, 'users');
|
|
eq(parsed.endpoints, 12, 'endpoints');
|
|
eq(parsed.servers, 2, 'servers');
|
|
eq(parsed.ztSeats, 5, 'ztSeats');
|
|
eq(parsed.ztRouters, 1, 'ztRouters');
|
|
eq(parsed.voipSeats, 10, 'voipSeats');
|
|
eq(parsed.phoneBill, 250, 'phoneBill');
|
|
eq(parsed.oneTimeFee, 500, 'oneTimeFee');
|
|
});
|
|
|
|
it('round-trip preserves all boolean fields', () => {
|
|
const parsed = JSON.parse(JSON.stringify(fullState));
|
|
const boolKeys = ['byol', 'addExtHours', 'addPWM', 'addINKY', 'addZT',
|
|
'addUSB', 'addBMB', 'addVoipPhone', 'addVoipFax',
|
|
'hstEnabled', 'adminWaived', 'onboardingWaived', 'onboardingManual'];
|
|
boolKeys.forEach(k => {
|
|
assert(typeof parsed[k] === 'boolean', `${k} should be boolean after round-trip`);
|
|
});
|
|
});
|
|
|
|
it('persistence state feeds engine correctly', () => {
|
|
const q = calculateQuote(fullState, DEFAULTS);
|
|
eq(q.users, 15, 'users');
|
|
eq(q.byol, true, 'byol');
|
|
eq(q.contractTerm, '24mo', 'contractTerm');
|
|
assert(q.MRR > 0, 'MRR > 0');
|
|
});
|
|
|
|
it('default state produces valid zero quote', () => {
|
|
const q = calculateQuote(buildPersistenceState(), DEFAULTS);
|
|
eq(q.users, 0, 'users');
|
|
eq(q.userTotal, 0, 'userTotal');
|
|
eq(q.endpointTotal, 0, 'endpointTotal');
|
|
eq(q.voipTotal, 0, 'voipTotal');
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ── EXPANSION: Import Payload Mapping ────────────────────────
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
describe('Import payload reverse-maps contract term correctly', () => {
|
|
const termReverseMap = {
|
|
'Month-to-Month': 'm2m',
|
|
'12-Month': '12mo',
|
|
'24-Month': '24mo'
|
|
};
|
|
|
|
it('Month-to-Month → m2m', () => eq(termReverseMap['Month-to-Month'], 'm2m', 'term'));
|
|
it('12-Month → 12mo', () => eq(termReverseMap['12-Month'], '12mo', 'term'));
|
|
it('24-Month → 24mo', () => eq(termReverseMap['24-Month'], '24mo', 'term'));
|
|
it('unknown defaults to m2m', () => eq(termReverseMap['bogus'] || 'm2m', 'm2m', 'fallback'));
|
|
});
|
|
|
|
describe('Import → engine round-trip (full payload)', () => {
|
|
// Build an export payload, then feed it back through the engine
|
|
const original = calculateQuote(makeState({
|
|
users: 20, endpoints: 18, servers: 2,
|
|
byol: true, addPWM: true, addINKY: true,
|
|
ztSeats: 3, ztRouters: 1,
|
|
voipSeats: 12, voipTier: 'premium', addVoipPhone: true,
|
|
contractTerm: '12mo', hstEnabled: true,
|
|
clientName: 'Round Trip Inc'
|
|
}), DEFAULTS);
|
|
|
|
// Simulate import: map export payload fields back to engine state
|
|
const imported = makeState({
|
|
users: original.users,
|
|
endpoints: original.endpoints,
|
|
servers: original.servers,
|
|
byol: original.byol,
|
|
addPWM: original.addPWM,
|
|
addINKY: original.addINKY,
|
|
addExtHours: original.addExtHours,
|
|
addZT: original.addZT,
|
|
addUSB: original.addUSB,
|
|
addBMB: original.addBMB,
|
|
ztSeats: original.ztSeats,
|
|
ztRouters: original.ztRouters,
|
|
voipTier: original.voipTier,
|
|
voipSeats: original.voipSeats,
|
|
addVoipPhone: original.addVoipPhone,
|
|
addVoipFax: original.addVoipFax,
|
|
contractTerm: original.contractTerm,
|
|
hstEnabled: original.hstEnabled,
|
|
clientName: original.clientName,
|
|
adminWaived: original.adminWaived
|
|
});
|
|
|
|
const reimported = calculateQuote(imported, DEFAULTS);
|
|
|
|
it('MRR matches after round-trip', () => eq(reimported.MRR, original.MRR, 'MRR'));
|
|
it('effectiveMrr matches', () => eq(reimported.effectiveMrr, original.effectiveMrr, 'effectiveMrr'));
|
|
it('mrrWithHst matches', () => eq(reimported.mrrWithHst, original.mrrWithHst, 'mrrWithHst'));
|
|
it('userTotal matches', () => eq(reimported.userTotal, original.userTotal, 'userTotal'));
|
|
it('endpointTotal matches', () => eq(reimported.endpointTotal, original.endpointTotal, 'endpointTotal'));
|
|
it('voipTotal matches', () => eq(reimported.voipTotal, original.voipTotal, 'voipTotal'));
|
|
it('adminFeeNet matches', () => eq(reimported.adminFeeNet, original.adminFeeNet, 'adminFeeNet'));
|
|
it('effectiveAnnual matches', () => eq(reimported.effectiveAnnual, original.effectiveAnnual, 'effectiveAnnual'));
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ── EXPANSION: Quote Output Invariants ───────────────────────
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
describe('Quote output invariants across random configs', () => {
|
|
const configs = [
|
|
{ users: 1, endpoints: 1 },
|
|
{ users: 25, endpoints: 25, servers: 5, addPWM: true },
|
|
{ users: 0, endpoints: 0, voipSeats: 30, voipTier: 'premium' },
|
|
{ users: 50, endpoints: 50, addZT: true, ztSeats: 20, ztRouters: 5, contractTerm: '24mo', hstEnabled: true },
|
|
{ users: 10, endpoints: 10, adminWaived: true, contractTerm: '12mo' },
|
|
{ users: 3, endpoints: 3, byol: true, addExtHours: true, addINKY: true }
|
|
];
|
|
|
|
configs.forEach((cfg, i) => {
|
|
const q = calculateQuote(makeState(cfg), DEFAULTS);
|
|
|
|
it(`config #${i + 1}: effectiveMrr = MRR - discountAmt`, () => {
|
|
eq(q.effectiveMrr, q.MRR - q.discountAmt, 'effectiveMrr');
|
|
});
|
|
|
|
it(`config #${i + 1}: effectiveAnnual = effectiveMrr * 12`, () => {
|
|
eq(q.effectiveAnnual, q.effectiveMrr * 12, 'effectiveAnnual');
|
|
});
|
|
|
|
it(`config #${i + 1}: mrrWithHst = effectiveMrr + hstAmt`, () => {
|
|
eq(q.mrrWithHst, q.effectiveMrr + q.hstAmt, 'mrrWithHst');
|
|
});
|
|
|
|
it(`config #${i + 1}: all monetary values ≥ 0`, () => {
|
|
assert(q.MRR >= 0, 'MRR >= 0');
|
|
assert(q.effectiveMrr >= 0, 'effectiveMrr >= 0');
|
|
assert(q.hstAmt >= 0, 'hstAmt >= 0');
|
|
assert(q.mrrWithHst >= 0, 'mrrWithHst >= 0');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ── Results ──────────────────────────────────────────────────
|
|
console.log(`\n${'─'.repeat(50)}`);
|
|
if (_failed === 0) {
|
|
console.log(`\x1b[32m All ${_passed} tests passed.\x1b[0m\n`);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`\x1b[31m ${_failed} of ${_passed + _failed} tests failed.\x1b[0m\n`);
|
|
process.exit(1);
|
|
}
|