/** * Client-side CGM coverage calculator — mirrors python-backend/core/coverage_calculator.py. * Used as fallback when the Signal API is unreachable. * * PHI CONTRACT: This module receives only patient_id, device_type, shipment_date, * quantity, payer, component. No names, SSNs, DOBs, or contact fields. */ export const FLAG = { OUT_OF_COVERAGE: "OUT_OF_COVERAGE", VISIT_DUE: "VISIT_DUE", REFILL_WINDOW: "REFILL_WINDOW", OK: "OK", }; const FLAG_LABELS = { [FLAG.OUT_OF_COVERAGE]: "Supply Lapsed", [FLAG.VISIT_DUE]: "Renewal Due", [FLAG.REFILL_WINDOW]: "Resupply Ready", [FLAG.OK]: "Active", }; const FLAG_ACTIONS = { [FLAG.OUT_OF_COVERAGE]: "Contact Prescriber", [FLAG.VISIT_DUE]: "Request Renewal", [FLAG.REFILL_WINDOW]: "Initiate Resupply", [FLAG.OK]: "No action needed", }; /** Wear-day rules — mirrors payer_rules.json */ const DEVICE_RULES = { dexcom_g7: { display: "Dexcom G7", sensor: 10 }, dexcom_g6: { display: "Dexcom G6", sensor: 10, transmitter: 90 }, freestyle_libre_2: { display: "FreeStyle Libre 2", sensor: 14 }, freestyle_libre_3: { display: "FreeStyle Libre 3", sensor: 14 }, omnipod_5: { display: "Omnipod 5", pod: 3, sensor: 14 }, }; export function getDeviceDisplay(deviceType) { return DEVICE_RULES[deviceType]?.display ?? deviceType; } export function getFlagLabel(flag) { return FLAG_LABELS[flag] ?? flag; } export function getFlagAction(flag) { return FLAG_ACTIONS[flag] ?? "Review"; } function today() { const d = new Date(); d.setHours(0, 0, 0, 0); return d; } function daysDiff(a, b) { return Math.round((a.getTime() - b.getTime()) / 86400000); } function addDays(dateStr, days) { const d = new Date(dateStr + "T00:00:00"); d.setDate(d.getDate() + days); return d; } function getWearDays(deviceType, component) { const device = DEVICE_RULES[deviceType]; if (!device) return null; return device[component] ?? null; } function getPayerConfig(payer) { const p = payer.toLowerCase(); if (p.includes("medicare")) { return { visitRenewalDays: 180, refillWindowDays: 30 }; } return { visitRenewalDays: null, refillWindowDays: 30 }; } function computePriority(flag, daysUntilEnd) { if (flag === FLAG.OUT_OF_COVERAGE) return 1000 + Math.abs(daysUntilEnd); if (flag === FLAG.VISIT_DUE) return 500 + Math.max(0, 90 - daysUntilEnd); if (flag === FLAG.REFILL_WINDOW) return 200 + Math.max(0, 30 - daysUntilEnd); return 0; } /** * Calculate coverage for a single record. * @param {{ patient_id, device_type, shipment_date, quantity, payer, component }} record * @returns {{ flag, daysUntilEnd, daysUntilVisit, priority }} or null on error */ export function calculateCoverage(record) { const wearDays = getWearDays(record.device_type, record.component); if (wearDays === null) return null; const now = today(); const totalWear = wearDays * parseInt(record.quantity, 10); const coverageEnd = addDays(record.shipment_date, totalWear); const daysUntilEnd = daysDiff(coverageEnd, now); const payerCfg = getPayerConfig(record.payer); let daysUntilVisit = null; if (payerCfg.visitRenewalDays) { const visitDue = addDays(record.shipment_date, payerCfg.visitRenewalDays); daysUntilVisit = daysDiff(visitDue, now); } let flag; if (daysUntilEnd < 0) { flag = FLAG.OUT_OF_COVERAGE; } else if (daysUntilVisit !== null && daysUntilVisit <= 30) { flag = FLAG.VISIT_DUE; } else if (daysUntilEnd <= payerCfg.refillWindowDays) { flag = FLAG.REFILL_WINDOW; } else { flag = FLAG.OK; } return { ...record, flag, daysUntilEnd, daysUntilVisit, priority: computePriority(flag, daysUntilEnd), coverageEndDate: coverageEnd.toISOString().slice(0, 10), }; } /** * Parse raw CSV text into record objects. */ export function parseCSV(text) { const lines = text.trim().split(/\r?\n/); if (lines.length < 2) return []; const headers = lines[0].split(",").map((h) => h.trim().toLowerCase()); return lines .slice(1) .filter((l) => l.trim()) .map((line) => { const cols = line.split(",").map((c) => c.trim()); const row = {}; headers.forEach((h, i) => (row[h] = cols[i] || "")); return row; }); } /** * Process a batch of parsed CSV rows and return sorted results + skip list. */ export function processBatch(rows) { const results = []; const skipped = []; for (const row of rows) { const result = calculateCoverage(row); if (result) { results.push(result); } else { skipped.push(row.patient_id || "?"); } } results.sort((a, b) => b.priority - a.priority); return { results, skipped }; }