""" 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