health-data-standards 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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">