Files
cyclone/backend/tests/test_resubmissions_cli.py
T

286 lines
9.7 KiB
Python

"""SP39: tests for the `cyclone resubmissions status` CLI."""
from __future__ import annotations
from datetime import datetime, timezone
from click.testing import CliRunner
from cyclone import db as cycl_db
from cyclone.cli import resubmissions_status_cmd
from cyclone.db import Resubmission
def _seed_one(claim_id: str = "claim-1", batch_id: str = "batch-1",
icn: str = "000000001") -> None:
cycl_db.init_db()
with cycl_db.SessionLocal()() as s:
s.add(Resubmission(
claim_id=claim_id,
batch_id=batch_id,
resubmitted_at=datetime.now(timezone.utc),
source_corrected_path=f"/tmp/{claim_id}.x12",
interchange_control_number=icn,
group_control_number="1",
))
s.commit()
def _cleanup(batch_id: str) -> None:
with cycl_db.SessionLocal()() as s:
s.query(Resubmission).filter(Resubmission.batch_id == batch_id).delete()
s.commit()
def test_no_resubmissions_prints_message_and_exits_0():
cycl_db.init_db()
runner = CliRunner()
result = runner.invoke(
resubmissions_status_cmd,
["--batch-id", "no-such-batch-zzz"],
)
assert result.exit_code == 0, result.output
assert "no resubmissions recorded" in result.output
def test_one_resubmission_prints_status_row():
try:
_seed_one(claim_id="claim-A", batch_id="batch-A")
runner = CliRunner()
result = runner.invoke(
resubmissions_status_cmd,
["--batch-id", "batch-A"],
)
assert result.exit_code == 0, result.output
assert "claim-A" in result.output
assert "pending_999" in result.output # no inbound ack yet
assert "batch-A" in result.output
finally:
_cleanup("batch-A")
def test_limit_truncates_output():
try:
for i in range(5):
_seed_one(
claim_id=f"claim-L{i}", batch_id="batch-L",
icn=f"00000000{i}",
)
runner = CliRunner()
result = runner.invoke(
resubmissions_status_cmd,
["--batch-id", "batch-L", "--limit", "2"],
)
assert result.exit_code == 0, result.output
assert "more" in result.output
# Only 2 rows printed in the table (plus 1 "(N more)" line).
body_lines = [
ln for ln in result.output.splitlines()
if ln.startswith("claim-L")
]
assert len(body_lines) == 2
finally:
_cleanup("batch-L")
def test_status_reflects_paid_when_remittance_links():
"""When a remittance.claim_id links to the resubmitted claim,
the payment column shows 'paid'."""
try:
from cyclone.db import Remittance
from decimal import Decimal
_seed_one(claim_id="claim-P", batch_id="batch-P")
with cycl_db.SessionLocal()() as s:
s.add(Remittance(
id="remit-P-1",
batch_id="batch-P",
payer_claim_control_number="claim-P",
claim_id="claim-P",
status_code="1",
status_label="Processed as Primary",
total_charge=Decimal("100.00"),
total_paid=Decimal("80.00"),
adjustment_amount=Decimal("0"),
claim_level_adjustment_amount=Decimal("0"),
received_at=datetime.now(timezone.utc),
))
s.commit()
runner = CliRunner()
result = runner.invoke(
resubmissions_status_cmd,
["--batch-id", "batch-P"],
)
assert result.exit_code == 0, result.output
assert "claim-P" in result.output
assert "paid" in result.output
finally:
from cyclone.db import Remittance as _R
with cycl_db.SessionLocal()() as s:
s.query(_R).filter(_R.id == "remit-P-1").delete()
s.commit()
_cleanup("batch-P")
# ---------------------------------------------------------------------------
# SP39: `cyclone resubmit-rejected-claims` instruments Resubmission rows.
# ---------------------------------------------------------------------------
def _seed_clearhouse_stub_false(tmp_path) -> None:
"""Seed the clearhouse singleton and flip ``sftp_block.stub=False``
with a non-routable host so the CLI proceeds past the guard and
the paramiko stub below intercepts the connection."""
import json
from cyclone import db as db_mod
from cyclone.db import ClearhouseORM
from cyclone.store import store as cycl_store
cycl_store.ensure_clearhouse_seeded()
with db_mod.SessionLocal()() as s:
ch = s.get(ClearhouseORM, 1)
assert ch is not None, "ensure_clearhouse_seeded should have created it"
block = json.loads(json.dumps(ch.sftp_block_json))
block["stub"] = False
block["host"] = "stub.invalid"
ch.sftp_block_json = block
s.commit()
def test_resubmit_rejected_claims_inserts_resubmission_row(tmp_path, monkeypatch):
"""SP39: after a successful upload, one Resubmission row per claim
is inserted with the parent dir's batch_id and the parsed envelope
control numbers."""
import sys
import types
from cyclone.cli import resubmit_rejected_claims
from cyclone.parsers.parse_837 import parse as parse_837_text
from cyclone.parsers.payer import PayerConfig
from cyclone.store import store as cycl_store
_seed_clearhouse_stub_false(tmp_path)
# Build a parseable 837P file (CO_TXIX payer, single claim).
cfg = PayerConfig.co_medicaid()
sample = tmp_path / "batch-deadbeef-1-claims"
sample.mkdir()
file_path = sample / "tp1-837P-test-1of1.x12"
src_text = open("tests/fixtures/co_medicaid_837p.txt").read()
file_path.write_text(src_text)
parsed = parse_837_text(src_text, cfg)
assert parsed.claims, "fixture must produce at least one claim"
first_claim_id = parsed.claims[0].claim_id
# Replace paramiko with a fake whose stat() raises (file not on
# remote yet, so the CLI takes the upload branch).
fake_paramiko = types.ModuleType("paramiko")
class _FakeSFTP:
def stat(self, path):
raise IOError("not found")
def put(self, local, remote):
return None
class _FakeSSH:
def open_sftp(self):
return _FakeSFTP()
def set_missing_host_key_policy(self, policy):
return None
def connect(self, *a, **kw):
return None
def close(self):
return None
fake_paramiko.SSHClient = lambda: _FakeSSH()
fake_paramiko.AutoAddPolicy = lambda: None
monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)
captured: list[dict] = []
monkeypatch.setattr(
cycl_store, "record_resubmission",
lambda **kw: captured.append(kw) or True,
)
runner = CliRunner()
result = runner.invoke(resubmit_rejected_claims, [
"--ingest-dir", str(tmp_path),
"--limit", "1",
"--no-validate",
])
assert result.exit_code == 0, (
f"CLI exited {result.exit_code}, output={result.output!r}, "
f"exception={result.exception!r}"
)
assert len(captured) >= 1, (
f"expected record_resubmission to be called, got {captured!r}"
)
call = captured[0]
assert call["claim_id"] == first_claim_id, call
assert call["batch_id"] == "deadbeef", call
assert "deadbeef-1-claims" in call["source_corrected_path"], call
assert call["interchange_control_number"], call
assert call["group_control_number"], call
def test_resubmit_idempotency_does_not_create_duplicate_rows(tmp_path, monkeypatch):
"""A second run against the same file (now on the SFTP) does NOT
insert a second Resubmission row — only the original upload branch
writes, matching the audit-log behavior."""
from cyclone.cli import resubmit_rejected_claims
from cyclone.store import store as cycl_store
import sys
import types
_seed_clearhouse_stub_false(tmp_path)
# Set up: one corrected file.
sample = tmp_path / "batch-cafebabe-1-claims"
sample.mkdir()
file_path = sample / "tp1-837P-test-1of1.x12"
src_text = open("tests/fixtures/co_medicaid_837p.txt").read()
file_path.write_text(src_text)
on_remote_size = len(src_text.encode())
# Build a paramiko fake where stat() returns matching size so the
# CLI takes the skip branch. The CLI only reads ``st_size`` off the
# return value, so a simple duck-typed object suffices.
fake_paramiko = types.ModuleType("paramiko")
class _FakeAttr:
def __init__(self, size: int) -> None:
self.st_size = size
class _FakeSFTPAlready:
def stat(self, path):
return _FakeAttr(on_remote_size)
def put(self, local, remote):
raise AssertionError("should not put when file already on remote")
class _FakeSSHAlready:
def open_sftp(self):
return _FakeSFTPAlready()
def set_missing_host_key_policy(self, policy):
return None
def connect(self, *a, **kw):
return None
def close(self):
return None
fake_paramiko.SSHClient = lambda: _FakeSSHAlready()
fake_paramiko.AutoAddPolicy = lambda: None
monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)
captured: list[dict] = []
monkeypatch.setattr(
cycl_store, "record_resubmission",
lambda **kw: captured.append(kw) or True,
)
runner = CliRunner()
result = runner.invoke(resubmit_rejected_claims, [
"--ingest-dir", str(tmp_path),
"--limit", "1",
"--no-validate",
])
# On the skip path, record_resubmission should NOT be called.
assert captured == [], f"expected no record_resubmission on skip, got {captured!r}"
assert "skipped" in result.output, result.output