feat(qpp): QP/Advanced-APM registry incl. CMS-1832-P proposed changes (refs #593)

This commit is contained in:
kert
2026-08-13 11:56:22 -04:00
parent bc248ea3a1
commit ab090e2840
5 changed files with 653 additions and 2 deletions

View File

@@ -94,6 +94,10 @@ pfs = [
"duckdb>=1.0.0", "duckdb>=1.0.0",
"narwhals>=2.17.0", "narwhals>=2.17.0",
] ]
qpp = [
"stack[conf]",
"pydantic>=2.0.0",
]
rec = [ rec = [
"stack[conf]", "stack[conf]",
"stack[pfs]", "stack[pfs]",
@@ -159,6 +163,7 @@ all = [
"stack[llm]", "stack[llm]",
"stack[perf]", "stack[perf]",
"stack[pfs]", "stack[pfs]",
"stack[qpp]",
"stack[rec]", "stack[rec]",
"stack[rex]", "stack[rex]",
"stack[lake]", "stack[lake]",
@@ -212,6 +217,6 @@ source = ["src"]
concurrency = ["multiprocessing", "thread"] concurrency = ["multiprocessing", "thread"]
[tool.uv.build-backend] [tool.uv.build-backend]
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "opps", "perf", "pfs", "prisma", "rec", "rex", "sem"] module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
namespace = true namespace = true
source-exclude = ["compose.yml","infra/**","data/**","notebooks/**","tuva/**","assets/**","docs/**","dev/**","bundle/**","cloud/**"] source-exclude = ["compose.yml","infra/**","data/**","notebooks/**","tuva/**","assets/**","docs/**","dev/**","bundle/**","cloud/**"]

407
src/qpp/__init__.py Normal file
View File

@@ -0,0 +1,407 @@
"""Quality Payment Program (QPP) — QP / Advanced-APM registry.
Under MACRA 2015 (section 1848(q) and section 1833(z) of the Act),
eligible clinicians who participate heavily enough in an Advanced
Alternative Payment Model (Advanced APM) become Qualifying APM
Participants (QPs) for a "QP Performance Period" and are, as a
result, excluded from MIPS reporting and payment adjustments for the
corresponding payment year. Clinicians who clear a lower bar become
"Partial QPs" and may elect whether to report to MIPS.
The ``QppYear`` model captures, for a single QP Performance Period,
the thresholds and financial-risk standards that govern QP/Partial
QP status, the payment-year APM Incentive Payment (where still in
effect), and whether the payment year's differential (QP vs. non-QP)
Advanced-APM conversion factor applies.
Performance year vs. payment year
----------------------------------
QP status is determined for a "QP Performance Period" (a calendar
year); the resulting MIPS exclusion / APM Incentive Payment /
differential CF apply two years later, in the "payment year"::
payment_year = performance_year + 2
e.g. QP Performance Period 2024 determines payment-year-2026
treatment. ``for_payment_year(2026)`` looks up the ``QppYear`` whose
``payment_year`` matches.
QP conversion-factor split
---------------------------
Section 1848(d)(20) of the Act establishes two separate PFS
conversion factors beginning payment year 2026: a "qualifying APM"
CF (update +0.75%/yr) for QPs, and a "nonqualifying APM" CF (update
+0.25%/yr) for everyone else. ``QppYear.qp_cf_applies`` is ``True``
starting with performance year 2024 (whose payment year, 2026, is
the first split-CF year) and ``False`` for earlier performance
years. See :class:`pfs.rules.RuleYear` (``cf_qp`` /
``conversion_factor`` on ``RULES[2026]``) for the actual dollar CFs.
APM Incentive Payment sunset
------------------------------
The lump-sum APM Incentive Payment (a fixed percentage of a QP's
prior-year Part B paid claims) is statutorily phased out: 5% for
payment years 2019-2024, 3.5% for payment year 2025 (QP Performance
Period 2023), 1.88% for payment year 2026 (QP Performance Period
2024) — and payment year 2026 is, under current law, the *last* year
these payments are made. ``QppYear.apm_incentive_pct`` is ``None``
for QP Performance Periods 2025 and later.
CMS-1832-P (CY2026 PFS proposed rule, 90 FR 32352, transcribed from
``data/fr_downloads/2025-13271.txt``) proposed several QPP
methodology changes effective beginning the 2026 QP Performance
Period — individual-level QP determinations, an expanded
attribution-eligible-beneficiary definition, and sunsetting the
Medical Home Model 50-eligible-clinician limit — captured on
``QPP[2026].proposed``. It proposed **no change** to the QP /
Partial QP percentage thresholds, the generally applicable nominal
amount standards, or the CEHRT requirement; see ``proposed.changes``
for the sourced disposition, including the Final Rule's correction
of an internal inconsistency in the NPRM's own regulatory-impact-
analysis text. Ingested into Zotero as :pincite:`2KVJ2HKX`.
Nominal amount standard sourcing caveat
-----------------------------------------
Neither the CY2026 NPRM nor the CY2026 Final Rule (``2025-19787.txt``)
restates the *numeric* generally applicable nominal amount standard
(42 CFR 414.1415(c)(3)(i)(A)/(B)) — both only cross-reference the
regulation by section number, which is itself evidence CMS proposed
no change to it. The revenue-based standard (8%) is transcribed from
the CY2019 PFS Final Rule (``data/fr_downloads/2018-24170.txt``, 83 FR
59939) via the CY2020 PFS Proposed Rule's restatement
(``data/fr_downloads/2019-16041.txt``, 84 FR 40825), both already
present in this repository's ``fr_downloads`` corpus, which state it
applies "for QP Performance Periods 2021 through 2024" — commenters
asked CMS to raise it for 2025+ and CMS did not finalize an increase.
The benchmark-based standard (3%) could **not** be found stated as a
bare number tied to section 414.1415(c)(3)(i)(B) anywhere in this
repository's FR corpus; the value used here is carried over from the
structurally parallel Other Payer Advanced APM standard at 42 CFR
414.1420(d)(3)(i) (same 2018-24170.txt document, 83 FR 59939), which
CMS designed to align with the Medicare-option standard. See
``RiskStandards.notes`` on each entry.
"""
from __future__ import annotations
from pydantic import BaseModel
class QpThresholds(BaseModel):
"""A pair of QP (or Partial QP) determination thresholds.
An eligible clinician (or APM Entity) attains status by meeting
*either* threshold — they are not additive requirements."""
payment_amount_pct: float
"""Minimum % of Medicare Part B payments for covered professional
services that must flow through Advanced APMs."""
patient_count_pct: float
"""Minimum % of Medicare patients that must be attributed through
Advanced APMs."""
class RiskStandards(BaseModel):
"""The "more than nominal" financial-risk standard an Advanced APM
must impose on its APM Entities, per 42 CFR 414.1415(c)(3)(i)."""
revenue_nominal_pct: float
"""Revenue-based nominal amount standard — % of the average
estimated total Medicare Parts A and B revenue of all providers
and suppliers in participating APM Entities that must be at risk.
42 CFR 414.1415(c)(3)(i)(A)."""
benchmark_nominal_pct: float
"""Expenditure/benchmark-based nominal amount standard — minimum %
of expected expenditures the APM Entity must be liable for.
42 CFR 414.1415(c)(3)(i)(B)."""
notes: str = ""
"""Sourcing caveats — see module docstring "Nominal amount
standard sourcing caveat" for why these values are not directly
restated as bare numbers in the CY2026 NPRM/Final Rule."""
class QppProposed(BaseModel):
"""QPP parameters as *proposed* in a year's PFS NPRM (pre-final).
Mirrors :class:`pfs.rules.ProposedRule` — captures the NPRM's own
stated QPP changes so they can be compared against the eventual
Final Rule disposition. Never itself used for QP/Partial QP
determinations; ``changes`` is the authoritative sourced summary
of what was proposed *and* finalized."""
cms_rule_id: str
"""CMS internal rule identifier, e.g. ``"CMS-1832-P"``."""
federal_register_citation: str
"""Federal Register citation of the NPRM, e.g. ``"90 FR 32352"``."""
changes: str
"""Sourced prose summary of the QPP-relevant proposals in this
NPRM and their Final Rule disposition (finalized as proposed,
finalized with modification, not finalized, or "proposed no
change to X"), with FR page citations for each claim."""
qp_thresholds: QpThresholds | None = None
"""Proposed QP thresholds, only if the NPRM proposed a numeric
change from the current-law values on the enclosing ``QppYear``."""
partial_qp_thresholds: QpThresholds | None = None
"""Proposed Partial QP thresholds, only if the NPRM proposed a
numeric change."""
risk_standards: RiskStandards | None = None
"""Proposed nominal amount standards, only if the NPRM proposed a
numeric change."""
class QppYear(BaseModel):
"""QP / Advanced-APM parameters for a single QP Performance Period.
Keyed in :data:`QPP` by *performance year*; use
:func:`for_payment_year` to look up by the payment year that
performance period's determinations affect (``performance_year +
2``)."""
performance_year: int
"""The QP Performance Period (calendar year in which Advanced-APM
participation is measured)."""
payment_year: int
"""The payment year affected by this performance period's QP
determinations — always ``performance_year + 2``."""
qp_thresholds: QpThresholds
"""Current-law QP determination thresholds for this performance
period (Medicare Option, generally applicable — section
1833(z)(2) of the Act)."""
partial_qp_thresholds: QpThresholds
"""Current-law Partial QP determination thresholds for this
performance period."""
risk_standards: RiskStandards
"""Current-law generally applicable nominal amount standard for
this performance period."""
apm_incentive_pct: float | None
"""Lump-sum APM Incentive Payment, as a % of the QP's base-year
(payment_year - 1) paid claims for covered professional services —
``None`` once the statutory sunset takes effect (payment years
2027+, i.e. performance years 2025+)."""
qp_cf_applies: bool
"""``True`` once the payment year for this performance period uses
the differential QP vs. non-QP conversion factor (payment year
2026 onward, i.e. performance year 2024 onward)."""
cehrt_required: bool = True
"""Whether an Advanced APM must require its participants to use
Certified EHR Technology (CEHRT) — 42 CFR 414.1415(a)."""
citation: str = ""
"""Federal Register citation this entry's current-law values were
transcribed from."""
proposed: QppProposed | None = None
"""NPRM (proposed-rule) QPP changes for this performance period,
when captured. Informational only — never used in place of the
fields above."""
# ── Annual QPP registry ──────────────────────────────────────────
#
# QP / Partial QP thresholds: the statutory schedule (section
# 1833(z)(2) of the Act) raised the Medicare-option thresholds to 75%
# (payment amount) / 50% (patient count) effective QP Performance
# Period 2023, with Partial QP thresholds set at the prior period's
# QP levels — 50% / 35%. Transcribed from data/fr_downloads/
# 2025-13271.txt (NPRM) and cross-checked against data/fr_downloads/
# 2025-19787.txt (Final Rule):
#
# - NPRM 90 FR 32772 (line 36531-36537): "...the QP thresholds have
# increased relative to previous years and, effective with the
# 2025 QP Performance Period are now at 75 percent for the
# payment amount and 50 percent for the patient count."
# - NPRM 90 FR 32819 (line 40647-40656, Regulatory Impact Analysis):
# "the thresholds to achieve QP status beginning in the 2026 QP
# Performance Period ... will increase from 50 percent to 75
# percent for the payment amount, and from 35 percent to 50
# percent for the patient count." This RIA sentence is internally
# inconsistent with the 90 FR 32772 recital above (both appear in
# the same NPRM) — it reads as stale boilerplate describing the
# pre-2023 -> 2023 statutory step-up, not a change CMS-1832-P
# itself proposes.
# - Final Rule 90 FR 49928 (line 69796-69798) drops the "increase
# from" framing entirely: "The thresholds to achieve QP status in
# the 2026 QP Performance Period (2028 payment year) are set to
# 75 percent for the payment amount, and 50 percent for the
# patient count" — consistent with 75/50 being the unchanged,
# already-current value, not a new increase.
# - Final Rule 90 FR 49928 (line 65021-65026): "...if the proposed
# policy would result in an ACO's payment amount score increasing
# from 48 percent to 51 percent, that would cross the 50 percent
# Partial QP threshold" — confirms Partial QP payment-amount
# threshold = 50%. The Partial QP patient-count threshold (35%) is
# not independently restated as a bare number in either document;
# it is inferred from the same NPRM RIA sentence's "35 percent"
# figure (the pre-2023 QP patient-count digit, which by the
# statute's step-down design becomes the post-2023 Partial QP
# digit) — see module docstring caveat.
#
# Risk standards: see module docstring "Nominal amount standard
# sourcing caveat" — 8% revenue-based / 3% benchmark-based, unchanged
# across 2023-2026 (no proposed or finalized change found in either
# CY2026 document).
#
# APM incentive %% and qp_cf_applies: transcribed from NPRM 90 FR
# 32818 / Final Rule 90 FR 49926-49927 ("b. APM Incentive Payments to
# QPs in Advanced APMs and Other Payer Advanced APMs"), and the
# sunset statement at NPRM 90 FR 32696 / Final Rule 90 FR 49838
# ("Under current law, payment year 2026 is the last year for these
# payments.").
_RISK_NOTES = (
"Revenue-based 8% per CY2019 PFS Final Rule (83 FR 59939, "
"data/fr_downloads/2018-24170.txt) 'for QP Performance Periods "
"2021 through 2024'; not extended or amended in either CY2026 "
"document (no proposed/final change found) — carried forward. "
"Benchmark-based 3% not stated as a bare number for 42 CFR "
"414.1415(c)(3)(i)(B) in this repository's FR corpus; carried "
"over from the parallel Other Payer Advanced APM standard at 42 "
"CFR 414.1420(d)(3)(i) (same 83 FR 59939 document)."
)
def _risk_standards() -> RiskStandards:
return RiskStandards(
revenue_nominal_pct=8.0,
benchmark_nominal_pct=3.0,
notes=_RISK_NOTES,
)
def _qp_thresholds() -> QpThresholds:
return QpThresholds(payment_amount_pct=75.0, patient_count_pct=50.0)
def _partial_qp_thresholds() -> QpThresholds:
return QpThresholds(payment_amount_pct=50.0, patient_count_pct=35.0)
QPP: dict[int, QppYear] = {
2023: QppYear(
performance_year=2023,
payment_year=2025,
qp_thresholds=_qp_thresholds(),
partial_qp_thresholds=_partial_qp_thresholds(),
risk_standards=_risk_standards(),
apm_incentive_pct=3.5,
qp_cf_applies=False,
citation="90 FR 49926-49927",
),
2024: QppYear(
# Payment year 2026 is the first year with a differential
# QP/non-QP conversion factor (section 1848(d)(20) of the
# Act) AND still receives the (final year of) lump-sum APM
# Incentive Payment — see NPRM 90 FR 32818, Final Rule 90 FR
# 49926-49927.
performance_year=2024,
payment_year=2026,
qp_thresholds=_qp_thresholds(),
partial_qp_thresholds=_partial_qp_thresholds(),
risk_standards=_risk_standards(),
apm_incentive_pct=1.88,
qp_cf_applies=True,
citation="90 FR 49926-49927",
),
2025: QppYear(
# Under current law, payment year 2026 (performance year
# 2024) was the last year with a lump-sum APM Incentive
# Payment — NPRM 90 FR 32696, Final Rule 90 FR 49838.
performance_year=2025,
payment_year=2027,
qp_thresholds=_qp_thresholds(),
partial_qp_thresholds=_partial_qp_thresholds(),
risk_standards=_risk_standards(),
apm_incentive_pct=None,
qp_cf_applies=True,
citation="90 FR 49838",
),
2026: QppYear(
performance_year=2026,
payment_year=2028,
qp_thresholds=_qp_thresholds(),
partial_qp_thresholds=_partial_qp_thresholds(),
risk_standards=_risk_standards(),
apm_incentive_pct=None,
qp_cf_applies=True,
citation="90 FR 49928",
# NPRM proposals per :pincite:`2KVJ2HKX` — CMS-1832-P, 90 FR
# 32352, transcribed from data/fr_downloads/2025-13271.txt;
# disposition from data/fr_downloads/2025-19787.txt.
proposed=QppProposed(
cms_rule_id="CMS-1832-P",
federal_register_citation="90 FR 32352",
changes=(
"Individual-level QP determination: proposed a new "
"Threshold Score calculation at the individual "
"eligible-clinician level (Sec. 414.1425(b)(3)) for "
"all Advanced APM participants beginning with the "
"2026 QP Performance Period, alongside the existing "
"APM Entity-level calculation (NPRM 90 FR 32771). "
"Finalized as proposed (Final Rule 90 FR 49923-49928, "
"49980). "
"Attribution-eligible beneficiary definition: "
"proposed to re-expand the sixth criterion at Sec. "
"414.1305 to include any beneficiary with a claim for "
"any covered professional service (not just E/M "
"services), beginning with the 2026 QP Performance "
"Period (NPRM 90 FR 32773) — a re-proposal of a CY2025 "
"proposal CMS did not finalize. Finalized WITH "
"MODIFICATION: CMS adopted a dual methodology that "
"continues to use E/M services alongside a covered- "
"professional-services methodology, rather than "
"replacing E/M outright (Final Rule 90 FR 49980). "
"Medical Home Model 50-eligible-clinician limit: "
"proposed to sunset Sec. 414.1415(c)(7) (and the "
"parallel Sec. 414.1420(d)(8) for Aligned Other Payer/"
"Medicaid Medical Home Models) so the 50-clinician cap "
"applies only through the 2025 QP Performance Period, "
"beginning with the 2026 QP Performance Period (NPRM "
"90 FR 32773). Finalized as proposed (Final Rule 90 FR "
"49929: 'finalizing this policy as proposed'). "
"QP/Partial QP thresholds: proposed no numeric change "
"— thresholds remain 75%/50% (QP) and 50%/35% "
"(Partial QP); see QPP[2026].qp_thresholds and the "
"module docstring re: the NPRM's internally "
"inconsistent RIA framing, corrected in the Final "
"Rule (NPRM 90 FR 32772 vs 90 FR 32819; Final Rule 90 "
"FR 49928). "
"Nominal amount standards (42 CFR "
"414.1415(c)(3)(i)(A)/(B)) and the CEHRT requirement "
"(Sec. 414.1415(a)(i)): proposed no change — neither "
"document restates or amends these provisions beyond "
"cross-referencing the section numbers (NPRM/Final "
"26819-26821 and 50693-50696 respectively)."
),
),
),
}
def for_payment_year(year: int) -> QppYear:
"""Return the :class:`QppYear` whose ``payment_year`` matches.
Raises ``KeyError`` if no performance period in :data:`QPP` maps
to this payment year."""
try:
return next(q for q in QPP.values() if q.payment_year == year)
except StopIteration:
raise KeyError(year) from None

0
tests/qpp/__init__.py Normal file
View File

231
tests/qpp/test_rules.py Normal file
View File

@@ -0,0 +1,231 @@
"""Tests for qpp — QP / Advanced-APM registry.
Covers src/qpp/__init__.py:
- QpThresholds / RiskStandards / QppProposed model construction
- QppYear model construction with required and default fields
- QPP dict: performance years 2023-2026, payment_year = performance_year + 2
- qp_cf_applies: False before performance year 2024, True from 2024 onward
- Every entry has a non-empty citation
- QPP[2026].proposed is CMS-1832-P
- for_payment_year(year) lookup, including KeyError on miss
- Exact-value asserts for transcribed thresholds (see task-6-report.md)
"""
from __future__ import annotations
import pytest
from qpp import QPP, QppProposed, QppYear, QpThresholds, RiskStandards, for_payment_year
# ── QpThresholds model ───────────────────────────────────────────
class TestQpThresholds:
def test_construction(self):
t = QpThresholds(payment_amount_pct=75.0, patient_count_pct=50.0)
assert t.payment_amount_pct == 75.0
assert t.patient_count_pct == 50.0
# ── RiskStandards model ──────────────────────────────────────────
class TestRiskStandards:
def test_construction(self):
r = RiskStandards(revenue_nominal_pct=8.0, benchmark_nominal_pct=3.0)
assert r.revenue_nominal_pct == 8.0
assert r.benchmark_nominal_pct == 3.0
def test_default_notes(self):
r = RiskStandards(revenue_nominal_pct=8.0, benchmark_nominal_pct=3.0)
assert r.notes == ""
# ── QppProposed model ────────────────────────────────────────────
class TestQppProposed:
def test_required_fields(self):
p = QppProposed(
cms_rule_id="CMS-0000-P",
federal_register_citation="99 FR 1",
changes="proposed no change to anything",
)
assert p.cms_rule_id == "CMS-0000-P"
assert p.changes == "proposed no change to anything"
def test_default_optional_blocks(self):
p = QppProposed(
cms_rule_id="CMS-0000-P",
federal_register_citation="99 FR 1",
changes="no change",
)
assert p.qp_thresholds is None
assert p.partial_qp_thresholds is None
assert p.risk_standards is None
# ── QppYear model ─────────────────────────────────────────────────
class TestQppYear:
def _minimal_kwargs(self):
return dict(
performance_year=2099,
payment_year=2101,
qp_thresholds=QpThresholds(payment_amount_pct=75.0, patient_count_pct=50.0),
partial_qp_thresholds=QpThresholds(
payment_amount_pct=50.0, patient_count_pct=35.0
),
risk_standards=RiskStandards(
revenue_nominal_pct=8.0, benchmark_nominal_pct=3.0
),
apm_incentive_pct=None,
qp_cf_applies=True,
)
def test_required_fields(self):
y = QppYear(**self._minimal_kwargs())
assert y.performance_year == 2099
assert y.payment_year == 2101
def test_default_cehrt_required(self):
y = QppYear(**self._minimal_kwargs())
assert y.cehrt_required is True
def test_default_citation(self):
y = QppYear(**self._minimal_kwargs())
assert y.citation == ""
def test_default_proposed(self):
y = QppYear(**self._minimal_kwargs())
assert y.proposed is None
# ── QPP dict structure ───────────────────────────────────────────
class TestQppDict:
EXPECTED_YEARS = [2023, 2024, 2025, 2026]
def test_count(self):
assert len(QPP) == 4
def test_keys(self):
assert sorted(QPP.keys()) == self.EXPECTED_YEARS
@pytest.mark.parametrize("year", EXPECTED_YEARS)
def test_values_are_qppyear(self, year):
assert isinstance(QPP[year], QppYear)
@pytest.mark.parametrize("year", EXPECTED_YEARS)
def test_performance_year_field_matches_key(self, year):
assert QPP[year].performance_year == year
@pytest.mark.parametrize("year", EXPECTED_YEARS)
def test_payment_year_is_performance_year_plus_2(self, year):
assert QPP[year].payment_year == year + 2
@pytest.mark.parametrize("year", EXPECTED_YEARS)
def test_citation_non_empty(self, year):
assert QPP[year].citation != ""
class TestQpCfApplies:
"""payment year 2026 (performance year 2024) is the first split-CF year."""
def test_2023_false(self):
assert QPP[2023].qp_cf_applies is False
def test_2024_true(self):
assert QPP[2024].qp_cf_applies is True
def test_2025_true(self):
assert QPP[2025].qp_cf_applies is True
def test_2026_true(self):
assert QPP[2026].qp_cf_applies is True
class TestApmIncentivePct:
"""Lump-sum APM Incentive Payment sunsets after payment year 2026."""
def test_2023_incentive(self):
assert QPP[2023].apm_incentive_pct == 3.5
def test_2024_incentive(self):
assert QPP[2024].apm_incentive_pct == 1.88
def test_2025_no_incentive(self):
assert QPP[2025].apm_incentive_pct is None
def test_2026_no_incentive(self):
assert QPP[2026].apm_incentive_pct is None
# ── QPP[2026].proposed — CMS-1832-P ──────────────────────────────
class TestProposed:
def test_2026_has_proposed(self):
p = QPP[2026].proposed
assert p is not None
assert p.cms_rule_id == "CMS-1832-P"
assert p.federal_register_citation == "90 FR 32352"
def test_2026_changes_non_empty(self):
assert len(QPP[2026].proposed.changes) > 0
def test_earlier_years_have_no_proposed_block(self):
assert QPP[2023].proposed is None
assert QPP[2024].proposed is None
assert QPP[2025].proposed is None
# ── Exact transcribed threshold values ───────────────────────────
class TestSpecificValues:
"""Verify transcribed QP/Partial QP thresholds and risk standards.
See task-6-report.md transcription table for sourcing per value."""
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
def test_qp_thresholds(self, year):
assert QPP[year].qp_thresholds.payment_amount_pct == 75.0
assert QPP[year].qp_thresholds.patient_count_pct == 50.0
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
def test_partial_qp_thresholds(self, year):
assert QPP[year].partial_qp_thresholds.payment_amount_pct == 50.0
assert QPP[year].partial_qp_thresholds.patient_count_pct == 35.0
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
def test_risk_standards(self, year):
assert QPP[year].risk_standards.revenue_nominal_pct == 8.0
assert QPP[year].risk_standards.benchmark_nominal_pct == 3.0
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
def test_cehrt_required(self, year):
assert QPP[year].cehrt_required is True
# ── for_payment_year lookup ──────────────────────────────────────
class TestForPaymentYear:
def test_2025_maps_to_performance_2023(self):
assert for_payment_year(2025) is QPP[2023]
def test_2026_maps_to_performance_2024(self):
assert for_payment_year(2026) is QPP[2024]
def test_2027_maps_to_performance_2025(self):
assert for_payment_year(2027) is QPP[2025]
def test_2028_maps_to_performance_2026(self):
assert for_payment_year(2028) is QPP[2026]
def test_unknown_payment_year_raises_keyerror(self):
with pytest.raises(KeyError):
for_payment_year(1999)

10
uv.lock generated
View File

@@ -4083,6 +4083,11 @@ prisma = [
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "resend" }, { name = "resend" },
] ]
qpp = [
{ name = "duckdb" },
{ name = "httpx" },
{ name = "pydantic" },
]
rec = [ rec = [
{ name = "duckdb" }, { name = "duckdb" },
{ name = "fsspec" }, { name = "fsspec" },
@@ -4182,6 +4187,7 @@ requires-dist = [
{ name = "pydantic", marker = "extra == 'ccw'", specifier = ">=2.0.0" }, { name = "pydantic", marker = "extra == 'ccw'", specifier = ">=2.0.0" },
{ name = "pydantic", marker = "extra == 'cms'", specifier = ">=2.0.0" }, { name = "pydantic", marker = "extra == 'cms'", specifier = ">=2.0.0" },
{ name = "pydantic", marker = "extra == 'conf'", specifier = ">=2.0.0" }, { name = "pydantic", marker = "extra == 'conf'", specifier = ">=2.0.0" },
{ name = "pydantic", marker = "extra == 'qpp'", specifier = ">=2.0.0" },
{ name = "pydo", marker = "extra == 'mail'", specifier = ">=0.29.0" }, { name = "pydo", marker = "extra == 'mail'", specifier = ">=0.29.0" },
{ name = "pydo", marker = "extra == 'prisma'", specifier = ">=0.29.0" }, { name = "pydo", marker = "extra == 'prisma'", specifier = ">=0.29.0" },
{ name = "pyjwt", marker = "extra == 'api'", specifier = ">=2.13.0" }, { name = "pyjwt", marker = "extra == 'api'", specifier = ">=2.13.0" },
@@ -4216,6 +4222,7 @@ requires-dist = [
{ name = "stack", extras = ["conf"], marker = "extra == 'perf'" }, { name = "stack", extras = ["conf"], marker = "extra == 'perf'" },
{ name = "stack", extras = ["conf"], marker = "extra == 'pfs'" }, { name = "stack", extras = ["conf"], marker = "extra == 'pfs'" },
{ name = "stack", extras = ["conf"], marker = "extra == 'prisma'" }, { name = "stack", extras = ["conf"], marker = "extra == 'prisma'" },
{ name = "stack", extras = ["conf"], marker = "extra == 'qpp'" },
{ name = "stack", extras = ["conf"], marker = "extra == 'rec'" }, { name = "stack", extras = ["conf"], marker = "extra == 'rec'" },
{ name = "stack", extras = ["conf"], marker = "extra == 'rex'" }, { name = "stack", extras = ["conf"], marker = "extra == 'rex'" },
{ name = "stack", extras = ["lake"], marker = "extra == 'all'" }, { name = "stack", extras = ["lake"], marker = "extra == 'all'" },
@@ -4225,6 +4232,7 @@ requires-dist = [
{ name = "stack", extras = ["perf"], marker = "extra == 'all'" }, { name = "stack", extras = ["perf"], marker = "extra == 'all'" },
{ name = "stack", extras = ["pfs"], marker = "extra == 'all'" }, { name = "stack", extras = ["pfs"], marker = "extra == 'all'" },
{ name = "stack", extras = ["pfs"], marker = "extra == 'rec'" }, { name = "stack", extras = ["pfs"], marker = "extra == 'rec'" },
{ name = "stack", extras = ["qpp"], marker = "extra == 'all'" },
{ name = "stack", extras = ["rec"], marker = "extra == 'all'" }, { name = "stack", extras = ["rec"], marker = "extra == 'all'" },
{ name = "stack", extras = ["rex"], marker = "extra == 'all'" }, { name = "stack", extras = ["rex"], marker = "extra == 'all'" },
{ name = "stack", extras = ["rex"], marker = "extra == 'opps'" }, { name = "stack", extras = ["rex"], marker = "extra == 'opps'" },
@@ -4236,7 +4244,7 @@ requires-dist = [
{ name = "uvicorn", marker = "extra == 'llm'", specifier = ">=0.41.0" }, { name = "uvicorn", marker = "extra == 'llm'", specifier = ">=0.41.0" },
{ name = "xlrd", specifier = ">=2.0.2" }, { name = "xlrd", specifier = ">=2.0.2" },
] ]
provides-extras = ["conf", "aco", "api", "bcda", "bib", "bls", "llm", "ccw", "cli", "mail", "cms", "opps", "pfs", "rec", "rex", "prisma", "perf", "sem", "lake", "aws", "gcp", "azure", "all"] provides-extras = ["conf", "aco", "api", "bcda", "bib", "bls", "llm", "ccw", "cli", "mail", "cms", "opps", "pfs", "qpp", "rec", "rex", "prisma", "perf", "sem", "lake", "aws", "gcp", "azure", "all"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [