#!/usr/bin/env python3 """SP39 sibling-project regen: re-serialize the 363 corrected files through the fixed serializer into ``ingest/corrected-v2/``. Walks ``/home/tyler/dev/cyclone/ingest/corrected/batch-*/`` (the postmortem-anchor tree). For each ``.x12`` / ``.txt`` file it re-parses via :func:`cyclone.parsers.parse_837.parse` and re-serializes via :func:`cyclone.parsers.serialize_837.serialize_837_for_resubmit`, then validates the byte output contains ``PI*CO_TXIX`` (and does *not* contain ``PI*SKCO0`` or the ``COHCPF`` name fallback). The output is written to ``ingest/corrected-v2/v2-batch---claims/`` with a single global counter that guarantees unique timestamps and unique interchange control numbers across the run. The original ``ingest/corrected/batch-*/`` tree is preserved (postmortem anchor); the new tree is the production-ready set the operator pushes to Gainwell. Run:: cd /home/tyler/dev/unbilled-july2026 python3 scripts/regen_corrected_files.py """ from __future__ import annotations import sys from datetime import datetime, timedelta from pathlib import Path from zoneinfo import ZoneInfo # Make the cyclone parser importable without an install. sys.path.insert(0, '/home/tyler/dev/cyclone/backend/src') from cyclone.parsers.parse_837 import parse as parse_837_text # noqa: E402 from cyclone.parsers.payer import PayerConfig # noqa: E402 from cyclone.parsers.serialize_837 import serialize_837_for_resubmit # noqa: E402 CYCLONE_ROOT = Path('/home/tyler/dev/cyclone') INGEST_ROOT = CYCLONE_ROOT / 'ingest' / 'corrected' OUT_ROOT = CYCLONE_ROOT / 'ingest' / 'corrected-v2' def _batch_id_from_dirname(d: Path) -> str: """``batch---claims`` -> ```` (the first hyphen-delimited chunk).""" return d.name.split('-')[1] def _interchange_index_for(global_counter: int) -> int: """The serialize helper needs a unique ISA13 per file; offset by 1000 so the resulting ISA13s stay well above the seeded fixture range (0-999).""" return 1000 + global_counter def main() -> int: """Walk every ``batch-*/.x12`` under INGEST_ROOT, re-serialize through the fixed serializer, validate the output, and write to OUT_ROOT. Returns 0 on a clean run, 1 if any file failed.""" payer_cfg_co = PayerConfig.co_medicaid() base_ts = datetime.now(ZoneInfo('America/Denver')).replace(microsecond=0) global_counter = 0 ok = 0 failed = 0 failures: list[dict] = [] batch_dirs = sorted(p for p in INGEST_ROOT.iterdir() if p.is_dir() and p.name.startswith('batch-')) print(f'Found {len(batch_dirs)} batch dirs under {INGEST_ROOT}', flush=True) for batch_dir in batch_dirs: batch_id = _batch_id_from_dirname(batch_dir) files = sorted(batch_dir.glob('*.x12')) + sorted(batch_dir.glob('*.txt')) if not files: continue n = len(files) out_dir = OUT_ROOT / f'v2-batch-{batch_id}-{n}-claims' out_dir.mkdir(parents=True, exist_ok=True) print(f' {batch_id[:12]}... ({n} files) -> {out_dir}', flush=True) for src in files: global_counter += 1 raw = src.read_text() try: parsed = parse_837_text(raw, payer_cfg_co) except Exception as exc: # noqa: BLE001 failed += 1 failures.append({'file': src.name, 'reason': f'parse: {exc}'}) continue if not parsed.claims: failed += 1 failures.append({'file': src.name, 'reason': 'no claims parsed'}) continue try: text = serialize_837_for_resubmit( parsed.claims[0], interchange_index=_interchange_index_for(global_counter), ) except Exception as exc: # noqa: BLE001 failed += 1 failures.append({'file': src.name, 'reason': f'serialize: {exc}'}) continue content = text.encode() if b'PI*CO_TXIX' not in content: failed += 1 failures.append({'file': src.name, 'reason': 'PI*CO_TXIX missing'}) continue if b'PI*SKCO0' in content or b'*COHCPF****' in content: failed += 1 failures.append({'file': src.name, 'reason': 'SKCO0/COHCPF still present'}) continue ts = base_ts + timedelta(milliseconds=global_counter) ts_str = ts.strftime('%Y%m%d%H%M%S') + f'{ts.microsecond // 1000:03d}' fname = f'tp-cyc-837P-{ts_str}-{global_counter:04d}-1of1.x12' (out_dir / fname).write_text(text) ok += 1 print(f'\nDONE: ok={ok} failed={failed}', flush=True) if failures: print('First failures:') for f in failures[:5]: print(f' {f}') return 0 if failed == 0 else 1 if __name__ == '__main__': sys.exit(main())