feat(api): extend /api/config/providers/{npi} with recent_claims + recent_activity
This commit is contained in:
@@ -32,6 +32,7 @@ from pydantic import ValidationError
|
||||
|
||||
from cyclone import __version__, db
|
||||
from cyclone.db import Claim, ClaimState, Remittance
|
||||
from sqlalchemy import desc, or_
|
||||
from cyclone.inbox_state import apply_999_rejections
|
||||
from cyclone.inbox_state_277ca import apply_277ca_rejections
|
||||
from cyclone.audit_log import AuditEvent, append_event, verify_chain
|
||||
@@ -2934,7 +2935,73 @@ def get_configured_provider(npi: str):
|
||||
p = store.get_provider(npi)
|
||||
if p is None:
|
||||
raise HTTPException(status_code=404, detail=f"provider {npi!r} not found")
|
||||
return json.loads(p.model_dump_json())
|
||||
provider_dict = json.loads(p.model_dump_json())
|
||||
|
||||
# SP21 Task 1.6: extend the response with two top-N arrays that the
|
||||
# drill-down peek panel hangs off. ``recent_claims`` reuses the
|
||||
# existing store projection (already returns UI-shaped dicts with
|
||||
# ``submissionDate``); ``recent_activity`` is a direct ORM join
|
||||
# because ``ActivityEvent`` has no ``provider_npi`` column — the
|
||||
# filter has to hop through ``Claim.id``.
|
||||
recent_claims = sorted(
|
||||
store.iter_claims(provider_npi=npi),
|
||||
key=lambda c: c.get("submissionDate") or "",
|
||||
reverse=True,
|
||||
)[:10]
|
||||
|
||||
# Activity filter has TWO join paths back to a Claim for this
|
||||
# provider:
|
||||
# 1. ``ActivityEvent.claim_id IN (claim_ids)`` — events that were
|
||||
# recorded with a claim FK already set (claim_submitted,
|
||||
# manual_match, claim_paid, etc.).
|
||||
# 2. ``Remittance.claim_id IN (claim_ids)`` — the original
|
||||
# ``remit_received`` event recorded at 835 ingest time
|
||||
# (``store.add`` lines 999-1003) is inserted with
|
||||
# ``claim_id=None`` because the remittance hasn't been matched
|
||||
# to a claim yet. The auto-reconcile pass later sets
|
||||
# ``Remittance.claim_id`` (``reconcile.run`` lines 289-293),
|
||||
# but the *original* ActivityEvent row stays orphaned. The
|
||||
# outer-join-then-OR lets us surface both shapes. Without
|
||||
# path 2, a provider's activity feed looks frozen the moment
|
||||
# an 835 lands — the most common activity, invisible.
|
||||
with db.SessionLocal()() as s:
|
||||
claim_ids = [
|
||||
cid
|
||||
for (cid,) in s.query(Claim.id)
|
||||
.filter(Claim.provider_npi == npi)
|
||||
.all()
|
||||
]
|
||||
activity_rows = []
|
||||
if claim_ids:
|
||||
activity_rows = (
|
||||
s.query(db.ActivityEvent)
|
||||
.outerjoin(
|
||||
Remittance,
|
||||
db.ActivityEvent.remittance_id == Remittance.id,
|
||||
)
|
||||
.filter(or_(
|
||||
db.ActivityEvent.claim_id.in_(claim_ids),
|
||||
Remittance.claim_id.in_(claim_ids),
|
||||
))
|
||||
.order_by(desc(db.ActivityEvent.ts))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
def _activity_to_ui(a):
|
||||
return {
|
||||
"id": a.id,
|
||||
"ts": a.ts.isoformat().replace("+00:00", "Z") if a.ts else "",
|
||||
"kind": a.kind,
|
||||
"batchId": a.batch_id,
|
||||
"claimId": a.claim_id,
|
||||
"remittanceId": a.remittance_id,
|
||||
"payload": a.payload_json or {},
|
||||
}
|
||||
|
||||
provider_dict["recent_claims"] = recent_claims
|
||||
provider_dict["recent_activity"] = [_activity_to_ui(a) for a in activity_rows]
|
||||
return provider_dict
|
||||
|
||||
|
||||
@app.get("/api/config/payers")
|
||||
|
||||
Reference in New Issue
Block a user