feat(sp31): add _content_keys_match pure helper (2-of-3 PCN/charge/NPI rule)
This commit is contained in:
@@ -117,6 +117,41 @@ def _pick_claim(
|
||||
return None
|
||||
|
||||
|
||||
# --- SP31: content-keys fallback matcher -----------------------------------
|
||||
|
||||
CHARGE_TOLERANCE = Decimal("0.01")
|
||||
KEYS_REQUIRED = 2
|
||||
|
||||
|
||||
def _content_keys_match(remit, claim) -> bool:
|
||||
"""Return True when at least KEYS_REQUIRED of {PCN, charge, NPI} agree.
|
||||
|
||||
Charge compared with ``CHARGE_TOLERANCE`` tolerance (rounding drift).
|
||||
NPI counts as "not matched" when the remit's NPI is empty.
|
||||
"""
|
||||
keys_matched = 0
|
||||
|
||||
# PCN: payer_claim_control_number ↔ patient_control_number (stripped).
|
||||
if (getattr(remit, "payer_claim_control_number", "") or "").strip() == \
|
||||
(getattr(claim, "patient_control_number", "") or "").strip():
|
||||
keys_matched += 1
|
||||
|
||||
# Charge: total_charge_amount ↔ total_charge (within $0.01).
|
||||
remit_charge = getattr(remit, "total_charge_amount", None)
|
||||
claim_charge = getattr(claim, "total_charge", None)
|
||||
if remit_charge is not None and claim_charge is not None and \
|
||||
abs(remit_charge - claim_charge) < CHARGE_TOLERANCE:
|
||||
keys_matched += 1
|
||||
|
||||
# NPI: rendering_provider_npi ↔ rendering_provider_npi (must be non-empty).
|
||||
remit_npi = (getattr(remit, "rendering_provider_npi", "") or "").strip()
|
||||
claim_npi = (getattr(claim, "rendering_provider_npi", "") or "").strip()
|
||||
if remit_npi and remit_npi == claim_npi:
|
||||
keys_matched += 1
|
||||
|
||||
return keys_matched >= KEYS_REQUIRED
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplyIntent:
|
||||
"""Result of applying a match. The caller persists this.
|
||||
|
||||
@@ -370,3 +370,120 @@ def test_run_reconcile_raise_in_session_leaves_prior_commits_alone(fixture_835):
|
||||
assert b is not None
|
||||
r = s.query(Remittance).first()
|
||||
assert r is not None
|
||||
|
||||
|
||||
# --- SP31: content-keys fallback matcher (pure helper) -----------------------
|
||||
|
||||
|
||||
def _make_remit_for_keys(remit_id: str, pcn: str, charge, npi: str):
|
||||
"""Tiny shim with the three fields _content_keys_match reads."""
|
||||
from cyclone.reconcile import _RemitLike # protocol not enforced at runtime
|
||||
return type("R", (), {
|
||||
"id": remit_id,
|
||||
"payer_claim_control_number": pcn,
|
||||
"total_charge_amount": charge,
|
||||
"rendering_provider_npi": npi,
|
||||
})()
|
||||
|
||||
|
||||
def _make_claim_for_keys(claim_id: str, pcn: str, charge, npi: str):
|
||||
return type("C", (), {
|
||||
"id": claim_id,
|
||||
"patient_control_number": pcn,
|
||||
"total_charge": charge,
|
||||
"rendering_provider_npi": npi,
|
||||
})()
|
||||
|
||||
|
||||
def test_content_keys_pcn_plus_charge_matches():
|
||||
"""PCN + charge match (NPI mismatched) → True."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "")
|
||||
c = _make_claim_for_keys("c1", "ABC", Decimal("100.00"), "1111111111")
|
||||
assert _content_keys_match(r, c) is True
|
||||
|
||||
|
||||
def test_content_keys_pcn_plus_npi_matches():
|
||||
"""PCN + NPI match (charge mismatched) → True."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "ABC", Decimal("200.00"), "1111111111")
|
||||
assert _content_keys_match(r, c) is True
|
||||
|
||||
|
||||
def test_content_keys_charge_plus_npi_matches():
|
||||
"""Charge + NPI match (PCN mismatched) → True."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "XYZ", Decimal("100.00"), "1111111111")
|
||||
assert _content_keys_match(r, c) is True
|
||||
|
||||
|
||||
def test_content_keys_all_three_match():
|
||||
"""All 3 match → True."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "ABC", Decimal("100.00"), "1111111111")
|
||||
assert _content_keys_match(r, c) is True
|
||||
|
||||
|
||||
def test_content_keys_only_pcn_does_not_match():
|
||||
"""Only PCN matches (charge + NPI both wrong) → False."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "ABC", Decimal("200.00"), "2222222222")
|
||||
assert _content_keys_match(r, c) is False
|
||||
|
||||
|
||||
def test_content_keys_only_charge_does_not_match():
|
||||
"""Only charge matches (PCN + NPI both wrong) → False."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "XYZ", Decimal("100.00"), "2222222222")
|
||||
assert _content_keys_match(r, c) is False
|
||||
|
||||
|
||||
def test_content_keys_none_match():
|
||||
"""All 3 fields disagree → False."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "XYZ", Decimal("200.00"), "2222222222")
|
||||
assert _content_keys_match(r, c) is False
|
||||
|
||||
|
||||
def test_content_keys_charge_tolerance_is_one_cent():
|
||||
"""Charge differs by exactly $0.005 (rounding) → True ($0.01 tolerance)."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.005"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "ABC", Decimal("100.00"), "1111111111")
|
||||
assert _content_keys_match(r, c) is True
|
||||
|
||||
|
||||
def test_content_keys_charge_differs_by_two_cents_no_match():
|
||||
"""Charge differs by $0.02 → False (outside tolerance)."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
# PCN and NPI deliberately differ so the only potential match is charge.
|
||||
# Charge is outside the $0.01 tolerance → 0 of 3 match → False.
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.02"), "1111111111")
|
||||
c = _make_claim_for_keys("c1", "XYZ", Decimal("100.00"), "2222222222")
|
||||
assert _content_keys_match(r, c) is False
|
||||
|
||||
|
||||
def test_content_keys_empty_npi_does_not_count():
|
||||
"""Empty NPI on remit counts as not-matched (falls back to PCN+charge rule)."""
|
||||
from cyclone.reconcile import _content_keys_match
|
||||
from decimal import Decimal
|
||||
r = _make_remit_for_keys("r1", "ABC", Decimal("100.00"), "")
|
||||
c = _make_claim_for_keys("c1", "ABC", Decimal("100.00"), "1111111111")
|
||||
assert _content_keys_match(r, c) is True # PCN + charge still match
|
||||
c2 = _make_claim_for_keys("c2", "XYZ", Decimal("100.00"), "1111111111")
|
||||
assert _content_keys_match(r, c2) is False # only charge, no PCN
|
||||
|
||||
Reference in New Issue
Block a user