Files
cyclone/backend/tests/test_validation_r200_r210.py
T
Tyler c62826daea feat(sp9): multi-payer, multi-NPI, SFTP stub for dzinesco/TOC
- providers table seeded with 3 NPIs (Montrose, Delta, Salida)
- payers + payer_configs (per-tx config) join table
- clearhouse singleton (dzinesco identity, SFTP block, filename block)
- config/payers.yaml loader with Pydantic schema validation
- cyclone/edi/filenames.py: HCPF X12 File Naming Standards helpers
- cyclone/clearhouse/sftp.py: stub that copies to ./var/sftp/staging/...
- cyclone/secrets.py: macOS Keychain wrapper via keyring
- 11 new validation rules R200-R210 (CO MAP + HCPF naming)
- 6 new API endpoints (/api/clearhouse/*, /api/config/*, /api/admin/reload-config)
- migration 0007
- 72 new tests (646 total passing, was 574)

See spec: docs/superpowers/specs/2026-06-20-cyclone-multi-payer-npi-sftp-design.md
2026-06-20 23:07:31 -06:00

264 lines
8.1 KiB
Python

"""SP9 — R200-R210 validation rule tests.
These tests use the in-code PAYER_FACTORIES (which include the live
payer_config blocks loaded from YAML) so the rules have a non-empty
cfg_block to read. The live ``cyclone.payers`` registry is the source
of truth for CO_MAP; the rules fall back to the in-code PayerConfig
when the registry is empty.
"""
from __future__ import annotations
from datetime import date
from decimal import Decimal
import pytest
from cyclone import db as db_mod
from cyclone import payers as payer_loader
from cyclone.parsers.models import (
Address,
BillingProvider,
ClaimHeader,
ClaimOutput,
Diagnosis,
Payer,
Procedure,
ServiceLine,
Subscriber,
ValidationReport,
)
from cyclone.parsers.payer import PayerConfig
from cyclone.parsers.validator import validate
from cyclone.store import store
@pytest.fixture(autouse=True)
def _seed_db(tmp_path, monkeypatch):
"""Seed an in-memory DB with 3 providers + CO_TXIX payer, load YAML."""
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
db_mod._reset_for_tests()
db_mod.init_db()
payer_loader.load_payer_configs()
store.ensure_clearhouse_seeded()
yield
db_mod._reset_for_tests()
payer_loader.reset()
def _co_txix_cfg() -> PayerConfig:
"""Build a PayerConfig matching the live CO_TXIX YAML block."""
return PayerConfig(
name="Colorado Medical Assistance Program",
sbr09_claim_filing="MC",
allowed_claim_frequencies={1, 7, 8},
require_ref_g1_for_adjustments=False,
allowed_bht06={"CH", "RP"},
payer_id="CO_TXIX",
payer_name="COHCPF",
no_patient_loop=True,
encounter_claim_in_same_batch=False,
)
def _claim(*, npi="1881068062", sbr09=None, prv_code=None, payer_id="CO_TXIX",
payer_name="COHCPF", ref_ei="721587149", transaction_type_code="CH",
has_pwk=False) -> ClaimOutput:
raw = []
if sbr09 is not None:
raw.append(["SBR", "P", "18", "", "", "", "", "", "", sbr09])
if prv_code is not None:
raw.append(["PRV", "BI", "PXC", f"PXC{prv_code}"])
raw.append(["NM1", "85", "2", "TOC, Inc.", "", "", "", "", "XX", npi])
if ref_ei is not None:
raw.append(["REF", "EI", ref_ei])
if has_pwk:
raw.append(["PWK"])
return ClaimOutput(
claim_id="CLM1",
control_number="0001",
transaction_date=date(2026, 6, 20),
billing_provider=BillingProvider(
name="TOC, Inc.",
npi=npi,
tax_id=ref_ei,
address=Address(line1="1100 East Main St", city="Montrose", state="CO", zip="81401"),
),
subscriber=Subscriber(first_name="John", last_name="Doe", member_id="M1"),
payer=Payer(name=payer_name, id=payer_id),
claim=ClaimHeader(
claim_id="CLM1",
total_charge=Decimal("100.00"),
frequency_code="1",
place_of_service="11",
facility_code_qualifier="B",
),
transaction_type_code=transaction_type_code,
diagnoses=[Diagnosis(code="R69")],
service_lines=[
ServiceLine(
line_number=1,
procedure=Procedure(qualifier="HC", code="99213"),
charge=Decimal("100.00"),
units=Decimal("1"),
),
],
raw_segments=raw,
validation=ValidationReport(passed=True, errors=[], warnings=[]),
)
# ----- R200: BHT06 allowed ------------------------------------------------
def test_r200_passes_for_allowed_bht06():
c = _claim(transaction_type_code="CH")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule.startswith("R200")]
assert bad == []
def test_r200_fails_for_disallowed_bht06():
c = _claim(transaction_type_code="XX")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R200_bht06_allowed"]
assert len(bad) == 1
assert "not in" in bad[0].message
def test_r200_skips_when_no_bht06():
c = _claim(transaction_type_code="")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R200_bht06_allowed"]
# ----- R201: BHT06 no mixed batch ------------------------------------------
def test_r201_is_a_per_claim_noop():
# Batch-level rule; per-claim the rule yields nothing
c = _claim(transaction_type_code="CH")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R201_bht06_no_mixed_batch"]
# ----- R202: SBR09 allowed ------------------------------------------------
def test_r202_passes_for_mc():
c = _claim(sbr09="MC")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R202_sbr09_allowed"]
def test_r202_fails_for_unknown():
c = _claim(sbr09="AB")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R202_sbr09_allowed"]
assert len(bad) == 1
def test_r202_passes_for_zz_mco_encounter():
c = _claim(sbr09="ZZ")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R202_sbr09_allowed"]
# ----- R203: PRV matches provider ----------------------------------------
def test_r203_passes_when_prv_matches_provider_taxonomy():
c = _claim(prv_code="251E00000X")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R203_prv_matches_provider"]
def test_r203_fails_when_prv_mismatch():
c = _claim(prv_code="999Z00000X")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R203_prv_matches_provider"]
assert len(bad) == 1
# ----- R204: NPI in providers table --------------------------------------
def test_r204_passes_for_known_npi():
c = _claim(npi="1881068062")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R204_npi_in_providers_table"]
def test_r204_fails_for_unknown_npi():
c = _claim(npi="9999999999")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R204_npi_in_providers_table"]
assert len(bad) == 1
# ----- R205: REF*EI matches provider --------------------------------------
def test_r205_passes_when_ref_ei_matches():
c = _claim(ref_ei="721587149")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R205_ref_ei_matches_provider"]
def test_r205_fails_when_ref_ei_mismatch():
c = _claim(ref_ei="000000000")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R205_ref_ei_matches_provider"]
assert len(bad) == 1
# ----- R206: Payer ID matches ---------------------------------------------
def test_r206_passes_for_co_txix():
c = _claim(payer_id="CO_TXIX")
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R206_payer_id_matches"]
def test_r206_fails_for_wrong_payer_id():
c = _claim(payer_id="ZZZZZZ")
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R206_payer_id_matches"]
assert len(bad) == 1
# ----- R207: no PWK segment ----------------------------------------------
def test_r207_passes_when_no_pwk():
c = _claim(has_pwk=False)
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R207_no_pwk_segment"]
def test_r207_fails_when_pwk_present():
c = _claim(has_pwk=True)
report = validate(c, _co_txix_cfg())
bad = [i for i in report.errors if i.rule == "R207_no_pwk_segment"]
assert len(bad) == 1
# ----- R208: 2320 CAS*PI* group ------------------------------------------
def test_r208_is_a_per_claim_noop():
c = _claim()
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule == "R208_cas_2320_group"]
# ----- R209, R210: filename rules (validate at submit / parse time) ----
def test_r209_r210_per_claim_noop():
# These run at file-routing time, not per-claim validate
c = _claim()
report = validate(c, _co_txix_cfg())
assert not [i for i in report.errors if i.rule in ("R209_outbound_filename", "R210_inbound_filename")]