Overview
The Tax Engine provides jurisdiction-specific tax calculations. Each jurisdiction has its own engine implementing the TaxEngine interface. Currently, Nigeria (NG) is fully implemented with support for VAT, WHT, Stamp Duty, and NITDA Levy.
File Locations
backend/internal/services/tax/
├── engine.go # TaxEngine interface, TaxContext, TaxResult
├── nigeria_engine.go # Nigeria-specific implementation
└── tax_calculation_service.go # Service wrapper
backend/internal/taxserver/
└── tax_server.go # gRPC server with currency conversion
backend/internal/paymentv3server/
└── payment_v3_server.go # Tax preview with currency conversionDependencies
CurrencyService- For multi-currency conversion to NGN (injected into PaymentV3Server and TaxServer)models.Jurisdiction- Jurisdiction configurationmodels.VATRule- VAT rules from databasemodels.WHTRule- WHT rules from databasemodels.StampDutyRule- Stamp duty rules from database
Architecture
TaxEngine Interface
type TaxEngine interface {
Calculate(ctx *TaxContext) (*TaxResult, error)
ValidateCompliance(ctx *TaxContext, result *TaxResult) []string
IncorporateProviderCollectedTax(result *TaxResult, providerTax *ProviderTaxInfo) error
}Engine Registration
func NewTaxCalculationService(db *gorm.DB) (*TaxCalculationService, error) {
nigeriaEngine, err := NewNigeriaTaxEngine(db)
if err != nil {
return nil, err
}
engines := map[string]TaxEngine{
"NG": nigeriaEngine,
// Future: "US": usEngine, "UK": ukEngine
}
return &TaxCalculationService{
db: db,
engines: engines,
}, nil
}Nigeria Tax Engine
The Nigeria engine implements FIRS (Federal Inland Revenue Service) tax rules including VAT, WHT, Stamp Duty, and NITDA Levy.
Supported Tax Types
VAT (Value Added Tax)
7.5% on goods and services
Authority: FIRS
WHT (Withholding Tax)
5-10% depending on service type
Authority: FIRS
Stamp Duty
₦50 flat on receipts ≥ ₦10,000
Authority: FIRS
NITDA Levy
1% on digital services
Authority: NITDA
Tax Determination Flow
TaxContext Structure
TaxContext carries all information needed for tax calculation.
Core Fields
type TaxContext struct {
// Transaction identity
ContextType string // "sale" or "expense"
OccurredAt time.Time // Transaction date
// Amounts
Amount float64 // Original amount
Currency string // Original currency (e.g., "USD", "EUR")
AmountNGN float64 // Amount converted to NGN (for thresholds)
FXRate float64 // Exchange rate used for conversion
// Jurisdiction
SellerJurisdictionCode string
CustomerJurisdictionCode string
PlaceOfSupplyCode string
// Item classification
ItemType string // "goods", "services", "digital_services", etc.
InstrumentType *string // For stamp duty: "receipt", "contract", etc.
// Counterparty
CounterpartyType string // "individual", "company"
CounterpartyResident bool // Nigerian resident?
// Workspace tax profile
WorkspaceProfile *WorkspaceProfileContext
// Provider fees (e.g., Paystack)
HasProviderFee bool
ProviderFeeAmount float64
}WorkspaceProfileContext
type WorkspaceProfileContext struct {
VATRegistered bool // Registered for VAT?
WHTAgentRegistered bool // WHT withholding agent?
AnnualTurnover float64 // In NGN
SellsDigitalServices bool // For NITDA levy
ImportsServices bool // For reverse charge VAT
}Tax Components
The engine returns a TaxResult containing multiple TaxComponents.
TaxResult Structure
type TaxResult struct {
TaxableBase float64
TimeOfSupply time.Time
Components []models.TaxComponent
Compliance Compliance
ProfileStatus string // "complete", "incomplete", "threshold_exempt"
RequiredActions []string // Warnings/prompts for user
}TaxComponent Structure
type TaxComponent struct {
Code string // "VAT_OUTPUT", "WHT_PAYABLE", "STAMP_DUTY"
JurisdictionCode string // "NG"
Authority string // "Federal Inland Revenue Service (FIRS)"
Rate *float64 // 7.5 for VAT
Amount float64 // Calculated tax amount
Direction string // "payable", "receivable", "informational"
Basis string // "gross", "net", "instrument", "fee"
RuleRef *string // UUID of the rule applied
IsFinalTax *bool // For WHT: is this a final tax?
CertificateRef *string // WHT certificate reference
}Component Codes
| Code | Description | Direction |
|---|---|---|
VAT_OUTPUT | VAT on sales | payable |
VAT_INPUT | VAT on provider fees | receivable |
VAT_REVERSE_CHARGE | VAT on imported services | payable |
WHT_PAYABLE | WHT to withhold on expenses | payable |
WHT_RECEIVABLE | WHT withheld on sales | receivable |
STAMP_DUTY | Stamp duty on receipts | payable |
NITDA_LEVY | NITDA levy on digital services | payable |
Multi-Currency Support
Nigerian tax law requires all thresholds to be evaluated in NGN. For transactions in foreign currencies, the system converts amounts using the CBN rate at the transaction date.
CBN Rate Requirement
Nigerian tax law requires using the Central Bank of Nigeria (CBN) official rate for all foreign currency conversions. The system uses the rate from the transaction date, not the current rate.
PaymentV3Server Tax Preview (Primary Entry Point)
The payment form uses CalculateTaxPreview to show tax calculations before submission.
func (s *PaymentV3Server) CalculateTaxPreview(ctx context.Context, req *pb.CalculateTaxPreviewRequest) {
taxCtx := &tax.TaxContext{
Amount: amount,
Currency: req.Currency,
// ... other fields
}
// Convert to NGN for Nigerian jurisdiction with foreign currency
if req.Currency != "" && req.Currency != "NGN" && jurisdictionCode == "NG" && s.currencyService != nil {
transactionDate := req.TransactionDate.AsTime()
amountNGN, err := s.currencyService.ConvertAmountForDate(
amount, req.Currency, "NGN", transactionDate,
)
if err == nil {
taxCtx.AmountNGN = amountNGN
}
rate, _ := s.currencyService.GetRateForDate(req.Currency, "NGN", transactionDate)
taxCtx.FXRate = rate
}
engine, _ := s.taxCalculationService.GetEngine(jurisdictionCode)
result, _ := engine.Calculate(taxCtx)
}TaxServer Currency Conversion
The gRPC TaxServer also performs conversion for direct API calls.
func (s *TaxServer) CalculateTax(ctx context.Context, req *pb.CalculateTaxRequest) {
taxCtx := &tax.TaxContext{
Amount: req.Amount,
Currency: req.Currency,
}
// Convert to NGN for threshold comparisons
if req.Currency != "" && req.Currency != "NGN" {
amountNGN, err := s.currencyService.ConvertAmountForDate(
req.Amount, req.Currency, "NGN", transactionDate,
)
if err == nil {
taxCtx.AmountNGN = amountNGN
}
rate, _ := s.currencyService.GetRateForDate(req.Currency, "NGN", transactionDate)
taxCtx.FXRate = rate
}
result, err := engine.Calculate(taxCtx)
}Engine Amount Helper
// getAmountInNGN returns the NGN equivalent for threshold comparisons
func (e *NigeriaTaxEngine) getAmountInNGN(ctx *TaxContext) float64 {
// If already NGN or no currency specified, use original
if ctx.Currency == "NGN" || ctx.Currency == "" {
return ctx.Amount
}
// If converted amount available, use it
if ctx.AmountNGN > 0 {
return ctx.AmountNGN
}
// Fallback to original (should not happen)
return ctx.Amount
}Stamp Duty with Currency Conversion
func (e *NigeriaTaxEngine) calculateStampDuty(ctx *TaxContext, result *TaxResult) error {
// Get NGN amount for threshold comparison
amountNGN := e.getAmountInNGN(ctx)
for _, rule := range e.stampRules {
// Compare threshold in NGN (₦10,000 minimum)
if rule.Threshold != nil && amountNGN < *rule.Threshold {
return nil // Below threshold, no stamp duty
}
// Calculate stamp duty on NGN amount
if rule.AdValoremRate != nil {
stampAmount = amountNGN * (*rule.AdValoremRate)
}
}
}Usage Examples
Basic Tax Calculation
taxCtx := &tax.TaxContext{
ContextType: "sale",
Amount: 100000.0,
Currency: "NGN",
SellerJurisdictionCode: "NG",
ItemType: "services",
OccurredAt: time.Now(),
WorkspaceProfile: &tax.WorkspaceProfileContext{
VATRegistered: true,
AnnualTurnover: 50000000.0,
},
}
result, err := engine.Calculate(taxCtx)
// result.Components contains:
// - VAT_OUTPUT: 7500.0 (7.5% of 100000)
// - STAMP_DUTY: 50.0 (flat fee for receipt >= 10000)Foreign Currency Transaction
taxCtx := &tax.TaxContext{
ContextType: "sale",
Amount: 1000.0, // USD
Currency: "USD",
AmountNGN: 1550000.0, // Converted at 1550 rate
FXRate: 1550.0,
SellerJurisdictionCode: "NG",
ItemType: "digital_services",
WorkspaceProfile: &tax.WorkspaceProfileContext{
VATRegistered: true,
SellsDigitalServices: true,
},
}
result, err := engine.Calculate(taxCtx)
// Thresholds checked against 1,550,000 NGN
// VAT calculated on original USD amount
// NITDA levy calculated on original USD amountExpense with WHT
taxCtx := &tax.TaxContext{
ContextType: "expense",
Amount: 500000.0,
Currency: "NGN",
SellerJurisdictionCode: "NG",
ItemType: "professional_services",
CounterpartyType: "company",
CounterpartyResident: true,
WorkspaceProfile: &tax.WorkspaceProfileContext{
WHTAgentRegistered: true,
},
}
result, err := engine.Calculate(taxCtx)
// result.Components contains:
// - WHT_PAYABLE: 50000.0 (10% for professional services)
// - STAMP_DUTY: 50.0Using TaxCalculationService
service, err := tax.NewTaxCalculationService(db)
// Calculate and persist to database
determination, err := service.CalculateAndStore(
ctx,
taxCtx,
transactionID,
)
// determination is saved to tax_determinations table
// with all components serialized as JSONRules and Thresholds
VAT Registration Threshold
Annual turnover exceeding ₦25,000,000 requires mandatory VAT registration.
const NGVATRegistrationThreshold = 25_000_000Stamp Duty Threshold
Receipts of ₦10,000 or more attract stamp duty (typically ₦50 flat).
const NGStampDutyReceiptThreshold = 10_000VAT Rates by Item Type
| Item Type | Rate | Mode |
|---|---|---|
| Standard goods/services | 7.5% | standard |
| Exported goods | 0% | zero_rated |
| Basic food, medical, education | Exempt | exempt |
WHT Rates by Service Type
| Service Type | Resident Rate | Non-Resident Rate |
|---|---|---|
| Professional services | 10% | 10% |
| Technical services | 10% | 10% |
| Consultancy | 10% | 10% |
| Commission | 10% | 10% |
| Rent | 10% | 10% |
Immutability Rule
Tax determinations stored in the database are IMMUTABLE. Once a transaction is posted, its tax calculation cannot be modified. For corrections, create a new adjusting transaction.
Currency Conversion Rule
All threshold comparisons MUST use NGN equivalents. Foreign currency amounts are converted using the CBN rate at the transaction date, not the current rate. This is a legal requirement for Nigerian tax compliance.