Cleanup member-match handler (#4431)

Cleanup member-match helper.

Pass RequestDetails down for partitioning context.
This commit is contained in:
michaelabuckley 2023-01-13 15:49:41 -05:00 committed by GitHub
parent 21b1820294
commit 0d79d2026e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 221 deletions

1
.gitignore vendored
View File

@ -168,3 +168,4 @@ Snap.*
/database/
/.run/

View File

@ -12,7 +12,7 @@ import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4;
import ca.uhn.fhir.jpa.graphql.GraphQLProvider;
import ca.uhn.fhir.jpa.graphql.GraphQLProviderWithIntrospection;
import ca.uhn.fhir.jpa.provider.JpaSystemProvider;
import ca.uhn.fhir.jpa.provider.r4.IConsentExtensionProvider;
import ca.uhn.fhir.jpa.provider.r4.IMemberMatchConsentHook;
import ca.uhn.fhir.jpa.provider.r4.MemberMatchR4ResourceProvider;
import ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper;
import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl;
@ -105,7 +105,7 @@ public class JpaR4Config {
@Autowired IFhirResourceDao<Coverage> theCoverageDao,
@Autowired IFhirResourceDao<Patient> thePatientDao,
@Autowired IFhirResourceDao<Consent> theConsentDao,
@Autowired(required = false) IConsentExtensionProvider theExtensionProvider
@Autowired(required = false) IMemberMatchConsentHook theExtensionProvider
) {
return new MemberMatcherR4Helper(
theContext,

View File

@ -20,22 +20,34 @@ package ca.uhn.fhir.jpa.provider.r4;
* #L%
*/
import ca.uhn.fhir.util.ExtensionUtil;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
public interface IConsentExtensionProvider {
/**
* Hook for Consent pre-save additions.
*
* @deprecated - we just use Consumer now
* TODO delete this.
*/
@Deprecated(since = "6.3.6", forRemoval = true)
public interface IConsentExtensionProvider extends IMemberMatchConsentHook {
Logger ourLog = LoggerFactory.getLogger(IConsentExtensionProvider.class);
Collection<IBaseExtension> getConsentExtension(IBaseResource theConsentResource);
default void accept(IBaseResource theResource) {
Collection<IBaseExtension> extensions = getConsentExtension(theResource);
for (IBaseExtension ext : extensions) {
IBaseExtension<?, ?> e = ExtensionUtil.addExtension(theResource, ext.getUrl());
e.setValue(ext.getValue());
}
ourLog.trace("{} extension(s) added to Consent", extensions.size());
}
/**
* Takes a Consent resource and returns a collection of Extensions that will
* be added to the base resource.
*
* @param theConsentResource - the consent resource
* @return - a collection of resources (or an empty collection if none).
*/
default Collection<IBaseExtension> getConsentExtension(IBaseResource theConsentResource) {
return Collections.emptyList();
};
}

View File

@ -0,0 +1,11 @@
package ca.uhn.fhir.jpa.provider.r4;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.function.Consumer;
/**
* Pre-save hook for Consent saved during $member-match.
*/
public interface IMemberMatchConsentHook extends Consumer<IBaseResource> {
}

View File

@ -34,7 +34,7 @@ import org.hl7.fhir.r4.model.Coverage;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import javax.servlet.http.HttpServletRequest;
import javax.annotation.Nullable;
import java.util.Optional;
public class MemberMatchR4ResourceProvider {
@ -76,32 +76,32 @@ public class MemberMatchR4ResourceProvider {
RequestDetails theRequestDetails
) {
return doMemberMatchOperation(theServletRequest, theMemberPatient, oldCoverage, newCoverage, theConsent, theRequestDetails);
return doMemberMatchOperation(theMemberPatient, oldCoverage, newCoverage, theConsent, theRequestDetails);
}
private Parameters doMemberMatchOperation(HttpServletRequest theServletRequest, Patient theMemberPatient,
Coverage theCoverageToMatch, Coverage theCoverageToLink, Consent theConsent, RequestDetails theRequestDetails) {
private Parameters doMemberMatchOperation(Patient theMemberPatient,
Coverage theCoverageToMatch, Coverage theCoverageToLink, Consent theConsent, RequestDetails theRequestDetails) {
validateParams(theMemberPatient, theCoverageToMatch, theCoverageToLink, theConsent);
Optional<Coverage> coverageOpt = myMemberMatcherR4Helper.findMatchingCoverage(theCoverageToMatch);
if ( ! coverageOpt.isPresent()) {
Optional<Coverage> coverageOpt = myMemberMatcherR4Helper.findMatchingCoverage(theCoverageToMatch, theRequestDetails);
if (coverageOpt.isEmpty()) {
String i18nMessage = myFhirContext.getLocalizer().getMessage(
"operation.member.match.error.coverage.not.found");
throw new UnprocessableEntityException(Msg.code(1155) + i18nMessage);
}
Coverage coverage = coverageOpt.get();
Optional<Patient> patientOpt = myMemberMatcherR4Helper.getBeneficiaryPatient(coverage);
if (! patientOpt.isPresent()) {
Optional<Patient> patientOpt = myMemberMatcherR4Helper.getBeneficiaryPatient(coverage, theRequestDetails);
if (patientOpt.isEmpty()) {
String i18nMessage = myFhirContext.getLocalizer().getMessage(
"operation.member.match.error.beneficiary.not.found");
throw new UnprocessableEntityException(Msg.code(1156) + i18nMessage);
}
Patient patient = patientOpt.get();
if (!myMemberMatcherR4Helper.validPatientMember(patient, theMemberPatient)) {
if (!myMemberMatcherR4Helper.validPatientMember(patient, theMemberPatient, theRequestDetails)) {
String i18nMessage = myFhirContext.getLocalizer().getMessage(
"operation.member.match.error.patient.not.found");
throw new UnprocessableEntityException(Msg.code(2146) + i18nMessage);
@ -120,7 +120,7 @@ public class MemberMatchR4ResourceProvider {
}
myMemberMatcherR4Helper.addMemberIdentifierToMemberPatient(theMemberPatient, patient.getIdentifierFirstRep());
myMemberMatcherR4Helper.updateConsentForMemberMatch(theConsent, patient, theMemberPatient);
myMemberMatcherR4Helper.updateConsentForMemberMatch(theConsent, patient, theMemberPatient, theRequestDetails);
return myMemberMatcherR4Helper.buildSuccessReturnParameters(theMemberPatient, theCoverageToLink, theConsent);
}
@ -133,7 +133,7 @@ public class MemberMatchR4ResourceProvider {
validateConsentParam(theConsent);
}
private void validateParam(Object theParam, String theParamName) {
private void validateParam(@Nullable Object theParam, String theParamName) {
if (theParam == null) {
String i18nMessage = myFhirContext.getLocalizer().getMessage(
"operation.member.match.error.missing.parameter", theParamName);

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.i18n.Msg;
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.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
@ -13,7 +14,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ParametersUtil;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -21,7 +21,6 @@ import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.Coverage;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
@ -29,9 +28,9 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static ca.uhn.fhir.rest.api.Constants.PARAM_CONSENT;
import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_IDENTIFIER;
@ -59,7 +58,7 @@ import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
*/
public class MemberMatcherR4Helper {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MemberMatcherR4Helper.class);
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MemberMatcherR4Helper.class);
private static final String OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v2-0203";
private static final String OUT_COVERAGE_IDENTIFIER_CODE = "MB";
@ -67,16 +66,14 @@ public class MemberMatcherR4Helper {
private static final String COVERAGE_TYPE = "Coverage";
private static final String CONSENT_POLICY_REGULAR_TYPE = "regular";
private static final String CONSENT_POLICY_SENSITIVE_TYPE = "sensitive";
public static final String CONSENT_IDENTIFIER_CODE_SYSTEM = "https://smilecdr.com/fhir/ns/member-match-fixme";
public static final String CONSENT_IDENTIFIER_CODE_SYSTEM = "https://smilecdr.com/fhir/ns/member-match-source-client";
private final FhirContext myFhirContext;
private final IFhirResourceDao<Coverage> myCoverageDao;
private final IFhirResourceDao<Patient> myPatientDao;
private final IFhirResourceDao<Consent> myConsentDao;
// by default, not provided
// but if it is, extensions can be added to Consent on $member-match
@Nullable
private final IConsentExtensionProvider myIConsentExtensionProvider;
/** A hook to modify the Consent before save */
private final Consumer<IBaseResource> myConsentModifier;
private boolean myRegularFilterSupported = false;
@ -85,27 +82,27 @@ public class MemberMatcherR4Helper {
IFhirResourceDao<Coverage> theCoverageDao,
IFhirResourceDao<Patient> thePatientDao,
IFhirResourceDao<Consent> theConsentDao,
@Nullable IConsentExtensionProvider theExtensionProvider
@Nullable IMemberMatchConsentHook theConsentModifier
) {
myFhirContext = theContext;
myConsentDao = theConsentDao;
myPatientDao = thePatientDao;
myCoverageDao = theCoverageDao;
myIConsentExtensionProvider = theExtensionProvider;
myConsentModifier = (theConsentModifier != null) ? theConsentModifier : noop -> {};
}
/**
* Find Coverage matching the received member (Patient) by coverage id or by coverage identifier only
*/
public Optional<Coverage> findMatchingCoverage(Coverage theCoverageToMatch) {
public Optional<Coverage> findMatchingCoverage(Coverage theCoverageToMatch, RequestDetails theRequestDetails) {
// search by received old coverage id
List<IBaseResource> foundCoverages = findCoverageByCoverageId(theCoverageToMatch);
List<IBaseResource> foundCoverages = findCoverageByCoverageId(theCoverageToMatch, theRequestDetails);
if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) {
return Optional.of((Coverage) foundCoverages.get(0));
}
// search by received old coverage identifier
foundCoverages = findCoverageByCoverageIdentifier(theCoverageToMatch);
foundCoverages = findCoverageByCoverageIdentifier(theCoverageToMatch, theRequestDetails);
if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) {
return Optional.of((Coverage) foundCoverages.get(0));
}
@ -114,7 +111,7 @@ public class MemberMatcherR4Helper {
}
private List<IBaseResource> findCoverageByCoverageIdentifier(Coverage theCoverageToMatch) {
private List<IBaseResource> findCoverageByCoverageIdentifier(Coverage theCoverageToMatch, RequestDetails theRequestDetails) {
TokenOrListParam identifierParam = new TokenOrListParam();
for (Identifier identifier : theCoverageToMatch.getIdentifier()) {
identifierParam.add(identifier.getSystem(), identifier.getValue());
@ -122,7 +119,7 @@ public class MemberMatcherR4Helper {
SearchParameterMap paramMap = new SearchParameterMap()
.add("identifier", identifierParam);
ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap);
ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap, theRequestDetails);
return retVal.getAllResources();
}
@ -133,21 +130,23 @@ public class MemberMatcherR4Helper {
}
private List<IBaseResource> findCoverageByCoverageId(Coverage theCoverageToMatch) {
private List<IBaseResource> findCoverageByCoverageId(Coverage theCoverageToMatch, RequestDetails theRequestDetails) {
SearchParameterMap paramMap = new SearchParameterMap()
.add("_id", new StringParam(theCoverageToMatch.getId()));
ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap);
ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap, theRequestDetails);
return retVal.getAllResources();
}
public void updateConsentForMemberMatch(Consent theConsent, Patient thePatient, Patient theMemberPatient) {
addClientIdAsExtensionToConsentIfAvailable(theConsent);
public void updateConsentForMemberMatch(Consent theConsent, Patient thePatient, Patient theMemberPatient, RequestDetails theRequestDetails) {
addIdentifierToConsent(theConsent, theMemberPatient);
updateConsentPatientAndPerformer(theConsent, thePatient);
myConsentModifier.accept(theConsent);
// save the resource
myConsentDao.create(theConsent);
// WIPMB REVIEW QUESTION Which partition should we target?
// Will RequestTenantPartitionInterceptor or PatientIdPartitionInterceptor do the right thing?
// Can we use the userdata field to hint at target partition?
myConsentDao.create(theConsent, theRequestDetails);
}
public Parameters buildSuccessReturnParameters(Patient theMemberPatient, Coverage theCoverage, Consent theConsent) {
@ -160,24 +159,25 @@ public class MemberMatcherR4Helper {
}
private Identifier getIdentifier(Patient theMemberPatient) {
List<Identifier> memberIdentifiers = theMemberPatient.getIdentifier();
if (memberIdentifiers != null && memberIdentifiers.size() > 0) {
for (Identifier memberIdentifier : memberIdentifiers) {
List<Coding> typeCodings = memberIdentifier.getType().getCoding();
if (memberIdentifier.getType() != null && typeCodings != null && typeCodings.size() > 0) {
for (Coding typeCoding : typeCodings) {
if (typeCoding.getCode().equals("MB")) {
return memberIdentifier;
}
}
}
}
}
String i18nMessage = myFhirContext.getLocalizer().getMessage(
"operation.member.match.error.beneficiary.without.identifier");
throw new UnprocessableEntityException(Msg.code(2219) + i18nMessage);
return theMemberPatient.getIdentifier()
.stream()
.filter(this::isTypeMB)
.findFirst()
.orElseThrow(()->{
String i18nMessage = myFhirContext.getLocalizer().getMessage(
"operation.member.match.error.beneficiary.without.identifier");
return new UnprocessableEntityException(Msg.code(2219) + i18nMessage);
});
}
private boolean isTypeMB(Identifier theMemberIdentifier) {
return theMemberIdentifier.getType() != null &&
theMemberIdentifier.getType().getCoding()
.stream()
.anyMatch(typeCoding->typeCoding.getCode().equals("MB"));
}
public void addMemberIdentifierToMemberPatient(Patient theMemberPatient, Identifier theNewIdentifier) {
Coding coding = new Coding()
.setSystem(OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM)
@ -198,31 +198,7 @@ public class MemberMatcherR4Helper {
theMemberPatient.addIdentifier(newIdentifier);
}
/**
* If there is a client id
*
* @param theConsent - the consent to modify
*/
private void addClientIdAsExtensionToConsentIfAvailable(Consent theConsent) {
if (myIConsentExtensionProvider != null) {
Collection<IBaseExtension> extensions = myIConsentExtensionProvider.getConsentExtension(theConsent);
for (IBaseExtension ext : extensions) {
if (ext instanceof Extension) {
theConsent.addExtension((Extension) ext);
} else {
Extension extR4 = new Extension();
extR4.setUrl(ext.getUrl());
extR4.setValue(ext.getValue());
theConsent.addExtension(extR4);
}
}
ourLog.trace("{} extension(s) added to Consent", extensions.size());
}
}
public Optional<Patient> getBeneficiaryPatient(Coverage theCoverage) {
public Optional<Patient> getBeneficiaryPatient(Coverage theCoverage, RequestDetails theRequestDetails) {
if (theCoverage.getBeneficiaryTarget() == null && theCoverage.getBeneficiary() == null) {
return Optional.empty();
}
@ -245,15 +221,15 @@ public class MemberMatcherR4Helper {
return Optional.empty();
}
Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference()));
Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference()), theRequestDetails);
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) {
public boolean validPatientMember(Patient thePatientFromContract, Patient thePatientToMatch, RequestDetails theRequestDetails) {
if (thePatientFromContract == null || thePatientFromContract.getIdElement() == null || thePatientToMatch == null) {
return false;
}
StringOrListParam familyName = new StringOrListParam();
@ -263,7 +239,7 @@ public class MemberMatcherR4Helper {
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);
ca.uhn.fhir.rest.api.server.IBundleProvider bundle = myPatientDao.search(map, theRequestDetails);
for (IBaseResource patientResource : bundle.getAllResources()) {
IIdType patientId = patientResource.getIdElement().toUnqualifiedVersionless();
if (patientId.getValue().equals(thePatientFromContract.getIdElement().toUnqualifiedVersionless().getValue())) {
@ -286,18 +262,16 @@ public class MemberMatcherR4Helper {
}
/**
* This is filtering the Consent policy data. The rule is specified in
* https://build.fhir.org/ig/HL7/davinci-ehrx/StructureDefinition-hrex-consent.html#notes
* The consent policy rules are
* <a href="https://build.fhir.org/ig/HL7/davinci-ehrx/StructureDefinition-hrex-consent.html#notes">
* described here.</a>
*/
private boolean validConsentPolicy(String thePolicyUri) {
String policyTypes = StringUtils.substringAfterLast(thePolicyUri, "#");
if (policyTypes.equals(CONSENT_POLICY_SENSITIVE_TYPE)) {
return true;
}
if (policyTypes.equals(CONSENT_POLICY_REGULAR_TYPE) && myRegularFilterSupported) {
return true;
}
return false;
return policyTypes.equals(CONSENT_POLICY_REGULAR_TYPE) && myRegularFilterSupported;
}
private void addIdentifierToConsent(Consent theConsent, Patient thePatient) {

View File

@ -1,64 +1,60 @@
package ca.uhn.fhir.jpa.provider;
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.r4.IConsentExtensionProvider;
import ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper;
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.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.test.util.LogbackCaptureTestExtension;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.Coverage;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Extension;
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;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static ca.uhn.fhir.jpa.provider.r4.MemberMatcherR4Helper.CONSENT_IDENTIFIER_CODE_SYSTEM;
import static ca.uhn.fhir.rest.api.Constants.PARAM_CONSENT;
import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_IDENTIFIER;
import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_PATIENT;
import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
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.never;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -66,11 +62,8 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class MemberMatcherR4HelperTest {
private static final Logger ourLog = (Logger) LoggerFactory.getLogger(MemberMatcherR4Helper.class);
@Mock
private ListAppender<ILoggingEvent> myAppender;
@RegisterExtension
LogbackCaptureTestExtension myLogCapture = new LogbackCaptureTestExtension((Logger) MemberMatcherR4Helper.ourLog, Level.TRACE);
@Spy
private final FhirContext myFhirContext = FhirContext.forR4();
@Mock
@ -81,6 +74,7 @@ public class MemberMatcherR4HelperTest {
private IFhirResourceDao<Consent> myConsentDao;
private MemberMatcherR4Helper myHelper;
RequestDetails myRequestDetails = new SystemRequestDetails();
@BeforeEach
public void before() {
@ -91,13 +85,6 @@ public class MemberMatcherR4HelperTest {
myConsentDao,
null // extension provider
);
ourLog.addAppender(myAppender);
}
@AfterEach
public void after() {
ourLog.detachAppender(myAppender);
}
@Mock private Coverage myCoverageToMatch;
@ -114,13 +101,13 @@ public class MemberMatcherR4HelperTest {
@Test
void findMatchingCoverageMatchByIdReturnsMatched() {
when(myCoverageToMatch.getId()).thenReturn("cvg-to-match-id");
when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider);
when(myCoverageDao.search(isA(SearchParameterMap.class), same(myRequestDetails))).thenReturn(myBundleProvider);
when(myBundleProvider.getAllResources()).thenReturn(Collections.singletonList(myMatchedCoverage));
Optional<Coverage> result = myHelper.findMatchingCoverage(myCoverageToMatch);
Optional<Coverage> result = myHelper.findMatchingCoverage(myCoverageToMatch, myRequestDetails);
assertEquals(Optional.of(myMatchedCoverage), result);
verify(myCoverageDao).search(mySearchParameterMapCaptor.capture());
verify(myCoverageDao).search(mySearchParameterMapCaptor.capture(), same(myRequestDetails));
SearchParameterMap spMap = mySearchParameterMapCaptor.getValue();
assertTrue(spMap.containsKey("_id"));
List<List<IQueryParameterType>> listListParams = spMap.get("_id");
@ -135,14 +122,14 @@ public class MemberMatcherR4HelperTest {
void findMatchingCoverageMatchByIdentifierReturnsMatched() {
when(myCoverageToMatch.getId()).thenReturn("non-matching-id");
when(myCoverageToMatch.getIdentifier()).thenReturn(Collections.singletonList(myMatchingIdentifier));
when(myCoverageDao.search(isA(SearchParameterMap.class))).thenReturn(myBundleProvider);
when(myCoverageDao.search(isA(SearchParameterMap.class), same(myRequestDetails))).thenReturn(myBundleProvider);
when(myBundleProvider.getAllResources()).thenReturn(
Collections.emptyList(), Collections.singletonList(myMatchedCoverage));
Optional<Coverage> result = myHelper.findMatchingCoverage(myCoverageToMatch);
Optional<Coverage> result = myHelper.findMatchingCoverage(myCoverageToMatch, myRequestDetails);
assertEquals(Optional.of(myMatchedCoverage), result);
verify(myCoverageDao, times(2)).search(mySearchParameterMapCaptor.capture());
verify(myCoverageDao, times(2)).search(mySearchParameterMapCaptor.capture(), same(myRequestDetails));
List<SearchParameterMap> spMap = mySearchParameterMapCaptor.getAllValues();
assertTrue(spMap.get(0).containsKey("_id"));
assertTrue(spMap.get(1).containsKey("identifier"));
@ -159,10 +146,10 @@ public class MemberMatcherR4HelperTest {
void findMatchingCoverageNoMatchReturnsEmpty() {
when(myCoverageToMatch.getId()).thenReturn("non-matching-id");
when(myCoverageToMatch.getIdentifier()).thenReturn(Collections.singletonList(myMatchingIdentifier));
when(myCoverageDao.search(any(SearchParameterMap.class))).thenReturn(myBundleProvider);
when(myCoverageDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenReturn(myBundleProvider);
when(myBundleProvider.getAllResources()).thenReturn(Collections.emptyList(), Collections.emptyList());
Optional<Coverage> result = myHelper.findMatchingCoverage(myCoverageToMatch);
Optional<Coverage> result = myHelper.findMatchingCoverage(myCoverageToMatch, myRequestDetails);
assertFalse(result.isPresent());
}
@ -245,7 +232,7 @@ public class MemberMatcherR4HelperTest {
when(coverage.getBeneficiaryTarget()).thenReturn(null);
when(coverage.getBeneficiary()).thenReturn(null);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails);
assertFalse(result.isPresent());
}
@ -256,7 +243,7 @@ public class MemberMatcherR4HelperTest {
when(coverage.getBeneficiary()).thenReturn(null);
when(coverage.getBeneficiaryTarget()).thenReturn(new Patient());
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails);
assertFalse(result.isPresent());
}
@ -267,7 +254,7 @@ public class MemberMatcherR4HelperTest {
Patient patient = new Patient().setIdentifier(Collections.singletonList(new Identifier()));
when(coverage.getBeneficiaryTarget()).thenReturn(patient);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails);
assertTrue(result.isPresent());
assertEquals(patient, result.get());
@ -280,7 +267,7 @@ public class MemberMatcherR4HelperTest {
when(coverage.getBeneficiaryTarget()).thenReturn(null);
when(coverage.getBeneficiary().getResource()).thenReturn(patient);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails);
assertTrue(result.isPresent());
assertEquals(patient, result.get());
@ -292,7 +279,7 @@ public class MemberMatcherR4HelperTest {
when(coverage.getBeneficiaryTarget()).thenReturn(null);
when(coverage.getBeneficiary()).thenReturn(new Reference());
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage);
Optional<Patient> result = myHelper.getBeneficiaryPatient(coverage, myRequestDetails);
assertFalse(result.isPresent());
}
@ -304,11 +291,10 @@ public class MemberMatcherR4HelperTest {
when(coverage.getBeneficiary().getResource()).thenReturn(null);
when(coverage.getBeneficiary().getReference()).thenReturn("patient-id");
myHelper.getBeneficiaryPatient(coverage);
myHelper.getBeneficiaryPatient(coverage, myRequestDetails);
verify(myPatientDao).read(new IdDt("patient-id"));
verify(myPatientDao).read(new IdDt("patient-id"), myRequestDetails);
}
}
/**
@ -317,19 +303,17 @@ public class MemberMatcherR4HelperTest {
@Nested
public class TestValidPatientMember {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Coverage coverage;
private Patient patient;
private final Patient patient = new Patient();
@Test
void noPatientFoundFromContractReturnsFalse() {
boolean result = myHelper.validPatientMember(null, patient);
boolean result = myHelper.validPatientMember(null, patient, myRequestDetails);
assertFalse(result);
}
@Test
void noPatientFoundFromPatientMemberReturnsFalse() {
boolean result = myHelper.validPatientMember(patient, null);
boolean result = myHelper.validPatientMember(patient, null, myRequestDetails);
assertFalse(result);
}
@ -337,11 +321,11 @@ public class MemberMatcherR4HelperTest {
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 -> {
when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123")));
return provider;
});
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails);
assertFalse(result);
}
@ -350,11 +334,11 @@ public class MemberMatcherR4HelperTest {
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 -> {
when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123")));
return provider;
});
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails);
assertFalse(result);
}
@ -362,11 +346,11 @@ public class MemberMatcherR4HelperTest {
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 -> {
when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(new Patient().setId("B123")));
return provider;
});
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails);
assertFalse(result);
}
@ -374,11 +358,11 @@ public class MemberMatcherR4HelperTest {
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 -> {
when(myPatientDao.search(any(SearchParameterMap.class), same(myRequestDetails))).thenAnswer(t -> {
IBundleProvider provider = new SimpleBundleProvider(Collections.singletonList(patientFromContractFound));
return provider;
});
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch);
boolean result = myHelper.validPatientMember(patientFromContractFound, patientFromMemberMatch, myRequestDetails);
assertTrue(result);
}
@ -404,9 +388,6 @@ public class MemberMatcherR4HelperTest {
@Nested
public class TestValidvalidConsentDataAccess {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Coverage coverage;
private Patient patient;
private Consent consent;
@Test
@ -521,45 +502,10 @@ public class MemberMatcherR4HelperTest {
return patient;
}
private void verifyConsentUpdatedAfterMemberMatch(
Consent theConsent,
Patient thePatient,
Patient theMemberPatient,
List<Extension> theSavedExtensions
) {
// check consent identifier
assertEquals(1, theConsent.getIdentifier().size());
assertEquals(CONSENT_IDENTIFIER_CODE_SYSTEM, theConsent.getIdentifier().get(0).getSystem());
assertNotNull(theConsent.getIdentifier().get(0).getValue());
assertEquals(theConsent.getIdentifier().get(0).getValue(), theMemberPatient.getIdentifier().get(0).getValue());
// check consent patient info
String patientRef = thePatient.getIdElement().toUnqualifiedVersionless().getValue();
assertEquals(patientRef, theConsent.getPatient().getReference());
assertEquals(patientRef, theConsent.getPerformer().get(0).getReference());
if (!theSavedExtensions.isEmpty()) {
// check consent extensions
assertNotNull(theConsent.getExtension());
assertEquals(theSavedExtensions.size(), theConsent.getExtension().size());
for (Extension ext : theSavedExtensions) {
boolean found = false;
for (Extension consentExt : theConsent.getExtension()) {
if (consentExt.getUrl().equals(ext.getUrl())
&& consentExt.getValue().equals(ext.getValue())) {
found = true;
break;
}
}
assertTrue(found,
"Extension " + ext.getUrl() + "|" + ext.getValue().toString() + " not found"
);
}
}
}
@Nested
public class MemberMatchWithoutConsentProvider {
@Captor
private ArgumentCaptor<Consent> myDaoCaptor;
@Test
public void updateConsentForMemberMatch_noProvider_addsIdentifierUpdatePatientButNotExtensionAndSaves() {
@ -569,19 +515,26 @@ public class MemberMatcherR4HelperTest {
Patient patient = createPatientForMemberMatchUpdate(false);
Patient memberPatient = createPatientForMemberMatchUpdate(true);
ourLog.setLevel(Level.TRACE);
// test
myHelper.updateConsentForMemberMatch(consent, patient, memberPatient);
myHelper.updateConsentForMemberMatch(consent, patient, memberPatient, myRequestDetails);
// verify
verify(myAppender, never())
.doAppend(any(ILoggingEvent.class));
ArgumentCaptor<Consent> consentCaptor = ArgumentCaptor.forClass(Consent.class);
verify(myConsentDao).create(consentCaptor.capture());
Consent saved = consentCaptor.getValue();
verifyConsentUpdatedAfterMemberMatch(saved, patient, memberPatient, Collections.emptyList());
verify(myConsentDao).create(myDaoCaptor.capture(), same(myRequestDetails));
Consent saved = myDaoCaptor.getValue();
// check consent identifier
assertEquals(1, saved.getIdentifier().size());
assertEquals(MemberMatcherR4Helper.CONSENT_IDENTIFIER_CODE_SYSTEM, saved.getIdentifier().get(0).getSystem());
assertNotNull(saved.getIdentifier().get(0).getValue());
assertEquals(saved.getIdentifier().get(0).getValue(), memberPatient.getIdentifier().get(0).getValue());
// check consent patient info
String patientRef = patient.getIdElement().toUnqualifiedVersionless().getValue();
assertEquals(patientRef, saved.getPatient().getReference());
assertEquals(patientRef, saved.getPerformer().get(0).getReference());
assertThat(myLogCapture.getLogEvents(), empty());
}
}
@ -589,6 +542,8 @@ public class MemberMatcherR4HelperTest {
public class MemberMatchWithConsentProvider {
@Mock
private IConsentExtensionProvider myExtensionProvider;
@Captor
private ArgumentCaptor<Consent> myHookCaptor;
@BeforeEach
public void before() {
@ -607,31 +562,17 @@ public class MemberMatcherR4HelperTest {
Consent consent = getConsent();
consent.addPolicy(constructConsentPolicyComponent("#sensitive"));
consent.setId("Consent/RED");
Extension ext = new Extension();
ext.setUrl("http://example.com");
ext.setValue(new StringType("value"));
Patient patient = createPatientForMemberMatchUpdate(false);
Patient memberPatient = createPatientForMemberMatchUpdate(true);
ourLog.setLevel(Level.TRACE);
// when
when(myExtensionProvider.getConsentExtension(any(IBaseResource.class)))
.thenReturn(Collections.singleton(ext));
// test
myHelper.updateConsentForMemberMatch(consent, patient, memberPatient);
myHelper.updateConsentForMemberMatch(consent, patient, memberPatient, myRequestDetails);
// verify
ArgumentCaptor<Consent> consentCaptor = ArgumentCaptor.forClass(Consent.class);
verify(myConsentDao).create(consentCaptor.capture());
Consent saved = consentCaptor.getValue();
verifyConsentUpdatedAfterMemberMatch(saved, patient, memberPatient, Collections.emptyList());
verify(myExtensionProvider).accept(myHookCaptor.capture());
ArgumentCaptor<ILoggingEvent> eventCaptor = ArgumentCaptor.forClass(ILoggingEvent.class);
verify(myAppender).doAppend(eventCaptor.capture());
ILoggingEvent event = eventCaptor.getValue();
assertEquals("1 extension(s) added to Consent", event.getFormattedMessage());
assertSame(consent, myHookCaptor.getValue());
assertThat(myLogCapture.getLogEvents(), empty());
}
}
}

View File

@ -113,13 +113,13 @@ public class LogbackCaptureTestExtension implements BeforeEachCallback, AfterEac
* Guts of beforeEach exposed for manual lifecycle.
*/
public void setUp() {
myListAppender = new ListAppender<>();
myListAppender.start();
myLogger.addAppender(myListAppender);
if (myLevel != null) {
mySavedLevel = myLogger.getLevel();
myLogger.setLevel(myLevel);
}
myListAppender = new ListAppender<>();
myListAppender.start();
myLogger.addAppender(myListAppender);
}
@Override