Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
3.5 KiB
Python
82 lines
3.5 KiB
Python
"""
|
|
Payer -> Group Name lookup for the categorization step.
|
|
|
|
Exact-string match against a small hand-edited table (`payer_groups.json`,
|
|
next to this package) — no fuzzy or substring matching. eCW's own PAYER
|
|
values already differ by case and spacing for what is really the same
|
|
carrier (see `tests/test_payer_groups.py`'s docstring), so "close enough"
|
|
matching has no safe way to pick which known string a new one is closest to.
|
|
An unrecognized payer must come back as None and get a human's attention —
|
|
never a guess, on billing data.
|
|
|
|
Since 2026-10-06 the table is the team's own template: the Payer Name ->
|
|
Payer Group Name pairs of their Claim Status Dashboard, imported with
|
|
`tools/import_payer_groups.py` (re-run it when the dashboard changes). Adding
|
|
a payer by hand is still a one-line edit to `payer_groups.json`.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
_CONFIG_PATH = Path(__file__).resolve().parent.parent / "payer_groups.json"
|
|
|
|
|
|
def load_mapping(path=None) -> dict:
|
|
"""The payer -> group table. `path` defaults to the shipped config."""
|
|
p = Path(path) if path else _CONFIG_PATH
|
|
if not p.exists():
|
|
raise FileNotFoundError(
|
|
f"{p} not found — the payer group table is missing")
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
if not isinstance(data, dict):
|
|
raise ValueError(
|
|
f"{p} does not contain a JSON object (payer -> group name) — "
|
|
f"got {type(data).__name__}")
|
|
return data
|
|
|
|
|
|
def classify(payer: str, mapping: dict) -> str:
|
|
"""The Group Name for this exact PAYER string, or None if unmapped."""
|
|
return mapping.get((payer or "").strip())
|
|
|
|
|
|
# ── Group names the CONDITIONS decide on ─────────────────────────────────────
|
|
# Since 2026-10-06 `payer_groups.json` is the team's own template — the
|
|
# "Payer Name" -> "Payer Group Name" pairs of their Claim Status Dashboard
|
|
# (tools/import_payer_groups.py) — so its group names are the dashboard's
|
|
# ("Commercial Ins", "RAE (Behavioral Health)", "Dental - CHP+/DentaQuest"...)
|
|
# and that is what the Group Name columns show. The condition modules were
|
|
# written against the earlier, shorter names, which stay the vocabulary of
|
|
# their rules; this table translates. A name not listed (including every
|
|
# earlier short name) passes through unchanged.
|
|
RULE_GROUP = {
|
|
"Commercial Ins": "Commercial",
|
|
"RAE (Behavioral Health)": "RAE payer",
|
|
"Regional Accountable Entity-RAE (Reporting Group)": "RAE payer",
|
|
"Dental - CHP+/DentaQuest": "Dentaquest",
|
|
"Self Pay": "Program", # the clinic's programs (Outreach, slides…)
|
|
"Payer Not In Group": None, # the dashboard's own "unclassified"
|
|
}
|
|
|
|
# A payer whose condition rule differs from its dashboard group. Empty since
|
|
# 2026-10-21: the Medicare Advantage plans (AARP Medicare Advantage, Aetna
|
|
# Medicare ADV) were kept on Medicare here until the operator ruled; the
|
|
# ruling is "follow the dashboard", so they are Commercial for every
|
|
# condition (#21 95/93 not #22 G2025; no #14, #19, #27-#29; #18 / #20 add).
|
|
RULE_OVERRIDES = {}
|
|
|
|
|
|
def rule_group(payer: str, mapping: dict) -> str:
|
|
"""
|
|
The group a condition's rule should use for this PAYER, or None if
|
|
unmapped: the payer's own override if it has one, else its Group Name
|
|
translated through RULE_GROUP.
|
|
"""
|
|
key = (payer or "").strip()
|
|
if key in RULE_OVERRIDES:
|
|
return RULE_OVERRIDES[key]
|
|
group = classify(key, mapping)
|
|
if group is None:
|
|
return None
|
|
return RULE_GROUP.get(group, group)
|