From a3831213cce2042edad331c04adfa7e975778996 Mon Sep 17 00:00:00 2001 From: Nora Date: Tue, 7 Jul 2026 20:57:18 -0600 Subject: [PATCH] fix(sp41): tighten pipeline_a write_files contract + cleanup imports --- backend/src/cyclone/rebill/pipeline_a.py | 17 +++++++++++------ backend/tests/test_rebill_pipeline_a.py | 2 -- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/src/cyclone/rebill/pipeline_a.py b/backend/src/cyclone/rebill/pipeline_a.py index a52ce14..0639d47 100644 --- a/backend/src/cyclone/rebill/pipeline_a.py +++ b/backend/src/cyclone/rebill/pipeline_a.py @@ -19,6 +19,8 @@ from cyclone.rebill.reconcile import ReconcileOutcome, OutcomeCategory @dataclass(frozen=True) class RebillClaim: + """Pre-emission 837P shape for Pipeline A. The serializer adapter (Task 12) converts this to a ClaimOutput for serialize_837.""" + claim_id: str # reuses original claim_submit_id member_id: str procedure: str @@ -35,9 +37,10 @@ def build_pipeline_a_claims( carc_decisions: list[CarcDecision], cas_reasons_per_visit: list[tuple[str, ...]], ) -> list[RebillClaim]: - """One input list per visit; align by index.""" + """Zip three parallel per-visit lists by index; drop PAID/NOT_IN_835 outcomes and EXCLUDED CARC decisions; emit one frequency-7 RebillClaim per survivor.""" out: list[RebillClaim] = [] - for vo, carc, reasons in zip(visit_outcomes, carc_decisions, cas_reasons_per_visit): + # strict=True: all three lists must be the same length; a mismatch is a contract bug, fail loud. + for vo, carc, reasons in zip(visit_outcomes, carc_decisions, cas_reasons_per_visit, strict=True): if vo.category not in (OutcomeCategory.DENIED, OutcomeCategory.PARTIAL): continue if carc == CarcDecision.EXCLUDED: @@ -50,7 +53,7 @@ def build_pipeline_a_claims( charge=vo.visit.billed, frequency_code="7", needs_review=(carc == CarcDecision.REVIEW), - original_carc_reasons=tuple(reasons), + original_carc_reasons=reasons, )) return out @@ -58,14 +61,16 @@ def build_pipeline_a_claims( def write_pipeline_a_files( claims: list[RebillClaim], out_dir: Path, - serialize_837_fn, # injected: cyclone.parsers.serialize_837.serialize_837 + serialize_837_fn, # injected: callable[[RebillClaim], str] (orchestrator-supplied adapter) tpid: str, ) -> list[Path]: """Write one 837P per claim, using HCPF-spec filenames via build_outbound_filename. - `serialize_837_fn(claim)` returns the X12 byte string. The filename is + `serialize_837_fn(claim)` returns the X12 string. The 837P envelope is + pure ASCII so we write it as text. The filename is `cyclone.edi.filenames.build_outbound_filename(tpid, '837P')`. """ + # Lazy: keep cyclone.rebill.pipeline_a importable without zoneinfo from filenames. from cyclone.edi.filenames import build_outbound_filename out_dir.mkdir(parents=True, exist_ok=True) @@ -74,6 +79,6 @@ def write_pipeline_a_files( body = serialize_837_fn(c) fname = build_outbound_filename(tpid, "837P") path = out_dir / fname - path.write_bytes(body) + path.write_text(body, encoding="ascii") paths.append(path) return paths diff --git a/backend/tests/test_rebill_pipeline_a.py b/backend/tests/test_rebill_pipeline_a.py index e4ebb1d..936bc48 100644 --- a/backend/tests/test_rebill_pipeline_a.py +++ b/backend/tests/test_rebill_pipeline_a.py @@ -1,9 +1,7 @@ """Pipeline A: denied/partial visits become frequency-7 replacement claims.""" from datetime import date from decimal import Decimal -from pathlib import Path from cyclone.rebill.pipeline_a import ( - RebillClaim, build_pipeline_a_claims, write_pipeline_a_files, )