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