health-data-standards 3.3.0 → 3.4.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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +16 -0
  3. data/lib/health-data-standards.rb +3 -0
  4. data/lib/health-data-standards/export/helper/cat1_view_helper.rb +1 -1
  5. data/lib/health-data-standards/export/rendering_context.rb +4 -2
  6. data/lib/health-data-standards/export/template_helper.rb +9 -3
  7. data/lib/health-data-standards/import/bulk_record_importer.rb +88 -0
  8. data/lib/health-data-standards/import/cat1/diagnostic_study_order_importer.rb +1 -1
  9. data/lib/health-data-standards/import/cat1/encounter_performed_importer.rb +38 -0
  10. data/lib/health-data-standards/import/cat1/patient_importer.rb +4 -4
  11. data/lib/health-data-standards/import/cat1/procedure_order_importer.rb +2 -2
  12. data/lib/health-data-standards/import/cat1/procedure_performed_importer.rb +1 -0
  13. data/lib/health-data-standards/import/cda/allergy_importer.rb +0 -2
  14. data/lib/health-data-standards/import/cda/encounter_importer.rb +24 -1
  15. data/lib/health-data-standards/import/cda/medical_equipment_importer.rb +8 -1
  16. data/lib/health-data-standards/import/cda/medication_importer.rb +0 -4
  17. data/lib/health-data-standards/import/cda/procedure_importer.rb +23 -2
  18. data/lib/health-data-standards/import/cda/result_importer.rb +1 -0
  19. data/lib/health-data-standards/import/cda/section_importer.rb +2 -5
  20. data/lib/health-data-standards/import/green_c32/encounter_importer.rb +0 -1
  21. data/lib/health-data-standards/import/green_c32/medication_importer.rb +0 -1
  22. data/lib/health-data-standards/import/green_c32/section_importer.rb +1 -5
  23. data/lib/health-data-standards/import/provider_import_utils.rb +15 -1
  24. data/lib/health-data-standards/models/cqm/aggregate_objects.rb +63 -20
  25. data/lib/health-data-standards/models/cqm/query_cache.rb +3 -26
  26. data/lib/health-data-standards/models/encounter.rb +3 -2
  27. data/lib/health-data-standards/models/entry.rb +8 -1
  28. data/lib/health-data-standards/models/insurance_provider.rb +0 -4
  29. data/lib/health-data-standards/models/provider.rb +10 -0
  30. data/lib/health-data-standards/models/record.rb +31 -8
  31. data/lib/health-data-standards/models/transfer.rb +8 -0
  32. data/templates/cat1/_2.16.840.1.113883.10.20.22.4.85.cat1.erb +1 -0
  33. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.101.cat1.erb +6 -3
  34. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.103.cat1.erb +1 -0
  35. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.12.cat1.erb +1 -0
  36. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.13.cat1.erb +1 -0
  37. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.14.cat1.erb +1 -0
  38. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.2.cat1.erb +1 -3
  39. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.20.cat1.erb +6 -2
  40. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.22.cat1.erb +1 -1
  41. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.23.cat1.erb +21 -0
  42. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.28.cat1.erb +6 -2
  43. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.31.cat1.erb +1 -0
  44. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.32.cat1.erb +1 -0
  45. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.34.cat1.erb +1 -0
  46. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.40.cat1.erb +6 -2
  47. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.46.cat1.erb +1 -0
  48. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.54.cat1.erb +1 -0
  49. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.57.cat1.erb +7 -2
  50. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.59.cat1.erb +5 -1
  51. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.66.cat1.erb +3 -3
  52. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.69.cat1.erb +4 -2
  53. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.7.cat1.erb +1 -1
  54. data/templates/cat3/_continuous_variable_value.cat3.erb +2 -2
  55. data/templates/cat3/_measure_data.cat3.erb +19 -19
  56. data/templates/cat3/_performance_rate.cat3.erb +2 -2
  57. data/templates/cat3/show.cat3.erb +7 -5
  58. data/templates/gc32/_advance_directive.gc32.erb +1 -1
  59. data/templates/gc32/_entry_attributes.gc32.erb +1 -1
  60. data/templates/gc32/_insurance_provider.gc32.erb +2 -2
  61. metadata +10 -29
@@ -13,6 +13,7 @@ module HealthDataStandards
13
13
  extract_interpretation(entry_element, result)
14
14
  extract_reference_range(entry_element, result)
15
15
  extract_negation(entry_element, result)
16
+ extract_reason_description(entry_element, result, nrh)
16
17
  result
17
18
  end
18
19
 
@@ -47,13 +47,10 @@ module HealthDataStandards
47
47
  if @value_xpath
48
48
  extract_value(entry_element, entry)
49
49
  end
50
- entry.free_text = entry_element.at_xpath("./cda:text").try("text")
50
+ entry.description = entry_element.at_xpath("./cda:text").try("text")
51
51
  if @status_xpath
52
52
  extract_status(entry_element, entry)
53
53
  end
54
- if @description_xpath
55
- extract_description(entry_element, entry, nrh)
56
- end
57
54
  entry
58
55
  end
59
56
 
@@ -76,7 +73,7 @@ module HealthDataStandards
76
73
  end
77
74
  end
78
75
 
79
- def extract_description(parent_element, entry, nrh)
76
+ def extract_reason_description(parent_element, entry, nrh)
80
77
  code_elements = parent_element.xpath(@description_xpath)
81
78
  code_elements.each do |code_element|
82
79
  tag = code_element['value']
@@ -17,7 +17,6 @@ module HealthDataStandards
17
17
  extract_code(encounter_element, encounter, "./gc32:admissionType", :admit_type)
18
18
  extract_code(encounter_element, encounter, "./gc32:reasonForVisit", :reason)
19
19
  extract_code(encounter_element, encounter)
20
- extract_free_text(encounter_element, encounter)
21
20
 
22
21
  facility_element = encounter_element.at_xpath("./gc32:facility")
23
22
  if facility_element
@@ -22,7 +22,6 @@ module HealthDataStandards
22
22
  extract_med_code_attribute(med_element, medication, :vehicle)
23
23
  extract_med_code_attribute(med_element, medication, :reaction)
24
24
  extract_med_code_attribute(med_element, medication, :deliveryMethod)
25
- extract_free_text(med_element, medication, "freeTextSig")
26
25
  medication.fulfillment_instructions = extract_node_text(med_element.at_xpath("./gc32:patientInstructions"))
27
26
  medication.dose_indicator = extract_node_text(med_element.at_xpath("./gc32:doseIndicator"))
28
27
  medication.fulfillment_history = extract_fulfillment_history(med_element)
@@ -105,7 +105,7 @@ module HealthDataStandards
105
105
  extract_status(element, entry)
106
106
  extract_value(element, entry)
107
107
  extract_effective_time(element, entry)
108
- entry.free_text = element.at_xpath("./gc32:freeText").try(:inner_text)
108
+ entry.description = element.at_xpath("./gc32:freeText").try(:inner_text)
109
109
  entry
110
110
  end
111
111
 
@@ -159,10 +159,6 @@ module HealthDataStandards
159
159
  telecom.preferred = extract_node_attribute(telecom_element, :preferred)
160
160
  telecom
161
161
  end
162
-
163
- def extract_free_text(element, entry, free_text_element="freeText")
164
- entry.free_text = extract_node_text(element.at_xpath("./gc32:#{free_text_element}"))
165
- end
166
162
 
167
163
  private
168
164
 
@@ -7,7 +7,21 @@ module ProviderImportUtils
7
7
 
8
8
  def find_or_create_provider(provider_hash)
9
9
  provider = Provider.where(npi: provider_hash[:npi]).first if provider_hash[:npi] && !provider_hash[:npi].empty?
10
- provider ||= Provider.create(provider_hash)
10
+ unless provider
11
+ if provider_hash[:npi]
12
+ provider = Provider.create(provider_hash)
13
+ else
14
+ provider ||= Provider.resolve_provider(provider_hash) if Provider.respond_to? :resolve_provider
15
+
16
+ provider_query = {:title => provider_hash[:title],
17
+ :given_name => provider_hash[:given_name],
18
+ :family_name=> provider_hash[:family_name],
19
+ :specialty => provider_hash[:specialty]}
20
+ provider ||= Provider.where(provider_query).first
21
+ provider ||= Provider.create(provider_hash)
22
+ end
23
+ end
24
+ provider
11
25
  end
12
26
 
13
27
  # Returns nil if result is an empty string, block allows text munging of result if there is one
@@ -1,6 +1,5 @@
1
1
  module HealthDataStandards
2
2
  module CQM
3
-
4
3
  module PopulationSelectors
5
4
  def numerator
6
5
  populations.find {|pop| pop.type == 'NUMER'}
@@ -51,42 +50,86 @@ module HealthDataStandards
51
50
  population_groups.values.any? { |pops| pops.size > 1 }
52
51
  end
53
52
  end
54
-
55
53
  class Population
56
- attr_accessor :type, :value, :id
54
+ attr_accessor :type, :value, :id, :stratifications, :supplemental_data
55
+
56
+ def initialize
57
+ @stratifications = []
58
+ end
59
+
60
+ def add_stratification(id,value)
61
+ unless stratifications.find{|st| st.id == id}
62
+ stratifications << Stratification.new(id,value)
63
+ end
64
+ end
65
+
57
66
  end
58
67
 
59
68
  class Stratification
60
- attr_accessor :id, :populations
69
+ attr_accessor :id, :value
70
+ def initialize(id,value)
71
+ @id = id
72
+ @value = value
73
+ end
74
+
75
+ end
76
+
77
+ class PopulationGroup
61
78
  include PopulationSelectors
79
+ attr_accessor :populations
80
+ def performance_rate
81
+ numerator_count.to_f /
82
+ (denominator_count - denominator_exclusions_count - denominator_exceptions_count)
83
+ end
62
84
 
63
- def initialize
64
- @populations = []
85
+ def is_cv?
86
+ populations.any? {|pop| pop.type == 'MSRPOPL'}
65
87
  end
88
+
66
89
  end
67
90
 
68
91
  class AggregateCount
69
- attr_accessor :measure_id, :stratifications, :top_level_populations, :supplemental_data
70
- alias :populations :top_level_populations
71
- include PopulationSelectors
92
+ attr_accessor :measure_id, :populations, :population_groups
72
93
 
73
- def initialize
74
- @stratifications = []
75
- @top_level_populations = []
94
+ def initialize(measure_id)
95
+ @populations = []
96
+ @measure_id = measure_id
97
+ @population_groups = []
76
98
  end
77
99
 
78
- def is_cv?
79
- top_level_populations.any? {|pop| pop.type == 'MSRPOPL'}
100
+ def add_entry(cache_entry)
101
+ entry_populations = []
102
+ cache_entry.population_ids.each do |population_type, population_id|
103
+ population = populations.find{|pop| pop.id == population_id}
104
+ if population.nil? && population_type != 'stratification'
105
+ population = Population.new
106
+ population.type = population_type
107
+ population.id = population_id
108
+ populations << population
109
+ end
110
+ unless population_type == 'stratification'
111
+ if cache_entry.is_stratification?
112
+ strat_id = cache_entry.population_ids['stratification']
113
+ population.add_stratification(strat_id,cache_entry[population_type])
114
+ else
115
+ population.value = cache_entry[population_type]
116
+ population.supplemental_data = cache_entry.supplemental_data[population_type]
117
+ end
118
+ end
119
+ entry_populations << population if population
120
+ end
121
+ pgroup = population_groups.find{|pg| pg.populations.collect{|p| p.id}.sort == entry_populations.collect{|p| p.id}.sort }
122
+ unless pgroup
123
+ pg = PopulationGroup.new
124
+ pg.populations = entry_populations
125
+ population_groups << pg
126
+ end
80
127
  end
81
128
 
82
- def performance_rate
83
- numerator_count.to_f /
84
- (denominator_count - denominator_exclusions_count - denominator_exceptions_count)
129
+ def is_cv?
130
+ populations.any? {|pop| pop.type == 'MSRPOPL'}
85
131
  end
86
132
 
87
- def supplemental_data_for(population_type, supplemental_data_type)
88
- supplemental_data[population_type][supplemental_data_type]
89
- end
90
133
  end
91
134
  end
92
135
  end
@@ -23,20 +23,9 @@ module HealthDataStandards
23
23
 
24
24
  def self.aggregate_measure(measure_id, effective_date, filter=nil, test_id=nil)
25
25
  cache_entries = self.where(effective_date: effective_date, measure_id: measure_id, test_id: test_id, filter: filter)
26
- aggregate_count = AggregateCount.new
27
- aggregate_count.measure_id = measure_id
26
+ aggregate_count = AggregateCount.new(measure_id)
28
27
  cache_entries.each do |cache_entry|
29
- if cache_entry.is_stratification?
30
- stratification = Stratification.new
31
- stratification.populations = cache_entry.build_populations
32
- stratification.id = cache_entry.population_ids['stratification']
33
- aggregate_count.stratifications << stratification
34
- else
35
- aggregate_count.top_level_populations = cache_entry.build_populations
36
- if cache_entry.supplemental_data.present?
37
- aggregate_count.supplemental_data = cache_entry.supplemental_data
38
- end
39
- end
28
+ aggregate_count.add_entry(cache_entry)
40
29
  end
41
30
  aggregate_count
42
31
  end
@@ -49,19 +38,7 @@ module HealthDataStandards
49
38
  population_ids.has_key?('MSRPOPL')
50
39
  end
51
40
 
52
- def build_populations
53
- populations = []
54
- population_ids.each do |population_type, population_id|
55
- unless population_type == 'stratification'
56
- population = Population.new
57
- population.type = population_type
58
- population.id = population_id
59
- population.value = self[population_type]
60
- populations << population
61
- end
62
- end
63
- populations
64
- end
41
+
65
42
  end
66
43
  end
67
44
  end
@@ -3,8 +3,9 @@ class Encounter < Entry
3
3
  field :dischargeDisposition, type: Hash
4
4
  field :admitTime, type: Integer
5
5
  field :dischargeTime, type: Integer
6
- field :transferTo, type: Hash
7
- field :transferFrom, type: Hash
6
+
7
+ embeds_one :transferTo, class_name: "Transfer"
8
+ embeds_one :transferFrom, class_name: "Transfer"
8
9
 
9
10
  embeds_one :facility
10
11
  embeds_one :reason, class_name: "Entry"
@@ -15,7 +15,6 @@ class Entry
15
15
  field :start_time, type: Integer
16
16
  field :end_time, type: Integer
17
17
  field :status_code, type: Hash
18
- field :free_text, type: String
19
18
  field :mood_code, type: String, default: "EVN"
20
19
  field :negationInd, type: Boolean
21
20
  field :negationReason, type: Hash
@@ -206,4 +205,12 @@ class Entry
206
205
  self.time = self.time.nil? ? nil : (self.time + date_diff)
207
206
  end
208
207
 
208
+ def identifier
209
+ if respond_to?(:cda_identifier) && self.cda_identifier.present?
210
+ self.cda_identifier
211
+ else
212
+ self.id
213
+ end
214
+ end
215
+
209
216
  end
@@ -8,14 +8,10 @@ class InsuranceProvider < Entry
8
8
  embeds_one :subscriber, class_name: "Person"
9
9
 
10
10
  field :type, type: String
11
- field :time, type: Integer
12
- field :start_time, type: Integer
13
- field :end_time, type: Integer
14
11
  field :member_id, type: String
15
12
  field :relationship, type: Hash
16
13
  field :financial_responsibility_type, type: Hash
17
14
  field :name, type: String
18
- field :free_text, type: String
19
15
 
20
16
 
21
17
  def shift_dates(date_diff)
@@ -47,4 +47,14 @@ class Provider
47
47
 
48
48
  return sum.to_s
49
49
  end
50
+
51
+ #this is intentially left blank. When using the ProviderImporter class this method will be called
52
+ # if a parsed provider can not be found in the database if the parsed provider does not have an
53
+ # npi number associated with it. This allows applications to handle this how they see fit by redefining
54
+ # this method. If this method call return nil an attempt will be made to discover the Provider by name
55
+ # matching and if that fails a Provider will be created in the db based on the information in the parsed
56
+ # hase
57
+ def self.resolve_provider(provider_hash)
58
+
59
+ end
50
60
  end
@@ -54,6 +54,17 @@ class Record
54
54
  scope :by_provider, ->(prov, effective_date) { (effective_date) ? where(provider_queries(prov.id, effective_date)) : where('provider_performances.provider_id'=>prov.id) }
55
55
  scope :by_patient_id, ->(id) { where(:medical_record_number => id) }
56
56
 
57
+ def self.update_or_create(data)
58
+ existing = Record.where(medical_record_number: data.medical_record_number).first
59
+ if existing
60
+ existing.update_attributes!(data.attributes.except('_id'))
61
+ existing
62
+ else
63
+ data.save!
64
+ data
65
+ end
66
+ end
67
+
57
68
  def providers
58
69
  provider_performances.map {|pp| pp.provider }
59
70
  end
@@ -79,20 +90,32 @@ class Record
79
90
  alias :clinical_trial_participant :clinicalTrialParticipant
80
91
  alias :clinical_trial_participant= :clinicalTrialParticipant=
81
92
 
82
- # Removed duplicate entries from a section based on id. This method may
83
- # lose information because it does not compare entries based on clinical
84
- # content
85
- def dedup_section!(section)
93
+
94
+ # Remove duplicate entries from a section based on cda_identifier or id.
95
+ # This method may lose information because it does not compare entries
96
+ # based on clinical content
97
+ def dedup_section_ignoring_content!(section)
86
98
  unique_entries = self.send(section).uniq do |entry|
87
- if entry.respond_to?(:cda_identifier) && entry.cda_identifier.present?
88
- entry.cda_identifier
99
+ entry.identifier
100
+ end
101
+ self.send("#{section}=", unique_entries)
102
+ end
103
+ def dedup_section_merging_codes_and_values!(section)
104
+ unique_entries = {}
105
+ self.send(section).each do |entry|
106
+ if unique_entries[entry.identifier]
107
+ unique_entries[entry.identifier].codes = unique_entries[entry.identifier].codes.deep_merge(entry.codes)
108
+ unique_entries[entry.identifier].values.concat(entry.values)
89
109
  else
90
- entry.id
110
+ unique_entries[entry.identifier] = entry
91
111
  end
92
112
  end
93
- self.send("#{section}=", unique_entries)
113
+ self.send("#{section}=", unique_entries.values)
94
114
  end
95
115
 
116
+ def dedup_section!(section)
117
+ [:results, :procedures].include?(section) ? dedup_section_merging_codes_and_values!(section) : dedup_section_ignoring_content!(section)
118
+ end
96
119
  def dedup_record!
97
120
  Record::Sections.each {|section| self.dedup_section!(section)}
98
121
  end
@@ -0,0 +1,8 @@
1
+ class Transfer
2
+ include Mongoid::Document
3
+ include ThingWithCodes
4
+
5
+ field :time, type: Integer
6
+
7
+ embedded_in :encounter, class_name: "Encounter"
8
+ end
@@ -15,5 +15,6 @@
15
15
  'tag_name' => 'value',
16
16
  'value_set_map' => value_set_map,
17
17
  'extra_content' => "xsi:type=\"CD\" sdtc:valueSet=\"#{value_set_oid}\"") %>
18
+ <text><%= entry.description %></text>
18
19
  </observation>
19
20
  </entry>
@@ -13,13 +13,16 @@
13
13
  <value xsi:type="PQ"
14
14
  <% if entry.codes['SNOMED-CT'].include?('80487005')-%>
15
15
  value="39"
16
- <% elsif entry.codes['SNOMED-CT'].include?('931004')-%>
17
- value="36"
16
+ <% elsif entry.codes['SNOMED-CT'].include?('13798002')-%>
17
+ value="38"
18
18
  <% elsif entry.codes['SNOMED-CT'].include?('43697006')-%>
19
19
  value="37"
20
+ <% elsif entry.codes['SNOMED-CT'].include?('931004')-%>
21
+ value="36"
20
22
  <% else -%>
21
23
  nullFlavor="UNK"
22
24
  <% end -%>
23
- unit="wk"/>
25
+ unit="wk"/>
26
+ <text><%= entry.description %></text>
24
27
  </observation>
25
28
  </entry>
@@ -8,5 +8,6 @@
8
8
  <%== code_display(entry, 'preferred_code_sets' => ['SNOMED-CT'],
9
9
  'tag_name' => 'value','value_set_map' => value_set_map,
10
10
  'extra_content' => "xsi:type=\"CD\" sdtc:valueSet=\"#{value_set_oid}\"") %>
11
+ <text><%= entry.description %></text>
11
12
  </observation>
12
13
  </entry>
@@ -43,6 +43,7 @@
43
43
  <%== code_display(entry, 'value_set_map' => value_set_map,'preferred_code_sets' => ['SNOMED-CT'],
44
44
  'tag_name' => 'value',
45
45
  'extra_content' => "xsi:type=\"CD\" sdtc:valueSet=\"#{value_set_oid}\"") %>
46
+ <text><%= entry.description %></text>
46
47
  </observation>
47
48
  </component>
48
49
  </organizer>
@@ -18,6 +18,7 @@
18
18
  <%== render(:partial => 'ordinality', :locals => {:entry => entry, :ordinality_oids=>field_oids["ORDINAL"]}) %>
19
19
 
20
20
  <%== code_display(entry, 'value_set_map' => value_set_map,'tag_name' => 'value', 'preferred_code_sets' => ['SNOMED-CT', 'ICD-9-CM', 'ICD-10-CM', 'LOINC'], 'extra_content' => "xsi:type=\"CD\" sdtc:valueSet=\"#{value_set_oid}\"") %>
21
+ <text><%= entry.description %></text>
21
22
  <!-- Status -->
22
23
  <entryRelationship typeCode="REFR">
23
24
  <observation classCode="OBS" moodCode="EVN">
@@ -18,6 +18,7 @@
18
18
 
19
19
  <%== code_display(entry, 'value_set_map' => value_set_map,'tag_name' => 'value', 'preferred_code_sets' => ['SNOMED-CT', 'ICD-9-CM', 'ICD-10-CM', 'CPT'],
20
20
  'extra_content' => "xsi:type=\"CD\" sdtc:valueSet=\"#{value_set_oid}\"") %>
21
+ <text><%= entry.description %></text>
21
22
  <!-- Status -->
22
23
  <entryRelationship typeCode="REFR">
23
24
  <observation classCode="OBS" moodCode="EVN">