Kaanta / tax_config.py
Eniiyanu's picture
Upload 8 files
8f0ef5f verified
"""
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" # Personal Income Tax Act (pre-2026)
NTA_2026 = "nta_2026" # Nigeria Tax Act 2026 (primary)
@dataclass(frozen=True)
class TaxBand:
"""Immutable tax band definition."""
lower: float
upper: float # Use float('inf') for unbounded
rate: float # Decimal (0.15 = 15%)
@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]
# Tax bands (progressive)
bands: tuple # Tuple of TaxBand
# Relief settings
cra_enabled: bool
cra_fixed_amount: float # e.g., 200,000
cra_percent_of_gross: float # e.g., 0.01 (1%)
cra_additional_percent: float # e.g., 0.20 (20%)
# Rent relief (NTA 2026)
rent_relief_enabled: bool
rent_relief_cap: float
rent_relief_percent: float
# Minimum tax
minimum_tax_rate: float # e.g., 0.01 (1%)
# Minimum wage exemption
minimum_wage_monthly: float
# Standard deduction rates
pension_rate: float # Employee contribution
nhf_rate: float # National Housing Fund
nhis_rate: float # National Health Insurance
# Legal citation
authority: str
# Nigeria Tax Act 2026 - PRIMARY REGIME
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), # 0% - Tax free
TaxBand(800_000, 3_000_000, 0.15), # 15%
TaxBand(3_000_000, 12_000_000, 0.18), # 18%
TaxBand(12_000_000, 25_000_000, 0.21), # 21%
TaxBand(25_000_000, 50_000_000, 0.23), # 23%
TaxBand(50_000_000, float('inf'), 0.25), # 25%
),
# CRA replaced by rent relief in NTA 2026
cra_enabled=False,
cra_fixed_amount=0,
cra_percent_of_gross=0,
cra_additional_percent=0,
# Rent relief replaces CRA
rent_relief_enabled=True,
rent_relief_cap=500_000,
rent_relief_percent=0.20,
# Minimum tax - NOT in NTA 2026 (was in old PITA only)
minimum_tax_rate=0.0,
# Minimum wage (2024 rate, pending update)
minimum_wage_monthly=70_000,
# Standard deductions
pension_rate=0.08, # 8% employee contribution
nhf_rate=0.025, # 2.5%
nhis_rate=0.05, # 5% (if enrolled)
authority="Nigeria Tax Act, 2025 (effective 2026)"
)
# PITA 2025 - LEGACY (for reference/comparison)
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
cra_enabled=True,
cra_fixed_amount=200_000,
cra_percent_of_gross=0.01,
cra_additional_percent=0.20,
# No rent relief
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"
)
# Registry of all regimes
TAX_REGIMES: Dict[str, TaxRegimeConfig] = {
"NTA_2026": NTA_2026_CONFIG,
"PITA_2025": PITA_2025_CONFIG,
}
# Default regime
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
# Fallback to default
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)
# Company Income Tax rates (NTA 2026)
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 configuration
VAT_CONFIG = {
"rate": 0.075, # 7.5%
"registration_threshold": 25_000_000,
"exempt_goods": [
"basic food items",
"medical and pharmaceutical products",
"educational materials",
"exported services"
]
}
# Withholding Tax rates
WHT_RATES = {
"dividend": 0.10,
"interest": 0.10,
"rent": 0.10,
"royalty": 0.10,
"contract": 0.05,
"consultancy": 0.05,
"director_fees": 0.10,
}