A full-stack practice management tool for aesthetic injectable treatments. Tracks clients, vial lifecycle, sessions, and profitability — all from a local SQLite database with a browser-based dashboard.
I work in healthcare and perform aesthetic injectable treatments. In real-world practice, small math mistakes, unit confusion, and incomplete cost visibility directly impact inventory tracking, profitability, pricing consistency, and operational efficiency.
This started as a reusable calculation script. It's now a complete practice operations tool.
- Clients — full client records with session history and touch-up scheduling
- Vials — lifecycle state machine (unopened → active → depleted / expired), real-time inventory tracking with concentration preview
- Sessions — treatment plan entry with automatic cost and margin calculation powered by the original ledger engine
- Analytics — monthly/quarterly revenue, per-client profitability, waste reporting, reorder alerts
- Dashboard — browser UI with KPI cards, upcoming touch-up list, revenue chart, profitability table
This is not a clinical dosing tool. It only performs operational and financial calculations using user-provided treatment values.
botox-session-ledger/
├── botox_session_ledger.py ← core calculation engine (v1, preserved)
├── api.py ← deprecated shim; re-exports app from app.main
├── app/
│ ├── main.py ← FastAPI app, /ledger endpoint, static serving
│ ├── database.py ← SQLAlchemy engine, SessionLocal, Base
│ ├── models.py ← ORM models (Client, Vial, Session, etc.)
│ ├── schemas.py ← Pydantic v2 request/response schemas
│ ├── routers/
│ │ ├── clients.py ← /clients CRUD
│ │ ├── vials.py ← /vials lifecycle
│ │ ├── sessions.py ← /sessions
│ │ └── analytics.py ← /analytics/*
│ ├── services/
│ │ ├── vial_service.py ← vial lifecycle + allocation logic
│ │ ├── session_service.py ← session creation, calls ledger engine
│ │ └── analytics_service.py ← revenue, profitability, waste, reorder
│ └── static/
│ ├── index.html ← single-page dashboard markup
│ ├── style.css ← all custom styles
│ └── app.js ← all frontend logic (Tailwind + Chart.js)
├── alembic/
│ ├── env.py
│ └── versions/
│ └── 001_initial_schema.py
├── tests/
│ ├── conftest.py
│ ├── test_ledger.py ← unit tests for the calculation engine
│ └── test_api.py ← HTTP contract tests for the API surface
├── alembic.ini
├── Dockerfile
├── Makefile
├── pyproject.toml
├── requirements.txt
├── requirements-dev.txt
└── .github/
└── workflows/
└── ci.yml
cd ~/code/botox-session-ledger
# Install dependencies
pip install -r requirements.txt
# Run database migrations
alembic upgrade head
# Start the server
uvicorn app.main:app --reloadThen open http://localhost:8000 in your browser.
make install # pip install -r requirements-dev.txt
make run # uvicorn app.main:app --reload
make migrate # alembic upgrade head
make test # pytest with coverage
make lint # ruff check .
make typecheck # mypy strict
make clean # remove __pycache__, .pytest_cache, .pycdocker build -t botox-ledger .
docker run -p 8000:8000 botox-ledgerThe container runs alembic upgrade head then starts uvicorn. Mount a volume to persist the SQLite database:
docker run -p 8000:8000 -v $(pwd)/data:/app/data \
-e DATABASE_URL=sqlite:///./data/ledger.db \
botox-ledgerAll endpoints return JSON. The interactive docs are at http://localhost:8000/docs.
| Method | Path | Description |
|---|---|---|
POST |
/clients |
Create a client |
GET |
/clients |
List all clients |
GET |
/clients/{id} |
Get client + session history |
PATCH |
/clients/{id} |
Update client |
| Method | Path | Description |
|---|---|---|
POST |
/vials |
Open a new vial |
GET |
/vials/active |
List active vials (auto-expires stale ones) |
GET |
/vials |
List all vials |
GET |
/vials/{id} |
Get vial detail |
PATCH |
/vials/{id}/status |
Manually update vial status |
| Method | Path | Description |
|---|---|---|
POST |
/sessions |
Record a session |
GET |
/sessions |
List sessions (filter by ?client_id=) |
GET |
/sessions/{id} |
Get session detail with area breakdown |
| Method | Path | Description |
|---|---|---|
GET |
/analytics/revenue |
Revenue by period (?period=month|quarter) |
GET |
/analytics/clients/profitability |
Per-client revenue, cost, margin |
GET |
/analytics/vials/waste |
Expired vial waste summary |
GET |
/analytics/reorder-alert |
Stock runway estimate, reorder flag |
GET |
/analytics/clients/touchup-due |
Clients due for a touch-up within 4 weeks |
Practitioner (nullable FK throughout — ready for multi-user, no auth required today)
└── Client
└── Session
├── SessionArea (per-area units, volume, U-100 markings)
└── VialAllocation → Vial
Vial states: unopened → active → depleted | expired
Pricing modes: standard (20% target margin) | family_friend ($10/unit) | custom ($/unit)
botox_session_ledger.py is the original v1 script, preserved intact as the calculation core. When a session is created, session_service.py calls build_ledger_data() from this module — so all unit tests continue to pass and the math is identical to the original.
The engine handles:
- Dilution calculation (
100 units ÷ diluent mL) - Per-area volume (
units ÷ concentration) - U-100 syringe markings
- Consumables cost (product, saline, syringes, gloves, prep pads)
- Gross margin and recommended pricing across all three pricing modes
from botox_session_ledger import (
BotoxLedgerError, # base
InvalidDiluentError,
InvalidTreatmentPlanError,
InvalidPricingError,
InvalidMoneyError,
)All validation errors subclass BotoxLedgerError (itself a ValueError), so existing except ValueError handlers still work.
GitHub Actions runs on every push:
ruff check .— lintingmypy --strict— type checkingpytest --cov-fail-under=80— tests with coverage gate
Authentication is intentionally out of scope for this demo. The API has no auth layer — any request can read or write any record. A production deployment would add OAuth2/JWT via FastAPI's built-in security utilities (OAuth2PasswordBearer, dependency-injected get_current_user), with per-practitioner row-level scoping on all queries. The data model already carries a practitioner_id foreign key throughout in anticipation of this.
Other production concerns not addressed here: HTTPS termination, secrets management, a persistent Postgres database (swap the DATABASE_URL env var), rate limiting, and audit logging.
This tool does not account for malpractice insurance, provider compensation, payroll, rent, taxes, merchant processing fees, licensing, marketing, spoilage, financing, or any fixed operating expenses.
It does not make clinical decisions. All treatment unit values must be provided by the practitioner.