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 @@ -481,29 +481,6 @@ p.person_uid IN (:ids)
""";


public static final String POSSIBLE_MATCH_GROUP = """
SELECT
person_uid,
STRING_AGG(CAST(mpi_person_id AS NVARCHAR(MAX)), ', ') AS mpi_person_ids,
date_identified
FROM
match_candidates
WHERE is_merge is NULL
GROUP BY
person_uid,
date_identified
ORDER BY person_uid
OFFSET :offset ROWS
FETCH NEXT :limit ROWS ONLY;
""";

public static final String PERSON_UIDS_BY_MPI_PATIENT_IDS = """
SELECT person_uid
FROM nbs_mpi_mapping
WHERE mpi_person IN (:mpiPersonIds)
AND person_uid=person_parent_uid
""";

public static final String UPDATE_MERGE_STATUS_FOR_GROUP = """
UPDATE match_candidates
SET is_merge = :isMerge
Expand Down Expand Up @@ -595,4 +572,36 @@ INSERT INTO PERSON_MERGE (
FROM person
WHERE person_parent_uid IN (:parentPersonIds)
""";

public static final String FETCH_PATIENT_NAME_AND_ADD_TIME_QUERY = """
SELECT TOP 1
p.add_time,
COALESCE(pn.first_nm, '') + ' ' + COALESCE(pn.last_nm, '') AS full_name
FROM
person p WITH (NOLOCK)
INNER JOIN
person_name pn WITH (NOLOCK)
ON pn.person_uid = p.person_uid
WHERE
p.person_uid = :personUid
ORDER BY
pn.status_time DESC;
""";


public static final String POSSIBLE_MATCH_PATIENTS = """
SELECT
person_uid,
count(mpi_person_id) AS num_of_matching,
date_identified
FROM
match_candidates
WHERE is_merge is NULL
GROUP BY
person_uid,
date_identified
ORDER BY person_uid
OFFSET :offset ROWS
FETCH NEXT :limit ROWS ONLY;
""";
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gov.cdc.nbs.deduplication.duplicates.controller;

import gov.cdc.nbs.deduplication.duplicates.model.GroupNoMergeRequest;
import gov.cdc.nbs.deduplication.duplicates.model.MergeGroupResponse;
import gov.cdc.nbs.deduplication.duplicates.model.MatchesRequireReviewResponse;
import gov.cdc.nbs.deduplication.duplicates.model.MergePatientRequest;
import gov.cdc.nbs.deduplication.duplicates.service.MergeGroupHandler;
import gov.cdc.nbs.deduplication.duplicates.service.MergePatientHandler;
Expand All @@ -14,22 +14,23 @@

@RestController
@RequestMapping("/deduplication")
public class MergeGroupController {
public class PatientMergeController {

private final MergeGroupHandler mergeGroupHandler;

private final MergePatientHandler mergePatientsHandler;

public MergeGroupController(MergeGroupHandler possibleMatchHandler, MergePatientHandler mergePatientsHandler) {
public PatientMergeController(MergeGroupHandler possibleMatchHandler, MergePatientHandler mergePatientsHandler) {
this.mergeGroupHandler = possibleMatchHandler;
this.mergePatientsHandler = mergePatientsHandler;
}

@GetMapping("/merge-groups")
public List<MergeGroupResponse> getPossibleMatchGroups(
@GetMapping("/matches/requiring-review")
public ResponseEntity<List<MatchesRequireReviewResponse>> getPotentialMatches(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "5") int size) {
return mergeGroupHandler.getMergeGroups(page, size);
List<MatchesRequireReviewResponse> matches = mergeGroupHandler.getPotentialMatches(page, size);
return ResponseEntity.ok(matches);
}

@PostMapping("/group-no-merge")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package gov.cdc.nbs.deduplication.duplicates.model;


import java.util.List;

public record PossibleMatchGroup(
public record MatchCandidateData(
String personUid,
List<String> mpiIds,
long numOfMatches,
String dateIdentified
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gov.cdc.nbs.deduplication.duplicates.model;


public record MatchesRequireReviewResponse(
String patientId,
String patientName,
String createdDate,
String identifiedDate,
long numOfMatchingRecords
) {
public MatchesRequireReviewResponse(MatchCandidateData matchCandidateData,
PatientNameAndTimeDTO patientNameAndTimeDTO) {
this(
matchCandidateData.personUid(),
patientNameAndTimeDTO.fullName(),
patientNameAndTimeDTO.addTime().toString(),
matchCandidateData.dateIdentified(),
matchCandidateData.numOfMatches()+1 // count of the matched patients + 1 for the patient itself
);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package gov.cdc.nbs.deduplication.duplicates.model;

import java.time.LocalDateTime;

public record PatientNameAndTimeDTO(LocalDateTime addTime, String fullName) {}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package gov.cdc.nbs.deduplication.duplicates.service;

import gov.cdc.nbs.deduplication.constants.QueryConstants;
import gov.cdc.nbs.deduplication.duplicates.model.PossibleMatchGroup;
import gov.cdc.nbs.deduplication.duplicates.model.MergeGroupResponse;
import gov.cdc.nbs.deduplication.seed.model.MpiPerson;
import gov.cdc.nbs.deduplication.duplicates.model.MatchCandidateData;
import gov.cdc.nbs.deduplication.duplicates.model.MatchesRequireReviewResponse;
import gov.cdc.nbs.deduplication.duplicates.model.PatientNameAndTimeDTO;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Component
Expand All @@ -28,54 +28,36 @@ public MergeGroupHandler(
this.patientRecordService = patientRecordService;
}

public List<MergeGroupResponse> getMergeGroups(int page, int size) {
public List<MatchesRequireReviewResponse> getPotentialMatches(int page, int size) {
int offset = page * size;
return getPossibleMatchGroups(offset, size).stream()
.map(possibleMatchGroup -> {
List<String> personUids = getPersonIdsByMpiIds(possibleMatchGroup.mpiIds());
personUids.add(possibleMatchGroup.personUid());
List<MpiPerson> patientRecords = patientRecordService.fetchPersonRecords(personUids);
String mostRecentName = getMostRecentNameOfTheGroup(patientRecords);
return new MergeGroupResponse(possibleMatchGroup.personUid(), possibleMatchGroup.dateIdentified(),
mostRecentName, patientRecords);
List<MatchCandidateData> matchCandidates = getMatchCandidateData(offset, size);
if (matchCandidates.isEmpty()) {
return Collections.emptyList();
}
return matchCandidates.stream()
.map(matchCandidateData -> {
PatientNameAndTimeDTO patientNameAndTimeDTO =
patientRecordService.fetchPatientNameAndAddTime(matchCandidateData.personUid());
return new MatchesRequireReviewResponse(matchCandidateData, patientNameAndTimeDTO);
})
.toList();
}

private List<PossibleMatchGroup> getPossibleMatchGroups(int offset, int limit) {
private List<MatchCandidateData> getMatchCandidateData(int offset, int limit) {
MapSqlParameterSource parameters = new MapSqlParameterSource()
.addValue("limit", limit)
.addValue("offset", offset);
return deduplicationTemplate.query(
QueryConstants.POSSIBLE_MATCH_GROUP,
QueryConstants.POSSIBLE_MATCH_PATIENTS,
parameters,
this::mapRowToPossibleMatchGroup);
this::mapRowToMatchCandidateData);
}

private PossibleMatchGroup mapRowToPossibleMatchGroup(ResultSet rs, int rowNum) throws SQLException {
private MatchCandidateData mapRowToMatchCandidateData(ResultSet rs, int rowNum) throws SQLException {
String personUid = rs.getString("person_uid");
String mpiPersonIds = rs.getString("mpi_person_ids");
long numOfMatches = rs.getInt("num_of_matching");
String dateIdentified = rs.getString("date_identified");
return new PossibleMatchGroup(personUid, Arrays.asList(mpiPersonIds.split(", ")), dateIdentified);
}

private String getMostRecentNameOfTheGroup(List<MpiPerson> mpiPersonList) {
MpiPerson oldestPersonInTheGroup = (mpiPersonList.isEmpty()) ? null : mpiPersonList.getFirst();
if (oldestPersonInTheGroup != null) {
MpiPerson.Name mostRecentName = oldestPersonInTheGroup.name().getFirst();
String givenName = mostRecentName.given().isEmpty() ? "" : mostRecentName.given().getFirst();
String familyName = mostRecentName.family() == null ? "" : mostRecentName.family();
return givenName.concat(" ").concat(familyName);
}
return "";
}

private List<String> getPersonIdsByMpiIds(List<String> mpiIds) {
return deduplicationTemplate.query(
QueryConstants.PERSON_UIDS_BY_MPI_PATIENT_IDS,
new MapSqlParameterSource("mpiPersonIds", mpiIds),
(rs, rowNum) -> rs.getString("person_uid")
);
return new MatchCandidateData(personUid, numOfMatches, dateIdentified);
}

private List<String> getMpiIdsByPersonIds(List<String> personIds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package gov.cdc.nbs.deduplication.duplicates.service;

import gov.cdc.nbs.deduplication.constants.QueryConstants;
import gov.cdc.nbs.deduplication.duplicates.model.PatientNameAndTimeDTO;
import gov.cdc.nbs.deduplication.seed.mapper.MpiPersonMapper;
import gov.cdc.nbs.deduplication.seed.model.MpiPerson;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
Expand Down Expand Up @@ -49,4 +51,20 @@ public List<MpiPerson> fetchPersonRecords(List<String> personUids) {
mpiPersonMapper);
}


public PatientNameAndTimeDTO fetchPatientNameAndAddTime(String personUid) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("personUid", personUid);
return namedParameterJdbcTemplate.queryForObject(
QueryConstants.FETCH_PATIENT_NAME_AND_ADD_TIME_QUERY,
params,
(rs, rowNum) -> {
LocalDateTime addTime = rs.getTimestamp("add_time").toLocalDateTime();
String nestedName = rs.getString("full_name");
return new PatientNameAndTimeDTO(addTime, nestedName);
}
);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.util.Arrays;
import java.util.List;

import gov.cdc.nbs.deduplication.duplicates.model.MergeGroupResponse;
import gov.cdc.nbs.deduplication.duplicates.model.MatchesRequireReviewResponse;
import gov.cdc.nbs.deduplication.duplicates.model.MergePatientRequest;
import gov.cdc.nbs.deduplication.duplicates.service.MergeGroupHandler;
import gov.cdc.nbs.deduplication.duplicates.service.MergePatientHandler;
Expand All @@ -24,7 +24,7 @@
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@ExtendWith(MockitoExtension.class)
class MergeGroupControllerTest {
class PatientMergeControllerTest {

@Mock
private MergeGroupHandler mergeGroupHandler;
Expand All @@ -33,24 +33,25 @@ class MergeGroupControllerTest {
private MergePatientHandler mergePatientHandler;

@InjectMocks
private MergeGroupController mergeGroupController;
private PatientMergeController patientMergeController;


private MockMvc mockMvc;

@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(mergeGroupController).build();
mockMvc = MockMvcBuilders.standaloneSetup(patientMergeController).build();
}

@Test
void testGetPossibleMatchGroups() throws Exception {
int page = 0;
int size = 5;
when(mergeGroupHandler.getMergeGroups(page, size)).thenReturn(expectedMergeGroupResponse());
when(mergeGroupHandler.getPotentialMatches(page, size)).thenReturn(expectedMergeGroupResponse());


// Act & Assert
mockMvc.perform(get("/deduplication/merge-groups")
mockMvc.perform(get("/deduplication/matches/requiring-review")
.param("page", String.valueOf(page))
.param("size", String.valueOf(size)))
.andExpect(status().isOk())
Expand Down Expand Up @@ -85,17 +86,7 @@ void testUpdateGroupNoMerge_Error() throws Exception {
verify(mergeGroupHandler).updateMergeStatusForGroup(100L);
}

private List<MergeGroupResponse> expectedMergeGroupResponse() {
return Arrays.asList(
new MergeGroupResponse("100", "1990-01-01", "john smith", null),
new MergeGroupResponse("200", "1990-02-02", "Andrew James", null)
);
}

private String expectedMergeGroupResponseJson() {
return "[{'personOfTheGroup':'100','dateIdentified': 1990-01-01, 'mostRecentPersonName': 'john smith'}, " +
"{'personOfTheGroup':'200','dateIdentified': 1990-02-02, 'mostRecentPersonName': 'Andrew James'}]";
}


@Test
Expand Down Expand Up @@ -148,6 +139,32 @@ void testMergeRecords_InternalServerError() throws Exception {
verify(mergePatientHandler).performMerge("survivor123", Arrays.asList("superseded1", "superseded2"));
}

private List<MatchesRequireReviewResponse> expectedMergeGroupResponse() {
return Arrays.asList(
new MatchesRequireReviewResponse("111122", "john smith", "1990-01-01", "2000-01-01", 2),
new MatchesRequireReviewResponse("111133", "Andrew James", "1990-02-02", "2000-02-02", 4)
);
}

private String expectedMergeGroupResponseJson() {
return """
[
{
"patientId": "111122",
"patientName": "john smith",
"createdDate": "1990-01-01",
"identifiedDate": "2000-01-01",
"numOfMatchingRecords": 2
},
{
"patientId": "111133",
"patientName": "Andrew James",
"createdDate": "1990-02-02",
"identifiedDate": "2000-02-02",
"numOfMatchingRecords": 4
}
]
""";
}

}
Loading
Loading