feat: patient rollup + nested patients payload (plan 02, P3)
core/rollup.py implements the locked truth table: worst gradeable line wins; a Plan Type Needed line caps the patient at Action Needed; non-gradeable lines are displayed but never touch the light; no-graded-lines patients carry None (no label invented). /api/upload gains additive patients[] (one entry per patient, nested device lines with doc checklists, citations, timing flags, dedup display fields) and patient_stats (counts patients, not rows). Flat records payload untouched until P6. Independent audit: SHIP; its LOW findings fixed (date coercion on timing keys, empty-rollup guard, severity divergence documented). 151 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d24340c47a
commit
bd92126384
4 changed files with 518 additions and 26 deletions
|
|
@ -185,6 +185,36 @@ class ReadinessItemOut(BaseModel):
|
||||||
rule_id: Optional[str] = None
|
rule_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LineOut(BaseModel):
|
||||||
|
"""One coverage line (patient x device x plan type) in the nested payload."""
|
||||||
|
|
||||||
|
device_type: str
|
||||||
|
device_display: str
|
||||||
|
plan_type: Optional[str] = None # None = plan type needed (never guessed)
|
||||||
|
gradeable: bool
|
||||||
|
line_status: Optional[str] = None # LineStatus verbatim; None = ungraded
|
||||||
|
timing_flag: Optional[str] = None # CoverageFlag of the current shipment
|
||||||
|
doc_items: Optional[list[ReadinessItemOut]] = None
|
||||||
|
# Display fields per the locked dedup design (section 6)
|
||||||
|
order_numbers: list[str] = []
|
||||||
|
hcpcs_codes: list[str] = []
|
||||||
|
total_quantity: int = 0
|
||||||
|
shipment_date: Optional[str] = None # current shipment DOS
|
||||||
|
payer: Optional[str] = None
|
||||||
|
prior_shipment_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PatientOut(BaseModel):
|
||||||
|
"""One patient in the nested payload; tabs count these, not rows (P6)."""
|
||||||
|
|
||||||
|
patient_id: str
|
||||||
|
rollup_status: Optional[str] = None # None = no graded gradeable lines
|
||||||
|
plan_type_needed: bool = False
|
||||||
|
cgm_line_count: int = 0
|
||||||
|
total_line_count: int = 0
|
||||||
|
lines: list[LineOut] = []
|
||||||
|
|
||||||
|
|
||||||
class RecordOut(BaseModel):
|
class RecordOut(BaseModel):
|
||||||
patient_id: str
|
patient_id: str
|
||||||
device_type: str
|
device_type: str
|
||||||
|
|
@ -234,6 +264,10 @@ class UploadResponse(BaseModel):
|
||||||
mapping_summary: dict
|
mapping_summary: dict
|
||||||
batch_id: str | None = None
|
batch_id: str | None = None
|
||||||
readiness_stats: dict | None = None
|
readiness_stats: dict | None = None
|
||||||
|
# Nested patient payload (P3, additive). The flat `records` list stays
|
||||||
|
# untouched until P6 switches the UI (locked migration strategy).
|
||||||
|
patients: list[PatientOut] | None = None
|
||||||
|
patient_stats: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
def _build_reason(
|
def _build_reason(
|
||||||
|
|
@ -333,6 +367,24 @@ class DocStatusRequest(BaseModel):
|
||||||
expiry_date: Optional[str] = None
|
expiry_date: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _readiness_items_out(readiness) -> Optional[list[ReadinessItemOut]]:
|
||||||
|
"""Serialize a ReadinessVerdict's doc items (shared by rows and lines)."""
|
||||||
|
if not readiness:
|
||||||
|
return None
|
||||||
|
return [
|
||||||
|
ReadinessItemOut(
|
||||||
|
doc_type=i.doc_type,
|
||||||
|
required_state=i.required_state.value,
|
||||||
|
input_state=i.input_state.value,
|
||||||
|
quality=i.quality.value,
|
||||||
|
value=i.value,
|
||||||
|
satisfied=i.satisfied,
|
||||||
|
rule_id=i.rule_id,
|
||||||
|
)
|
||||||
|
for i in readiness.doc_items
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _to_record_out(
|
def _to_record_out(
|
||||||
r,
|
r,
|
||||||
record=None,
|
record=None,
|
||||||
|
|
@ -422,20 +474,7 @@ def _to_record_out(
|
||||||
else None,
|
else None,
|
||||||
plan_type=getattr(record, "plan_type", None) if record else None,
|
plan_type=getattr(record, "plan_type", None) if record else None,
|
||||||
readiness_status=readiness.line_status.value if readiness else None,
|
readiness_status=readiness.line_status.value if readiness else None,
|
||||||
readiness_items=[
|
readiness_items=_readiness_items_out(readiness),
|
||||||
ReadinessItemOut(
|
|
||||||
doc_type=i.doc_type,
|
|
||||||
required_state=i.required_state.value,
|
|
||||||
input_state=i.input_state.value,
|
|
||||||
quality=i.quality.value,
|
|
||||||
value=i.value,
|
|
||||||
satisfied=i.satisfied,
|
|
||||||
rule_id=i.rule_id,
|
|
||||||
)
|
|
||||||
for i in readiness.doc_items
|
|
||||||
]
|
|
||||||
if readiness
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -461,6 +500,104 @@ def _compute_stats(records: list[RecordOut]) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_patients_payload(index, reachable_line_keys, results):
|
||||||
|
"""Assemble the nested patients payload (P3) from the lines reachable in
|
||||||
|
THIS response, so patients/lines always reconcile with the flat records.
|
||||||
|
Rollup rules are locked in core/rollup.py (worst gradeable line; PTN caps
|
||||||
|
at Action Needed; non-gradeable lines excluded from the light)."""
|
||||||
|
from core.dedup import UNKNOWN_PLAN_TYPE
|
||||||
|
from core.rollup import rollup_patients
|
||||||
|
|
||||||
|
reachable = {
|
||||||
|
k: index.lines[k] for k in reachable_line_keys if k in index.lines
|
||||||
|
}
|
||||||
|
if not reachable:
|
||||||
|
return [], {"patients_total": 0}
|
||||||
|
|
||||||
|
from core.dedup import _as_date
|
||||||
|
|
||||||
|
# Line-level timing flag = the worst-urgency row of the line's CURRENT
|
||||||
|
# shipment group (twin rows can differ; worst wins, fail-safe).
|
||||||
|
flag_by_group: dict = {}
|
||||||
|
for r in results:
|
||||||
|
gk = (r.patient_id, r.device_type, _as_date(r.last_shipment_date))
|
||||||
|
flag_val = r.flag.value if hasattr(r.flag, "value") else str(r.flag)
|
||||||
|
prev = flag_by_group.get(gk)
|
||||||
|
if prev is None or r.priority_score > prev[1]:
|
||||||
|
flag_by_group[gk] = (flag_val, r.priority_score)
|
||||||
|
|
||||||
|
# DISPLAY ordering, intentionally different from rollup.SEVERITY: the
|
||||||
|
# worklist queues At Risk above Plan Type Needed (urgency), while the
|
||||||
|
# rollup's capping rule treats PTN as the worst LINE state. Do not unify.
|
||||||
|
severity = {
|
||||||
|
"At Risk": 4,
|
||||||
|
"Plan Type Needed": 3,
|
||||||
|
"Action Needed": 2,
|
||||||
|
"On Track": 1,
|
||||||
|
"Clear to Ship": 0,
|
||||||
|
}
|
||||||
|
patients = []
|
||||||
|
for pid, ru in rollup_patients(reachable).items():
|
||||||
|
lines_out = []
|
||||||
|
for line in sorted(
|
||||||
|
ru.lines,
|
||||||
|
key=lambda l: (
|
||||||
|
-(severity.get(l.line_status, -1)),
|
||||||
|
l.device_type,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
line_key = (line.patient_id, line.device_type, line.plan_type)
|
||||||
|
verdict = index.by_line.get(line_key)
|
||||||
|
cur = line.current_shipment
|
||||||
|
timing = flag_by_group.get(
|
||||||
|
(line.patient_id, line.device_type, _as_date(cur.shipment_date))
|
||||||
|
)
|
||||||
|
lines_out.append(
|
||||||
|
LineOut(
|
||||||
|
device_type=line.device_type,
|
||||||
|
device_display=DEVICE_DISPLAY.get(
|
||||||
|
line.device_type, line.device_type
|
||||||
|
),
|
||||||
|
plan_type=None
|
||||||
|
if line.plan_type == UNKNOWN_PLAN_TYPE
|
||||||
|
else line.plan_type,
|
||||||
|
gradeable=line.gradeable,
|
||||||
|
line_status=line.line_status,
|
||||||
|
timing_flag=timing[0] if timing else None,
|
||||||
|
doc_items=_readiness_items_out(verdict),
|
||||||
|
order_numbers=cur.order_numbers,
|
||||||
|
hcpcs_codes=cur.hcpcs_codes,
|
||||||
|
total_quantity=cur.total_quantity,
|
||||||
|
shipment_date=cur.shipment_date.isoformat(),
|
||||||
|
payer=cur.payer or None,
|
||||||
|
prior_shipment_count=len(line.prior_shipments),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
patients.append(
|
||||||
|
PatientOut(
|
||||||
|
patient_id=pid,
|
||||||
|
rollup_status=ru.rollup_status.value
|
||||||
|
if ru.rollup_status
|
||||||
|
else None,
|
||||||
|
plan_type_needed=ru.plan_type_needed,
|
||||||
|
cgm_line_count=ru.cgm_line_count,
|
||||||
|
total_line_count=ru.total_line_count,
|
||||||
|
lines=lines_out,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Worklist order: worst patients first, stable by patient_id.
|
||||||
|
patients.sort(
|
||||||
|
key=lambda p: (-(severity.get(p.rollup_status, -1)), p.patient_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
stats: dict = {"patients_total": len(patients)}
|
||||||
|
for p in patients:
|
||||||
|
key = p.rollup_status if p.rollup_status else "ungraded"
|
||||||
|
stats[key] = stats.get(key, 0) + 1
|
||||||
|
return patients, stats
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {"status": "ok", "service": "signal-api", "version": "1.0.0"}
|
return {"status": "ok", "service": "signal-api", "version": "1.0.0"}
|
||||||
|
|
@ -551,10 +688,13 @@ async def upload_csv(
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
used_lines = {}
|
used_lines = {}
|
||||||
|
reachable_line_keys = set()
|
||||||
for r in results:
|
for r in results:
|
||||||
verdict, line_key, merged = resolve_group(
|
verdict, line_key, merged = resolve_group(
|
||||||
index, r.patient_id, r.device_type, r.last_shipment_date
|
index, r.patient_id, r.device_type, r.last_shipment_date
|
||||||
)
|
)
|
||||||
|
if line_key is not None:
|
||||||
|
reachable_line_keys.add(line_key)
|
||||||
if verdict is not None and line_key is not None:
|
if verdict is not None and line_key is not None:
|
||||||
used_lines[line_key] = verdict
|
used_lines[line_key] = verdict
|
||||||
# Exact row match first (byte-identical legacy path). CoverageResult
|
# Exact row match first (byte-identical legacy path). CoverageResult
|
||||||
|
|
@ -610,6 +750,10 @@ async def upload_csv(
|
||||||
clerk_org_id=clerk_org_id,
|
clerk_org_id=clerk_org_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
patients_out, patient_stats = _build_patients_payload(
|
||||||
|
index, reachable_line_keys, results
|
||||||
|
)
|
||||||
|
|
||||||
return UploadResponse(
|
return UploadResponse(
|
||||||
records=out,
|
records=out,
|
||||||
total=len(out),
|
total=len(out),
|
||||||
|
|
@ -622,6 +766,8 @@ async def upload_csv(
|
||||||
# reconcile with the records beside them (a record calculate_batch
|
# reconcile with the records beside them (a record calculate_batch
|
||||||
# skipped cannot leave a phantom line in the counts).
|
# skipped cannot leave a phantom line in the counts).
|
||||||
readiness_stats=readiness_stats(used_lines),
|
readiness_stats=readiness_stats(used_lines),
|
||||||
|
patients=patients_out,
|
||||||
|
patient_stats=patient_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
101
python-backend/core/rollup.py
Normal file
101
python-backend/core/rollup.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""
|
||||||
|
rollup.py
|
||||||
|
Signal — STTIL Solutions
|
||||||
|
|
||||||
|
Patient-level rollup of coverage-line readiness verdicts (build plan 02, P3).
|
||||||
|
Design locked in readiness-model-brief section 4 and the phase-2 design Item 3:
|
||||||
|
|
||||||
|
- The light = worst gradeable line. Severity (worst first):
|
||||||
|
Plan Type Needed > At Risk > Action Needed > On Track > Clear to Ship.
|
||||||
|
- Any gradeable Plan Type Needed line caps the patient no greener than
|
||||||
|
Action Needed (the patient IS being served; one unmapped plan type must not
|
||||||
|
read as the whole patient being ungradeable).
|
||||||
|
- Non-gradeable lines (non-CGM) never participate in the light or the counts.
|
||||||
|
- Tabs count patients, not rows.
|
||||||
|
|
||||||
|
A patient with no graded gradeable lines has rollup_status None — the UI
|
||||||
|
treatment is an open Kisa question; no label is invented here.
|
||||||
|
|
||||||
|
PHI CONTRACT: operates on patient_id only; this module logs nothing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from core.dedup import CoverageLine
|
||||||
|
from core.readiness import LineStatus
|
||||||
|
|
||||||
|
# Higher = worse. Plan Type Needed ranks worst for LINE severity ordering;
|
||||||
|
# its patient-level effect is the capping rule below, not raw worstness.
|
||||||
|
SEVERITY = {
|
||||||
|
LineStatus.CLEAR_TO_SHIP: 0,
|
||||||
|
LineStatus.ON_TRACK: 1,
|
||||||
|
LineStatus.ACTION_NEEDED: 2,
|
||||||
|
LineStatus.AT_RISK: 3,
|
||||||
|
LineStatus.PLAN_TYPE_NEEDED: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PatientRollup:
|
||||||
|
patient_id: str
|
||||||
|
lines: list[CoverageLine] # all lines, incl. non-gradeable
|
||||||
|
rollup_status: Optional[LineStatus] # None = no graded gradeable lines
|
||||||
|
cgm_line_count: int # gradeable lines
|
||||||
|
total_line_count: int # all lines
|
||||||
|
plan_type_needed: bool # any gradeable line in PTN
|
||||||
|
|
||||||
|
|
||||||
|
def rollup_patient(lines: list[CoverageLine]) -> PatientRollup:
|
||||||
|
"""Roll one patient's coverage lines into a single verdict, per the
|
||||||
|
locked truth table (plan 02 P3):
|
||||||
|
|
||||||
|
| gradeable statuses present | rollup_status |
|
||||||
|
| none | None |
|
||||||
|
| all Plan Type Needed | Plan Type Needed |
|
||||||
|
| some PTN + others | worse of (Action Needed, worst other)|
|
||||||
|
| no PTN | worst by severity |
|
||||||
|
"""
|
||||||
|
if not lines:
|
||||||
|
raise ValueError("rollup_patient requires at least one CoverageLine")
|
||||||
|
graded = [
|
||||||
|
l for l in lines if l.gradeable and l.line_status is not None
|
||||||
|
]
|
||||||
|
statuses = [LineStatus(l.line_status) for l in graded]
|
||||||
|
ptn = [s for s in statuses if s == LineStatus.PLAN_TYPE_NEEDED]
|
||||||
|
|
||||||
|
if not statuses:
|
||||||
|
status: Optional[LineStatus] = None
|
||||||
|
elif len(ptn) == len(statuses):
|
||||||
|
status = LineStatus.PLAN_TYPE_NEEDED
|
||||||
|
elif ptn:
|
||||||
|
worst_other = max(
|
||||||
|
(s for s in statuses if s != LineStatus.PLAN_TYPE_NEEDED),
|
||||||
|
key=SEVERITY.get,
|
||||||
|
)
|
||||||
|
status = max(worst_other, LineStatus.ACTION_NEEDED, key=SEVERITY.get)
|
||||||
|
else:
|
||||||
|
status = max(statuses, key=SEVERITY.get)
|
||||||
|
|
||||||
|
return PatientRollup(
|
||||||
|
patient_id=lines[0].patient_id,
|
||||||
|
lines=lines,
|
||||||
|
rollup_status=status,
|
||||||
|
cgm_line_count=sum(1 for l in lines if l.gradeable),
|
||||||
|
total_line_count=len(lines),
|
||||||
|
plan_type_needed=bool(ptn),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollup_patients(lines: dict) -> dict[str, PatientRollup]:
|
||||||
|
"""Group CoverageLines (any keying) by patient and roll each up."""
|
||||||
|
by_patient: dict[str, list[CoverageLine]] = defaultdict(list)
|
||||||
|
for line in lines.values():
|
||||||
|
by_patient[line.patient_id].append(line)
|
||||||
|
return {
|
||||||
|
patient_id: rollup_patient(patient_lines)
|
||||||
|
for patient_id, patient_lines in by_patient.items()
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ from typing import Optional
|
||||||
from core.coverage_calculator import ShipmentRecord
|
from core.coverage_calculator import ShipmentRecord
|
||||||
from core.dedup import (
|
from core.dedup import (
|
||||||
UNKNOWN_PLAN_TYPE,
|
UNKNOWN_PLAN_TYPE,
|
||||||
|
CoverageLine,
|
||||||
MergedShipment,
|
MergedShipment,
|
||||||
_as_date,
|
_as_date,
|
||||||
assign_to_coverage_lines,
|
assign_to_coverage_lines,
|
||||||
|
|
@ -54,11 +55,13 @@ class ReadinessIndex:
|
||||||
"""Dedup-derived resolution index for one upload batch.
|
"""Dedup-derived resolution index for one upload batch.
|
||||||
|
|
||||||
by_line: verdicts per gradeable coverage line
|
by_line: verdicts per gradeable coverage line
|
||||||
|
lines: every CoverageLine (graded ones carry line_status)
|
||||||
line_by_group: dedup group -> the line that group's shipment belongs to
|
line_by_group: dedup group -> the line that group's shipment belongs to
|
||||||
merged_by_group: dedup group -> its MergedShipment (display-field truth)
|
merged_by_group: dedup group -> its MergedShipment (display-field truth)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
by_line: dict[LineKey, ReadinessVerdict] = field(default_factory=dict)
|
by_line: dict[LineKey, ReadinessVerdict] = field(default_factory=dict)
|
||||||
|
lines: dict[LineKey, "CoverageLine"] = field(default_factory=dict)
|
||||||
line_by_group: dict[GroupKey, LineKey] = field(default_factory=dict)
|
line_by_group: dict[GroupKey, LineKey] = field(default_factory=dict)
|
||||||
merged_by_group: dict[GroupKey, MergedShipment] = field(default_factory=dict)
|
merged_by_group: dict[GroupKey, MergedShipment] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
@ -93,22 +96,26 @@ def build_readiness_index(
|
||||||
|
|
||||||
index = ReadinessIndex()
|
index = ReadinessIndex()
|
||||||
for key, line in lines.items():
|
for key, line in lines.items():
|
||||||
|
line_key = (key.patient_id, key.device_type, key.plan_type_id)
|
||||||
|
index.lines[line_key] = line
|
||||||
if not line.gradeable:
|
if not line.gradeable:
|
||||||
continue
|
continue
|
||||||
cur = line.current_shipment
|
cur = line.current_shipment
|
||||||
index.by_line[(key.patient_id, key.device_type, key.plan_type_id)] = (
|
verdict = evaluate_readiness(
|
||||||
evaluate_readiness(
|
line.plan_type if line.plan_type != UNKNOWN_PLAN_TYPE else None,
|
||||||
line.plan_type if line.plan_type != UNKNOWN_PLAN_TYPE else None,
|
csv_swo_status=cur.csv_swo_status,
|
||||||
csv_swo_status=cur.csv_swo_status,
|
csv_visit_date=cur.csv_visit_date,
|
||||||
csv_visit_date=cur.csv_visit_date,
|
confirmed_visit_date=_confirmed_for(
|
||||||
confirmed_visit_date=_confirmed_for(
|
key.patient_id, confirmed_visits
|
||||||
key.patient_id, confirmed_visits
|
),
|
||||||
),
|
csv_pecos_verified=cur.csv_pecos_verified,
|
||||||
csv_pecos_verified=cur.csv_pecos_verified,
|
csv_pa_status=cur.csv_pa_status,
|
||||||
csv_pa_status=cur.csv_pa_status,
|
csv_diagnosis_on_file=cur.csv_diagnosis_on_file,
|
||||||
csv_diagnosis_on_file=cur.csv_diagnosis_on_file,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
index.by_line[line_key] = verdict
|
||||||
|
# The grading step owns line_status (dedup leaves it None) — the
|
||||||
|
# rollup (P3) reads it from the line.
|
||||||
|
line.line_status = verdict.line_status.value
|
||||||
|
|
||||||
for m in merged:
|
for m in merged:
|
||||||
gkey: GroupKey = (m.patient_id, m.device_type, m.shipment_date)
|
gkey: GroupKey = (m.patient_id, m.device_type, m.shipment_date)
|
||||||
|
|
|
||||||
238
tests/test_rollup.py
Normal file
238
tests/test_rollup.py
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
"""
|
||||||
|
Tests for the patient rollup (build plan 02, P3).
|
||||||
|
|
||||||
|
Rollup rules locked in readiness-model-brief section 4 / phase-2 design Item 3:
|
||||||
|
worst gradeable line; any gradeable Plan Type Needed line caps the patient no
|
||||||
|
greener than Action Needed; non-gradeable lines never touch the light; a
|
||||||
|
patient with no graded gradeable lines has rollup_status None (no label
|
||||||
|
invented).
|
||||||
|
|
||||||
|
PHI CONTRACT: synthetic patient_ids only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "python-backend"))
|
||||||
|
|
||||||
|
from core.dedup import CoverageLine, MergedShipment # noqa: E402
|
||||||
|
from core.readiness import LineStatus # noqa: E402
|
||||||
|
from core.rollup import rollup_patient, rollup_patients # noqa: E402
|
||||||
|
|
||||||
|
TODAY = date.today()
|
||||||
|
|
||||||
|
|
||||||
|
def make_line(patient_id="PT-1", device="dexcom_g7", plan="medicare",
|
||||||
|
status=None, gradeable=True):
|
||||||
|
shipment = MergedShipment(
|
||||||
|
patient_id=patient_id,
|
||||||
|
device_type=device,
|
||||||
|
shipment_date=TODAY - timedelta(days=5),
|
||||||
|
total_quantity=3,
|
||||||
|
components=["sensor"],
|
||||||
|
order_numbers=[],
|
||||||
|
hcpcs_codes=[],
|
||||||
|
payer="Medicare Part B",
|
||||||
|
plan_type=None if plan == "unknown" else plan,
|
||||||
|
)
|
||||||
|
return CoverageLine(
|
||||||
|
patient_id=patient_id,
|
||||||
|
device_type=device,
|
||||||
|
plan_type=plan,
|
||||||
|
gradeable=gradeable,
|
||||||
|
current_shipment=shipment,
|
||||||
|
prior_shipments=[],
|
||||||
|
line_status=status,
|
||||||
|
timing_flag=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_single_line():
|
||||||
|
ru = rollup_patient([make_line(status="On Track")])
|
||||||
|
assert ru.rollup_status == LineStatus.ON_TRACK
|
||||||
|
assert ru.cgm_line_count == 1
|
||||||
|
assert ru.total_line_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_mixed_statuses():
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="Clear to Ship"),
|
||||||
|
make_line(device="freestyle_libre_2", status="At Risk"),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.AT_RISK
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_plan_type_needed_capping():
|
||||||
|
# PTN + Clear to Ship = Action Needed (capped, not PTN)
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="Clear to Ship"),
|
||||||
|
make_line(device="freestyle_libre_2", plan="unknown",
|
||||||
|
status="Plan Type Needed"),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.ACTION_NEEDED
|
||||||
|
assert ru.plan_type_needed is True
|
||||||
|
# PTN + At Risk = At Risk (the worse of the two)
|
||||||
|
ru2 = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="At Risk"),
|
||||||
|
make_line(device="freestyle_libre_2", plan="unknown",
|
||||||
|
status="Plan Type Needed"),
|
||||||
|
])
|
||||||
|
assert ru2.rollup_status == LineStatus.AT_RISK
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_all_lines_plan_type_needed():
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", plan="unknown", status="Plan Type Needed"),
|
||||||
|
make_line(device="freestyle_libre_2", plan="unknown",
|
||||||
|
status="Plan Type Needed"),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.PLAN_TYPE_NEEDED
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_non_cgm_excluded():
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="Clear to Ship"),
|
||||||
|
make_line(device="omnipod_5", gradeable=False, status=None),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.CLEAR_TO_SHIP # pod changes nothing
|
||||||
|
assert ru.cgm_line_count == 1
|
||||||
|
assert ru.total_line_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_no_gradeable_lines():
|
||||||
|
ru = rollup_patient([make_line(device="omnipod_5", gradeable=False)])
|
||||||
|
assert ru.rollup_status is None
|
||||||
|
assert ru.total_line_count == 1
|
||||||
|
assert ru.cgm_line_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollup_patients_groups_by_patient():
|
||||||
|
lines = {
|
||||||
|
("A", "dexcom_g7", "medicare"): make_line("A", status="Clear to Ship"),
|
||||||
|
("A", "freestyle_libre_2", "medicare"): make_line(
|
||||||
|
"A", device="freestyle_libre_2", status="Action Needed"),
|
||||||
|
("B", "dexcom_g7", "unknown"): make_line(
|
||||||
|
"B", plan="unknown", status="Plan Type Needed"),
|
||||||
|
}
|
||||||
|
rollups = rollup_patients(lines)
|
||||||
|
assert set(rollups) == {"A", "B"}
|
||||||
|
assert rollups["A"].rollup_status == LineStatus.ACTION_NEEDED
|
||||||
|
assert rollups["B"].rollup_status == LineStatus.PLAN_TYPE_NEEDED
|
||||||
|
|
||||||
|
|
||||||
|
# --- API level ---------------------------------------------------------------
|
||||||
|
|
||||||
|
httpx = pytest.importorskip("httpx", reason="httpx required for TestClient")
|
||||||
|
from fastapi.testclient import TestClient # noqa: E402
|
||||||
|
|
||||||
|
import api.main as api_main # noqa: E402
|
||||||
|
|
||||||
|
client = TestClient(api_main.app)
|
||||||
|
|
||||||
|
SHIP = (TODAY - timedelta(days=5)).isoformat()
|
||||||
|
VISIT = (TODAY - timedelta(days=30)).isoformat()
|
||||||
|
HEADER = (
|
||||||
|
"patient_id,device_type,shipment_date,quantity,payer,plan_type,"
|
||||||
|
"swo_status,visit_date,pecos_verified,pa_status,diagnosis_on_file"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _upload(csv_text: str):
|
||||||
|
return client.post(
|
||||||
|
"/api/upload",
|
||||||
|
files={"file": ("fixture.csv", io.BytesIO(csv_text.encode()), "text/csv")},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_response_contains_patients_nested():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-N-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
+ f"\nPT-N-1,Libre 2,{SHIP},1,Medicare Part B,medicare,"
|
||||||
|
+ "On File,,Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
patients = body["patients"]
|
||||||
|
assert len(patients) == 1
|
||||||
|
p = patients[0]
|
||||||
|
assert p["patient_id"] == "PT-N-1"
|
||||||
|
assert p["rollup_status"] == "Action Needed" # worst line (Libre 2 no visit)
|
||||||
|
assert len(p["lines"]) == 2
|
||||||
|
by_dev = {l["device_type"]: l for l in p["lines"]}
|
||||||
|
assert by_dev["dexcom_g7"]["line_status"] == "Clear to Ship"
|
||||||
|
assert by_dev["freestyle_libre_2"]["line_status"] == "Action Needed"
|
||||||
|
items = {i["doc_type"]: i for i in by_dev["freestyle_libre_2"]["doc_items"]}
|
||||||
|
assert items["visit"]["satisfied"] is False
|
||||||
|
assert items["visit"]["rule_id"] == "R101"
|
||||||
|
# Worst line renders first
|
||||||
|
assert p["lines"][0]["device_type"] == "freestyle_libre_2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_stats_count_patients_not_rows():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-S-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
+ f"\nPT-S-1,Libre 2,{SHIP},1,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
+ f"\nPT-S-2,Dexcom G6,{SHIP},3,Medicaid - GA,,"
|
||||||
|
+ f"On File,{VISIT},,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
stats = resp.json()["patient_stats"]
|
||||||
|
assert stats["patients_total"] == 2 # 2-device patient counts once
|
||||||
|
assert stats["Clear to Ship"] == 1
|
||||||
|
assert stats["Plan Type Needed"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_records_untouched_by_nested_payload():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-F-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
body = resp.json()
|
||||||
|
assert body["total"] == 1
|
||||||
|
assert len(body["records"]) == 1 # flat path byte-identical (P6 switches UI)
|
||||||
|
assert body["records"][0]["readiness_status"] == "Clear to Ship"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_gradeable_only_patient_has_null_rollup():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-P-1,Omnipod 5,{SHIP},1,Highmark BCBS,commercial,"
|
||||||
|
+ f"On File,{VISIT},Yes,Approved,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
body = resp.json()
|
||||||
|
p = body["patients"][0]
|
||||||
|
assert p["rollup_status"] is None
|
||||||
|
assert p["cgm_line_count"] == 0
|
||||||
|
assert p["lines"][0]["gradeable"] is False
|
||||||
|
assert p["lines"][0]["line_status"] is None
|
||||||
|
assert body["patient_stats"]["ungraded"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollup_patient_empty_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
rollup_patient([])
|
||||||
|
|
||||||
|
|
||||||
|
def test_line_timing_flag_populated():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-T-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
line = resp.json()["patients"][0]["lines"][0]
|
||||||
|
assert line["timing_flag"] == "RESUPPLY_READY"
|
||||||
Loading…
Reference in a new issue