feat(sp31): integrate content-keys fallback into reconcile.run() with auto_matched_835 event

This commit is contained in:
Nora
2026-07-02 15:58:01 -06:00
parent 9bade2429c
commit ab91449e62
2 changed files with 151 additions and 4 deletions
+34 -4
View File
@@ -372,7 +372,7 @@ def run(session, batch_id: str) -> ReconcileResult:
""" """
from sqlalchemy import select from sqlalchemy import select
from cyclone.db import ( from cyclone.db import (
Batch, Claim, Remittance, CasAdjustment, Match, ActivityEvent, Batch, Claim, Remittance, CasAdjustment, Match as MatchORM, ActivityEvent,
ClaimState, ClaimState,
) )
@@ -390,6 +390,31 @@ def run(session, batch_id: str) -> ReconcileResult:
matches = match(unmatched_claims, new_remits) matches = match(unmatched_claims, new_remits)
# SP31: content-keys fallback for remits that PCN-exact couldn't pair.
# Operates on the still-unmatched remits so we don't double-match.
matched_remit_ids = {m.remittance.id for m in matches}
used_claim_ids = {m.claim.id for m in matches}
for remit in new_remits:
if remit.id in matched_remit_ids:
continue
if getattr(remit, "claim_id", None) is not None:
continue # already linked
matched_claim_id = _score_fallback_candidates(session, remit)
if matched_claim_id is None:
continue
# Find the claim object in the unmatched_claims list (it was loaded).
target_claim = next(
(c for c in unmatched_claims if c.id == matched_claim_id),
None,
)
if target_claim is None or target_claim.id in used_claim_ids:
continue
matches.append(Match(
claim=target_claim, remittance=remit,
strategy="score-auto", is_reversal=remit.is_reversal,
))
used_claim_ids.add(target_claim.id)
applied = 0 applied = 0
skipped = 0 skipped = 0
for m in matches: for m in matches:
@@ -413,7 +438,7 @@ def run(session, batch_id: str) -> ReconcileResult:
skipped += 1 skipped += 1
continue continue
session.add(Match( session.add(MatchORM(
claim_id=m.claim.id, remittance_id=m.remittance.id, claim_id=m.claim.id, remittance_id=m.remittance.id,
strategy=m.strategy, strategy=m.strategy,
matched_at=datetime.now(timezone.utc), matched_at=datetime.now(timezone.utc),
@@ -431,10 +456,15 @@ def run(session, batch_id: str) -> ReconcileResult:
m.remittance.claim_id = m.claim.id m.remittance.claim_id = m.claim.id
session.add(ActivityEvent( session.add(ActivityEvent(
ts=datetime.now(timezone.utc), kind=intent.activity_kind, ts=datetime.now(timezone.utc),
kind=("auto_matched_835" if m.strategy == "score-auto" else intent.activity_kind),
batch_id=batch_id, claim_id=m.claim.id, batch_id=batch_id, claim_id=m.claim.id,
remittance_id=m.remittance.id, remittance_id=m.remittance.id,
payload_json={"new_state": m.claim.state.value}, payload_json=(
{"new_state": m.claim.state.value, "strategy": m.strategy}
if m.strategy == "score-auto"
else {"new_state": m.claim.state.value}
),
)) ))
applied += 1 applied += 1
+117
View File
@@ -653,3 +653,120 @@ def test_content_keys_npi_from_remit_raw_json(db_session, make_claim):
db_session.add(remit) db_session.add(remit)
db_session.flush() db_session.flush()
assert _content_keys_match(remit, claim) is True # charge + NPI = 2 of 3 assert _content_keys_match(remit, claim) is True # charge + NPI = 2 of 3
# --- SP31: end-to-end integration via reconcile.run() -----------------------
def test_reconcile_run_emits_score_auto_match_when_pcn_misses(db_session, make_claim, make_remit):
"""A remit with no PCN match but matching charge+NPI triggers score-auto."""
from decimal import Decimal
from datetime import date
from sqlalchemy import select
from cyclone.db import Batch, ClaimState
from cyclone.reconcile import run as reconcile_run
claim = make_claim(patient_control_number="UNIQUE-CLM",
total_charge=Decimal("100.00"),
rendering_provider_npi="1111111111",
service_date_from=date(2026, 6, 1))
remit = make_remit(payer_claim_control_number="DIFFERENT-PCN",
total_charge_amount=Decimal("100.00"),
rendering_provider_npi="1111111111",
service_date=date(2026, 6, 5))
batch = Batch(id="batch-1", kind="835", input_filename="x.835", parsed_at=date(2026, 6, 5))
db_session.add(batch)
db_session.flush()
# Re-parent the remit to this batch (make_remit doesn't set batch_id).
remit.batch_id = batch.id
db_session.flush()
result = reconcile_run(db_session, batch.id)
db_session.flush() # autoflush=False on SessionLocal; force flush pending Match row
assert result.matched == 1
# Verify Match row was written with strategy="score-auto"
from cyclone.db import Match
matches = list(db_session.execute(
select(Match).where(Match.remittance_id == remit.id)
).scalars().all())
assert len(matches) == 1
assert matches[0].strategy == "score-auto"
assert matches[0].claim_id == claim.id
# Verify ActivityEvent was emitted with kind="auto_matched_835"
from cyclone.db import ActivityEvent
events = list(db_session.execute(
select(ActivityEvent).where(
ActivityEvent.remittance_id == remit.id,
ActivityEvent.kind == "auto_matched_835",
)
).scalars().all())
assert len(events) == 1
assert events[0].claim_id == claim.id
# Verify claim state was flipped
db_session.refresh(claim)
assert claim.state in {ClaimState.PAID, ClaimState.PARTIAL, ClaimState.RECEIVED}
def test_reconcile_run_pcn_path_unchanged(db_session, make_claim, make_remit):
"""Existing PCN-exact match still produces strategy='pcn-exact' (regression guard)."""
from decimal import Decimal
from datetime import date
from sqlalchemy import select
from cyclone.db import Batch, Match
from cyclone.reconcile import run as reconcile_run
claim = make_claim(patient_control_number="SAME-PCN",
total_charge=Decimal("100.00"),
rendering_provider_npi="1111111111",
service_date_from=date(2026, 6, 1))
remit = make_remit(payer_claim_control_number="SAME-PCN",
total_charge_amount=Decimal("100.00"),
rendering_provider_npi="1111111111",
service_date=date(2026, 6, 2))
batch = Batch(id="batch-1", kind="835", input_filename="x.835", parsed_at=date(2026, 6, 5))
db_session.add(batch)
db_session.flush()
remit.batch_id = batch.id
db_session.flush()
reconcile_run(db_session, batch.id)
db_session.flush() # autoflush=False on SessionLocal; force flush pending Match row
matches = list(db_session.execute(
select(Match).where(Match.remittance_id == remit.id)
).scalars().all())
assert len(matches) == 1
assert matches[0].strategy == "pcn-exact" # NOT "score-auto"
def test_reconcile_run_no_match_leaves_remit_unlinked(db_session, make_claim, make_remit):
"""No 2-of-3 match → no Match row, claim state unchanged."""
from decimal import Decimal
from datetime import date
from sqlalchemy import select
from cyclone.db import Batch, Match, ClaimState
from cyclone.reconcile import run as reconcile_run
claim = make_claim(patient_control_number="UNRELATED",
total_charge=Decimal("999.99"),
rendering_provider_npi="2222222222",
service_date_from=date(2026, 6, 1))
remit = make_remit(payer_claim_control_number="OTHER",
total_charge_amount=Decimal("100.00"),
rendering_provider_npi="1111111111",
service_date=date(2026, 6, 5))
batch = Batch(id="batch-1", kind="835", input_filename="x.835", parsed_at=date(2026, 6, 5))
db_session.add(batch)
db_session.flush()
remit.batch_id = batch.id
db_session.flush()
result = reconcile_run(db_session, batch.id)
assert result.matched == 0
matches = list(db_session.execute(
select(Match).where(Match.remittance_id == remit.id)
).scalars().all())
assert len(matches) == 0
db_session.refresh(claim)
assert claim.state == ClaimState.SUBMITTED # unchanged
db_session.refresh(remit)
assert remit.claim_id is None # unchanged