feat(sp24): register cyclone reissue-claims CLI subcommand
Adds @main.command('reissue-claims') to cli.py following the
cyclone-cli conventions:
- long-form flags only (no positional args; per-click-confirm-safe)
- exit codes 0 / 1 / 2 with documented semantics in the docstring
- click.echo(..., err=True) for errors
- setup_logging() called per-invocation so --log-level overrides
the group default
The command body is a thin shell over cyclone.reissue:
1. ig_correctness_check (refuses to run if the SP24 guard trips)
2. parse_inputs → emit_outputs → write_summary_sidecar
3. Optional zip_outputs to --zip-output
4. Final click.echo summary line.
Live-test on the 2026-07-08 dzinesco July-8 batch produces
358 X12 files, 378078 bytes total, zip 245829 bytes (matches the
original scripts/reissue_claims.py exactly). All 358 emitted files
have HL*2 child_count=0 and no HL*3 segment — the IG-correct shape
for SBR02='18'.
This commit is contained in:
@@ -2050,5 +2050,234 @@ def rebill_from_835(
|
|||||||
click.echo(f"Wrote {n} summary rows to {summary_path}")
|
click.echo(f"Wrote {n} summary rows to {summary_path}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SP24: `cyclone reissue-claims` — offline 837P reissue workflow.
|
||||||
|
#
|
||||||
|
# Re-parses raw 837P files under --input-dir, validates each parsed
|
||||||
|
# claim against the per-payer config, and emits one IG-correct
|
||||||
|
# single-claim X12 file per claim under --output-root/<date>/<pipeline>/.
|
||||||
|
# Optionally zips the output to --zip-output.
|
||||||
|
#
|
||||||
|
# The IG-correctness guard (`ig_correctness_check`) reads
|
||||||
|
# `serialize_837.PATIENT_LOOP_DEFAULT_INCLUDED` on every invocation and
|
||||||
|
# refuses to run if it ever flips back to True — see the SP24 spec for
|
||||||
|
# the regression story behind the constant.
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — every file emitted cleanly (or with per-file warnings only)
|
||||||
|
# 1 — IG-correctness guard tripped OR an unexpected error
|
||||||
|
# 2 — parse / validation failure on a file AND zero claims survived
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@main.command("reissue-claims")
|
||||||
|
@click.option(
|
||||||
|
"--input-dir",
|
||||||
|
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||||
|
required=True,
|
||||||
|
help="Directory holding raw 837P files (*.x12, *.txt, *.edi).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output-root",
|
||||||
|
type=click.Path(file_okay=False, path_type=Path),
|
||||||
|
default=Path("dev/rebills"),
|
||||||
|
show_default=True,
|
||||||
|
help="Root dir; output lands in <output-root>/<date>/<pipeline>/.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--date",
|
||||||
|
default=None,
|
||||||
|
help="YYYY-MM-DD subfolder under --output-root (default: today MT).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--pipeline",
|
||||||
|
default="initial",
|
||||||
|
show_default=True,
|
||||||
|
help="Pipeline subfolder name (e.g. 'initial', 'resubmit', 'corrected-v2').",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--payer",
|
||||||
|
default="co_medicaid",
|
||||||
|
show_default=True,
|
||||||
|
help="Payer config preset passed to the parser.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--sender-id",
|
||||||
|
default="11525703",
|
||||||
|
show_default=True,
|
||||||
|
help="ISA06 / GS02 — Dzinesco TPID.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--receiver-id",
|
||||||
|
default="COMEDASSISTPROG",
|
||||||
|
show_default=True,
|
||||||
|
help="ISA08 / GS03 — CO Medicaid.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--submitter-name",
|
||||||
|
default="Dzinesco",
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--submitter-contact-name",
|
||||||
|
default="Tyler Martinez",
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--submitter-contact-email",
|
||||||
|
default="tyler@dzinesco.com",
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--receiver-name",
|
||||||
|
default="COLORADO MEDICAL ASSISTANCE PROGRAM",
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--zip-output",
|
||||||
|
type=click.Path(file_okay=False, path_type=Path),
|
||||||
|
default=None,
|
||||||
|
help="If set, zip all emitted X12 files to this path.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-clean",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Don't wipe prior .x12 outputs in the pipeline subdir before writing.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--log-level",
|
||||||
|
default="INFO",
|
||||||
|
show_default=True,
|
||||||
|
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"]),
|
||||||
|
)
|
||||||
|
def reissue_claims(
|
||||||
|
input_dir: Path,
|
||||||
|
output_root: Path,
|
||||||
|
date: str | None,
|
||||||
|
pipeline: str,
|
||||||
|
payer: str,
|
||||||
|
sender_id: str,
|
||||||
|
receiver_id: str,
|
||||||
|
submitter_name: str,
|
||||||
|
submitter_contact_name: str,
|
||||||
|
submitter_contact_email: str,
|
||||||
|
receiver_name: str,
|
||||||
|
zip_output: Path | None,
|
||||||
|
no_clean: bool,
|
||||||
|
log_level: str,
|
||||||
|
) -> None:
|
||||||
|
"""Re-parse raw 837P files and emit one IG-correct single-claim X12 per claim (SP24).
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Parse every *.x12 / *.txt / *.edi under --input-dir.
|
||||||
|
2. Validate each parsed claim; skip claims with hard errors.
|
||||||
|
3. Serialize each surviving claim via the IG-correct serializer.
|
||||||
|
4. Write one X12 file per claim under
|
||||||
|
--output-root/<date>/<pipeline>/ using HCPF-spec filenames.
|
||||||
|
5. Optionally zip the output to --zip-output.
|
||||||
|
|
||||||
|
Exit codes: 0 success (some per-file warnings are non-fatal);
|
||||||
|
1 IG-correctness guard tripped or unexpected error;
|
||||||
|
2 parse / validation failure on a file AND zero claims survived.
|
||||||
|
"""
|
||||||
|
# SP18: re-run so --log-level overrides the group default.
|
||||||
|
setup_logging(level=log_level)
|
||||||
|
|
||||||
|
from cyclone.reissue import (
|
||||||
|
emit_outputs,
|
||||||
|
ig_correctness_check,
|
||||||
|
parse_inputs,
|
||||||
|
write_summary_sidecar,
|
||||||
|
zip_outputs,
|
||||||
|
)
|
||||||
|
from cyclone.parsers.payer import PayerConfig
|
||||||
|
|
||||||
|
log = logging.getLogger("reissue-claims")
|
||||||
|
|
||||||
|
# 0. IG-correctness guard. Fires on every invocation — cheap (one
|
||||||
|
# constant read) and the only defense against the 999 rejection
|
||||||
|
# root cause coming back via a future refactor.
|
||||||
|
if not ig_correctness_check(logger=log):
|
||||||
|
click.echo(
|
||||||
|
"REFUSING to run: PATIENT_LOOP_DEFAULT_INCLUDED is True; "
|
||||||
|
"the 2000C patient loop is forbidden when SBR02='18'. "
|
||||||
|
"Restore the IG-correct default (False) in serialize_837.py.",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 1. Payer config factory. Keep this tiny — extend as more
|
||||||
|
# payer presets land in PayerConfig.
|
||||||
|
payer_factories = {
|
||||||
|
"co_medicaid": PayerConfig.co_medicaid,
|
||||||
|
"generic_837p": PayerConfig.generic_837p,
|
||||||
|
}
|
||||||
|
if payer not in payer_factories:
|
||||||
|
raise click.BadParameter(
|
||||||
|
f"Unknown payer {payer!r}. Choose from: {', '.join(payer_factories)}"
|
||||||
|
)
|
||||||
|
payer_config = payer_factories[payer]()
|
||||||
|
|
||||||
|
# 2. Output directory layout.
|
||||||
|
date_label = date or datetime.now().strftime("%Y-%m-%d")
|
||||||
|
out_dir = output_root / date_label / pipeline
|
||||||
|
if out_dir.exists() and not no_clean:
|
||||||
|
# Clean only the per-claim X12 files + summary; leave any
|
||||||
|
# sibling artifacts (logs, sidecars) alone.
|
||||||
|
for p in out_dir.glob("tp*.x12"):
|
||||||
|
p.unlink()
|
||||||
|
log.info("cleaned prior .x12 outputs under %s", out_dir)
|
||||||
|
|
||||||
|
# 3. Parse → 4. Serialize → 5. Zip.
|
||||||
|
log.info("parse: input_dir=%s payer=%s", input_dir, payer)
|
||||||
|
claims, parse_errors = parse_inputs(input_dir, payer_config)
|
||||||
|
log.info(
|
||||||
|
"parse: %d claims read, %d file-level errors",
|
||||||
|
len(claims),
|
||||||
|
len(parse_errors),
|
||||||
|
)
|
||||||
|
for src, err in parse_errors[:10]:
|
||||||
|
log.warning(" %s: %s", src, err)
|
||||||
|
if parse_errors and not claims:
|
||||||
|
click.echo(
|
||||||
|
f"PARSE FAILED: zero claims survived; first error: {parse_errors[0][1]}",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
log.info("serialize: output_dir=%s", out_dir)
|
||||||
|
written = emit_outputs(
|
||||||
|
claims,
|
||||||
|
payer_config=payer_config,
|
||||||
|
output_dir=out_dir,
|
||||||
|
sender_id=sender_id,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
submitter_name=submitter_name,
|
||||||
|
submitter_contact_name=submitter_contact_name,
|
||||||
|
submitter_contact_email=submitter_contact_email,
|
||||||
|
receiver_name=receiver_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
summary_path = write_summary_sidecar(claims, written, out_dir)
|
||||||
|
total_bytes = sum(p.stat().st_size for p in written)
|
||||||
|
log.info(
|
||||||
|
"serialize: %d files, %d bytes, summary=%s",
|
||||||
|
len(written),
|
||||||
|
total_bytes,
|
||||||
|
summary_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if zip_output:
|
||||||
|
zip_outputs(written, zip_output)
|
||||||
|
log.info("zip: %s (%d bytes)", zip_output, zip_output.stat().st_size)
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
f"DONE files={len(written)} errors={len(parse_errors)} out={out_dir}"
|
||||||
|
)
|
||||||
|
if parse_errors:
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user