|
|
"""
|
|
|
Centralized Tax Configuration for Nigeria Tax Act 2026.
|
|
|
|
|
|
Single source of truth for tax brackets, rates, reliefs, and thresholds.
|
|
|
All tax calculations MUST reference this module.
|
|
|
"""
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
from typing import Dict, List, Optional, Any
|
|
|
from datetime import date
|
|
|
from enum import Enum
|
|
|
|
|
|
|
|
|
class TaxRegime(Enum):
|
|
|
"""Available tax regimes."""
|
|
|
PITA_2025 = "pita_2025"
|
|
|
NTA_2026 = "nta_2026"
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
class TaxBand:
|
|
|
"""Immutable tax band definition."""
|
|
|
lower: float
|
|
|
upper: float
|
|
|
rate: float
|
|
|
|
|
|
@property
|
|
|
def rate_percent(self) -> float:
|
|
|
return self.rate * 100
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
class TaxRegimeConfig:
|
|
|
"""Complete configuration for a tax regime."""
|
|
|
name: str
|
|
|
code: str
|
|
|
effective_from: date
|
|
|
effective_to: Optional[date]
|
|
|
|
|
|
|
|
|
bands: tuple
|
|
|
|
|
|
|
|
|
cra_enabled: bool
|
|
|
cra_fixed_amount: float
|
|
|
cra_percent_of_gross: float
|
|
|
cra_additional_percent: float
|
|
|
|
|
|
|
|
|
rent_relief_enabled: bool
|
|
|
rent_relief_cap: float
|
|
|
rent_relief_percent: float
|
|
|
|
|
|
|
|
|
minimum_tax_rate: float
|
|
|
|
|
|
|
|
|
minimum_wage_monthly: float
|
|
|
|
|
|
|
|
|
pension_rate: float
|
|
|
nhf_rate: float
|
|
|
nhis_rate: float
|
|
|
|
|
|
|
|
|
authority: str
|
|
|
|
|
|
|
|
|
|
|
|
NTA_2026_CONFIG = TaxRegimeConfig(
|
|
|
name="Nigeria Tax Act 2026",
|
|
|
code="NTA_2026",
|
|
|
effective_from=date(2026, 1, 1),
|
|
|
effective_to=None,
|
|
|
|
|
|
bands=(
|
|
|
TaxBand(0, 800_000, 0.00),
|
|
|
TaxBand(800_000, 3_000_000, 0.15),
|
|
|
TaxBand(3_000_000, 12_000_000, 0.18),
|
|
|
TaxBand(12_000_000, 25_000_000, 0.21),
|
|
|
TaxBand(25_000_000, 50_000_000, 0.23),
|
|
|
TaxBand(50_000_000, float('inf'), 0.25),
|
|
|
),
|
|
|
|
|
|
|
|
|
cra_enabled=False,
|
|
|
cra_fixed_amount=0,
|
|
|
cra_percent_of_gross=0,
|
|
|
cra_additional_percent=0,
|
|
|
|
|
|
|
|
|
rent_relief_enabled=True,
|
|
|
rent_relief_cap=500_000,
|
|
|
rent_relief_percent=0.20,
|
|
|
|
|
|
|
|
|
minimum_tax_rate=0.0,
|
|
|
|
|
|
|
|
|
minimum_wage_monthly=70_000,
|
|
|
|
|
|
|
|
|
pension_rate=0.08,
|
|
|
nhf_rate=0.025,
|
|
|
nhis_rate=0.05,
|
|
|
|
|
|
authority="Nigeria Tax Act, 2025 (effective 2026)"
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
PITA_2025_CONFIG = TaxRegimeConfig(
|
|
|
name="Personal Income Tax Act 2025",
|
|
|
code="PITA_2025",
|
|
|
effective_from=date(2011, 1, 1),
|
|
|
effective_to=date(2025, 12, 31),
|
|
|
|
|
|
bands=(
|
|
|
TaxBand(0, 300_000, 0.07),
|
|
|
TaxBand(300_000, 600_000, 0.11),
|
|
|
TaxBand(600_000, 1_100_000, 0.15),
|
|
|
TaxBand(1_100_000, 1_600_000, 0.19),
|
|
|
TaxBand(1_600_000, 3_200_000, 0.21),
|
|
|
TaxBand(3_200_000, float('inf'), 0.24),
|
|
|
),
|
|
|
|
|
|
|
|
|
cra_enabled=True,
|
|
|
cra_fixed_amount=200_000,
|
|
|
cra_percent_of_gross=0.01,
|
|
|
cra_additional_percent=0.20,
|
|
|
|
|
|
|
|
|
rent_relief_enabled=False,
|
|
|
rent_relief_cap=0,
|
|
|
rent_relief_percent=0,
|
|
|
|
|
|
minimum_tax_rate=0.01,
|
|
|
minimum_wage_monthly=70_000,
|
|
|
|
|
|
pension_rate=0.08,
|
|
|
nhf_rate=0.025,
|
|
|
nhis_rate=0.05,
|
|
|
|
|
|
authority="Personal Income Tax Act (as amended), PITA s.33, First Schedule"
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
TAX_REGIMES: Dict[str, TaxRegimeConfig] = {
|
|
|
"NTA_2026": NTA_2026_CONFIG,
|
|
|
"PITA_2025": PITA_2025_CONFIG,
|
|
|
}
|
|
|
|
|
|
|
|
|
DEFAULT_REGIME = "NTA_2026"
|
|
|
|
|
|
|
|
|
def get_regime(code: str = None) -> TaxRegimeConfig:
|
|
|
"""Get a tax regime configuration by code."""
|
|
|
code = code or DEFAULT_REGIME
|
|
|
if code not in TAX_REGIMES:
|
|
|
raise ValueError(f"Unknown tax regime: {code}. Available: {list(TAX_REGIMES.keys())}")
|
|
|
return TAX_REGIMES[code]
|
|
|
|
|
|
|
|
|
def get_active_regime(as_of: date = None) -> TaxRegimeConfig:
|
|
|
"""Get the applicable tax regime for a given date."""
|
|
|
as_of = as_of or date.today()
|
|
|
|
|
|
for regime in TAX_REGIMES.values():
|
|
|
if regime.effective_from <= as_of:
|
|
|
if regime.effective_to is None or as_of <= regime.effective_to:
|
|
|
return regime
|
|
|
|
|
|
|
|
|
return TAX_REGIMES[DEFAULT_REGIME]
|
|
|
|
|
|
|
|
|
def format_bands(regime: TaxRegimeConfig = None) -> str:
|
|
|
"""Format tax bands for display."""
|
|
|
regime = regime or get_regime()
|
|
|
lines = [f"Tax Bands - {regime.name}", "=" * 50]
|
|
|
|
|
|
for band in regime.bands:
|
|
|
if band.upper == float('inf'):
|
|
|
lines.append(f"Above N{band.lower:,.0f}: {band.rate_percent:.0f}%")
|
|
|
elif band.rate == 0:
|
|
|
lines.append(f"N{band.lower:,.0f} - N{band.upper:,.0f}: TAX FREE")
|
|
|
else:
|
|
|
lines.append(f"N{band.lower:,.0f} - N{band.upper:,.0f}: {band.rate_percent:.0f}%")
|
|
|
|
|
|
lines.append(f"\nLegal Basis: {regime.authority}")
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
CIT_RATES = {
|
|
|
"small": {
|
|
|
"threshold": 25_000_000,
|
|
|
"rate": 0.00,
|
|
|
"description": "Small company (turnover <= N25m): 0%"
|
|
|
},
|
|
|
"medium": {
|
|
|
"threshold": 100_000_000,
|
|
|
"rate": 0.20,
|
|
|
"description": "Medium company (N25m < turnover < N100m): 20%"
|
|
|
},
|
|
|
"large": {
|
|
|
"threshold": float('inf'),
|
|
|
"rate": 0.30,
|
|
|
"description": "Large company (turnover >= N100m): 30%"
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VAT_CONFIG = {
|
|
|
"rate": 0.075,
|
|
|
"registration_threshold": 25_000_000,
|
|
|
"exempt_goods": [
|
|
|
"basic food items",
|
|
|
"medical and pharmaceutical products",
|
|
|
"educational materials",
|
|
|
"exported services"
|
|
|
]
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
WHT_RATES = {
|
|
|
"dividend": 0.10,
|
|
|
"interest": 0.10,
|
|
|
"rent": 0.10,
|
|
|
"royalty": 0.10,
|
|
|
"contract": 0.05,
|
|
|
"consultancy": 0.05,
|
|
|
"director_fees": 0.10,
|
|
|
}
|
|
|
|