feat(backend): add reconcile.match with date-window + multi-claim fallback

This commit is contained in:
Tyler
2026-06-19 21:59:23 -06:00
parent 05a49be964
commit c30416e68c
2 changed files with 226 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
"""Pure reconciliation functions.
These functions take lists of objects (ORM or stand-in dataclasses) and
return intent objects describing what should change. They do not own a
database session; the caller (cyclone.store) persists the intents.
Match algorithm:
- Group claims by patient_control_number.
- For each remit, find a candidate claim with the same PCN.
- Prefer the claim whose service_date_from is closest to the remit's
service_date, within +/- window_days.
- If the remit has no service_date, fall back to the first claim for
that PCN (in input order).
- Skip claims that already have matched_remittance_id set.
- is_reversal flag is propagated to the Match.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from typing import Iterable, Optional, Protocol
WINDOW_DAYS_DEFAULT = 7
class _ClaimLike(Protocol):
id: str
patient_control_number: str
service_date_from: Optional[date]
matched_remittance_id: Optional[str]
class _RemitLike(Protocol):
id: str
payer_claim_control_number: str
service_date: Optional[date]
is_reversal: bool
@dataclass
class Match:
claim: _ClaimLike
remittance: _RemitLike
strategy: str
is_reversal: bool
def match(
claims: Iterable[_ClaimLike],
remittances: Iterable[_RemitLike],
*,
window_days: int = WINDOW_DAYS_DEFAULT,
) -> list[Match]:
"""Pair each remit with the closest eligible claim. Returns Match intents."""
claims_list = list(claims)
remits_list = list(remittances)
# Eligible = no current match, has a PCN.
eligible = [c for c in claims_list if c.patient_control_number and not c.matched_remittance_id]
by_pcn: dict[str, list[_ClaimLike]] = {}
for c in eligible:
by_pcn.setdefault(c.patient_control_number, []).append(c)
matches: list[Match] = []
used_claim_ids: set[str] = set()
for r in remits_list:
candidates = by_pcn.get(r.payer_claim_control_number, [])
candidates = [c for c in candidates if c.id not in used_claim_ids]
if not candidates:
continue
chosen = _pick_claim(candidates, r.service_date, window_days)
if chosen is None:
continue
matches.append(Match(
claim=chosen,
remittance=r,
strategy="auto",
is_reversal=r.is_reversal,
))
used_claim_ids.add(chosen.id)
return matches
def _pick_claim(
candidates: list[_ClaimLike],
remit_service_date: Optional[date],
window_days: int,
) -> Optional[_ClaimLike]:
"""Choose the claim with the closest service_date_from within the window."""
if remit_service_date is None:
return candidates[0] # no date info → first claim for that PCN
best: tuple[int, _ClaimLike] | None = None
for c in candidates:
if c.service_date_from is None:
continue
delta = abs((c.service_date_from - remit_service_date).days)
if delta > window_days:
continue
if best is None or delta < best[0]:
best = (delta, c)
if best is not None:
return best[1]
# Remit has a date but no candidate was within the window → no match.
return None