KEY INSIGHT
Nigerian SaaS requires Naira-native invoicing with Paystack integration, where invoice generation must handle timezone differences, Nigerian public holidays, and recurring billing edge cases that break Western-built systems.
Invoice generation in a Nigerian AI SaaS environment extends beyond simple PDF creation. The system must account for NGN currency formatting, tax compliance with FIRS requirements, and integration with local payment processors that have different settlement timelines than Stripe or PayPal.
```python
from datetime import datetime, timedelta
from decimal import Decimal
from enum import Enum
from typing import Optional
import jinja2
from weasyprint import HTML
class InvoiceStatus(Enum):
DRAFT = "draft"
PENDING = "pending"
PAID = "paid"
OVERDUE = "overdue"
CANCELLED = "cancelled"
class InvoiceGenerator:
"""Handles invoice generation with Nigerian market specifics."""
def __init__(self, db_session, paystack_client, template_dir: str):
self.db = db_session
self.paystack = paystack_client
self.env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir)
)
self.nigerian_holidays = self._load_holidays()
def generate_invoice(
self,
tenant_id: str,
billing_period_start: datetime,
billing_period_end: datetime,
line_items: list[dict]
) -> str:
"""Generate invoice for tenant with NGN formatting."""
tenant = self.db.query(Tenant).filter(
Tenant.id == tenant_id
).first()
if not tenant:
raise ValueError(f"Tenant {tenant_id} not found")
subtotal = sum(Decimal(str(item['amount'])) for item in line_items)
vat_rate = Decimal('0.075') # 7.5% VAT for professional services
vat_amount = subtotal * vat_rate
total = subtotal + vat_amount
invoice = Invoice(
tenant_id=tenant_id,
invoice_number=self._generate_invoice_number(tenant),
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
subtotal=subtotal,
vat_amount=vat_amount,
total=total,
currency='NGN',
status=InvoiceStatus.DRAFT,
due_date=datetime.utcnow() + timedelta(days=30)
)
self.db.add(invoice)
self.db.flush()
for item in line_items:
invoice_line = InvoiceLine(
invoice_id=invoice.id,
description=item['description'],
quantity=item.get('quantity', 1),
unit_price=Decimal(str(item['unit_price'])),
total=Decimal(str(item['amount']))
)
self.db.add(invoice_line)
self.db.commit()
pdf_path = self._render_pdf(invoice, tenant)
invoice.pdf_path = pdf_path
self.db.commit()
return invoice.id
def _generate_invoice_number(self, tenant: Tenant) -> str:
"""Generate sequential invoice numbers with tenant prefix."""
year = datetime.utcnow().year
prefix = tenant.invoice_prefix or f"INV-{tenant.id[:4].upper()}"
last_invoice = self.db.query(Invoice).filter(
Invoice.tenant_id == tenant.id,
Invoice.invoice_number.like(f"{prefix}-{year}%")
).order_by(Invoice.invoice_number.desc()).first()
if last_invoice:
sequence = int(last_invoice.invoice_number.split('-')[-1]) + 1
else:
sequence = 1
return f"{prefix}-{year}-{sequence:04d}"
def send_invoice(self, invoice_id: str, recipient_email: str):
"""Send invoice via Paystack invoice or email."""
invoice = self.db.query(Invoice).filter(
Invoice.id == invoice_id
).first()
if invoice.status != InvoiceStatus.DRAFT:
raise ValueError(f"Cannot send invoice with status {invoice.status}")
payment_link = self.paystack.create_invoice(
amount=int(invoice.total * 100), # kobo
currency='NGN',
customer_email=recipient_email,
description=f"Invoice {invoice.invoice_number}",
due_date=invoice.due_date.isoformat()
)
invoice.paystack_invoice_id = payment_link['id']
invoice.payment_url = payment_link['url']
invoice.status = InvoiceStatus.PENDING
invoice.sent_at = datetime.utcnow()
self.db.commit()
self._send_email_notification(invoice, recipient_email)
```
**Common Failure Modes:**
The timezone handling between Lagos and server UTC causes invoices to show wrong due dates. Nigeria operates in West Africa Time (WAT, UTC+1), so due date calculations must explicitly convert when displaying to users.
```python
from zoneinfo import ZoneInfo
def calculate_due_date_ngn(
invoice_date: datetime,
due_days: int = 30
) -> datetime:
"""Calculate due date properly for Nigerian display."""
lagosp_tz = ZoneInfo("Africa/Lagos")
due_date = invoice_date + timedelta(days=due_days)
# Adjust for weekend due dates (shift to Monday)
if due_date.weekday() == 5: # Saturday
due_date += timedelta(days=2)
elif due_date.weekday() == 6: # Sunday
due_date += timedelta(days=1)
return due_date
```
Recurring invoice generation requires careful handling of mid-cycle upgrades or downgrades. When a tenant upgrades mid-month, prorated calculations must be accurate to the day, which many billing libraries handle poorly.
```python
def calculate_proration(
days_in_period: int,
days_used: int,
monthly_price: Decimal
) -> Decimal:
"""Calculate prorated amount for mid-cycle changes."""
daily_rate = monthly_price / Decimal(str(days_in_period))
return daily_rate * Decimal(str(days_used))
```