001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2023 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.provider.r4; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 025import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 026import ca.uhn.fhir.model.primitive.IdDt; 027import ca.uhn.fhir.rest.api.server.RequestDetails; 028import ca.uhn.fhir.rest.param.DateParam; 029import ca.uhn.fhir.rest.param.StringOrListParam; 030import ca.uhn.fhir.rest.param.StringParam; 031import ca.uhn.fhir.rest.param.TokenOrListParam; 032import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 033import ca.uhn.fhir.util.ParametersUtil; 034import com.google.common.collect.Lists; 035import org.apache.commons.lang3.StringUtils; 036import org.hl7.fhir.instance.model.api.IBaseParameters; 037import org.hl7.fhir.instance.model.api.IBaseResource; 038import org.hl7.fhir.instance.model.api.IIdType; 039import org.hl7.fhir.r4.model.CodeableConcept; 040import org.hl7.fhir.r4.model.Coding; 041import org.hl7.fhir.r4.model.Consent; 042import org.hl7.fhir.r4.model.Coverage; 043import org.hl7.fhir.r4.model.HumanName; 044import org.hl7.fhir.r4.model.Identifier; 045import org.hl7.fhir.r4.model.Parameters; 046import org.hl7.fhir.r4.model.Patient; 047import org.hl7.fhir.r4.model.Reference; 048 049import java.util.List; 050import java.util.Optional; 051import java.util.function.Consumer; 052import javax.annotation.Nullable; 053 054import static ca.uhn.fhir.rest.api.Constants.PARAM_CONSENT; 055import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_IDENTIFIER; 056import static ca.uhn.fhir.rest.api.Constants.PARAM_MEMBER_PATIENT; 057import static ca.uhn.fhir.rest.api.Constants.PARAM_NEW_COVERAGE; 058 059public class MemberMatcherR4Helper { 060 static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MemberMatcherR4Helper.class); 061 062 private static final String OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v2-0203"; 063 private static final String OUT_COVERAGE_IDENTIFIER_CODE = "MB"; 064 private static final String OUT_COVERAGE_IDENTIFIER_TEXT = "Member Number"; 065 private static final String COVERAGE_TYPE = "Coverage"; 066 private static final String CONSENT_POLICY_REGULAR_TYPE = "regular"; 067 private static final String CONSENT_POLICY_SENSITIVE_TYPE = "sensitive"; 068 public static final String CONSENT_IDENTIFIER_CODE_SYSTEM = 069 "https://smilecdr.com/fhir/ns/member-match-source-client"; 070 071 private final FhirContext myFhirContext; 072 private final IFhirResourceDao<Coverage> myCoverageDao; 073 private final IFhirResourceDao<Patient> myPatientDao; 074 private final IFhirResourceDao<Consent> myConsentDao; 075 /** A hook to modify the Consent before save */ 076 private final Consumer<IBaseResource> myConsentModifier; 077 078 private boolean myRegularFilterSupported = false; 079 080 public MemberMatcherR4Helper( 081 FhirContext theContext, 082 IFhirResourceDao<Coverage> theCoverageDao, 083 IFhirResourceDao<Patient> thePatientDao, 084 IFhirResourceDao<Consent> theConsentDao, 085 @Nullable IMemberMatchConsentHook theConsentModifier) { 086 myFhirContext = theContext; 087 myConsentDao = theConsentDao; 088 myPatientDao = thePatientDao; 089 myCoverageDao = theCoverageDao; 090 myConsentModifier = (theConsentModifier != null) ? theConsentModifier : noop -> {}; 091 } 092 093 /** 094 * Find Coverage matching the received member (Patient) by coverage id or by coverage identifier only 095 */ 096 public Optional<Coverage> findMatchingCoverage(Coverage theCoverageToMatch, RequestDetails theRequestDetails) { 097 // search by received old coverage id 098 List<IBaseResource> foundCoverages = findCoverageByCoverageId(theCoverageToMatch, theRequestDetails); 099 if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) { 100 return Optional.of((Coverage) foundCoverages.get(0)); 101 } 102 103 // search by received old coverage identifier 104 foundCoverages = findCoverageByCoverageIdentifier(theCoverageToMatch, theRequestDetails); 105 if (foundCoverages.size() == 1 && isCoverage(foundCoverages.get(0))) { 106 return Optional.of((Coverage) foundCoverages.get(0)); 107 } 108 109 return Optional.empty(); 110 } 111 112 private List<IBaseResource> findCoverageByCoverageIdentifier( 113 Coverage theCoverageToMatch, RequestDetails theRequestDetails) { 114 TokenOrListParam identifierParam = new TokenOrListParam(); 115 for (Identifier identifier : theCoverageToMatch.getIdentifier()) { 116 identifierParam.add(identifier.getSystem(), identifier.getValue()); 117 } 118 119 SearchParameterMap paramMap = new SearchParameterMap().add("identifier", identifierParam); 120 ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap, theRequestDetails); 121 122 return retVal.getAllResources(); 123 } 124 125 private boolean isCoverage(IBaseResource theIBaseResource) { 126 return theIBaseResource.fhirType().equals(COVERAGE_TYPE); 127 } 128 129 private List<IBaseResource> findCoverageByCoverageId( 130 Coverage theCoverageToMatch, RequestDetails theRequestDetails) { 131 SearchParameterMap paramMap = new SearchParameterMap().add("_id", new StringParam(theCoverageToMatch.getId())); 132 ca.uhn.fhir.rest.api.server.IBundleProvider retVal = myCoverageDao.search(paramMap, theRequestDetails); 133 134 return retVal.getAllResources(); 135 } 136 137 public void updateConsentForMemberMatch( 138 Consent theConsent, Patient thePatient, Patient theMemberPatient, RequestDetails theRequestDetails) { 139 addIdentifierToConsent(theConsent, theMemberPatient); 140 updateConsentPatientAndPerformer(theConsent, thePatient); 141 myConsentModifier.accept(theConsent); 142 143 // Trust RequestTenantPartitionInterceptor or PatientIdPartitionInterceptor to assign the partition. 144 myConsentDao.create(theConsent, theRequestDetails); 145 } 146 147 public Parameters buildSuccessReturnParameters(Patient theMemberPatient, Coverage theCoverage, Consent theConsent) { 148 IBaseParameters parameters = ParametersUtil.newInstance(myFhirContext); 149 ParametersUtil.addParameterToParameters(myFhirContext, parameters, PARAM_MEMBER_PATIENT, theMemberPatient); 150 ParametersUtil.addParameterToParameters(myFhirContext, parameters, PARAM_NEW_COVERAGE, theCoverage); 151 ParametersUtil.addParameterToParameters(myFhirContext, parameters, PARAM_CONSENT, theConsent); 152 ParametersUtil.addParameterToParameters( 153 myFhirContext, parameters, PARAM_MEMBER_IDENTIFIER, getIdentifier(theMemberPatient)); 154 return (Parameters) parameters; 155 } 156 157 private Identifier getIdentifier(Patient theMemberPatient) { 158 return theMemberPatient.getIdentifier().stream() 159 .filter(this::isTypeMB) 160 .findFirst() 161 .orElseThrow(() -> { 162 String i18nMessage = myFhirContext 163 .getLocalizer() 164 .getMessage("operation.member.match.error.beneficiary.without.identifier"); 165 return new UnprocessableEntityException(Msg.code(2219) + i18nMessage); 166 }); 167 } 168 169 private boolean isTypeMB(Identifier theMemberIdentifier) { 170 return theMemberIdentifier.getType() != null 171 && theMemberIdentifier.getType().getCoding().stream() 172 .anyMatch(typeCoding -> typeCoding.getCode().equals("MB")); 173 } 174 175 public void addMemberIdentifierToMemberPatient(Patient theMemberPatient, Identifier theNewIdentifier) { 176 Coding coding = new Coding() 177 .setSystem(OUT_COVERAGE_IDENTIFIER_CODE_SYSTEM) 178 .setCode(OUT_COVERAGE_IDENTIFIER_CODE) 179 .setDisplay(OUT_COVERAGE_IDENTIFIER_TEXT) 180 .setUserSelected(false); 181 182 CodeableConcept concept = 183 new CodeableConcept().setCoding(Lists.newArrayList(coding)).setText(OUT_COVERAGE_IDENTIFIER_TEXT); 184 185 Identifier newIdentifier = new Identifier() 186 .setUse(Identifier.IdentifierUse.USUAL) 187 .setType(concept) 188 .setSystem(theNewIdentifier.getSystem()) 189 .setValue(theNewIdentifier.getValue()); 190 191 theMemberPatient.addIdentifier(newIdentifier); 192 } 193 194 public Optional<Patient> getBeneficiaryPatient(Coverage theCoverage, RequestDetails theRequestDetails) { 195 if (theCoverage.getBeneficiaryTarget() == null && theCoverage.getBeneficiary() == null) { 196 return Optional.empty(); 197 } 198 199 if (theCoverage.getBeneficiaryTarget() != null 200 && !theCoverage.getBeneficiaryTarget().getIdentifier().isEmpty()) { 201 return Optional.of(theCoverage.getBeneficiaryTarget()); 202 } 203 204 Reference beneficiaryRef = theCoverage.getBeneficiary(); 205 if (beneficiaryRef == null) { 206 return Optional.empty(); 207 } 208 209 if (beneficiaryRef.getResource() != null) { 210 return Optional.of((Patient) beneficiaryRef.getResource()); 211 } 212 213 if (beneficiaryRef.getReference() == null) { 214 return Optional.empty(); 215 } 216 217 Patient beneficiary = myPatientDao.read(new IdDt(beneficiaryRef.getReference()), theRequestDetails); 218 return Optional.ofNullable(beneficiary); 219 } 220 221 /** 222 * Matching by member patient demographics - family name and birthdate only 223 */ 224 public boolean validPatientMember( 225 Patient thePatientFromContract, Patient thePatientToMatch, RequestDetails theRequestDetails) { 226 if (thePatientFromContract == null 227 || thePatientFromContract.getIdElement() == null 228 || thePatientToMatch == null) { 229 return false; 230 } 231 StringOrListParam familyName = new StringOrListParam(); 232 for (HumanName name : thePatientToMatch.getName()) { 233 familyName.addOr(new StringParam(name.getFamily())); 234 } 235 SearchParameterMap map = new SearchParameterMap() 236 .add("family", familyName) 237 .add( 238 "birthdate", 239 new DateParam(thePatientToMatch.getBirthDateElement().getValueAsString())); 240 ca.uhn.fhir.rest.api.server.IBundleProvider bundle = myPatientDao.search(map, theRequestDetails); 241 for (IBaseResource patientResource : bundle.getAllResources()) { 242 IIdType patientId = patientResource.getIdElement().toUnqualifiedVersionless(); 243 if (patientId 244 .getValue() 245 .equals(thePatientFromContract 246 .getIdElement() 247 .toUnqualifiedVersionless() 248 .getValue())) { 249 return true; 250 } 251 } 252 return false; 253 } 254 255 public boolean validConsentDataAccess(Consent theConsent) { 256 if (theConsent.getPolicy().isEmpty()) { 257 return false; 258 } 259 for (Consent.ConsentPolicyComponent policyComponent : theConsent.getPolicy()) { 260 if (policyComponent.getUri() == null || !validConsentPolicy(policyComponent.getUri())) { 261 return false; 262 } 263 } 264 return true; 265 } 266 267 /** 268 * The consent policy rules are 269 * <a href="https://build.fhir.org/ig/HL7/davinci-ehrx/StructureDefinition-hrex-consent.html#notes"> 270 * described here.</a> 271 */ 272 private boolean validConsentPolicy(String thePolicyUri) { 273 String policyTypes = StringUtils.substringAfterLast(thePolicyUri, "#"); 274 if (policyTypes.equals(CONSENT_POLICY_SENSITIVE_TYPE)) { 275 return true; 276 } 277 return policyTypes.equals(CONSENT_POLICY_REGULAR_TYPE) && myRegularFilterSupported; 278 } 279 280 private void addIdentifierToConsent(Consent theConsent, Patient thePatient) { 281 String consentId = getIdentifier(thePatient).getValue(); 282 Identifier consentIdentifier = 283 new Identifier().setSystem(CONSENT_IDENTIFIER_CODE_SYSTEM).setValue(consentId); 284 theConsent.addIdentifier(consentIdentifier); 285 } 286 287 public void setRegularFilterSupported(boolean theRegularFilterSupported) { 288 myRegularFilterSupported = theRegularFilterSupported; 289 } 290 291 private void updateConsentPatientAndPerformer(Consent theConsent, Patient thePatient) { 292 String patientRef = thePatient.getIdElement().toUnqualifiedVersionless().getValue(); 293 theConsent.getPatient().setReference(patientRef); 294 theConsent.getPerformer().set(0, new Reference(patientRef)); 295 } 296}