Skip to content

Commit 3d19877

Browse files
Merge pull request #1545 from CMSgov/QPPA-10635
QPPA-10635: Add support for SSP program
2 parents 60e1248 + 97912e5 commit 3d19877

13 files changed

Lines changed: 1336 additions & 6 deletions

File tree

ERROR_MESSAGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,4 @@ Any text in the following format `(Example)` are considered variables to be fill
9191
* 106 : CT - Promoting Interoperability data should not be reported in a PCF QRDA III file.
9292
* 107 : CT - There's missing NPI/TIN combination. The NPI/TIN `(npi)`-`(tin)` was active on the PCF practitioner roster for `(apm)` during the performance year but was not found in the file. Ensure your submission contains all NPI/TIN combinations that were active on your roster at any point during the performance year. Your QRDA III file and/or roster may require updates. The QPP website doesn't have access to roster updates made after December 13, 2025. It's critical to ensure your roster is up to date and your QRDA III file contains all NPI/TIN values that were active on your roster during the performance year. Contact your health IT vendor if your QRDA III file requires updates. You can find instructions on updating rosters in the PCF Practice Management Guide: (https://cmmi.my.salesforce.com/sfc/p/#i0000000iryR/a/t00000028RsP/dMF_romOmf5VLe7p5lUj8vch11mPmELP6ZuyI16vS.Y).
9393
* 108 : CT - Found an unexpected NPI/TIN combination. The NPI/TIN `(npi)`-`(tin)` was reported in the file but does not exist at the practice or was not active on the PCF practitioner roster for `(apm)` during the performance year. Ensure your submission only contains NPI/TIN combinations that were active on your roster at any point during the performance year. Your QRDA III file and/or roster may require updates. Note: The QPP website does not have access to roster updates made after December 13, 2025. It's critical that you ensure your roster is up to date and your QRDA III file contains all NPI/TIN values that were active on your roster during the performance year. Please contact your health IT vendor if your QRDA III file requires updates. You can find instructions on how updating rosters in the PCF Practice Management Guide (https://cmmi.my.salesforce.com/sfc/p/#i0000000iryR/a/t00000028RsP/dMF_romOmf5VLe7p5lUj8vch11mPmELP6ZuyI16vS.Y).
94+
* 109 : CT - Review the Clinical Document for program `(program name)`. Only the Promoting Interoperability (PI) measure category is permitted; found `(provided category)`.

commons/src/main/java/gov/cms/qpp/conversion/model/error/ProblemCode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,10 @@ public enum ProblemCode implements LocalizedProblem {
203203
+ "all NPI/TIN values that were active on your roster during the performance year. "
204204
+ "Please contact your health IT vendor if your QRDA III file requires updates. "
205205
+ "You can find instructions on how updating rosters in the PCF Practice Management Guide "
206-
+ "(https://cmmi.my.salesforce.com/sfc/p/#i0000000iryR/a/t00000028RsP/dMF_romOmf5VLe7p5lUj8vch11mPmELP6ZuyI16vS.Y).", true);
206+
+ "(https://cmmi.my.salesforce.com/sfc/p/#i0000000iryR/a/t00000028RsP/dMF_romOmf5VLe7p5lUj8vch11mPmELP6ZuyI16vS.Y).", true),
207+
SSP_PI_ONLY_MEASURE_CATEGORY(109, "Review the Clinical Document for program `(program name)`. "
208+
+ "Only the Promoting Interoperability (PI) measure category is permitted; "
209+
+ "found `(provided category)`.", true);
207210

208211
private static final Map<Integer, ProblemCode> CODE_TO_VALUE = Arrays.stream(values())
209212
.collect(Collectors.toMap(ProblemCode::getCode, Function.identity()));

converter/src/main/java/gov/cms/qpp/conversion/decode/ClinicalDocumentDecoder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,15 @@ private Pair<String, String> getProgramNameEntityPair(String name) {
258258
break;
259259

260260
case SSP_PI_INDIVIDUAL:
261-
pair = new ImmutablePair<>(SSP_PI_INDIVIDUAL, ENTITY_INDIVIDUAL);
261+
pair = new ImmutablePair<>(SSP_PROGRAM_NAME, ENTITY_INDIVIDUAL);
262262
break;
263263

264264
case SSP_PI_GROUP:
265-
pair = new ImmutablePair<>(SSP_PI_GROUP, ENTITY_GROUP);
265+
pair = new ImmutablePair<>(SSP_PROGRAM_NAME, ENTITY_GROUP);
266266
break;
267267

268268
case SSP_PI_APM:
269-
pair = new ImmutablePair<>(SSP_PI_APM, ENTITY_APM);
269+
pair = new ImmutablePair<>(SSP_PROGRAM_NAME, ENTITY_APM);
270270
break;
271271

272272
default:

converter/src/main/java/gov/cms/qpp/conversion/encode/ClinicalDocumentEncoder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ private void encodeToplevel(JsonWrapper wrapper, Node thisNode) {
7979
wrapper.put(ENTITY_ID, thisNode.getValue(ENTITY_ID));
8080
}
8181

82-
if ((Program.isApp(thisNode) || Program.isMips(thisNode) || Program.isAppPlus(thisNode))
82+
if ((Program.isApp(thisNode) || Program.isMips(thisNode) || Program.isAppPlus(thisNode) || Program.isSsp(thisNode))
8383
&& ENTITY_APM.equalsIgnoreCase(entityType)) {
8484
wrapper.put(ENTITY_ID, thisNode.getValue(ENTITY_ID));
8585
}

converter/src/main/java/gov/cms/qpp/conversion/model/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class Constants {
6767
public static final String ENTITY_VIRTUAL_GROUP = "virtualGroup";
6868
public static final String APP_PROGRAM_NAME = "app1";
6969
public static final String APP_PLUS_PROGRAM_NAME = "appPlus";
70+
public static final String SSP_PROGRAM_NAME = "ssp";
7071
public static final String MIPS = "MIPS";
7172
public static final Set<String> MVP_ENTITIES = Set.of(ENTITY_INDIVIDUAL, ENTITY_GROUP, ENTITY_SUBGROUP, ENTITY_APM);
7273

converter/src/main/java/gov/cms/qpp/conversion/model/Program.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public enum Program {
1717
PCF("PCF"),
1818
APP("MIPS_APP1_INDIV", "MIPS_APP1_GROUP", "MIPS_APP1_APMENTITY"),
1919
APP_PLUS("APP_PLUS_INDIV", "APP_PLUS_GROUP", "APP_PLUS_APMENTITY"),
20+
SSP("SSP_PI_INDIV", "SSP_PI_GROUP", "SSP_PI_APMENTITY"),
2021
ALL;
2122

2223
private final Set<String> aliases;
@@ -70,6 +71,16 @@ public static boolean isAppPlus(Node node) {
7071
return extractProgram(node) == Program.APP_PLUS;
7172
}
7273

74+
/**
75+
* Checks if a node is using the ssp program
76+
*
77+
* @param node
78+
* @return
79+
*/
80+
public static boolean isSsp(Node node) {
81+
return extractProgram(node) == Program.SSP;
82+
}
83+
7384
/**
7485
* Extracts a program type from a node
7586
*

converter/src/main/java/gov/cms/qpp/conversion/validate/ClinicalDocumentValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ protected void performValidation(final Node node) {
5353
String entityType = Optional.ofNullable(node.getValue(ENTITY_TYPE)).orElse("<missing>");
5454

5555
forceCheckErrors(node).valueIn(ProblemCode.CLINICAL_DOCUMENT_INCORRECT_PROGRAM_NAME.format(VALID_PROGRAM_NAMES, programName),
56-
PROGRAM_NAME, MIPS_PROGRAM_NAME, PCF, APP_PROGRAM_NAME, APP_PLUS_PROGRAM_NAME);
56+
PROGRAM_NAME, MIPS_PROGRAM_NAME, PCF, APP_PROGRAM_NAME, APP_PLUS_PROGRAM_NAME, SSP_PROGRAM_NAME);
5757

5858
if (ENTITY_VIRTUAL_GROUP.equals(entityType)) {
5959
forceCheckErrors(node).value(ProblemCode.VIRTUAL_GROUP_ID_REQUIRED, ENTITY_ID);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package gov.cms.qpp.conversion.validate;
2+
3+
import gov.cms.qpp.conversion.model.Node;
4+
import gov.cms.qpp.conversion.model.Program;
5+
import gov.cms.qpp.conversion.model.TemplateId;
6+
import gov.cms.qpp.conversion.model.Validator;
7+
import gov.cms.qpp.conversion.model.error.LocalizedProblem;
8+
import gov.cms.qpp.conversion.model.error.ProblemCode;
9+
10+
import java.util.Locale;
11+
12+
import static gov.cms.qpp.conversion.model.Constants.CATEGORY;
13+
import static gov.cms.qpp.conversion.model.Constants.PROGRAM_NAME;
14+
15+
@Validator(value = TemplateId.CLINICAL_DOCUMENT, program = Program.SSP)
16+
public class SspClinicalDocumentValidator extends NodeValidator {
17+
private static final String ALLOWED_CATEGORY = "pi";
18+
19+
@Override
20+
protected void performValidation(Node clinicalDocument) {
21+
String programName = clinicalDocument.getValue(PROGRAM_NAME).toUpperCase(Locale.ROOT);
22+
clinicalDocument
23+
.getChildNodes(TemplateId.MEASURE_SECTION_V5)
24+
.forEach(sectionNode -> {
25+
String category = sectionNode.getValue(CATEGORY);
26+
System.out.println("category: " + category);
27+
if (category == null || !ALLOWED_CATEGORY.equalsIgnoreCase(category)) {
28+
LocalizedProblem error = ProblemCode.SSP_PI_ONLY_MEASURE_CATEGORY
29+
.format(programName, category == null ? "none" : category);
30+
checkErrors(sectionNode)
31+
.valueIn(error, CATEGORY, ALLOWED_CATEGORY);
32+
}
33+
});
34+
}
35+
}

converter/src/test/java/gov/cms/qpp/conversion/model/ProgramTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,41 @@ void testIsAppPlusAppEntityIsTrue() {
136136
assertThat(Program.isAppPlus(node)).isTrue();
137137
}
138138

139+
@Test
140+
void testIsSspIndividualIsTrue() {
141+
Node node = new Node();
142+
node.putValue(RAW_PROGRAM_NAME, "SSP_PI_INDIV");
143+
assertThat(Program.isSsp(node)).isTrue();
144+
}
145+
146+
@Test
147+
void testIsSspGroupIsTrue() {
148+
Node node = new Node();
149+
node.putValue(RAW_PROGRAM_NAME, "SSP_PI_GROUP");
150+
assertThat(Program.isSsp(node)).isTrue();
151+
}
152+
153+
@Test
154+
void testIsSspAppEntityIsTrue() {
155+
Node node = new Node();
156+
node.putValue(RAW_PROGRAM_NAME, "SSP_PI_APMENTITY");
157+
assertThat(Program.isSsp(node)).isTrue();
158+
}
159+
160+
@Test
161+
void testIsSspReturnsFalseForMips() {
162+
Node node = new Node();
163+
node.putValue(RAW_PROGRAM_NAME, "MIPS_INDIV");
164+
assertThat(Program.isSsp(node)).isFalse();
165+
}
166+
167+
@Test
168+
void testIsSspReturnsFalseForNullValue() {
169+
Node node = new Node();
170+
node.putValue(RAW_PROGRAM_NAME, null);
171+
assertThat(Program.isSsp(node)).isFalse();
172+
}
173+
139174
@Override
140175
public Class<? extends Enum<?>> getEnumType() {
141176
return Program.class;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package gov.cms.qpp.conversion.validate;
2+
3+
import static com.google.common.truth.Truth.assertWithMessage;
4+
5+
import java.util.List;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
10+
import gov.cms.qpp.conversion.model.Node;
11+
import gov.cms.qpp.conversion.model.TemplateId;
12+
import gov.cms.qpp.conversion.model.error.Detail;
13+
import gov.cms.qpp.conversion.model.error.ProblemCode;
14+
import gov.cms.qpp.conversion.model.error.correspondence.DetailsErrorEquals;
15+
16+
class SspClinicalDocumentValidatorTest {
17+
18+
private Node clinicalDocumentNode;
19+
private Node measureSectionNode;
20+
21+
@BeforeEach
22+
void setUp() {
23+
clinicalDocumentNode = new Node(TemplateId.CLINICAL_DOCUMENT);
24+
clinicalDocumentNode.putValue("programName", "SSP");
25+
26+
measureSectionNode = new Node(TemplateId.MEASURE_SECTION_V5);
27+
}
28+
29+
@Test
30+
void testValidCategory_NoErrors() {
31+
measureSectionNode.putValue("category", "pi");
32+
clinicalDocumentNode.addChildNode(measureSectionNode);
33+
34+
SspClinicalDocumentValidator validator = new SspClinicalDocumentValidator();
35+
List<Detail> errors = validator.validateSingleNode(clinicalDocumentNode).getErrors();
36+
37+
assertWithMessage("There should be no errors for category 'pi'")
38+
.that(errors).isEmpty();
39+
}
40+
41+
@Test
42+
void testNullCategory_ShouldError() {
43+
// category is not set
44+
clinicalDocumentNode.addChildNode(measureSectionNode);
45+
46+
SspClinicalDocumentValidator validator = new SspClinicalDocumentValidator();
47+
List<Detail> errors = validator.validateSingleNode(clinicalDocumentNode).getErrors();
48+
49+
assertWithMessage("Must report SSP_PI_ONLY_MEASURE_CATEGORY when category is null")
50+
.that(errors)
51+
.comparingElementsUsing(DetailsErrorEquals.INSTANCE)
52+
.containsExactly(
53+
ProblemCode.SSP_PI_ONLY_MEASURE_CATEGORY.format("SSP", "none")
54+
);
55+
}
56+
57+
@Test
58+
void testInvalidCategory_ShouldError() {
59+
measureSectionNode.putValue("category", "invalid");
60+
clinicalDocumentNode.addChildNode(measureSectionNode);
61+
62+
SspClinicalDocumentValidator validator = new SspClinicalDocumentValidator();
63+
List<Detail> errors = validator.validateSingleNode(clinicalDocumentNode).getErrors();
64+
65+
assertWithMessage("Must report SSP_PI_ONLY_MEASURE_CATEGORY when category is invalid")
66+
.that(errors)
67+
.comparingElementsUsing(DetailsErrorEquals.INSTANCE)
68+
.containsExactly(
69+
ProblemCode.SSP_PI_ONLY_MEASURE_CATEGORY.format("SSP", "invalid")
70+
);
71+
}
72+
}

0 commit comments

Comments
 (0)