155 lines
4.5 KiB
Markdown
155 lines
4.5 KiB
Markdown
# Cyclone Backend — X12 837P Parser
|
|
|
|
Python module + CLI for parsing X12 837P professional claim files into one
|
|
validated JSON per claim. Output is consumed by the Cyclone Vite/React
|
|
frontend via a FastAPI service (see **REST API** below).
|
|
|
|
## Install
|
|
|
|
```bash
|
|
cd backend
|
|
python -m venv .venv
|
|
. .venv/bin/activate
|
|
pip install -e ".[dev]"
|
|
```
|
|
|
|
## CLI
|
|
|
|
```bash
|
|
python -m cyclone.cli parse-837 path/to/837p.txt --output-dir ./claims
|
|
```
|
|
|
|
Options:
|
|
- `--payer {co_medicaid,generic_837p}` — default `co_medicaid`
|
|
- `--strict` — promote warnings to errors
|
|
- `--max-retries N` — stub in v1 (no auto-fix yet)
|
|
- `--include-raw-segments / --no-raw-segments` — default: include
|
|
- `--log-level {DEBUG,INFO,WARNING,ERROR}`
|
|
|
|
Exit codes:
|
|
- `0` — every claim parsed
|
|
- `2` — file-level failure (no envelope, no claims)
|
|
- `1` — unexpected exception
|
|
|
|
## Output
|
|
|
|
```
|
|
claims/
|
|
├── claim-991102977-20260611-CLM001.json # one per claim
|
|
├── claim-991102977-20260611-CLM002.json
|
|
└── summary.json # batch summary
|
|
```
|
|
|
|
## Programmatic use
|
|
|
|
```python
|
|
from pathlib import Path
|
|
from cyclone.parsers.parse_837 import parse
|
|
from cyclone.parsers.payer import PayerConfig
|
|
from cyclone.parsers.writer import write_outputs
|
|
|
|
result = parse(Path("input.txt").read_text(), PayerConfig.co_medicaid())
|
|
write_outputs(result, Path("./claims"))
|
|
print(f"passed={result.summary.passed} failed={result.summary.failed}")
|
|
```
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
pytest -v
|
|
```
|
|
|
|
The CLI smoke test against `docs/prodfiles/837p-from-axiscare/*.txt` is
|
|
auto-skipped if no production files are present.
|
|
|
|
## REST API
|
|
|
|
The Cyclone Vite/React frontend (default dev origin `http://localhost:5173`)
|
|
talks to the parser over HTTP. The API module is `cyclone.api:app`.
|
|
|
|
### Run the server
|
|
|
|
```bash
|
|
uvicorn cyclone.api:app --reload --port 8000
|
|
# or, equivalently:
|
|
python -m cyclone serve
|
|
```
|
|
|
|
### Endpoints
|
|
|
|
| Method | Path | Purpose |
|
|
| ------ | ---------------- | -------------------------------------------------- |
|
|
| GET | `/api/health` | Liveness probe: `{"status": "ok", "version": ...}` |
|
|
| POST | `/api/parse-837` | Upload an X12 file, get parsed claims back |
|
|
|
|
`POST /api/parse-837` accepts `multipart/form-data` with a single `file`
|
|
field. Optional query parameters:
|
|
|
|
- `payer` — `co_medicaid` (default) or `generic_837p`
|
|
- `include_raw_segments` — `true` (default) or `false`
|
|
- `strict` — `false` (default) or `true` (promote warnings to errors)
|
|
|
|
The response shape depends on the request's `Accept` header:
|
|
|
|
- `Accept: application/json` → a single `ParseResult`-shaped JSON object
|
|
(envelope, claims, summary).
|
|
- No `Accept` header (or anything that isn't `application/json`) →
|
|
`application/x-ndjson` streaming, one JSON object per line in this order:
|
|
`{"type": "envelope", "data": {...}}`, one `{"type": "claim", "data": {...}}`
|
|
per claim, then `{"type": "summary", "data": {...}}`.
|
|
|
|
Error responses are JSON of the form `{"error": "...", "detail": "..."}`:
|
|
|
|
- `400` — file-level parse error (`CycloneParseError`) or unknown `payer`
|
|
- `422` — at least one claim failed validation (body still includes the
|
|
full `ParseResult` so the client can render the errors)
|
|
- `500` — unexpected internal error
|
|
|
|
CORS is configured to allow `http://localhost:5173` with `GET` and `POST`
|
|
and any headers.
|
|
|
|
### Example: upload + JSON response
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8000/api/parse-837 \
|
|
-F "file=@./samples/co_837p.txt" \
|
|
-H "Accept: application/json"
|
|
```
|
|
|
|
```json
|
|
{
|
|
"envelope": { "sender_id": "...", "receiver_id": "...", ... },
|
|
"claims": [ { "claim_id": "C1", ... }, { "claim_id": "C2", ... } ],
|
|
"summary": { "total_claims": 2, "passed": 2, "failed": 0, ... }
|
|
}
|
|
```
|
|
|
|
### Example: stream NDJSON from the browser
|
|
|
|
```js
|
|
const resp = await fetch("http://localhost:8000/api/parse-837", {
|
|
method: "POST",
|
|
body: formData, // FormData with a "file" entry
|
|
});
|
|
const reader = resp.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
for (const line of buffer.split("\n")) {
|
|
if (!line) continue;
|
|
const evt = JSON.parse(line);
|
|
if (evt.type === "claim") renderClaim(evt.data);
|
|
else if (evt.type === "summary") showSummary(evt.data);
|
|
}
|
|
buffer = buffer.slice(buffer.lastIndexOf("\n") + 1);
|
|
}
|
|
```
|
|
|
|
## Spec & plan
|
|
|
|
- Design: `docs/superpowers/specs/2026-06-19-cyclone-837p-parser-design.md`
|
|
- Plan: `docs/superpowers/plans/2026-06-19-cyclone-837p-parser.md`
|