Signal/python-backend/core/coverage_calculator.py
Kisa cc2a5f4b47 coverage calculator and UI refinements; add demo MVP gap analysis and quality review docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 13:04:23 -04:00

392 lines
15 KiB
Python

"""
coverage_calculator.py
Signal CGM — STTIL Solutions
Calculates CGM coverage status per patient based on shipment history
and payer-specific wear-day rules.
PHI CONTRACT:
This module receives only: patient_id, device_type, shipment_date,
quantity, payer, component, and optional doc status fields.
No names, SSNs, DOBs, or contact fields may be added to any function
signature or data structure in this file.
Flag types emitted:
SUPPLY_LAPSED — supply cycle ended, no new shipment
VISIT_REQUIRED — visit date past due, no confirmed new visit
TRANSFER_PENDING — transferred patient, all docs need verification
RENEWAL_CRITICAL — <= 45 days to next visit due
RENEWAL_ELEVATED — <= 60 days to next visit due
RENEWAL_SOON — <= 90 days to next visit due
RESUPPLY_READY — within refill window, visit not yet urgent
ACTIVE — all clear
NO_RECENT_SHIPMENT — last shipment 12+ months ago, no confirmed active care;
verify patient status with prescriber office
"""
import json
import logging
from dataclasses import dataclass, field
from datetime import date, timedelta
from enum import Enum
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
RULE_VERSION = "v0.2"
PAYER_RULES_PATH = Path(__file__).parent.parent / "config" / "payer_rules.json"
class CoverageFlag(str, Enum):
SUPPLY_LAPSED = "SUPPLY_LAPSED" # supply cycle ended, no new shipment
VISIT_REQUIRED = "VISIT_REQUIRED" # visit date past due, no confirmed new visit
TRANSFER_PENDING = "TRANSFER_PENDING" # transferred patient, all docs need verification
RENEWAL_CRITICAL = "RENEWAL_CRITICAL" # <= 45 days to next visit due
RENEWAL_ELEVATED = "RENEWAL_ELEVATED" # <= 60 days to next visit due
RENEWAL_SOON = "RENEWAL_SOON" # <= 90 days to next visit due
RESUPPLY_READY = "RESUPPLY_READY" # within refill window, visit not yet urgent
ACTIVE = "ACTIVE" # all clear
NO_RECENT_SHIPMENT = "NO_RECENT_SHIPMENT" # last shipment 12+ months ago, no confirmed active care
@dataclass(frozen=True)
class ShipmentRecord:
"""
Minimal shipment record. Only non-PHI fields allowed.
patient_id: Supplier's internal MRN or account number.
This is the sole crosswalk key — no real identity data here.
csv_* fields: Optional doc status columns parsed from the CSV.
csv_transfer_from: Non-empty string means this is a transferred patient.
"""
patient_id: str
device_type: str
shipment_date: date
quantity: int
payer: str
component: str = "sensor"
# Optional doc fields — populated when CSV contains these columns
csv_visit_date: Optional[date] = None
csv_swo_status: Optional[str] = None # "On File" | "Pending" | "Expired"
csv_pecos_verified: Optional[str] = None # "Yes" | "No"
csv_pa_status: Optional[str] = None # "Approved" | "Pending" | "Denied" | "Not Required"
csv_diagnosis_on_file: Optional[str] = None # "Yes" | "No"
csv_transfer_from: Optional[str] = None # prior supplier name if transfer
order_number: Optional[str] = None # pass-through — billing system order/claim number
hcpcs: Optional[str] = None # pass-through — HCPCS code from supplier CSV
@dataclass
class CoverageResult:
patient_id: str
device_type: str
payer: str
component: str
last_shipment_date: date
coverage_end_date: date
next_visit_due_date: Optional[date]
flag: CoverageFlag
days_until_coverage_end: int
days_until_visit_due: Optional[int]
priority_score: int
visit_date_confidence: str = "estimated" # "confirmed" | "estimated"
is_transfer: bool = False
rule_version: str = RULE_VERSION
def _load_payer_rules() -> dict:
with open(PAYER_RULES_PATH, "r") as f:
return json.load(f)
def _get_wear_days(rules: dict, device_type: str, component: str) -> int:
"""
Return wear days for a given device type and component.
Raises ValueError for unknown device types.
"""
devices = rules.get("devices", {})
device = devices.get(device_type)
if device is None:
raise ValueError(f"Unknown device_type: '{device_type}'. "
f"Valid types: {list(devices.keys())}")
component_key = f"{component}_wear_days"
wear_days = device.get(component_key)
if wear_days is None:
raise ValueError(
f"Device '{device_type}' has no wear-day rule for component "
f"'{component}'. Check payer_rules.json."
)
return wear_days
_COMMERCIAL_KEYWORDS = {
"aetna", "united", "unitedhealthcare", "cigna", "humana",
"bcbs", "blue cross", "blue shield", "anthem", "molina",
"centene", "wellcare", "oscar", "ambetter", "carefirst",
"highmark", "geisinger", "kaiser", "commercial",
}
def _normalize_payer(payer: str) -> str:
"""
Map raw DME CSV payer strings to canonical payer_rules.json keys.
"Medicare Part B""medicare"
"Medicare Advantage""medicare_advantage"
"Medicaid - GA""medicaid"
"Aetna PPO""commercial"
Preserves original value in CoverageResult.payer for display.
"""
s = payer.strip().lower()
if "medicare advantage" in s or "medicare_advantage" in s:
return "medicare_advantage"
if "medicare" in s:
return "medicare"
if "tricare for life" in s or s == "tfl":
return "medicare"
if "medicaid" in s:
return "medicaid"
if "tricare" in s:
return "commercial"
for keyword in _COMMERCIAL_KEYWORDS:
if keyword in s:
return "commercial"
return s
def _get_payer_config(rules: dict, payer: str) -> dict:
payer_rules = rules.get("payer_rules", {})
normalized = _normalize_payer(payer)
return payer_rules.get(normalized, payer_rules.get("default", {}))
def _resolve_visit_date(
record: ShipmentRecord,
confirmed_visit_date: Optional[date] = None,
) -> tuple[Optional[date], str]:
"""
Visit date priority chain:
1. CSV visit date column (if present + valid)
2. Supabase confirmed visit date (stored by staff)
3. Estimated proxy: shipment_date - 30 days
Returns (resolved_date, confidence) where confidence is 'confirmed' or 'estimated'.
"""
if record.csv_visit_date:
return record.csv_visit_date, "confirmed"
if confirmed_visit_date:
return confirmed_visit_date, "confirmed"
estimated = record.shipment_date - timedelta(days=30)
return estimated, "estimated"
def _compute_visit_flag(
next_visit_due: date,
today: date,
) -> CoverageFlag:
"""Map days-to-next-visit to the appropriate tier flag."""
days = (next_visit_due - today).days
if days < 0:
return CoverageFlag.VISIT_REQUIRED
if days <= 45:
return CoverageFlag.RENEWAL_CRITICAL
if days <= 60:
return CoverageFlag.RENEWAL_ELEVATED
if days <= 90:
return CoverageFlag.RENEWAL_SOON
return CoverageFlag.ACTIVE
def _compute_priority(flag: CoverageFlag, days_until_visit: Optional[int]) -> int:
"""
Priority score for worklist sort. Higher = more urgent.
Supply Lapsed and Transfer Pending are always highest.
Within visit tiers, urgency increases as days decrease.
"""
urgency_days = abs(days_until_visit) if days_until_visit is not None else 0
if flag == CoverageFlag.SUPPLY_LAPSED:
return 2000 + urgency_days
if flag == CoverageFlag.TRANSFER_PENDING:
return 1800
if flag == CoverageFlag.VISIT_REQUIRED:
return 1500 + urgency_days
if flag == CoverageFlag.RENEWAL_CRITICAL:
return 1000 + (45 - max(0, urgency_days))
if flag == CoverageFlag.RENEWAL_ELEVATED:
return 700 + (60 - max(0, urgency_days))
if flag == CoverageFlag.RENEWAL_SOON:
return 400 + (90 - max(0, urgency_days))
if flag == CoverageFlag.RESUPPLY_READY:
return 200
return 0 # ACTIVE
def calculate_coverage(
record: ShipmentRecord,
as_of: Optional[date] = None,
confirmed_visit_date: Optional[date] = None,
) -> CoverageResult:
"""
Calculate coverage status for a single shipment record.
Args:
record: ShipmentRecord with non-PHI fields only.
as_of: Date to evaluate against. Defaults to today.
confirmed_visit_date: Confirmed visit date from Supabase (staff-entered).
"""
rules = _load_payer_rules()
today = as_of or date.today()
# Transferred patients: all docs need verification before anything else
if record.csv_transfer_from:
return CoverageResult(
patient_id=record.patient_id,
device_type=record.device_type,
payer=record.payer,
component=record.component,
last_shipment_date=record.shipment_date,
coverage_end_date=record.shipment_date,
next_visit_due_date=None,
flag=CoverageFlag.TRANSFER_PENDING,
days_until_coverage_end=0,
days_until_visit_due=None,
priority_score=1800,
visit_date_confidence="estimated",
is_transfer=True,
rule_version=RULE_VERSION,
)
wear_days = _get_wear_days(rules, record.device_type, record.component)
payer_config = _get_payer_config(rules, record.payer)
total_wear_days = wear_days * record.quantity
coverage_end = record.shipment_date + timedelta(days=total_wear_days)
days_until_end = (coverage_end - today).days
# Catch-all FIRST: patient shows no active-care signals and last shipment was
# 12+ months ago. Must precede the supply-lapsed check — a 12-month-stale record
# always reads as lapsed, so checking lapsed first made this flag unreachable.
# A confirmed or CSV visit date within the last 12 months overrides this check,
# treating the patient as still active even if the shipment record is old.
NO_RECENT_SHIPMENT_DAYS = 365
days_since_shipment = (today - record.shipment_date).days
confirmed_visit_within_12m = (
confirmed_visit_date is not None
and (today - confirmed_visit_date).days < NO_RECENT_SHIPMENT_DAYS
)
csv_visit_within_12m = (
record.csv_visit_date is not None
and (today - record.csv_visit_date).days < NO_RECENT_SHIPMENT_DAYS
)
if (
days_since_shipment >= NO_RECENT_SHIPMENT_DAYS
and not confirmed_visit_within_12m
and not csv_visit_within_12m
):
_, confidence = _resolve_visit_date(record, confirmed_visit_date)
return CoverageResult(
patient_id=record.patient_id,
device_type=record.device_type,
payer=record.payer,
component=record.component,
last_shipment_date=record.shipment_date,
coverage_end_date=coverage_end,
next_visit_due_date=None,
flag=CoverageFlag.NO_RECENT_SHIPMENT,
days_until_coverage_end=days_until_end,
days_until_visit_due=None,
priority_score=5, # lowest — drops to bottom of worklist sort
visit_date_confidence=confidence,
rule_version=RULE_VERSION,
)
# Supply lapsed — coverage cycle ended
if days_until_end < 0:
visit_date, confidence = _resolve_visit_date(record, confirmed_visit_date)
next_visit_due = visit_date + timedelta(days=180) if visit_date else None
days_visit = (next_visit_due - today).days if next_visit_due else None
return CoverageResult(
patient_id=record.patient_id,
device_type=record.device_type,
payer=record.payer,
component=record.component,
last_shipment_date=record.shipment_date,
coverage_end_date=coverage_end,
next_visit_due_date=next_visit_due,
flag=CoverageFlag.SUPPLY_LAPSED,
days_until_coverage_end=days_until_end,
days_until_visit_due=days_visit,
priority_score=_compute_priority(CoverageFlag.SUPPLY_LAPSED, days_visit),
visit_date_confidence=confidence,
rule_version=RULE_VERSION,
)
visit_renewal_days = payer_config.get("visit_renewal_days")
refill_window_days = payer_config.get("refill_window_days", 30)
# Resolve visit date using priority chain
visit_date, confidence = _resolve_visit_date(record, confirmed_visit_date)
next_visit_due: Optional[date] = None
days_until_visit: Optional[int] = None
flag = CoverageFlag.ACTIVE
if visit_renewal_days and visit_date:
next_visit_due = visit_date + timedelta(days=visit_renewal_days)
days_until_visit = (next_visit_due - today).days
flag = _compute_visit_flag(next_visit_due, today)
# If no visit urgency but within refill window, mark resupply ready
if flag == CoverageFlag.ACTIVE and days_until_end <= refill_window_days:
flag = CoverageFlag.RESUPPLY_READY
priority = _compute_priority(flag, days_until_visit)
return CoverageResult(
patient_id=record.patient_id,
device_type=record.device_type,
payer=record.payer,
component=record.component,
last_shipment_date=record.shipment_date,
coverage_end_date=coverage_end,
next_visit_due_date=next_visit_due,
flag=flag,
days_until_coverage_end=days_until_end,
days_until_visit_due=days_until_visit,
priority_score=priority,
visit_date_confidence=confidence,
rule_version=RULE_VERSION,
)
def calculate_batch(
records: list[ShipmentRecord],
as_of: Optional[date] = None,
confirmed_visits: Optional[dict[str, date]] = None,
) -> list[CoverageResult]:
"""
Calculate coverage for a list of shipment records and return a
worklist sorted by priority (highest first).
confirmed_visits: dict mapping patient_id_hash -> confirmed visit date.
Loaded from Supabase by persistence layer before calling this function.
Skips records that raise ValueError (unknown device/component) and
logs a warning so the batch continues.
"""
import hashlib
confirmed_visits = confirmed_visits or {}
results = []
for record in records:
patient_hash = hashlib.sha256(record.patient_id.encode()).hexdigest()
confirmed_date = confirmed_visits.get(patient_hash)
try:
result = calculate_coverage(record, as_of=as_of, confirmed_visit_date=confirmed_date)
results.append(result)
except ValueError as exc:
logger.warning("Skipping record for patient_id hash: %s", exc)
results.sort(key=lambda r: r.priority_score, reverse=True)
return results