/** * 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 };