145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
"""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
|