RUNLOCALAIv38
->Will it run?Best GPUCompareTroubleshootStartLearnPulseModelsHardwareToolsBench
Run check
RUNLOCALAI

Independently operated catalog for local-AI hardware and software. Hand-written verdicts. Source-cited claims. Reproducible commands when we have them.

OP·Eruo Fredoline
DIR
  • Models
  • Hardware
  • Tools
  • Benchmarks
TOOLS
  • Will it run?
  • Compare hardware
  • Cost vs cloud
  • Choose my GPU
  • Prompting kits
  • Quick answers
REF
  • All buyer guides
  • Learn local AI
  • Methodology
  • Glossary
  • Errors KB
  • Trust
EDITOR
  • About
  • Author
  • How we make money
  • Editorial policy
  • Contact
LEGAL
  • Privacy
  • Terms
  • Sitemap
MAIL · MONTHLY DIGEST
Get monthly local AI changes
Monthly recap. No spam.
DISCLOSURE

Some links on this site are affiliate links (Amazon Associates and other first-class retailers). When you buy through them, we earn a small commission at no extra cost to you. Affiliate links do not influence our verdicts — there are cards we rate highly that we don't have affiliate relationships with, and cards that sell well that we refuse to recommend. Read more →

© 2026 runlocalai.coIndependently operated
RUNLOCALAI · v38
  1. >
  2. Home
  3. /Learn
  4. /Courses
  5. /AI-Powered SaaS Products
  6. /Ch. 13
AI-Powered SaaS Products

13. Invoice Generation

Chapter 13 of 24 · 20 min
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)) ```

EXERCISE

Implement invoice generation that handles proration for mid-month upgrades. Test with a tenant upgrading from the Starter (₦15,000/month) to Professional tier (₦45,000/month) on the 15th of a 31-day month. Verify the prorated amounts calculate correctly for both the downgrade credit and upgrade charge.

← Chapter 12
Naira Pricing Tiers
Chapter 14 →
API Rate Limiting