make run— start app + PostgreSQL (dev mode, auto-reload on:8000)make stop/make clean— stop / stop + remove volumesmake test-unit— unit tests only (no DB needed)make test-integration— full-stack tests against real PostgreSQL (runs clean → run → test → clean)make test— both suites sequentiallymake migrations msg='describe change'— generate Alembic migration via Docker
All commands run inside Docker containers. There is no local dev setup without Docker.
Modular monolith organized by domain, not by technical layer. Three modules, each self-contained:
| Module | Entry | Key routes |
|---|---|---|
app.modules.orders |
models, schemas, routes, service, repository |
GET/POST /orders/, PUT/DELETE /orders/{id}, POST /orders/{id}/cancel |
app.modules.users |
same | POST /users/register, POST /users/login |
app.modules.payment |
same | POST /payment/generate, POST /payment/pay/{order_id}, GET /payment/invoice/..., GET /payment/ |
app/api/router.py— central router aggregating all module routersapp/core/config.py—Settingssingleton;AUTH_JWT_SECRET_KEYenv var is mandatory (raisesValueErrorif missing)app/core/auth.py— JWT auth (HS256, 30min expiry);get_current_user()is a FastAPI dependencyapp/db/session.py— SQLAlchemy engine +get_db()generator dependency
Strict FSM in app.modules.orders.service.OrderStateMachine. Invalid transitions raise InvalidOrderTransition:
RECEIVED → PROCESSING, CANCELLED
PROCESSING → FULFILLED, CANCELLED
FULFILLED → SHIPPED, CANCELLED
SHIPPED → DELIVERED
DELIVERED → (terminal)
CANCELLED → (terminal)
Payment depends on Orders (invoice generation advances order to PROCESSING; payment completion advances to FULFILLED). Orders depends on Payment (cancellation calls cancel_invoice_by_order_id). The mark_invoice_paid_and_update_order() method in the invoice repository updates both invoice and order atomically in a single transaction.
- Unit tests (
tests/unit/): Call service functions directly withMagicMockrepos. No DB, no HTTP. Run viamake test-unit. - Integration tests (
tests/integration/): FullTestClient+ real PostgreSQL.conftest.pycreates all tables before each test and drops them after. Runs viamake test-integrationwhich also manages container lifecycle. - Important:
make test-integrationrunsclean → run → test → clean. Do not run it while the app is already running — it will conflict with port 8000 and DB volumes.
Integration tests use TEST_DATABASE_URL env var, falling back to DATABASE_URL, falling back to postgresql://postgres:postgres@localhost:5432/test_db. When running inside Docker via docker-compose.unittests.yml, DATABASE_URL is not set for unit tests (they don't need a DB). For local integration testing outside Docker, set TEST_DATABASE_URL or ensure PostgreSQL is running on localhost:5432.
- Run via Docker:
make migrations msg='describe change' alembic/env.pyusespkgutil.walk_packages('app.modules')to auto-discover all model submodules for autogenerate — this is the only way Alembic sees all tables. Do not addBase.metadataimports manually.- Models are in
app.modules.<name>.models. After creating a new module, add itsmodelssubmodule import or Alembic autogenerate will miss it.
AUTH_JWT_SECRET_KEY— required at startup. Set indocker-compose.ymlastest-secret-key-for-local-dev-only.DATABASE_URL— PostgreSQL connection string. Default:postgresql://postgres:postgres@localhost:5432/appDEBUG=true— enables debug mode in docker-compose.
- Routes are thin — all business logic goes in services.
- Services are thin — all data access goes in repositories.
- Pydantic schemas for all API contracts (input/output).
pytest.ini_optionssetspythonpath = ["."]andasyncio_mode = "auto"— no@pytest.mark.asyncioneeded.- No linter/formatter/typechecker configured in
pyproject.toml— none run in CI.