feat(backend): 835 ERA parser with BPR/CLP/SVC balancing, CO Medicaid config, CLI, and FastAPI route

This commit is contained in:
Tyler
2026-06-19 17:01:57 -06:00
parent 3dff38045a
commit 8ea951f436
18 changed files with 2413 additions and 26 deletions
+94 -5
View File
@@ -1,8 +1,9 @@
# Cyclone Backend — X12 837P Parser
# Cyclone Backend — X12 837P / 835 ERA 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).
Python module + CLI for parsing X12 837P professional claim files and X12
835 ERA (Health Care Claim Payment/Advice) files into one validated JSON
per claim / payout. Output is consumed by the Cyclone Vite/React frontend
via a FastAPI service (see **REST API** below).
## Install
@@ -80,7 +81,8 @@ python -m cyclone serve
| 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` | Upload an X12 837P file, get parsed claims back |
| POST | `/api/parse-835` | Upload an X12 835 ERA file, get parsed payouts back |
`POST /api/parse-837` accepts `multipart/form-data` with a single `file`
field. Optional query parameters:
@@ -152,3 +154,90 @@ while (true) {
- Design: `docs/superpowers/specs/2026-06-19-cyclone-837p-parser-design.md`
- Plan: `docs/superpowers/plans/2026-06-19-cyclone-837p-parser.md`
## 835 ERA parser
The 835 ERA parser mirrors the 837P architecture but is a separate
module (`cyclone.parsers.parse_835`) with its own models
(`cyclone.parsers.models_835`), writer (`cyclone.parsers.writer_835`),
validator (`cyclone.parsers.validator_835`), and `PayerConfig835` preset.
It parses one 835 batch into:
- `envelope` — ISA / GS / ST control segments
- `financial_info` — BPR (total paid, handling code, payer tax id, date)
- `trace` — TRN (reassociation trace number + originating company id)
- `payer` — Loop 1000A (NM1*PR + N3/N4/PER contact URL)
- `payee` — Loop 1000B (NM1*PE + N3/N4)
- `claims` — one `ClaimPayment` per Loop 2100 CLP, with embedded
`ServicePayment` rows for each Loop 2110 SVC
- `summary` — same `BatchSummary` used by 837P
### 835 CLI
```bash
python -m cyclone.cli parse-835 path/to/835.txt --output-dir ./payouts
```
Options:
- `--payer {co_medicaid_835,generic_835}` — default `co_medicaid_835`
- `--strict` — promote warnings to errors
- `--include-raw-segments / --no-raw-segments` — default: include
- `--log-level {DEBUG,INFO,WARNING,ERROR}`
Output:
```
payouts/
├── payout-991102984-20260617-CLM001.json # one per claim, with envelope/financial/trace/payer/payee embedded
├── payout-991102984-20260617-CLM002.json
└── summary.json
```
### 835 programmatic use
```python
from pathlib import Path
from cyclone.parsers.parse_835 import parse
from cyclone.parsers.payer import PayerConfig835
from cyclone.parsers.validator_835 import validate
from cyclone.parsers.writer_835 import write_outputs_835
result = parse(Path("input.txt").read_text(), PayerConfig835.co_medicaid_835())
report = validate(result, PayerConfig835.co_medicaid_835())
print(f"passed={report.passed} errors={len(report.errors)}")
write_outputs_835(result, Path("./payouts"))
```
### 835 payout balancing
The 835 validator enforces two monetary-balancing rules:
- `R835_BAL_BPR_vs_CLP04` — the BPR02 total paid must equal the sum of
`CLP04` (total claim paid) across all claims in the batch. Tolerance
is **$0.01**.
- `R835_BAL_CLP04_vs_SVC03` — each `CLP04` must equal the sum of its
embedded `SVC03` service payment amounts. Tolerance is **$0.01**.
These rules catch the common "BPR doesn't match the sum of the claims"
mismatch (often caused by a missing or duplicated claim in the batch).
### 835 REST API
`POST /api/parse-835` mirrors `POST /api/parse-837` exactly:
- `multipart/form-data` with a single `file` field
- Query params: `payer` (default `co_medicaid_835`), `include_raw_segments`
(default `true`), `strict` (default `false`)
- Content negotiation: `Accept: application/json` → single `ParseResult835`
object; default → NDJSON stream with `envelope → financial_info → trace
→ payer → payee → claim_payment (×N) → summary` lines
- `400` on `CycloneParseError` or unknown `payer`
- `422` when `validate()` reports errors (body still includes the full
`ParseResult835` so the UI can show the issues)
```bash
curl -X POST http://localhost:8000/api/parse-835 \
-F "file=@./samples/co_835.txt" \
-H "Accept: application/json"
```