Extend $member match to validate matched patient (#4136)

* added validation for family name and birthdate

* making changes to tests

* update patient identifer return and validatePatientMember

* update patient identifer type for test

* update method name

* update test comment
This commit is contained in:
samuelwlee2 2022-10-18 12:19:03 -06:00 committed by GitHub
parent bd22b8d1c0
commit dac266b719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 194 additions and 100 deletions

View File

@ -210,6 +210,8 @@ public class Constants {
public static final String PARAM_MEMBER_PATIENT = "MemberPatient";
public static final String PARAM_OLD_COVERAGE = "OldCoverage";
public static final String PARAM_NEW_COVERAGE = "NewCoverage";
public static final String PARAM_MEMBER_PATIENT_NAME = PARAM_MEMBER_PATIENT + " Name";
public static final String PARAM_MEMBER_PATIENT_BIRTHDATE = PARAM_MEMBER_PATIENT + " Birthdate";
public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";

View File

@ -189,4 +189,5 @@ operation.member.match.error.coverage.not.found=Could not find coverage for memb
operation.member.match.error.beneficiary.not.found=Could not find beneficiary for coverage.
operation.member.match.error.missing.parameter=Parameter "{0}" is required.
operation.member.match.error.beneficiary.without.identifier=Coverage beneficiary does not have an identifier.
operation.member.match.error.patient.not.found=Could not find matching patient for coverage.

View File

@ -0,0 +1,4 @@
---
type: add
issue: 4133
title: "Extend $member-match to validate matched patient against family name and birthdate"

View File

@ -194,8 +194,8 @@ public abstract class BaseJpaResourceProviderPatientR4 extends JpaResourceProvid
/**
* /Patient/$member-match operation
* Basic implementation matching by coverage id or by coverage identifier. Not matching by
* Beneficiary (Patient) demographics in this version
* Basic implementation matching by coverage id or by coverage identifier. Matching by
* Beneficiary (Patient) demographics on family name and birthdate in this version
*/
@Operation(name = ProviderConstants.OPERATION_MEMBER_MATCH, canonicalUrl = "http://hl7.org/fhir/us/davinci-hrex/OperationDefinition/member-match", idempotent = false, returnParameters = {
@OperationParam(name = "MemberIdentifier", typeName = "string")
@ -240,7 +240,13 @@ public abstract class BaseJpaResourceProviderPatientR4 extends JpaResourceProvid
"operation.member.match.error.beneficiary.not.found");
throw new UnprocessableEntityException(Msg.code(1156) + i18nMessage);
}
Patient patient = patientOpt.get();
if (!myMemberMatcherR4Helper.validPatientMember(patient, theMemberPatient)) {
String i18nMessage = getContext().getLocalizer().getMessage(
"operation.member.match.error.patient.not.found");
throw new UnprocessableEntityException(Msg.code(2146) + i18nMessage);
}
if (patient.getIdentifier().isEmpty()) {
String i18nMessage = getContext().getLocalizer().getMessage(
@ -258,9 +264,9 @@ public abstract class BaseJpaResourceProviderPatientR4 extends JpaResourceProvid
validateParam(theMemberPatient, Constants.PARAM_MEMBER_PATIENT);
validateParam(theOldCoverage, Constants.PARAM_OLD_COVERAGE);
validateParam(theNewCoverage, Constants.PARAM_NEW_COVERAGE);
validateMemberPatientParam(theMemberPatient);
}
private void validateParam(Object theParam, String theParamName) {
if (theParam == null) {
String i18nMessage = getContext().getLocalizer().getMessage(
@ -269,6 +275,14 @@ public abstract class BaseJpaResourceProviderPatientR4 extends JpaResourceProvid
}
}
private void validateMemberPatientParam(Patient theMemberPatient) {
if (theMemberPatient.getName().isEmpty()) {
validateParam(null, Constants.PARAM_MEMBER_PATIENT_NAME);
}
validateParam(theMemberPatient.getName().get(0).getFamily(), Constants.PARAM_MEMBER_PATIENT_NAME);
validateParam(theMemberPatient.getBirthDate(), Constants.PARAM_MEMBER_PATIENT_BIRTHDATE);
}
/**
* Given a list of string types, return only the ID portions of any parameters passed in.

View File

@ -4,15 +4,19 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.util.ParametersUtil;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Coverage;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
@ -48,7 +52,7 @@ import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
public class MemberMatcherR4Helper {
private static final String OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v2-0203";
private static final String OUT_COVERAGE_IDENTIFIER_CODE = "UMB";
private static final String OUT_COVERAGE_IDENTIFIER_CODE = "MB";
private static final String OUT_COVERAGE_IDENTIFIER_TEXT = "Member Number";
private static final String COVERAGE_TYPE = "Coverage";
@ -66,8 +70,7 @@ public class MemberMatcherR4Helper {
}
/**
* Find Coverage matching the received member (Patient) by coverage id or by coverage identifier
* Matching by member patient demographics is not supported.
* Find Coverage matching the received member (Patient) by coverage id or by coverage identifier only
*/
public Optional<Coverage> findMatchingCoverage(Coverage theCoverageToMatch) {
// search by received old coverage id
@ -169,4 +172,28 @@ public class MemberMatcherR4Helper {
Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference()));
return Optional.ofNullable(beneficiary);
}
/**
* Matching by member patient demographics - family name and birthdate only
*/
public boolean validPatientMember(Patient thePatientFromContract, Patient thePatientToMatch) {
if (thePatientFromContract == null || thePatientFromContract.getIdElement() == null) {
return false;
}
StringOrListParam familyName = new StringOrListParam();
for (HumanName name: thePatientToMatch.getName()) {
familyName.addOr(new StringParam(name.getFamily()));
}
SearchParameterMap map = new SearchParameterMap()
.add("family", familyName)
.add("birthdate", new DateParam(thePatientToMatch.getBirthDateElement().getValueAsString()));
ca.uhn.fhir.rest.api.server.IBundleProvider bundle = myPatientDao.search(map);
for (IBaseResource patientResource: bundle.getAllResources()) {
IIdType patientId = patientResource.getIdElement().toUnqualifiedVersionless();
if ( patientId.getValue().equals(thePatientFromContract.getIdElement().toUnqualifiedVersionless().getValue())) {
return true;
}
}
return false;
}
}

View File

@ -6,8 +6,11 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import com.google.common.collect.Lists;
import org.hl7.fhir.r4.model.Coverage;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
@ -32,6 +35,7 @@ import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -47,10 +51,10 @@ class MemberMatcherR4HelperTest {
private MemberMatcherR4Helper myTestedHelper;
@Mock private Coverage myCoverageToMatch;
@Mock private Patient myPatient;
@Mock private IBundleProvider myBundleProvider;
private final Coverage myMatchedCoverage = new Coverage();
private final Coverage myMatchedCoverage = new Coverage()
.setBeneficiary(new Reference("Patient/123"));
private final Identifier myMatchingIdentifier = new Identifier()
.setSystem("identifier-system").setValue("identifier-value");
@ -237,5 +241,87 @@ class MemberMatcherR4HelperTest {
}
@Nested
public class TestValidPatientMember {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Coverage coverage;
private Patient patient;
@Test
void noPatientFoundFromContractReturnsFalse() {
boolean result = myTestedHelper.validPatientMember(null, patient);
assertFalse(result);
}
@Test
void noPatientFoundFromPatientMemberReturnsFalse() {
boolean result = myTestedHelper.validPatientMember(patient, null);
assertFalse(result);
}
@Test
void noMatchingFamilyNameReturnsFalse() {
Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "2020-01-01");
Patient patientFromContractFound = getPatientWithIDParm("A123", "Smith", "2020-01-01");
when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123")));
return provider;
});
boolean result = myTestedHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
assertFalse(result);
}
@Test
void noMatchingBirthdayReturnsFalse() {
Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "1990-01-01");
Patient patientFromContractFound = getPatientWithIDParm("A123", "Person", "2020-01-01");
when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123")));
return provider;
});
boolean result = myTestedHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
assertFalse(result);
}
@Test
void noMatchingFieldsReturnsFalse() {
Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "1990-01-01");
Patient patientFromContractFound = getPatientWithIDParm("A123", "Smith", "2020-01-01");
when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123")));
return provider;
});
boolean result = myTestedHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
assertFalse(result);
}
@Test
void patientMatchingReturnTrue() {
Patient patientFromMemberMatch = getPatientWithNoIDParm("Person", "2020-01-01");
Patient patientFromContractFound = getPatientWithIDParm("A123", "Person", "2020-01-01");
when(myPatientDao.search(any(SearchParameterMap.class))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(patientFromContractFound));
return provider;
});
boolean result = myTestedHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
assertTrue(result);
}
private Patient getPatientWithNoIDParm(String familyName, String birthdate) {
Patient patient = new Patient().setName(Lists.newArrayList(new HumanName()
.setUse(HumanName.NameUse.OFFICIAL).setFamily(familyName)))
.setBirthDateElement(new DateType(birthdate));
return patient;
}
private Patient getPatientWithIDParm(String id, String familyName, String birthdate) {
Patient patient = getPatientWithNoIDParm(familyName, birthdate);
patient.setId(id);
return patient;
}
}
}

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.apache.ResourceEntity;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.util.ParametersUtil;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
@ -14,6 +15,7 @@ import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Coverage;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Identifier;
@ -51,7 +53,9 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
private static final String EXISTING_COVERAGE_PATIENT_IDENT_VALUE = "DHU-55678";
private Identifier ourExistingCoverageIdentifier;
private Patient myPatient;
private Coverage oldCoverage; // Old Coverage (must match field)
private Coverage newCoverage; // New Coverage (must return unchanged)
@BeforeEach
public void beforeDisableResultReuse() {
@ -68,41 +72,52 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
}
@Override
@BeforeEach
public void before() throws Exception {
super.before();
myFhirContext.setParserErrorHandler(new StrictErrorHandler());
}
myPatient = (Patient) new Patient().setName(Lists.newArrayList(new HumanName()
.setUse(HumanName.NameUse.OFFICIAL).setFamily("Person")))
.setBirthDateElement(new DateType("2020-01-01"))
.setId("Patient/A123");
// Old Coverage (must match field)
oldCoverage = (Coverage) new Coverage()
.setId(EXISTING_COVERAGE_ID);
// New Coverage (must return unchanged)
newCoverage = (Coverage) new Coverage()
.setIdentifier(Lists.newArrayList(new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")))
.setId("AA87654");
}
private void createCoverageWithBeneficiary(
boolean theAssociateBeneficiaryPatient, boolean includeBeneficiaryIdentifier) {
Patient member = null;
Patient member = new Patient();
if (theAssociateBeneficiaryPatient) {
// Patient
member = new Patient().setName(Lists.newArrayList(new HumanName()
.setUse(HumanName.NameUse.OFFICIAL).setFamily("Person").addGiven("Patricia").addGiven("Ann")));
member.setName(Lists.newArrayList(new HumanName()
.setUse(HumanName.NameUse.OFFICIAL).setFamily("Person")))
.setBirthDateElement(new DateType("2020-01-01"))
.setId("Patient/A123");
if (includeBeneficiaryIdentifier) {
member.setIdentifier(Collections.singletonList(new Identifier()
.setSystem(EXISTING_COVERAGE_PATIENT_IDENT_SYSTEM).setValue(EXISTING_COVERAGE_PATIENT_IDENT_VALUE)));
}
myClient.create().resource(member).execute().getId().toUnqualifiedVersionless().getValue();
member.setActive(true);
myClient.update().resource(member).execute();
}
// Coverage
ourExistingCoverageIdentifier = new Identifier()
.setSystem(EXISTING_COVERAGE_IDENT_SYSTEM).setValue(EXISTING_COVERAGE_IDENT_VALUE);
Coverage ourExistingCoverage = new Coverage()
Coverage ourExistingCoverage = (Coverage) new Coverage()
.setStatus(Coverage.CoverageStatus.ACTIVE)
.setIdentifier(Collections.singletonList(ourExistingCoverageIdentifier));
if (theAssociateBeneficiaryPatient) {
// this doesn't work
// myOldCoverage.setBeneficiaryTarget(patient)
ourExistingCoverage.setBeneficiary(new Reference(member))
.setId(EXISTING_COVERAGE_ID);
}
@ -115,20 +130,7 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
public void testMemberMatchByCoverageId() throws Exception {
createCoverageWithBeneficiary(true, true);
// patient doesn't participate in match
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
// Old Coverage
Coverage oldCoverage = new Coverage();
oldCoverage.setId(EXISTING_COVERAGE_ID); // must match field
// New Coverage (must return unchanged)
Coverage newCoverage = new Coverage();
newCoverage.setId("AA87654");
newCoverage.setIdentifier(Lists.newArrayList(
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
Parameters parametersResponse = performOperation(ourServerBase + ourQuery,
EncodingEnum.JSON, inputParameters);
@ -141,47 +143,39 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
public void testCoverageNoBeneficiaryReturns422() throws Exception {
createCoverageWithBeneficiary(false, false);
// patient doesn't participate in match
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
// Old Coverage
Coverage oldCoverage = new Coverage();
oldCoverage.setId(EXISTING_COVERAGE_ID); // must match field
// New Coverage (must return unchanged)
Coverage newCoverage = new Coverage();
newCoverage.setId("AA87654");
newCoverage.setIdentifier(Lists.newArrayList(
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
"Could not find beneficiary for coverage.");
}
@Test
public void testCoverageBeneficiaryNoIdentifierReturns422() throws Exception {
createCoverageWithBeneficiary(true, false);
// patient doesn't participate in match
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
// Old Coverage
Coverage oldCoverage = new Coverage();
oldCoverage.setId(EXISTING_COVERAGE_ID); // must match field
// New Coverage (must return unchanged)
Coverage newCoverage = new Coverage();
newCoverage.setId("AA87654");
newCoverage.setIdentifier(Lists.newArrayList(
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
"Coverage beneficiary does not have an identifier.");
}
@Test
public void testCoverageNoMatchingPatientFamilyNameReturns422() throws Exception {
createCoverageWithBeneficiary(true, true);
myPatient.setName(Lists.newArrayList(new HumanName().setUse(HumanName.NameUse.OFFICIAL).setFamily("Smith")));
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
"Could not find matching patient for coverage.");
}
@Test
public void testCoverageNoMatchingPatientBirthdateReturns422() throws Exception {
createCoverageWithBeneficiary(true, false);
myPatient.setBirthDateElement(new DateType("2000-01-01"));
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
"Could not find matching patient for coverage.");
}
@Nested
public class ValidateParameterErrors {
@ -229,21 +223,7 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
public void testMemberMatchByCoverageIdentifier() throws Exception {
createCoverageWithBeneficiary(true, true);
// patient doesn't participate in match
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
// Old Coverage
Coverage oldCoverage = new Coverage();
oldCoverage.setId("9876B1");
oldCoverage.setIdentifier(Lists.newArrayList(ourExistingCoverageIdentifier)); // must match field
// New Coverage (must return unchanged)
Coverage newCoverage = new Coverage();
newCoverage.setId("AA87654");
newCoverage.setIdentifier(Lists.newArrayList(
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
Parameters parametersResponse = performOperation(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters);
validateMemberPatient(parametersResponse);
@ -299,27 +279,7 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
@Test
public void testNoCoverageMatchFound() throws Exception {
// Patient doesn't participate in match
Patient patient = new Patient().setGender(Enumerations.AdministrativeGender.FEMALE);
// Old Coverage
Coverage oldCoverage = new Coverage();
oldCoverage.setId("9876B1");
oldCoverage.setIdentifier(Lists.newArrayList(
new Identifier().setSystem("http://oldhealthplan.example.com").setValue("DH10001235")));
// New Coverage
Organization newOrg = new Organization();
newOrg.setId("Organization/ProviderOrg1");
newOrg.setName("New Health Plan");
Coverage newCoverage = new Coverage();
newCoverage.setId("AA87654");
newCoverage.getContained().add(newOrg);
newCoverage.setIdentifier(Lists.newArrayList(
new Identifier().setSystem("http://newealthplan.example.com").setValue("234567")));
Parameters inputParameters = buildInputParameters(patient, oldCoverage, newCoverage);
Parameters inputParameters = buildInputParameters(myPatient, oldCoverage, newCoverage);
performOperationExpecting422(ourServerBase + ourQuery, EncodingEnum.JSON, inputParameters,
"Could not find coverage for member");
}
@ -371,14 +331,14 @@ public class PatientMemberMatchOperationR4Test extends BaseResourceProviderR4Tes
// "coding": [
// {
// "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
// "code": "UMB",
// "code": "MB",
// "display": "Member Number",
// "userSelected": false
// }
// * ]
Coding coding = theType.getCoding().get(0);
assertEquals("http://terminology.hl7.org/CodeSystem/v2-0203", coding.getSystem());
assertEquals("UMB", coding.getCode());
assertEquals("MB", coding.getCode());
assertEquals("Member Number", coding.getDisplay());
assertFalse(coding.getUserSelected());
}