A pharmacy inventory demo for Rodolfo’s Pharmacy, powered by Polea, with exactly two locations. Python owns the backend and every business rule. The browser is a lightweight same-origin HTML/CSS/JavaScript client with seven operational screens. Telegram is intentionally deferred.
Product-specific icons appear in inventory rows and stock/order forms. Icons follow product names using local rules; no external model is called. The top-right Help button (or ? keyboard shortcut) opens searchable task instructions and shortcuts to the relevant form or screen.
This is a synthetic operations demo. No patient records, controlled substances, supplier submissions, or live pharmacy integrations are supported. It is not production or regulatory ready.
Python 3.10+ and make are required. Node is only needed for browser tests.
make setup
cp .env.example .env
make seed
make run
Open http://127.0.0.1:8000. The API serves the frontend too; opening index.html directly will not work.
Demo password: polea-demo-2026. Accounts:
| Account | Access |
|---|---|
owner@polea.demo |
All locations, approvals, staff visibility |
manager@polea.demo |
Both locations, approvals |
clerk@polea.demo |
North only, sales, receipts, photo submissions; no approvals |
pharmacist@polea.demo |
South only, sales, receipts; no approvals |
Set DEMO_PASSWORD before initial seeding to choose another password. Changing the variable later does not rewrite an existing account. make seed is non-destructive and skips existing data; it also generates image fixtures and creates missing first-run analysis. There is no destructive reset command.
Tesseract is optional for real local OCR: brew install tesseract on macOS, or the appropriate system package elsewhere. Bundled sample images work without it, through an explicitly labeled fixture adapter. No external OCR is called.
docker compose up --build
Open the same local URL. Compose runs PostgreSQL 16, Redis, the API with the frontend, and an RQ worker. Migrations and seed run before the API starts; the first start can take a minute. Database and sanitized uploads use named volumes. Credentials in Compose are public local-demo defaults, and the web port binds only to loopback. Do not reuse them in a deployed environment.
The local SQLite path needs no Redis. Background recomputation runs inside the API process. With REDIS_URL configured, jobs run in RQ; make worker starts a worker. In-process jobs are not durable across restarts. Failed enqueue/recompute operations are logged, and the model-health endpoint exposes stale forecasts; request a new analysis after a failure. Queue retries and an outbox are future work.
⌘K / Ctrl+K opens inventory search. Receipt/sale forms accept a barcode or SKU scan followed by Enter.
inventory_events is the source of truth. stock_balances is a transactional projection. All quantities are integer base units (boxes in this demo); pack conversion applies to purchasing. Each event has a product, lot, location, signed delta, actor, source, correlation ID, effective timestamp and recorded timestamp. SQLite and PostgreSQL migration triggers reject updates/deletes to stock events and audit records.
Mutation requests require Idempotency-Key (8–100 characters). The server stores a canonical request fingerprint and response, scoped to the actor. A retry returns the original response; reusing the key for a different operation/body returns 409. The browser retains the same key for an unchanged form retry. PostgreSQL locks actor and balance rows; SQLite serializes write transactions with BEGIN IMMEDIATE. Database checks enforce nonnegative stock and valid reservations.
Sales select usable lots by FEFO and exclude expired/quarantined/recalled lots. Approved transfers reserve stock and preserve a product’s source safety stock. Dispatch subtracts source stock; receiving adds destination stock. Transit is counted separately. The first draft supports full receiving; partial shipments, in-transit loss/discrepancy, returns, and recall-management operations remain future work.
Opaque server-side sessions expire after eight hours. Passwords use salted scrypt; cookies are HttpOnly/SameSite=Strict. Mutations verify a session CSRF token and same-origin requests. User activation and location access are checked on every authenticated call. SESSION_SECURE=true is required behind HTTPS. There is no production MFA, identity provider, password-reset, or staff-management UI.
The seed is fixed at 42: 80 fictional products, 4 suppliers, 4 lots/product, 2 locations, 450 daily observations/product/location, synthetic seasonality, weekday effects, promotions, intermittent demand and censored shortages. Historical receipts/sales conserve stock. The history-to-live boundary uses explicit synthetic reconciliation events; it is not presented as a dispensing history.
Forecasting never reads latent synthetic demand. Censored observations are imputed inside each training fold and excluded from holdout scoring. Completed operational days are rolled from sales events into observations on recomputation; the current partial day is excluded. Post-seed zero-sale days are conservatively censored because continuous availability telemetry is absent. The available models are simple baselines and Croston, with Poisson or negative-binomial predictive distributions, not a fitted negative-binomial regression. There is no hierarchical pooling, holiday model, boosting, or calibrated service guarantee.
Monte Carlo simulation consumes lots by FEFO, removes expired supply, and includes dispatched transfers and approved POs on assumed arrival days. Supplier lead times are configured synthetic priors, not empirical reliability estimates. Demand is sampled independently across days. Recorded coverage and errors are measured, not invented; inspect Intelligence for the current seed’s results. Depletion beyond 28 days is shown as >28, not an exact date.
SciPy/HiGHS MILP chooses integer lot transfers, purchase packs and binary supplier-order indicators. Constraints include usable lots, expiry-before-arrival checks, source safety stock and 7-day source demand, destination storage capacities, whole packs, supplier minimum values, fixed shipping and a $2,000 total purchasing budget. The transparent objective weights are purchase cost + $0.12/unit transfer + shipping + $25/unit target shortage + $0.04/pack holding. Expected-demand targets are soft with shortage slack. Buying immediately does not claim to eliminate pre-arrival stockout risk; simulation shows that risk separately. Transfers have an assumed one-day transit. No substitutions are allowed. Expiry costs are simulated separately, not included as scenario-optimized objective terms. Proposal approval creates a draft; it never moves stock.
Production scenario optimization, empirical supplier fitting, calibrated quantiles, drift detection, recurring retraining, configurable policies, hard service levels and capacity management remain future work.
frontend/assets/ledgers/ contains clean.png, messy.png, ambiguous.png, and duplicate.png (identical to clean). Generated images are illustrative synthetic fixtures; the messy fixture is not evidence of handwriting OCR accuracy. The fixture adapter preserves unreadable values and ambiguous aliases on purpose.
The local parser accepts lines of PRODUCT | LOT | COUNT, exact SKU/barcode first, then unique names/aliases. Fuzzy matches are suggestions only. Broad ledger-table extraction, OCR word coordinates/confidences, crop controls, opening/receipt/sale arithmetic and historical-count reconciliation are not implemented. The UI calls confidence match confidence, not OCR certainty.
Only today’s physical counts can be committed. Review chooses a verified product/lot/count and explicitly confirms each adjustment. expected_version prevents overwriting concurrent stock changes. Images deduplicate by file hash; counts deduplicate by date/location/product/lot even across different images. All approved rows post in one database transaction as count adjustments, never inferred sales. Original extraction, corrected rows, image, reviewer and reason remain available. Sanitized images are stored; original untrusted binary retention, malware scanning, encrypted storage and retention automation are future work.
Interactive OpenAPI: http://127.0.0.1:8000/docs. Schema: /openapi.json.
import uuid
import requests
client = requests.Session()
login = client.post('http://127.0.0.1:8000/api/v1/auth/login', json={
'email': 'owner@polea.demo', 'password': 'polea-demo-2026'
}).json()
client.headers['X-CSRF-Token'] = login['csrf_token']
products = client.get('http://127.0.0.1:8000/api/v1/products?q=SKU123').json()['items']
locations = client.get('http://127.0.0.1:8000/api/v1/locations').json()['items']
result = client.post('http://127.0.0.1:8000/api/v1/receipts',
headers={'Idempotency-Key': str(uuid.uuid4())},
json={'product_id': products[0]['id'], 'location_id': locations[0]['id'],
'quantity': 20, 'lot_code': 'L77', 'expiry': '2028-08-31'})
result.raise_for_status()
print(result.json())
Key routes under /api/v1: /auth/login|me|logout, /locations, /users, /roles, /products, /inventory, /inventory/ledger, /inventory/counts, /receipts, /sales, /transfers, /transfers/{id}/approve|dispatch|receive|cancel, /suppliers, /purchase-orders, /purchase-orders/{id}/approve|receive, /imports, /imports/{id}/rows|image|commit, /forecasts, /forecasts/recompute, /risk, /recommendations, /recommendations/{id}/approve|dismiss, /model-health, /audit, /health.
Ledger/audit endpoints accept opaque cursor and limit (max 200). Inventory is a bounded demo projection; product lists cap at 200, other operational lists cap at 100–200 and do not yet expose cursors. Errors use {"error":{"code":"http_409","message":"...","request_id":"..."}}. Validation errors list field names without echoing passwords or bodies. Request IDs and structured method/path/status/duration logs are emitted. Rate limits are in-process and suitable for a single demo instance, not distributed abuse protection.
make test
make check
make verify
npm ci
npm run test:e2e
Local browser tests use installed Google Chrome and start an isolated temporary database/server on 8011; they do not mutate your demo database. On Linux CI: npx playwright install --with-deps chromium and set CI=true. To deliberately test another running server: BASE_URL=http://127.0.0.1:8000 npm run test:e2e (this does mutate that server’s demo data).
The interface uses the black/cream/gold palette and locally served fonts from Polea’s website. Home focuses on three daily actions, urgent tasks, and observed weekly sales trends. Reports separates historical sales from forecasts; technical model details are collapsed by default. The read-only /api/v1/trends endpoint calculates sales from ledger events, respects location access, and excludes the incomplete current business day. INTELLIGENCE.md explains the implementation, evidence gaps, and a credible founder pitch.
The current build passes 32 backend tests and 7 real Chrome flows. Tests cover conservation, append-only records, FEFO, stock eligibility, exactly-once retries, concurrent overselling, reservations/transit, role/location isolation, session revocation, CSRF/origin controls, import ambiguity/duplicates/atomicity/stale review, purchase receipt flow, random ledger event sequences, reproducible forecasts, rolling splits, DST business days, optimizer constraints, and scoped observed-sales trends. Browser checks cover seven screens, owner receipt/audit, clerk restrictions, manager ambiguous-ledger review, mobile layout, complete transfer receiving, approved physical-count posting, and the simplified overview/reports with location-filtered trends. make verify checks every balance against all ledger events and validates nonnegative stock at each effective timestamp. One upstream Starlette test-client deprecation warning remains; no application tests fail.
Business dates default to America/New_York (configurable with BUSINESS_TIMEZONE). Sessions/events are stored in UTC; displayed timestamps and full-day demand aggregation use the business timezone, including DST transitions.
Browser (frontend/: index.html, css/, js/)
→ FastAPI /api/v1 (backend/main.py)
→ security.py / schemas.py
→ inventory.py → append-only events + balance projection
→ documents.py → OCR adapter → reviewed count adjustments
→ intelligence.py → backtests + Monte Carlo + HiGHS MILP
→ SQLAlchemy / Alembic → SQLite or PostgreSQL
→ optional Redis / RQ worker for recomputation
backend/ FastAPI app, domain logic, seed/fixtures, worker
frontend/ index.html, css/, js/, assets/ (fonts, favicon, sample ledgers)
migrations/ Alembic migrations
tests/ pytest suites; tests/e2e/ holds Playwright specs and their server
docs/ build spec, phase plan, intelligence write-up
SKU123 shortage and South near-expiry Vitamin C Plus.See PLAN.md for phase status and SECURITY.md for the threat model. No hosting or Telegram integration was attempted. Docker/PostgreSQL execution requires Docker, which was unavailable in the initial build environment; local SQLite and Chrome are the verified path.