fix(pfs): pay only A/R/T statuses; window over RULES years; labelled CFs (refs P48)
Multiplying RVUs by the CF for a bundled (B) or carrier-priced (C) code put a payment on screen that Medicare never makes: pay_nf/pay_f are now None outside statuses A/R/T and every row carries a status_note saying why. The last-N window counted vintages pfs.rules has no CF for, which could empty the table; it now counts only RULES years and warns about the rest (and about a missing NPRM ProposedRule). cf_note labels which of the year's several conversion factors is shown (non-APM standard, the QP CF, a mid-year split).
This commit is contained in:
@@ -7,11 +7,14 @@ narwhals (this module runs inside the ``llm`` container).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Sequence
|
from typing import Any, Sequence
|
||||||
|
|
||||||
from pfs.nprm import NPRM_SOURCES
|
from pfs.nprm import NPRM_SOURCES
|
||||||
from pfs.rules import RULES, proposed_for
|
from pfs.rules import RULES, ProposedRule, RuleYear, proposed_for
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -34,6 +37,8 @@ class ValuationRow:
|
|||||||
label: str # "[PFS CY2026 Addendum B]"
|
label: str # "[PFS CY2026 Addendum B]"
|
||||||
citation: str # "90 FR 49266"
|
citation: str # "90 FR 49266"
|
||||||
url: str
|
url: str
|
||||||
|
status_note: str = "" # why a non-A/R/T status is not paid ("" when paid)
|
||||||
|
cf_note: str = "" # which of the year's several CFs this one is
|
||||||
|
|
||||||
|
|
||||||
def vintage_label(year: int, proposed: bool) -> str:
|
def vintage_label(year: int, proposed: bool) -> str:
|
||||||
@@ -47,8 +52,80 @@ def fr_citation_url(cite: str) -> str:
|
|||||||
return f"https://www.federalregister.gov/citation/{parts[0]}-FR-{parts[2]}"
|
return f"https://www.federalregister.gov/citation/{parts[0]}-FR-{parts[2]}"
|
||||||
|
|
||||||
|
|
||||||
def _pay(total: float | None, cf: float) -> float | None:
|
#: Payment status codes paid as total RVUs × CF. Every other status is
|
||||||
return None if total is None else round(total * cf, 2)
|
#: priced some other way (or not at all), so multiplying its RVUs by the
|
||||||
|
#: CF would put a number on screen that Medicare never pays.
|
||||||
|
PAID_STATUSES = frozenset({"A", "R", "T"})
|
||||||
|
|
||||||
|
_STATUS_NOTES = {
|
||||||
|
"B": "bundled — no separate PFS payment",
|
||||||
|
"C": "carrier priced by the MAC",
|
||||||
|
"J": "anesthesia — paid on base units × the anesthesia CF",
|
||||||
|
"I": "not valid for Medicare",
|
||||||
|
"N": "non-covered",
|
||||||
|
"E": "excluded by regulation",
|
||||||
|
"M": "measurement code — not paid",
|
||||||
|
"X": "statutory exclusion",
|
||||||
|
"P": "bundled/excluded",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def status_note(status: str) -> str:
|
||||||
|
"""Why *status* is not paid from RVUs × CF — ``""`` when it is."""
|
||||||
|
code = (status or "").strip().upper()
|
||||||
|
if code in PAID_STATUSES:
|
||||||
|
return ""
|
||||||
|
if code in _STATUS_NOTES:
|
||||||
|
return _STATUS_NOTES[code]
|
||||||
|
if not code:
|
||||||
|
return "status not stated — no RVU-based payment"
|
||||||
|
return f"status {code} — no RVU-based payment"
|
||||||
|
|
||||||
|
|
||||||
|
def final_cf_note(rule: RuleYear) -> str:
|
||||||
|
"""Which of a final year's several conversion factors ``cf`` is.
|
||||||
|
|
||||||
|
Since CY2026 CMS publishes separate QP (APM) and non-QP standard
|
||||||
|
CFs, and a mid-year legislative update can split a year in two — an
|
||||||
|
unlabelled dollar figure would hide both.
|
||||||
|
"""
|
||||||
|
parts = ["non-APM standard CF"]
|
||||||
|
if rule.cf_qp is not None and rule.cf_qp != rule.conversion_factor:
|
||||||
|
parts.append(f"QP CF ${rule.cf_qp:.4f}")
|
||||||
|
segs = sorted(rule.cf_segments, key=lambda s: s.start)
|
||||||
|
if len(segs) > 1:
|
||||||
|
earlier = next(
|
||||||
|
(
|
||||||
|
(seg, nxt)
|
||||||
|
for seg, nxt in zip(segs, segs[1:])
|
||||||
|
if seg.cf != rule.conversion_factor
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if earlier is None:
|
||||||
|
parts.append("mid-year split — dominant CF shown")
|
||||||
|
else:
|
||||||
|
seg, nxt = earlier
|
||||||
|
parts.append(
|
||||||
|
f"mid-year split — dominant CF shown (${seg.cf:.4f} before {nxt.start})"
|
||||||
|
)
|
||||||
|
return "; ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def proposed_cf_note(prop: ProposedRule) -> str:
|
||||||
|
"""Which CF a proposed row shows (the NPRM's non-QP standard one)."""
|
||||||
|
parts = ["proposed; non-QP CF"]
|
||||||
|
if prop.cf_qp is not None and prop.cf_qp != prop.conversion_factor:
|
||||||
|
parts.append(f"QP CF ${prop.cf_qp:.4f}")
|
||||||
|
return "; ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _pay(total: float | None, cf: float, status: str) -> float | None:
|
||||||
|
"""National unadjusted payment, or ``None`` when the status is not
|
||||||
|
paid from RVUs (see :data:`PAID_STATUSES`)."""
|
||||||
|
if total is None or (status or "").strip().upper() not in PAID_STATUSES:
|
||||||
|
return None
|
||||||
|
return round(total * cf, 2)
|
||||||
|
|
||||||
|
|
||||||
_FINAL_SQL = """
|
_FINAL_SQL = """
|
||||||
@@ -57,6 +134,7 @@ WITH base AS (
|
|||||||
fac_pe_rvu, mp_rvu, non_fac_total, fac_total
|
fac_pe_rvu, mp_rvu, non_fac_total, fac_total
|
||||||
FROM pfs.rvu
|
FROM pfs.rvu
|
||||||
WHERE hcpcs IN (SELECT UNNEST(?::VARCHAR[]))
|
WHERE hcpcs IN (SELECT UNNEST(?::VARCHAR[]))
|
||||||
|
AND year IN (SELECT UNNEST(?::INTEGER[]))
|
||||||
QUALIFY row_number() OVER (
|
QUALIFY row_number() OVER (
|
||||||
PARTITION BY hcpcs, year ORDER BY (mod IS NULL OR mod = '') DESC, mod
|
PARTITION BY hcpcs, year ORDER BY (mod IS NULL OR mod = '') DESC, mod
|
||||||
) = 1
|
) = 1
|
||||||
@@ -77,6 +155,14 @@ ORDER BY hcpcs
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
_UNKNOWN_YEARS_SQL = """
|
||||||
|
SELECT DISTINCT year FROM pfs.rvu
|
||||||
|
WHERE hcpcs IN (SELECT UNNEST(?::VARCHAR[]))
|
||||||
|
AND year NOT IN (SELECT UNNEST(?::INTEGER[]))
|
||||||
|
ORDER BY year
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _sum(*vals: float | None) -> float | None:
|
def _sum(*vals: float | None) -> float | None:
|
||||||
if any(v is None for v in vals):
|
if any(v is None for v in vals):
|
||||||
return None
|
return None
|
||||||
@@ -93,13 +179,24 @@ def valuation(
|
|||||||
if not wanted:
|
if not wanted:
|
||||||
return [], []
|
return [], []
|
||||||
by_code: dict[str, list[ValuationRow]] = {c: [] for c in wanted}
|
by_code: dict[str, list[ValuationRow]] = {c: [] for c in wanted}
|
||||||
|
rule_years = sorted(RULES)
|
||||||
|
|
||||||
|
skipped = [
|
||||||
|
int(y)
|
||||||
|
for (y,) in con.execute(_UNKNOWN_YEARS_SQL, [wanted, rule_years]).fetchall()
|
||||||
|
]
|
||||||
|
if skipped:
|
||||||
|
log.warning(
|
||||||
|
"valuation: %s priced in pfs.rvu year(s) %s with no pfs.rules entry "
|
||||||
|
"— skipped (no conversion factor)",
|
||||||
|
", ".join(wanted),
|
||||||
|
", ".join(str(y) for y in skipped),
|
||||||
|
)
|
||||||
|
|
||||||
for hcpcs, year, desc, status, work, pe_nf, pe_f, mp, tot_nf, tot_f in con.execute(
|
for hcpcs, year, desc, status, work, pe_nf, pe_f, mp, tot_nf, tot_f in con.execute(
|
||||||
_FINAL_SQL, [wanted, years]
|
_FINAL_SQL, [wanted, rule_years, years]
|
||||||
).fetchall():
|
).fetchall():
|
||||||
rule = RULES.get(int(year))
|
rule = RULES[int(year)]
|
||||||
if rule is None:
|
|
||||||
continue
|
|
||||||
cf = rule.conversion_factor
|
cf = rule.conversion_factor
|
||||||
cite = rule.federal_register_citation
|
cite = rule.federal_register_citation
|
||||||
by_code[hcpcs].append(
|
by_code[hcpcs].append(
|
||||||
@@ -110,6 +207,7 @@ def valuation(
|
|||||||
year=int(year),
|
year=int(year),
|
||||||
proposed=False,
|
proposed=False,
|
||||||
status=status or "",
|
status=status or "",
|
||||||
|
status_note=status_note(status or ""),
|
||||||
work=work,
|
work=work,
|
||||||
pe_nf=pe_nf,
|
pe_nf=pe_nf,
|
||||||
pe_f=pe_f,
|
pe_f=pe_f,
|
||||||
@@ -117,8 +215,9 @@ def valuation(
|
|||||||
total_nf=tot_nf,
|
total_nf=tot_nf,
|
||||||
total_f=tot_f,
|
total_f=tot_f,
|
||||||
cf=cf,
|
cf=cf,
|
||||||
pay_nf=_pay(tot_nf, cf),
|
pay_nf=_pay(tot_nf, cf, status or ""),
|
||||||
pay_f=_pay(tot_f, cf),
|
pay_f=_pay(tot_f, cf, status or ""),
|
||||||
|
cf_note=final_cf_note(rule),
|
||||||
label=vintage_label(int(year), False),
|
label=vintage_label(int(year), False),
|
||||||
citation=cite,
|
citation=cite,
|
||||||
url=fr_citation_url(cite),
|
url=fr_citation_url(cite),
|
||||||
@@ -127,7 +226,13 @@ def valuation(
|
|||||||
|
|
||||||
nprm_year, _tag, rule_id, _pin = max(NPRM_SOURCES, key=lambda s: s[0])
|
nprm_year, _tag, rule_id, _pin = max(NPRM_SOURCES, key=lambda s: s[0])
|
||||||
prop = proposed_for(nprm_year)
|
prop = proposed_for(nprm_year)
|
||||||
if prop is not None:
|
if prop is None:
|
||||||
|
log.warning(
|
||||||
|
"valuation: no ProposedRule for CY%s — the newest NPRM vintage is "
|
||||||
|
"omitted from the table",
|
||||||
|
nprm_year,
|
||||||
|
)
|
||||||
|
else:
|
||||||
cf = prop.conversion_factor
|
cf = prop.conversion_factor
|
||||||
cite = prop.federal_register_citation
|
cite = prop.federal_register_citation
|
||||||
for hcpcs, desc, status, work, pe_nf, pe_f, mp in con.execute(
|
for hcpcs, desc, status, work, pe_nf, pe_f, mp in con.execute(
|
||||||
@@ -142,6 +247,7 @@ def valuation(
|
|||||||
year=nprm_year,
|
year=nprm_year,
|
||||||
proposed=True,
|
proposed=True,
|
||||||
status=status or "",
|
status=status or "",
|
||||||
|
status_note=status_note(status or ""),
|
||||||
work=work,
|
work=work,
|
||||||
pe_nf=pe_nf,
|
pe_nf=pe_nf,
|
||||||
pe_f=pe_f,
|
pe_f=pe_f,
|
||||||
@@ -149,8 +255,9 @@ def valuation(
|
|||||||
total_nf=tot_nf,
|
total_nf=tot_nf,
|
||||||
total_f=tot_f,
|
total_f=tot_f,
|
||||||
cf=cf,
|
cf=cf,
|
||||||
pay_nf=_pay(tot_nf, cf),
|
pay_nf=_pay(tot_nf, cf, status or ""),
|
||||||
pay_f=_pay(tot_f, cf),
|
pay_f=_pay(tot_f, cf, status or ""),
|
||||||
|
cf_note=proposed_cf_note(prop),
|
||||||
label=vintage_label(nprm_year, True),
|
label=vintage_label(nprm_year, True),
|
||||||
citation=cite,
|
citation=cite,
|
||||||
url=fr_citation_url(cite),
|
url=fr_citation_url(cite),
|
||||||
|
|||||||
@@ -87,6 +87,37 @@ def con():
|
|||||||
)
|
)
|
||||||
for y in range(2021, 2027)
|
for y in range(2021, 2027)
|
||||||
],
|
],
|
||||||
|
# A vintage older than the first year pfs.rules knows (RULES starts
|
||||||
|
# at 2014): it has no CF, so it must not consume a window slot.
|
||||||
|
(
|
||||||
|
"G0556",
|
||||||
|
None,
|
||||||
|
"Adv prim care mgmt lvl 1",
|
||||||
|
"A",
|
||||||
|
0.25,
|
||||||
|
0.20,
|
||||||
|
0.10,
|
||||||
|
0.02,
|
||||||
|
0.47,
|
||||||
|
0.37,
|
||||||
|
30.0,
|
||||||
|
2013,
|
||||||
|
),
|
||||||
|
# B-status code with full RVUs — bundled, so never paid
|
||||||
|
(
|
||||||
|
"G0559",
|
||||||
|
None,
|
||||||
|
"Bundled service",
|
||||||
|
"B",
|
||||||
|
0.30,
|
||||||
|
0.20,
|
||||||
|
0.10,
|
||||||
|
0.02,
|
||||||
|
0.52,
|
||||||
|
0.42,
|
||||||
|
33.4009,
|
||||||
|
2026,
|
||||||
|
),
|
||||||
# I-status code with NULL components
|
# I-status code with NULL components
|
||||||
(
|
(
|
||||||
"G9999",
|
"G9999",
|
||||||
@@ -204,6 +235,55 @@ class TestValuation:
|
|||||||
(r,) = rows
|
(r,) = rows
|
||||||
assert r.status == "I" and r.work is None and r.pay_nf is None
|
assert r.status == "I" and r.work is None and r.pay_nf is None
|
||||||
|
|
||||||
|
def test_bundled_status_is_not_paid_and_is_annotated(self, con):
|
||||||
|
rows, _ = valuation(con, ["G0559"])
|
||||||
|
(r,) = rows
|
||||||
|
assert (r.status, r.total_nf, r.total_f) == ("B", 0.52, 0.42)
|
||||||
|
assert r.pay_nf is None and r.pay_f is None
|
||||||
|
assert r.status_note == "bundled — no separate PFS payment"
|
||||||
|
|
||||||
|
def test_paid_statuses_have_no_note(self, con):
|
||||||
|
rows, _ = valuation(con, ["G0556"], years=1)
|
||||||
|
r = rows[0]
|
||||||
|
assert r.status == "A" and r.status_note == ""
|
||||||
|
assert r.pay_nf == round(r.total_nf * r.cf, 2)
|
||||||
|
|
||||||
|
def test_unknown_status_note_names_the_code(self, con):
|
||||||
|
con.execute(
|
||||||
|
"INSERT INTO pfs.rvu VALUES "
|
||||||
|
"('G0560',NULL,'Odd','Q',0.1,0.1,0.1,0.0,0.2,0.2,33.4009,2026)"
|
||||||
|
)
|
||||||
|
(r,) = valuation(con, ["G0560"])[0]
|
||||||
|
assert r.pay_nf is None
|
||||||
|
assert r.status_note == "status Q — no RVU-based payment"
|
||||||
|
|
||||||
|
def test_window_counts_only_years_pfs_rules_knows(self, con, caplog):
|
||||||
|
"""A vintage with no RuleYear must not eat a slot in the last-N
|
||||||
|
window — otherwise asking for one year returns nothing at all."""
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
rows, _ = valuation(con, ["G0556"], years=1)
|
||||||
|
assert [(r.year, r.proposed) for r in rows] == [(2026, False), (2027, True)]
|
||||||
|
assert "2013" in caplog.text and "G0556" in caplog.text
|
||||||
|
|
||||||
|
def test_cf_note_final_year(self, con):
|
||||||
|
rows, _ = valuation(con, ["G0556"], years=1)
|
||||||
|
r = rows[0]
|
||||||
|
assert r.cf_note == (f"non-APM standard CF; QP CF ${RULES[2026].cf_qp:.4f}")
|
||||||
|
|
||||||
|
def test_cf_note_flags_a_mid_year_split(self, con):
|
||||||
|
rows, _ = valuation(con, ["99490"], years=3)
|
||||||
|
r24 = next(r for r in rows if r.year == 2024)
|
||||||
|
assert r24.cf_note.startswith("non-APM standard CF; mid-year split")
|
||||||
|
assert "$32.7442 before 2024-03-09" in r24.cf_note
|
||||||
|
|
||||||
|
def test_cf_note_proposed(self, con):
|
||||||
|
rows, _ = valuation(con, ["G0556"], years=1)
|
||||||
|
p = rows[-1]
|
||||||
|
assert p.proposed
|
||||||
|
assert p.cf_note == (
|
||||||
|
f"proposed; non-QP CF; QP CF ${proposed_for(2027).cf_qp:.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
def test_order_is_code_then_year(self, con):
|
def test_order_is_code_then_year(self, con):
|
||||||
rows, _ = valuation(con, ["G0556", "99490"], years=2)
|
rows, _ = valuation(con, ["G0556", "99490"], years=2)
|
||||||
assert [(r.code, r.year) for r in rows] == [
|
assert [(r.code, r.year) for r in rows] == [
|
||||||
|
|||||||
Reference in New Issue
Block a user