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).
296 lines
9.1 KiB
Python
296 lines
9.1 KiB
Python
"""pfs.valuation — RVUs + national payment by vintage from a DuckDB replica."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import duckdb
|
|
import pytest
|
|
|
|
from pfs.rules import RULES, proposed_for
|
|
from pfs.valuation import ValuationRow, fr_citation_url, valuation, vintage_label
|
|
|
|
RVU_COLS = (
|
|
"hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, "
|
|
"work_rvu DOUBLE, non_fac_pe_rvu DOUBLE, fac_pe_rvu DOUBLE, mp_rvu DOUBLE, "
|
|
"non_fac_total DOUBLE, fac_total DOUBLE, conv_factor DOUBLE, year INTEGER"
|
|
)
|
|
PROPOSED_COLS = (
|
|
"hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, "
|
|
"work_rvu DOUBLE, non_fac_pe_rvu DOUBLE, fac_pe_rvu DOUBLE, mp_rvu DOUBLE, cms_rule_id VARCHAR"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def con():
|
|
c = duckdb.connect(":memory:")
|
|
c.execute("CREATE SCHEMA pfs")
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
c.execute(f"CREATE TABLE pfs.rvu_proposed ({PROPOSED_COLS})")
|
|
rows = [
|
|
# G0556: 2025 + 2026 final, with a modifier row that must be ignored
|
|
(
|
|
"G0556",
|
|
None,
|
|
"Adv prim care mgmt lvl 1",
|
|
"A",
|
|
0.25,
|
|
0.20,
|
|
0.10,
|
|
0.02,
|
|
0.47,
|
|
0.37,
|
|
32.3465,
|
|
2025,
|
|
),
|
|
(
|
|
"G0556",
|
|
None,
|
|
"Adv prim care mgmt lvl 1",
|
|
"A",
|
|
0.25,
|
|
0.22,
|
|
0.06,
|
|
0.02,
|
|
0.49,
|
|
0.33,
|
|
33.4009,
|
|
2026,
|
|
),
|
|
(
|
|
"G0556",
|
|
"26",
|
|
"Adv prim care mgmt lvl 1",
|
|
"A",
|
|
9.0,
|
|
9.0,
|
|
9.0,
|
|
9.0,
|
|
27.0,
|
|
27.0,
|
|
33.4009,
|
|
2026,
|
|
),
|
|
# 99490: six years so the last-N cut applies (2021..2026)
|
|
*[
|
|
(
|
|
"99490",
|
|
"",
|
|
"Chrnc care mgmt srvc 20 min",
|
|
"A",
|
|
1.0,
|
|
1.0,
|
|
0.5,
|
|
0.05,
|
|
2.05,
|
|
1.55,
|
|
30.0,
|
|
y,
|
|
)
|
|
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
|
|
(
|
|
"G9999",
|
|
None,
|
|
"Not priced",
|
|
"I",
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
33.4009,
|
|
2026,
|
|
),
|
|
]
|
|
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", rows)
|
|
c.executemany(
|
|
"INSERT INTO pfs.rvu_proposed VALUES (?,?,?,?,?,?,?,?,?)",
|
|
[
|
|
(
|
|
"G0556",
|
|
None,
|
|
"Adv prim care mgmt lvl 1",
|
|
"A",
|
|
0.25,
|
|
0.23,
|
|
0.07,
|
|
0.02,
|
|
"CMS-1848-P",
|
|
),
|
|
(
|
|
"G0556",
|
|
"26",
|
|
"Adv prim care mgmt lvl 1",
|
|
"A",
|
|
9.0,
|
|
9.0,
|
|
9.0,
|
|
9.0,
|
|
"CMS-1848-P",
|
|
),
|
|
("G0556", None, "older nprm", "A", 0.1, 0.1, 0.1, 0.1, "CMS-1832-P"),
|
|
],
|
|
)
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
class TestHelpers:
|
|
def test_vintage_label(self):
|
|
assert vintage_label(2026, False) == "[PFS CY2026 Addendum B]"
|
|
assert vintage_label(2027, True) == "[CY2027 NPRM Addendum B]"
|
|
|
|
def test_fr_citation_url(self):
|
|
assert (
|
|
fr_citation_url("90 FR 49266")
|
|
== "https://www.federalregister.gov/citation/90-FR-49266"
|
|
)
|
|
assert fr_citation_url("") == ""
|
|
|
|
|
|
class TestValuation:
|
|
def test_final_and_proposed_rows_with_cf_and_payment(self, con):
|
|
rows, unpriced = valuation(con, ["G0556"], years=4)
|
|
assert unpriced == []
|
|
assert [(r.year, r.proposed) for r in rows] == [
|
|
(2025, False),
|
|
(2026, False),
|
|
(2027, True),
|
|
]
|
|
r26 = rows[1]
|
|
assert isinstance(r26, ValuationRow)
|
|
assert (r26.code, r26.status, r26.work, r26.pe_nf, r26.pe_f, r26.mp) == (
|
|
"G0556",
|
|
"A",
|
|
0.25,
|
|
0.22,
|
|
0.06,
|
|
0.02,
|
|
)
|
|
assert (r26.total_nf, r26.total_f) == (0.49, 0.33)
|
|
assert r26.cf == RULES[2026].conversion_factor
|
|
assert r26.pay_nf == round(0.49 * RULES[2026].conversion_factor, 2)
|
|
assert r26.pay_f == round(0.33 * RULES[2026].conversion_factor, 2)
|
|
assert r26.label == "[PFS CY2026 Addendum B]"
|
|
assert r26.citation == RULES[2026].federal_register_citation
|
|
assert r26.url == fr_citation_url(RULES[2026].federal_register_citation)
|
|
assert r26.vintage == "CY2026 final"
|
|
p = rows[2]
|
|
assert p.label == "[CY2027 NPRM Addendum B]" and p.vintage == "CY2027 proposed"
|
|
assert p.cf == proposed_for(2027).conversion_factor
|
|
assert p.total_nf == round(0.25 + 0.23 + 0.02, 4) and p.total_f == round(
|
|
0.25 + 0.07 + 0.02, 4
|
|
)
|
|
assert p.pay_nf == round(p.total_nf * p.cf, 2)
|
|
assert p.citation == proposed_for(2027).federal_register_citation
|
|
|
|
def test_modifier_rows_are_ignored(self, con):
|
|
rows, _ = valuation(con, ["G0556"])
|
|
assert all(r.work == 0.25 for r in rows)
|
|
|
|
def test_last_n_years_only(self, con):
|
|
rows, _ = valuation(con, ["99490"], years=4)
|
|
assert [r.year for r in rows if not r.proposed] == [2023, 2024, 2025, 2026]
|
|
|
|
def test_unpriced_codes_reported(self, con):
|
|
rows, unpriced = valuation(con, ["G0556", "Z9999"])
|
|
assert unpriced == ["Z9999"]
|
|
assert {r.code for r in rows} == {"G0556"}
|
|
|
|
def test_null_components_pass_through(self, con):
|
|
rows, unpriced = valuation(con, ["G9999"])
|
|
assert unpriced == []
|
|
(r,) = rows
|
|
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):
|
|
rows, _ = valuation(con, ["G0556", "99490"], years=2)
|
|
assert [(r.code, r.year) for r in rows] == [
|
|
("99490", 2025),
|
|
("99490", 2026),
|
|
("G0556", 2025),
|
|
("G0556", 2026),
|
|
("G0556", 2027),
|
|
]
|