test(sp41): parametrized test driving shipped SP40 validator on real visit fixture
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
"Client","Visit Date","Payer","Authorized","Member ID","Auth Start Date","Auth End Date","Authorization #","ICD-10","Service","Procedure Code","Modifiers","Client Classes","Billable Hours","Billable Amount","Invoice #","Claimed"
|
||||||
|
"Roberts, Alice","02/05/2026","CO Medicaid","Yes","Q944140","01/01/2026","12/31/2026","3125","R69","PCS T1019","T1019","U1","DD Waiver","1","$333.62","INV-2026-02-05-Q944140","Yes"
|
||||||
|
"Smith, Bob","06/25/2026","CO Medicaid","Yes","R649327","01/01/2026","12/31/2026","4521","R69","S5150","S5150","U8","DD Waiver","1","$235.55","INV-2026-06-25-R649327","Yes"
|
||||||
|
"Davis, Carol","02/25/2026","CO Medicaid","Yes","Y188426","01/01/2026","12/31/2026","2890","R69","PCS T1019","T1019","KX:SC:U2","DD Waiver","1","$222.72","INV-2026-02-25-Y188426","Yes"
|
||||||
|
"Lee, David","06/04/2026","CO Medicaid","Yes","R649327","01/01/2026","12/31/2026","4521","R69","S5150","S5150","U8","DD Waiver","1","$214.22","INV-2026-06-04-R649327","Yes"
|
||||||
|
"Garcia, Eve","06/22/2026","CO Medicaid","Yes","R649327","01/01/2026","12/31/2026","4521","R69","S5150","S5150","U8","DD Waiver","1","$213.25","INV-2026-06-22-R649327","Yes"
|
||||||
|
@@ -336,6 +336,109 @@ def test_claim_passes_shipped_sp40_local_validator():
|
|||||||
assert not report.errors, f"unexpected errors: {report.errors}"
|
assert not report.errors, f"unexpected errors: {report.errors}"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_visit_rows_from_csv(path) -> list[VisitRow]:
|
||||||
|
"""Load ``VisitRow`` records from the canonical AxisCare visits CSV shape.
|
||||||
|
|
||||||
|
The CSV is the same shape as ``ingest/jan1-jun272026.csv`` — see
|
||||||
|
:func:`cyclone.rebill.spot_check.parse_visits_csv` for the production
|
||||||
|
parser. We re-parse inline here so the test does not import the
|
||||||
|
scratch-dir script.
|
||||||
|
"""
|
||||||
|
import csv as _csv
|
||||||
|
from datetime import datetime as _dt
|
||||||
|
|
||||||
|
out: list[VisitRow] = []
|
||||||
|
with open(path, newline="", encoding="utf-8") as f:
|
||||||
|
reader = _csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
raw_dos = (row.get("Visit Date") or "").strip()
|
||||||
|
if not raw_dos:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
dos = _dt.strptime(raw_dos, "%m/%d/%Y").date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
member_id = (row.get("Member ID") or "").strip()
|
||||||
|
procedure = (row.get("Procedure Code") or "").strip()
|
||||||
|
if not (member_id and procedure):
|
||||||
|
continue
|
||||||
|
billed_clean = (row.get("Billable Amount") or "0").replace("$", "").replace(",", "")
|
||||||
|
try:
|
||||||
|
billed = Decimal(billed_clean or "0")
|
||||||
|
except Exception:
|
||||||
|
billed = Decimal("0")
|
||||||
|
mods_raw = (row.get("Modifiers") or "").strip()
|
||||||
|
modifiers = tuple(m.strip() for m in mods_raw.split(",") if m.strip())
|
||||||
|
out.append(VisitRow(
|
||||||
|
dos=dos, member_id=member_id,
|
||||||
|
client_name=(row.get("Client") or "").strip(),
|
||||||
|
procedure_code=procedure, modifiers=modifiers,
|
||||||
|
billed_amount=billed,
|
||||||
|
icd10=(row.get("ICD-10") or "").strip() or None,
|
||||||
|
prior_auth=(row.get("Authorization #") or "").strip() or None,
|
||||||
|
))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fixture_csv", ["tests/fixtures/spot_check_visits.csv"])
|
||||||
|
def test_every_visit_in_fixture_passes_full_pipeline(fixture_csv):
|
||||||
|
"""For each visit in the fixture CSV, the full pipeline (build → serialize →
|
||||||
|
SP40 local validator) passes.
|
||||||
|
|
||||||
|
This is the "drives shipped code on the real path" test: the fixture
|
||||||
|
CSV uses the exact same column shape and value shapes as
|
||||||
|
``ingest/jan1-jun272026.csv`` (the production visits export), and
|
||||||
|
each visit row runs through:
|
||||||
|
|
||||||
|
1. :func:`cyclone.rebill.spot_check.build_claim_output` (the
|
||||||
|
canonical helper)
|
||||||
|
2. :func:`cyclone.parsers.serialize_837.serialize_837` (the
|
||||||
|
shipped serializer)
|
||||||
|
3. :func:`cyclone.parsers.validator.validate` (the shipped SP40
|
||||||
|
local validator — 25 R-codes)
|
||||||
|
4. :func:`cyclone.rebill.spot_check.structural_spot_check`
|
||||||
|
(segment-class check)
|
||||||
|
|
||||||
|
The test pins all four on real visit data; if any step regresses
|
||||||
|
(e.g., SBR09 default flips, SVC date format breaks, NPI checksum
|
||||||
|
changes), the test fails.
|
||||||
|
"""
|
||||||
|
from cyclone.parsers.payer import PayerConfig
|
||||||
|
from cyclone.parsers.validator import validate as validate_claim
|
||||||
|
|
||||||
|
visits = _load_visit_rows_from_csv(fixture_csv)
|
||||||
|
assert visits, f"no visits parsed from {fixture_csv}"
|
||||||
|
|
||||||
|
cfg = PayerConfig.co_medicaid()
|
||||||
|
for idx, visit in enumerate(visits, start=1):
|
||||||
|
claim_id = f"{visit.dos.strftime('%Y%m%d')}-{visit.member_id}-{idx:02d}"
|
||||||
|
claim = build_claim_output(visit, claim_id=claim_id)
|
||||||
|
edi = serialize_837(
|
||||||
|
claim,
|
||||||
|
sender_id=SENDER_ID,
|
||||||
|
receiver_id="CO_TXIX",
|
||||||
|
submitter_name="Dzinesco",
|
||||||
|
submitter_contact_name="Tyler Martinez",
|
||||||
|
submitter_contact_email="tyler@dzinesco.com",
|
||||||
|
claim_filing_indicator_code=SBR09_DEFAULT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Structural segment-class check.
|
||||||
|
struct = structural_spot_check(edi)
|
||||||
|
assert struct.passed, (
|
||||||
|
f"{claim_id} failed structural spot check: "
|
||||||
|
f"missing={list(struct.missing)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Shipped SP40 local validator (25 R-codes).
|
||||||
|
report = validate_claim(claim, cfg)
|
||||||
|
assert report.passed, (
|
||||||
|
f"{claim_id} failed SP40 local validator: "
|
||||||
|
f"errors={[(i.rule, i.message) for i in report.errors]}"
|
||||||
|
)
|
||||||
|
assert not report.errors, f"{claim_id}: unexpected errors: {report.errors}"
|
||||||
|
|
||||||
|
|
||||||
# --- Helpers -----------------------------------------------------------
|
# --- Helpers -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user