Datalumina

Application

Backend API

Wrap the proven pipeline in FastAPI with SQLite persistence, a small set of routes, and a clear review state machine.

By the end of this stage the workflow is an application. One multipart upload produces a persisted review, corrections rerun policy without new model calls, and the whole thing is drivable with curl before any frontend exists.

The ordering is the lesson. Persistence and HTTP arrive now, after the pipeline is measured, so they wrap known behavior instead of becoming the place where extraction and policy get invented under pressure.

Why SQLite

The database is SQLite in a single file under backend/data/. For a local, single-user teaching application that is the honest choice. Zero infrastructure, zero configuration, trivially resettable, and SQLAlchemy on top means the repository code would look the same against Postgres. Production would bring a managed database and migrations; the deployment stage returns to exactly this decision.

Build order

Five slices, each one testable:

  1. The InvoiceRecord model and repository, one row per processing attempt, storing the uploaded file under a UUID-based local filename
  2. InvoiceService, which orchestrates classification, extraction, merge, validation, and the GL suggestion as one workflow
  3. Persistence of everything the review needs, normalized data, provenance, comparison evidence, issues, coding, status, and timestamps
  4. The routes, thin HTTP handlers that delegate to the service
  5. File validation at the boundary, unsupported types, empty files, and anything over 4 MB are rejected before a single Azure call is made

The API surface stays small:

GET    /health
GET    /api/invoices
POST   /api/invoices
PUT    /api/invoices/{invoice_id}
PUT    /api/invoices/{invoice_id}/accounting
POST   /api/invoices/{invoice_id}/decision
DELETE /api/invoices/{invoice_id}
POST   /api/invoices/{invoice_id}/correction-email
GET    /api/accounting/gl-accounts

Routes own HTTP, the service owns the workflow, the repository owns SQLite. When something breaks, the layer names tell you where to look.

The review state machine

Every record moves through a small, explicit set of states:

Warnings never block approval; errors do, and so does a missing or invalid GL selection. Approved and rejected records become immutable, though any record can be explicitly deleted, which removes both the row and the stored file. That deletion is what makes a repeatable demo possible.

Drive it with curl

Start only the backend:

cd backend
uv run --locked --no-sync uvicorn app.main:create_app --factory --reload

In a second terminal, upload the happy-path Dutch invoice:

curl -F \
  "file=@samples/generated/02-nl-happy-compact.pdf;type=application/pdf" \
  http://localhost:8000/api/invoices

The response is a complete persisted review, status, combined fields, provenance, issues, and the GL suggestion. Then correct a field with a PUT and notice the response comes back instantly. Corrections rerun the pure policy against stored data; they do not re-extract, so they cost nothing and cannot alter the provider evidence.

Checkpoint

  • One multipart request produces a persisted review in SQLite
  • A correction reruns policy without any Azure call
  • Approving a document with blocking errors or an invalid GL account is rejected
  • Deleting a record removes the row and the uploaded file

Next: Frontend app

Give Maya a guided review portal, wire the remaining endpoints, and rehearse the complete story in the browser.

On this page