From 3b4f18eef68cb05c3309eefb4257e261ffe5df83 Mon Sep 17 00:00:00 2001 From: Nora Date: Tue, 7 Jul 2026 20:12:28 -0600 Subject: [PATCH] =?UTF-8?q?feat(sp41):=20CARC-aware=20filter=20=E2=80=94?= =?UTF-8?q?=20CO-45/26/129=20excluded,=20PI-16/OA-18=20reviewed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/cyclone/rebill/carc_filter.py | 44 ++++++++++++++++++++ backend/tests/test_rebill_carc_filter.py | 49 +++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 backend/src/cyclone/rebill/carc_filter.py create mode 100644 backend/tests/test_rebill_carc_filter.py diff --git a/backend/src/cyclone/rebill/carc_filter.py b/backend/src/cyclone/rebill/carc_filter.py new file mode 100644 index 0000000..47d4f31 --- /dev/null +++ b/backend/src/cyclone/rebill/carc_filter.py @@ -0,0 +1,44 @@ +"""CARC-aware filter. + +EXCLUDED = contractual / not recoverable / not a denial-of-payment in the +narrow sense (charge exceeds fee schedule, prior to coverage, etc.). +REVIEW = operator must decide (claim/service lacks info, non-covered +charges, duplicate, etc.). Anything else falls through to REBILL. +""" +from __future__ import annotations + +from enum import Enum + + +class CarcDecision(str, Enum): + EXCLUDED = "EXCLUDED" + REVIEW = "REVIEW" + REBILL = "REBILL" + + +# These CARCs are NOT recoverable denials. Do not rebill. +EXCLUDED_CARCS: frozenset[str] = frozenset({ + "CO-45", # charge exceeds fee schedule / contractual obligation + "CO-26", # expenses incurred prior to coverage + "CO-129", # prior processing information; forward to next payer +}) + +# These CARCs need human review before rebill. +REVIEW_CARCS: frozenset[str] = frozenset({ + "PI-16", # claim/service lacks information + "PI-96", # non-covered charges + "PI-15", # authorization / certification absent + "PI-4", # procedure not paid separately + "PI-110", # billing date predates service date + "OA-18", # exact duplicate (resubmit noise) + "OA-23", # impact of prior payer adjudication +}) + + +def decide_carc(reasons: tuple[str, ...] | list[str]) -> CarcDecision: + """Pick the strongest action: EXCLUDED > REVIEW > REBILL.""" + if any(r in EXCLUDED_CARCS for r in reasons): + return CarcDecision.EXCLUDED + if any(r in REVIEW_CARCS for r in reasons): + return CarcDecision.REVIEW + return CarcDecision.REBILL \ No newline at end of file diff --git a/backend/tests/test_rebill_carc_filter.py b/backend/tests/test_rebill_carc_filter.py new file mode 100644 index 0000000..cd078f9 --- /dev/null +++ b/backend/tests/test_rebill_carc_filter.py @@ -0,0 +1,49 @@ +"""CARC-aware filter for Pipeline A. + +Contractual / non-recoverable denials (CO-45, CO-26, CO-129) must be +excluded from rebill. CARCs that need operator review (PI-16, PI-96, +PI-15, PI-4, PI-110, OA-18, OA-23) must be surfaced as 'REVIEW' so the +operator can decide. +""" +from cyclone.rebill.carc_filter import ( + CarcDecision, + decide_carc, + EXCLUDED_CARCS, + REVIEW_CARCS, +) + + +def test_co45_is_excluded(): + assert decide_carc(("CO-45",)) == CarcDecision.EXCLUDED + + +def test_co26_is_excluded(): + assert decide_carc(("CO-26",)) == CarcDecision.EXCLUDED + + +def test_co129_is_excluded(): + assert decide_carc(("CO-129",)) == CarcDecision.EXCLUDED + + +def test_pi16_is_review(): + assert decide_carc(("PI-16",)) == CarcDecision.REVIEW + + +def test_o18_is_review(): + """OA-18 is the duplicate noise — surface for review, don't auto-rebill.""" + assert decide_carc(("OA-18",)) == CarcDecision.REVIEW + + +def test_no_carc_is_rebill(): + assert decide_carc(()) == CarcDecision.REBILL + + +def test_mixed_excluded_wins(): + """If any CARC is excluded, the whole service is excluded.""" + assert decide_carc(("PI-16", "CO-45")) == CarcDecision.EXCLUDED + + +def test_sets_have_expected_members(): + assert "CO-45" in EXCLUDED_CARCS + assert "PI-16" in REVIEW_CARCS + assert "OA-18" in REVIEW_CARCS \ No newline at end of file