191 lines
7.0 KiB
Markdown
191 lines
7.0 KiB
Markdown
# Cyclone
|
|
|
|
A self-hosted EDI claims management suite for a single billing office.
|
|
Parses 837P professional claims and 835 ERA remittances (X12 005010X222A1
|
|
and 005010X221A1), with a local-only FastAPI backend and a React UI for
|
|
browsing, filtering, and inspecting the parsed data.
|
|
|
|
Local-only on purpose: binds to `127.0.0.1`, no auth, no internet exposure.
|
|
Built for one operator, one machine, one trading partner (Colorado
|
|
Medicaid, currently).
|
|
|
|
## Install
|
|
|
|
```bash
|
|
# Backend (Python 3.11+)
|
|
cd backend
|
|
python -m venv .venv
|
|
.venv/bin/pip install -e '.[dev]'
|
|
|
|
# Frontend (Node 20+)
|
|
cd ..
|
|
npm install
|
|
```
|
|
|
|
## Dev
|
|
|
|
Two terminals:
|
|
|
|
```bash
|
|
# Terminal 1 — backend
|
|
cd backend
|
|
.venv/bin/python -m cyclone serve
|
|
# (defaults to 127.0.0.1:8000; override with CYCLONE_PORT=...; reload with CYCLONE_RELOAD=1)
|
|
|
|
# Terminal 2 — frontend
|
|
npm run dev
|
|
# (Vite on http://localhost:5173)
|
|
```
|
|
|
|
Then open `http://localhost:5173`. Drop an `.txt` 837P or 835 file on the
|
|
Upload page; navigate to Claims, Remittances, Providers, or Activity to
|
|
see the parsed data.
|
|
|
|
The frontend reads its backend URL from `VITE_API_BASE_URL` (default
|
|
empty). Create a `.env.local` at the repo root with:
|
|
|
|
```
|
|
VITE_API_BASE_URL=http://127.0.0.1:8000
|
|
```
|
|
|
|
Without that, the UI falls back to its in-memory sample store via the
|
|
existing `data` adapter (parses are disabled).
|
|
|
|
## Test
|
|
|
|
```bash
|
|
# Backend
|
|
cd backend && .venv/bin/pytest
|
|
|
|
# Frontend type-check + build
|
|
npm run typecheck
|
|
npm run build
|
|
|
|
# Frontend unit tests
|
|
npm test
|
|
```
|
|
|
|
## Persistence
|
|
|
|
Parsed batches, claims, remittances, matches, and activity events are
|
|
stored in a SQLite file at `~/.local/share/cyclone/cyclone.db` by
|
|
default. The directory is auto-created on first run.
|
|
|
|
To use a different location, set `CYCLONE_DB_URL`:
|
|
|
|
```bash
|
|
export CYCLONE_DB_URL=sqlite:///path/to/cyclone.db
|
|
# or
|
|
export CYCLONE_DB_URL=postgresql://user:pass@host:5432/cyclone
|
|
```
|
|
|
|
### Backup
|
|
|
|
```bash
|
|
sqlite3 ~/.local/share/cyclone/cyclone.db ".backup /path/to/backup.db"
|
|
```
|
|
|
|
This is safe to run while the backend is running (uses SQLite's online
|
|
backup API).
|
|
|
|
## Project layout
|
|
|
|
```
|
|
.
|
|
├── backend/
|
|
│ ├── src/cyclone/
|
|
│ │ ├── api.py # FastAPI app, GET + parse routes
|
|
│ │ ├── store.py # InMemoryStore, mappers
|
|
│ │ ├── __main__.py # `python -m cyclone serve`
|
|
│ │ ├── cli.py # click CLI
|
|
│ │ └── parsers/ # X12 tokenizer, models, validator, writers
|
|
│ └── tests/
|
|
│ ├── fixtures/ # co_medicaid_*.txt, minimal_*.txt
|
|
│ ├── test_api.py # parse-837/835 round-trip
|
|
│ ├── test_api_gets.py # 6 GET endpoints
|
|
│ ├── test_store.py # store + mappers + iterators
|
|
│ ├── test_api_streaming.py
|
|
│ └── test_api_parse_persists.py
|
|
├── src/ # React + Vite + TypeScript UI
|
|
│ ├── components/
|
|
│ │ └── ui/ # Skeleton, EmptyState, ErrorState, FilterChips, Pagination, …
|
|
│ ├── pages/ # Claims, Remittances, Providers, Activity, Upload
|
|
│ ├── hooks/ # useBatches, useClaims, useRemittances, useProviders, useActivity, useParse
|
|
│ ├── lib/ # api.ts (6 GET + parse837/parse835/health), format.ts, utils.ts
|
|
│ ├── store/ # zustand sample-data + parsed-batches store
|
|
│ └── types/ # shared TS types
|
|
├── docs/
|
|
│ ├── reference/ # condensed 837P/835/X12/CO Medicaid notes
|
|
│ └── superpowers/plans/ # implementation plan
|
|
├── tailwind.config.js # shimmer, scan, row-flash keyframes
|
|
└── package.json
|
|
```
|
|
|
|
## Roadmap
|
|
|
|
Sub-projects 2, 3, and the first half of 4 are **shipped**. Next up:
|
|
|
|
- **Sub-project 3 (shipped) — More 837P/835 features.**
|
|
- **837P validation rules:** R034 enforces `REF*G1` on frequency-code
|
|
7/8 claims; R035 enforces `BHT06` transaction-type-code is in the
|
|
allowed set per payer config.
|
|
- **835 CAS deep-parsing:** every CAS adjustment now surfaces with its
|
|
CARC reason code + label (e.g. `CO-29: The time limit for filing has
|
|
expired`), surfaced via `GET /api/remittances/{id}` and rendered as
|
|
an expansion row on the Remittances page.
|
|
- **999 ACK transaction set:** full inbound parser + outbound
|
|
serializer. Auto-generated on 837 ingest when `?ack=true` is passed
|
|
to `POST /api/parse-837`. Persisted to the `acks` table, browsable
|
|
on a new `/acks` page, and downloadable as the regenerated raw
|
|
999 text.
|
|
- **270/271 eligibility (API-only):** `POST /api/eligibility/request`
|
|
builds a 270 from a JSON payload (subscriber, provider, payer,
|
|
service type); `POST /api/eligibility/parse-271` ingests the
|
|
response and returns structured `coverage_benefits`.
|
|
- **Sub-project 4 (partially shipped) — Frontend features.**
|
|
- **Per-claim detail drawer (shipped):** click any row on the Claims
|
|
page to open a side-panel drawer with the full claim context —
|
|
state + amount header, validation panel, service lines, diagnoses,
|
|
parties (billing provider, subscriber, payer), raw X12 segments,
|
|
matched remittance summary, and a vertical state-history timeline.
|
|
URL is synced (`?claim=...`) so links and back-button restore the
|
|
drawer. Keyboard nav: `j`/`k` move between claims, `esc` closes,
|
|
`?` opens the cheatsheet overlay. Skeleton + error + not-found
|
|
(404) states are all distinct. Powered by a new backend endpoint
|
|
`GET /api/claims/{claim_id}` that returns the full drawer payload
|
|
in a single round-trip.
|
|
- **Remaining SP4:** batch diff view, real-time streaming of NDJSON
|
|
GET responses, advanced filters (date range, multi-status, saved
|
|
filter sets).
|
|
|
|
### SP3 endpoints
|
|
|
|
- `POST /api/parse-837?ack=true` — existing 837 parse, plus optional
|
|
auto-generated 999 ACK persisted alongside the batch.
|
|
- `POST /api/parse-999` — parse an inbound 999 ACK and persist it.
|
|
- `GET /api/acks` — list ACKs.
|
|
- `GET /api/acks/{id}` — ACK detail, including the regenerated
|
|
`raw_999_text`.
|
|
- `POST /api/eligibility/request` — build a 270 from JSON.
|
|
- `POST /api/eligibility/parse-271` — ingest a 271 and return parsed
|
|
coverage benefits.
|
|
|
|
The UI gains a new **Acks** page (sidebar entry) that lists persisted
|
|
ACKs and lets you download the regenerated 999 text.
|
|
|
|
### SP4 endpoints (claim drawer)
|
|
|
|
- `GET /api/claims/{claim_id}` — full claim context for the drawer
|
|
payload: header fields, diagnoses, service lines, parties
|
|
(billing provider / subscriber / payer), validation result, raw
|
|
X12 segments, matched remittance summary (or `null`), and the
|
|
claim's state history (ordered newest-first). 404 with a structured
|
|
`{ "error": "Not found", "detail": "Claim {id} not found" }` body
|
|
when the claim doesn't exist — distinct from a transient fetch
|
|
failure so the UI can render a dedicated not-found state.
|
|
|
|
## License
|
|
|
|
No license file yet; this is internal-use software. Add a `LICENSE` file
|
|
when one is decided.
|