Skip to content

InvoicePDFs — Architecture at a Glance

Layered Architecture

┌─────────────────────────────────────────────────────────┐
│                  main.py (composition root)               │
│    FastAPI factory, router wiring, dependency injection    │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  HORIZONTAL LAYERS (cross-cutting, used by all features)  │
│                                                           │
│  ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────┐  │
│  │ Auth /   │ │ Pagina-  │ │  Audit  │ │    Rate     │  │
│  │  Deps    │ │  tion    │ │         │ │   Limiter   │  │
│  └──────────┘ └──────────┘ └─────────┘ └─────────────┘  │
│  ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────┐  │
│  │  Errors  │ │  Config  │ │ Storage │ │   Schemas   │  │
│  └──────────┘ └──────────┘ └─────────┘ └─────────────┘  │
│                                                           │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  VERTICAL FEATURE SLICES (api/v1/*.py)                    │
│  Each slice owns its route handlers and business logic    │
│                                                           │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐  │
│  │ Invoice │ │ Customer │ │  Batch   │ │Credit Notes │  │
│  ├─────────┤ ├──────────┤ ├──────────┤ ├─────────────┤  │
│  │Template │ │ Payment  │ │ Webhook  │ │   Files     │  │
│  ├─────────┤ ├──────────┤ ├──────────┤ ├─────────────┤  │
│  │Branding │ │Tax Rates │ │   Jobs   │ │  Imports    │  │
│  ├─────────┤ ├──────────┤ ├──────────┤ ├─────────────┤  │
│  │Numbering│ │Workspace │ │  Audit   │ │  Renders    │  │
│  └─────────┘ └──────────┘ └──────────┘ └─────────────┘  │
│                                                           │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  ENGINE (pure compute, zero app.* imports)                │
│  Can be extracted as a standalone library                  │
│                                                           │
│  ┌────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│  │ Validation │ │ Calculation │ │ Renderer (WeasyPrint)│ │
│  └────────────┘ └─────────────┘ └──────────────────────┘ │
│  ┌────────────────┐ ┌──────────────┐                      │
│  │Template Engine │ │ Render Model │                      │
│  └────────────────┘ └──────────────┘                      │
│                                                           │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  DATABASE (SQLAlchemy 2.0)                                │
│                                                           │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐               │
│  │  Models  │  │ Session  │  │ Init DB  │               │
│  │ (30 tbl) │  │ Factory  │  │          │               │
│  └──────────┘  └──────────┘  └──────────┘               │
│                                                           │
└─────────────────────────────────────────────────────────┘

Horizontal Layers

These are cross-cutting concerns shared by all (or most) vertical feature slices.

Layer Files Responsibility
Auth & Identity api/deps.py, auth/api_keys.py API key extraction/validation, AuthContext (account + key), monthly quota enforcement, idempotency key handling
Pagination api/pagination.py Cursor-based pagination: encode_cursor, decode_cursor, apply_cursor_filter, finalize_cursor_page
Audit audit.py emit_audit_event() — captures actor, action, resource, IP, user-agent, request_id. Called before db.commit() in all write operations
Rate Limiting middleware/rate_limit.py Sliding-window in-memory rate limiter. Configurable via RATE_LIMIT_PER_SECOND and RATE_LIMIT_BURST
Error Handling errors.py raise_api_error() for consistent {error: {code, message}} envelope. Global exception handlers
Schemas schemas/v1.py All Pydantic v2 request/response models (~120 classes). Single source of truth for API contracts
Configuration config.py Pydantic Settings loaded from environment variables
Storage storage/ Abstract Storage ABC + LocalFilesystemStorage (default) + GCSStorage (Google Cloud Storage). Backend selected via STORAGE_BACKEND env var
App Composition main.py create_app() factory: wires routers, middleware, error handlers, state (engine, storage, sessions)
Database db/models.py, db/session.py, db/base.py, db/init_db.py 30 SQLAlchemy models, session factory, schema initialization

Storage

The storage layer abstracts file persistence behind a common interface, allowing the application to run on local disk in development and cloud storage in production.

Architecture (app/storage/)

File Role
base.py Storage ABC with 5 methods: save_pdf, load_pdf, save_file, load_file, delete_file
local.py LocalFilesystemStorage — writes to STORAGE_LOCAL_DIR (default .data/). Default backend
gcs.py GCSStorage — Google Cloud Storage backend using google-cloud-storage SDK
__init__.py create_storage(settings) factory — selects backend from config, lazy-imports GCS only when needed

Configuration

Env Var Default Description
STORAGE_BACKEND local local or gcs
STORAGE_LOCAL_DIR .data Directory for local storage
GCS_BUCKET (required for gcs) Google Cloud Storage bucket name
GCS_PREFIX (empty) Optional key prefix within the bucket

Usage

# Local development (default)
STORAGE_BACKEND=local

# Google Cloud Storage
STORAGE_BACKEND=gcs
GCS_BUCKET=my-invoicepdfs-bucket
GCS_PREFIX=prod

# Install GCS dependencies
uv pip install invoicepdfs[gcs]

Vertical Feature Slices

Each slice is a self-contained feature with its own route file in api/v1/. Slices are independent of each other — they only depend on horizontal layers.

Feature Route File Key Models Horizontal Deps
Invoices invoices.py (1,256 lines) Invoice, InvoiceRender auth, pagination, audit, engine, storage, webhooks
Customers customers.py Customer auth, pagination, audit, idempotency
Business Profiles business_profiles.py BusinessProfile auth, pagination, audit
Templates templates.py (engine-based) auth, engine, storage
Template Versions template_versions.py TemplateVersion auth, audit
Documents documents.py Render, UsageEvent auth, engine, storage
Renders renders.py Render auth, storage
Batches batches.py Batch, BatchItem, Job auth, pagination, engine, storage
Credit Notes credit_notes.py CreditNote auth, pagination, audit, engine
Payments payments.py Payment auth, pagination, audit
Deliveries deliveries.py Delivery auth
Files files.py FileAsset auth, audit, storage
Branding branding.py BrandingSettings auth, audit, storage
Attachments attachments.py InvoiceAttachment auth
Numbering numbering.py NumberingSequence auth, pagination, audit
Tax Rates tax_rates.py TaxRate auth, pagination, audit
Webhooks webhooks.py WebhookEndpoint, WebhookEvent auth, pagination, audit
Jobs jobs.py Job auth, audit
Imports imports.py ImportRecord, Job auth, audit
Workspaces workspaces.py Workspace, WorkspaceMember auth, pagination
API Keys api_keys.py ApiKey auth, audit
Usage usage.py UsageEvent auth
Audit Log audit.py (route) AuditEvent auth, pagination
Reference Data reference.py (none) (none)

Engine (Pure Compute Layer)

The engine has zero app.* imports — it operates on pure data structures and could be extracted as a standalone Python library.

InvoiceData (input)
    ├── validate()     → ValidationError or pass
    ├── calculate()    → CalculationResult (subtotal, taxes, discounts, shipping, total)
    └── render()       → EngineResult (pdf_bytes, calculation, render_id, timestamps)
                            ├── template_engine → Jinja2 HTML
                            ├── render_model    → template context dict
                            └── renderer        → WeasyPrint HTML→PDF
File Role
engine/schemas.py Canonical data model: InvoiceData, LineItem, Party, Address, etc.
engine/validation.py Business rules: non-empty line items, positive quantities, valid discount values
engine/calculation.py Tax computation (inclusive/exclusive), discounts (line/document), shipping, totals
engine/template_engine.py Jinja2 template discovery, render_invoice_html()
engine/render_model.py Builds template context with formatted currency, dates, line amounts
engine/renderer.py WeasyPrintRenderer — HTML to PDF conversion
engine/engine.py InvoiceEngine orchestrator: validate → calculate → render

Request Flow

Every API request follows the same pattern:

Request
  → Rate Limiter (middleware)
  → Auth (extract API key → load Account)
  → Route Handler
      → Validate input (Pydantic schema)
      → [Idempotency check]
      → Business logic (DB queries, engine calls)
      → emit_audit_event()
      → db.commit()
      → [fire_event() for webhooks]
  → Response ({data: ...} envelope)

Cross-Feature Integration Points

  • Tax Rates → Invoices: _resolve_tax() snapshots tax rate values at invoice creation time
  • Jobs → Batches: create_batch() creates a Job; batch processor updates job.progress_current per-item
  • Jobs → Imports: confirm_import() creates a completed Job record
  • Audit → All writes: Every mutation emits an audit event in the same DB transaction
  • Webhooks → Batch/Invoice events: fire_event() dispatches asynchronously via thread pool
  • Attachments → Invoices + Files: Validates ownership of both before linking

Codebase Stats

Metric Value
Python files in app/ 60
Total lines (app/) ~10,100
SQLAlchemy models 30
Pydantic schemas ~120 classes
API endpoints ~85 across 26 route files
Largest files invoices.py (1,256), schemas/v1.py (1,199), models.py (555)