7 * @link http://www.open-emr.org
8 * @author Stephen Nielson <stephen@nielson.org>
9 * @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org>
10 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
13 namespace OpenEMR\Services
;
15 use OpenEMR\Services\FHIR\FhirCodeSystemConstants
;
18 * Service for code type
20 class CodeTypesService
25 const CODE_TYPE_SNOMED_CT
= "SNOMED-CT";
26 const CODE_TYPE_SNOMED
= "SNOMED";
27 const CODE_TYPE_CPT4
= "CPT4";
28 const CODE_TYPE_LOINC
= "LOINC";
29 const CODE_TYPE_NUCC
= "NUCC";
30 const CODE_TYPE_RXNORM
= "RXNORM";
31 const CODE_TYPE_RXCUI
= "RXCUI";
32 const CODE_TYPE_ICD10
= 'ICD10';
33 const CODE_TYPE_ICD10PCS
= 'ICD10PCS';
34 const CODE_TYPE_CPT
= 'CPT';
35 const CODE_TYPE_CVX
= 'CVX';
36 const CODE_TYPE_OID_HEALTHCARE_PROVIDER_TAXONOMY
= "2.16.840.1.114222.4.11.1066";
37 const CODE_TYPE_OID
= array(
38 '2.16.840.1.113883.6.96' => self
::CODE_TYPE_SNOMED_CT
,
39 '2.16.840.1.113883.6.12' => self
::CODE_TYPE_CPT4
,
40 '2.16.840.1.113883.6.1' => self
::CODE_TYPE_LOINC
,
41 '2.16.840.1.113883.6.101' => self
::CODE_TYPE_NUCC
,
42 '2.16.840.1.113883.6.88' => self
::CODE_TYPE_RXNORM
,
43 '2.16.840.1.113883.6.90' => self
::CODE_TYPE_ICD10
,
44 '2.16.840.1.113883.6.103' => 'ICD9-CM',
45 '2.16.840.1.113883.6.104' => 'ICD9-PCS',
46 '2.16.840.1.113883.6.4' => 'ICD10-PCS',
47 '2.16.840.1.113883.6.14' => 'HCP',
48 '2.16.840.1.113883.6.285' => 'HCPCS',
49 '2.16.840.1.113883.5.2' => "HL7 Marital Status",
50 '2.16.840.1.113883.12.292' => 'CVX',
51 '2.16.840.1.113883.5.83' => 'HITSP C80 Observation Status',
52 '2.16.840.1.113883.3.26.1.1' => 'NCI Thesaurus',
53 '2.16.840.1.113883.3.88.12.80.20' => 'FDA',
54 "2.16.840.1.113883.4.9" => "UNII",
55 "2.16.840.1.113883.6.69" => "NDC",
56 '2.16.840.1.113883.5.14' => 'HL7 ActStatus',
57 '2.16.840.1.113883.6.259' => 'HL7 Healthcare Service Location',
58 '2.16.840.1.113883.12.112' => 'DischargeDisposition',
59 '2.16.840.1.113883.5.4' => 'HL7 Act Code',
60 '2.16.840.1.113883.1.11.18877' => 'HL7 Relationship Code',
61 '2.16.840.1.113883.6.238' => 'CDC Race',
62 '2.16.840.1.113883.6.177' => 'NLM MeSH',
63 '2.16.840.1.113883.5.1076' => "Religious Affiliation",
64 '2.16.840.1.113883.1.11.19717' => "HL7 ActNoImmunicationReason",
65 '2.16.840.1.113883.3.88.12.80.33' => "NUBC",
66 '2.16.840.1.113883.1.11.78' => "HL7 Observation Interpretation",
67 '2.16.840.1.113883.3.221.5' => "Source of Payment Typology",
68 '2.16.840.1.113883.6.13' => 'CDT',
69 '2.16.840.1.113883.18.2' => 'AdministrativeSex',
70 '2.16.840.1.113883.5.1' => 'AdministrativeGender',
71 self
::CODE_TYPE_OID_HEALTHCARE_PROVIDER_TAXONOMY
=> 'HealthCareProviderTaxonomy'
76 public $installedCodeTypes;
80 private $snomedInstalled;
81 private $cpt4Installed;
82 private $rxnormInstalled;
84 public function __construct()
86 // currently, our installed code types are
88 $this->installedCodeTypes
= $code_types;
90 $this->snomedInstalled
= isset($code_types[self
::CODE_TYPE_SNOMED_CT
]);
91 $this->cpt4Installed
= isset($code_types[self
::CODE_TYPE_CPT4
]);
92 $this->rxnormInstalled
= isset($code_types[self
::CODE_TYPE_RXNORM
]);
96 * Lookup the description for a series of codes in the database. Eventually we would want to
97 * remove the global lookup_code_descriptions and use this so we can mock these codes and improve our unit test speed.
99 * @param string $codes - Is of the form "type:code;type:code; etc.".
100 * @param string $desc_detail - Can choose either the normal description('code_text') or the brief description('code_text_short').
101 * @return string - Is of the form "description;description; etc.".
102 * @see code_types.inc.php
104 public function lookup_code_description($codes, $desc_detail = "code_text"): string
109 $code_text = lookup_code_descriptions($codes, $desc_detail);
110 if (empty($code_text)) {
111 // let's return a description of code if available.
112 // sometimes depending on code, long and/or short description may not be available.
113 $desc_detail = $desc_detail == "code_text" ?
"code_text_short" : "code_text";
114 $code_text = lookup_code_descriptions($codes, $desc_detail);
121 * @return bool - Returns true if the snomed-ct codes are installed
123 public function isSnomedCodesInstalled(): bool
125 return $this->snomedInstalled
;
130 * @return bool - Returns whether the system has the cpt4 codes installed
132 public function isCPT4Installed(): bool
134 return $this->cpt4Installed
;
139 * @return bool - Returns whether the system has the rxnorm codes installed
141 public function isRXNORMInstalled(): bool
143 return $this->rxnormInstalled
;
146 public function isInstalledCodeType($codeType): bool
148 return isset($this->installedCodeTypes
[$codeType]);
153 * @param false $useOid
154 * @return string|null
156 public function getSystemForCode($code, $useOid = false)
158 $codeType = $this->getCodeTypeForCode($code);
159 if (!empty($codeType)) {
160 return $this->getSystemForCodeType($codeType);
169 public function parseCode($code)
173 if (is_string($code) && strpos($code, ":") !== false) {
174 $parts = explode(":", $code);
175 $parsedCode = $parts[1];
176 $parsedType = $parts[0];
178 return ['code' => $parsedCode, 'code_type' => $parsedType];
182 * Returns a code with the code type prefixed
184 * @param $code string The value for the code that exists in the given code_type datadatabse
185 * @param $type string The code_type that the code belongs to (SNOMED, RXCUI, ICD10, etc).
186 * @return string The fully typed code (TYPE:CODE)
188 public function getCodeWithType($code, $type, $oe_format = false)
190 if (empty($type) ||
empty($code)) {
193 $tmp = explode(':', $code);
194 if (is_array($tmp) && count($tmp ??
[]) === 2) {
198 // rebuild when code type format flag is set
203 $type = $this->formatCodeType($type ??
"");
205 return ($type ??
"") . ":" . ($code ??
"");
212 public function getCodeTypeForCode($code)
214 $parsedCode = $this->parseCode($code);
215 return $parsedCode['code_type'];
219 * @param string $codeType
220 * @param false $useOid
221 * @return string|null
223 public function getSystemForCodeType($codeType, $useOid = false)
228 if (self
::CODE_TYPE_SNOMED_CT
== $codeType) {
229 $system = '2.16.840.1.113883.6.96';
230 } elseif (self
::CODE_TYPE_SNOMED
== $codeType) {
231 $system = '2.16.840.1.113883.6.96';
232 } elseif (self
::CODE_TYPE_CPT4
== $codeType) {
233 $system = '2.16.840.1.113883.6.12';
234 } elseif (self
::CODE_TYPE_LOINC
== $codeType) {
235 $system = '2.16.840.1.113883.6.1';
236 } elseif (self
::CODE_TYPE_ICD10
== $codeType) {
237 $system = '2.16.840.1.113883.6.90';
238 } elseif (self
::CODE_TYPE_RXCUI
== $codeType || self
::CODE_TYPE_RXNORM
== $codeType) {
239 $system = '2.16.840.1.113883.6.88';
240 } elseif (self
::CODE_TYPE_CPT
== $codeType) {
241 $system = '2.16.840.1.113883.6.12';
242 } elseif (self
::CODE_TYPE_CVX
== $codeType) {
243 $system = '2.16.840.1.113883.12.292';
244 } elseif (self
::CODE_TYPE_ICD10PCS
== $codeType) {
245 $system = '2.16.840.1.113883.6.4';
248 if (self
::CODE_TYPE_SNOMED_CT
== $codeType) {
249 $system = FhirCodeSystemConstants
::SNOMED_CT
;
250 } elseif (self
::CODE_TYPE_SNOMED
== $codeType) {
251 $system = FhirCodeSystemConstants
::SNOMED_CT
;
252 } elseif (self
::CODE_TYPE_NUCC
== $codeType) {
253 $system = FhirCodeSystemConstants
::NUCC_PROVIDER
;
254 } elseif (self
::CODE_TYPE_LOINC
== $codeType) {
255 $system = FhirCodeSystemConstants
::LOINC
;
256 } elseif (self
::CODE_TYPE_RXNORM
== $codeType || self
::CODE_TYPE_RXCUI
== $codeType) {
257 $system = FhirCodeSystemConstants
::RXNORM
;
260 if (empty($system)) {
261 foreach (self
::CODE_TYPE_OID
as $oid => $system_code) {
262 if ($system_code == $codeType) {
271 * @param string $type
274 public function formatCodeType($type)
276 switch (strtoupper(trim($type))) {
287 if (!$this->isSnomedCodesInstalled()) {
288 $type = 'SNOMED CT'; // for valueset table lookups
294 if (!$this->isRXNORMInstalled() && $this->isInstalledCodeType('RXCUI')) {
295 $type = 'RXCUI'; // then let's use RxCUI for lookups
305 if (strpos($type, '2.16.840.1.113883.') !== false) {
306 $type = $this->getCodeSystemNameFromSystem($type);
312 public function getCodeSystemNameFromSystem($oid): string
314 return self
::CODE_TYPE_OID
[$oid] ??
'';
318 * Returns a resolved code set including using valueset table to try and get a code set.
322 * @param string $currentCodeText
323 * @param string $codeDescriptionType
326 public function resolveCode($code, $codeType, $currentCodeText = '', $codeDescriptionType = 'code_text')
331 'code' => $code ??
'',
332 'formatted_code' => $code . ':' . $codeType,
333 'formatted_code_type' => $codeType ??
'',
334 'code_text' => $currentCodeText,
337 'valueset_name' => ''
340 $default['formatted_code'] = '';
344 $formatted_type = $this->formatCodeType($codeType ?
: '');
345 $oid = $this->getSystemForCodeType($formatted_type, true);
346 $formatted_code = $this->getCodeWithType($code ??
'', $formatted_type);
347 if (empty($currentCodeText) && $this->isInstalledCodeType($formatted_type)) {
348 $currentCodeText = $this->lookup_code_description($formatted_code, $codeDescriptionType);
351 // use valueset table if code description not found.
352 if (empty($currentCodeText)) {
353 if (strpos($codeType, '2.16.840.1.113883.') !== false) {
354 $oid = trim($codeType);
357 $value = $this->lookupFromValueset($code, $formatted_type, $oid);
358 $formatted_type = ($value['code_type'] ??
null) ?
: $formatted_type;
359 if (!empty($code) && !empty($formatted_type)) {
360 $formatted_code = $formatted_type . ':' . $code;
362 $oid = $value['code_system'] ??
'';
363 $currentCodeText = $value['description'] ??
'';
364 $valueset_name = $value['valueset_name'] ??
'';
365 $valueset = $value['valueset'] ??
'';
369 'code' => $code ??
"",
370 'formatted_code' => $formatted_code ?
: $code,
371 'formatted_code_type' => $formatted_type ?
: $codeType,
372 'code_text' => trim($currentCodeText),
373 'system_oid' => $oid ??
"",
374 'valueset' => $valueset ??
"",
375 'valueset_name' => $valueset_name ??
""
379 public function getInstalledCodeTypes()
381 return $this->installedCodeTypes ??
null;
384 public function lookupFromValueset($code, $codeType, $codeSystem)
386 if (empty($codeSystem) && empty($codeType)) {
388 "Select * From valueset Where code = ? LIMIT 1",
393 "Select * From valueset Where code = ? And (code_type = ? Or code_type LIKE ? Or code_system = ?)",
394 array($code, $codeType, "$codeType%", $codeSystem)
400 public function dischargeOptionIdFromCode($formatted_code)
402 $listService = new ListService();
403 $ret = $listService->getOptionsByListName('discharge-disposition', ['codes' => $formatted_code]) ??
'';
404 return $ret[0]['option_id'] ??
'';
407 public function dischargeCodeFromOptionId($option_id)
409 $listService = new ListService();
410 return $listService->getListOption('discharge-disposition', $option_id)['codes'] ??
'';
413 public function parseCodesIntoCodeableConcepts($codes)
415 $codes = explode(";", $codes);
416 $codeableConcepts = array();
417 foreach ($codes as $codeItem) {
418 $parsedCode = $this->parseCode($codeItem);
419 $codeType = $parsedCode['code_type'];
420 $code = $parsedCode['code'];
421 $system = $this->getSystemForCodeType($codeType);
422 $codedesc = $this->lookup_code_description($codeItem);
423 $codeableConcepts[$code] = [
425 , 'description' => $codedesc
426 , 'code_type' => $codeType
427 , 'system' => $system
430 return $codeableConcepts;