From-Scratch Build · Python · FastAPI

Expense Management

A working expense system whose substance is the approval-routing engine: give it an expense and an org policy and it computes the ordered chain of approvers, advances a small state machine as each one approves or rejects, and records every step in an audit trail. SQLite persistence and a FastAPI surface sit on top.

Python 3.12FastAPISQLite State machine24 testsMIT

The core

Policy in, ordered approval chain out

The interesting part of an expense system isn't the form — it's deciding who has to sign off, in what order, and why. That logic lives in one pure function: given an expense and a policy, it returns an ordered, reasoned list of approvers. Same inputs, same chain, no side effects — which is exactly what makes it testable.

The policy is data, not code. Thresholds, the finance-review category list and the auto-approve limit all come from data/policy.json, so the rules change without touching the engine.

RuleDefaultEffect
auto_approve_under25Below this, auto-approves on submit (empty chain).
manager_limit500Above this, a director is appended after the manager.
finance_categoriestravel, equipmentAlways add a final finance review.
finance_amount_floor2000Any amount at/above this adds finance review.

The state machine

DRAFT → SUBMITTED → PENDING → APPROVED / REJECTED

Submitting computes the chain and either auto-approves (empty chain) or parks the expense PENDING on its first approver. Each approval advances to the next step; the final approval finalises it. Any rejection halts the chain immediately — later approvers never get a turn. Only the current step's approver may decide; an out-of-turn or out-of-state decision is refused.

# real output — python demo.py, expense #5 (5000.00 other)
  1. Priya Anand (manager)   manager approval
  2. Dana Reyes  (director)  director approval (amount over 500)
  3. Marco Lindt (finance)   finance review (amount >= 2000)

# and when the submitter reports to an absent manager, it escalates:
  1. Dana Reyes  (director)  manager approval (escalated past absent approver)

The audit trail

Every routing decision is recorded

The store keeps an append-only audit trail. Each submit, route, approval, rejection and finalisation is a timestamped row tied to the expense and the actor — so you can always reconstruct why a claim ended where it did.

# real audit trail for the 5000 expense, approved end-to-end
[submitted ] Lena Ortiz    other expense for 5000
[routed    ] system        awaiting manager (employee 3): manager approval
[approved  ] Priya Anand   looks good
[routed    ] Priya Anand   awaiting director (employee 1): director approval (amount over 500)
[approved  ] Dana Reyes    looks good
[routed    ] Dana Reyes    awaiting finance (employee 2): finance review (amount >= 2000)
[approved  ] Marco Lindt   looks good
[finalized ] Marco Lindt   all approvals complete

The API

The engine, exposed over FastAPI

The acting employee is passed via an X-Employee-Id header. Run it with uvicorn expenses.api:app --reload and the interactive docs are at /docs.

POST /expenses

Submit

File an expense; the response carries the computed approval chain.

GET /expenses/mine

My expenses

Everything I've filed, with current status.

GET /expenses/awaiting

My queue

Expenses waiting on me right now — only my turn shows up.

POST …/approve

Approve

Advance the step assigned to me to the next approver.

POST …/reject

Reject

Halt the chain; the expense is final.

GET …/audit

Audit trail

The full timestamped history of one expense.

Tested

The engine is the thing that's proven

The routing engine is tested directly and the API through FastAPI's TestClient24 tests, all passing.

Scope

An honest note on the "cloud" part

The original framing for this project was a cloud deployment — serverless functions, a managed database, object storage for receipts, hosted auth. That deployment is out of scope here and not claimed.

What this repo actually contains is the real application underneath it: the routing engine, the state machine, the SQLite store and the FastAPI surface — all runnable and tested locally. Receipts are referenced by path rather than uploaded to a blob store, and the X-Employee-Id header stands in for a managed identity service. This page describes the code that exists, not a deployment that doesn't.