4 * FhirDeviceService.php
6 * @link http://www.open-emr.org
7 * @author Robert Jones (Analog Informatics Corporation) <robert@analoginfo.com>, <robert@justjones.org>
8 * @copyright Copyright (c) 2023 Analog Informatics Corporation <https://analoginfo.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\FHIRValueSet
;
15 use OpenEMR\FHIR\R4\FHIRResource\FHIRValueSet\FHIRValueSetCompose
;
16 use OpenEMR\FHIR\R4\FHIRResource\FHIRValueSet\FHIRValueSetInclude
;
17 use OpenEMR\FHIR\R4\FHIRResource\FHIRValueSet\FHIRValueSetConcept
;
18 use OpenEMR\FHIR\R4\FHIRElement\FHIRCode
;
19 use OpenEMR\FHIR\R4\FHIRElement\FHIRDateTime
;
20 use OpenEMR\FHIR\R4\FHIRElement\FHIRId
;
21 use OpenEMR\Services\AppointmentService
;
22 use OpenEMR\Services\ListService
;
23 use OpenEMR\Services\FHIR\Traits\BulkExportSupportAllOperationsTrait
;
24 use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait
;
25 use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait
;
26 use OpenEMR\Services\Search\FhirSearchParameterDefinition
;
27 use OpenEMR\Services\Search\SearchFieldType
;
28 use OpenEMR\Services\Search\ServiceField
;
29 use OpenEMR\Validators\ProcessingResult
;
31 class FhirValueSetService
extends FhirServiceBase
implements IResourceUSCIGProfileService
, IFhirExportableResourceService
33 use FhirServiceBaseEmptyTrait
;
34 use BulkExportSupportAllOperationsTrait
;
35 use FhirBulkExportDomainResourceTrait
;
38 * @var AppointmentService
40 private $appointmentService;
45 private $listOptionService;
48 * NB: started w Device as a base, but DocumentReference is what I needed... no underlying service either
49 * pc_catid => openemr_postcalendar_categories
50 * doc category => categories
53 * option_lists by list_id
55 * MariaDB [openemr]> select list_id , option_id , title , seq , is_default , option_value , mapping , notes, codes from list_options limit 40;
56 * +---------------------------------+-------------------+-----------------------+-----+------------+--------------+---------+-------+-------+
57 * | list_id | option_id | title | seq | is_default | option_value | mapping | notes | codes |
58 * +---------------------------------+-------------------+-----------------------+-----+------------+--------------+---------+-------+-------+
59 * | abook_type | bill_svc | Billing Service | 120 | 0 | 3 | | NULL | |
60 * | abook_type | ccda | Care Coordination | 35 | 0 | 2 | | NULL | |
61 * | abook_type | dist | Distributor | 30 | 0 | 3 | | NULL | |
62 * | abook_type | emr_direct | EMR Direct | 105 | 0 | 4 | | NULL | |
63 * | abook_type | external_org | External Organization | 120 | 0 | 1 | | NULL | |
64 * | abook_type | external_provider | External Provider | 110 | 0 | 1 | | NULL | |
65 * | abook_type | ord_img | Imaging Service | 5 | 0 | 3 | | NULL | |
66 * | abook_type | ord_imm | Immunization Service | 10 | 0 | 3 | | NULL | |
67 * | abook_type | ord_lab | Lab Service | 15 | 0 | 3 | | NULL | |
68 * | abook_type | oth | Other | 95 | 0 | 1 | | NULL | |
69 * | abook_type | spe | Specialist | 20 | 0 | 2 | | NULL | |
70 * | abook_type | vendor | Vendor | 25 | 0 | 3 | | NULL | |
71 * | address-types | both | Postal & Physical | 30 | 0 | 0 | | NULL | |
72 * | address-types | physical | Physical | 20 | 0 | 0 | | NULL | |
73 * | address-types | postal | Postal | 10 | 0 | 0 | | NULL | |
74 * | address-uses | billing | Billing | 50 | 0 | 0 | | NULL | |
75 * | address-uses | home | Home | 10 | 0 | 0 | | NULL | |
76 * | address-uses | old | Old/Incorrect | 40 | 0 | 0 | | NULL | |
77 * | address-uses | temp | Temporary | 30 | 0 | 0 | | NULL | |
78 * | address-uses | work | Work | 20 | 0 | 0 | | NULL | |
79 * | adjreason | Adm adjust | Adm adjust | 5 | 0 | 1 | | NULL | |
80 * | adjreason | After hrs calls | After hrs calls | 10 | 0 | 1 | | NULL | |
81 * | adjreason | Bad check | Bad check | 15 | 0 | 1 | | NULL | |
82 * | adjreason | Bad debt | Bad debt | 20 | 0 | 1 | | NULL | |
83 * | adjreason | Coll w/o | Coll w/o | 25 | 0 | 1 | | NULL | |
88 const USCGI_PROFILE_URI
= 'http://hl7.org/fhir/StructureDefinition/shareablevalueset';
89 const APPOINTMENT_TYPE
= 'appointment-type';
91 public function __construct()
93 parent
::__construct();
94 // TODO: @adunsulag we need to look at adding a mapping service here in order to get our value sets out.
95 $this->appointmentService
= new AppointmentService();
96 $this->listOptionService
= new ListService();
100 * Returns an array mapping FHIR Resource search parameters to OpenEMR search parameters
102 protected function loadSearchParameters()
105 '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType
::TOKEN
, [new ServiceField('id', ServiceField
::TYPE_STRING
)]),
106 '_lastUpdated' => $this->getLastModifiedSearchField(),
110 public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition
112 return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType
::DATETIME
, ['sublist_updated_date', 'last_updated']);
115 private function getLastModifiedSearchFieldForAppointmentCategories()
117 return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType
::DATETIME
, ['pc_last_updated']);
121 * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements.
122 * @param $fhirSearchParameters The FHIR resource search parameters
123 * @return processing result
125 public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResult
127 $fhirSearchResult = new ProcessingResult();
129 // we don't really deal with provenance for ValueSet pieces so we will ignore this property
130 if (isset($fhirSearchParameters['_revinclude'])) {
131 unset($fhirSearchParameters['_revinclude']);
134 $this->addAppointmentCategoriesValueSetForSearch($fhirSearchResult, $fhirSearchParameters);
136 $this->addListOptionsValueSetsForSearch($fhirSearchResult, $fhirSearchParameters, $puuidBind);
137 } catch (SearchFieldException
$exception) {
138 (new SystemLogger())->errorLogCaller("search exception thrown", ['message' => $exception->getMessage(),
139 'field' => $exception->getField()]);
140 // put our exception information here
141 $fhirSearchResult->setValidationMessages([$exception->getField() => $exception->getMessage()]);
143 return $fhirSearchResult;
148 * Returns the Canonical URIs for the FHIR resource for each of the US Core Implementation Guide Profiles that the
149 * resource implements. Most resources have only one profile, but several like DiagnosticReport and Observation
150 * has multiple profiles that must be conformed to.
151 * @see https://www.hl7.org/fhir/us/core/CapabilityStatement-us-core-server.html for the list of profiles
154 function getProfileURIs(): array
156 return [self
::USCGI_PROFILE_URI
];
159 private function addAppointmentCategoriesValueSetForSearch(ProcessingResult
$fhirSearchResult, array $fhirSearchParameters, string $puuidBind = null)
161 $this->getSearchFieldFactory()->setSearchFieldDefinition('_lastUpdated', $this->getLastModifiedSearchFieldForAppointmentCategories());
162 $oeSearchParameters = $this->createOpenEMRSearchParameters($fhirSearchParameters, $puuidBind);
164 !isset($oeSearchParameters['_id'])
165 // could be array (AND) or comma-delimited string value (OR)
166 // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous)
167 ||
$oeSearchParameters['_id']->hasCodeValue(self
::APPOINTMENT_TYPE
)
169 if (!isset($oeSearchParameters['_id'])) {
170 // if we have any match on categories we want to return everything... hate the double db call
171 // but rather than mess with a complex query we will just do it this way
172 $processingResult = $this->appointmentService
->searchCalendarCategories($oeSearchParameters);
173 // nothing to do here as we have no categories matching so we return
174 if (!$processingResult->hasData()) {
175 return $fhirSearchResult;
178 $calendarCategories = $this->appointmentService
->getCalendarCategories();
179 $valueSet = new FHIRValueSet();
180 $valueSet->setId(self
::APPOINTMENT_TYPE
);
181 $compose = new FHIRValueSetCompose();
182 $include = new FHIRValueSetInclude();
183 foreach ($calendarCategories as $category) {
184 if ($category["pc_cattype"] != 0) {
185 continue; // only cat_type==0
187 $concept = new FHIRValueSetConcept();
188 $code = new FHIRCode();
189 $code->setValue($category["pc_constant_id"]);
190 $concept->setCode($code);
191 $concept->setDisplay($category["pc_catname"]);
192 $include->addConcept($concept);
194 $compose->addInclude($include);
195 $valueSet->setCompose($compose);
196 $fhirSearchResult->addData($valueSet);
198 return $fhirSearchResult;
201 private function addListOptionsValueSetsForSearch(ProcessingResult
$fhirSearchResult, array $fhirSearchParameters, ?
string $puuidBind = null)
203 $this->getSearchFieldFactory()->setSearchFieldDefinition('_lastUpdated', $this->getLastModifiedSearchField());
204 $oeSearchParameters = $this->createOpenEMRSearchParameters($fhirSearchParameters, $puuidBind);
206 // Now the same for list_options selected in $listNames
207 $listsResult = $this->listOptionService
->searchLists($oeSearchParameters);
208 if (!$listsResult->hasData()) {
209 $fhirSearchResult->addProcessingResult($listsResult);
210 return $fhirSearchResult;
212 foreach ($listsResult->getData() as $listRecord) {
213 $listName = $listRecord["option_id"];
214 $options = $this->listOptionService
->getOptionsByListName($listName); // does not return title
215 if (count($options) == 0) {
218 $valueSet = new FHIRValueSet();
219 $valueSet->setId($listName);
220 $compose = new FHIRValueSetCompose();
221 $include = new FHIRValueSetInclude();
222 foreach ($options as $option) {
223 $concept = new FHIRValueSetConcept();
224 $code = new FHIRCode();
225 $code->setValue($option["option_id"]);
226 $concept->setCode($code);
227 $concept->setDisplay($option["title"]);
228 $include->addConcept($concept);
230 $compose->addInclude($include);
231 $valueSet->setCompose($compose);
232 $fhirSearchResult->addData($valueSet);
234 return $fhirSearchResult;