his_emr_api_lab 1.0.5 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
@@ -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,9 @@ 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(
18
19
  tracking_number: serialized_order.accession_number,
19
20
  sending_facility: current_facility_name,
20
21
  receiving_facility: serialized_order.target_lab,
@@ -69,7 +70,11 @@ module Lab
69
70
  end
70
71
 
71
72
  def format_sample_type(name)
72
- name.casecmp?('Unknown') ? 'not_specified' : name.titleize
73
+ return 'not_specified' if name.casecmp?('Unknown')
74
+
75
+ return 'CSF' if name.casecmp?('Cerebrospinal Fluid')
76
+
77
+ name.titleize
73
78
  end
74
79
 
75
80
  def format_sample_status(name)
@@ -77,7 +82,9 @@ module Lab
77
82
  end
78
83
 
79
84
  def format_sample_status_trail(order)
80
- return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
85
+ if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
86
+ return []
87
+ end
81
88
 
82
89
  user = User.find(order.discontinued_by || order.creator)
83
90
  drawn_by = PersonName.find_by_person_id(user.user_id)
@@ -97,7 +104,9 @@ module Lab
97
104
  end
98
105
 
99
106
  def format_test_status_trail(order)
100
- order.tests.each_with_object({}) do |test, trail|
107
+ tests = order.voided.zero? ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
108
+
109
+ tests.each_with_object({}) do |test, trail|
101
110
  test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
102
111
 
103
112
  current_test_trail = trail[test_name] = {}
@@ -107,6 +116,13 @@ module Lab
107
116
  updated_by: find_user(test.creator)
108
117
  }
109
118
 
119
+ unless test.voided.zero?
120
+ current_test_trail[test.date_voided.strftime('%Y%m%d%H%M%S')] = {
121
+ status: 'Voided',
122
+ updated_by: find_user(test.voided_by)
123
+ }
124
+ end
125
+
110
126
  next unless test.result
111
127
 
112
128
  current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
@@ -122,7 +138,10 @@ module Lab
122
138
 
123
139
  def format_test_results(order)
124
140
  order.tests&.each_with_object({}) do |test, results|
125
- next unless test.result
141
+ next if test.result.nil? || test.result.empty?
142
+
143
+ test_creator = User.find(Observation.find(test.result.first.id).creator)
144
+ test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
126
145
 
127
146
  results[format_test_name(test.name)] = {
128
147
  results: test.result.each_with_object({}) do |measure, measures|
@@ -131,7 +150,11 @@ module Lab
131
150
  }
132
151
  end,
133
152
  result_date: test.result.first&.date,
134
- result_entered_by: {}
153
+ result_entered_by: {
154
+ first_name: test_creator_name&.given_name,
155
+ last_name: test_creator_name&.family_name,
156
+ id: test_creator.username
157
+ }
135
158
  }
136
159
  end
137
160
  end
@@ -165,7 +188,7 @@ module Lab
165
188
  return district if district
166
189
 
167
190
  GlobalProperty.create(property: 'current_health_center_district',
168
- property_value: Config.application['district'],
191
+ property_value: Lims::Config.application['district'],
169
192
  uuid: SecureRandom.uuid)
170
193
 
171
194
  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 unless test_results['results']
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