Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ public class MeasureListDTO {
private boolean hasLockedTestCases;
private String ownerDisplayName;
private boolean component;
private String reviewStatus;
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ private Map<String, MeasureSetMatchCountDTO> findMatchedMeasureSets(
|| measureSearchCriteria.getOptionalSearchProperties().contains("cmsId")) {
aggregationOperations.add(SearchAggregationUtils.addCmsIdDisplayField());
}
if (SearchAggregationUtils.isReviewSearch(measureSearchCriteria)) {
aggregationOperations.addAll(SearchAggregationUtils.getReviewStages());
}
SearchUtils.appendAdditionalSearchCriteria(measureCriteria, measureSearchCriteria);
}

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

// Sort those measures based on active status, version and draft status
// Active measures should come first, then draft measures, then by version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public List<MeasureListDTO> findMeasuresByMeasureSetId(
|| measureSearchCriteria.getOptionalSearchProperties().contains("cmsId")) {
aggregationOperations.add(SearchAggregationUtils.addCmsIdDisplayField());
}
if (SearchAggregationUtils.isReviewSearch(measureSearchCriteria)) {
aggregationOperations.addAll(SearchAggregationUtils.getReviewStages());
}
SearchUtils.appendAdditionalSearchCriteria(measureCriteria, measureSearchCriteria);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import cms.gov.madie.measure.exceptions.*;
import cms.gov.madie.measure.repositories.GeneratorRepository;
import cms.gov.madie.measure.repositories.MeasureRepository;
import cms.gov.madie.measure.repositories.MeasureReviewRepository;
import cms.gov.madie.measure.repositories.MeasureSetActionLogRepository;
import cms.gov.madie.measure.repositories.MeasureSetRepository;
import gov.cms.madie.models.access.AclOperation;
Expand All @@ -17,8 +18,10 @@
import gov.cms.madie.models.common.Action;
import gov.cms.madie.models.common.ActionType;
import gov.cms.madie.models.common.MeasureSetActionLog;
import gov.cms.madie.models.common.ReviewStatus;
import gov.cms.madie.models.dto.UserDetailsDto;
import gov.cms.madie.models.measure.Measure;
import gov.cms.madie.models.measure.MeasureReview;
import gov.cms.madie.models.measure.MeasureSet;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -46,6 +49,7 @@ public class MeasureSetService {
private final ActionLogService actionLogService;
private final UserServiceClient userServiceClient;
private final MeasureSetActionLogRepository measureSetActionLogRepository;
private final MeasureReviewRepository measureReviewRepository;

public void createMeasureSet(
final String harpId, final String measureId, final String savedMeasureSetId, String cmsId) {
Expand Down Expand Up @@ -322,8 +326,29 @@ public List<MeasureListDTO> getMeasuresByMeasureSetId(
String measureSetId,
boolean sortByLatestVersion,
MeasureSearchCriteria measureSearchCriteria) {
return measureSetRepository.findMeasuresByMeasureSetId(
measureSetId, sortByLatestVersion, measureSearchCriteria);
List<MeasureListDTO> measuresByMeasureSetId =
measureSetRepository.findMeasuresByMeasureSetId(
measureSetId, sortByLatestVersion, measureSearchCriteria);

enrichWithReviewStatus(measureSetId, measuresByMeasureSetId);

return measuresByMeasureSetId;
}

private void enrichWithReviewStatus(String measureSetId, List<MeasureListDTO> measures) {
if (CollectionUtils.isEmpty(measures)) {
return;
}
Set<String> readyForReviewMeasureIds =
measureReviewRepository.findAllByMeasureSetId(measureSetId).stream()
.filter(review -> ReviewStatus.READY_FOR_REVIEW.equals(review.getStatus()))
.map(MeasureReview::getMeasureId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
measures.forEach(
measure ->
measure.setReviewStatus(
readyForReviewMeasureIds.contains(measure.getId()) ? "Ready" : ""));
}

public List<Measure> getRecentMeasuresByMeasureSetId(List<String> measureSetIds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
package cms.gov.madie.measure.utils;

import cms.gov.madie.measure.dto.MeasureSearchCriteria;
import gov.cms.madie.models.common.ReviewStatus;
import org.apache.commons.collections4.CollectionUtils;
import org.bson.Document;
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.ArrayOperators;
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators;
import org.springframework.data.mongodb.core.aggregation.ConvertOperators;
import org.springframework.data.mongodb.core.query.Criteria;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.springframework.data.mongodb.core.aggregation.Aggregation.addFields;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.lookup;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;

public class SearchAggregationUtils {

public static List<AggregationOperation> getReviewStages() {
return Arrays.asList(
addFields()
.addField("measureIdString")
.withValue(ConvertOperators.ToString.toString("$_id"))
.build(),

lookup("measureReview", "measureIdString", "measureId", "review"),

addFields()
.addField("reviewStatus")
.withValue(
ConditionalOperators.when(
ComparisonOperators.Eq.valueOf(
ArrayOperators.ArrayElemAt.arrayOf("$review.status").elementAt(0))
.equalToValue(ReviewStatus.READY_FOR_REVIEW.name()))
.then("Ready")
.otherwise(""))
.build());
}

public static boolean isReviewSearch(MeasureSearchCriteria measureSearchCriteria) {
return measureSearchCriteria != null
&& CollectionUtils.isNotEmpty(measureSearchCriteria.getOptionalSearchProperties())
&& measureSearchCriteria.getOptionalSearchProperties().contains("review");
}

// Add string field called cmsIdDisplay. The CMS ID is zero-padded to 4 digits
// for display & search consistency. Values wider than 4 digits
// are left as-is. For QI-Core measures the "FHIR" suffix is appended.
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/cms/gov/madie/measure/utils/SearchUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ && isNumeric(versionParts[1])) {
orConditions.add(
Criteria.where("model").regex(".*" + Pattern.quote(searchField) + ".*", "i"));
break;
case "review":
orConditions.add(
Criteria.where("reviewStatus").regex(".*" + Pattern.quote(searchField) + ".*", "i"));
break;
default:
if (!StringUtils.isBlank(property)) {
orConditions.add(Criteria.where(property).regex(searchField, "i"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
Expand Down Expand Up @@ -115,6 +116,105 @@ public void testFindOwnedActiveMeasures() {
assertEquals(page1Measures.get(2).getId(), measure3.getId());
}

private ArgumentCaptor<Aggregation> stubAggregatesReturning(List<MeasureListDTO> queryResults) {
FacetDTO facetDTO =
FacetDTO.builder()
.queryResults(queryResults)
.count(Arrays.asList(queryResults.toArray()))
.build();
AggregationResults<FacetDTO> pagedResults =
new AggregationResults<>(List.of(facetDTO), new Document());
AggregationResults<MeasureSetMatchCountDTO> measureSetResults =
new AggregationResults<>(
List.of(MeasureSetMatchCountDTO.builder().measureSetId("1-1").build()), new Document());

when(mongoTemplate.aggregate(
any(Aggregation.class),
ArgumentMatchers.eq(Measure.class),
ArgumentMatchers.eq(MeasureSetMatchCountDTO.class)))
.thenReturn(measureSetResults);
when(mongoTemplate.aggregate(
any(Aggregation.class),
ArgumentMatchers.eq(Measure.class),
ArgumentMatchers.eq(FacetDTO.class)))
.thenReturn(pagedResults);

return ArgumentCaptor.forClass(Aggregation.class);
}

@Test
public void testFacetQueryAlwaysJoinsReviewSoStatusIsDisplayed() {
ArgumentCaptor<Aggregation> captor = stubAggregatesReturning(List.of(measure1));

measureAclRepository.searchMeasuresByCriteria(
"john", PageRequest.of(0, 10), null, List.of(OwnershipType.OWNED));

verify(mongoTemplate)
.aggregate(
captor.capture(),
ArgumentMatchers.eq(Measure.class),
ArgumentMatchers.eq(FacetDTO.class));

String pipeline = captor.getValue().toString();
assertTrue(pipeline.contains("measureReview"), "facet query should join the review collection");
assertTrue(pipeline.contains("reviewStatus"), "facet query should project reviewStatus");
assertTrue(pipeline.contains("READY_FOR_REVIEW"));
}

@Test
public void testMatchQueryJoinsReviewOnlyWhenSearchingByReview() {
ArgumentCaptor<Aggregation> captor = stubAggregatesReturning(List.of(measure1));
MeasureSearchCriteria criteria =
MeasureSearchCriteria.builder()
.searchField("Ready")
.optionalSearchProperties(List.of("review"))
.build();

measureAclRepository.searchMeasuresByCriteria(
"john", PageRequest.of(0, 10), criteria, List.of(OwnershipType.OWNED));

verify(mongoTemplate)
.aggregate(
captor.capture(),
ArgumentMatchers.eq(Measure.class),
ArgumentMatchers.eq(MeasureSetMatchCountDTO.class));

String pipeline = captor.getValue().toString();
assertTrue(pipeline.contains("measureReview"), "match query should join review when filtering");
assertTrue(pipeline.contains("reviewStatus"));
}

@Test
public void testMatchQuerySkipsReviewJoinForNonReviewSearch() {
ArgumentCaptor<Aggregation> captor = stubAggregatesReturning(List.of(measure1));
MeasureSearchCriteria criteria =
MeasureSearchCriteria.builder().searchField("test measure").build();

measureAclRepository.searchMeasuresByCriteria(
"john", PageRequest.of(0, 10), criteria, List.of(OwnershipType.OWNED));

verify(mongoTemplate)
.aggregate(
captor.capture(),
ArgumentMatchers.eq(Measure.class),
ArgumentMatchers.eq(MeasureSetMatchCountDTO.class));

assertFalse(captor.getValue().toString().contains("measureReview"));
}

@Test
public void testReviewStatusIsCarriedThroughToResults() {
MeasureListDTO reviewed =
MeasureListDTO.builder().id("1").measureSetId("1-1").reviewStatus("Ready").build();
stubAggregatesReturning(List.of(reviewed));

Page<MeasureListDTO> page =
measureAclRepository.searchMeasuresByCriteria(
"john", PageRequest.of(0, 10), null, List.of(OwnershipType.OWNED));

assertEquals("Ready", page.getContent().get(0).getReviewStatus());
}

@Test
public void testFindOwnedActiveMeasuresWithSearchTerm() {
PageRequest pageRequest = PageRequest.of(0, 3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,49 @@ void shouldApplyCmsIdSearchCriteriaAndReturnMatchingMeasures() {
assertThat(pipelineString).contains("cmsIdDisplay");
}

@Test
void shouldJoinReviewCollectionWhenFilteringAssociatedMeasuresByReview() {
MeasureSearchCriteria criteria =
MeasureSearchCriteria.builder()
.searchField("Ready")
.optionalSearchProperties(List.of("review"))
.build();

List<MeasureListDTO> mockResults = List.of(createDTO("Ready Associated Measure"));
when(mongoTemplate.aggregate(any(Aggregation.class), eq("measure"), eq(MeasureListDTO.class)))
.thenReturn(new AggregationResults<>(mockResults, new Document()));

repository.findMeasuresByMeasureSetId(MEASURE_SET_ID, false, criteria);

ArgumentCaptor<Aggregation> captor = ArgumentCaptor.forClass(Aggregation.class);
verify(mongoTemplate).aggregate(captor.capture(), eq("measure"), eq(MeasureListDTO.class));

String pipelineString = captor.getValue().toString();
assertThat(pipelineString).contains("measureReview");
assertThat(pipelineString).contains("reviewStatus");
assertThat(pipelineString).contains("READY_FOR_REVIEW");
}

@Test
void shouldNotJoinReviewCollectionForNonReviewSearches() {
MeasureSearchCriteria criteria =
MeasureSearchCriteria.builder()
.searchField("Measure 1")
.optionalSearchProperties(List.of("measure"))
.build();

List<MeasureListDTO> mockResults = List.of(createDTO("Measure 1"));
when(mongoTemplate.aggregate(any(Aggregation.class), eq("measure"), eq(MeasureListDTO.class)))
.thenReturn(new AggregationResults<>(mockResults, new Document()));

repository.findMeasuresByMeasureSetId(MEASURE_SET_ID, false, criteria);

ArgumentCaptor<Aggregation> captor = ArgumentCaptor.forClass(Aggregation.class);
verify(mongoTemplate).aggregate(captor.capture(), eq("measure"), eq(MeasureListDTO.class));

assertThat(captor.getValue().toString()).doesNotContain("measureReview");
}

@Test
void shouldApplyDraftFilterForCompositeMeasureComponents() {
MeasureSearchCriteria criteria =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import cms.gov.madie.measure.exceptions.*;
import cms.gov.madie.measure.repositories.GeneratorRepository;
import cms.gov.madie.measure.repositories.MeasureRepository;
import cms.gov.madie.measure.repositories.MeasureReviewRepository;
import cms.gov.madie.measure.repositories.MeasureSetActionLogRepository;
import cms.gov.madie.measure.repositories.MeasureSetRepository;
import gov.cms.madie.models.access.AclOperation;
Expand All @@ -15,6 +16,7 @@
import gov.cms.madie.models.common.*;
import gov.cms.madie.models.dto.UserDetailsDto;
import gov.cms.madie.models.measure.Measure;
import gov.cms.madie.models.measure.MeasureReview;
import gov.cms.madie.models.measure.MeasureSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -43,6 +45,7 @@ public class MeasureSetServiceTest {
@Mock private ActionLogService actionLogService;
@Mock private UserServiceClient userServiceClient;
@Mock private MeasureSetActionLogRepository measureSetActionLogRepository;
@Mock private MeasureReviewRepository measureReviewRepository;

private final String MEASURE_SET_ID = "measureSet1";
private final String ACCESS_TOKEN = "test-token";
Expand Down Expand Up @@ -727,6 +730,46 @@ void testGetMeasuresByMeasureSetIdDelegatesToRepository() {
verify(measureSetRepository).findMeasuresByMeasureSetId(MEASURE_SET_ID, true, criteria);
}

@Test
void testGetMeasuresByMeasureSetIdMarksOnlyReadyForReviewMeasures() {
MeasureListDTO readyMeasure = MeasureListDTO.builder().id("m1").build();
MeasureListDTO notReadyMeasure = MeasureListDTO.builder().id("m2").build();
MeasureListDTO noReviewMeasure = MeasureListDTO.builder().id("m3").build();

when(measureSetRepository.findMeasuresByMeasureSetId(MEASURE_SET_ID, true, null))
.thenReturn(List.of(readyMeasure, notReadyMeasure, noReviewMeasure));
when(measureReviewRepository.findAllByMeasureSetId(MEASURE_SET_ID))
.thenReturn(
List.of(
MeasureReview.builder()
.measureId("m1")
.status(ReviewStatus.READY_FOR_REVIEW)
.build(),
MeasureReview.builder()
.measureId("m2")
.status(ReviewStatus.NOT_READY_FOR_REVIEW)
.build()));

List<MeasureListDTO> actual =
measureSetService.getMeasuresByMeasureSetId(MEASURE_SET_ID, true, null);

assertEquals("Ready", actual.get(0).getReviewStatus());
assertEquals("", actual.get(1).getReviewStatus());
assertEquals("", actual.get(2).getReviewStatus());
}

@Test
void testGetMeasuresByMeasureSetIdSkipsReviewLookupWhenNoMeasures() {
when(measureSetRepository.findMeasuresByMeasureSetId(MEASURE_SET_ID, true, null))
.thenReturn(List.of());

List<MeasureListDTO> actual =
measureSetService.getMeasuresByMeasureSetId(MEASURE_SET_ID, true, null);

assertTrue(actual.isEmpty());
verify(measureReviewRepository, never()).findAllByMeasureSetId(anyString());
}

@Test
void testGetRecentMeasuresByMeasureSetIdReturnsMeasuresInOrder() {
List<String> measureSetIds = List.of("set1", "set2");
Expand Down
Loading
Loading