his_emr_api_lab 1.0.4 → 1.1.2

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.
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket.io-client-simple'
4
+
5
+ module Lab
6
+ module Lims
7
+ module Api
8
+ ##
9
+ # Retrieve results from LIMS only through a websocket
10
+ class WsApi
11
+ def initialize(config)
12
+ @config = config
13
+ @results_queue = []
14
+ @socket = nil
15
+ end
16
+
17
+ def consume_orders(**_kwargs)
18
+ loop do
19
+ results = fetch_results
20
+ unless results
21
+ Rails.logger.debug('No results available... Waiting for results...')
22
+ sleep(Lab::Lims::Config.updates_poll_frequency)
23
+ next
24
+ end
25
+
26
+ Rails.logger.info("Received result for ##{results['tracking_number']}")
27
+ order = find_order(results['tracking_number'])
28
+ next unless order
29
+
30
+ Rails.logger.info("Updating result for order ##{order.order_id}")
31
+ yield make_order_dto(order, results), OpenStruct.new(last_seq: 1)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def initialize_socket
38
+ Rails.logger.debug('Establishing connection to socket...')
39
+ socket = SocketIO::Client::Simple.connect(socket_url)
40
+ socket.on(:connect, &method(:on_socket_connect))
41
+ socket.on(:disconnect, &method(:on_socket_disconnect))
42
+ socket.on(:results, &method(:on_results_received))
43
+ end
44
+
45
+ def socket_url
46
+ @config.fetch('url')
47
+ end
48
+
49
+ def on_socket_connect
50
+ Rails.logger.debug('Connection to LIMS results socket established...')
51
+ end
52
+
53
+ def on_socket_disconnect
54
+ Rails.logger.debug('Connection to LIMS results socket lost...')
55
+ @socket = nil
56
+ end
57
+
58
+ def on_results_received(result)
59
+ Rails.logger.debug("Received result from LIMS: #{result}")
60
+ tracking_number = result['tracking_number']
61
+
62
+ Rails.logger.debug("Queueing result for order ##{tracking_number}")
63
+ @results_queue.push(result)
64
+ end
65
+
66
+ def order_exists?(tracking_number)
67
+ Rails.logger.debug("Looking for order for result ##{tracking_number}")
68
+ orders = OrdersSearchService.find_orders_without_results
69
+ .where(accession_number: tracking_number)
70
+ # The following ensures that the order was previously pushed to LIMS
71
+ # or was received from LIMS
72
+ Lab::LimsOrderMapping.where.not(order: orders).exists?
73
+ end
74
+
75
+ def fetch_results
76
+ loop do
77
+ @socket ||= initialize_socket
78
+
79
+ results = @results_queue.shift
80
+ return nil unless results
81
+
82
+ unless order_exists?(results['tracking_number'])
83
+ Rails.logger.debug("Ignoring result for order ##{tracking_number}")
84
+ next
85
+ end
86
+
87
+ return results
88
+ end
89
+ end
90
+
91
+ def find_order(lims_id)
92
+ mapping = Lab::LimsOrderMapping.where(lims_id: lims_id).select(:order_id)
93
+ Lab::LabOrder.find_by(order_id: mapping)
94
+ end
95
+
96
+ def make_order_dto(order, results)
97
+ Lab::Lims::OrderSerializer
98
+ .serialize_order(order)
99
+ .merge(
100
+ id: order.accession_number,
101
+ test_results: {
102
+ results['test_name'] => {
103
+ results: results['results'].each_with_object({}) do |measure, formatted_measures|
104
+ measure_name, measure_value = measure
105
+
106
+ formatted_measures[measure_name] = { result_value: measure_value }
107
+ end,
108
+ result_date: results['date_updated'],
109
+ result_entered_by: {
110
+ first_name: results['who_updated']['first_name'],
111
+ last_name: results['who_updated']['last_name'],
112
+ id: results['who_updated']['id_number']
113
+ }
114
+ }
115
+ }
116
+ )
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -5,6 +5,9 @@ module Lab
5
5
  ##
6
6
  # Load LIMS' configuration files
7
7
  module Config
8
+ # TODO: Replace this maybe with `Rails.application.configuration.lab.lims`
9
+ # so that we do not have to directly mess with configuration files.
10
+
8
11
  class ConfigNotFound < RuntimeError; end
9
12
 
10
13
  class << self
@@ -12,31 +15,63 @@ module Lab
12
15
  # Returns LIMS' couchdb configuration file for the current environment (Rails.env)
13
16
  def couchdb
14
17
  config_path = begin
15
- find_config_path('couchdb.yml')
16
- rescue ConfigNotFound => e
17
- Rails.logger.error("Failed to find default LIMS couchdb config: #{e.message}")
18
- find_config_path('couchdb-lims.yml') # This can be placed in HIS-EMR-API/config
19
- end
18
+ find_config_path('couchdb.yml')
19
+ rescue ConfigNotFound => e
20
+ Rails.logger.error("Failed to find default LIMS couchdb config: #{e.message}")
21
+ find_config_path('couchdb-lims.yml') # This can be placed in HIS-EMR-API/config
22
+ end
20
23
 
21
24
  Rails.logger.debug("Using LIMS couchdb config: #{config_path}")
22
25
 
23
26
  YAML.load_file(config_path)[Rails.env]
24
27
  end
25
28
 
29
+ def rest_api
30
+ @rest_api ||= {
31
+ protocol: emr_api_application('lims_protocol', 'http'),
32
+ host: emr_api_application('lims_host'),
33
+ port: emr_api_application('lims_port'),
34
+ username: emr_api_application('lims_username'),
35
+ password: emr_api_application('lims_password')
36
+ }
37
+ end
38
+
39
+ def updates_socket
40
+ @updates_socket ||= {
41
+ 'url' => emr_api_application('lims_realtime_updates_url')
42
+ }
43
+ end
44
+
45
+ def updates_poll_frequency
46
+ 30 # Seconds
47
+ end
48
+
26
49
  ##
27
50
  # Returns LIMS' application.yml configuration file
28
51
  def application
29
- YAML.load_file(find_config_path('application.yml'))
52
+ @application ||= YAML.load_file(find_config_path('application.yml'))
30
53
  end
31
54
 
32
55
  ##
33
56
  # Returns LIMS' database.yml configuration file
34
57
  def database
35
- YAML.load_file(find_config_path('database.yml'))
58
+ @database ||= YAML.load_file(find_config_path('database.yml'))
36
59
  end
37
60
 
38
61
  private
39
62
 
63
+ def emr_api_application(param, fallback = nil)
64
+ @emr_api_application ||= YAML.load_file(Rails.root.join('config', 'application.yml'))
65
+
66
+ @emr_api_application.fetch(param) do
67
+ unless fallback
68
+ raise ConfigNotFound, "Missing config param: #{param}"
69
+ end
70
+
71
+ fallback
72
+ end
73
+ end
74
+
40
75
  ##
41
76
  # Looks for a config file in various LIMS installation directories
42
77
  #
@@ -48,7 +83,9 @@ module Lab
48
83
  Rails.root.parent.join("nlims_controller/config/#{filename}")
49
84
  ]
50
85
 
51
- paths = [Rails.root.join('config/lims-couchdb.yml'), *paths] if filename == 'couchdb.yml'
86
+ if filename == 'couchdb.yml'
87
+ paths = [Rails.root.join('config/lims-couchdb.yml'), *paths]
88
+ end
52
89
 
53
90
  paths.each do |path|
54
91
  Rails.logger.debug("Looking for LIMS couchdb config at: #{path}")
@@ -29,8 +29,11 @@ require 'lab/lab_test'
29
29
  require 'lab/lims_order_mapping'
30
30
  require 'lab/lims_failed_import'
31
31
 
32
+ require_relative './api/couchdb_api'
32
33
  require_relative './config'
33
- require_relative './worker'
34
+ require_relative './pull_worker'
35
+ require_relative './utils'
36
+
34
37
  require_relative '../orders_service'
35
38
  require_relative '../results_service'
36
39
  require_relative '../tests_service'
@@ -46,7 +49,7 @@ module Lab
46
49
  module Migrator
47
50
  MAX_THREADS = ENV.fetch('MIGRATION_WORKERS', 6).to_i
48
51
 
49
- class CouchDbMigratorApi < Api::CouchDbApi
52
+ class CouchDbMigratorApi < Lab::Lims::Api::CouchDbApi
50
53
  def initialize(*args, processes: 1, on_merge_processes: nil, **kwargs)
51
54
  super(*args, **kwargs)
52
55
 
@@ -88,8 +91,8 @@ module Lab
88
91
  end
89
92
  end
90
93
 
91
- class MigrationWorker < Worker
92
- LOG_FILE_PATH = LIMS_LOG_PATH.join('migration-last-id.dat')
94
+ class MigrationWorker < PullWorker
95
+ LOG_FILE_PATH = Utils::LIMS_LOG_PATH.join('migration-last-id.dat')
93
96
 
94
97
  attr_reader :rejections
95
98
 
@@ -132,7 +135,8 @@ module Lab
132
135
  end
133
136
  end
134
137
 
135
- MIGRATION_REJECTIONS_CSV_PATH = LIMS_LOG_PATH.join('migration-rejections.csv')
138
+ # NOTE: LIMS_LOG_PATH below is defined in worker.rb
139
+ MIGRATION_REJECTIONS_CSV_PATH = Utils::LIMS_LOG_PATH.join('migration-rejections.csv')
136
140
 
137
141
  def self.export_rejections(rejections)
138
142
  headers = ['doc_id', 'Accession number', 'NHID', 'First name', 'Last name', 'Reason']
@@ -150,7 +154,7 @@ module Lab
150
154
  save_csv(MIGRATION_REJECTIONS_CSV_PATH, headers: headers, rows: rows)
151
155
  end
152
156
 
153
- MIGRATION_FAILURES_CSV_PATH = LIMS_LOG_PATH.join('migration-failures.csv')
157
+ MIGRATION_FAILURES_CSV_PATH = Utils::LIMS_LOG_PATH.join('migration-failures.csv')
154
158
 
155
159
  def self.export_failures
156
160
  headers = ['doc_id', 'Accession number', 'NHID', 'Reason', 'Difference']
@@ -167,10 +171,10 @@ module Lab
167
171
  save_csv(MIGRATION_FAILURES_CSV_PATH, headers: headers, rows: rows)
168
172
  end
169
173
 
170
- MIGRATION_LOG_PATH = LIMS_LOG_PATH.join('migration.log')
174
+ MIGRATION_LOG_PATH = Utils::LIMS_LOG_PATH.join('migration.log')
171
175
 
172
176
  def self.start_migration
173
- Dir.mkdir(LIMS_LOG_PATH) unless File.exist?(LIMS_LOG_PATH)
177
+ Dir.mkdir(Utils::LIMS_LOG_PATH) unless File.exist?(Utils::LIMS_LOG_PATH)
174
178
 
175
179
  logger = LoggerMultiplexor.new(Logger.new($stdout), MIGRATION_LOG_PATH)
176
180
  logger.level = :debug
@@ -185,7 +189,6 @@ module Lab
185
189
  end
186
190
 
187
191
  worker = MigrationWorker.new(api_class)
188
-
189
192
  worker.pull_orders(batch_size: 10_000)
190
193
  ensure
191
194
  worker && export_rejections(worker.rejections)
@@ -22,7 +22,7 @@ module Lab
22
22
  date: start_date,
23
23
  target_lab: facility_name(self['receiving_facility']),
24
24
  order_location: facility_name(self['sending_facility']),
25
- reason_for_test: reason_for_test
25
+ reason_for_test_id: reason_for_test
26
26
  )
27
27
  end
28
28
 
@@ -69,7 +69,9 @@ module Lab
69
69
  end
70
70
 
71
71
  def start_date
72
- raise LimsException, 'Order missing created date' if self['date_created'].blank?
72
+ if self['date_created'].blank?
73
+ raise LimsException, 'Order missing created date'
74
+ end
73
75
 
74
76
  Utils.parse_date(self['date_created'])
75
77
  end
@@ -85,7 +87,12 @@ module Lab
85
87
  def reason_for_test
86
88
  return unknown_concept.concept_id unless self['priority']
87
89
 
88
- ConceptName.find_by_name!(self['priority']).concept_id
90
+ name = case self['priority']
91
+ when %r{Reapet / Missing}i then 'Repeat / Missing'
92
+ else self['priority']
93
+ end
94
+
95
+ ConceptName.find_by_name!(name).concept_id
89
96
  end
90
97
 
91
98
  def lab_program
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './config'
3
4
  require_relative './order_dto'
4
5
  require_relative './utils'
5
6
 
@@ -12,9 +13,10 @@ module Lab
12
13
  include Utils
13
14
 
14
15
  def serialize_order(order)
15
- serialized_order = Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
16
+ serialized_order = Lims::Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
16
17
 
17
- OrderDTO.new(
18
+ Lims::OrderDTO.new(
19
+ _id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
18
20
  tracking_number: serialized_order.accession_number,
19
21
  sending_facility: current_facility_name,
20
22
  receiving_facility: serialized_order.target_lab,
@@ -69,7 +71,11 @@ module Lab
69
71
  end
70
72
 
71
73
  def format_sample_type(name)
72
- name.casecmp?('Unknown') ? 'not_specified' : name.titleize
74
+ return 'not_specified' if name.casecmp?('Unknown')
75
+
76
+ return 'CSF' if name.casecmp?('Cerebrospinal Fluid')
77
+
78
+ name.titleize
73
79
  end
74
80
 
75
81
  def format_sample_status(name)
@@ -77,7 +83,9 @@ module Lab
77
83
  end
78
84
 
79
85
  def format_sample_status_trail(order)
80
- return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
86
+ if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
87
+ return []
88
+ end
81
89
 
82
90
  user = User.find(order.discontinued_by || order.creator)
83
91
  drawn_by = PersonName.find_by_person_id(user.user_id)
@@ -97,7 +105,9 @@ module Lab
97
105
  end
98
106
 
99
107
  def format_test_status_trail(order)
100
- order.tests.each_with_object({}) do |test, trail|
108
+ tests = order.voided.zero? ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
109
+
110
+ tests.each_with_object({}) do |test, trail|
101
111
  test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
102
112
 
103
113
  current_test_trail = trail[test_name] = {}
@@ -107,6 +117,13 @@ module Lab
107
117
  updated_by: find_user(test.creator)
108
118
  }
109
119
 
120
+ unless test.voided.zero?
121
+ current_test_trail[test.date_voided.strftime('%Y%m%d%H%M%S')] = {
122
+ status: 'Voided',
123
+ updated_by: find_user(test.voided_by)
124
+ }
125
+ end
126
+
110
127
  next unless test.result
111
128
 
112
129
  current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
@@ -122,7 +139,10 @@ module Lab
122
139
 
123
140
  def format_test_results(order)
124
141
  order.tests&.each_with_object({}) do |test, results|
125
- next unless test.result
142
+ next if test.result.nil? || test.result.empty?
143
+
144
+ test_creator = User.find(Observation.find(test.result.first.id).creator)
145
+ test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
126
146
 
127
147
  results[format_test_name(test.name)] = {
128
148
  results: test.result.each_with_object({}) do |measure, measures|
@@ -131,7 +151,11 @@ module Lab
131
151
  }
132
152
  end,
133
153
  result_date: test.result.first&.date,
134
- result_entered_by: {}
154
+ result_entered_by: {
155
+ first_name: test_creator_name&.given_name,
156
+ last_name: test_creator_name&.family_name,
157
+ id: test_creator.username
158
+ }
135
159
  }
136
160
  end
137
161
  end
@@ -165,7 +189,7 @@ module Lab
165
189
  return district if district
166
190
 
167
191
  GlobalProperty.create(property: 'current_health_center_district',
168
- property_value: Config.application['district'],
192
+ property_value: Lims::Config.application['district'],
169
193
  uuid: SecureRandom.uuid)
170
194
 
171
195
  Config.application['district']
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ class PullWorker
6
+ attr_reader :lims_api
7
+
8
+ include Utils # for logger
9
+
10
+ LIMS_LOG_PATH = Rails.root.join('log', 'lims')
11
+
12
+ def initialize(lims_api)
13
+ @lims_api = lims_api
14
+ end
15
+
16
+ ##
17
+ # Pulls orders from the LIMS queue and writes them to the local database
18
+ def pull_orders(batch_size: 10_000, **kwargs)
19
+ logger.info("Retrieving LIMS orders starting from #{last_seq}")
20
+
21
+ lims_api.consume_orders(from: last_seq, limit: batch_size, **kwargs) do |order_dto, context|
22
+ logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
23
+
24
+ patient = find_patient_by_nhid(order_dto[:patient][:id])
25
+ unless patient
26
+ logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
27
+ order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
28
+ next
29
+ end
30
+
31
+ if order_dto[:tests].empty?
32
+ logger.debug("Discarding order: Missing tests on order ##{order_dto[:tracking_number]}")
33
+ order_rejected(order_dto, 'Order is missing tests')
34
+ next
35
+ end
36
+
37
+ diff = match_patient_demographics(patient, order_dto['patient'])
38
+ if diff.empty?
39
+ save_order(patient, order_dto)
40
+ order_saved(order_dto)
41
+ else
42
+ save_failed_import(order_dto, 'Demographics not matching', diff)
43
+ end
44
+
45
+ update_last_seq(context.current_seq)
46
+ rescue DuplicateNHID
47
+ logger.warn("Failed to import order due to duplicate patient NHID: #{order_dto[:patient][:id]}")
48
+ save_failed_import(order_dto, "Duplicate local patient NHID: #{order_dto[:patient][:id]}")
49
+ rescue MissingAccessionNumber
50
+ logger.warn("Failed to import order due to missing accession number: #{order_dto[:_id]}")
51
+ save_failed_import(order_dto, 'Order missing tracking number')
52
+ rescue LimsException => e
53
+ logger.warn("Failed to import order due to #{e.class} - #{e.message}")
54
+ save_failed_import(order_dto, e.message)
55
+ end
56
+ end
57
+
58
+ protected
59
+
60
+ def order_saved(order_dto); end
61
+
62
+ def order_rejected(order_dto, message); end
63
+
64
+ def last_seq
65
+ File.open(last_seq_path, File::RDONLY | File::CREAT, 0o644) do |fin|
66
+ data = fin.read&.strip
67
+ return nil if data.blank?
68
+
69
+ return data
70
+ end
71
+ end
72
+
73
+ def update_last_seq(last_seq)
74
+ File.open(last_seq_path, File::WRONLY | File::CREAT, 0o644) do |fout|
75
+ fout.flock(File::LOCK_EX)
76
+
77
+ fout.write(last_seq.to_s)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def find_patient_by_nhid(nhid)
84
+ national_id_type = PatientIdentifierType.where(name: ['National id', 'Old Identification Number'])
85
+ identifiers = PatientIdentifier.where(type: national_id_type, identifier: nhid)
86
+ .joins('INNER JOIN person ON person.person_id = patient_identifier.patient_id AND person.voided = 0')
87
+ if identifiers.count.zero?
88
+ identifiers = PatientIdentifier.unscoped
89
+ .where(voided: 1, type: national_id_type, identifier: nhid)
90
+ .joins('INNER JOIN person ON person.person_id = patient_identifier.patient_id AND person.voided = 0')
91
+ end
92
+
93
+ # Joining to person above to ensure that the person is not voided,
94
+ # it was noted at one site that there were some people that were voided
95
+ # upon merging but the patient and patient_identifier was not voided
96
+
97
+ return nil if identifiers.count.zero?
98
+
99
+ patients = Patient.where(patient_id: identifiers.select(:patient_id))
100
+ .distinct(:patient_id)
101
+ .all
102
+
103
+ if patients.size > 1
104
+ raise DuplicateNHID, "Duplicate National Health ID: #{nhid}"
105
+ end
106
+
107
+ patients.first
108
+ end
109
+
110
+ ##
111
+ # Matches a local patient's demographics to a LIMS patient's demographics
112
+ def match_patient_demographics(local_patient, lims_patient)
113
+ diff = {}
114
+ person = Person.find(local_patient.id)
115
+ person_name = PersonName.find_by_person_id(local_patient.id)
116
+
117
+ unless (person.gender.blank? && lims_patient['gender'].blank?)\
118
+ || person.gender&.first&.casecmp?(lims_patient['gender']&.first)
119
+ diff[:gender] = { local: person.gender, lims: lims_patient['gender'] }
120
+ end
121
+
122
+ unless names_match?(person_name&.given_name, lims_patient['first_name'])
123
+ diff[:given_name] = { local: person_name&.given_name, lims: lims_patient['first_name'] }
124
+ end
125
+
126
+ unless names_match?(person_name&.family_name, lims_patient['last_name'])
127
+ diff[:family_name] = { local: person_name&.family_name, lims: lims_patient['last_name'] }
128
+ end
129
+
130
+ diff
131
+ end
132
+
133
+ def names_match?(name1, name2)
134
+ name1 = name1&.gsub(/'/, '')&.strip
135
+ name2 = name2&.gsub(/'/, '')&.strip
136
+
137
+ return true if name1.blank? && name2.blank?
138
+
139
+ return false if name1.blank? || name2.blank?
140
+
141
+ name1.casecmp?(name2)
142
+ end
143
+
144
+ def save_order(patient, order_dto)
145
+ raise MissingAccessionNumber if order_dto[:tracking_number].blank?
146
+
147
+ logger.info("Importing LIMS order ##{order_dto[:tracking_number]}")
148
+ mapping = find_order_mapping_by_lims_id(order_dto[:_id])
149
+
150
+ ActiveRecord::Base.transaction do
151
+ if mapping
152
+ order = update_order(patient, mapping.order_id, order_dto)
153
+ mapping.update(pulled_at: Time.now)
154
+ else
155
+ order = create_order(patient, order_dto)
156
+ mapping = LimsOrderMapping.create(lims_id: order_dto[:_id],
157
+ order_id: order['id'],
158
+ pulled_at: Time.now,
159
+ revision: order_dto['_rev'])
160
+ end
161
+
162
+ order
163
+ end
164
+ end
165
+
166
+ def create_order(patient, order_dto)
167
+ logger.debug("Creating order ##{order_dto['_id']}")
168
+ order = OrdersService.order_test(order_dto.to_order_service_params(patient_id: patient.patient_id))
169
+ unless order_dto['test_results'].empty?
170
+ update_results(order, order_dto['test_results'])
171
+ end
172
+
173
+ order
174
+ end
175
+
176
+ def update_order(patient, order_id, order_dto)
177
+ logger.debug("Updating order ##{order_dto['_id']}")
178
+ order = OrdersService.update_order(order_id, order_dto.to_order_service_params(patient_id: patient.patient_id)
179
+ .merge(force_update: 'true'))
180
+ unless order_dto['test_results'].empty?
181
+ update_results(order, order_dto['test_results'])
182
+ end
183
+
184
+ order
185
+ end
186
+
187
+ def update_results(order, lims_results)
188
+ logger.debug("Updating results for order ##{order[:accession_number]}: #{lims_results}")
189
+
190
+ lims_results.each do |test_name, test_results|
191
+ test = find_test(order['id'], test_name)
192
+ unless test
193
+ logger.warn("Couldn't find test, #{test_name}, in order ##{order[:id]}")
194
+ next
195
+ end
196
+
197
+ next if test.result || test_results['results'].blank?
198
+
199
+ measures = test_results['results'].map do |indicator, value|
200
+ measure = find_measure(order, indicator, value)
201
+ next nil unless measure
202
+
203
+ measure
204
+ end
205
+
206
+ measures = measures.compact
207
+ next if measures.empty?
208
+
209
+ creator = format_result_entered_by(test_results['result_entered_by'])
210
+
211
+ ResultsService.create_results(test.id, provider_id: User.current.person_id,
212
+ date: Utils.parse_date(test_results['date_result_entered'], order[:order_date].to_s),
213
+ comments: "LIMS import: Entered by: #{creator}",
214
+ measures: measures)
215
+ end
216
+ end
217
+
218
+ def find_test(order_id, test_name)
219
+ test_name = Utils.translate_test_name(test_name)
220
+ test_concept = Utils.find_concept_by_name(test_name)
221
+ raise "Unknown test name, #{test_name}!" unless test_concept
222
+
223
+ LabTest.find_by(order_id: order_id, value_coded: test_concept.concept_id)
224
+ end
225
+
226
+ def find_measure(_order, indicator_name, value)
227
+ indicator = Utils.find_concept_by_name(indicator_name)
228
+ unless indicator
229
+ logger.warn("Result indicator #{indicator_name} not found in concepts list")
230
+ return nil
231
+ end
232
+
233
+ value_modifier, value, value_type = parse_lims_result_value(value)
234
+ return nil if value.blank?
235
+
236
+ ActiveSupport::HashWithIndifferentAccess.new(
237
+ indicator: { concept_id: indicator.concept_id },
238
+ value_type: value_type,
239
+ value: value_type == 'numeric' ? value.to_f : value,
240
+ value_modifier: value_modifier.blank? ? '=' : value_modifier
241
+ )
242
+ end
243
+
244
+ def parse_lims_result_value(value)
245
+ value = value['result_value']&.strip
246
+ return nil, nil, nil if value.blank?
247
+
248
+ match = value&.match(/^(>|=|<|<=|>=)(.*)$/)
249
+ return nil, value, guess_result_datatype(value) unless match
250
+
251
+ [match[1], match[2].strip, guess_result_datatype(match[2])]
252
+ end
253
+
254
+ def guess_result_datatype(result)
255
+ return 'numeric' if result.strip.match?(/^[+-]?((\d+(\.\d+)?)|\.\d+)$/)
256
+
257
+ 'text'
258
+ end
259
+
260
+ def format_result_entered_by(result_entered_by)
261
+ first_name = result_entered_by['first_name']
262
+ last_name = result_entered_by['last_name']
263
+ phone_number = result_entered_by['phone_number']
264
+ id = result_entered_by['id'] # Looks like a user_id of some sort
265
+
266
+ "#{id}:#{first_name} #{last_name}:#{phone_number}"
267
+ end
268
+
269
+ def save_failed_import(order_dto, reason, diff = nil)
270
+ logger.info("Failed to import LIMS order ##{order_dto[:tracking_number]} due to '#{reason}'")
271
+ LimsFailedImport.create!(lims_id: order_dto[:_id],
272
+ tracking_number: order_dto[:tracking_number],
273
+ patient_nhid: order_dto[:patient][:id],
274
+ reason: reason,
275
+ diff: diff&.to_json)
276
+ end
277
+
278
+ def last_seq_path
279
+ LIMS_LOG_PATH.join('last_seq.dat')
280
+ end
281
+
282
+ def find_order_mapping_by_lims_id(lims_id)
283
+ mapping = Lab::LimsOrderMapping.find_by(lims_id: lims_id)
284
+ return nil unless mapping
285
+
286
+ if Lab::LabOrder.where(order_id: mapping.order_id).exists?
287
+ return mapping
288
+ end
289
+
290
+ mapping.destroy
291
+ nil
292
+ end
293
+ end
294
+ end
295
+ end