feat(sp37): cyclone submit-batch CLI

Walks batch-*-claims/*.x12 under --ingest-dir, calls submit_file per
file, prints submitted/skipped/failed counts. Exits 0 even on
per-file failures (details in stdout); exits 2 on config-level
errors (no clearhouse, stub mode, missing dir).

This is the canonical outbound path; resubmit-rejected-claims
remains for one-off cases where no DB row is wanted.
This commit is contained in:
Nora
2026-07-07 11:10:10 -06:00
parent ec065a9082
commit dc990cfde3
2 changed files with 106 additions and 0 deletions
+93
View File
@@ -768,6 +768,99 @@ def resubmit_rejected_claims(
)
@main.command("submit-batch")
@click.option("--ingest-dir", default="ingest",
show_default=True,
help="Root dir holding batch-*-claims/ subfolders of .x12 files.")
@click.option("--actor", default="cli-submit-batch",
show_default=True,
help="Audit-log actor tag for the clearhouse.submitted events.")
@click.option("--validate/--no-validate", default=True,
help="Parse each file via parse_837 before upload.")
@click.option("--limit", type=int, default=None,
help="Stop after N files (smoke-tests).")
def submit_batch(
ingest_dir: str,
actor: str,
validate: bool,
limit: int | None,
) -> None:
"""Parse → DB-write → SFTP-upload each batch-*-claims/*.x12 (SP37).
Canonical outbound path. Mirrors ``resubmit-rejected-claims`` but
routes through ``cyclone.submission.submit_file``, which writes to
the DB before uploading. Use this as the preferred outbound path;
``resubmit-rejected-claims`` remains for one-off cases where you
don't want a DB row (dzinesco-generated fixes not ready for
canonical tracking).
Exit codes: 0 = run completed (even with per-file failures), 1 =
unexpected exception, 2 = config-level failure (no clearhouse,
SFTP block in stub mode, ingest-dir missing).
"""
from cyclone import db as db_mod
from cyclone.submission.core import submit_file
from cyclone.submission.result import SubmitOutcome
from cyclone.store import store as cycl_store
db_mod.init_db()
cycl_store.ensure_clearhouse_seeded()
clearhouse = cycl_store.get_clearhouse()
if clearhouse is None:
click.echo("No clearhouse seeded; cannot resolve SFTP block.", err=True)
sys.exit(2)
sftp_block = clearhouse.sftp_block
if sftp_block.stub:
click.echo("Clearhouse SFTP block is in stub mode; refusing to upload.", err=True)
sys.exit(2)
root = Path(ingest_dir).resolve()
if not root.exists():
click.echo(f"--ingest-dir does not exist: {root}", err=True)
sys.exit(2)
files: list[Path] = []
for batch_dir in sorted(root.glob("batch-*-claims")):
files.extend(sorted(p for p in batch_dir.glob("*.x12")
if not p.name.startswith("._")))
if not files:
click.echo(f"No .x12 files under {root}", err=True)
sys.exit(2)
click.echo(f"found {len(files)} files under {root}")
submitted = 0
skipped = 0
failed = 0
for i, src in enumerate(files, 1):
if limit is not None and i > limit:
break
try:
result = submit_file(
src, sftp_block=sftp_block, actor=actor, validate=validate,
)
except Exception as exc: # noqa: BLE001
click.echo(f"UNEXPECTED {src.name}: {exc.__class__.__name__}: {exc}", err=True)
failed += 1
continue
if result.outcome == SubmitOutcome.SUBMITTED:
submitted += 1
click.echo(f"submitted {src.name} batch={result.batch_id}")
elif result.outcome == SubmitOutcome.SKIPPED:
skipped += 1
click.echo(f"skipped {src.name} (already uploaded)")
else:
failed += 1
click.echo(
f"{result.outcome.value:<10} {src.name} {result.error or ''}",
err=True,
)
click.echo(f"\nsummary: submitted={submitted} skipped={skipped} failed={failed}")
# Per-file failures don't bump exit code; details in stdout.
# (Per cyclone-cli convention.)
@main.group()
def backup() -> None:
"""Encrypted DB backup management (SP17)."""
+13
View File
@@ -193,3 +193,16 @@ def test_submit_file_uses_default_paramiko_factory(monkeypatch):
)
assert result.outcome == SubmitOutcome.SUBMITTED
assert len(fake_paramiko_sftp.put_calls) == 1
def test_submit_batch_cli_help():
"""`cyclone submit-batch --help` exits 0 and shows the flags."""
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-m", "cyclone.cli", "submit-batch", "--help"],
capture_output=True, text=True,
cwd=str(Path(__file__).parent.parent),
)
assert result.returncode == 0
assert "--ingest-dir" in result.stdout