feat(sp7): claim detail includes lineReconciliation slim projection

This commit is contained in:
Tyler
2026-06-20 19:37:38 -06:00
parent 48d4739864
commit 217f14a52a
2 changed files with 95 additions and 1 deletions
+72
View File
@@ -1297,6 +1297,78 @@ class CycloneStore:
# default ``None`` — the UI shows "no match" rather
# than crashing.
# SP7 §5.2: slim per-line projection so the ServiceLinesTable
# can show Paid + Adjustments columns without a second fetch.
# The 837 side is keyed by ``claim_service_line_number`` (the
# 1-based line number from raw_json) since 837 service lines
# are not a separate ORM table.
from cyclone.db import (
LineReconciliation, ServiceLinePayment, CasAdjustment,
)
slim_lrs = list(
s.query(LineReconciliation)
.filter(LineReconciliation.claim_id == claim_id)
.all()
)
svc_ids_for_cas = [
lr.service_line_payment_id
for lr in slim_lrs
if lr.service_line_payment_id is not None
]
cas_sums_by_svc: dict = {}
svc_by_id_slim: dict = {}
if svc_ids_for_cas:
cas_rows = (
s.query(CasAdjustment.service_line_payment_id, CasAdjustment.amount)
.filter(CasAdjustment.service_line_payment_id.in_(svc_ids_for_cas))
.all()
)
from collections import defaultdict
agg = defaultdict(lambda: Decimal("0"))
for svc_id, amount in cas_rows:
agg[svc_id] += Decimal(str(amount))
cas_sums_by_svc = {k: str(v) for k, v in agg.items()}
for svc in (
s.query(ServiceLinePayment)
.filter(ServiceLinePayment.id.in_(svc_ids_for_cas))
.all()
):
svc_by_id_slim[svc.id] = svc
slim_by_num: dict = {
lr.claim_service_line_number: lr
for lr in slim_lrs
if lr.claim_service_line_number is not None
}
line_reconciliation_slim: list = []
for sl in detail["serviceLines"]:
ln = sl.get("lineNumber")
lr = slim_by_num.get(ln)
if lr is None:
line_reconciliation_slim.append({
"lineNumber": ln,
"status": "unmatched_837_only",
"paid": None,
"adjustmentsSum": None,
})
continue
svc = (
svc_by_id_slim.get(lr.service_line_payment_id)
if lr.service_line_payment_id
else None
)
line_reconciliation_slim.append({
"lineNumber": ln,
"status": lr.status,
"paid": str(Decimal(str(svc.payment))) if svc else None,
"adjustmentsSum": (
cas_sums_by_svc.get(lr.service_line_payment_id)
if lr.service_line_payment_id
else None
),
})
detail["lineReconciliation"] = line_reconciliation_slim
return detail
def all(self) -> list[BatchRecord]:
+23 -1
View File
@@ -211,4 +211,26 @@ def test_get_claim_detail_matched_remit_inlines_summary(seeded_store_with_pair):
assert mr["status"] in {"received", "reconciled"}
# ISO timestamp must end in Z — the spec contract that lets the
# UI render the time without a tz suffix shim.
assert mr["receivedAt"].endswith("Z"), mr["receivedAt"]
assert mr["receivedAt"].endswith("Z"), mr["receivedAt"]
def test_get_claim_detail_includes_line_reconciliation_slim_projection(seeded_store):
"""SP7 §5.2: claim detail exposes a `lineReconciliation` slim list
parallel to `serviceLines`, indexed by line number. The values are
strings (Decimals serialized) or nulls when the line is unmatched."""
cid = seeded_store.get("/api/claims", headers=JSON).json()["items"][0]["id"]
resp = seeded_store.get(f"/api/claims/{cid}")
assert resp.status_code == 200, resp.text
body = resp.json()
assert "lineReconciliation" in body
lr_list = body["lineReconciliation"]
assert isinstance(lr_list, list)
# No 835 was ingested in this fixture, so every line is unmatched
# on the 835 side — the default for an unpaired claim.
for row in lr_list:
assert "lineNumber" in row
assert "status" in row
assert "paid" in row
assert "adjustmentsSum" in row
# One entry per service line.
assert len(lr_list) == len(body["serviceLines"])