fix(backend): make 835 summary counts consistent with total_claims
This commit is contained in:
@@ -292,12 +292,22 @@ async def parse_835_endpoint(
|
|||||||
|
|
||||||
# Always run the validator; attach the report so the JSON path can
|
# Always run the validator; attach the report so the JSON path can
|
||||||
# surface it and the NDJSON path can fold the counts into the summary.
|
# surface it and the NDJSON path can fold the counts into the summary.
|
||||||
|
# 835 validation is batch-level, so pass/fail applies uniformly to every
|
||||||
|
# claim payment in the batch (passed=N or 0, failed=0 or N).
|
||||||
report = validate_835(result, config)
|
report = validate_835(result, config)
|
||||||
passed = 1 if report.passed else 0
|
n = len(result.claims)
|
||||||
failed = 1 if not report.passed else 0
|
claim_ids = [c.payer_claim_control_number for c in result.claims]
|
||||||
|
if report.passed:
|
||||||
|
passed, failed, failed_claim_ids = n, 0, []
|
||||||
|
else:
|
||||||
|
passed, failed, failed_claim_ids = 0, n, claim_ids
|
||||||
result = result.model_copy(update={
|
result = result.model_copy(update={
|
||||||
"validation": report,
|
"validation": report,
|
||||||
"summary": result.summary.model_copy(update={"passed": passed, "failed": failed}),
|
"summary": result.summary.model_copy(update={
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"failed_claim_ids": failed_claim_ids,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if strict:
|
if strict:
|
||||||
|
|||||||
@@ -148,17 +148,26 @@ def parse_835(
|
|||||||
raise click.UsageError("No claim payments found in file") from None
|
raise click.UsageError("No claim payments found in file") from None
|
||||||
|
|
||||||
# Run validation; populate summary.passed/failed from the report.
|
# Run validation; populate summary.passed/failed from the report.
|
||||||
|
# 835 validation is batch-level (BPR/CLP/SVC balancing is cross-cutting),
|
||||||
|
# so pass/fail applies uniformly to every claim payment in the batch:
|
||||||
|
# when the batch passes, all N claims "passed" (passed=N, failed=0);
|
||||||
|
# when the batch fails, all N claims "failed" (passed=0, failed=N).
|
||||||
report = validate_835(result, config)
|
report = validate_835(result, config)
|
||||||
if strict:
|
if strict:
|
||||||
promoted = [i.model_copy(update={"severity": "error"}) for i in report.warnings]
|
promoted = [i.model_copy(update={"severity": "error"}) for i in report.warnings]
|
||||||
new_errors = report.errors + promoted
|
new_errors = report.errors + promoted
|
||||||
report = report.model_copy(update={"errors": new_errors, "passed": not new_errors})
|
report = report.model_copy(update={"errors": new_errors, "passed": not new_errors})
|
||||||
passed = 1 if report.passed else 0
|
n = len(result.claims)
|
||||||
failed = 1 if not report.passed else 0
|
claim_ids = [c.payer_claim_control_number for c in result.claims]
|
||||||
|
if report.passed:
|
||||||
|
passed, failed, failed_claim_ids = n, 0, []
|
||||||
|
else:
|
||||||
|
passed, failed, failed_claim_ids = 0, n, claim_ids
|
||||||
result = result.model_copy(update={"validation": report})
|
result = result.model_copy(update={"validation": report})
|
||||||
result.summary = result.summary.model_copy(update={
|
result.summary = result.summary.model_copy(update={
|
||||||
"passed": passed,
|
"passed": passed,
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
|
"failed_claim_ids": failed_claim_ids,
|
||||||
"issues_by_rule": _count_issues(report),
|
"issues_by_rule": _count_issues(report),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ def test_parse_835_endpoint_returns_json(client: TestClient):
|
|||||||
assert "claims" in body and len(body["claims"]) == 2
|
assert "claims" in body and len(body["claims"]) == 2
|
||||||
assert "summary" in body
|
assert "summary" in body
|
||||||
assert body["summary"]["total_claims"] == 2
|
assert body["summary"]["total_claims"] == 2
|
||||||
assert body["summary"]["passed"] == 1
|
assert body["summary"]["passed"] == 2
|
||||||
|
assert body["summary"]["failed"] == 0
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -79,7 +80,7 @@ def test_parse_835_endpoint_streams_ndjson(client: TestClient):
|
|||||||
|
|
||||||
# Summary numbers match the JSON path.
|
# Summary numbers match the JSON path.
|
||||||
assert parsed[7]["data"]["total_claims"] == 2
|
assert parsed[7]["data"]["total_claims"] == 2
|
||||||
assert parsed[7]["data"]["passed"] == 1
|
assert parsed[7]["data"]["passed"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_parse_835_endpoint_streams_ndjson_without_raw_segments(client: TestClient):
|
def test_parse_835_endpoint_streams_ndjson_without_raw_segments(client: TestClient):
|
||||||
@@ -123,7 +124,7 @@ def test_parse_835_endpoint_handles_payer_query_param(client: TestClient):
|
|||||||
assert resp.status_code == 200, (payer, resp.text)
|
assert resp.status_code == 200, (payer, resp.text)
|
||||||
body = resp.json()
|
body = resp.json()
|
||||||
assert body["summary"]["total_claims"] == 2
|
assert body["summary"]["total_claims"] == 2
|
||||||
assert body["summary"]["passed"] == 1
|
assert body["summary"]["passed"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_parse_835_endpoint_rejects_unknown_payer(client: TestClient):
|
def test_parse_835_endpoint_rejects_unknown_payer(client: TestClient):
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from cyclone.cli import main
|
|||||||
|
|
||||||
FIXTURE = Path(__file__).parent / "fixtures" / "minimal_835.txt"
|
FIXTURE = Path(__file__).parent / "fixtures" / "minimal_835.txt"
|
||||||
UNBALANCED = Path(__file__).parent / "fixtures" / "unbalanced_835.txt"
|
UNBALANCED = Path(__file__).parent / "fixtures" / "unbalanced_835.txt"
|
||||||
|
CO_FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt"
|
||||||
|
|
||||||
|
|
||||||
def test_cli_parse_835_writes_outputs(tmp_path: Path):
|
def test_cli_parse_835_writes_outputs(tmp_path: Path):
|
||||||
@@ -71,3 +72,21 @@ def test_cli_parse_835_generic_payer(tmp_path: Path):
|
|||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
summary = json.loads((tmp_path / "summary.json").read_text())
|
summary = json.loads((tmp_path / "summary.json").read_text())
|
||||||
assert summary["passed"] == 1
|
assert summary["passed"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_parse_835_summary_counts_consistent(tmp_path: Path):
|
||||||
|
"""Regression: passed + failed must equal total_claims for any batch size.
|
||||||
|
|
||||||
|
The CO Medicaid fixture has 2 claim payments; if the batch validates
|
||||||
|
cleanly, both must show as passed (passed=2, failed=0). A pre-fix
|
||||||
|
implementation set passed=1 regardless of claim count.
|
||||||
|
"""
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["parse-835", str(CO_FIXTURE), "--output-dir", str(tmp_path)])
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
summary = json.loads((tmp_path / "summary.json").read_text())
|
||||||
|
assert summary["total_claims"] == 2
|
||||||
|
assert summary["passed"] + summary["failed"] == summary["total_claims"]
|
||||||
|
assert summary["passed"] == 2
|
||||||
|
assert summary["failed"] == 0
|
||||||
|
assert summary["failed_claim_ids"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user