updated US CDC website link to current immunization VIS page (#7855)
[openemr.git] / src / Services / CodeTypesService.php
bloba339d2f16af6eb24f616c181369d5f5c0a1980b5
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
22 /**
23 * @const string
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'
73 /**
74 * @var array
76 public $installedCodeTypes;
77 /**
78 * @var bool
80 private $snomedInstalled;
81 private $cpt4Installed;
82 private $rxnormInstalled;
84 public function __construct()
86 // currently, our installed code types are
87 global $code_types;
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]);
95 /**
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
106 if (empty($codes)) {
107 return "";
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);
116 return $code_text;
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]);
152 * @param $code
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);
162 return null;
166 * @param $code
167 * @return array
169 public function parseCode($code)
171 $parsedCode = $code;
172 $parsedType = null;
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)) {
191 return "";
193 $tmp = explode(':', $code);
194 if (is_array($tmp) && count($tmp ?? []) === 2) {
195 if (!$oe_format) {
196 return $code;
198 // rebuild when code type format flag is set
199 $type = $tmp[0];
200 $code = $tmp[1];
202 if ($oe_format) {
203 $type = $this->formatCodeType($type ?? "");
205 return ($type ?? "") . ":" . ($code ?? "");
209 * @param $code
210 * @return mixed
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)
225 $system = null;
227 if ($useOid) {
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';
247 } else {
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) {
263 return $oid;
267 return $system;
271 * @param string $type
272 * @return string
274 public function formatCodeType($type)
276 switch (strtoupper(trim($type))) {
277 case 'ICD10':
278 case 'ICD10-CM':
279 case 'ICD-10-CM':
280 case 'ICD10CM':
281 $type = 'ICD10';
282 break;
283 case 'SNOMED CT':
284 case 'SNOMED-CT':
285 case 'SNOMEDCT':
286 $type = 'SNOMED-CT';
287 if (!$this->isSnomedCodesInstalled()) {
288 $type = 'SNOMED CT'; // for valueset table lookups
289 break;
291 break;
292 case 'RXCUI':
293 case 'RXNORM':
294 if (!$this->isRXNORMInstalled() && $this->isInstalledCodeType('RXCUI')) {
295 $type = 'RXCUI'; // then let's use RxCUI for lookups
296 } else {
297 $type = 'RXNORM';
299 break;
300 case 'CPT':
301 case 'CPT4':
302 $type = 'CPT4';
303 break;
304 default:
305 if (strpos($type, '2.16.840.1.113883.') !== false) {
306 $type = $this->getCodeSystemNameFromSystem($type);
309 return $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.
320 * @param $code
321 * @param $codeType
322 * @param string $currentCodeText
323 * @param string $codeDescriptionType
324 * @return array
326 public function resolveCode($code, $codeType, $currentCodeText = '', $codeDescriptionType = 'code_text')
328 $valueset = '';
329 $valueset_name = '';
330 $default = array(
331 'code' => $code ?? '',
332 'formatted_code' => $code . ':' . $codeType,
333 'formatted_code_type' => $codeType ?? '',
334 'code_text' => $currentCodeText,
335 'system_oid' => '',
336 'valueset' => '',
337 'valueset_name' => ''
339 if (empty($code)) {
340 $default['formatted_code'] = '';
341 return $default;
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);
355 $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'] ?? '';
368 return array(
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)) {
387 $value = sqlQuery(
388 "Select * From valueset Where code = ? LIMIT 1",
389 array($code)
391 } else {
392 $value = sqlQuery(
393 "Select * From valueset Where code = ? And (code_type = ? Or code_type LIKE ? Or code_system = ?)",
394 array($code, $codeType, "$codeType%", $codeSystem)
397 return $value;
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] = [
424 'code' => $code
425 , 'description' => $codedesc
426 , 'code_type' => $codeType
427 , 'system' => $system
430 return $codeableConcepts;