process-aware-ai / frontend /__tests__ /calculation-utils.test.ts
borndeveloper's picture
Deploy Process Aware AI Dashboard without binaries
b4a2e7f
Raw
History Blame Contribute Delete
10.1 kB
/**
* Calculation Utilities Tests
* Verifies that frontend calculation logic matches backend formulas exactly.
*/
import { ManualCalculator, mockSaleOrderResponse } from './test-data-mocking';
// Test configuration
const TOLERANCE = 0.5; // Allow 0.5% tolerance for floating point differences
interface TestResult {
name: string;
passed: boolean;
expected: number;
actual: number;
difference: number;
error?: string;
}
interface TestSuite {
category: string;
results: TestResult[];
passed: number;
failed: number;
}
const testResults: TestSuite[] = [];
function runTest(category: string, name: string, expected: number, actual: number): TestResult {
const difference = Math.abs(expected - actual);
const passed = difference <= TOLERANCE;
const result: TestResult = {
name,
passed,
expected,
actual,
difference
};
// Find or create category suite
let suite = testResults.find(s => s.category === category);
if (!suite) {
suite = { category, results: [], passed: 0, failed: 0 };
testResults.push(suite);
}
suite.results.push(result);
if (passed) {
suite.passed++;
} else {
suite.failed++;
}
return result;
}
// ============================================
// TEST SUITE 1: Percentage Calculations
// ============================================
function testPercentageCalculations() {
const m = mockSaleOrderResponse.metrics;
const orderQty = m["Order Qty"];
const reserved = m["Reserved Qty"];
const issued = m["Actual Issued"];
const packing = m["Total Packing"];
const packFresh = m["Pack Fresh"];
// Extra Gr Reserved %
runTest(
"Percentage Calculations",
"Extra Gr Reserved %",
m["Extra Gr Reserved %"],
ManualCalculator.extra_gr_reserved_pct(reserved, orderQty)
);
// Actual Gr Issue %
runTest(
"Percentage Calculations",
"Actual Gr Issue %",
m["Actual Gr Issue %"],
ManualCalculator.actual_gr_issue_pct(issued, orderQty)
);
// Shrinkage %
runTest(
"Percentage Calculations",
"Shrinkage %",
m["Shrinkage %"],
ManualCalculator.shrinkage_pct(issued, packing)
);
// Fresh Pkg %
runTest(
"Percentage Calculations",
"Fresh Pkg %",
m["Fresh Pkg %"],
ManualCalculator.fresh_pkg_pct(packFresh, packing)
);
// Fresh Yield %
runTest(
"Percentage Calculations",
"Fresh Yield %",
m["Fresh Yield %"],
ManualCalculator.fresh_yield_pct(packFresh, issued)
);
}
// ============================================
// TEST SUITE 2: Shortfall & Status
// ============================================
function testShortfallCalculations() {
const m = mockSaleOrderResponse.metrics;
const orderQty = m["Order Qty"];
const packFresh = m["Pack Fresh"];
// Shortfall
runTest(
"Shortfall & Status",
"Shortfall",
m["Shortfall"],
ManualCalculator.shortfall(orderQty, packFresh)
);
// Status determination
const expectedStatus = m["Status"];
const calculatedShortfall = ManualCalculator.shortfall(orderQty, packFresh);
const actualStatus = calculatedShortfall > 0 ? "Shortfall" : "Fulfilled";
runTest(
"Shortfall & Status",
"Status",
expectedStatus === actualStatus ? 1 : 0,
1
);
}
// ============================================
// TEST SUITE 3: Waterfall Calculations
// ============================================
function testWaterfallCalculations() {
const waterfall = mockSaleOrderResponse.intelligence.waterfall;
// Verify waterfall values
const demand = waterfall.find((w: any) => w.label === "Demand")?.value || 0;
const policyGap = waterfall.find((w: any) => w.label === "Policy Gap")?.value || 0;
const executionAdj = waterfall.find((w: any) => w.label === "Execution Adj")?.value || 0;
const processLoss = waterfall.find((w: any) => w.label === "Process Loss")?.value || 0;
const delivered = waterfall.find((w: any) => w.label === "Delivered")?.value || 0;
// Waterfall should add up: Demand + Policy + Execution + Process ≈ Delivered
const calculatedDelivered = demand + policyGap + executionAdj + processLoss;
runTest(
"Waterfall",
"Waterfall Sum",
delivered,
calculatedDelivered
);
// Verify individual components against metrics
const m = mockSaleOrderResponse.metrics;
runTest(
"Waterfall",
"Demand = Order Qty",
m["Order Qty"],
demand
);
runTest(
"Waterfall",
"Delivered = Pack Fresh",
m["Pack Fresh"],
delivered
);
}
// ============================================
// TEST SUITE 4: Blame Attribution
// ============================================
function testBlameAttribution() {
const blame = mockSaleOrderResponse.intelligence.blame_breakdown;
// Blame percentages should sum to 100%
const totalPct = blame.policy_pct + blame.execution_pct + blame.process_pct;
runTest(
"Blame Attribution",
"Total % = 100",
100,
totalPct
);
// Verify individual impacts match waterfall
const waterfall = mockSaleOrderResponse.intelligence.waterfall;
const policyGap = Math.abs(waterfall.find((w: any) => w.label === "Policy Gap")?.value || 0);
const executionAdj = Math.abs(waterfall.find((w: any) => w.label === "Execution Adj")?.value || 0);
const processLoss = Math.abs(waterfall.find((w: any) => w.label === "Process Loss")?.value || 0);
const totalImpact = policyGap + executionAdj + processLoss;
// Verify policy percentage calculation
const expectedPolicyPct = (policyGap / totalImpact) * 100;
runTest(
"Blame Attribution",
"Policy %",
blame.policy_pct,
expectedPolicyPct
);
// Verify execution percentage calculation
const expectedExecutionPct = (executionAdj / totalImpact) * 100;
runTest(
"Blame Attribution",
"Execution %",
blame.execution_pct,
expectedExecutionPct
);
// Verify process percentage calculation
const expectedProcessPct = (processLoss / totalImpact) * 100;
runTest(
"Blame Attribution",
"Process %",
blame.process_pct,
expectedProcessPct
);
}
// ============================================
// TEST SUITE 5: Risk Fingerprint
// ============================================
function testRiskFingerprint() {
const risk = mockSaleOrderResponse.intelligence.risk_fingerprint;
const normAdequacy = mockSaleOrderResponse.intelligence.norm_adequacy;
// Norm reliability should be norm_adequacy / 100
const expectedReliability = normAdequacy / 100;
runTest(
"Risk Fingerprint",
"Norm Reliability",
risk.norm_reliability,
expectedReliability
);
// Risk level determination
let expectedRiskLevel = "HIGH";
if (risk.norm_reliability >= 0.98) {
expectedRiskLevel = "LOW";
} else if (risk.norm_reliability >= 0.95) {
expectedRiskLevel = "MEDIUM";
}
runTest(
"Risk Fingerprint",
"Risk Level",
risk.risk_level === expectedRiskLevel ? 1 : 0,
1
);
}
// ============================================
// TEST SUITE 6: Yield & Norm Score
// ============================================
function testYieldAndNormScore() {
const m = mockSaleOrderResponse.metrics;
const intel = mockSaleOrderResponse.intelligence;
// Yield rate = Pack Fresh / Issued * 100
runTest(
"Yield & Norm Score",
"Yield Rate",
intel.yield_rate,
ManualCalculator.yield_rate(m["Pack Fresh"], m["Actual Issued"])
);
// Norm adequacy = Pack Fresh / Order Qty * 100
runTest(
"Yield & Norm Score",
"Norm Adequacy",
intel.norm_adequacy,
ManualCalculator.norm_score(m["Pack Fresh"], m["Order Qty"])
);
}
// ============================================
// TEST SUITE 7: Edge Cases
// ============================================
function testEdgeCases() {
// Zero division handling
runTest(
"Edge Cases",
"Zero PO Qty - Extra Gr %",
0,
ManualCalculator.extra_gr_reserved_pct(100, 0)
);
runTest(
"Edge Cases",
"Zero Issued - Yield",
0,
ManualCalculator.yield_rate(100, 0)
);
runTest(
"Edge Cases",
"Zero Order Qty - Norm Score",
0,
ManualCalculator.norm_score(100, 0)
);
// Negative values (under-issuance)
runTest(
"Edge Cases",
"Negative Deviation",
-10,
ManualCalculator.extra_gr_reserved_pct(90, 100)
);
}
// ============================================
// RUN ALL TESTS
// ============================================
export function runAllCalculationTests(): {
suites: TestSuite[];
summary: {
total: number;
passed: number;
failed: number;
passRate: number;
};
} {
// Clear previous results
testResults.length = 0;
// Run all test suites
testPercentageCalculations();
testShortfallCalculations();
testWaterfallCalculations();
testBlameAttribution();
testRiskFingerprint();
testYieldAndNormScore();
testEdgeCases();
// Calculate summary
const total = testResults.reduce((sum, s) => sum + s.passed + s.failed, 0);
const passed = testResults.reduce((sum, s) => sum + s.passed, 0);
const failed = testResults.reduce((sum, s) => sum + s.failed, 0);
return {
suites: testResults,
summary: {
total,
passed,
failed,
passRate: total > 0 ? (passed / total) * 100 : 0
}
};
}
// Export for running
export { testResults };