QRDA Import adds and fixes (#4727)
[openemr.git] / src / Services / CodeTypesService.php
blob0ffe2da495e51abd4a60d3684dbd12b050dd4c88
1 <?php
3 /**
4 * CodeTypesService.php
6 * @package openemr
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;
17 /**
18 * Service for code type
20 class CodeTypesService
23 /**
24 * @const string
26 const CODE_TYPE_SNOMED_CT = "SNOMED-CT";
27 const CODE_TYPE_SNOMED = "SNOMED";
28 const CODE_TYPE_CPT4 = "CPT4";
29 const CODE_TYPE_LOINC = "LOINC";
30 const CODE_TYPE_NUCC = "NUCC";
31 const CODE_TYPE_RXNORM = "RXNORM";
32 const CODE_TYPE_RXCUI = "RXCUI";
33 const CODE_TYPE_ICD10 = 'ICD10';
34 const CODE_TYPE_OID = array(
35 '2.16.840.1.113883.6.96' => self::CODE_TYPE_SNOMED_CT,
36 '2.16.840.1.113883.6.12' => self::CODE_TYPE_CPT4,
37 '2.16.840.1.113883.6.1' => self::CODE_TYPE_LOINC,
38 '2.16.840.1.113883.6.101' => self::CODE_TYPE_NUCC,
39 '2.16.840.1.113883.6.88' => self::CODE_TYPE_RXNORM,
40 '2.16.840.1.113883.6.90' => self::CODE_TYPE_ICD10,
42 /**
43 * @var array
45 public $installedCodeTypes;
46 /**
47 * @var bool
49 private $snomedInstalled;
50 private $cpt4Installed;
51 private $rxnormInstalled;
53 public function __construct()
55 // currently, our installed code types are
56 global $code_types;
57 $this->installedCodeTypes = $code_types;
59 $this->snomedInstalled = isset($code_types[self::CODE_TYPE_SNOMED_CT]);
60 $this->cpt4Installed = isset($code_types[self::CODE_TYPE_CPT4]);
61 $this->rxnormInstalled = isset($code_types[self::CODE_TYPE_RXNORM]);
64 /**
65 * Lookup the description for a series of codes in the database. Eventually we would want to
66 * remove the global lookup_code_descriptions and use this so we can mock these codes and improve our unit test speed.
68 * @param string $codes - Is of the form "type:code;type:code; etc.".
69 * @param string $desc_detail - Can choose either the normal description('code_text') or the brief description('code_text_short').
70 * @return string - Is of the form "description;description; etc.".
71 * @see code_types.inc.php
73 public function lookup_code_description($codes, $desc_detail = "code_text"): string
75 if (empty($codes)) {
76 return "";
78 $code_text = lookup_code_descriptions($codes, $desc_detail);
79 if (empty($code_text)) {
80 // let's return a description of code if available.
81 // sometimes depending on code, long and/or short description may not be available.
82 $desc_detail = $desc_detail == "code_text" ? "code_text_short" : "code_text";
83 $code_text = lookup_code_descriptions($codes, $desc_detail);
85 return $code_text;
88 /**
90 * @return bool - Returns true if the snomed-ct codes are installed
92 public function isSnomedCodesInstalled(): bool
94 return $this->snomedInstalled;
97 /**
99 * @return bool - Returns whether the system has the cpt4 codes installed
101 public function isCPT4Installed(): bool
103 return $this->cpt4Installed;
108 * @return bool - Returns whether the system has the rxnorm codes installed
110 public function isRXNORMInstalled(): bool
112 return $this->rxnormInstalled;
115 public function isInstalledCodeType($codeType): bool
117 return isset($this->installedCodeTypes[$codeType]);
121 * @param $code
122 * @param false $useOid
123 * @return string|null
125 public function getSystemForCode($code, $useOid = false)
127 $codeType = $this->getCodeTypeForCode($code);
128 if (!empty($codeType)) {
129 return $this->getSystemForCodeType($codeType);
131 return null;
135 * @param $code
136 * @return array
138 public function parseCode($code)
140 $parsedCode = $code;
141 $parsedType = null;
142 if (is_string($code) && strpos($code, ":") !== false) {
143 $parts = explode(":", $code);
144 $parsedCode = $parts[1];
145 $parsedType = $parts[0];
147 return ['code' => $parsedCode, 'code_type' => $parsedType];
151 * Returns a code with the code type prefixed
153 * @param $code string The value for the code that exists in the given code_type datadatabse
154 * @param $type string The code_type that the code belongs to (SNOMED, RXCUI, ICD10, etc).
155 * @return string The fully typed code (TYPE:CODE)
157 public function getCodeWithType($code, $type, $oe_format = false)
159 if (empty($type) || empty($code)) {
160 return "";
162 if ($oe_format) {
163 $type = $this->formatCodeType($type ?? "");
165 return ($type ?? "") . ":" . ($code ?? "");
169 * @param $code
170 * @return mixed
172 public function getCodeTypeForCode($code)
174 $parsedCode = $this->parseCode($code);
175 return $parsedCode['code_type'];
179 * @param string $codeType
180 * @param false $useOid
181 * @return string|null
183 public function getSystemForCodeType($codeType, $useOid = false)
185 $system = null;
187 if ($useOid) {
188 if (self::CODE_TYPE_SNOMED_CT == $codeType) {
189 $system = '2.16.840.1.113883.6.96';
190 } elseif (self::CODE_TYPE_SNOMED == $codeType) {
191 $system = '2.16.840.1.113883.6.96';
192 } elseif (self::CODE_TYPE_CPT4 == $codeType) {
193 $system = '2.16.840.1.113883.6.12';
194 } elseif (self::CODE_TYPE_LOINC == $codeType) {
195 $system = '2.16.840.1.113883.6.1';
196 } elseif (self::CODE_TYPE_ICD10 == $codeType) {
197 $system = '2.16.840.1.113883.6.90';
198 } elseif (self::CODE_TYPE_RXCUI == $codeType || self::CODE_TYPE_RXNORM == $codeType) {
199 $system = '2.16.840.1.113883.6.88';
201 } else {
202 if (self::CODE_TYPE_SNOMED_CT == $codeType) {
203 $system = FhirCodeSystemConstants::SNOMED_CT;
204 } elseif (self::CODE_TYPE_SNOMED == $codeType) {
205 $system = FhirCodeSystemConstants::SNOMED_CT;
206 } elseif (self::CODE_TYPE_NUCC == $codeType) {
207 $system = FhirCodeSystemConstants::NUCC_PROVIDER;
208 } elseif (self::CODE_TYPE_LOINC == $codeType) {
209 $system = FhirCodeSystemConstants::LOINC;
210 } elseif (self::CODE_TYPE_RXNORM == $codeType || self::CODE_TYPE_RXCUI == $codeType) {
211 $system = FhirCodeSystemConstants::RXNORM;
214 return $system;
218 * @param string $type
219 * @return string
221 public function formatCodeType($type)
223 switch (strtoupper(trim($type))) {
224 case 'ICD10':
225 case 'ICD10-CM':
226 case 'ICD-10-CM':
227 case 'ICD10CM':
228 $type = 'ICD10';
229 break;
230 case 'SNOMED CT':
231 case 'SNOMED-CT':
232 case 'SNOMEDCT':
233 $type = 'SNOMED-CT';
234 if (!$this->isSnomedCodesInstalled()) {
235 $type = 'SNOMED CT'; // for valueset table lookups
236 break;
238 break;
239 case 'RXCUI':
240 case 'RXNORM':
241 if (!$this->isRXNORMInstalled() && $this->isInstalledCodeType('RXCUI')) {
242 $type = 'RXCUI'; // then let's use RxCUI for lookups
243 } else {
244 $type = 'RXNORM';
246 break;
247 case 'CPT':
248 case 'CPT4':
249 $type = 'CPT4';
250 break;
251 default:
252 if (strpos($type, '2.16.840.1.113883.') !== false) {
253 $type = $this->getCodeTypeFromSystem($type);
256 return $type ?? "";
259 public function getCodeTypeFromSystem($oid): string
261 return self::CODE_TYPE_OID[$oid] ?? '';
265 * Returns a resolved code set including using valueset table to try and get a code set.
267 * @param $code
268 * @param $codeType
269 * @param string $currentCodeText
270 * @param string $codeDescriptionType
271 * @return array
273 public function resolveCode($code, $codeType, $currentCodeText = '', $codeDescriptionType = 'code_text')
275 $valueset = '';
276 $valueset_name = '';
277 $default = array(
278 'code' => $code ?? '',
279 'formatted_code' => $code . ':' . $codeType,
280 'formatted_code_type' => $codeType ?? '',
281 'code_text' => $currentCodeText,
282 'system_oid' => '',
283 'valueset' => '',
284 'valueset_name' => ''
286 if (empty($code)) {
287 $default['formatted_code'] = '';
288 return $default;
291 $formatted_type = $this->formatCodeType($codeType ?: '');
292 $oid = $this->getSystemForCodeType($formatted_type, true);
293 $formatted_code = $this->getCodeWithType($code ?? '', $formatted_type);
294 if (empty($currentCodeText) && $this->isInstalledCodeType($formatted_type)) {
295 $currentCodeText = $this->lookup_code_description($formatted_code, $codeDescriptionType);
298 // use valueset table if code description not found.
299 if (empty($currentCodeText)) {
300 if (strpos($codeType, '2.16.840.1.113883.') !== false) {
301 $oid = trim($codeType);
302 $codeType = "";
304 $value = $this->lookupFromValueset($code, $formatted_type, $oid);
305 $formatted_type = $value['code_type'] ?: $formatted_type;
306 if (!empty($code) && !empty($formatted_type)) {
307 $formatted_code = $formatted_type . ':' . $code;
309 $oid = $value['code_system'];
310 $currentCodeText = $value['description'];
311 $valueset_name = $value['valueset_name'];
312 $valueset = $value['valueset'];
315 return array(
316 'code' => $code ?? "",
317 'formatted_code' => $formatted_code ?: $code,
318 'formatted_code_type' => $formatted_type ?: $codeType,
319 'code_text' => trim($currentCodeText),
320 'system_oid' => $oid ?? "",
321 'valueset' => $valueset ?? "",
322 'valueset_name' => $valueset_name ?? ""
326 public function getInstalledCodeTypes()
328 return $this->installedCodeTypes ?? null;
331 public function lookupFromValueset($code, $codeType, $codeSystem)
333 $value = sqlQuery(
334 "Select * From valueset Where code = ? And (code_type = ? Or code_type LIKE ? Or code_system = ?)",
335 array($code, $codeType, "$codeType%", $codeSystem)
337 return $value;