feat(sp7): claim detail includes lineReconciliation slim projection
This commit is contained in:
@@ -1297,6 +1297,78 @@ class CycloneStore:
|
|||||||
# default ``None`` — the UI shows "no match" rather
|
# default ``None`` — the UI shows "no match" rather
|
||||||
# than crashing.
|
# 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
|
return detail
|
||||||
|
|
||||||
def all(self) -> list[BatchRecord]:
|
def all(self) -> list[BatchRecord]:
|
||||||
|
|||||||
@@ -212,3 +212,25 @@ def test_get_claim_detail_matched_remit_inlines_summary(seeded_store_with_pair):
|
|||||||
# ISO timestamp must end in Z — the spec contract that lets the
|
# ISO timestamp must end in Z — the spec contract that lets the
|
||||||
# UI render the time without a tz suffix shim.
|
# 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"])
|
||||||
Reference in New Issue
Block a user