feat(sp7): match_service_lines() pure function + tests

This commit is contained in:
Tyler
2026-06-20 19:29:21 -06:00
parent 8442bf91ce
commit 9626eae5f1
2 changed files with 222 additions and 0 deletions
+97
View File
@@ -320,3 +320,100 @@ def run(session, batch_id: str) -> ReconcileResult:
unmatched_remittances=len(unmatched_remits_after),
skipped=skipped,
)
# ---------------------------------------------------------------------------
# SP7 §4 — per-service-line matching
# ---------------------------------------------------------------------------
@dataclass
class LineMatch:
"""One row of a line-level reconciliation result.
At least one of ``claim_line`` / ``service_payment`` is non-None.
Spec §4.1.
"""
claim_line: Optional["_ClaimLineLike"] = None
service_payment: Optional["_ServicePaymentLike"] = None
status: str = "" # matched | unmatched_835_only | unmatched_837_only
class _ClaimLineLike(Protocol):
line_number: int
procedure_code: str
modifiers: list
service_date: Optional[date]
units: Optional[Decimal]
class _ServicePaymentLike(Protocol):
line_number: int
procedure_code: str
modifiers: list
service_date: Optional[date]
units: Optional[Decimal]
def _norm_code(code: str) -> str:
return (code or "").strip().upper()
def _norm_mods(mods) -> frozenset:
return frozenset((m or "").strip().upper() for m in (mods or []) if m)
def _svc_matches_claim(svc, claim) -> bool:
"""Strict match per spec §4.2."""
return (
_norm_code(svc.procedure_code) == _norm_code(claim.procedure_code)
and _norm_mods(svc.modifiers) == _norm_mods(claim.modifiers)
and svc.service_date == claim.service_date
and svc.units == claim.units
)
def match_service_lines(
claim_lines,
service_payments,
) -> list:
"""Pair each 837 SV1 line with the first 835 SVC composite that matches.
Spec §4. Unmatched lines on either side produce their own LineMatch
rows so every line is accounted for in the persistence step.
"""
matches: list = []
used_svc_ids: set = set()
for claim in sorted(claim_lines, key=lambda c: c.line_number):
match_idx: Optional[int] = None
for i, svc in enumerate(service_payments):
if i in used_svc_ids:
continue
if _svc_matches_claim(svc, claim):
match_idx = i
break
if match_idx is not None:
used_svc_ids.add(match_idx)
matches.append(LineMatch(
claim_line=claim,
service_payment=service_payments[match_idx],
status="matched",
))
else:
matches.append(LineMatch(
claim_line=claim,
service_payment=None,
status="unmatched_837_only",
))
for i, svc in enumerate(service_payments):
if i in used_svc_ids:
continue
matches.append(LineMatch(
claim_line=None,
service_payment=svc,
status="unmatched_835_only",
))
return matches