""" Test suite for NTA 2026 Tax Calculation Engine. Validates: - Tax config consistency - PAYE calculations accuracy - Progressive band computations - Deduction calculations - Response formatting """ import unittest from datetime import date from decimal import Decimal from tax_config import ( get_regime, NTA_2026_CONFIG, PITA_2025_CONFIG, TaxBand, format_bands, CIT_RATES, VAT_CONFIG ) from paye_calculator import PAYECalculator, calculate_paye class TestTaxConfig(unittest.TestCase): """Test tax configuration module.""" def test_nta_2026_regime_exists(self): """NTA 2026 should be the default regime.""" regime = get_regime("NTA_2026") self.assertEqual(regime.code, "NTA_2026") self.assertEqual(regime.name, "Nigeria Tax Act 2026") def test_nta_2026_has_six_bands(self): """NTA 2026 should have 6 tax bands.""" regime = get_regime("NTA_2026") self.assertEqual(len(regime.bands), 6) def test_first_band_is_tax_free(self): """First N800,000 should be tax-free.""" regime = get_regime("NTA_2026") first_band = regime.bands[0] self.assertEqual(first_band.lower, 0) self.assertEqual(first_band.upper, 800_000) self.assertEqual(first_band.rate, 0.00) def test_highest_band_is_25_percent(self): """Highest band should be 25%.""" regime = get_regime("NTA_2026") last_band = regime.bands[-1] self.assertEqual(last_band.rate, 0.25) def test_rent_relief_enabled(self): """NTA 2026 should have rent relief enabled.""" regime = get_regime("NTA_2026") self.assertTrue(regime.rent_relief_enabled) self.assertEqual(regime.rent_relief_cap, 500_000) def test_cra_disabled_in_nta_2026(self): """CRA should be disabled in NTA 2026.""" regime = get_regime("NTA_2026") self.assertFalse(regime.cra_enabled) def test_cit_rates(self): """CIT rates should be correctly defined.""" self.assertEqual(CIT_RATES["small"]["rate"], 0.00) self.assertEqual(CIT_RATES["medium"]["rate"], 0.20) self.assertEqual(CIT_RATES["large"]["rate"], 0.30) def test_vat_rate(self): """VAT rate should be 7.5%.""" self.assertEqual(VAT_CONFIG["rate"], 0.075) class TestPAYECalculator(unittest.TestCase): """Test PAYE calculator.""" def setUp(self): self.calc = PAYECalculator("NTA_2026") def test_zero_income(self): """Zero income should have zero tax.""" result = self.calc.calculate(gross_income=0) self.assertEqual(result.final_tax, 0) self.assertEqual(result.effective_rate, 0) def test_minimum_wage_exempt(self): """Income at minimum wage should be exempt.""" # Annual minimum wage = 70,000 * 12 = 840,000 result = self.calc.calculate(gross_income=840_000, period="annual") self.assertEqual(result.final_tax, 0) def test_tax_free_first_800k(self): """First N800,000 taxable income should be tax-free.""" # With deductions, need higher gross to get N800k taxable result = self.calc.calculate(gross_income=800_000, period="annual") # After deductions, taxable < 800k, so tax should be 0 self.assertEqual(result.computed_tax, 0) def test_progressive_taxation(self): """Higher income should pay progressive rates.""" low_result = self.calc.calculate(gross_income=3_000_000, period="annual") high_result = self.calc.calculate(gross_income=30_000_000, period="annual") # Higher income should have higher effective rate self.assertGreater(high_result.effective_rate, low_result.effective_rate) def test_pension_deduction(self): """Pension should default to 8% of gross.""" result = self.calc.calculate(gross_income=1_000_000, period="annual") expected_pension = 1_000_000 * 0.08 self.assertEqual(result.deductions.pension_contribution, expected_pension) def test_nhf_deduction(self): """NHF should default to 2.5% of gross.""" result = self.calc.calculate(gross_income=1_000_000, period="annual") expected_nhf = 1_000_000 * 0.025 self.assertEqual(result.deductions.nhf_contribution, expected_nhf) def test_rent_relief_capped(self): """Rent relief should be capped at N500,000.""" result = self.calc.calculate( gross_income=100_000_000, period="annual", annual_rent_paid=10_000_000 # Would be 2M at 20%, but capped ) self.assertEqual(result.deductions.rent_relief, 500_000) def test_rent_relief_calculation(self): """Rent relief should be 20% of rent paid, up to cap.""" result = self.calc.calculate( gross_income=10_000_000, period="annual", annual_rent_paid=1_000_000 # 20% = 200k, under cap ) self.assertEqual(result.deductions.rent_relief, 200_000) def test_tax_never_exceeds_income(self): """Tax should never exceed gross income.""" for income in [100_000, 1_000_000, 10_000_000, 100_000_000]: result = self.calc.calculate(gross_income=income, period="annual") self.assertLess(result.final_tax, result.gross_annual_income) def test_effective_rate_below_max(self): """Effective rate should never exceed 25%.""" result = self.calc.calculate(gross_income=1_000_000_000, period="annual") self.assertLess(result.effective_rate, 30) # Some margin for calculation def test_monthly_to_annual_conversion(self): """Monthly calculations should convert correctly.""" monthly_result = self.calc.calculate(gross_income=500_000, period="monthly") annual_result = self.calc.calculate(gross_income=6_000_000, period="annual") # Should be approximately equal self.assertAlmostEqual( monthly_result.gross_annual_income, annual_result.gross_annual_income, places=0 ) class TestCalculationAccuracy(unittest.TestCase): """Test specific calculation scenarios for accuracy.""" def setUp(self): self.calc = PAYECalculator("NTA_2026") def test_500k_monthly_scenario(self): """Verify N500k monthly calculation.""" result = self.calc.calculate(gross_income=500_000, period="monthly") # Gross annual = 6M self.assertEqual(result.gross_annual_income, 6_000_000) # Pension = 8% of 6M = 480k self.assertEqual(result.deductions.pension_contribution, 480_000) # NHF = 2.5% of 6M = 150k self.assertEqual(result.deductions.nhf_contribution, 150_000) # Taxable = 6M - 480k - 150k = 5,370,000 expected_taxable = 6_000_000 - 480_000 - 150_000 self.assertEqual(result.taxable_income, expected_taxable) def test_band_calculations(self): """Verify band-by-band calculations.""" # Use a simple taxable income of exactly 3M result = self.calc.calculate( gross_income=3_000_000, period="annual", pension_contribution=0, # Override to simplify nhf_contribution=0 ) # First 800k at 0% = 0 # 800k-3M at 15% = 2,200,000 * 0.15 = 330,000 expected_tax = 0 + (2_200_000 * 0.15) self.assertAlmostEqual(result.computed_tax, expected_tax, places=0) class TestOutputFormatting(unittest.TestCase): """Test output formatting.""" def setUp(self): self.calc = PAYECalculator("NTA_2026") self.result = self.calc.calculate(gross_income=500_000, period="monthly") def test_whatsapp_format_no_emojis(self): """WhatsApp format should not contain emojis.""" output = self.calc.format_whatsapp(self.result) # Common emoji codepoint ranges emoji_patterns = ['📊', '💰', '✅', '❌', '📈', '🔴', '🟢'] for emoji in emoji_patterns: self.assertNotIn(emoji, output) def test_whatsapp_format_has_key_info(self): """WhatsApp format should contain key information.""" output = self.calc.format_whatsapp(self.result) self.assertIn("Gross Income", output) self.assertIn("Tax Payable", output) self.assertIn("Take-Home", output) self.assertIn("Kaanta", output) def test_web_format_is_dict(self): """Web format should return a dictionary.""" output = self.calc.format_web(self.result) self.assertIsInstance(output, dict) self.assertIn("summary", output) self.assertIn("income", output) self.assertIn("tax", output) def test_detailed_format_has_sections(self): """Detailed format should have all sections.""" output = self.calc.format_detailed(self.result) self.assertIn("INCOME", output) self.assertIn("DEDUCTIONS", output) self.assertIn("TAX COMPUTATION", output) self.assertIn("LEGAL BASIS", output) class TestValidation(unittest.TestCase): """Test calculation validation.""" def setUp(self): self.calc = PAYECalculator("NTA_2026") def test_normal_calculation_is_valid(self): """Normal calculations should pass validation.""" result = self.calc.calculate(gross_income=5_000_000, period="annual") self.assertTrue(result.validation.is_valid) def test_confidence_score(self): """Confidence score should be between 0 and 1.""" result = self.calc.calculate(gross_income=5_000_000, period="annual") self.assertGreaterEqual(result.validation.confidence, 0) self.assertLessEqual(result.validation.confidence, 1) if __name__ == "__main__": unittest.main(verbosity=2)