feat(sp33): fix resubmit CLI session reuse + mismatch skip

- payer-mismatch files are now skipped instead of uploaded
- SFTP session persists across files (was reopened and leaked per file)
- --reconnect-every now counts successful uploads, after increment
- drop unused SftpClient instantiation; read each file once
- apply_999_rejections docstring: R/E/X, not R/E

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Nora
2026-07-03 12:32:30 -06:00
parent f10ab83628
commit 27bca33b09
2 changed files with 24 additions and 23 deletions
+23 -22
View File
@@ -577,7 +577,6 @@ def resubmit_rejected_claims(
from cyclone import db as db_mod from cyclone import db as db_mod
from cyclone.audit_log import AuditEvent, append_event from cyclone.audit_log import AuditEvent, append_event
from cyclone.clearhouse import SftpClient
from cyclone.parsers.parse_837 import parse as parse_837_text from cyclone.parsers.parse_837 import parse as parse_837_text
from cyclone.parsers.payer import PayerConfig from cyclone.parsers.payer import PayerConfig
from cyclone.store import store as cycl_store from cyclone.store import store as cycl_store
@@ -614,7 +613,6 @@ def resubmit_rejected_claims(
failed = 0 failed = 0
validated = 0 validated = 0
payer_cfg = PayerConfig.co_medicaid() payer_cfg = PayerConfig.co_medicaid()
sftp_client = SftpClient(sftp_block)
start = time.monotonic() start = time.monotonic()
last_progress = start last_progress = start
@@ -641,36 +639,39 @@ def resubmit_rejected_claims(
try: ssh.close() try: ssh.close()
except Exception: pass except Exception: pass
ssh: paramiko.SSHClient | None = None
sftp: paramiko.SFTPClient | None = None
for i, src in enumerate(files, 1): for i, src in enumerate(files, 1):
if limit is not None and i > limit: if limit is not None and i > limit:
break break
content = src.read_bytes()
# Validate: parse must succeed AND payer_id must match the # Validate: parse must succeed AND payer_id must match the
# companion-guide CO_TXIX (catches a bad byte-fix early). # companion-guide CO_TXIX (catches a bad byte-fix early).
if validate: if validate:
try: try:
parsed = parse_837_text(src.read_text(), payer_cfg) parsed = parse_837_text(content.decode(), payer_cfg)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
failed += 1 failed += 1
click.echo(f"PARSE FAIL {src.name}: {exc.__class__.__name__}: {exc}", err=True) click.echo(f"PARSE FAIL {src.name}: {exc.__class__.__name__}: {exc}", err=True)
continue continue
for c in parsed.claims: mismatch = next(
if c.payer.id != "CO_TXIX": (c for c in parsed.claims if c.payer.id != "CO_TXIX"), None
failed += 1 )
click.echo( if mismatch is not None:
f"PAYER MISMATCH {src.name}: payer.id={c.payer.id!r} " failed += 1
f"(expected 'CO_TXIX')", err=True, click.echo(
) f"PAYER MISMATCH {src.name}: payer.id={mismatch.payer.id!r} "
break f"(expected 'CO_TXIX')", err=True,
else: )
validated += 1 continue
validated += 1
content = src.read_bytes()
local_size = len(content) local_size = len(content)
remote_path = f"{remote_root}/{src.name}" remote_path = f"{remote_root}/{src.name}"
ssh: paramiko.SSHClient | None = None
sftp: paramiko.SFTPClient | None = None
attempts = 0 attempts = 0
ok = False ok = False
while attempts < 3: while attempts < 3:
@@ -713,12 +714,6 @@ def resubmit_rejected_claims(
failed += 1 failed += 1
continue continue
# Reconnect periodically to dodge MOVEit's per-session cap.
if uploaded > 0 and uploaded % reconnect_every == 0:
click.echo(f"reconnecting (after {uploaded} uploads)", err=True)
_close_session(ssh)
ssh, sftp = None, None
# Audit (best-effort; if the DB is unavailable we still keep # Audit (best-effort; if the DB is unavailable we still keep
# the file on the wire). # the file on the wire).
try: try:
@@ -741,6 +736,12 @@ def resubmit_rejected_claims(
uploaded += 1 uploaded += 1
# Reconnect periodically to dodge MOVEit's per-session cap.
if uploaded % reconnect_every == 0:
click.echo(f"reconnecting (after {uploaded} uploads)", err=True)
_close_session(ssh)
ssh, sftp = None, None
# Progress every 10s of wall-clock (or at end). # Progress every 10s of wall-clock (or at end).
now = time.monotonic() now = time.monotonic()
if now - last_progress >= 10 or i == len(files): if now - last_progress >= 10 or i == len(files):
+1 -1
View File
@@ -35,7 +35,7 @@ def apply_999_rejections(
claim_lookup: Callable[[str], Claim | None], claim_lookup: Callable[[str], Claim | None],
batch_envelope_index: dict[str, list[str]] | None = None, batch_envelope_index: dict[str, list[str]] | None = None,
) -> Apply999Result: ) -> Apply999Result:
"""For each set response with code R or E, look up the matching claim and """For each set response with code R, E, or X, look up the matching claim and
move it to REJECTED. Idempotent on already-rejected claims. move it to REJECTED. Idempotent on already-rejected claims.
Args: Args: