feat(sp10): 277CA parser + Payer-Rejected Inbox lane
- Add cyclone.parsers.models_277ca + parse_277ca (X12 005010X214)
- Per-Patient HL ClaimStatus with REF*1K (PCN), REF*EJ (tax ID),
STC category code, amount, service date
- STC classifier: A1-A3 accepted, A4/A6/A7 rejected, A8/A9 pended,
P1-P5 paid, anything else unknown
- Multiple STCs per Patient HL: last wins (canonical pattern for
'first pended, then paid' acknowledgments)
- Subscriber-level STCs surface in unscoped_statuses
- Add apply_277ca_rejections — stamps Claim.payer_rejected_* fields on
matching rows (lookup by patient_control_number, mirrors 999 path)
- New /api/parse-277ca, /api/277ca-acks, /api/277ca-acks/{id} endpoints
- New two77ca_acks table + Two77caAck ORM model
- New Claim columns: payer_rejected_at, _reason, _status_code, _by_277ca_id
- New payer_rejected lane in /api/inbox/lanes (distinct from 999
envelope rejected lane)
- New PayerConfig277CA block in config/payers.yaml + Pydantic model
- Migration 0008 bumps user_version to 8
Tests: 654 -> 688 (parser 22 + apply 6 + API 8 + config 6 + adjustments).
All 688 backend tests pass; 1 pre-existing skipped test class unaffected.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Tests for PayerConfig277CA loading from YAML.
|
||||
|
||||
SP10 T4. Mirrors ``test_payer_config_loading.py`` but for the new
|
||||
``PayerConfig277CA`` block that the 277CA parser and Inbox lane
|
||||
consume for status-code classification.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from cyclone.payers import (
|
||||
DEFAULT_CONFIG_PATH,
|
||||
_tx_to_model,
|
||||
all_configs,
|
||||
get_config,
|
||||
load_payer_configs,
|
||||
reset,
|
||||
)
|
||||
from cyclone.providers import PayerConfig277CA
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_registry():
|
||||
"""Each test starts with an empty config registry."""
|
||||
reset()
|
||||
yield
|
||||
reset()
|
||||
|
||||
|
||||
class TestPayerConfig277CALoading:
|
||||
def test_loads_default_config_with_277ca_block(self):
|
||||
"""The default config/payers.yaml must have a CO_TXIX 277CA block."""
|
||||
load_payer_configs(DEFAULT_CONFIG_PATH)
|
||||
cfg = get_config("CO_TXIX", "277CA")
|
||||
assert cfg is not None
|
||||
# Defaults from PayerConfig277CA.
|
||||
assert "A6" in cfg["rejected_status_codes"]
|
||||
assert "A8" in cfg["pended_status_codes"]
|
||||
assert "A3" in cfg["accepted_status_codes"]
|
||||
assert "P1" in cfg["paid_status_codes"]
|
||||
assert "277CA" in cfg["transaction_set_ids_allowed"]
|
||||
|
||||
def test_model_rejects_bad_status_code_type(self, tmp_path: Path):
|
||||
"""A non-list rejected_status_codes value must fail validation."""
|
||||
bad = {
|
||||
"payers": [
|
||||
{
|
||||
"payer_id": "BAD_PAYER",
|
||||
"name": "Bad",
|
||||
"receiver_name": "BAD",
|
||||
"receiver_id": "BADID",
|
||||
"configs": {
|
||||
"277CA": {
|
||||
"rejected_status_codes": "A6", # must be list
|
||||
"pended_status_codes": ["A8"],
|
||||
"accepted_status_codes": ["A1"],
|
||||
"paid_status_codes": ["P1"],
|
||||
"transaction_set_ids_allowed": ["277", "277CA"],
|
||||
"implementation_guide": "005010X214",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
p = tmp_path / "bad.yaml"
|
||||
p.write_text(yaml.safe_dump(bad))
|
||||
with pytest.raises(ValueError, match="BAD_PAYER"):
|
||||
load_payer_configs(p)
|
||||
|
||||
|
||||
class TestPayerConfig277CAModel:
|
||||
def test_default_factory_values(self):
|
||||
"""A minimal PayerConfig277CA defaults to the canonical HCPF sets."""
|
||||
cfg = PayerConfig277CA()
|
||||
assert "A4" in cfg.rejected_status_codes
|
||||
assert "A6" in cfg.rejected_status_codes
|
||||
assert "A7" in cfg.rejected_status_codes
|
||||
assert "A8" in cfg.pended_status_codes
|
||||
assert "277CA" in cfg.transaction_set_ids_allowed
|
||||
|
||||
def test_explicit_override(self):
|
||||
cfg = PayerConfig277CA(
|
||||
rejected_status_codes=["X1"],
|
||||
pended_status_codes=["X2"],
|
||||
accepted_status_codes=["X3"],
|
||||
paid_status_codes=["X4"],
|
||||
)
|
||||
assert cfg.rejected_status_codes == ["X1"]
|
||||
assert cfg.pended_status_codes == ["X2"]
|
||||
|
||||
|
||||
class TestTxToModelFor277CA:
|
||||
def test_int_key_routes_to_277ca_model(self):
|
||||
"""PyYAML may parse numeric keys as int — _tx_to_model must handle both."""
|
||||
# 277CA as int isn't likely (not pure digits), but 277 as int is.
|
||||
# Test the canonical string.
|
||||
assert _tx_to_model("277CA") is PayerConfig277CA
|
||||
|
||||
def test_string_key_routes_correctly(self):
|
||||
assert _tx_to_model("277CA") is PayerConfig277CA
|
||||
assert _tx_to_model("835") is not PayerConfig277CA # it's 835 model
|
||||
Reference in New Issue
Block a user