A backend service for a logistics operation: manage customers, vehicles, and orders, with a guarded order-status state machine and automatic vehicle dispatch. Built to demonstrate object-oriented design and a clean, layered backend architecture.
Replace
USER/REPOin the badge once the project is on GitHub, and add a live demo link here after deploying (see Deployment).
| Skill | Where to look |
|---|---|
| OOP / inheritance | Customer → Corporate / Private via SQLAlchemy single-table inheritance (db/models.py) |
| Business modeling | domain/ + db/models.py (Order, OrderItem, Vehicle, status history) |
| Order status flow | domain/status_flow.py — a state machine that rejects illegal transitions |
| Dispatch rules | domain/dispatch.py — chooses the smallest vehicle that fits the load |
| Database CRUD | repositories/ — the only layer that touches the DB |
| REST API | api/routers/ — orders, vehicles, customers, shipment status |
| Tests | tests/ — 30 tests: domain unit + repository/service integration + API |
A strict layering — each layer depends only on the one below it:
HTTP (FastAPI) CLI
api/routers/ ───────┐ cli/main.py
▼ ▼
services/ (business logic, orchestration)
│
┌──────────────┼───────────────┐
▼ ▼ ▼
domain/ repositories/ (rules) (CRUD)
(pure logic: db/models.py + db/base.py
status_flow, SQLite / Postgres
dispatch, enums)
Why it matters: the domain layer is pure Python (no database, no web), so the business rules are trivially testable. The services layer is the single home of business logic, which is why the API and CLI can share it without duplication.
PENDING ─▶ CONFIRMED ─▶ ASSIGNED ─▶ IN_TRANSIT ─▶ DELIVERED
│ │ │
└───────────┴────────────┴──▶ CANCELLED
DELIVERED and CANCELLED are terminal. Any illegal jump (e.g.
DELIVERED → PENDING) is rejected with HTTP 409 and a message listing the
allowed next states. Every transition is recorded in an order_status_history
table, exposed as a timeline at GET /orders/{id}/status.
Each vehicle type has a capacity; assign-vehicle picks the smallest type
that fits the order's item count and weight, reserves an available vehicle,
marks it busy, and frees it again when the order is delivered.
| Type | Max items | Max weight |
|---|---|---|
| bike | 2 | 10 kg |
| truck | 100 | 3,000 kg |
| ship | 10,000 | 100,000 kg |
Python 3.11+ · FastAPI · Pydantic v2 · SQLAlchemy 2 · SQLite / Postgres · pytest · Ruff · Docker · GitHub Actions.
src/transport_ops/
├── config.py schemas/ customer.py order.py vehicle.py
├── domain/ enums.py status_flow.py dispatch.py errors.py
├── db/ base.py models.py
├── repositories/ customer_repo.py order_repo.py vehicle_repo.py
├── services/ customer_service.py order_service.py vehicle_service.py
├── api/ app.py deps.py routers/ customers.py vehicles.py orders.py
└── cli/ main.py
tests/ Dockerfile docker-compose.yml Makefile pyproject.toml
Requires Python 3.11+.
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Run the API (http://127.0.0.1:8000)
uvicorn transport_ops.api.app:app --reloadOpen http://127.0.0.1:8000/docs for interactive Swagger docs. Tables are
created automatically on startup (SQLite by default).
A Makefile wraps common commands: make install, make test, make lint,
make run, make cli, make docker-up.
# 1. Create a customer (corporate or private — one polymorphic endpoint)
curl -X POST localhost:8000/customers -H 'Content-Type: application/json' -d \
'{"customer_type":"private","reference_person":"Alice","contact_email":"a@x.com","invoice_email":"a@x.com"}'
# 2. Add a vehicle
curl -X POST localhost:8000/vehicles -H 'Content-Type: application/json' -d \
'{"vehicle_type":"truck","current_location":"Beijing"}'
# 3. Create an order, add items
curl -X POST localhost:8000/orders -H 'Content-Type: application/json' -d \
'{"customer_id":1,"delivery_location":"456 Oak Ave"}'
curl -X POST localhost:8000/orders/1/items -H 'Content-Type: application/json' -d \
'{"name":"Laptop","weight":2,"quantity":10}'
# 4. Walk the status flow (confirm → auto-assign vehicle → in_transit → delivered)
curl -X POST localhost:8000/orders/1/status -H 'Content-Type: application/json' -d '{"status":"confirmed"}'
curl -X POST localhost:8000/orders/1/assign-vehicle
curl -X POST localhost:8000/orders/1/status -H 'Content-Type: application/json' -d '{"status":"in_transit"}'
curl -X POST localhost:8000/orders/1/status -H 'Content-Type: application/json' -d '{"status":"delivered"}'
# 5. Shipment status timeline
curl localhost:8000/orders/1/status| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness probe |
POST/GET/DELETE |
/customers … |
Manage customers (corporate/private) |
POST/GET/PATCH |
/vehicles … |
Manage vehicles |
POST/GET |
/orders … |
Create & list orders |
POST |
/orders/{id}/items |
Add an item |
POST |
/orders/{id}/assign-vehicle |
Auto-select & assign a vehicle |
POST |
/orders/{id}/status |
Guarded status transition |
GET |
/orders/{id}/status |
Current status + history timeline |
The same services power a terminal menu:
python -m transport_ops.cli.mainpytest # 30 tests
ruff check . # lintTests cover the state machine and dispatch rules (pure unit tests), the
repositories and order service (integration, against an isolated SQLite DB), and
the API — including that an illegal transition returns 409 and a missing
entity returns 404.
docker compose up --buildBuilds the API image and starts it with a Postgres database on
http://localhost:8000. (Locally without Docker it defaults to SQLite.)
The image is self-contained, so any container host works. Quickest public demo:
Render (free tier):
- Push the repo to GitHub.
- On render.com, create a Web Service from the repo, environment Docker.
- Add a Postgres instance and set
DATABASE_URLto itspostgresql+psycopg://...connection string. - Deploy and put the live URL at the top of this README.
The same image deploys to Railway or Fly.io via their "deploy from Dockerfile" flows.
MIT.