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.model.api.annotation.Description;
025import ca.uhn.fhir.rest.annotation.Operation;
026import ca.uhn.fhir.rest.annotation.OperationParam;
027import ca.uhn.fhir.rest.api.Constants;
028import ca.uhn.fhir.rest.api.server.RequestDetails;
029import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
030import ca.uhn.fhir.rest.server.provider.ProviderConstants;
031import org.hl7.fhir.r4.model.Consent;
032import org.hl7.fhir.r4.model.Coverage;
033import org.hl7.fhir.r4.model.Parameters;
034import org.hl7.fhir.r4.model.Patient;
035
036import java.util.Optional;
037import javax.annotation.Nullable;
038
039public class MemberMatchR4ResourceProvider {
040
041        private final MemberMatcherR4Helper myMemberMatcherR4Helper;
042        private final FhirContext myFhirContext;
043
044        public MemberMatchR4ResourceProvider(FhirContext theFhirContext, MemberMatcherR4Helper theMemberMatcherR4Helper) {
045                myFhirContext = theFhirContext;
046                myMemberMatcherR4Helper = theMemberMatcherR4Helper;
047        }
048
049        /**
050         * /Patient/$member-match operation
051         * Basic implementation matching by coverage id or by coverage identifier. Matching by
052         * Beneficiary (Patient) demographics on family name and birthdate in this version
053         */
054        @Operation(
055                        name = ProviderConstants.OPERATION_MEMBER_MATCH,
056                        typeName = "Patient",
057                        canonicalUrl = "http://hl7.org/fhir/us/davinci-hrex/OperationDefinition/member-match",
058                        idempotent = false,
059                        returnParameters = {@OperationParam(name = "MemberIdentifier", typeName = "string")})
060        public Parameters patientMemberMatch(
061                        javax.servlet.http.HttpServletRequest theServletRequest,
062                        @Description(
063                                                        shortDefinition =
064                                                                        "The target of the operation. Will be returned with Identifier for matched coverage added.")
065                                        @OperationParam(name = Constants.PARAM_MEMBER_PATIENT, min = 1, max = 1)
066                                        Patient theMemberPatient,
067                        @Description(shortDefinition = "Old coverage information as extracted from beneficiary's card.")
068                                        @OperationParam(name = Constants.PARAM_OLD_COVERAGE, min = 1, max = 1)
069                                        Coverage oldCoverage,
070                        @Description(
071                                                        shortDefinition =
072                                                                        "New Coverage information. Provided as a reference. Optionally returned unmodified.")
073                                        @OperationParam(name = Constants.PARAM_NEW_COVERAGE, min = 1, max = 1)
074                                        Coverage newCoverage,
075                        @Description(
076                                                        shortDefinition =
077                                                                        "Consent information. Consent held by the system seeking the match that grants permission to access the patient information.")
078                                        @OperationParam(name = Constants.PARAM_CONSENT, min = 1, max = 1)
079                                        Consent theConsent,
080                        RequestDetails theRequestDetails) {
081                return doMemberMatchOperation(theMemberPatient, oldCoverage, newCoverage, theConsent, theRequestDetails);
082        }
083
084        private Parameters doMemberMatchOperation(
085                        Patient theMemberPatient,
086                        Coverage theCoverageToMatch,
087                        Coverage theCoverageToLink,
088                        Consent theConsent,
089                        RequestDetails theRequestDetails) {
090
091                validateParams(theMemberPatient, theCoverageToMatch, theCoverageToLink, theConsent);
092
093                Optional<Coverage> coverageOpt =
094                                myMemberMatcherR4Helper.findMatchingCoverage(theCoverageToMatch, theRequestDetails);
095                if (coverageOpt.isEmpty()) {
096                        String i18nMessage =
097                                        myFhirContext.getLocalizer().getMessage("operation.member.match.error.coverage.not.found");
098                        throw new UnprocessableEntityException(Msg.code(1155) + i18nMessage);
099                }
100                Coverage coverage = coverageOpt.get();
101
102                Optional<Patient> patientOpt = myMemberMatcherR4Helper.getBeneficiaryPatient(coverage, theRequestDetails);
103                if (patientOpt.isEmpty()) {
104                        String i18nMessage =
105                                        myFhirContext.getLocalizer().getMessage("operation.member.match.error.beneficiary.not.found");
106                        throw new UnprocessableEntityException(Msg.code(1156) + i18nMessage);
107                }
108
109                Patient patient = patientOpt.get();
110                if (!myMemberMatcherR4Helper.validPatientMember(patient, theMemberPatient, theRequestDetails)) {
111                        String i18nMessage =
112                                        myFhirContext.getLocalizer().getMessage("operation.member.match.error.patient.not.found");
113                        throw new UnprocessableEntityException(Msg.code(2146) + i18nMessage);
114                }
115
116                if (patient.getIdentifier().isEmpty()) {
117                        String i18nMessage = myFhirContext
118                                        .getLocalizer()
119                                        .getMessage("operation.member.match.error.beneficiary.without.identifier");
120                        throw new UnprocessableEntityException(Msg.code(1157) + i18nMessage);
121                }
122
123                if (!myMemberMatcherR4Helper.validConsentDataAccess(theConsent)) {
124                        String i18nMessage = myFhirContext
125                                        .getLocalizer()
126                                        .getMessage("operation.member.match.error.consent.release.data.mismatch");
127                        throw new UnprocessableEntityException(Msg.code(2147) + i18nMessage);
128                }
129
130                myMemberMatcherR4Helper.addMemberIdentifierToMemberPatient(theMemberPatient, patient.getIdentifierFirstRep());
131                myMemberMatcherR4Helper.updateConsentForMemberMatch(theConsent, patient, theMemberPatient, theRequestDetails);
132                return myMemberMatcherR4Helper.buildSuccessReturnParameters(theMemberPatient, theCoverageToLink, theConsent);
133        }
134
135        private void validateParams(
136                        Patient theMemberPatient, Coverage theOldCoverage, Coverage theNewCoverage, Consent theConsent) {
137                validateParam(theMemberPatient, Constants.PARAM_MEMBER_PATIENT);
138                validateParam(theOldCoverage, Constants.PARAM_OLD_COVERAGE);
139                validateParam(theNewCoverage, Constants.PARAM_NEW_COVERAGE);
140                validateParam(theConsent, Constants.PARAM_CONSENT);
141                validateMemberPatientParam(theMemberPatient);
142                validateConsentParam(theConsent);
143        }
144
145        private void validateParam(@Nullable Object theParam, String theParamName) {
146                if (theParam == null) {
147                        String i18nMessage = myFhirContext
148                                        .getLocalizer()
149                                        .getMessage("operation.member.match.error.missing.parameter", theParamName);
150                        throw new UnprocessableEntityException(Msg.code(1158) + i18nMessage);
151                }
152        }
153
154        private void validateMemberPatientParam(Patient theMemberPatient) {
155                if (theMemberPatient.getName().isEmpty()) {
156                        validateParam(null, Constants.PARAM_MEMBER_PATIENT_NAME);
157                }
158
159                validateParam(theMemberPatient.getName().get(0).getFamily(), Constants.PARAM_MEMBER_PATIENT_NAME);
160                validateParam(theMemberPatient.getBirthDate(), Constants.PARAM_MEMBER_PATIENT_BIRTHDATE);
161        }
162
163        private void validateConsentParam(Consent theConsent) {
164                if (theConsent.getPatient().isEmpty()) {
165                        validateParam(null, Constants.PARAM_CONSENT_PATIENT_REFERENCE);
166                }
167                if (theConsent.getPerformer().isEmpty()) {
168                        validateParam(null, Constants.PARAM_CONSENT_PERFORMER_REFERENCE);
169                }
170        }
171}