feat(sp39): cli resubmissions status
This commit is contained in:
@@ -1376,6 +1376,68 @@ def ack_orphans_reconcile_cmd(dry_run: bool) -> None:
|
|||||||
click.echo("(dry-run — no rows inserted)")
|
click.echo("(dry-run — no rows inserted)")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SP39: `cyclone resubmissions status` — operator view onto the
|
||||||
|
# resubmissions audit table. Joins against claim_acks (via the
|
||||||
|
# existing SP28/31 auto-link) + remittances (via CLP->claim) so the
|
||||||
|
# operator can confirm post-submission outcomes of the corrected
|
||||||
|
# 837 files pushed via `cyclone resubmit-rejected-claims`.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@main.group("resubmissions")
|
||||||
|
def resubmissions_group() -> None:
|
||||||
|
"""SP39: inspect post-submission status of corrected-file pushes."""
|
||||||
|
|
||||||
|
|
||||||
|
@resubmissions_group.command("status")
|
||||||
|
@click.option("--batch-id", default=None,
|
||||||
|
help="Restrict to a single source batch_id "
|
||||||
|
"(matches ingest/corrected-v2/<batch-id>-<N>-claims/).")
|
||||||
|
@click.option("--limit", default=200, type=int,
|
||||||
|
help="Maximum rows to print (default 200).")
|
||||||
|
def resubmissions_status_cmd(batch_id: str | None, limit: int) -> None:
|
||||||
|
"""Print per-claim resubmission status (ack + payment).
|
||||||
|
|
||||||
|
One row per ``resubmissions`` row, optionally filtered by
|
||||||
|
``--batch-id``. The ``ack`` column reflects the latest inbound
|
||||||
|
999 / 277CA accept-reject code linked to the claim; the
|
||||||
|
``payment`` column is ``paid`` once a remittance CLP links.
|
||||||
|
|
||||||
|
Exit codes: 0 on success, 1 on DB error.
|
||||||
|
"""
|
||||||
|
from cyclone import db as db_mod
|
||||||
|
from cyclone.store import store as cycl_store
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_mod.init_db()
|
||||||
|
rows = cycl_store.find_resubmission_status(batch_id=batch_id)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
click.echo(f"resubmissions status failed: {type(exc).__name__}: {exc}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
click.echo("no resubmissions recorded")
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
f"{'claim_id':<22} {'batch_id':<36} {'resubmitted_at':<20} "
|
||||||
|
f"{'ack':<16} {'payment':<14}"
|
||||||
|
)
|
||||||
|
click.echo("-" * 110)
|
||||||
|
for r in rows[:limit]:
|
||||||
|
click.echo(
|
||||||
|
f"{r.claim_id:<22} {r.batch_id:<36} "
|
||||||
|
f"{r.resubmitted_at.isoformat(timespec='seconds'):<20} "
|
||||||
|
f"{(r.ack_status or 'pending_999'):<16} "
|
||||||
|
f"{(r.payment_status or '-'):<14}"
|
||||||
|
)
|
||||||
|
if len(rows) > limit:
|
||||||
|
click.echo(
|
||||||
|
f"... ({len(rows) - limit} more; pass --limit to see all)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# SP25: ``cyclone recover-ingest`` — offline parse + DB-write helper for
|
# SP25: ``cyclone recover-ingest`` — offline parse + DB-write helper for
|
||||||
# stranded source files in ingest/. Mirrors the data-writing half of
|
# stranded source files in ingest/. Mirrors the data-writing half of
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""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")
|
||||||
Reference in New Issue
Block a user