feat(sp7): inbox lanes matched_remittance includes matched/total line counts
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""SP7 — inbox lanes expose matched_remittance line counts.
|
||||
|
||||
For each claim row in the rejected / done_today lanes that has a
|
||||
matched remittance, the row must include a ``matched_remittance`` block
|
||||
with ``id``, ``matched_lines``, and ``total_lines``. These power the
|
||||
``MatchedRemitCard`` badge in T17.
|
||||
|
||||
Architecture note: 837 service lines live in ``Claim.raw_json``, so
|
||||
``total_lines`` is read from the JSON blob.
|
||||
"""
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from cyclone import db
|
||||
from cyclone.api import app
|
||||
from cyclone.parsers.models_835 import ClaimPayment, ServicePayment
|
||||
from cyclone.reconcile import run as reconcile_run
|
||||
from cyclone.store import _persist_835_remit, _remittance_835_row
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_app_state():
|
||||
yield
|
||||
if hasattr(app.state, "dismissed_pairs"):
|
||||
app.state.dismissed_pairs = set()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _seed_pair_with_two_lines():
|
||||
"""One 837 claim (2 service lines) + one 835 remit (1 svc composite
|
||||
matching line 1). After reconcile: 1 matched, 1 unmatched_837_only.
|
||||
Returns the claim_id (and the remittance_id via matched_remittance).
|
||||
"""
|
||||
bid_837 = str(uuid.uuid4())
|
||||
bid_835 = str(uuid.uuid4())
|
||||
claim_id = "CLM-INBOX-1"
|
||||
|
||||
svc_lines_json = [
|
||||
{
|
||||
"line_number": 1,
|
||||
"procedure": {"qualifier": "HC", "code": "99213", "modifiers": []},
|
||||
"charge": "100.00",
|
||||
"unit_type": "UN",
|
||||
"units": "1",
|
||||
"place_of_service": "11",
|
||||
"service_date": "2026-06-01",
|
||||
"provider_reference": None,
|
||||
},
|
||||
{
|
||||
"line_number": 2,
|
||||
"procedure": {"qualifier": "HC", "code": "99214", "modifiers": []},
|
||||
"charge": "100.00",
|
||||
"unit_type": "UN",
|
||||
"units": "1",
|
||||
"place_of_service": "11",
|
||||
"service_date": "2026-06-01",
|
||||
"provider_reference": None,
|
||||
},
|
||||
]
|
||||
|
||||
with db.SessionLocal()() as s:
|
||||
# 837 batch + claim
|
||||
s.add(db.Batch(
|
||||
id=bid_837, kind="837p", input_filename="x.837",
|
||||
parsed_at=datetime.now(timezone.utc),
|
||||
totals_json={}, validation_json={}, raw_result_json={},
|
||||
))
|
||||
s.add(db.Claim(
|
||||
id=claim_id,
|
||||
patient_control_number="PCN-INBOX",
|
||||
charge_amount=Decimal("200.00"),
|
||||
provider_npi="1234567890",
|
||||
payer_id="SKCO0",
|
||||
service_date_from=date(2026, 6, 1),
|
||||
service_date_to=date(2026, 6, 1),
|
||||
state=db.ClaimState.SUBMITTED,
|
||||
batch_id=bid_837,
|
||||
raw_json={"service_lines": svc_lines_json},
|
||||
))
|
||||
# 835 batch + remit
|
||||
s.add(db.Batch(
|
||||
id=bid_835, kind="835", input_filename="x.835",
|
||||
parsed_at=datetime.now(timezone.utc),
|
||||
totals_json={}, validation_json={}, raw_result_json={},
|
||||
))
|
||||
cp = ClaimPayment(
|
||||
payer_claim_control_number="PCN-INBOX",
|
||||
status_code="1",
|
||||
total_charge=Decimal("100.00"),
|
||||
total_paid=Decimal("80.00"),
|
||||
service_payments=[
|
||||
ServicePayment(
|
||||
line_number=1, procedure_qualifier="HC",
|
||||
procedure_code="99213",
|
||||
charge=Decimal("100.00"),
|
||||
payment=Decimal("80.00"),
|
||||
units=Decimal("1"),
|
||||
service_date=date(2026, 6, 1),
|
||||
),
|
||||
],
|
||||
)
|
||||
remit = _remittance_835_row(cp, bid_835)
|
||||
s.add(remit)
|
||||
s.flush()
|
||||
_persist_835_remit(s, cp, remit.id)
|
||||
# Force the claim to a terminal state so it lands in done_today.
|
||||
# Run reconcile which auto-matches by PCN + sets state to PAID.
|
||||
reconcile_run(s, bid_835)
|
||||
s.commit()
|
||||
|
||||
return claim_id
|
||||
|
||||
|
||||
def test_done_today_lane_includes_matched_remittance_line_counts(client):
|
||||
claim_id = _seed_pair_with_two_lines()
|
||||
# Backfill state_changed_at so the claim lands in the done_today lane
|
||||
# (reconcile.run() doesn't write it; the API surface is exercised by
|
||||
# the existing manual-match flow which sets it).
|
||||
from sqlalchemy.orm import Session
|
||||
with db.SessionLocal()() as s:
|
||||
cl = s.get(db.Claim, claim_id)
|
||||
cl.state_changed_at = datetime.now(timezone.utc)
|
||||
s.commit()
|
||||
|
||||
r = client.get("/api/inbox/lanes")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
# The seeded claim should be in done_today (auto-reconciled by run()).
|
||||
ids = [row["id"] for row in body["done_today"]]
|
||||
assert claim_id in ids, f"expected claim in done_today, got ids={ids}"
|
||||
row = next(r for r in body["done_today"] if r["id"] == claim_id)
|
||||
mr = row.get("matched_remittance")
|
||||
assert mr is not None, "expected matched_remittance block on done_today claim row"
|
||||
assert "id" in mr
|
||||
assert mr["total_lines"] == 2 # two 837 service lines
|
||||
assert mr["matched_lines"] == 1 # only line 1 has a matching SVC composite
|
||||
Reference in New Issue
Block a user