Signal/python-backend/core/coverage_calculator.py
Kisa bcb1a10fe6 feat: demo MVP — 90/60/45 priority tiers, doc state machine, confirm visit workflow
- New CoverageFlag enum: SUPPLY_LAPSED, VISIT_REQUIRED, TRANSFER_PENDING,
  RENEWAL_CRITICAL/ELEVATED/SOON, RESUPPLY_READY, ACTIVE
- Doc state machine: 5-item payer-dependent status per patient (SWO, Visit,
  PECOS, PA, Diagnosis) with cascade chain
- Confirm Visit endpoint: staff enters prescriber-confirmed date, persisted in
  Supabase confirmed_visits table, survives all future CSV imports
- Supabase migration: 001_add_confirmed_visits.sql (run manually in SQL editor)
- Frontend: Badge rebuilt for 8 flags, DocStatusBar 5-dot display,
  ConfirmVisitModal, expandable WorklistTable rows
- Legal: LOI, NDA, BAA drafts at pitch/legal/ for Nixon Law Group review
- Compliance docs: privacy policy, incident response, data handling
- CSV generator: market_data.json + PA/NJ generator scripts
- 15/15 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:29:33 -04:00

350 lines
12 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
"""
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
@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
@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
# 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