Skip to content

Commit ca18e1e

Browse files
Merge pull request #1148 from MeasureAuthoringTool/MAT-10141
MAT-10141, 1042 adding review column and searching based on the review field
2 parents d33db26 + 5218a9d commit ca18e1e

10 files changed

Lines changed: 279 additions & 2 deletions

File tree

src/main/java/cms/gov/madie/measure/dto/MeasureListDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ public class MeasureListDTO {
6161
private boolean hasLockedTestCases;
6262
private String ownerDisplayName;
6363
private boolean component;
64+
private String reviewStatus;
6465
}

src/main/java/cms/gov/madie/measure/repositories/MeasureSearchServiceImpl.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ private Map<String, MeasureSetMatchCountDTO> findMatchedMeasureSets(
186186
|| measureSearchCriteria.getOptionalSearchProperties().contains("cmsId")) {
187187
aggregationOperations.add(SearchAggregationUtils.addCmsIdDisplayField());
188188
}
189+
if (SearchAggregationUtils.isReviewSearch(measureSearchCriteria)) {
190+
aggregationOperations.addAll(SearchAggregationUtils.getReviewStages());
191+
}
189192
SearchUtils.appendAdditionalSearchCriteria(measureCriteria, measureSearchCriteria);
190193
}
191194

@@ -263,6 +266,7 @@ private List<FacetDTO> fetchFacetResults(
263266
postMatchPipeline.add(initialProjection);
264267
postMatchPipeline.add(match(Criteria.where("measureSetId").in(matchedMeasureSetIds)));
265268
postMatchPipeline.addAll(getLockStages(userId));
269+
postMatchPipeline.addAll(SearchAggregationUtils.getReviewStages());
266270

267271
// Sort those measures based on active status, version and draft status
268272
// Active measures should come first, then draft measures, then by version

src/main/java/cms/gov/madie/measure/repositories/MeasureSetSearchRepositoryImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public List<MeasureListDTO> findMeasuresByMeasureSetId(
4949
|| measureSearchCriteria.getOptionalSearchProperties().contains("cmsId")) {
5050
aggregationOperations.add(SearchAggregationUtils.addCmsIdDisplayField());
5151
}
52+
if (SearchAggregationUtils.isReviewSearch(measureSearchCriteria)) {
53+
aggregationOperations.addAll(SearchAggregationUtils.getReviewStages());
54+
}
5255
SearchUtils.appendAdditionalSearchCriteria(measureCriteria, measureSearchCriteria);
5356
}
5457

src/main/java/cms/gov/madie/measure/services/MeasureSetService.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import cms.gov.madie.measure.exceptions.*;
99
import cms.gov.madie.measure.repositories.GeneratorRepository;
1010
import cms.gov.madie.measure.repositories.MeasureRepository;
11+
import cms.gov.madie.measure.repositories.MeasureReviewRepository;
1112
import cms.gov.madie.measure.repositories.MeasureSetActionLogRepository;
1213
import cms.gov.madie.measure.repositories.MeasureSetRepository;
1314
import gov.cms.madie.models.access.AclOperation;
@@ -17,8 +18,10 @@
1718
import gov.cms.madie.models.common.Action;
1819
import gov.cms.madie.models.common.ActionType;
1920
import gov.cms.madie.models.common.MeasureSetActionLog;
21+
import gov.cms.madie.models.common.ReviewStatus;
2022
import gov.cms.madie.models.dto.UserDetailsDto;
2123
import gov.cms.madie.models.measure.Measure;
24+
import gov.cms.madie.models.measure.MeasureReview;
2225
import gov.cms.madie.models.measure.MeasureSet;
2326
import lombok.RequiredArgsConstructor;
2427
import lombok.extern.slf4j.Slf4j;
@@ -46,6 +49,7 @@ public class MeasureSetService {
4649
private final ActionLogService actionLogService;
4750
private final UserServiceClient userServiceClient;
4851
private final MeasureSetActionLogRepository measureSetActionLogRepository;
52+
private final MeasureReviewRepository measureReviewRepository;
4953

5054
public void createMeasureSet(
5155
final String harpId, final String measureId, final String savedMeasureSetId, String cmsId) {
@@ -322,8 +326,29 @@ public List<MeasureListDTO> getMeasuresByMeasureSetId(
322326
String measureSetId,
323327
boolean sortByLatestVersion,
324328
MeasureSearchCriteria measureSearchCriteria) {
325-
return measureSetRepository.findMeasuresByMeasureSetId(
326-
measureSetId, sortByLatestVersion, measureSearchCriteria);
329+
List<MeasureListDTO> measuresByMeasureSetId =
330+
measureSetRepository.findMeasuresByMeasureSetId(
331+
measureSetId, sortByLatestVersion, measureSearchCriteria);
332+
333+
enrichWithReviewStatus(measureSetId, measuresByMeasureSetId);
334+
335+
return measuresByMeasureSetId;
336+
}
337+
338+
private void enrichWithReviewStatus(String measureSetId, List<MeasureListDTO> measures) {
339+
if (CollectionUtils.isEmpty(measures)) {
340+
return;
341+
}
342+
Set<String> readyForReviewMeasureIds =
343+
measureReviewRepository.findAllByMeasureSetId(measureSetId).stream()
344+
.filter(review -> ReviewStatus.READY_FOR_REVIEW.equals(review.getStatus()))
345+
.map(MeasureReview::getMeasureId)
346+
.filter(Objects::nonNull)
347+
.collect(Collectors.toSet());
348+
measures.forEach(
349+
measure ->
350+
measure.setReviewStatus(
351+
readyForReviewMeasureIds.contains(measure.getId()) ? "Ready" : ""));
327352
}
328353

329354
public List<Measure> getRecentMeasuresByMeasureSetId(List<String> measureSetIds) {

src/main/java/cms/gov/madie/measure/utils/SearchAggregationUtils.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,54 @@
11
package cms.gov.madie.measure.utils;
22

3+
import cms.gov.madie.measure.dto.MeasureSearchCriteria;
4+
import gov.cms.madie.models.common.ReviewStatus;
5+
import org.apache.commons.collections4.CollectionUtils;
36
import org.bson.Document;
47
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
58
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
9+
import org.springframework.data.mongodb.core.aggregation.ArrayOperators;
10+
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
11+
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators;
12+
import org.springframework.data.mongodb.core.aggregation.ConvertOperators;
613
import org.springframework.data.mongodb.core.query.Criteria;
714

815
import java.util.Arrays;
916
import java.util.Collections;
1017
import java.util.List;
1118

19+
import static org.springframework.data.mongodb.core.aggregation.Aggregation.addFields;
20+
import static org.springframework.data.mongodb.core.aggregation.Aggregation.lookup;
1221
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;
1322

1423
public class SearchAggregationUtils {
24+
25+
public static List<AggregationOperation> getReviewStages() {
26+
return Arrays.asList(
27+
addFields()
28+
.addField("measureIdString")
29+
.withValue(ConvertOperators.ToString.toString("$_id"))
30+
.build(),
31+
32+
lookup("measureReview", "measureIdString", "measureId", "review"),
33+
34+
addFields()
35+
.addField("reviewStatus")
36+
.withValue(
37+
ConditionalOperators.when(
38+
ComparisonOperators.Eq.valueOf(
39+
ArrayOperators.ArrayElemAt.arrayOf("$review.status").elementAt(0))
40+
.equalToValue(ReviewStatus.READY_FOR_REVIEW.name()))
41+
.then("Ready")
42+
.otherwise(""))
43+
.build());
44+
}
45+
46+
public static boolean isReviewSearch(MeasureSearchCriteria measureSearchCriteria) {
47+
return measureSearchCriteria != null
48+
&& CollectionUtils.isNotEmpty(measureSearchCriteria.getOptionalSearchProperties())
49+
&& measureSearchCriteria.getOptionalSearchProperties().contains("review");
50+
}
51+
1552
// Add string field called cmsIdDisplay. The CMS ID is zero-padded to 4 digits
1653
// for display & search consistency. Values wider than 4 digits
1754
// are left as-is. For QI-Core measures the "FHIR" suffix is appended.

src/main/java/cms/gov/madie/measure/utils/SearchUtils.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ && isNumeric(versionParts[1])) {
7474
orConditions.add(
7575
Criteria.where("model").regex(".*" + Pattern.quote(searchField) + ".*", "i"));
7676
break;
77+
case "review":
78+
orConditions.add(
79+
Criteria.where("reviewStatus").regex(".*" + Pattern.quote(searchField) + ".*", "i"));
80+
break;
7781
default:
7882
if (!StringUtils.isBlank(property)) {
7983
orConditions.add(Criteria.where(property).regex(searchField, "i"));

src/test/java/cms/gov/madie/measure/repositories/MeasureSearchServiceImplTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.junit.jupiter.api.Test;
1919
import org.junit.jupiter.api.extension.ExtendWith;
20+
import org.mockito.ArgumentCaptor;
2021
import org.mockito.ArgumentMatchers;
2122
import org.mockito.InjectMocks;
2223
import org.mockito.Mock;
@@ -115,6 +116,105 @@ public void testFindOwnedActiveMeasures() {
115116
assertEquals(page1Measures.get(2).getId(), measure3.getId());
116117
}
117118

119+
private ArgumentCaptor<Aggregation> stubAggregatesReturning(List<MeasureListDTO> queryResults) {
120+
FacetDTO facetDTO =
121+
FacetDTO.builder()
122+
.queryResults(queryResults)
123+
.count(Arrays.asList(queryResults.toArray()))
124+
.build();
125+
AggregationResults<FacetDTO> pagedResults =
126+
new AggregationResults<>(List.of(facetDTO), new Document());
127+
AggregationResults<MeasureSetMatchCountDTO> measureSetResults =
128+
new AggregationResults<>(
129+
List.of(MeasureSetMatchCountDTO.builder().measureSetId("1-1").build()), new Document());
130+
131+
when(mongoTemplate.aggregate(
132+
any(Aggregation.class),
133+
ArgumentMatchers.eq(Measure.class),
134+
ArgumentMatchers.eq(MeasureSetMatchCountDTO.class)))
135+
.thenReturn(measureSetResults);
136+
when(mongoTemplate.aggregate(
137+
any(Aggregation.class),
138+
ArgumentMatchers.eq(Measure.class),
139+
ArgumentMatchers.eq(FacetDTO.class)))
140+
.thenReturn(pagedResults);
141+
142+
return ArgumentCaptor.forClass(Aggregation.class);
143+
}
144+
145+
@Test
146+
public void testFacetQueryAlwaysJoinsReviewSoStatusIsDisplayed() {
147+
ArgumentCaptor<Aggregation> captor = stubAggregatesReturning(List.of(measure1));
148+
149+
measureAclRepository.searchMeasuresByCriteria(
150+
"john", PageRequest.of(0, 10), null, List.of(OwnershipType.OWNED));
151+
152+
verify(mongoTemplate)
153+
.aggregate(
154+
captor.capture(),
155+
ArgumentMatchers.eq(Measure.class),
156+
ArgumentMatchers.eq(FacetDTO.class));
157+
158+
String pipeline = captor.getValue().toString();
159+
assertTrue(pipeline.contains("measureReview"), "facet query should join the review collection");
160+
assertTrue(pipeline.contains("reviewStatus"), "facet query should project reviewStatus");
161+
assertTrue(pipeline.contains("READY_FOR_REVIEW"));
162+
}
163+
164+
@Test
165+
public void testMatchQueryJoinsReviewOnlyWhenSearchingByReview() {
166+
ArgumentCaptor<Aggregation> captor = stubAggregatesReturning(List.of(measure1));
167+
MeasureSearchCriteria criteria =
168+
MeasureSearchCriteria.builder()
169+
.searchField("Ready")
170+
.optionalSearchProperties(List.of("review"))
171+
.build();
172+
173+
measureAclRepository.searchMeasuresByCriteria(
174+
"john", PageRequest.of(0, 10), criteria, List.of(OwnershipType.OWNED));
175+
176+
verify(mongoTemplate)
177+
.aggregate(
178+
captor.capture(),
179+
ArgumentMatchers.eq(Measure.class),
180+
ArgumentMatchers.eq(MeasureSetMatchCountDTO.class));
181+
182+
String pipeline = captor.getValue().toString();
183+
assertTrue(pipeline.contains("measureReview"), "match query should join review when filtering");
184+
assertTrue(pipeline.contains("reviewStatus"));
185+
}
186+
187+
@Test
188+
public void testMatchQuerySkipsReviewJoinForNonReviewSearch() {
189+
ArgumentCaptor<Aggregation> captor = stubAggregatesReturning(List.of(measure1));
190+
MeasureSearchCriteria criteria =
191+
MeasureSearchCriteria.builder().searchField("test measure").build();
192+
193+
measureAclRepository.searchMeasuresByCriteria(
194+
"john", PageRequest.of(0, 10), criteria, List.of(OwnershipType.OWNED));
195+
196+
verify(mongoTemplate)
197+
.aggregate(
198+
captor.capture(),
199+
ArgumentMatchers.eq(Measure.class),
200+
ArgumentMatchers.eq(MeasureSetMatchCountDTO.class));
201+
202+
assertFalse(captor.getValue().toString().contains("measureReview"));
203+
}
204+
205+
@Test
206+
public void testReviewStatusIsCarriedThroughToResults() {
207+
MeasureListDTO reviewed =
208+
MeasureListDTO.builder().id("1").measureSetId("1-1").reviewStatus("Ready").build();
209+
stubAggregatesReturning(List.of(reviewed));
210+
211+
Page<MeasureListDTO> page =
212+
measureAclRepository.searchMeasuresByCriteria(
213+
"john", PageRequest.of(0, 10), null, List.of(OwnershipType.OWNED));
214+
215+
assertEquals("Ready", page.getContent().get(0).getReviewStatus());
216+
}
217+
118218
@Test
119219
public void testFindOwnedActiveMeasuresWithSearchTerm() {
120220
PageRequest pageRequest = PageRequest.of(0, 3);

src/test/java/cms/gov/madie/measure/repositories/MeasureSetSearchRepositoryImplTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,49 @@ void shouldApplyCmsIdSearchCriteriaAndReturnMatchingMeasures() {
119119
assertThat(pipelineString).contains("cmsIdDisplay");
120120
}
121121

122+
@Test
123+
void shouldJoinReviewCollectionWhenFilteringAssociatedMeasuresByReview() {
124+
MeasureSearchCriteria criteria =
125+
MeasureSearchCriteria.builder()
126+
.searchField("Ready")
127+
.optionalSearchProperties(List.of("review"))
128+
.build();
129+
130+
List<MeasureListDTO> mockResults = List.of(createDTO("Ready Associated Measure"));
131+
when(mongoTemplate.aggregate(any(Aggregation.class), eq("measure"), eq(MeasureListDTO.class)))
132+
.thenReturn(new AggregationResults<>(mockResults, new Document()));
133+
134+
repository.findMeasuresByMeasureSetId(MEASURE_SET_ID, false, criteria);
135+
136+
ArgumentCaptor<Aggregation> captor = ArgumentCaptor.forClass(Aggregation.class);
137+
verify(mongoTemplate).aggregate(captor.capture(), eq("measure"), eq(MeasureListDTO.class));
138+
139+
String pipelineString = captor.getValue().toString();
140+
assertThat(pipelineString).contains("measureReview");
141+
assertThat(pipelineString).contains("reviewStatus");
142+
assertThat(pipelineString).contains("READY_FOR_REVIEW");
143+
}
144+
145+
@Test
146+
void shouldNotJoinReviewCollectionForNonReviewSearches() {
147+
MeasureSearchCriteria criteria =
148+
MeasureSearchCriteria.builder()
149+
.searchField("Measure 1")
150+
.optionalSearchProperties(List.of("measure"))
151+
.build();
152+
153+
List<MeasureListDTO> mockResults = List.of(createDTO("Measure 1"));
154+
when(mongoTemplate.aggregate(any(Aggregation.class), eq("measure"), eq(MeasureListDTO.class)))
155+
.thenReturn(new AggregationResults<>(mockResults, new Document()));
156+
157+
repository.findMeasuresByMeasureSetId(MEASURE_SET_ID, false, criteria);
158+
159+
ArgumentCaptor<Aggregation> captor = ArgumentCaptor.forClass(Aggregation.class);
160+
verify(mongoTemplate).aggregate(captor.capture(), eq("measure"), eq(MeasureListDTO.class));
161+
162+
assertThat(captor.getValue().toString()).doesNotContain("measureReview");
163+
}
164+
122165
@Test
123166
void shouldApplyDraftFilterForCompositeMeasureComponents() {
124167
MeasureSearchCriteria criteria =

src/test/java/cms/gov/madie/measure/services/MeasureSetServiceTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import cms.gov.madie.measure.exceptions.*;
88
import cms.gov.madie.measure.repositories.GeneratorRepository;
99
import cms.gov.madie.measure.repositories.MeasureRepository;
10+
import cms.gov.madie.measure.repositories.MeasureReviewRepository;
1011
import cms.gov.madie.measure.repositories.MeasureSetActionLogRepository;
1112
import cms.gov.madie.measure.repositories.MeasureSetRepository;
1213
import gov.cms.madie.models.access.AclOperation;
@@ -15,6 +16,7 @@
1516
import gov.cms.madie.models.common.*;
1617
import gov.cms.madie.models.dto.UserDetailsDto;
1718
import gov.cms.madie.models.measure.Measure;
19+
import gov.cms.madie.models.measure.MeasureReview;
1820
import gov.cms.madie.models.measure.MeasureSet;
1921
import org.junit.jupiter.api.BeforeEach;
2022
import org.junit.jupiter.api.Test;
@@ -43,6 +45,7 @@ public class MeasureSetServiceTest {
4345
@Mock private ActionLogService actionLogService;
4446
@Mock private UserServiceClient userServiceClient;
4547
@Mock private MeasureSetActionLogRepository measureSetActionLogRepository;
48+
@Mock private MeasureReviewRepository measureReviewRepository;
4649

4750
private final String MEASURE_SET_ID = "measureSet1";
4851
private final String ACCESS_TOKEN = "test-token";
@@ -727,6 +730,46 @@ void testGetMeasuresByMeasureSetIdDelegatesToRepository() {
727730
verify(measureSetRepository).findMeasuresByMeasureSetId(MEASURE_SET_ID, true, criteria);
728731
}
729732

733+
@Test
734+
void testGetMeasuresByMeasureSetIdMarksOnlyReadyForReviewMeasures() {
735+
MeasureListDTO readyMeasure = MeasureListDTO.builder().id("m1").build();
736+
MeasureListDTO notReadyMeasure = MeasureListDTO.builder().id("m2").build();
737+
MeasureListDTO noReviewMeasure = MeasureListDTO.builder().id("m3").build();
738+
739+
when(measureSetRepository.findMeasuresByMeasureSetId(MEASURE_SET_ID, true, null))
740+
.thenReturn(List.of(readyMeasure, notReadyMeasure, noReviewMeasure));
741+
when(measureReviewRepository.findAllByMeasureSetId(MEASURE_SET_ID))
742+
.thenReturn(
743+
List.of(
744+
MeasureReview.builder()
745+
.measureId("m1")
746+
.status(ReviewStatus.READY_FOR_REVIEW)
747+
.build(),
748+
MeasureReview.builder()
749+
.measureId("m2")
750+
.status(ReviewStatus.NOT_READY_FOR_REVIEW)
751+
.build()));
752+
753+
List<MeasureListDTO> actual =
754+
measureSetService.getMeasuresByMeasureSetId(MEASURE_SET_ID, true, null);
755+
756+
assertEquals("Ready", actual.get(0).getReviewStatus());
757+
assertEquals("", actual.get(1).getReviewStatus());
758+
assertEquals("", actual.get(2).getReviewStatus());
759+
}
760+
761+
@Test
762+
void testGetMeasuresByMeasureSetIdSkipsReviewLookupWhenNoMeasures() {
763+
when(measureSetRepository.findMeasuresByMeasureSetId(MEASURE_SET_ID, true, null))
764+
.thenReturn(List.of());
765+
766+
List<MeasureListDTO> actual =
767+
measureSetService.getMeasuresByMeasureSetId(MEASURE_SET_ID, true, null);
768+
769+
assertTrue(actual.isEmpty());
770+
verify(measureReviewRepository, never()).findAllByMeasureSetId(anyString());
771+
}
772+
730773
@Test
731774
void testGetRecentMeasuresByMeasureSetIdReturnsMeasuresInOrder() {
732775
List<String> measureSetIds = List.of("set1", "set2");

0 commit comments

Comments
 (0)