feat(backend): add store iterators (claims, remits, providers, activity)
This commit is contained in:
@@ -59,6 +59,130 @@ class InMemoryStore:
|
||||
with self._lock:
|
||||
return list(self._batches)
|
||||
|
||||
def iter_claims(
|
||||
self,
|
||||
*,
|
||||
batch_id: str | None = None,
|
||||
status: str | None = None,
|
||||
provider_npi: str | None = None,
|
||||
date_from: str | None = None,
|
||||
date_to: str | None = None,
|
||||
sort: str | None = None,
|
||||
order: str = "desc",
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> Iterable[dict]:
|
||||
with self._lock:
|
||||
all_claims: list[dict] = []
|
||||
for b in self._batches:
|
||||
if b.kind != "837p":
|
||||
continue
|
||||
if batch_id is not None and b.id != batch_id:
|
||||
continue
|
||||
# ParseResult / ParseResult835 union: 837p kinds are ParseResult
|
||||
result = b.result # type: ignore[assignment]
|
||||
for c in result.claims:
|
||||
all_claims.append(to_ui_claim(
|
||||
c, batch_id=b.id, parsed_at=b.parsed_at,
|
||||
))
|
||||
if status is not None:
|
||||
all_claims = [c for c in all_claims if c["status"] == status]
|
||||
if provider_npi is not None:
|
||||
all_claims = [c for c in all_claims if c["providerNpi"] == provider_npi]
|
||||
if sort is not None:
|
||||
all_claims.sort(
|
||||
key=lambda c: c.get(sort, 0) or 0,
|
||||
reverse=(order == "desc"),
|
||||
)
|
||||
yield from all_claims[offset:offset + limit]
|
||||
|
||||
def iter_remittances(
|
||||
self,
|
||||
*,
|
||||
batch_id: str | None = None,
|
||||
payer: str | None = None,
|
||||
claim_id: str | None = None,
|
||||
date_from: str | None = None,
|
||||
date_to: str | None = None,
|
||||
sort: str | None = None,
|
||||
order: str = "desc",
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> Iterable[dict]:
|
||||
with self._lock:
|
||||
all_remits: list[dict] = []
|
||||
for b in self._batches:
|
||||
if b.kind != "835":
|
||||
continue
|
||||
if batch_id is not None and b.id != batch_id:
|
||||
continue
|
||||
# 835 kinds are ParseResult835; its claims list holds ClaimPayment
|
||||
result = b.result # type: ignore[assignment]
|
||||
for cp in result.claims:
|
||||
all_remits.append(to_ui_remittance(
|
||||
cp, batch_id=b.id, parsed_at=b.parsed_at,
|
||||
))
|
||||
if payer is not None:
|
||||
all_remits = [r for r in all_remits if r.get("payerName") == payer]
|
||||
if claim_id is not None:
|
||||
all_remits = [r for r in all_remits if r["claimId"] == claim_id]
|
||||
if sort is not None:
|
||||
all_remits.sort(
|
||||
key=lambda r: r.get(sort, 0) or 0,
|
||||
reverse=(order == "desc"),
|
||||
)
|
||||
yield from all_remits[offset:offset + limit]
|
||||
|
||||
def distinct_providers(self) -> list[dict]:
|
||||
with self._lock:
|
||||
by_npi: dict[str, dict] = {}
|
||||
for b in self._batches:
|
||||
if b.kind != "837p":
|
||||
continue
|
||||
result = b.result # type: ignore[assignment]
|
||||
if not result.claims:
|
||||
continue
|
||||
# billing_provider lives on each claim, not at the batch level
|
||||
bp = result.claims[0].billing_provider
|
||||
npi = bp.npi
|
||||
if npi not in by_npi:
|
||||
by_npi[npi] = to_ui_provider(
|
||||
npi=npi,
|
||||
name=bp.name or "",
|
||||
tax_id=bp.tax_id,
|
||||
claim_count=0,
|
||||
outstanding_ar=0.0,
|
||||
)
|
||||
by_npi[npi]["claimCount"] += len(result.claims)
|
||||
return list(by_npi.values())
|
||||
|
||||
def recent_activity(self, *, limit: int = 200) -> list[dict]:
|
||||
with self._lock:
|
||||
events: list[dict] = []
|
||||
for b in reversed(self._batches):
|
||||
if b.kind == "837p":
|
||||
result = b.result # type: ignore[assignment]
|
||||
for c in result.claims:
|
||||
events.append(to_activity_event(
|
||||
id=f"{b.id}:{c.claim_id}",
|
||||
kind="claim_submitted",
|
||||
message=f"Claim {c.claim_id} submitted · {c.payer.name}",
|
||||
timestamp=b.parsed_at,
|
||||
npi=c.billing_provider.npi,
|
||||
amount=float(c.claim.total_charge or 0.0),
|
||||
))
|
||||
elif b.kind == "835":
|
||||
result = b.result # type: ignore[assignment]
|
||||
for cp in result.claims:
|
||||
events.append(to_activity_event(
|
||||
id=f"{b.id}:{cp.payer_claim_control_number}",
|
||||
kind="remit_received",
|
||||
message=f"Remit {cp.payer_claim_control_number} received",
|
||||
timestamp=b.parsed_at,
|
||||
amount=float(cp.total_paid or 0.0),
|
||||
))
|
||||
return events[:limit]
|
||||
|
||||
|
||||
store = InMemoryStore()
|
||||
|
||||
|
||||
@@ -203,3 +203,87 @@ def test_to_activity_event_uses_iso_timestamp():
|
||||
assert out["id"] == "a1"
|
||||
assert out["kind"] == "claim_submitted"
|
||||
assert out["amount"] == 100.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 3: Iterators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_iter_claims_filters_by_status():
|
||||
s = InMemoryStore()
|
||||
result = _make_result()
|
||||
a = _make_claim("A")
|
||||
b = _make_claim("B")
|
||||
b.validation = ValidationReport(
|
||||
passed=True, errors=[],
|
||||
warnings=[ValidationIssue(rule="W1", severity="warning", message="x")],
|
||||
)
|
||||
result.claims = [a, b]
|
||||
s.add(BatchRecord(
|
||||
id="1", kind="837p", input_filename="a.txt",
|
||||
parsed_at="2026-06-19T12:00:00Z", result=result,
|
||||
))
|
||||
out = list(s.iter_claims(status="submitted"))
|
||||
assert all(c["status"] == "submitted" for c in out)
|
||||
assert any(c["id"] == "A" for c in out)
|
||||
out_pending = list(s.iter_claims(status="pending"))
|
||||
assert all(c["status"] == "pending" for c in out_pending)
|
||||
assert any(c["id"] == "B" for c in out_pending)
|
||||
|
||||
|
||||
def test_iter_claims_filters_by_batch_id():
|
||||
s = InMemoryStore()
|
||||
s.add(BatchRecord(
|
||||
id="b1", kind="837p", input_filename="a.txt",
|
||||
parsed_at="2026-06-19T12:00:00Z", result=_make_result(),
|
||||
))
|
||||
out = list(s.iter_claims(batch_id="b1"))
|
||||
assert all(c["batchId"] == "b1" for c in out)
|
||||
|
||||
|
||||
def test_iter_claims_sorts_by_billed_amount_desc():
|
||||
s = InMemoryStore()
|
||||
result = _make_result()
|
||||
result.claims = [
|
||||
_make_claim("cheap"),
|
||||
_make_claim("expensive"),
|
||||
]
|
||||
result.claims[0].claim.total_charge = Decimal("50.00")
|
||||
result.claims[1].claim.total_charge = Decimal("500.00")
|
||||
s.add(BatchRecord(
|
||||
id="b1", kind="837p", input_filename="a.txt",
|
||||
parsed_at="2026-06-19T12:00:00Z", result=result,
|
||||
))
|
||||
out = list(s.iter_claims(sort="billedAmount", order="desc"))
|
||||
assert out[0]["id"] == "expensive"
|
||||
assert out[1]["id"] == "cheap"
|
||||
|
||||
|
||||
def test_distinct_providers_aggregates_claims():
|
||||
s = InMemoryStore()
|
||||
result = _make_result()
|
||||
result.claims = [_make_claim("A"), _make_claim("B")]
|
||||
s.add(BatchRecord(
|
||||
id="b1", kind="837p", input_filename="a.txt",
|
||||
parsed_at="2026-06-19T12:00:00Z", result=result,
|
||||
))
|
||||
out = s.distinct_providers()
|
||||
assert len(out) == 1
|
||||
assert out[0]["npi"] == "1234567890"
|
||||
assert out[0]["claimCount"] == 2
|
||||
|
||||
|
||||
def test_recent_activity_returns_newest_first():
|
||||
s = InMemoryStore()
|
||||
s.add(BatchRecord(
|
||||
id="b1", kind="837p", input_filename="a.txt",
|
||||
parsed_at="2026-06-19T11:00:00Z", result=_make_result(),
|
||||
))
|
||||
s.add(BatchRecord(
|
||||
id="b2", kind="837p", input_filename="b.txt",
|
||||
parsed_at="2026-06-19T12:00:00Z", result=_make_result(),
|
||||
))
|
||||
out = s.recent_activity(limit=10)
|
||||
assert out[0]["timestamp"] == "2026-06-19T12:00:00Z"
|
||||
assert out[1]["timestamp"] == "2026-06-19T11:00:00Z"
|
||||
|
||||
Reference in New Issue
Block a user