his_emr_api_lab 1.0.5 → 1.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.
@@ -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