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
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""SP9 — payer config YAML loader tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from cyclone import payers
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_registry():
|
||||
payers.reset()
|
||||
yield
|
||||
payers.reset()
|
||||
|
||||
|
||||
def test_load_default_config_has_co_txix_837p():
|
||||
configs = payers.load_payer_configs()
|
||||
assert ("CO_TXIX", "837P") in configs
|
||||
block = configs[("CO_TXIX", "837P")]
|
||||
assert block["payer_id"] == "CO_TXIX"
|
||||
assert "CH" in block["bht06_allowed"]
|
||||
assert "RP" in block["bht06_allowed"]
|
||||
assert block["bht06_default"] == "CH"
|
||||
assert block["sbr09_default"] == "MC"
|
||||
|
||||
|
||||
def test_load_default_config_has_co_txix_835():
|
||||
configs = payers.load_payer_configs()
|
||||
assert ("CO_TXIX", "835") in configs
|
||||
block = configs[("CO_TXIX", "835")]
|
||||
assert "1811725341" in block["expected_payer_tax_ids"]
|
||||
assert block["expected_payer_health_plan_id"] == "7912900843"
|
||||
|
||||
|
||||
def test_get_config_returns_block():
|
||||
payers.load_payer_configs()
|
||||
block = payers.get_config("CO_TXIX", "837P")
|
||||
assert block is not None
|
||||
assert block["submitter_name"] == "Dzinesco"
|
||||
|
||||
|
||||
def test_get_config_returns_none_for_missing():
|
||||
payers.load_payer_configs()
|
||||
assert payers.get_config("UNKNOWN_PAYER", "837P") is None
|
||||
|
||||
|
||||
def test_load_invalid_yaml_raises(tmp_path):
|
||||
bad = tmp_path / "bad.yaml"
|
||||
bad.write_text(yaml.safe_dump({"payers": [
|
||||
{"payer_id": "X", "configs": {"837P": {"submitter_name": "ok"}}} # missing required keys
|
||||
]}))
|
||||
with pytest.raises(ValueError, match="invalid config"):
|
||||
payers.load_payer_configs(bad)
|
||||
|
||||
|
||||
def test_load_missing_payers_key_raises(tmp_path):
|
||||
bad = tmp_path / "bad.yaml"
|
||||
bad.write_text("not_a_payers_key: 1")
|
||||
with pytest.raises(ValueError, match="missing top-level 'payers:'"):
|
||||
payers.load_payer_configs(bad)
|
||||
|
||||
|
||||
def test_load_missing_file_warns_and_clears(tmp_path, caplog):
|
||||
fake = tmp_path / "does-not-exist.yaml"
|
||||
with caplog.at_level("WARNING"):
|
||||
result = payers.load_payer_configs(fake)
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_all_configs_returns_snapshot():
|
||||
payers.load_payer_configs()
|
||||
snap = payers.all_configs()
|
||||
assert isinstance(snap, dict)
|
||||
assert len(snap) >= 2
|
||||
# Mutating snapshot must not affect registry
|
||||
snap.clear()
|
||||
assert len(payers.all_configs()) >= 2
|
||||
|
||||
|
||||
def test_reload_picks_up_changes(tmp_path):
|
||||
a = tmp_path / "a.yaml"
|
||||
a.write_text(yaml.safe_dump({"payers": [
|
||||
{"payer_id": "AAA", "configs": {"837P": {
|
||||
"submitter_name": "A", "submitter_contact_name": "A",
|
||||
"submitter_contact_email": "a@a",
|
||||
"receiver_name": "R", "receiver_id": "R",
|
||||
"bht06_allowed": ["CH"], "bht06_default": "CH",
|
||||
"sbr09_default": "MC", "sbr09_allowed": ["MC"],
|
||||
"payer_id_qualifier": "PI", "payer_id": "AAA",
|
||||
"pwk_supported": False, "cas_2320_group_allowed": False,
|
||||
}}}
|
||||
]}))
|
||||
b = tmp_path / "b.yaml"
|
||||
b.write_text(yaml.safe_dump({"payers": [
|
||||
{"payer_id": "BBB", "configs": {"837P": {
|
||||
"submitter_name": "B", "submitter_contact_name": "B",
|
||||
"submitter_contact_email": "b@b",
|
||||
"receiver_name": "R", "receiver_id": "R",
|
||||
"bht06_allowed": ["CH"], "bht06_default": "CH",
|
||||
"sbr09_default": "MC", "sbr09_allowed": ["MC"],
|
||||
"payer_id_qualifier": "PI", "payer_id": "BBB",
|
||||
"pwk_supported": False, "cas_2320_group_allowed": False,
|
||||
}}}
|
||||
]}))
|
||||
payers.load_payer_configs(a)
|
||||
assert payers.get_config("AAA", "837P") is not None
|
||||
payers.load_payer_configs(b)
|
||||
assert payers.get_config("BBB", "837P") is not None
|
||||
assert payers.get_config("AAA", "837P") is None
|
||||
Reference in New Issue
Block a user