feat(backend): 835 ERA parser with BPR/CLP/SVC balancing, CO Medicaid config, CLI, and FastAPI route
This commit is contained in:
+94
-5
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user