Cardiosense-AG commited on
Commit
ae6ca67
·
verified ·
1 Parent(s): e1abe02

Create billing.py

Browse files
Files changed (1) hide show
  1. 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