4 * FhirAppointmentService handles the mapping of data from the OpenEMR appointment service into FHIR resources.
6 * @link http://www.open-emr.org
7 * @author Stephen Nielson <snielson@discoverandchange.com>
8 * @copyright Copyright (c) 2022 Discover and Change, Inc. <snielson@discoverandchange.com>
9 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
12 namespace OpenEMR\Services\FHIR
;
14 use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRAppointment
;
15 use OpenEMR\FHIR\R4\FHIRElement\FHIRAppointmentStatus
;
16 use OpenEMR\FHIR\R4\FHIRElement\FHIRCodeableConcept
;
17 use OpenEMR\FHIR\R4\FHIRElement\FHIRCoding
;
18 use OpenEMR\FHIR\R4\FHIRElement\FHIRId
;
19 use OpenEMR\FHIR\R4\FHIRElement\FHIRInstant
;
20 use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta
;
21 use OpenEMR\FHIR\R4\FHIRElement\FHIRParticipationStatus
;
22 use OpenEMR\FHIR\R4\FHIRResource\FHIRAppointment\FHIRAppointmentParticipant
;
23 use OpenEMR\Services\AppointmentService
;
24 use OpenEMR\Services\FHIR\Traits\BulkExportSupportAllOperationsTrait
;
25 use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait
;
26 use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait
;
27 use OpenEMR\Services\FHIR\Traits\PatientSearchTrait
;
28 use OpenEMR\Services\Search\FhirSearchParameterDefinition
;
29 use OpenEMR\Services\Search\SearchFieldType
;
30 use OpenEMR\Services\Search\ServiceField
;
31 use OpenEMR\Validators\ProcessingResult
;
33 class FhirAppointmentService
extends FhirServiceBase
implements IPatientCompartmentResourceService
, IFhirExportableResourceService
35 use FhirServiceBaseEmptyTrait
;
36 use BulkExportSupportAllOperationsTrait
;
37 use FhirBulkExportDomainResourceTrait
;
38 use PatientSearchTrait
;
40 const APPOINTMENT_TYPE_LOCATION
= "LOC";
41 const APPOINTMENT_TYPE_LOCATION_TEXT
= "Location";
42 const PARTICIPANT_TYPE_LOCATION
= "LOC";
43 const PARTICIPANT_TYPE_LOCATION_TEXT
= "Location";
44 const PARTICIPANT_TYPE_PARTICIPANT
= "PART";
45 const PARTICIPANT_TYPE_PRIMARY_PERFORMER
= "PPRF";
46 const PARTICIPANT_TYPE_PRIMARY_PERFORMER_TEXT
= "Primary Performer";
47 const PARTICIPANT_TYPE_PARTICIPANT_TEXT
= "Participant";
50 * @var AppointmentService
52 private $appointmentService;
54 public function __construct($fhirApiURL = null)
56 parent
::__construct($fhirApiURL);
57 $this->appointmentService
= new AppointmentService();
61 * Returns an array mapping FHIR Resource search parameters to OpenEMR search parameters
63 protected function loadSearchParameters()
66 'patient' => $this->getPatientContextSearchField(),
67 '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType
::TOKEN
, [new ServiceField('pc_uuid', ServiceField
::TYPE_UUID
)]),
68 '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType
::DATETIME
, ['pc_time']),
69 'date' => new FhirSearchParameterDefinition('date', SearchFieldType
::DATE
, ['pc_eventDate']),
74 * Parses an OpenEMR data record, returning the equivalent FHIR Resource
76 * @param $dataRecord The source OpenEMR data record
77 * @param $encode Indicates if the returned resource is encoded into a string. Defaults to True.
78 * @return the FHIR Resource. Returned format is defined using $encode parameter.
80 public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
82 $appt = new FHIRAppointment();
84 $fhirMeta = new FHIRMeta();
85 $fhirMeta->setVersionId("1");
86 $fhirMeta->setLastUpdated(UtilsService
::getLocalDateAsUTC($dataRecord['pc_time']));
87 $appt->setMeta($fhirMeta);
90 $id->setValue($dataRecord['pc_uuid']);
93 // now we need to parse out our status
94 $statusCode = 'pending'; // there can be a lot of different status and we will default to pending
95 switch ($dataRecord['pc_apptstatus']) {
97 // None of the participant(s) have finalized their acceptance of the appointment request, and the start/end time might not be set yet.
98 $statusCode = 'proposed';
101 case '#': // insurance / financial issue
103 // Some or all of the participant(s) have not finalized their acceptance of the appointment request.
104 $statusCode = 'pending';
106 case '>': // checked out
107 case '$': // coding done
108 $statusCode = 'fulfilled';
110 case 'AVM': // AVM confirmed
111 case 'SMS': // SMS confirmed
112 case 'EMAIL': // Email confirmed
113 case '*': // reminder done
114 // All participant(s) have been considered and the appointment is confirmed to go ahead at the date/times specified.
115 $statusCode = 'booked';
117 case '%': // Cancelled < 24h
118 case '!': // left w/o visit
120 // The appointment has been cancelled.
121 $statusCode = 'cancelled';
124 // Some or all of the participant(s) have not/did not appear for the appointment (usually the patient).
125 $statusCode = 'noshow';
127 case '~': // arrived late
129 $statusCode = 'arrived';
131 case '<': // in exam room
132 case '+': // chart pulled
133 // When checked in, all pre-encounter administrative work is complete, and the encounter may begin. (where multiple patients are involved, they are all present).
134 $statusCode = 'checked-in';
136 case 'CALL': // Callback requested
137 $statusCode = 'waitlist';
138 // The appointment has been placed on a waitlist, to be scheduled/confirmed in the future when a slot/service is available. A specific time might or might not be pre-allocated.
141 // TODO: add an event here allowing people to update / configure the FHIR status
142 $apptStatus = new FHIRAppointmentStatus();
143 $apptStatus->setValue($statusCode);
144 $appt->setStatus($apptStatus);
146 // now add appointmentType coding
147 if (!empty($dataRecord['pc_catid'])) {
148 $category = $this->appointmentService
->getOneCalendarCategory($dataRecord['pc_catid']);
149 $appointmentType = new FHIRCodeableConcept();
150 $code = new FHIRCoding();
151 $code->setCode($category[ 0 ][ 'pc_constant_id' ]);
152 $code->setDisplay($category[ 0 ][ 'pc_catname' ]);
153 // var_dump( $_SERVER );
154 $system = str_replace('/Appointment', '/ValueSet/appointment-type', $GLOBALS['site_addr_oath'] . ($_SERVER['REDIRECT_URL'] ??
''));
155 $code->setSystem($system);
156 $appointmentType->addCoding($code);
157 $appt->setAppointmentType($appointmentType);
161 // now parse out the participants
163 if (!empty($dataRecord['puuid'])) {
164 $patient = new FHIRAppointmentParticipant();
165 $participantType = UtilsService
::createCodeableConcept([
166 self
::PARTICIPANT_TYPE_PARTICIPANT
=>
168 'code' => self
::PARTICIPANT_TYPE_PARTICIPANT
169 ,'description' => self
::PARTICIPANT_TYPE_PARTICIPANT_TEXT
170 ,'system' => FhirCodeSystemConstants
::HL7_PARTICIPATION_TYPE
173 $patient->addType($participantType);
174 $patient->setActor(UtilsService
::createRelativeReference('Patient', $dataRecord['puuid']));
175 $status = new FHIRParticipationStatus();
176 $status->setValue('accepted'); // we don't really track any other field right now in FHIR
177 $patient->setStatus($status);
178 $appt->addParticipant($patient);
182 if (!empty($dataRecord['pce_aid_uuid'])) {
183 $provider = new FHIRAppointmentParticipant();
184 $providerType = UtilsService
::createCodeableConcept([
185 self
::PARTICIPANT_TYPE_PRIMARY_PERFORMER
=>
187 'code' => self
::PARTICIPANT_TYPE_PRIMARY_PERFORMER
188 ,'description' => self
::PARTICIPANT_TYPE_PRIMARY_PERFORMER_TEXT
189 ,'system' => FhirCodeSystemConstants
::HL7_PARTICIPATION_TYPE
192 $provider->addType($providerType);
193 // we can only provide the provider if they have an NPI, otherwise they are a person
194 if (!empty($dataRecord['pce_aid_npi'])) {
195 $provider->setActor(UtilsService
::createRelativeReference('Practitioner', $dataRecord['pce_aid_uuid']));
197 $provider->setActor(UtilsService
::createRelativeReference('Person', $dataRecord['pce_aid_uuid']));
199 $status = new FHIRParticipationStatus();
200 $status->setValue('accepted'); // we don't really track any other field right now in FHIR
201 $provider->setStatus($status);
202 $appt->addParticipant($provider);
206 if (!empty($dataRecord['facility_uuid'])) {
207 $location = new FHIRAppointmentParticipant();
208 $participantType = UtilsService
::createCodeableConcept([
209 self
::PARTICIPANT_TYPE_LOCATION
=>
211 'code' => self
::PARTICIPANT_TYPE_LOCATION
212 ,'description' => self
::PARTICIPANT_TYPE_LOCATION_TEXT
213 ,'system' => FhirCodeSystemConstants
::HL7_PARTICIPATION_TYPE
216 $location->addType($participantType);
217 $location->setActor(UtilsService
::createRelativeReference('Location', $dataRecord['facility_uuid']));
218 $status = new FHIRParticipationStatus();
219 $status->setValue('accepted'); // we don't really track any other field right now in FHIR
220 $location->setStatus($status);
221 $appt->addParticipant($location);
224 // now let's get start and end dates
227 if (!empty($dataRecord['pc_eventDate'])) {
228 $concatenatedDate = $dataRecord['pc_eventDate'] . ' ' . $dataRecord['pc_startTime'];
229 $startInstant = UtilsService
::getLocalDateAsUTC($concatenatedDate);
230 $appt->setStart(new FHIRInstant($startInstant));
231 } elseif ($dataRecord['pc_endDate'] != '0000-00-00' && !empty($dataRecord['pc_startTime'])) {
232 $concatenatedDate = $dataRecord['pc_endDate'] . ' ' . $dataRecord['pc_startTime'];
233 $startInstant = UtilsService
::getLocalDateAsUTC($concatenatedDate);
234 $appt->setStart(new FHIRInstant($startInstant));
237 // if we have a start date and and end time we will use that
238 if (!empty($dataRecord['pc_eventDate']) && !empty($dataRecord['pc_endTime'])) {
239 $concatenatedDate = $dataRecord['pc_eventDate'] . ' ' . $dataRecord['pc_endTime'];
240 $endInstant = UtilsService
::getLocalDateAsUTC($concatenatedDate);
241 $appt->setEnd(new FHIRInstant($endInstant));
242 } elseif (!empty($dataRecord['pc_endDate']) && !empty($dataRecord['pc_endTime'])) {
243 $concatenatedDate = $dataRecord['pc_endDate'] . ' ' . $dataRecord['pc_endTime'];
244 $endInstant = UtilsService
::getLocalDateAsUTC($concatenatedDate);
245 $appt->setEnd(new FHIRInstant($endInstant));
248 if (!empty($dataRecord['pc_hometext'])) {
249 $appt->setComment($dataRecord['pc_hometext']);
257 * Searches for OpenEMR records using OpenEMR search parameters
258 * @param openEMRSearchParameters OpenEMR search fields
259 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
260 * @return OpenEMR records
262 protected function searchForOpenEMRRecords($openEMRSearchParameters, $puuidBind = null): ProcessingResult
264 return $this->appointmentService
->search($openEMRSearchParameters, true, $puuidBind);
268 * Creates the Provenance resource for the equivalent FHIR Resource
270 * @param $dataRecord The source OpenEMR data record
271 * @param $encode Indicates if the returned resource is encoded into a string. Defaults to True.
272 * @return the FHIR Resource. Returned format is defined using $encode parameter.
274 public function createProvenanceResource($dataRecord, $encode = false)
276 if (!($dataRecord instanceof FHIRAppointment
)) {
277 throw new \
BadMethodCallException("Data record should be correct instance class");
279 $fhirProvenanceService = new FhirProvenanceService();
280 // we don't have an individual authorship right now for appointments so we default to billing organization
281 $fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord);
283 return json_encode($fhirProvenance);
285 return $fhirProvenance;