From 1978e696d75c3ac96bdef53454f83e7520d3a64a Mon Sep 17 00:00:00 2001 From: Tyler Martinez Date: Wed, 8 Jul 2026 01:45:08 -0600 Subject: [PATCH] test(sp41): parametrized test driving shipped SP40 validator on real visit fixture --- backend/tests/fixtures/spot_check_visits.csv | 6 ++ backend/tests/test_rebill_spot_check.py | 103 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 backend/tests/fixtures/spot_check_visits.csv diff --git a/backend/tests/fixtures/spot_check_visits.csv b/backend/tests/fixtures/spot_check_visits.csv new file mode 100644 index 0000000..98ce508 --- /dev/null +++ b/backend/tests/fixtures/spot_check_visits.csv @@ -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" diff --git a/backend/tests/test_rebill_spot_check.py b/backend/tests/test_rebill_spot_check.py index 52196e6..f5debf0 100644 --- a/backend/tests/test_rebill_spot_check.py +++ b/backend/tests/test_rebill_spot_check.py @@ -336,6 +336,109 @@ def test_claim_passes_shipped_sp40_local_validator(): 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 -----------------------------------------------------------