cqm-reports 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +0 -1
  3. data/README.md +27 -0
  4. data/lib/cqm_report.rb +1 -4
  5. data/lib/qrda-export/catI-r5/_header.mustache +2 -2
  6. data/lib/qrda-export/catI-r5/qrda1_r5.mustache +2 -2
  7. data/lib/qrda-export/catI-r5/qrda1_r5.rb +24 -2
  8. data/lib/qrda-export/catI-r5/qrda_header/_record_target.mustache +24 -11
  9. data/lib/qrda-export/catI-r5/qrda_templates/allergy_intolerance.mustache +1 -6
  10. data/lib/qrda-export/catI-r5/qrda_templates/communication_performed.mustache +2 -2
  11. data/lib/qrda-export/catI-r5/qrda_templates/diagnosis.mustache +1 -6
  12. data/lib/qrda-export/catI-r5/qrda_templates/immunization_administered.mustache +7 -2
  13. data/lib/qrda-export/catI-r5/qrda_templates/medication_discharge.mustache +0 -2
  14. data/lib/qrda-export/catI-r5/qrda_templates/substance_recommended.mustache +1 -0
  15. data/lib/qrda-export/catI-r5/qrda_templates/symptom.mustache +1 -6
  16. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_medication_supply_request.mustache +1 -1
  17. data/lib/qrda-export/helper/cat1_view_helper.rb +2 -0
  18. data/lib/qrda-import/base-importers/demographics_importer.rb +3 -3
  19. data/lib/qrda-import/base-importers/section_importer.rb +44 -10
  20. data/lib/qrda-import/data-element-importers/diagnostic_study_performed_importer.rb +1 -1
  21. data/lib/qrda-import/data-element-importers/encounter_performed_importer.rb +10 -0
  22. data/lib/qrda-import/data-element-importers/immunization_order_importer.rb +1 -1
  23. data/lib/qrda-import/patient_importer.rb +85 -63
  24. metadata +18 -6
  25. data/lib/ext/code.rb +0 -11
  26. data/lib/ext/data_element.rb +0 -24
  27. data/lib/qrda-import/entry_package.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: ad6bc7084fe1a758d982a929b4175c5ffecc3c3ec1f070bd8ff4944539f469da
4
- data.tar.gz: fb3c251103f1d3e6dae7f138ba9e6f72f261e43650aaada83895eeaca6e1a64b
2
+ SHA1:
3
+ metadata.gz: e8ef5e1f1c6d14646a8d523109b41d9d9609503c
4
+ data.tar.gz: 1ccc64f200b387965a3271501cb933e795435cba
5
5
  SHA512:
6
- metadata.gz: 643b2cc26827bf8507df366ee0354dd2ef6a9b9039be532a289cdebc4178e05edb894bf2eaa4c3fc9b1f3ff8c8d6516c6d82fdc4bdf68189b759d0b83cb9bf13
7
- data.tar.gz: d968b34bdc0e4f0e07ac4a52a403fd494ac1e23dd540fa6557b42a055e2c2ff20782db2d80d11e5ce1695c3be467a1a22795d832dc27165c38178770d19ad09c
6
+ metadata.gz: 62af1b280c4701bed265910f768bb6d19be6f087bd028b022b9cdfbd4b02fae834252fc1ed0798e6555b4e978962f9e4d237d27c9528523cc5c6ba89d8ace2e1
7
+ data.tar.gz: 71cc7d395c9af17f8d946e28ac108f25142c9d2e4e6028e4799e8297fb8a5fc47f1b898939554cbd7616494320ec9d79c8caa489fc8bda0b60f3eb4050f8d69f
data/Gemfile CHANGED
@@ -28,6 +28,5 @@ group :test do
28
28
  gem 'minitest', '~> 5.3'
29
29
  gem 'minitest-reporters'
30
30
  gem 'awesome_print', :require => 'ap'
31
- gem 'cqm-validators'
32
31
  gem 'nokogiri-diff'
33
32
  end
data/README.md CHANGED
@@ -18,6 +18,33 @@ Starting with version **2.0.0** released on 6/20/2019, cqm-reports versioning ha
18
18
 
19
19
  For the versions available, see [tags on this repository](https://github.com/projecttacoma/cqm-validators/tags).
20
20
 
21
+ =======
22
+ Importing QRDA
23
+ ==========
24
+
25
+ A QRDA document can be imported into a CQM::Patient (defined in [cqm-models](https://github.com/projecttacoma/cqm-models)) using the following commands.
26
+
27
+ doc = Nokogiri::XML(file)
28
+ patient, warnings = QRDA::Cat1::PatientImporter.instance.parse_cat1(doc)
29
+
30
+ Exporting QRDA Category I
31
+ ==========
32
+
33
+ Exporting a QRDA document from a CQM::Patient (defined in [cqm-models](https://github.com/projecttacoma/cqm-models)) using the following command.
34
+
35
+ Qrda1R5.new(patient, measures, options).render
36
+ * patient is a [CQM::Patient](https://github.com/projecttacoma/cqm-models/blob/master/app/models/cqm/patient.rb)
37
+ * measures is an array of [CQM::Measure](https://github.com/projecttacoma/cqm-models/blob/master/app/models/cqm/measure.rb)
38
+ * options is a hash that can be used to pass in:
39
+ * provider
40
+ * patient_addresses
41
+ * patient_telecoms
42
+ * start_time
43
+ * end_time
44
+ * submission_program
45
+
46
+ QRDA export requires the [mustache](https://github.com/mustache/mustache) gem
47
+
21
48
  ## License
22
49
 
23
50
  Copyright 2019 The MITRE Corporation
@@ -18,7 +18,6 @@ require_relative 'html-export/qdm-patient/qdm_patient.rb'
18
18
  require_relative 'qrda-export/catI-r5/qrda1_r5.rb'
19
19
  require_relative 'qrda-export/catIII-r2-1/qrda3_r21.rb'
20
20
 
21
- require_relative 'qrda-import/entry_package.rb'
22
21
  require_relative 'qrda-import/cda_identifier.rb'
23
22
  require_relative 'qrda-import/narrative_reference_handler.rb'
24
23
  require_relative 'qrda-import/entry_finder.rb'
@@ -72,6 +71,4 @@ require_relative 'qrda-import/data-element-importers/substance_administered_impo
72
71
  require_relative 'qrda-import/data-element-importers/substance_order_importer.rb'
73
72
  require_relative 'qrda-import/data-element-importers/substance_recommended_importer.rb'
74
73
  require_relative 'qrda-import/data-element-importers/symptom_importer.rb'
75
- require_relative 'qrda-import/patient_importer.rb'
76
- require_relative 'ext/data_element.rb'
77
- require_relative 'ext/code.rb'
74
+ require_relative 'qrda-import/patient_importer.rb'
@@ -6,9 +6,9 @@
6
6
  <!-- QRDA templateId -->
7
7
  <templateId root="2.16.840.1.113883.10.20.24.1.1" extension="2017-08-01"/>
8
8
  <!-- QDM-based QRDA templateId -->
9
- <templateId root="2.16.840.1.113883.10.20.24.1.2" extension="2017-08-01"/>
9
+ <templateId root="2.16.840.1.113883.10.20.24.1.2" extension="2018-10-01"/>
10
10
  <!-- CMS QRDA templateId -->
11
- <templateId root="2.16.840.1.113883.10.20.24.1.3" extension="2018-02-01"/>
11
+ <templateId root="2.16.840.1.113883.10.20.24.1.3" extension="2019-02-01"/>
12
12
  <!-- This is the globally unique identifier for this QRDA document -->
13
13
  <id root="{{random_id}}"/>
14
14
  <!-- QRDA document type code -->
@@ -12,8 +12,8 @@
12
12
  <!-- This is the templateId for Patient Data section -->
13
13
  <templateId root="2.16.840.1.113883.10.20.17.2.4"/>
14
14
  <!-- This is the templateId for Patient Data QDM section -->
15
- <templateId extension="2017-08-01" root="2.16.840.1.113883.10.20.24.2.1"/>
16
- <templateId extension="2018-02-01" root="2.16.840.1.113883.10.20.24.2.1.1"/>
15
+ <templateId extension="2018-10-01" root="2.16.840.1.113883.10.20.24.2.1"/>
16
+ <templateId extension="2019-02-01" root="2.16.840.1.113883.10.20.24.2.1.1"/>
17
17
  <code code="55188-7" codeSystem="2.16.840.1.113883.6.1"/>
18
18
  <title>Patient Data</title>
19
19
  <text/>
@@ -13,11 +13,33 @@ class Qrda1R5 < Mustache
13
13
  @qdmPatient = patient.qdmPatient
14
14
  @measures = measures
15
15
  @provider = options[:provider]
16
+ @patient_address_option = options[:patient_addresses]
17
+ @patient_telecom_option = options[:patient_telecoms]
16
18
  @performance_period_start = options[:start_time]
17
19
  @performance_period_end = options[:end_time]
18
20
  @submission_program = options[:submission_program]
19
21
  end
20
22
 
23
+ def patient_addresses
24
+ @patient_address_option ||= [CQM::Address.new(
25
+ use: 'HP',
26
+ street: ['202 Burlington Rd.'],
27
+ city: 'Bedford',
28
+ state: 'MA',
29
+ zip: '01730',
30
+ country: 'US'
31
+ )]
32
+ JSON.parse(@patient_address_option.to_json)
33
+ end
34
+
35
+ def patient_telecoms
36
+ @patient_telecom_option ||= [CQM::Telecom.new(
37
+ use: 'HP',
38
+ value: '555-555-2003'
39
+ )]
40
+ JSON.parse(@patient_telecom_option.to_json)
41
+ end
42
+
21
43
  def patient_characteristic_payer
22
44
  JSON.parse(@qdmPatient.get_data_elements('patient_characteristic', 'payer').to_json)
23
45
  end
@@ -173,7 +195,7 @@ class Qrda1R5 < Mustache
173
195
  def physical_exam_order
174
196
  JSON.parse(@qdmPatient.get_data_elements('physical_exam', 'order').to_json)
175
197
  end
176
-
198
+
177
199
  def physical_exam_performed
178
200
  JSON.parse(@qdmPatient.get_data_elements('physical_exam', 'performed').to_json)
179
201
  end
@@ -213,7 +235,7 @@ class Qrda1R5 < Mustache
213
235
  def substance_recommended
214
236
  JSON.parse(@qdmPatient.get_data_elements('substance', 'recommended').to_json)
215
237
  end
216
-
238
+
217
239
  def symptom
218
240
  JSON.parse(@qdmPatient.get_data_elements('symptom', nil).to_json)
219
241
  end
@@ -1,14 +1,20 @@
1
1
  <recordTarget>
2
2
  <patientRole>
3
3
  <id extension="{{mrn}}" root="1.3.6.1.4.1.115" />
4
- <addr use="HP">
5
- <streetAddressLine>202 Burlington Rd.</streetAddressLine>
6
- <city>Bedford</city>
7
- <state>MA</state>
8
- <postalCode>01730</postalCode>
9
- <country>US</country>
10
- </addr>
11
- <telecom use="WP" value="tel:+1-781-271-3000"/>
4
+ {{#patient_addresses}}
5
+ <addr use="{{use}}">
6
+ {{#street}}
7
+ <streetAddressLine>{{.}}</streetAddressLine>
8
+ {{/street}}
9
+ <city>{{city}}</city>
10
+ <state>{{state}}</state>
11
+ <postalCode>{{zip}}</postalCode>
12
+ <country>{{country}}</country>
13
+ </addr>
14
+ {{/patient_addresses}}
15
+ {{#patient_telecoms}}
16
+ <telecom use="{{use}}" value="tel:{{value}}"/>
17
+ {{/patient_telecoms}}
12
18
  <patient>
13
19
  {{#patient}}
14
20
  <name>
@@ -21,6 +27,9 @@
21
27
  <administrativeGenderCode {{> _code}}/>
22
28
  {{/dataElementCodes}}
23
29
  {{/patient_characteristic_sex}}
30
+ {{^patient_characteristic_sex}}
31
+ <administrativeGenderCode nullFlavor="UNK"/>
32
+ {{/patient_characteristic_sex}}
24
33
  {{#patient_characteristic_birthdate}}
25
34
  {{{birth_date_time}}}
26
35
  {{/patient_characteristic_birthdate}}
@@ -29,16 +38,20 @@
29
38
  <raceCode {{> _code}}/>
30
39
  {{/dataElementCodes}}
31
40
  {{/patient_characteristic_race}}
41
+ {{^patient_characteristic_race}}
42
+ <raceCode nullFlavor="UNK"/>
43
+ {{/patient_characteristic_race}}
32
44
  {{#patient_characteristic_ethnicity}}
33
45
  {{#dataElementCodes}}
34
46
  <ethnicGroupCode {{> _code}}/>
35
47
  {{/dataElementCodes}}
36
48
  {{/patient_characteristic_ethnicity}}
49
+ {{^patient_characteristic_ethnicity}}
50
+ <ethnicGroupCode nullFlavor="UNK"/>
51
+ {{/patient_characteristic_ethnicity}}
37
52
  <languageCommunication>
38
- <templateId root="2.16.840.1.113883.3.88.11.83.2" assigningAuthorityName="HITSP/C83"/>
39
- <templateId root="1.3.6.1.4.1.19376.1.5.3.1.2.1" assigningAuthorityName="IHE/PCC"/>
40
53
  <languageCode code="eng"/>
41
54
  </languageCommunication>
42
55
  </patient>
43
56
  </patientRole>
44
- </recordTarget>
57
+ </recordTarget>
@@ -6,13 +6,8 @@
6
6
  <templateId root="2.16.840.1.113883.10.20.24.3.147" extension="2017-08-01"/>
7
7
  <id root="1.3.6.1.4.1.115" extension="{{object_id}}"/>
8
8
  <code code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
9
- {{#prevalencePeriod}}
10
- {{#completed_prevalence_period}}
11
9
  <statusCode code="completed" />
12
- {{/completed_prevalence_period}}
13
- {{^completed_prevalence_period}}
14
- <statusCode code="active" />
15
- {{/completed_prevalence_period}}
10
+ {{#prevalencePeriod}}
16
11
  <!-- QDM Attribute: Prevalence Period -->
17
12
  {{{prevalence_period}}}
18
13
  {{/prevalencePeriod}}
@@ -8,7 +8,7 @@
8
8
  {{/category}}
9
9
  {{^category}}
10
10
  <!-- QDM Attribute: Category -->
11
- <code nullFlavor="UNK"/>
11
+ <code nullFlavor="NA"/>
12
12
  {{/category}}
13
13
  <statusCode code="completed"/>
14
14
  {{#relevantPeriod}}
@@ -48,7 +48,7 @@
48
48
  {{/negationRationale}}
49
49
  <entryRelationship typeCode="REFR">
50
50
  <observation classCode="OBS" moodCode="EVN">
51
- <templateId root="2.16.840.1.113883.10.20.24.3.88" extension="2014-12-01"/>
51
+ <templateId root="2.16.840.1.113883.10.20.24.3.88" extension="2017-08-01"/>
52
52
  <id root="1.3.6.1.4.1.115" extension="{{object_id}}" />
53
53
  <code code="77301-0" codeSystem="2.16.840.1.113883.6.1" displayName="reason" codeSystemName="LOINC"/>
54
54
  <statusCode code="completed"/>
@@ -25,13 +25,8 @@
25
25
  <code code="29308-4" codeSystem="2.16.840.1.113883.6.1">
26
26
  <translation code="282291009" codeSystem="2.16.840.1.113883.6.96"/>
27
27
  </code>
28
- {{#prevalencePeriod}}
29
- {{#completed_prevalence_period}}
30
28
  <statusCode code="completed" />
31
- {{/completed_prevalence_period}}
32
- {{^completed_prevalence_period}}
33
- <statusCode code="active" />
34
- {{/completed_prevalence_period}}
29
+ {{#prevalencePeriod}}
35
30
  <!-- QDM Attribute: Prevalence Period -->
36
31
  {{{prevalence_period}}}
37
32
  {{/prevalencePeriod}}
@@ -1,7 +1,12 @@
1
1
  <entry>
2
- <substanceAdministration classCode="SBADM" moodCode="EVN" {{{negation_ind}}}>
2
+ {{#negated}}
3
+ <substanceAdministration classCode="SBADM" moodCode="EVN" {{{negation_ind}}}>
4
+ {{/negated}}
5
+ {{^negated}}
6
+ <substanceAdministration classCode="SBADM" moodCode="EVN" negationInd="false">
7
+ {{/negated}}
3
8
  <!-- C-CDA R2 Immunization Activity -->
4
- <templateId root="2.16.840.1.113883.10.20.22.4.52" extension="2014-06-09"/>
9
+ <templateId root="2.16.840.1.113883.10.20.22.4.52" extension="2015-08-01"/>
5
10
  <!-- Immunization Administered -->
6
11
  <templateId root="2.16.840.1.113883.10.20.24.3.140" extension="2017-08-01"/>
7
12
  <id root="1.3.6.1.4.1.115" extension="{{object_id}}"/>
@@ -9,8 +9,6 @@
9
9
  <substanceAdministration moodCode="EVN" classCode="SBADM">
10
10
  <!-- Medication Activity (consolidation) template -->
11
11
  <templateId root="2.16.840.1.113883.10.20.22.4.16" extension="2014-06-09"/>
12
- <!-- Medication, Active template -->
13
- <templateId root="2.16.840.1.113883.10.20.24.3.41" extension="2016-02-01"/>
14
12
  <id root="1.3.6.1.4.1.115" extension="{{object_id}}"/>
15
13
  <text>{{description}}</text>
16
14
  <statusCode code="active"/>
@@ -6,6 +6,7 @@
6
6
  <templateId root="2.16.840.1.113883.10.20.24.3.75" extension="2017-08-01"/>
7
7
  <id root="1.3.6.1.4.1.115" extension="{{object_id}}"/>
8
8
  <statusCode code="active"/>
9
+ {{{author_effective_time}}}
9
10
  {{> qrda_templates/template_partials/_medication_details}}
10
11
  <consumable>
11
12
  <manufacturedProduct classCode="MANU">
@@ -25,13 +25,8 @@
25
25
  <code code="75325-1" codeSystem="2.16.840.1.113883.6.1">
26
26
  <translation code="418799008" displayName="Symptom" codeSystem="2.16.840.1.113883.6.96"/>
27
27
  </code>
28
- {{#prevalencePeriod}}
29
- {{#completed_prevalence_period}}
30
28
  <statusCode code="completed" />
31
- {{/completed_prevalence_period}}
32
- {{^completed_prevalence_period}}
33
- <statusCode code="active" />
34
- {{/completed_prevalence_period}}
29
+ {{#prevalencePeriod}}
35
30
  <!-- QDM Attribute: Prevalence Period -->
36
31
  {{{prevalence_period}}}
37
32
  {{/prevalencePeriod}}
@@ -1,6 +1,6 @@
1
1
  <supply classCode="SPLY" moodCode="RQO">
2
2
  <!-- Medication Supply Request (V2) template -->
3
- <templateId root="2.16.840.1.113883.10.20.24.3.99" extension="2014-04-05"/>
3
+ <templateId root="2.16.840.1.113883.10.20.24.3.99" extension="2015-04-05"/>
4
4
  <!-- Conforms to C-CDA R2 Planned Supply (V2) template-->
5
5
  <templateId root="2.16.840.1.113883.10.20.22.4.43" extension="2014-06-09" />
6
6
  <id root="{{random_id}}"/>
@@ -67,6 +67,8 @@ module Qrda
67
67
  result_value_as_string(self['result'][0])
68
68
  elsif self['result'].is_a? Hash
69
69
  result_value_as_string(self['result'])
70
+ elsif self['result'].is_a? String
71
+ "<value xsi:type=\"ST\">#{self['result']}</value>"
70
72
  elsif !self['result'].nil?
71
73
  "<value xsi:type=\"PQ\" value=\"#{self['result']}\" unit=\"1\"/>"
72
74
  end
@@ -15,17 +15,17 @@ module QRDA
15
15
  pcs = QDM::PatientCharacteristicSex.new
16
16
  code_element = patient_element.at_xpath('cda:administrativeGenderCode')
17
17
  pcs.dataElementCodes = [code_if_present(code_element)]
18
- patient.qdmPatient.dataElements << pcs
18
+ patient.qdmPatient.dataElements << pcs unless pcs.dataElementCodes.compact.blank?
19
19
 
20
20
  pcr = QDM::PatientCharacteristicRace.new
21
21
  code_element = patient_element.at_xpath('cda:raceCode')
22
22
  pcr.dataElementCodes = [code_if_present(code_element)]
23
- patient.qdmPatient.dataElements << pcr
23
+ patient.qdmPatient.dataElements << pcr unless pcr.dataElementCodes.compact.blank?
24
24
 
25
25
  pce = QDM::PatientCharacteristicEthnicity.new
26
26
  code_element = patient_element.at_xpath('cda:ethnicGroupCode')
27
27
  pce.dataElementCodes = [code_if_present(code_element)]
28
- patient.qdmPatient.dataElements << pce
28
+ patient.qdmPatient.dataElements << pce unless pce.dataElementCodes.compact.blank?
29
29
  end
30
30
 
31
31
  def code_if_present(code_element)
@@ -1,7 +1,7 @@
1
1
  module QRDA
2
2
  module Cat1
3
3
  class SectionImporter
4
- attr_accessor :check_for_usable, :status_xpath, :code_xpath
4
+ attr_accessor :check_for_usable, :status_xpath, :code_xpath, :warnings, :codes_modifiers
5
5
 
6
6
  def initialize(entry_finder)
7
7
  @entry_finder = entry_finder
@@ -9,6 +9,8 @@ module QRDA
9
9
  @entry_id_map = {}
10
10
  @check_for_usable = true
11
11
  @entry_class = QDM::DataElement
12
+ @warnings = []
13
+ @codes_modifiers = {}
12
14
  end
13
15
 
14
16
  # Traverses an HL7 CDA document passed in and creates an Array of Entry
@@ -41,7 +43,7 @@ module QRDA
41
43
  # This is the id found in the QRDA file
42
44
  entry_qrda_id = extract_id(entry_element, @id_xpath)
43
45
  # Create a hash to map all of entry.ids to the same QRDA ids. This will be used to merge QRDA entries
44
- # that represent the same event.
46
+ # that represent the same event.
45
47
  @entry_id_map["#{entry_qrda_id.value}_#{entry_qrda_id.namingSystem}"] ||= []
46
48
  @entry_id_map["#{entry_qrda_id.value}_#{entry_qrda_id.namingSystem}"] << entry.id
47
49
  entry.dataElementCodes = extract_codes(entry_element, @code_xpath)
@@ -90,6 +92,7 @@ module QRDA
90
92
  end
91
93
 
92
94
  def extract_interval(parent_element, interval_xpath)
95
+ return nil unless parent_element.at_xpath(interval_xpath)
93
96
  if parent_element.at_xpath("#{interval_xpath}/@value")
94
97
  low_time = DateTime.parse(parent_element.at_xpath(interval_xpath)['value'])
95
98
  high_time = DateTime.parse(parent_element.at_xpath(interval_xpath)['value'])
@@ -108,6 +111,21 @@ module QRDA
108
111
  low_time = Time.parse(parent_element.at_xpath("#{interval_xpath}/cda:center")['value'])
109
112
  high_time = Time.parse(parent_element.at_xpath("#{interval_xpath}/cda:center")['value'])
110
113
  end
114
+ if low_time && high_time && low_time > high_time
115
+ # pass warning: current code continues as expected, but adds warning
116
+ id_attr = parent_element.at_xpath(".//cda:id")&.attributes
117
+ id_str = id_attr ? "and id: #{id_attr['root']&.value}(root), #{id_attr['extension']&.value}(extension)" : ""
118
+ qrda_type = @entry_class.to_s.split("::")[1]
119
+ @warnings << ValidationError.new(message: "Interval with low time after high time. Located in element with QRDA type: #{qrda_type} #{id_str}",
120
+ location: parent_element.path)
121
+ end
122
+ if low_time.nil? && high_time.nil?
123
+ id_attr = parent_element.at_xpath(".//cda:id")&.attributes
124
+ id_str = id_attr ? "and id: #{id_attr['root']&.value}(root), #{id_attr['extension']&.value}(extension)" : ""
125
+ qrda_type = @entry_class.to_s.split("::")[1]
126
+ @warnings << ValidationError.new(message: "Interval with nullFlavor low time and nullFlavor high time. Located in element with QRDA type: #{qrda_type} #{id_str}",
127
+ location: parent_element.path)
128
+ end
111
129
  QDM::Interval.new(low_time, high_time).shift_dates(0)
112
130
  end
113
131
 
@@ -150,12 +168,11 @@ module QRDA
150
168
  parent_element.xpath(@result_xpath).each do |elem|
151
169
  result << extract_result_value(elem)
152
170
  end
153
- result.size > 1 ? result : result.first
171
+ result.size > 1 ? result : result.first
154
172
  end
155
173
 
156
174
  def extract_result_value(value_element)
157
175
  return unless value_element && !value_element['nullFlavor']
158
-
159
176
  value = value_element['value']
160
177
  if value.present?
161
178
  return value.strip.to_f if (value_element['unit'] == "1" || value_element['unit'].nil?)
@@ -163,6 +180,13 @@ module QRDA
163
180
  return QDM::Quantity.new(value.strip.to_f, value_element['unit'])
164
181
  elsif value_element['code'].present?
165
182
  return code_if_present(value_element)
183
+ elsif value_element.text.present?
184
+ id_attr = value_element.parent.at_xpath(".//cda:id")&.attributes
185
+ id_str = id_attr ? "and id: #{id_attr['root']&.value}(root), #{id_attr['extension']&.value}(extension)" : ""
186
+ qrda_type = @entry_class.to_s.split("::")[1]
187
+ @warnings << ValidationError.new(message: "Value with string type found. When possible, it's best practice to use a coded value or scalar. Located in element with QRDA type: #{qrda_type} #{id_str}",
188
+ location: value_element.path)
189
+ return value_element.text
166
190
  end
167
191
  end
168
192
 
@@ -172,16 +196,16 @@ module QRDA
172
196
  negation_indicator = parent_element['negationInd']
173
197
  # Return and do not set reason attribute if the entry is negated
174
198
  return nil if negation_indicator.eql?('true')
175
-
176
- reason_element.blank? ? nil : code_if_present(reason_element.first)
199
+
200
+ reason_element.blank? ? nil : code_if_present(reason_element.first)
177
201
  end
178
202
 
179
203
  def extract_negation(parent_element, entry)
180
204
  negation_element = parent_element.xpath("./cda:entryRelationship[@typeCode='RSON']/cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.24.3.88']/cda:value")
181
205
  negation_indicator = parent_element['negationInd']
182
206
  # Return and do not set negationRationale attribute if the entry is not negated
183
- return unless negation_indicator.eql?('true')
184
-
207
+ return unless negation_indicator.eql?('true')
208
+
185
209
  entry.negationRationale = code_if_present(negation_element.first) unless negation_element.blank?
186
210
  extract_negated_code(parent_element, entry)
187
211
  end
@@ -189,8 +213,18 @@ module QRDA
189
213
  def extract_negated_code(parent_element, entry)
190
214
  code_elements = parent_element.xpath(@code_xpath)
191
215
  code_elements.each do |code_element|
192
- if code_element['nullFlavor'] == 'NA' && code_element['sdtc:valueSet']
193
- entry.dataElementCodes = [{ code: code_element['sdtc:valueSet'], codeSystemOid: '1.2.3.4.5.6.7.8.9.10' }]
216
+ if code_element['nullFlavor'] == 'NA'
217
+ if code_element['sdtc:valueSet']
218
+ entry.dataElementCodes = [{ code: code_element['sdtc:valueSet'], codeSystemOid: '1.2.3.4.5.6.7.8.9.10' }]
219
+ else
220
+ # negated code is nullFlavored with no valueset
221
+ entry.dataElementCodes = [{ code: "NA", codeSystemOid: '1.2.3.4.5.6.7.8.9.10' }]
222
+ id_attr = parent_element.at_xpath(".//cda:id")&.attributes
223
+ id_str = id_attr ? "and id: #{id_attr['root']&.value}(root), #{id_attr['extension']&.value}(extension)" : ""
224
+ qrda_type = @entry_class.to_s.split("::")[1]
225
+ @warnings << ValidationError.new(message: "Negated code element contains nullFlavor code but no valueset. Located in element with QRDA type: #{qrda_type} #{id_str}.",
226
+ location: parent_element.path)
227
+ end
194
228
  end
195
229
  end
196
230
  end
@@ -20,7 +20,7 @@ module QRDA
20
20
  def create_entry(entry_element, nrh = NarrativeReferenceHandler.new)
21
21
  diagnostic_study_performed = super
22
22
  diagnostic_study_performed.resultDatetime = extract_time(entry_element, @result_datetime_xpath)
23
- diagnostic_study_performed.resultDatetime ||= extract_interval(entry_element, @result_datetime_xpath).low
23
+ diagnostic_study_performed.resultDatetime ||= extract_interval(entry_element, @result_datetime_xpath)&.low
24
24
  diagnostic_study_performed.status = code_if_present(entry_element.at_xpath(@status_xpath))
25
25
  diagnostic_study_performed.method = code_if_present(entry_element.at_xpath(@method_xpath))
26
26
  diagnostic_study_performed.facilityLocation = extract_facility_locations(entry_element)[0]
@@ -26,11 +26,21 @@ module QRDA
26
26
  los = encounter_performed.relevantPeriod.high - encounter_performed.relevantPeriod.low
27
27
  encounter_performed.lengthOfStay = QDM::Quantity.new(los.to_i, 'd')
28
28
  end
29
+ extract_modifier_code(encounter_performed, entry_element)
29
30
  encounter_performed
30
31
  end
31
32
 
32
33
  private
33
34
 
35
+ def extract_modifier_code(encounter_performed, entry_element)
36
+ code_element = entry_element.at_xpath(@code_xpath)
37
+ return unless code_element
38
+
39
+ qualifier_name = code_element.at_xpath('./cda:qualifier/cda:name')
40
+ qualifier_value = code_element.at_xpath('./cda:qualifier/cda:value')
41
+ codes_modifiers[encounter_performed.id] = { name: code_if_present(qualifier_name), value: code_if_present(qualifier_value), xpath_location: entry_element.path } if qualifier_value || qualifier_name
42
+ end
43
+
34
44
  def extract_diagnoses(parent_element)
35
45
  diagnosis_elements = parent_element.xpath(@diagnosis_xpath)
36
46
  diagnosis_list = []
@@ -16,7 +16,7 @@ module QRDA
16
16
 
17
17
  def create_entry(entry_element, nrh = NarrativeReferenceHandler.new)
18
18
  immunization_order = super
19
- immunization_order.activeDatetime = extract_interval(entry_element, @active_datetime_xpath).low
19
+ immunization_order.activeDatetime = extract_interval(entry_element, @active_datetime_xpath)&.low
20
20
  immunization_order.dosage = extract_scalar(entry_element, @dosage_xpath)
21
21
  immunization_order.supply = extract_scalar(entry_element, @supply_xpath)
22
22
  immunization_order.route = code_if_present(entry_element.at_xpath(@route_xpath))
@@ -12,89 +12,117 @@ module QRDA
12
12
  def initialize
13
13
  # This differs from other HDS patient importers in that sections can have multiple importers
14
14
  @data_element_importers = []
15
- @data_element_importers << generate_importer(AdverseEventImporter)
16
- @data_element_importers << generate_importer(AllergyIntoleranceImporter)
17
- @data_element_importers << generate_importer(AssessmentOrderImporter)
18
- @data_element_importers << generate_importer(AssessmentPerformedImporter)
19
- @data_element_importers << generate_importer(AssessmentRecommendedImporter)
20
- @data_element_importers << generate_importer(CommunicationPerformedImporter)
21
- @data_element_importers << generate_importer(DeviceAppliedImporter)
22
- @data_element_importers << generate_importer(DeviceOrderImporter)
23
- @data_element_importers << generate_importer(DeviceRecommendedImporter)
24
- @data_element_importers << generate_importer(DiagnosisImporter)
25
- @data_element_importers << generate_importer(DiagnosticStudyOrderImporter)
26
- @data_element_importers << generate_importer(DiagnosticStudyPerformedImporter)
27
- @data_element_importers << generate_importer(DiagnosticStudyRecommendedImporter)
28
- @data_element_importers << generate_importer(EncounterOrderImporter)
29
- @data_element_importers << generate_importer(EncounterPerformedImporter)
30
- @data_element_importers << generate_importer(EncounterRecommendedImporter)
31
- @data_element_importers << generate_importer(FamilyHistoryImporter)
32
- @data_element_importers << generate_importer(ImmunizationAdministeredImporter)
33
- @data_element_importers << generate_importer(ImmunizationOrderImporter)
34
- @data_element_importers << generate_importer(InterventionOrderImporter)
35
- @data_element_importers << generate_importer(InterventionPerformedImporter)
36
- @data_element_importers << generate_importer(InterventionRecommendedImporter)
37
- @data_element_importers << generate_importer(LaboratoryTestOrderImporter)
38
- @data_element_importers << generate_importer(LaboratoryTestPerformedImporter)
39
- @data_element_importers << generate_importer(LaboratoryTestRecommendedImporter)
40
- @data_element_importers << generate_importer(MedicationActiveImporter)
41
- @data_element_importers << generate_importer(MedicationAdministeredImporter)
42
- @data_element_importers << generate_importer(MedicationDischargeImporter)
43
- @data_element_importers << generate_importer(MedicationDispensedImporter)
44
- @data_element_importers << generate_importer(MedicationOrderImporter)
45
- @data_element_importers << generate_importer(PatientCareExperienceImporter)
46
- @data_element_importers << generate_importer(PatientCharacteristicClinicalTrialParticipantImporter)
47
- @data_element_importers << generate_importer(PatientCharacteristicExpiredImporter)
48
- @data_element_importers << generate_importer(PatientCharacteristicPayerImporter)
49
- @data_element_importers << generate_importer(PhysicalExamOrderImporter)
50
- @data_element_importers << generate_importer(PhysicalExamPerformedImporter)
51
- @data_element_importers << generate_importer(PhysicalExamRecommendedImporter)
52
- @data_element_importers << generate_importer(ProcedureOrderImporter)
53
- @data_element_importers << generate_importer(ProcedurePerformedImporter)
54
- @data_element_importers << generate_importer(ProcedureRecommendedImporter)
55
- @data_element_importers << generate_importer(ProviderCareExperienceImporter)
56
- @data_element_importers << generate_importer(ProviderCharacteristicImporter)
57
- @data_element_importers << generate_importer(SubstanceAdministeredImporter)
58
- @data_element_importers << generate_importer(SubstanceOrderImporter)
59
- @data_element_importers << generate_importer(SubstanceRecommendedImporter)
60
- @data_element_importers << generate_importer(SymptomImporter)
15
+ @data_element_importers << AdverseEventImporter.new
16
+ @data_element_importers << AllergyIntoleranceImporter.new
17
+ @data_element_importers << AssessmentOrderImporter.new
18
+ @data_element_importers << AssessmentPerformedImporter.new
19
+ @data_element_importers << AssessmentRecommendedImporter.new
20
+ @data_element_importers << CommunicationPerformedImporter.new
21
+ @data_element_importers << DeviceAppliedImporter.new
22
+ @data_element_importers << DeviceOrderImporter.new
23
+ @data_element_importers << DeviceRecommendedImporter.new
24
+ @data_element_importers << DiagnosisImporter.new
25
+ @data_element_importers << DiagnosticStudyOrderImporter.new
26
+ @data_element_importers << DiagnosticStudyPerformedImporter.new
27
+ @data_element_importers << DiagnosticStudyRecommendedImporter.new
28
+ @data_element_importers << EncounterOrderImporter.new
29
+ @data_element_importers << EncounterPerformedImporter.new
30
+ @data_element_importers << EncounterRecommendedImporter.new
31
+ @data_element_importers << FamilyHistoryImporter.new
32
+ @data_element_importers << ImmunizationAdministeredImporter.new
33
+ @data_element_importers << ImmunizationOrderImporter.new
34
+ @data_element_importers << InterventionOrderImporter.new
35
+ @data_element_importers << InterventionPerformedImporter.new
36
+ @data_element_importers << InterventionRecommendedImporter.new
37
+ @data_element_importers << LaboratoryTestOrderImporter.new
38
+ @data_element_importers << LaboratoryTestPerformedImporter.new
39
+ @data_element_importers << LaboratoryTestRecommendedImporter.new
40
+ @data_element_importers << MedicationActiveImporter.new
41
+ @data_element_importers << MedicationAdministeredImporter.new
42
+ @data_element_importers << MedicationDischargeImporter.new
43
+ @data_element_importers << MedicationDispensedImporter.new
44
+ @data_element_importers << MedicationOrderImporter.new
45
+ @data_element_importers << PatientCareExperienceImporter.new
46
+ @data_element_importers << PatientCharacteristicClinicalTrialParticipantImporter.new
47
+ @data_element_importers << PatientCharacteristicExpiredImporter.new
48
+ @data_element_importers << PatientCharacteristicPayerImporter.new
49
+ @data_element_importers << PhysicalExamOrderImporter.new
50
+ @data_element_importers << PhysicalExamPerformedImporter.new
51
+ @data_element_importers << PhysicalExamRecommendedImporter.new
52
+ @data_element_importers << ProcedureOrderImporter.new
53
+ @data_element_importers << ProcedurePerformedImporter.new
54
+ @data_element_importers << ProcedureRecommendedImporter.new
55
+ @data_element_importers << ProviderCareExperienceImporter.new
56
+ @data_element_importers << ProviderCharacteristicImporter.new
57
+ @data_element_importers << SubstanceAdministeredImporter.new
58
+ @data_element_importers << SubstanceOrderImporter.new
59
+ @data_element_importers << SubstanceRecommendedImporter.new
60
+ @data_element_importers << SymptomImporter.new
61
61
  end
62
62
 
63
63
  def parse_cat1(doc)
64
64
  patient = CQM::Patient.new
65
+ warnings = []
66
+ codes_modifiers = {}
65
67
  entry_id_map = {}
66
- import_data_elements(patient, doc, entry_id_map)
68
+ import_data_elements(patient, doc, entry_id_map, codes_modifiers, warnings)
67
69
  normalize_references(patient, entry_id_map)
68
70
  get_demographics(patient, doc)
69
- patient
71
+ [patient, warnings, codes_modifiers]
70
72
  end
71
73
 
72
- def import_data_elements(patient, doc, entry_id_map)
74
+ def import_data_elements(patient, doc, entry_id_map, codes_modifiers, warnings = [])
73
75
  context = doc.xpath("/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section[cda:templateId/@root = '2.16.840.1.113883.10.20.24.2.1']")
74
76
  nrh = NarrativeReferenceHandler.new
75
77
  nrh.build_id_map(doc)
76
- @data_element_importers.each do |entry_package|
77
- data_elements, id_map = entry_package.package_entries(context, nrh)
78
+ @data_element_importers.each do |importer|
79
+ data_elements, id_map = importer.create_entries(context, nrh)
78
80
  new_data_elements = []
79
81
 
80
82
  id_map.each_value do |elem_ids|
81
-
82
83
  elem_id = elem_ids.first
83
84
  data_element = data_elements.find { |de| de.id == elem_id }
85
+ # Keep the first element with a shared ID
86
+ new_data_elements << data_element
84
87
 
85
- elem_ids[1,elem_ids.length].each do |merge_id|
86
- merge_element = data_elements.find { |de| de.id == merge_id }
87
- data_element.merge!(merge_element)
88
- end
88
+ # Encounters require elements beyond id for uniqueness
89
+ next unless data_element._type == 'QDM::EncounterPerformed'
90
+ unique_element_keys = []
91
+ # Add key_elements_for_determining_encounter_uniqueness to array, this is used to determine if other
92
+ # elements with the same ID should be considered as unique
93
+ unique_element_keys << key_elements_for_determining_encounter_uniqueness(data_element)
89
94
 
90
- new_data_elements << data_element
95
+ # Loop through all other data elements with the same id
96
+ elem_ids[1,elem_ids.length].each do |dup_id|
97
+ dup_element = data_elements.find { |de| de.id == dup_id }
98
+ dup_element_keys = key_elements_for_determining_encounter_uniqueness(dup_element)
99
+ # See if a previously selected data element shared all of the keys files
100
+ # If all key fields match, move on.
101
+ next if unique_element_keys.include?(dup_element_keys)
102
+ # If all key fields don't match, keep element
103
+ new_data_elements << dup_element
104
+ # Add to list of unique element keys
105
+ unique_element_keys << dup_element_keys
106
+ end
91
107
  end
92
108
 
93
109
  patient.qdmPatient.dataElements << new_data_elements
94
110
  entry_id_map.merge!(id_map)
111
+ warnings.concat(importer.warnings)
112
+ codes_modifiers.merge!(importer.codes_modifiers)
113
+ # reset warnings after they're captured so that the importer can be re-used
114
+ importer.warnings = []
115
+ importer.codes_modifiers = {}
95
116
  end
96
117
  end
97
118
 
119
+ def key_elements_for_determining_encounter_uniqueness(encounter)
120
+ codes = encounter.codes.collect { |dec| "#{dec.code}_#{dec.codeSystemOid}" }.sort.to_s
121
+ admission_date_time = encounter&.relevantPeriod&.low.to_s
122
+ discharge_date_time = encounter&.relevantPeriod&.high.to_s
123
+ "#{codes}#{admission_date_time}#{discharge_date_time}"
124
+ end
125
+
98
126
  def get_patient_expired(record, doc)
99
127
  entry_elements = doc.xpath("/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section[cda:templateId/@root = '2.16.840.1.113883.10.20.24.2.1']/cda:entry/cda:observation[cda:templateId/@root = '2.16.840.1.113883.10.20.24.3.54']")
100
128
  return unless entry_elements.empty?
@@ -117,12 +145,6 @@ module QRDA
117
145
  end
118
146
  end
119
147
  end
120
-
121
- private
122
-
123
- def generate_importer(importer_class)
124
- EntryPackage.new(importer_class.new)
125
- end
126
148
  end
127
149
  end
128
150
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cqm-reports
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.4
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MITRE Corporation
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-28 00:00:00.000000000 Z
11
+ date: 2020-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cqm-models
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 2.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: cqm-validators
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: mustache
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -147,8 +161,6 @@ files:
147
161
  - README.md
148
162
  - Rakefile
149
163
  - lib/cqm_report.rb
150
- - lib/ext/code.rb
151
- - lib/ext/data_element.rb
152
164
  - lib/html-export/qdm-patient/_header_css.mustache
153
165
  - lib/html-export/qdm-patient/_javascript.mustache
154
166
  - lib/html-export/qdm-patient/data_element/_data_element.mustache
@@ -308,7 +320,6 @@ files:
308
320
  - lib/qrda-import/data-element-importers/substance_recommended_importer.rb
309
321
  - lib/qrda-import/data-element-importers/symptom_importer.rb
310
322
  - lib/qrda-import/entry_finder.rb
311
- - lib/qrda-import/entry_package.rb
312
323
  - lib/qrda-import/narrative_reference_handler.rb
313
324
  - lib/qrda-import/patient_importer.rb
314
325
  - lib/util/code_system_helper.rb
@@ -338,7 +349,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
338
349
  - !ruby/object:Gem::Version
339
350
  version: '0'
340
351
  requirements: []
341
- rubygems_version: 3.0.3
352
+ rubyforge_project:
353
+ rubygems_version: 2.6.14
342
354
  signing_key:
343
355
  specification_version: 4
344
356
  summary: A library for import and export of reports for use with electronic Clinical
@@ -1,11 +0,0 @@
1
- module QDM
2
- class Code
3
- def ==(other)
4
- return false unless other.is_a? QDM::Code
5
-
6
- (code == other.code) && (codeSystem == other.codeSystem) && (codeSystemOid == other.codeSystemOid) && (version == other.version)
7
- end
8
-
9
- alias eql? ==
10
- end
11
- end
@@ -1,24 +0,0 @@
1
- module QDM
2
- class DataElement
3
- def merge!(other)
4
- # ensure they're the same category (e.g. 'encounter')
5
- return unless qdmCategory == other.qdmCategory
6
-
7
- # ensure they're the same status (e.g. 'performed'), and that they both have a status set (or that they both don't)
8
- return if respond_to?(:qdmStatus) && !other.respond_to?(:qdmStatus)
9
- return if !respond_to?(:qdmStatus) && other.respond_to?(:qdmStatus)
10
- return if respond_to?(:qdmStatus) && other.respond_to?(:qdmStatus) && qdmStatus != other.qdmStatus
11
-
12
- # iterate over non-code fields
13
- fields.each_key do |field|
14
- next if field[0] == '_' || %w[dataElementCodes qdmCategory qdmVersion qdmStatus].include?(field)
15
-
16
- if send(field).nil?
17
- send(field + '=', other.send(field))
18
- end
19
- end
20
-
21
- self.dataElementCodes = dataElementCodes.concat(other.dataElementCodes).uniq
22
- end
23
- end
24
- end
@@ -1,16 +0,0 @@
1
- module QRDA
2
- module Cat1
3
- class EntryPackage
4
-
5
- attr_accessor :importer_type
6
-
7
- def initialize(type)
8
- self.importer_type = type
9
- end
10
-
11
- def package_entries(doc, nrh)
12
- importer_type.create_entries(doc, nrh)
13
- end
14
- end
15
- end
16
- end