feat(sp41): pipeline A — denied/partial rebill with frequency-7 + CARC gate
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
"""Pipeline A: denied/partial → frequency-7 replacement 837Ps.
|
||||||
|
|
||||||
|
For each visit the previous adjudication said was DENIED or PARTIAL,
|
||||||
|
emit a fresh 837P with CLM05-3 = '7' (replacement claim) and the
|
||||||
|
original claim_submit_id preserved as CLM01. The 837P carries the
|
||||||
|
original DOS and the same procedure / member.
|
||||||
|
|
||||||
|
The CARC-aware filter is the gate — only REBILL / REVIEW decisions are
|
||||||
|
emitted; EXCLUDED visits never make it to a file.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cyclone.rebill.carc_filter import CarcDecision
|
||||||
|
from cyclone.rebill.reconcile import ReconcileOutcome, OutcomeCategory
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RebillClaim:
|
||||||
|
claim_id: str # reuses original claim_submit_id
|
||||||
|
member_id: str
|
||||||
|
procedure: str
|
||||||
|
svc_date: date
|
||||||
|
charge: Decimal
|
||||||
|
frequency_code: str # always "7" for Pipeline A
|
||||||
|
needs_review: bool # True for CARC REVIEW decisions
|
||||||
|
original_carc_reasons: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def build_pipeline_a_claims(
|
||||||
|
original_claim_id: str,
|
||||||
|
visit_outcomes: list[ReconcileOutcome],
|
||||||
|
carc_decisions: list[CarcDecision],
|
||||||
|
cas_reasons_per_visit: list[tuple[str, ...]],
|
||||||
|
) -> list[RebillClaim]:
|
||||||
|
"""One input list per visit; align by index."""
|
||||||
|
out: list[RebillClaim] = []
|
||||||
|
for vo, carc, reasons in zip(visit_outcomes, carc_decisions, cas_reasons_per_visit):
|
||||||
|
if vo.category not in (OutcomeCategory.DENIED, OutcomeCategory.PARTIAL):
|
||||||
|
continue
|
||||||
|
if carc == CarcDecision.EXCLUDED:
|
||||||
|
continue
|
||||||
|
out.append(RebillClaim(
|
||||||
|
claim_id=original_claim_id,
|
||||||
|
member_id=vo.visit.member_id,
|
||||||
|
procedure=vo.visit.procedure,
|
||||||
|
svc_date=vo.visit.date,
|
||||||
|
charge=vo.visit.billed,
|
||||||
|
frequency_code="7",
|
||||||
|
needs_review=(carc == CarcDecision.REVIEW),
|
||||||
|
original_carc_reasons=tuple(reasons),
|
||||||
|
))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def write_pipeline_a_files(
|
||||||
|
claims: list[RebillClaim],
|
||||||
|
out_dir: Path,
|
||||||
|
serialize_837_fn, # injected: cyclone.parsers.serialize_837.serialize_837
|
||||||
|
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
|
||||||
|
`cyclone.edi.filenames.build_outbound_filename(tpid, '837P')`.
|
||||||
|
"""
|
||||||
|
from cyclone.edi.filenames import build_outbound_filename
|
||||||
|
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
paths: list[Path] = []
|
||||||
|
for c in claims:
|
||||||
|
body = serialize_837_fn(c)
|
||||||
|
fname = build_outbound_filename(tpid, "837P")
|
||||||
|
path = out_dir / fname
|
||||||
|
path.write_bytes(body)
|
||||||
|
paths.append(path)
|
||||||
|
return paths
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""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,
|
||||||
|
)
|
||||||
|
from cyclone.rebill.reconcile import VisitRow, ReconcileOutcome, OutcomeCategory
|
||||||
|
from cyclone.rebill.carc_filter import CarcDecision
|
||||||
|
|
||||||
|
|
||||||
|
def _outcome(date_, member, proc, billed, cat, unpaid=Decimal("0")):
|
||||||
|
visit = VisitRow(date=date_, member_id=member, procedure=proc, billed=Decimal(billed))
|
||||||
|
return ReconcileOutcome(visit, cat, unpaid, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pipeline_a_emits_frequency_7():
|
||||||
|
"""CLM05-3 must be 7 (replacement), preserving the original claim_submit_id."""
|
||||||
|
out = build_pipeline_a_claims(
|
||||||
|
original_claim_id="ORIG-001",
|
||||||
|
visit_outcomes=[
|
||||||
|
_outcome(date(2026, 6, 27), "J813715", "T1019", "2.32",
|
||||||
|
OutcomeCategory.DENIED, unpaid=Decimal("2.32")),
|
||||||
|
],
|
||||||
|
carc_decisions=[CarcDecision.REBILL],
|
||||||
|
cas_reasons_per_visit=[()],
|
||||||
|
)
|
||||||
|
assert len(out) == 1
|
||||||
|
assert out[0].claim_id == "ORIG-001"
|
||||||
|
assert out[0].frequency_code == "7"
|
||||||
|
assert out[0].svc_date == date(2026, 6, 27)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pipeline_a_excludes_carc_excluded_visits():
|
||||||
|
out = build_pipeline_a_claims(
|
||||||
|
original_claim_id="ORIG-002",
|
||||||
|
visit_outcomes=[
|
||||||
|
_outcome(date(2026, 6, 27), "J813715", "T1019", "2.32",
|
||||||
|
OutcomeCategory.DENIED),
|
||||||
|
],
|
||||||
|
carc_decisions=[CarcDecision.EXCLUDED],
|
||||||
|
cas_reasons_per_visit=[("CO-45",)],
|
||||||
|
)
|
||||||
|
assert out == [] # not emitted
|
||||||
|
|
||||||
|
|
||||||
|
def test_pipeline_a_surfaces_review_visits_with_flag():
|
||||||
|
out = build_pipeline_a_claims(
|
||||||
|
original_claim_id="ORIG-003",
|
||||||
|
visit_outcomes=[
|
||||||
|
_outcome(date(2026, 6, 27), "J813715", "T1019", "2.32",
|
||||||
|
OutcomeCategory.DENIED),
|
||||||
|
],
|
||||||
|
carc_decisions=[CarcDecision.REVIEW],
|
||||||
|
cas_reasons_per_visit=[("PI-16",)],
|
||||||
|
)
|
||||||
|
assert len(out) == 1
|
||||||
|
assert out[0].needs_review is True
|
||||||
Reference in New Issue
Block a user