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}