FHIR Appointment/Patient/Encounter/ValueSet (#7066)
[openemr.git] / src / Services / FHIR / FhirAppointmentService.php
blob658c71544b1185cb16c255abeda4221bdf87f2e2
1 <?php
3 /**
4 * FhirAppointmentService handles the mapping of data from the OpenEMR appointment service into FHIR resources.
5 * @package openemr
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";
49 /**
50 * @var AppointmentService
52 private $appointmentService;
54 public function __construct($fhirApiURL = null)
56 parent::__construct($fhirApiURL);
57 $this->appointmentService = new AppointmentService();
60 /**
61 * Returns an array mapping FHIR Resource search parameters to OpenEMR search parameters
63 protected function loadSearchParameters()
65 return [
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']),
73 /**
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);
89 $id = new FHIRId();
90 $id->setValue($dataRecord['pc_uuid']);
91 $appt->setId($id);
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']) {
96 case '-': // none
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';
99 break;
101 case '#': // insurance / financial issue
102 case '^': // pending
103 // Some or all of the participant(s) have not finalized their acceptance of the appointment request.
104 $statusCode = 'pending';
105 break;
106 case '>': // checked out
107 case '$': // coding done
108 $statusCode = 'fulfilled';
109 break;
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';
116 break;
117 case '%': // Cancelled < 24h
118 case '!': // left w/o visit
119 case 'x':
120 // The appointment has been cancelled.
121 $statusCode = 'cancelled';
122 break;
123 case '?':
124 // Some or all of the participant(s) have not/did not appear for the appointment (usually the patient).
125 $statusCode = 'noshow';
126 break;
127 case '~': // arrived late
128 case '@':
129 $statusCode = 'arrived';
130 break;
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';
135 break;
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.
139 break;
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
162 // patient first
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);
181 // now provider
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']));
196 } else {
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);
205 // now location
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
226 // start time
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']);
252 return $appt;
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);
282 if ($encode) {
283 return json_encode($fhirProvenance);
284 } else {
285 return $fhirProvenance;