From 5eb34c99130a243c9e5eac30b3397c1e3f96b23a Mon Sep 17 00:00:00 2001 From: John Manack Date: Tue, 26 Aug 2025 11:24:02 -0400 Subject: [PATCH 1/4] feat: QPPA-10471 adds updates for PY25 changes --- .../components/guides/advanced-tutorial.tsx | 12 +-- src/app/components/guides/basic-tutorial.tsx | 19 ++--- src/app/components/references/benchmarks.tsx | 10 +-- src/app/components/references/data.ts | 27 +------ src/app/components/references/error-codes.tsx | 5 +- .../references/measurement-sets.tsx | 15 +--- src/app/components/references/references.tsx | 9 +-- src/app/components/references/scoring.tsx | 73 +++++++------------ .../frequently-asked-questions.tsx | 10 +-- src/app/components/topics/announcements.tsx | 10 +-- src/app/components/topics/change-log.tsx | 18 ++++- 11 files changed, 78 insertions(+), 130 deletions(-) diff --git a/src/app/components/guides/advanced-tutorial.tsx b/src/app/components/guides/advanced-tutorial.tsx index 47299de1..c599bfa1 100644 --- a/src/app/components/guides/advanced-tutorial.tsx +++ b/src/app/components/guides/advanced-tutorial.tsx @@ -1,5 +1,4 @@ -import { CodeTab, LinkToId, ExternalLink, ApiExample } from '../../../shared'; -import { steps, apiExamples } from './data'; +import { LinkToId, ExternalLink } from '../../../shared'; import envConfig from '../../../envConfig'; import '../../../styles/components/tutorial.scss'; @@ -8,7 +7,7 @@ import { DocPageProps } from '../../../shared/types'; const AdvancedTutorial: React.FC = ({dataTestId}: DocPageProps) => { return (
-

Last Updated: 08/28/2024

{/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

Last Updated: 08/28/2025

{/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

Tutorial: Add and update data via API

In the we covered how to create a measurement set and retrieve the score in two different API requests. We're now going to build on the previous tutorial by adding another measure to the measurement set we created in the previous tutorial. All of these examples serve to illustrate how the Submissions API can make it easier to react to and fix issues that arise. @@ -16,10 +15,11 @@ const AdvancedTutorial: React.FC = ({dataTestId}: DocPageProps) =>

Add more measures to an existing measurement set

- Here's a PATCH request to add more measures to an existing measurement set. You can pass an Accept header to specify the API version and the desired response format by using our custom mime type, application/vnd.qpp.cms.gov.v1+json. We support JSON by using +json. You can also continue to use a standard application/json header, which will point to the latest version. Check out the response and request below! + Here's a PATCH request to add more measures to an existing measurement set. You can pass an Accept header to specify the API version and the desired response format by using our custom mime type, application/vnd.qpp.cms.gov.v1+json. We support JSON by using +json. You can also continue to use a standard application/json header, which will point to the latest version. +

+

+ To see an example request and response, please see the PATCH /measurement-sets/:id section within the .

- -

Next steps

diff --git a/src/app/components/guides/basic-tutorial.tsx b/src/app/components/guides/basic-tutorial.tsx index 8a5d1c24..3ef2c079 100644 --- a/src/app/components/guides/basic-tutorial.tsx +++ b/src/app/components/guides/basic-tutorial.tsx @@ -1,12 +1,11 @@ -import { ExternalLink, CodeTab, ApiExample, LinkToId } from '../../../shared'; +import { ExternalLink, LinkToId } from '../../../shared'; import envConfig from '../../../envConfig'; -import { steps, apiExamples } from './data'; import { DocPageProps } from '../../../shared/types'; const BasicTutorial: React.FC = ({dataTestId}: DocPageProps) => { return (

-

Last Updated: 08/28/2024

{/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

Last Updated: 08/28/2025

{/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

Tutorial: Create and Score Data via API

The Submissions API is an easy way to manage your performance data with CMS. Performance data is organized into submissions, which can have many measurements. Measurements within a submission are also grouped by category (e.g. quality) and submission method (e.g. registry) and program name (e.g. mips) into measurement sets. @@ -26,26 +25,22 @@ const BasicTutorial: React.FC = ({dataTestId}: DocPageProps) => { When submitting, you can pass an Accept header to specify the API version and type of response back by using our custom mime type, application/vnd.qpp.cms.gov.v1+json. We support JSON by using +json. Right now, only version v1 is supported. You can also continue to use a standard application/json header, which will point to the latest version.

- We'll also need to supply some information to tell CMS how to identify this particular submission, which you can see below. Every submission is unique to the combination of the fields provided. Note that we enforce fake TINs (starting with 000) in the Developer Preview Testing Environment to avoid accidentally collecting personally identifiable information. + We'll also need to supply some information to tell CMS how to identify this particular submission. Every submission is unique to the combination of the fields provided. Note that we enforce fake TINs (starting with 000) in the Developer Preview Testing Environment to avoid accidentally collecting personally identifiable information.

- Take a look at the request below, and then click the 'Response' tab to see what the API returns when we submit this request! + To see an example request and response, please see the POST /measurement-sets section within the .

- -

Scoring a submission

With the submission id we were given, we can ask the API to calculate the submission score with a GET request. We don't need to include a request body this time since we're only interested in retrieving the score, and CMS doesn't need any information other than the submission id.

- - -

- In general, we can think about the Submissions API as a way to have a live conversation with CMS about performance measurements. Rather than waiting months to hear back about missing information or a score, the API gives us feedback that is immediate, specific, and actionable - we can easily make another API request if necessary. + To see an example request and response, please see the GET /submissions/:id/score section within the .

+

- What we've shown is an example of working directly with the API - typically these requests are made through a web interface or script, but the requests & responses above illustrate the kind of power and speed the Submissions API and applications built against it can provide. + In general, we can think about the Submissions API as a way to have a live conversation with CMS about performance measurements. Rather than waiting months to hear back about missing information or a score, the API gives us feedback that is immediate, specific, and actionable - we can easily make another API request if necessary.

Disclaimer: Scoring is subject to change, based on periodic policy updates, eligibility reviews, and technical integration developments. diff --git a/src/app/components/references/benchmarks.tsx b/src/app/components/references/benchmarks.tsx index 371e804b..d1695908 100644 --- a/src/app/components/references/benchmarks.tsx +++ b/src/app/components/references/benchmarks.tsx @@ -7,17 +7,17 @@ import { DocPageProps } from '../../../shared/types'; const Benchmarks: React.FC = ({dataTestId}: DocPageProps) => { return (

-

Last Updated: 08/28/2024

{/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

Last Updated: 08/28/2025

{/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

Benchmarks

- Benchmarks serve as the reference points for measurements and are used to score submissions. Each benchmark is unique based upon its combination of measureId, submissionMethod, and performanceYear, and each has a list of 9 data points. + Benchmarks serve as the reference points for measurements and are used to score submissions. Each benchmark is unique based upon its combination of measureId, submissionMethod, and performanceYear, and each has a list of 11 data points.

- Please see the for more information on MIPS Quality Benchmarks. + Please see the for more information on MIPS Quality Benchmarks.

- You can view the benchmarks for a Performance Year using the publicly accessible . + You can view the benchmarks for a Performance Year using the publicly accessible .


diff --git a/src/app/components/references/data.ts b/src/app/components/references/data.ts index 83f86f51..ff8f0e2e 100644 --- a/src/app/components/references/data.ts +++ b/src/app/components/references/data.ts @@ -19,10 +19,6 @@ interface IScoringData { [k: string]: ICodeTab[]; } -export const measurementSetPracticeDetails = { - 'Practice Details': 'practice-details', -}; - export const measurementsTitleAndId = { 'Boolean': 'boolean-measurements', 'Proportion': 'proportion-measurements', @@ -329,18 +325,13 @@ export const measurementSetsFields: IFields = { { name: 'updatedAt', value: 'datetime', description: 'The modification time of the measurement set in RFC 3339 format.', notes: ' ' }, { name: 'submissionId', value: 'string', description: 'The id of the submission in which the measurement set belongs.', notes: ' ' }, { name: 'category', value: 'string', description: 'The category of the measurement set. Acceptable values are:
  • "quality"
  • "pi"
  • "ia"
  • ', notes: 'writable, required' }, - { name: 'cehrtId', value: 'string', description: 'The CMS EHR Certification Identification Number is generated by the CHPL. This is only applicable to Promoting Interoperability measurement sets.', notes: 'writable, required' }, + { name: 'cehrtId', value: 'string', description: 'The CMS EHR Certification Identification Number is generated by the CHPL. This is applicable to Promoting Interoperability and eCQM Quality measurement sets. CEHRT ID must be in the format 2025CXXXXXXXXXX or XX15CXXXXXXXXXX.', notes: 'writable, required' }, { name: 'submissionMethod', value: 'string', description: 'The method by which the measurement set data was submitted. Acceptable values are:
    • Quality Category:
      • "registry" for MIPS CQMs reporting
      • "electronicHealthRecord" for eCQM Reporting
    • Promoting Interoperability and Improvement Activities:
      • "registry" for non QRDA format
      • "electronicHealthRecord" for QRDA
    ', notes: 'writable, required' }, - { name: 'programName', value: 'string', description: 'The quality payment program under which the measurementSet should be scored. Acceptable values are:
  • "mips" for Traditional MIPS Reporting
  • "app1" for the APM Performance Pathway
  • "MVP ID" more information on the IDs can be found at the QPP Resource Library
  • "pcf" for PCF Program submissions
  • If not provided, the programName will be recorded as "mips".', notes: 'writable, optional' }, - { name: 'practiceDetails', value: 'object', description: `This object contains the taxpayerIdentificationNumber and/or nationalProviderIdentifiers of the practice associated with the measurement set. Optional if programName is set to "pcf". Must be omitted if programName is not set to "pcf". More details below.`, notes: 'writeable, optional'}, + { name: 'programName', value: 'string', description: 'The quality payment program under which the measurementSet should be scored. Acceptable values are:
  • "mips" for Traditional MIPS Reporting
  • "app1" for the legacy APM Performance Pathway
  • "appPlus" for the APM Performance Pathway Plus Quality Measure Set
  • "MVP ID" more information on the IDs can be found at the QPP Resource Library
  • If not provided, the programName will be recorded as "mips".', notes: 'writable, optional' }, { name: 'performanceStart', value: 'string', description: 'A date in RFC 3339 format with only the date part (for instance, "2013-01-15"). The first date when the measurement data is applicable.', notes: 'writable, required' }, { name: 'performanceEnd', value: 'string', description: 'A date in RFC 3339 format with only the date part (for instance, "2013-01-15"). The last date when the measurement data is applicable.', notes: 'writable, required' }, { name: 'measurements', value: 'Array(measurements)', description: 'Measurements associated with the measurement set.', notes: 'writable, optional' }, ], - practiceDetails: [ - { name: 'taxpayerIdentificationNumber', value: 'string', description: 'The 9-digit identifier of the practice associated with the measurementSet. ', notes: 'writeable, optional'}, - { name: 'nationalProviderIdentifiers', value: 'string[]', description: 'An array of strings containing the 10-digit identifiers of the practice associated with the measurementSet, separated by commas.', notes: 'writeable, optional'}, - ], }; export const measurementSetsTabs: ITabs = { @@ -356,19 +347,9 @@ export const measurementSetsTabs: ITabs = { "cehrtId": string, "submissionMethod": string, "programName": string, - "practiceDetails": object(Practice Details), "performanceStart": date, "performanceEnd": date, "measurements": array(Measurements Resource) -}`, - }, - ], - practiceDetails: [ - { - tab: 'Sample JSON', - code: `{ - "taxpayerIdentificationNumber": string, - "nationalProviderIdentifiers": string[] }`, }, ], @@ -428,8 +409,8 @@ export const benchmarksTabs: ITabs = { { tab: 'Sample JSON', code: `{ - "benchmarkYear": 2021, - "performanceYear": 2024, + "benchmarkYear": 2023, + "performanceYear": 2025, "metricType": "singlePerformanceRate", "status": "historical", "isInverse": true, diff --git a/src/app/components/references/error-codes.tsx b/src/app/components/references/error-codes.tsx index 4bee889b..e5ef9061 100644 --- a/src/app/components/references/error-codes.tsx +++ b/src/app/components/references/error-codes.tsx @@ -1,13 +1,14 @@ +import envConfig from '../../../envConfig'; import { ExternalLink } from '../../../shared'; import { DocPageProps } from '../../../shared/types'; const ErrorCodes: React.FC = ({dataTestId}: DocPageProps) => { return (
    -

    Last Updated: 08/31/2022

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Error Codes

    -

    The Submissions API Error Codes summary can be found in the

    +

    To review a list of response codes provided by the API, please visit the .

    ); }; diff --git a/src/app/components/references/measurement-sets.tsx b/src/app/components/references/measurement-sets.tsx index 8ace0e76..827f3f5a 100644 --- a/src/app/components/references/measurement-sets.tsx +++ b/src/app/components/references/measurement-sets.tsx @@ -1,12 +1,12 @@ import { ExternalLink, DataModelTable, CodeTab } from '../../../shared'; import envConfig from '../../../envConfig'; -import { measurementSetsFields, measurementSetsTabs, measurementSetPracticeDetails } from './data'; +import { measurementSetsFields, measurementSetsTabs } from './data'; import { DocPageProps } from '../../../shared/types'; const MeasurementSets: React.FC = ({dataTestId}: DocPageProps) => { return (
    -

    Last Updated: 07/06/2023

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Measurement Sets

    The MeasurementSets resource represents performance data for a specified category. Each Submission can have multiple MeasurementSets. Each MeasurementSet in a given Submission is uniquely identified by category, submission method, and programName. MeasurementSets contain Measurements, which can be accessed both via MeasurementSets methods and Measurements methods. @@ -17,17 +17,6 @@ const MeasurementSets: React.FC = ({dataTestId}: DocPageProps) => -

    - Practice Details -

    -

    - The MeasurementSet property practiceDetails is an optional property only available when programName is set to "pcf". -

    - -
    ); }; diff --git a/src/app/components/references/references.tsx b/src/app/components/references/references.tsx index 1a26525e..76615649 100644 --- a/src/app/components/references/references.tsx +++ b/src/app/components/references/references.tsx @@ -5,7 +5,7 @@ import { DocPageProps } from '../../../shared/types'; const References: React.FC = ({dataTestId}: DocPageProps) => { return (
    -

    Last Updated: 05/12/2021

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    References

    General References

    @@ -50,13 +50,6 @@ const References: React.FC = ({dataTestId}: DocPageProps) => { Interactive AUTH API Documentation: - -

    CMS Web Interface API

    -
      -
    • - Interactive Web Interface Documentation: -
    • -
    ); }; diff --git a/src/app/components/references/scoring.tsx b/src/app/components/references/scoring.tsx index f675757b..8db45ec3 100644 --- a/src/app/components/references/scoring.tsx +++ b/src/app/components/references/scoring.tsx @@ -1,5 +1,4 @@ -import { ExternalLink, CodeTab, LinkToId } from '../../../shared'; -import { scoringData } from './data'; +import { ExternalLink, LinkToId } from '../../../shared'; import envConfig from '../../../envConfig'; import { DocPageProps } from '../../../shared/types'; @@ -39,7 +38,7 @@ const textAndId: ITextAndId = { const Scoring: React.FC = ({dataTestId}: DocPageProps) => { return (
    -

    Last Updated: 08/28/2024

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Scoring

    The scoring engine is responsible for interpreting submissions and outputting a score. Each category score is utilized to create the QPP score object. @@ -50,7 +49,7 @@ const Scoring: React.FC = ({dataTestId}: DocPageProps) => {

    Developer Preview Testing Environment

    In the Developer Preview Testing Environment you can use the POST .../score-preview endpoint to view the scoring object returned for the data you are proposing to submit. - The data is not saved. Try the POST .../submissions/score-preview endpoint . + The data is not saved. Try the POST .../submissions/score-preview endpoint .

    Note: You can use the to see the score responses for different types of eligibility profiles. @@ -59,9 +58,8 @@ const Scoring: React.FC = ({dataTestId}: DocPageProps) => {

    Once submitting quality data during the Submission window you can use the GET .../submissions/{id}/score to view the score for the data after it is submitted. This submission data is saved under the submissionId and retrieved to view the score. - Try the GET .../submissions/{id}/score endpoint . + Try the GET .../submissions/{id}/score endpoint .

    -

    Improvement Activities (IA) Scoring

    The only available option for reporting Improvement Activities is boolean, and only Activities completed need to be reported. @@ -72,37 +70,23 @@ const Scoring: React.FC = ({dataTestId}: DocPageProps) => { - -

    Example IA Submission

    +

    Promoting Interoperability (PI) Scoring

    - The example submission below contains 4 activities. The reported activities contain both High and Medium weighted activities. -

    - - - -

    Example Scoring

    -

    - Use the .../submisisons/score-preview endpoint (above) to see the scoring response. + The Promoting Interoperability Category requires all measures associated with the category to either be reported or their corresponding exclusion to be claimed. Additionally, to receive credit for the category, all the criteria below must be fulfilled:

    +
      +
    • Utilization of the required CEHRT and the reporting of the corresponding CMS CEHRT ID in the submission
    • +
    - -
    -

    Promoting Interoperability (PI) Scoring

    - The Promoting Interoperability Category requires all measures associated with the category to either be reported or their corresponding exclusion to be claimed. Additionally, to receive credit for the category, all the criteria below must be fulfilled: + For additional information on reporting Promoting Interoperability, visit the Resource Library for your specific program needs.

      -
    • Utilization of 2015 CEHRT and the reporting of the corresponding CMS CEHRT ID in the submission
    • -
        -
      • XX15CXXXXXXXXXX
      • -
      -
    • Minimum 180 day performance period
    • -
    • Completion of Required Attestation Statements
    • -
    • Completion of All Required Measures
    • -
    • Bonus Measures
    • +
    • +
    • +
    -

    Example PI Submission

    The measure types available for submission are outlined below. Each measure in the repo will dictate which type is to be utilized.

    @@ -114,22 +98,17 @@ const Scoring: React.FC = ({dataTestId}: DocPageProps) => { - - -

    Example Scoring

    -

    - Use the .../submisisons/score-preview endpoint (above) to see the scoring response. -

    - - -

    Quality

    - The Quality category requires 6 measures to receive full credit, one of which must be either an Outcome measure or High Priority. If no Outcome or High Priority measure is submitted, you will only be scored on the top 5 measures and receive a score of 0 for the sixth measure. + For additional information on reporting Quality, visit the for your specific program needs.

    +
      +
    • +
    • +
    • +
    -

    Example Quality Submission

    Submission structure in the Quality category are contingent on the measure being submitted. If there are questions around the data to be submitted in the fields, please refer to the measure specification. The available types related to the measures are outlined below:

    @@ -144,16 +123,14 @@ const Scoring: React.FC = ({dataTestId}: DocPageProps) => { -

    - In the sample below, measure 305 is a multi-strata performance measure, 102 is a single performance measure, ACRAD15 is a non-proportion measure. -

    - - -

    Example Scoring

    -

    - Use the .../submisisons/score-preview endpoint (above) to see the scoring response. +

    + eCQMs require the utilization of CEHRT and the reporting of the corresponding CMS CEHRT ID in the submission

    +
      +
    • 2025CXXXXXXXXXX
    • +
    • XX15CXXXXXXXXXX
    • +
    ); diff --git a/src/app/components/resources-and-support/frequently-asked-questions.tsx b/src/app/components/resources-and-support/frequently-asked-questions.tsx index eaced256..be8fcbf8 100644 --- a/src/app/components/resources-and-support/frequently-asked-questions.tsx +++ b/src/app/components/resources-and-support/frequently-asked-questions.tsx @@ -8,12 +8,9 @@ const tableData = {
  • Submissions UI: EHRs can submit via the Submissions UI by logging into the QPP site qpp.cms.gov. They will have to be connected as a Staff user for each of the Individuals and Groups they need to submit quality data for. Submissions for each individual or group could be uploaded as an excel file for efficiency.
  • -
  • - Burden Reduction: EHRs may apply for Burden Reduction which allows them to obtain a registry token to use to submit via the Submissions API. -
  • `, - 'What is the performance threshold for the Submissions API?': '25 rps avg threshold, 40 rps burst threshold.', + 'What is the performance response threshold for the Submissions API?': '25 rps avg threshold, 40 rps burst threshold.', }; const tableData1 = { @@ -27,13 +24,12 @@ const tableData1 = { `, - 'How do I report SubGroups?': `SubGroups are available for testing in DevPre as of 9/29/23. See the tutorial here QPP Developer Preview Documentation`, }; const FrequentlyAskedQuestions: React.FC = ({dataTestId}: DocPageProps) => { return (
    -

    Last Updated: 08/28/2024

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Frequently Asked Questions

    General and Developer Preview

    @@ -60,7 +56,7 @@ const FrequentlyAskedQuestions: React.FC = ({dataTestId}: DocPageP )} -

    PY24 Questions and Known Issues

    +

    PY25 Questions and Known Issues

    diff --git a/src/app/components/topics/announcements.tsx b/src/app/components/topics/announcements.tsx index f280af79..46c22298 100644 --- a/src/app/components/topics/announcements.tsx +++ b/src/app/components/topics/announcements.tsx @@ -5,7 +5,7 @@ import { DocPageProps } from '../../../shared/types'; const Announcements: React.FC = ({dataTestId}: DocPageProps) => { return (
    -

    Last Updated: 08/28/2024

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Announcements

    General

    @@ -16,10 +16,10 @@ const Announcements: React.FC = ({dataTestId}: DocPageProps) => {

    Submission API

    • - Updates for traditional MIPS submissions and scoring for PY 2024 are now available. + Updates for traditional MIPS submissions and scoring for PY 2025 are now available.
    • - For APM participants only, APP submissions and scoring rules for PY 2024 are now available. + For APM participants only, APP submissions and scoring rules (app1 and appPlus programs) for PY 2025 are now available.
    • The Submissions API has received infastructure updates with minor changes to end users. @@ -32,10 +32,10 @@ const Announcements: React.FC = ({dataTestId}: DocPageProps) => {

      Measures Repository

      • - 2024 Measures are published and available here: + 2025 Measures are published and available here:
      • - 2024 MVP Measures are published and available here: + 2025 MVP Measures are published and available here:
    diff --git a/src/app/components/topics/change-log.tsx b/src/app/components/topics/change-log.tsx index 69749fe8..e900d0e2 100644 --- a/src/app/components/topics/change-log.tsx +++ b/src/app/components/topics/change-log.tsx @@ -2,6 +2,20 @@ import envConfig from '../../../envConfig'; import { DocPageProps } from '../../../shared/types'; const submissionChangesTable = [ + ['08/28/2025', `Updates to Developer Documentation for Performance Year 2025 Description`, ` +
      +
    • Simplified Scenarios for MVP Reporting in Developer Preview. See the Special Scoring Scenarios.
    • +
    • There is now 1 TIN and 1 Subgroup that will work when testing any MVP that is valid for the performance year.
    • +
    • Negative use tests can be completed using any other TIN/Subgroup.
    • +
    • Added support for APP Plus Reporting. See the Special Scoring Scenarios.
    • +
    • Allowed CEHRT ID format change for Promoting Interoperability. See Measurement Sets.
    • +
    • Quality eCQM reporting now requires the inclusion of a CEHRT ID.
    • +
    • Removal of IA weights.
    • +
    • Removal of measureSet property from /measurement-sets endpoints.
    • +
    • Removal of pcf program.
    • +
    • CMS Web Interface has been sunset across QPP.
    • +
    + `], ['08/28/2024', `Updates to Developer Documentation for Performance Year 2024`, `
    • Added new Scenarios for MVP Reporting. See the Special Scoring Scenarios.
    • @@ -186,6 +200,8 @@ const submissionChangesTable = [
    `], ]; const measuresChangeTable = [ + ['3/18/25', `allowedRegistrationTypes has been added to all measures.`, `For all measures, allowedRegistrationTypes has been added to indicate which entity types can submit the measure. Note, as this is intended for use with MVP registration workflows, virtual groups are not listed. Virtual Groups can still report to measures outside of MVPs.`], + ['11/8/24', `isSevenPointCapRemoved and sevenPointCapRemoved have been added to all quality measures.`, `For category: "quality", isSevenPointCapRemoved and sevenPointCapRemoved is added. These fields will indicate when a measure will no longer have the 7-point cap applied but rather scored according to the new topped out measure benchmarks.`], ['08/01/24', `companionMeasureId is added to all quality Measures.`, `For category: "quality", companionMeasureId is added to list the corresponding CQM or Medicare CQM measure id.`], ['8/15/21', `allowedPrograms is added to all quality Measures.`, `For category: "quality", allowedPrograms is added to list the programs to which the measure can be submitted.`], ['8/15/21', `requiredForPrograms is added to all quality Measures.`, `For category: "quality", requiredForPrograms is added to list the programs to which the measure must be submitted.`], @@ -223,7 +239,7 @@ const buildTableBody = (data: string[][]) => const ChangeLog: React.FC = ({dataTestId}: DocPageProps) =>{ return (
    -

    Last Updated: 08/28/2024

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */} +

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Change Log

    The Change log is updated with each significant change to the API with the details that you need to be aware of. From e953b250d6e818766e085fb379f99f275ef487ad Mon Sep 17 00:00:00 2001 From: John Manack Date: Tue, 26 Aug 2025 11:36:50 -0400 Subject: [PATCH 2/4] test: QPPA-10471 increases coverage for unit tests --- src/app/components/app.test.tsx | 586 +++++++++++++++++++++++++++++++- 1 file changed, 585 insertions(+), 1 deletion(-) diff --git a/src/app/components/app.test.tsx b/src/app/components/app.test.tsx index bd3c072e..cad41134 100644 --- a/src/app/components/app.test.tsx +++ b/src/app/components/app.test.tsx @@ -5,7 +5,10 @@ import '@testing-library/jest-dom'; import App from './app'; import LeftNav from './left-nav'; import { combinedRoutes } from '../routes'; -import { ExternalLink } from '../../shared'; +import { ExternalLink, LinkToId } from '../../shared'; +import { ApiExample, IApiExample } from '../../shared/api-example'; +import { CodeTab, ICodeTab } from '../../shared/code-tab'; +import NotFound from './notFound/not-found'; describe('App tests', () => { afterAll(() => { @@ -145,4 +148,585 @@ describe('App tests', () => { ); }); }); + + describe('LinkToId Component', () => { + + beforeEach(() => { + // Mock window.scrollBy since it's not implemented in jsdom + window.scrollBy = jest.fn(); + // Mock scrollIntoView + Element.prototype.scrollIntoView = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render a link with correct href when to prop does not include hash', () => { + const { getByRole } = render( + + + + ); + + const link = getByRole('link', { name: 'Go to Section 1' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/section1#section1'); + }); + + it('should render a link with correct href when to prop includes hash', () => { + const { getByRole } = render( + + + + ); + + const link = getByRole('link', { name: 'Go to Section 2' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/page#section2'); + }); + + it('should apply custom attributes when attrs prop is provided', () => { + const { getByRole } = render( + + + + ); + + const link = getByRole('link', { name: 'Custom Link' }); + expect(link).toBeInTheDocument(); + }); + + it('should use custom offset when provided', () => { + const { getByRole } = render( + + + + ); + + const link = getByRole('link', { name: 'Custom Offset Link' }); + expect(link).toBeInTheDocument(); + + // Simulate clicking the link to test scroll behavior + fireEvent.click(link); + + // The component should call scrollIntoView and then scrollBy with the custom offset + setTimeout(() => { + expect(window.scrollBy).toHaveBeenCalledWith({ top: -50, behavior: 'smooth' }); + }, 0); + }); + + it('should use default offset of 1 when offset is not provided', () => { + const { getByRole } = render( + + + + ); + + const link = getByRole('link', { name: 'Default Offset Link' }); + expect(link).toBeInTheDocument(); + + // Simulate clicking the link to test scroll behavior + fireEvent.click(link); + + // The component should call scrollIntoView and then scrollBy with default offset + setTimeout(() => { + expect(window.scrollBy).toHaveBeenCalledWith({ top: -1, behavior: 'smooth' }); + }, 0); + }); + }); + + describe('ApiExample Component', () => { + + it('should render basic API example with verb and URL', () => { + const apiData: IApiExample = { + verb: 'GET', + url: '/api/submissions' + }; + + const { getByText } = render(); + + expect(getByText('GET')).toBeInTheDocument(); + expect(getByText('/api/submissions')).toBeInTheDocument(); + }); + + it('should render API example with table rows', () => { + const apiData: IApiExample = { + verb: 'POST', + url: '/api/submissions', + rows: [ + { + row: ['Parameter', 'Type', 'Description'], + classes: ['header-class', 'type-class', 'desc-class'] + }, + { + row: ['submissionId', 'string', 'Unique identifier for submission'] + } + ] + }; + + const { getByText, getByRole } = render(); + + expect(getByText('POST')).toBeInTheDocument(); + expect(getByText('/api/submissions')).toBeInTheDocument(); + expect(getByRole('table')).toBeInTheDocument(); + expect(getByText('Parameter')).toBeInTheDocument(); + expect(getByText('Type')).toBeInTheDocument(); + expect(getByText('Description')).toBeInTheDocument(); + expect(getByText('submissionId')).toBeInTheDocument(); + expect(getByText('string')).toBeInTheDocument(); + expect(getByText('Unique identifier for submission')).toBeInTheDocument(); + }); + + it('should apply CSS classes to table cells when provided', () => { + const apiData: IApiExample = { + verb: 'PUT', + url: '/api/submissions/123', + rows: [ + { + row: ['Field', 'Value'], + classes: ['field-class', 'value-class'] + } + ] + }; + + const { container } = render(); + + const cells = container.querySelectorAll('td'); + expect(cells[0]).toHaveClass('field-class'); + expect(cells[1]).toHaveClass('value-class'); + }); + + it('should render HTML content using dangerouslySetInnerHTML when HTML is detected', () => { + const apiData: IApiExample = { + verb: 'DELETE', + url: '/api/submissions/456', + rows: [ + { + row: ['Status', 'Success'] + } + ] + }; + + const { getByText, container } = render(); + + expect(getByText('Status')).toBeInTheDocument(); + // Check that the HTML was rendered as actual HTML, not as text + const strongElement = container.querySelector('strong'); + expect(strongElement).toBeInTheDocument(); + expect(strongElement).toHaveTextContent('Success'); + }); + + it('should render plain text when no HTML is detected', () => { + const apiData: IApiExample = { + verb: 'PATCH', + url: '/api/submissions/789', + rows: [ + { + row: ['Message', 'Plain text message'] + } + ] + }; + + const { getByText, container } = render(); + + expect(getByText('Message')).toBeInTheDocument(); + expect(getByText('Plain text message')).toBeInTheDocument(); + // Ensure no HTML tags were created + expect(container.querySelector('strong')).not.toBeInTheDocument(); + }); + + it('should not render table when rows are not provided', () => { + const apiData: IApiExample = { + verb: 'OPTIONS', + url: '/api/health' + }; + + const { queryByRole } = render(); + + expect(queryByRole('table')).not.toBeInTheDocument(); + }); + + it('should render empty table when rows array is empty', () => { + const apiData: IApiExample = { + verb: 'HEAD', + url: '/api/status', + rows: [] + }; + + const { getByRole, container } = render(); + + expect(getByRole('table')).toBeInTheDocument(); + expect(container.querySelectorAll('tr')).toHaveLength(0); + }); + + it('should handle mixed HTML and plain text in the same row', () => { + const apiData: IApiExample = { + verb: 'GET', + url: '/api/mixed', + rows: [ + { + row: ['Plain text', 'Emphasized text', 'More plain text'] + } + ] + }; + + const { getByText, container } = render(); + + expect(getByText('Plain text')).toBeInTheDocument(); + expect(getByText('More plain text')).toBeInTheDocument(); + + const emElement = container.querySelector('em'); + expect(emElement).toBeInTheDocument(); + expect(emElement).toHaveTextContent('Emphasized text'); + }); + + it('should handle multiple rows with different class configurations', () => { + const apiData: IApiExample = { + verb: 'POST', + url: '/api/complex', + rows: [ + { + row: ['Header 1', 'Header 2'], + classes: ['header-1', 'header-2'] + }, + { + row: ['Data 1', 'Data 2'] + // No classes for this row + }, + { + row: ['Footer 1', 'Footer 2'], + classes: ['footer-1'] // Only one class for two cells + } + ] + }; + + const { container } = render(); + + const rows = container.querySelectorAll('tr'); + expect(rows).toHaveLength(3); + + // First row with classes + const firstRowCells = rows[0].querySelectorAll('td'); + expect(firstRowCells[0]).toHaveClass('header-1'); + expect(firstRowCells[1]).toHaveClass('header-2'); + + // Second row without classes + const secondRowCells = rows[1].querySelectorAll('td'); + expect(secondRowCells[0]).not.toHaveClass(); + expect(secondRowCells[1]).not.toHaveClass(); + + // Third row with partial classes + const thirdRowCells = rows[2].querySelectorAll('td'); + expect(thirdRowCells[0]).toHaveClass('footer-1'); + expect(thirdRowCells[1]).not.toHaveClass(); + }); + + it('should render with proper structure and IDs', () => { + const apiData: IApiExample = { + verb: 'GET', + url: '/api/test' + }; + + const { container } = render(); + + const apiExampleDiv = container.querySelector('#api-example'); + expect(apiExampleDiv).toBeInTheDocument(); + + const verbSpan = container.querySelector('.verb'); + const urlSpan = container.querySelector('.url'); + + expect(verbSpan).toBeInTheDocument(); + expect(urlSpan).toBeInTheDocument(); + + const codeElements = container.querySelectorAll('code'); + expect(codeElements).toHaveLength(2); + expect(codeElements[0]).toHaveTextContent('GET'); + expect(codeElements[1]).toHaveTextContent('/api/test'); + }); + }); + + describe('CodeTab Component', () => { + + it('should render code tabs with default first tab selected', () => { + const codeData: ICodeTab[] = [ + { + tab: 'JavaScript', + code: 'console.log("Hello World");' + }, + { + tab: 'Python', + code: 'print("Hello World")' + } + ]; + + const { getByText, getByRole } = render(); + + expect(getByText('JavaScript')).toBeInTheDocument(); + expect(getByText('Python')).toBeInTheDocument(); + + // First tab should be selected by default + const jsButton = getByRole('button', { name: 'JavaScript' }); + const pyButton = getByRole('button', { name: 'Python' }); + + expect(jsButton).toHaveClass('selected'); + expect(jsButton).toHaveClass('tab-button'); + expect(pyButton).toHaveClass('tab-button'); + expect(pyButton).not.toHaveClass('selected'); + }); + + it('should switch tabs when tab buttons are clicked', () => { + const codeData: ICodeTab[] = [ + { + tab: 'JavaScript', + code: 'console.log("Hello World");' + }, + { + tab: 'Python', + code: 'print("Hello World")' + }, + { + tab: 'Java', + code: 'System.out.println("Hello World");' + } + ]; + + const { getByRole } = render(); + + const jsButton = getByRole('button', { name: 'JavaScript' }); + const pyButton = getByRole('button', { name: 'Python' }); + const javaButton = getByRole('button', { name: 'Java' }); + + // Initially JavaScript should be selected + expect(jsButton).toHaveClass('selected'); + expect(pyButton).not.toHaveClass('selected'); + expect(javaButton).not.toHaveClass('selected'); + + // Click Python tab + fireEvent.click(pyButton); + expect(jsButton).not.toHaveClass('selected'); + expect(pyButton).toHaveClass('selected'); + expect(javaButton).not.toHaveClass('selected'); + + // Click Java tab + fireEvent.click(javaButton); + expect(jsButton).not.toHaveClass('selected'); + expect(pyButton).not.toHaveClass('selected'); + expect(javaButton).toHaveClass('selected'); + }); + + it('should display response section when response is provided', () => { + const codeData: ICodeTab[] = [ + { + tab: 'Request', + code: 'GET /api/data', + response: '200 OK' + }, + { + tab: 'Response', + code: '{"status": "success", "data": []}' + } + ]; + + const { getByText, queryByText, getByRole } = render(); + + // Initially should show Request tab with response + expect(getByText('Request code:')).toBeInTheDocument(); + expect(getByText('200 OK')).toBeInTheDocument(); + expect(getByText('Request body:')).toBeInTheDocument(); + + // Switch to Response tab (no response field) + fireEvent.click(getByRole('button', { name: 'Response' })); + expect(queryByText('Response code:')).not.toBeInTheDocument(); + expect(getByText('Response body:')).toBeInTheDocument(); + }); + + it('should render HTML content using dangerouslySetInnerHTML when HTML is detected', () => { + const codeData: ICodeTab[] = [ + { + tab: 'HTML', + code: '

    Bold text
    ' + } + ]; + + const { container, getByText } = render(); + + expect(getByText('HTML body:')).toBeInTheDocument(); + + // Check that the HTML was rendered as actual HTML elements + const strongElement = container.querySelector('strong'); + expect(strongElement).toBeInTheDocument(); + expect(strongElement).toHaveTextContent('Bold text'); + + const divElement = container.querySelector('div.example'); + expect(divElement).toBeInTheDocument(); + }); + + it('should handle single tab data', () => { + const codeData: ICodeTab[] = [ + { + tab: 'Only Tab', + code: 'const single = "only option";', + response: 'Single response' + } + ]; + + const { getByText, getByRole } = render(); + + const button = getByRole('button', { name: 'Only Tab' }); + expect(button).toHaveClass('selected'); + expect(button).toHaveClass('tab-button'); + + expect(getByText('Only Tab code:')).toBeInTheDocument(); + expect(getByText('Single response')).toBeInTheDocument(); + expect(getByText('Only Tab body:')).toBeInTheDocument(); + }); + + it('should handle mixed HTML and non-HTML tabs', () => { + const codeData: ICodeTab[] = [ + { + tab: 'Plain Code', + code: 'function hello() { return "world"; }' + }, + { + tab: 'HTML Code', + code: 'Hello World' + } + ]; + + const { container, getByRole } = render(); + + // Initially on Plain Code tab - should use CopyBlock + const codeElements = container.querySelectorAll('pre'); + expect(codeElements.length).toBeGreaterThan(0); + + // Switch to HTML tab - should use dangerouslySetInnerHTML + fireEvent.click(getByRole('button', { name: 'HTML Code' })); + + const spanElement = container.querySelector('span'); + const emElement = container.querySelector('em'); + expect(spanElement).toBeInTheDocument(); + expect(emElement).toBeInTheDocument(); + expect(emElement).toHaveTextContent('World'); + }); + + it('should maintain proper CSS classes for show/hide states', () => { + const codeData: ICodeTab[] = [ + { + tab: 'Tab 1', + code: 'code1' + }, + { + tab: 'Tab 2', + code: 'code2' + } + ]; + + const { container, getByRole } = render(); + + const codeSections = container.querySelectorAll('.code-section'); + expect(codeSections).toHaveLength(2); + + // Initially first tab should be shown + expect(codeSections[0]).toHaveClass('show'); + expect(codeSections[1]).toHaveClass('hide'); + + // Switch to second tab + fireEvent.click(getByRole('button', { name: 'Tab 2' })); + + expect(codeSections[0]).toHaveClass('hide'); + expect(codeSections[1]).toHaveClass('show'); + }); + + it('should render with proper component structure and ID', () => { + const codeData: ICodeTab[] = [ + { + tab: 'Test', + code: 'test code' + } + ]; + + const { container } = render(); + + const codeTabsDiv = container.querySelector('#code-tabs'); + expect(codeTabsDiv).toBeInTheDocument(); + + const tabButtons = container.querySelectorAll('.tab-button'); + expect(tabButtons).toHaveLength(1); + + const codeSections = container.querySelectorAll('.code-section'); + expect(codeSections).toHaveLength(1); + }); + + it('should handle empty code gracefully', () => { + const codeData: ICodeTab[] = [ + { + tab: 'Empty', + code: '' + } + ]; + + const { getByText, getByRole } = render(); + + const button = getByRole('button', { name: 'Empty' }); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('selected'); + + expect(getByText('Empty body:')).toBeInTheDocument(); + }); + }); + + describe('NotFound Component', () => { + + beforeEach(() => { + // Mock window.scrollBy since it's not implemented in jsdom + window.scrollBy = jest.fn(); + // Mock scrollIntoView + Element.prototype.scrollIntoView = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the page not found heading', () => { + const { getByRole } = render( + + + + ); + + const heading = getByRole('heading', { name: 'Page Not Found' }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveClass('ds-text-heading--2xl'); + expect(heading).toHaveAttribute('id', 'help'); + }); + + it('should render a link back to the homepage', () => { + const { getByRole } = render( + + + + ); + + const homepageLink = getByRole('link', { name: 'homepage' }); + expect(homepageLink).toBeInTheDocument(); + expect(homepageLink).toHaveAttribute('href', '/#/'); + }); + + + it('should have proper styling on the heading', () => { + const { getByRole } = render( + + + + ); + + const heading = getByRole('heading', { name: 'Page Not Found' }); + expect(heading).toHaveStyle('margin-top: 0'); + }); + }); }); From df03d75a8129672725fa17f5636ee1d5f25fcb29 Mon Sep 17 00:00:00 2001 From: John Manack Date: Tue, 26 Aug 2025 14:58:18 -0400 Subject: [PATCH 3/4] feat: QPPA-10471 rename Submission swagger docs to interactice docs --- src/app/components/references/error-codes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/references/error-codes.tsx b/src/app/components/references/error-codes.tsx index e5ef9061..d096dfa8 100644 --- a/src/app/components/references/error-codes.tsx +++ b/src/app/components/references/error-codes.tsx @@ -8,7 +8,7 @@ const ErrorCodes: React.FC = ({dataTestId}: DocPageProps) => {

    Last Updated: 08/28/2025

    {/* IMPORTANT: update this Last-Updated value if you have made any changes to this page's content. */}

    Error Codes

    -

    To review a list of response codes provided by the API, please visit the .

    +

    To review a list of response codes provided by the API, please visit the .

    ); }; From f97fc6b8f8ce1c4908813e6d87e459348e6934d8 Mon Sep 17 00:00:00 2001 From: John Manack Date: Tue, 26 Aug 2025 16:26:28 -0400 Subject: [PATCH 4/4] feat: QPPA-10471 minor wording and formatting updates --- src/app/components/guides/advanced-tutorial.tsx | 2 +- src/app/components/references/scoring.tsx | 7 +------ src/app/components/topics/change-log.tsx | 7 +++++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/app/components/guides/advanced-tutorial.tsx b/src/app/components/guides/advanced-tutorial.tsx index c599bfa1..734e536e 100644 --- a/src/app/components/guides/advanced-tutorial.tsx +++ b/src/app/components/guides/advanced-tutorial.tsx @@ -15,7 +15,7 @@ const AdvancedTutorial: React.FC = ({dataTestId}: DocPageProps) =>

    Add more measures to an existing measurement set

    - Here's a PATCH request to add more measures to an existing measurement set. You can pass an Accept header to specify the API version and the desired response format by using our custom mime type, application/vnd.qpp.cms.gov.v1+json. We support JSON by using +json. You can also continue to use a standard application/json header, which will point to the latest version. + You can use a PATCH request to add more measures to an existing measurement set. You can pass an Accept header to specify the API version and the desired response format by using our custom mime type, application/vnd.qpp.cms.gov.v1+json. We support JSON by using +json. You can also continue to use a standard application/json header, which will point to the latest version.

    To see an example request and response, please see the PATCH /measurement-sets/:id section within the . diff --git a/src/app/components/references/scoring.tsx b/src/app/components/references/scoring.tsx index 8db45ec3..d28e7ae8 100644 --- a/src/app/components/references/scoring.tsx +++ b/src/app/components/references/scoring.tsx @@ -125,13 +125,8 @@ const Scoring: React.FC = ({dataTestId}: DocPageProps) => {

    - eCQMs require the utilization of CEHRT and the reporting of the corresponding CMS CEHRT ID in the submission + eCQMs require the utilization of CEHRT and the reporting of the corresponding CMS CEHRT ID in the submission.

    -
      -
    • 2025CXXXXXXXXXX
    • -
    • XX15CXXXXXXXXXX
    • -
    - ); }; diff --git a/src/app/components/topics/change-log.tsx b/src/app/components/topics/change-log.tsx index e900d0e2..9686dd51 100644 --- a/src/app/components/topics/change-log.tsx +++ b/src/app/components/topics/change-log.tsx @@ -5,8 +5,11 @@ const submissionChangesTable = [ ['08/28/2025', `Updates to Developer Documentation for Performance Year 2025 Description`, `
    • Simplified Scenarios for MVP Reporting in Developer Preview. See the Special Scoring Scenarios.
    • -
    • There is now 1 TIN and 1 Subgroup that will work when testing any MVP that is valid for the performance year.
    • -
    • Negative use tests can be completed using any other TIN/Subgroup.
    • +
    • There is now 1 TIN and 1 Subgroup that will work when testing any MVP that is valid for the performance year. +
        +
      • Negative use tests can be completed using any other TIN/Subgroup.
      • +
      +
    • Added support for APP Plus Reporting. See the Special Scoring Scenarios.
    • Allowed CEHRT ID format change for Promoting Interoperability. See Measurement Sets.
    • Quality eCQM reporting now requires the inclusion of a CEHRT ID.