# 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`