Files
stack/tests/pfs/test_rules.py
kert 24664656c3
Some checks failed
CI / lint (push) Successful in 1m12s
CI / notebooks-smoke (push) Successful in 1m56s
Deploy / changes (push) Successful in 18s
CI / test (push) Successful in 4m48s
Infra CI / notebooks (push) Successful in 1m37s
Infra CI / zotero (push) Successful in 21s
Infra CI / docs (push) Failing after 1m4s
Deploy / notebooks (push) Has been cancelled
Deploy / report (push) Has been cancelled
Deploy / zotero (push) Has been cancelled
Deploy / docs (push) Has been cancelled
Deploy / api (push) Has been cancelled
Deploy / bcda (push) Has been cancelled
Deploy / llm (push) Has been cancelled
Deploy / mc (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
Infra CI / bcda (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
Infra CI / api (push) Has been cancelled
feat(pfs): CY2027 final rule parameters; fix CY2025/2026 telehealth fees
RULES[2027] from the CY2027 PFS final rule (CMS-1848-F, 91 FR 70992,
:pincite:JCWARTJD): non-QP CF 32.6286, QP CF 32.9548, non-QP anesthesia
20.1056 (QP 20.3068 noted), -0.12% budget neutrality (91 FR 71653), and
the Q3014 fee $32.68 read from Table A-C1, an image at 91 FR 71027.
The NPRM block moves from PROPOSED[2027] to RULES[2027].proposed, so
proposed_for(2027) is unchanged for callers; PROPOSED is empty until
CY2028's NPRM.

Table A-C1 is CMS's full Q3014 history, and it shows two registry
errors: CY2025 held 29.96 (CY2024's fee; correct $31.01) and CY2026
held 30.38 (no such year; correct $31.85).
2026-11-11 13:23:47 -05:00

431 lines
15 KiB
Python

"""Tests for pfs.rules — RuleYear model and RULES dict with annual PFS parameters.
Covers every line of pfs/rules/__init__.py:
- RuleYear construction with required and default fields
- RULES dict: years 2014-2026 (13 entries), all RuleYear instances
- Convenience aliases: cy2024, cy2025, cy2026
- Specific parameter values for spot-checked years
- PROPOSED registry (final-less years) and proposed_for() helper
"""
from __future__ import annotations
import pytest
from pfs.rules import (
PROPOSED,
RULES,
ProposedRule,
RuleYear,
cy2024,
cy2025,
cy2026,
proposed_for,
)
# ── RuleYear model ───────────────────────────────────────────────
class TestRuleYear:
"""RuleYear Pydantic model construction and defaults."""
def test_required_fields(self):
ry = RuleYear(year=2026, conversion_factor=32.3465)
assert ry.year == 2026
assert ry.conversion_factor == 32.3465
def test_default_cf_qp(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.cf_qp is None
def test_default_budget_neutrality_adjustor(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.budget_neutrality_adjustor == 1.0
def test_default_work_gpci_floor(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.work_gpci_floor == 1.0
def test_default_pe_gpci_floor(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.pe_gpci_floor is False
def test_default_anesthesia_cf(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.anesthesia_cf is None
def test_default_mppr_diagnostic_imaging_pct(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.mppr_diagnostic_imaging_pct == 0.50
def test_default_mppr_therapy_pct(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.mppr_therapy_pct == 0.50
def test_default_telehealth_fee(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.telehealth_originating_site_fee is None
def test_default_clinical_labor_rate_update(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.clinical_labor_rate_update is False
def test_default_supply_equipment_price_update(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.supply_equipment_price_update is False
def test_default_federal_register_citation(self):
ry = RuleYear(year=2020, conversion_factor=36.0)
assert ry.federal_register_citation == ""
def test_all_fields_supplied(self):
ry = RuleYear(
year=2026,
conversion_factor=32.3465,
cf_qp=32.67,
budget_neutrality_adjustor=1.0,
work_gpci_floor=1.0,
pe_gpci_floor=True,
anesthesia_cf=22.0,
mppr_diagnostic_imaging_pct=0.50,
mppr_therapy_pct=0.50,
telehealth_originating_site_fee=30.38,
clinical_labor_rate_update=True,
supply_equipment_price_update=True,
federal_register_citation="90 FR 101174",
)
assert ry.year == 2026
assert ry.pe_gpci_floor is True
assert ry.anesthesia_cf == 22.0
assert ry.supply_equipment_price_update is True
# ── RULES dict structure ─────────────────────────────────────────
class TestRulesDict:
"""RULES maps calendar year -> RuleYear for 2014-2027."""
EXPECTED_YEARS = list(range(2014, 2028))
def test_count(self):
assert len(RULES) == 14
def test_keys(self):
assert sorted(RULES.keys()) == self.EXPECTED_YEARS
@pytest.mark.parametrize("year", list(range(2014, 2028)))
def test_values_are_ruleyear(self, year):
assert isinstance(RULES[year], RuleYear)
@pytest.mark.parametrize("year", list(range(2014, 2028)))
def test_year_field_matches_key(self, year):
assert RULES[year].year == year
# ── Convenience aliases ──────────────────────────────────────────
class TestAliases:
"""cy2024, cy2025, cy2026 point to the correct RULES entries."""
def test_cy2024_is_rules_2024(self):
assert cy2024 is RULES[2024]
def test_cy2025_is_rules_2025(self):
assert cy2025 is RULES[2025]
def test_cy2026_is_rules_2026(self):
assert cy2026 is RULES[2026]
# ── Spot-check specific year values ─────────────────────────────
class TestSpecificValues:
"""Verify key parameters for specific years match published CMS data."""
def test_cy2026_conversion_factor(self):
# CY2026 non-APM standard CF per AMA history :pincite:`VVBEVYLC`.
# Earlier values (32.3465, etc.) were stale copies from CY2025.
assert cy2026.conversion_factor == 33.4009
def test_cy2026_cf_qp(self):
# CY2026 APM standard CF — $0.1666 higher than non-APM under
# MACRA 2015 per :pincite:`VVBEVYLC`.
assert cy2026.cf_qp == 33.5675
def test_cy2026_telehealth_fee(self):
# Table A-C1 (Q3014 history), 91 FR 71027 — was mis-recorded as 30.38.
assert cy2026.telehealth_originating_site_fee == 31.85
def test_cy2026_federal_register(self):
assert cy2026.federal_register_citation == "90 FR 49266"
def test_cy2026_clinical_labor_update(self):
assert cy2026.clinical_labor_rate_update is True
def test_cy2025_conversion_factor(self):
assert cy2025.conversion_factor == 32.3465
def test_cy2025_telehealth_fee(self):
# Table A-C1, 91 FR 71027 — 29.96 was CY2024's fee.
assert cy2025.telehealth_originating_site_fee == 31.01
def test_cy2024_conversion_factor(self):
assert cy2024.conversion_factor == 33.2875
def test_cy2024_federal_register(self):
assert cy2024.federal_register_citation == "88 FR 78818"
def test_2022_clinical_labor_rate_update(self):
assert RULES[2022].clinical_labor_rate_update is True
def test_2022_conversion_factor(self):
assert RULES[2022].conversion_factor == 34.6062
def test_2023_supply_equipment_price_update(self):
assert RULES[2023].supply_equipment_price_update is True
def test_2014_conversion_factor(self):
assert RULES[2014].conversion_factor == 35.8228
def test_2014_federal_register(self):
assert RULES[2014].federal_register_citation == "78 FR 74230"
def test_2021_conversion_factor(self):
assert RULES[2021].conversion_factor == 34.8931
# ── Gap coverage — missed lines ────────────────────────────────
class TestCfForDate:
"""Lines 176-181: cf_for_date with segments, no segments, and outside segments."""
def test_cf_for_date_with_segments(self):
"""Line 176-178: date falls within a segment."""
from datetime import date
from pfs.rules import CfSegment
ry = RuleYear(
year=2024,
conversion_factor=33.2875,
cf_segments=[
CfSegment(start=date(2024, 1, 1), end=date(2024, 3, 8), cf=32.7442),
CfSegment(start=date(2024, 3, 9), end=date(2024, 12, 31), cf=33.2875),
],
)
assert ry.cf_for_date(date(2024, 2, 1)) == 32.7442
assert ry.cf_for_date(date(2024, 6, 1)) == 33.2875
def test_cf_for_date_no_segments(self):
"""Lines 179-180: no segments → returns conversion_factor."""
from datetime import date
ry = RuleYear(year=2025, conversion_factor=32.3465)
assert ry.cf_for_date(date(2025, 6, 1)) == 32.3465
def test_cf_for_date_outside_segments(self):
"""Line 181: date outside all segments → ValueError."""
from datetime import date
import pytest
from pfs.rules import CfSegment
ry = RuleYear(
year=2024,
conversion_factor=33.2875,
cf_segments=[
CfSegment(start=date(2024, 3, 1), end=date(2024, 6, 30), cf=33.0),
],
)
with pytest.raises(ValueError, match="outside every CF segment"):
ry.cf_for_date(date(2024, 1, 15))
# ── ProposedRule — CY2026 NPRM parameters ────────────────────────
class TestProposedRule:
"""ProposedRule model and RULES[2026].proposed values."""
def test_cy2026_has_proposed_rule(self):
p = RULES[2026].proposed
assert p is not None
assert p.cms_rule_id == "CMS-1832-P"
assert p.fr_document_number == "2025-13271"
assert p.published.year == 2025
assert p.comment_close > p.published
# exact transcribed values — from data/fr_downloads/2025-13271.txt
assert p.conversion_factor == 33.4209
assert p.cf_qp == 33.5875
assert p.conversion_factor != RULES[2026].conversion_factor # proposed ≠ final
def test_cy2026_proposed_anesthesia_cfs(self):
p = RULES[2026].proposed
assert p.anesthesia_cf == 20.5728
assert p.anesthesia_cf_qp == 20.6754
def test_cy2026_proposed_budget_neutrality_adjustor(self):
assert RULES[2026].proposed.budget_neutrality_adjustor == 1.0055
def test_cy2026_proposed_telehealth_fee(self):
assert RULES[2026].proposed.telehealth_originating_site_fee == 31.85
def test_cy2026_proposed_federal_register_citation(self):
assert RULES[2026].proposed.federal_register_citation == "90 FR 32352"
def test_cy2026_proposed_published_date(self):
from datetime import date
assert RULES[2026].proposed.published == date(2025, 7, 16)
def test_cy2026_proposed_comment_close_date(self):
from datetime import date
assert RULES[2026].proposed.comment_close == date(2025, 9, 12)
def test_pre_2026_years_have_no_proposed_block(self):
assert RULES[2025].proposed is None
def test_proposed_rule_default_notes(self):
from datetime import date
p = ProposedRule(
cms_rule_id="CMS-0000-P",
fr_document_number="2099-00000",
federal_register_citation="99 FR 1",
published=date(2099, 1, 1),
comment_close=date(2099, 3, 1),
conversion_factor=1.0,
)
assert p.notes == ""
assert p.cf_qp is None
assert p.anesthesia_cf is None
assert p.anesthesia_cf_qp is None
assert p.budget_neutrality_adjustor == 1.0
assert p.telehealth_originating_site_fee is None
# ── PROPOSED registry + proposed_for() ───────────────────────────
class TestProposedForHelper:
"""proposed_for() resolution across RULES[year].proposed and PROPOSED."""
def test_resolves_from_rules_when_year_present(self):
# 2026 is in RULES and has a .proposed block — proposed_for
# should return that same object, not consult PROPOSED.
assert proposed_for(2026) is RULES[2026].proposed
def test_resolves_from_proposed_registry_when_year_absent_from_rules(
self, monkeypatch
):
# A final-less year (CY2028's NPRM, next July) lives in PROPOSED.
prop = RULES[2027].proposed.model_copy(update={"cms_rule_id": "CMS-9999-P"})
monkeypatch.setitem(PROPOSED, 2028, prop)
assert 2028 not in RULES
assert proposed_for(2028) is prop
def test_missing_year_returns_none(self):
assert proposed_for(1999) is None
assert 1999 not in RULES
assert 1999 not in PROPOSED
def test_rules_year_without_proposed_block_returns_none(self):
# 2025 is in RULES but has no .proposed block.
assert RULES[2025].proposed is None
assert proposed_for(2025) is None
def test_2027_moved_from_proposed_to_rules(self):
# The CY2027 final rule shipped: its NPRM block now hangs off
# RULES[2027] and the PROPOSED entry is retired.
assert 2027 not in PROPOSED
assert proposed_for(2027) is RULES[2027].proposed
assert proposed_for(2027).cms_rule_id == "CMS-1848-P"
class TestCy2027Proposed:
"""proposed_for(2027) — CY2027 NPRM (CMS-1848-P) transcribed values.
Source: data/fr_downloads/2026-14327.txt, 91 FR 43842, published
2026-07-16. Per :pincite:`5ITGVDJV`.
"""
def test_identity_fields(self):
from datetime import date
p = proposed_for(2027)
assert p.cms_rule_id == "CMS-1848-P"
assert p.fr_document_number == "2026-14327"
assert p.federal_register_citation == "91 FR 43842"
assert p.published == date(2026, 7, 16)
def test_comment_close_date(self):
from datetime import date
# DATES block, file lines 108-109: "comments must be received
# ... by September 14, 2026."
assert proposed_for(2027).comment_close == date(2026, 9, 14)
assert proposed_for(2027).comment_close > proposed_for(2027).published
def test_standard_conversion_factors(self):
p = proposed_for(2027)
# RIA detailed derivation, file lines ~39095-39102 (91 FR 44242).
assert p.cf_qp == 33.1693 # qualifying APM
assert p.conversion_factor == 32.8409 # nonqualifying APM
def test_anesthesia_conversion_factors(self):
p = proposed_for(2027)
assert p.anesthesia_cf_qp == 20.4165 # qualifying APM
assert p.anesthesia_cf == 20.2143 # nonqualifying APM
def test_budget_neutrality_adjustor(self):
# "0.53 percent positive budget neutrality adjustment"
assert proposed_for(2027).budget_neutrality_adjustor == 1.0053
def test_telehealth_originating_site_fee(self):
# HCPCS Q3014, file line ~2261 (91 FR 43843): "$32.65"
assert proposed_for(2027).telehealth_originating_site_fee == 32.65
def test_not_equal_to_cy2026_final_or_proposed(self):
p2027 = proposed_for(2027)
p2026 = RULES[2026].proposed
assert p2027.conversion_factor != RULES[2026].conversion_factor
assert p2027.conversion_factor != p2026.conversion_factor
assert p2027.cf_qp != p2026.cf_qp
class TestCy2027Final:
"""RULES[2027] — CY2027 PFS Final Rule (CMS-1848-F), 91 FR 70992,
published 2026-11-09, per :pincite:`JCWARTJD`. CF sentence at
91 FR 71653; Q3014 fee in Table A-C1, 91 FR 71027."""
def test_conversion_factors(self):
r = RULES[2027]
assert r.conversion_factor == 32.6286 # nonqualifying APM
assert r.cf_qp == 32.9548 # qualifying APM
assert r.anesthesia_cf == 20.1056 # nonqualifying APM anesthesia
def test_budget_neutrality_and_fee(self):
r = RULES[2027]
assert r.budget_neutrality_adjustor == 0.9988 # -0.12 percent
assert r.telehealth_originating_site_fee == 32.68
def test_citation_and_single_segment(self):
from datetime import date
r = RULES[2027]
assert r.federal_register_citation == "91 FR 70992"
assert r.cf_for_date(date(2027, 7, 1)) == 32.6286
def test_final_differs_from_proposed(self):
r = RULES[2027]
# NPRM 32.8409 / 33.1693 → final 32.6286 / 32.9548: BN flipped
# from +0.53% proposed to -0.12% final.
assert r.conversion_factor < r.proposed.conversion_factor
assert r.cf_qp < r.proposed.cf_qp