feat(qpp): QP/Advanced-APM registry incl. CMS-1832-P proposed changes (refs #593)
This commit is contained in:
@@ -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
407
src/qpp/__init__.py
Normal 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
0
tests/qpp/__init__.py
Normal file
231
tests/qpp/test_rules.py
Normal file
231
tests/qpp/test_rules.py
Normal 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
10
uv.lock
generated
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user