|
|
"""
|
|
|
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."""
|
|
|
|
|
|
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."""
|
|
|
|
|
|
result = self.calc.calculate(gross_income=800_000, period="annual")
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
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
|
|
|
)
|
|
|
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
|
|
|
)
|
|
|
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)
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
self.assertEqual(result.gross_annual_income, 6_000_000)
|
|
|
|
|
|
|
|
|
self.assertEqual(result.deductions.pension_contribution, 480_000)
|
|
|
|
|
|
|
|
|
self.assertEqual(result.deductions.nhf_contribution, 150_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."""
|
|
|
|
|
|
result = self.calc.calculate(
|
|
|
gross_income=3_000_000,
|
|
|
period="annual",
|
|
|
pension_contribution=0,
|
|
|
nhf_contribution=0
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|