FHIR Appointment/Patient/Encounter/ValueSet (#7066)
[openemr.git] / src / Services / FHIR / FhirEncounterService.php
blob0bfada182ee56ef3bd4bb118257f18505fa5507f
1 <?php
3 /**
4 * FhirEncounterService
6 * @package OpenEMR
7 * @link http://www.open-emr.org
8 * @author Yash Bothra <yashrajbothra786@gmail.com>
9 * @author Stephen Waite <stephen.waite@cmsvt.com>
10 * @author Vishnu Yarmaneni <vardhanvishnu@gmail.com>
11 * @author Brady Miller <brady.g.miller@gmail.com>
12 * @author Stephen Nielson snielson@discoverandchange.com
13 * @copyright Copyright (c) 2020 Yash Bothra <yashrajbothra786@gmail.com>
14 * @copyright Copyright (c) 2020, 2022 Stephen Waite <stephen.waite@cmsvt.com>
15 * @copyright Copyright (c) 2020 Vishnu Yarmaneni <vardhanvishnu@gmail.com>
16 * @copyright Copyright (c) 2021 Brady Miller <brady.g.miller@gmail.com>
17 * @copyright Copyright (c) 2022 Stephen Nielson <snielson@discoverandchange.com>
18 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
21 namespace OpenEMR\Services\FHIR;
23 use DateTime;
24 use OpenEMR\FHIR\R4\FHIRElement\FHIRIdentifier;
25 use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta;
26 use OpenEMR\FHIR\R4\FHIRResource\FHIREncounter\FHIREncounterHospitalization;
27 use OpenEMR\FHIR\R4\FHIRResource\FHIREncounter\FHIREncounterLocation;
28 use OpenEMR\Services\EncounterService;
29 use OpenEMR\Services\FHIR\FhirServiceBase;
30 use OpenEMR\FHIR\R4\FHIRDomainResource\FHIREncounter;
31 use OpenEMR\FHIR\R4\FHIRElement\FHIRCode;
32 use OpenEMR\FHIR\R4\FHIRElement\FHIRId;
33 use OpenEMR\FHIR\R4\FHIRElement\FHIRCodeableConcept;
34 use OpenEMR\FHIR\R4\FHIRElement\FHIRCoding;
35 use OpenEMR\FHIR\R4\FHIRElement\FHIRPeriod;
36 use OpenEMR\FHIR\R4\FHIRResource\FHIREncounter\FHIREncounterParticipant;
37 use OpenEMR\Services\FHIR\Traits\BulkExportSupportAllOperationsTrait;
38 use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait;
39 use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait;
40 use OpenEMR\Services\FHIR\Traits\PatientSearchTrait;
41 use OpenEMR\Services\Search\FhirSearchParameterDefinition;
42 use OpenEMR\Services\Search\SearchFieldType;
43 use OpenEMR\Services\Search\ServiceField;
44 use OpenEMR\Validators\ProcessingResult;
46 class FhirEncounterService extends FhirServiceBase implements
47 IFhirExportableResourceService,
48 IPatientCompartmentResourceService,
49 IResourceUSCIGProfileService
51 use PatientSearchTrait;
52 use FhirServiceBaseEmptyTrait;
53 use BulkExportSupportAllOperationsTrait;
54 use FhirBulkExportDomainResourceTrait;
56 public const ENCOUNTER_STATUS_FINISHED = "finished";
58 public const ENCOUNTER_TYPE_CHECK_UP = "185349003";
59 public const ENCOUNTER_TYPE_CHECK_UP_DESCRIPTION = "Encounter for check up (procedure)";
61 public const ENCOUNTER_PARTICIPANT_TYPE_PRIMARY_PERFORMER = "PPRF";
62 public const ENCOUNTER_PARTICIPANT_TYPE_PRIMARY_PERFORMER_TEXT = "Primary Performer";
64 public const ENCOUNTER_PARTICIPANT_TYPE_REFERRER = "REF";
65 public const ENCOUNTER_PARTICIPANT_TYPE_REFERRER_TEXT = "Referrer";
68 /**
69 * @var EncounterService
71 private $encounterService;
73 public function __construct()
75 parent::__construct();
76 $this->encounterService = new EncounterService();
79 /**
80 * Returns an array mapping FHIR Encounter Resource search parameters to OpenEMR Encounter search parameters
81 * @return array The search parameters
83 protected function loadSearchParameters()
85 return [
86 '_id' => new FhirSearchParameterDefinition(
87 '_id',
88 SearchFieldType::TOKEN,
90 new ServiceField(
91 'euuid',
92 ServiceField::TYPE_UUID
96 'patient' => $this->getPatientContextSearchField(),
97 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']),
98 '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_update'])
103 * Parses an OpenEMR patient record, returning the equivalent FHIR Patient Resource
104 * https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-encounter-definitions.html
106 * @param array $dataRecord The source OpenEMR data record
107 * @param boolean $encode Indicates if the returned resource is encoded into a string. Defaults to false.
108 * @return FHIREncounter
110 public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
112 $encounterResource = new FHIREncounter();
114 $meta = new FHIRMeta();
115 $meta->setVersionId('1');
116 $meta->setLastUpdated((new \DateTime($dataRecord['last_update']) )->format(DATE_ATOM)); // stored as utc
117 $encounterResource->setMeta($meta);
119 $id = new FhirId();
120 $id->setValue($dataRecord['euuid']);
121 $encounterResource->setId($id);
123 // identifier - required
124 $identifier = new FHIRIdentifier();
125 $identifier->setValue($dataRecord['euuid']);
126 // the system is a unique urn
127 $identifier->setSystem("urn:uuid:" . strtolower($dataRecord['euuid']));
128 $encounterResource->addIdentifier($identifier);
130 // status - required
131 $status = new FHIRCode(self::ENCOUNTER_STATUS_FINISHED);
132 $encounterResource->setStatus($status);
134 // class
135 if (!empty($dataRecord['class_code'])) {
136 $class = new FHIRCoding();
137 $class->setSystem(FhirCodeSystemConstants::HL7_V3_ACT_CODE);
138 $class->setCode($dataRecord['class_code']);
139 $class->setDisplay($dataRecord['class_title']);
140 $encounterResource->setClass($class);
141 } else {
142 $encounterResource->setClass(UtilsService::createDataAbsentUnknownCodeableConcept());
145 // TODO: @adunsulag check with @brady.miller and find out if this really is the only possible encounter type
146 // ... it was here originally
147 $type = UtilsService::createCodeableConcept(
148 [self::ENCOUNTER_TYPE_CHECK_UP => [
149 'code' => self::ENCOUNTER_TYPE_CHECK_UP
150 , "description" => self::ENCOUNTER_TYPE_CHECK_UP_DESCRIPTION
151 , "system" => FhirCodeSystemConstants::SNOMED_CT
154 $encounterResource->addType($type);
156 // subject - required
157 if (!empty($dataRecord['puuid'])) {
158 $encounterResource->setSubject(UtilsService::createRelativeReference('Patient', $dataRecord['puuid']));
159 } else {
160 $encounterResource->setSubject(UtilsService::createDataMissingExtension());
163 // participant - must support
164 if (!empty($dataRecord['provider_uuid'])) {
165 $participant = new FHIREncounterParticipant();
166 $participant->setIndividual(
167 UtilsService::createRelativeReference(
168 "Practitioner",
169 $dataRecord['provider_uuid']
172 $period = new FHIRPeriod();
173 $period->setStart(UtilsService::getLocalDateAsUTC($dataRecord['date']));
174 $participant->setPeriod($period);
176 $participantType = UtilsService::createCodeableConcept([
177 self::ENCOUNTER_PARTICIPANT_TYPE_PRIMARY_PERFORMER =>
179 'code' => self::ENCOUNTER_PARTICIPANT_TYPE_PRIMARY_PERFORMER
180 ,'description' => self::ENCOUNTER_PARTICIPANT_TYPE_PRIMARY_PERFORMER_TEXT
181 ,'system' => FhirCodeSystemConstants::HL7_PARTICIPATION_TYPE
184 $participant->addType($participantType);
185 $encounterResource->addParticipant($participant);
188 // referring provider
189 if (!empty($dataRecord['referrer_uuid'])) {
190 $participant = new FHIREncounterParticipant();
191 $participant->setIndividual(
192 UtilsService::createRelativeReference(
193 "Practitioner",
194 $dataRecord['referrer_uuid']
197 $period = new FHIRPeriod();
198 $period->setStart(UtilsService::getLocalDateAsUTC($dataRecord['date']));
199 $participant->setPeriod($period);
201 $participantType = UtilsService::createCodeableConcept([
202 self::ENCOUNTER_PARTICIPANT_TYPE_REFERRER =>
204 'code' => self::ENCOUNTER_PARTICIPANT_TYPE_REFERRER
205 ,'description' => self::ENCOUNTER_PARTICIPANT_TYPE_REFERRER_TEXT
206 ,'system' => FhirCodeSystemConstants::HL7_PARTICIPATION_TYPE
209 $participant->addType($participantType);
210 $encounterResource->addParticipant($participant);
213 // period - must support
214 if (!empty($dataRecord['date'])) {
215 $period = new FHIRPeriod();
216 $period->setStart(UtilsService::getLocalDateAsUTC($dataRecord['date']));
217 $encounterResource->setPeriod($period);
220 // reasonCode - must support OR must support reasonReference
221 if (!empty($dataRecord['reason'])) {
222 // Note: that we use the encounter textual representation for the reason here which is just fine as ccda
223 // uses a textual representation of this. According to HL7 chat this is just fine as epoch and
224 // other systems do it this way
225 // @see https://chat.fhir.org/#narrow/stream/179175-argonaut/topic/Encounter.20Reason.20For.20Visit
226 // (beware of link rot)
227 $reason = new FHIRCodeableConcept();
228 $reasonText = $dataRecord['reason'] ?? "";
229 $reason->setText(trim($reasonText));
230 $encounterResource->addReasonCode($reason);
232 // hospitalization - must support
234 // hospitalization.dischargeDisposition - must support
235 if (!empty($dataRecord['discharge_disposition'])) {
236 $code = $dataRecord['discharge_disposition'];
237 $text = $dataRecord['discharge_disposition_text'];
239 $hospitalization = new FHIREncounterHospitalization();
240 $hospitalization->setDischargeDisposition(UtilsService::createCodeableConcept(
242 $code => [
243 'code' => $text,
244 'description' => $text,
245 'system' => FhirCodeSystemConstants::HL7_DISCHARGE_DISPOSITION
249 $encounterResource->setHospitalization($hospitalization);
252 // SHALL support either location.location OR serviceProvider
253 // however ONC inferno requires both serviceProvider AND location.location
254 // location.location - must support
255 // serviceProvider - must support
256 if (!empty($dataRecord['facility_uuid'])) {
257 $encounterResource->setServiceProvider(
258 UtilsService::createRelativeReference(
259 'Organization',
260 $dataRecord['facility_uuid']
264 // grab the facility location address
265 if (!empty($dataRecord['facility_location_uuid'])) {
266 $location = new FHIREncounterLocation();
267 $location->setLocation(
268 UtilsService::createRelativeReference(
269 "Location",
270 $dataRecord['facility_location_uuid']
273 $encounterResource->addLocation($location);
277 if ($encode) {
278 return json_encode($encounterResource);
279 } else {
280 return $encounterResource;
284 public function createProvenanceResource($dataRecord = array(), $encode = false)
286 if (!($dataRecord instanceof FHIREncounter)) {
287 throw new \BadMethodCallException("Data record should be correct instance class");
289 $provenanceService = new FhirProvenanceService();
290 $author = null;
291 if (!empty($dataRecord->getParticipant())) {
292 // grab the first one for author
293 $participant = reset($dataRecord->getParticipant());
294 $author = $participant->getIndividual() ?? null;
296 $provenance = $provenanceService->createProvenanceForDomainResource($dataRecord, $author);
297 return $provenance;
301 * Searches for OpenEMR records using OpenEMR search parameters
303 * @param array openEMRSearchParameters OpenEMR search fields
304 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
305 * @return ProcessingResult
307 protected function searchForOpenEMRRecords($searchParam, $puuidBind = null): ProcessingResult
309 return $this->encounterService->search($searchParam, true, $puuidBind);
313 * Returns the Canonical URIs for the FHIR resource for each of the US Core Implementation Guide Profiles that the
314 * resource implements. Most resources have only one profile, but several like DiagnosticReport and Observation
315 * has multiple profiles that must be conformed to.
316 * @see https://www.hl7.org/fhir/us/core/CapabilityStatement-us-core-server.html for the list of profiles
317 * @return string[]
319 function getProfileURIs(): array
321 return [
322 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter'