Create billing.py
Browse files- src/billing.py +101 -0
src/billing.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/billing.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
"""Billing helpers: CPT autosuggest and 837 claim builder (demo-only)."""
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import List, Dict, Any, Optional
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from .config import CPT, PROVIDER
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class CPTSuggestion:
|
| 15 |
+
code: str
|
| 16 |
+
rate: float
|
| 17 |
+
descriptor: str
|
| 18 |
+
eligible: bool
|
| 19 |
+
why: str
|
| 20 |
+
|
| 21 |
+
def _why_for_minutes(code: str, minutes: int, spoke: bool) -> str:
|
| 22 |
+
if code == "99451":
|
| 23 |
+
if not spoke:
|
| 24 |
+
return "Eligible when no live interprofessional call; written report only; requires ≥5 minutes."
|
| 25 |
+
return "Ineligible: 99451 is for written report without live discussion; 'spoke' was checked."
|
| 26 |
+
ranges = {
|
| 27 |
+
"99446": (5, 10),
|
| 28 |
+
"99447": (11, 20),
|
| 29 |
+
"99448": (21, 30),
|
| 30 |
+
"99449": (31, 10**9),
|
| 31 |
+
}
|
| 32 |
+
lo, hi = ranges[code]
|
| 33 |
+
base = f"Eligible minutes window {lo}–{hi if hi<10**9 else '∞'}; requires live discussion with referring clinician."
|
| 34 |
+
if not spoke:
|
| 35 |
+
return f"Ineligible: requires live discussion (spoke=False). Suggested 99451 if ≥5 minutes."
|
| 36 |
+
if minutes < lo:
|
| 37 |
+
return f"Ineligible for {code}: minutes below required threshold ({minutes}<{lo})."
|
| 38 |
+
return base
|
| 39 |
+
|
| 40 |
+
def autosuggest_cpt(minutes: int, spoke: bool) -> List[CPTSuggestion]:
|
| 41 |
+
"""Suggest CPT codes based on minutes and whether a live discussion occurred."""
|
| 42 |
+
suggestions: List[CPTSuggestion] = []
|
| 43 |
+
if spoke:
|
| 44 |
+
for code in ["99446", "99447", "99448", "99449"]:
|
| 45 |
+
lo_hi = {
|
| 46 |
+
"99446": (5, 10),
|
| 47 |
+
"99447": (11, 20),
|
| 48 |
+
"99448": (21, 30),
|
| 49 |
+
"99449": (31, 10**9),
|
| 50 |
+
}[code]
|
| 51 |
+
eligible = (minutes >= lo_hi[0]) and (minutes <= lo_hi[1])
|
| 52 |
+
suggestions.append(CPTSuggestion(
|
| 53 |
+
code=code,
|
| 54 |
+
rate=float(CPT[code]["rate"]),
|
| 55 |
+
descriptor=str(CPT[code]["descriptor"]),
|
| 56 |
+
eligible=eligible,
|
| 57 |
+
why=_why_for_minutes(code, minutes, spoke),
|
| 58 |
+
))
|
| 59 |
+
else:
|
| 60 |
+
eligible = minutes >= 5
|
| 61 |
+
suggestions.append(CPTSuggestion(
|
| 62 |
+
code="99451",
|
| 63 |
+
rate=float(CPT["99451"]["rate"]),
|
| 64 |
+
descriptor=str(CPT["99451"]["descriptor"]),
|
| 65 |
+
eligible=eligible,
|
| 66 |
+
why=_why_for_minutes("99451", minutes, spoke),
|
| 67 |
+
))
|
| 68 |
+
return suggestions
|
| 69 |
+
|
| 70 |
+
def build_837_claim(case: Dict[str, Any], code: str, rate: float, minutes: int,
|
| 71 |
+
spoke: bool, attested: bool, *, template_path: Optional[Path] = None) -> Dict[str, Any]:
|
| 72 |
+
"""Build a demo 837 claim JSON from a case and billing selection."""
|
| 73 |
+
now = datetime.now(timezone.utc).isoformat()
|
| 74 |
+
# Load optional template
|
| 75 |
+
template: Dict[str, Any] = {}
|
| 76 |
+
try:
|
| 77 |
+
if template_path and Path(template_path).exists():
|
| 78 |
+
template = json.loads(Path(template_path).read_text(encoding="utf-8"))
|
| 79 |
+
except Exception:
|
| 80 |
+
template = {}
|
| 81 |
+
|
| 82 |
+
claim_id = f"EC-{case.get('case_id','UNKNOWN')}-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}"
|
| 83 |
+
out: Dict[str, Any] = {
|
| 84 |
+
"schema_version": 2,
|
| 85 |
+
"demo_only": True,
|
| 86 |
+
"claim_id": claim_id,
|
| 87 |
+
"created_at": now,
|
| 88 |
+
"provider": PROVIDER,
|
| 89 |
+
"patient": case.get("patient", {}),
|
| 90 |
+
"referring": {"name": (case.get("referrer") or {}).get("name", "Referring Clinician")},
|
| 91 |
+
"service": {
|
| 92 |
+
"cpt_code": code,
|
| 93 |
+
"minutes": minutes,
|
| 94 |
+
"spoke_to_referrer": bool(spoke),
|
| 95 |
+
"amount": float(rate),
|
| 96 |
+
},
|
| 97 |
+
"attested": bool(attested),
|
| 98 |
+
"source_case_id": case.get("case_id"),
|
| 99 |
+
}
|
| 100 |
+
out = {**template, **out}
|
| 101 |
+
return out
|