his_emr_api_lab 1.1.22 → 1.1.23

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +71 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/lab/application_controller.rb +6 -0
  6. data/app/controllers/lab/labels_controller.rb +17 -0
  7. data/app/controllers/lab/orders_controller.rb +38 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +19 -0
  10. data/app/controllers/lab/specimen_types_controller.rb +15 -0
  11. data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
  12. data/app/controllers/lab/test_types_controller.rb +15 -0
  13. data/app/controllers/lab/tests_controller.rb +26 -0
  14. data/app/jobs/lab/application_job.rb +4 -0
  15. data/app/jobs/lab/push_order_job.rb +12 -0
  16. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  17. data/app/jobs/lab/void_order_job.rb +17 -0
  18. data/app/mailers/lab/application_mailer.rb +6 -0
  19. data/app/models/lab/application_record.rb +5 -0
  20. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  21. data/app/models/lab/lab_encounter.rb +7 -0
  22. data/app/models/lab/lab_order.rb +58 -0
  23. data/app/models/lab/lab_result.rb +31 -0
  24. data/app/models/lab/lab_test.rb +19 -0
  25. data/app/models/lab/lims_failed_import.rb +4 -0
  26. data/app/models/lab/lims_order_mapping.rb +10 -0
  27. data/app/serializers/lab/lab_order_serializer.rb +55 -0
  28. data/app/serializers/lab/result_serializer.rb +36 -0
  29. data/app/serializers/lab/test_serializer.rb +29 -0
  30. data/app/services/lab/accession_number_service.rb +77 -0
  31. data/app/services/lab/concepts_service.rb +82 -0
  32. data/app/services/lab/labelling_service/order_label.rb +106 -0
  33. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  34. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  35. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  36. data/app/services/lab/lims/api/rest_api.rb +416 -0
  37. data/app/services/lab/lims/api/ws_api.rb +121 -0
  38. data/app/services/lab/lims/api_factory.rb +19 -0
  39. data/app/services/lab/lims/config.rb +100 -0
  40. data/app/services/lab/lims/exceptions.rb +11 -0
  41. data/app/services/lab/lims/migrator.rb +216 -0
  42. data/app/services/lab/lims/order_dto.rb +105 -0
  43. data/app/services/lab/lims/order_serializer.rb +244 -0
  44. data/app/services/lab/lims/pull_worker.rb +289 -0
  45. data/app/services/lab/lims/push_worker.rb +149 -0
  46. data/app/services/lab/lims/utils.rb +91 -0
  47. data/app/services/lab/lims/worker.rb +86 -0
  48. data/app/services/lab/metadata.rb +24 -0
  49. data/app/services/lab/orders_search_service.rb +66 -0
  50. data/app/services/lab/orders_service.rb +212 -0
  51. data/app/services/lab/results_service.rb +149 -0
  52. data/app/services/lab/tests_service.rb +93 -0
  53. data/config/routes.rb +17 -0
  54. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  55. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  56. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  57. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  58. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  59. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  60. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  61. data/lib/auto12epl.rb +201 -0
  62. data/lib/couch_bum/couch_bum.rb +92 -0
  63. data/lib/generators/lab/install/USAGE +9 -0
  64. data/lib/generators/lab/install/install_generator.rb +19 -0
  65. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  66. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  67. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  68. data/lib/his_emr_api_lab.rb +5 -0
  69. data/lib/lab/engine.rb +15 -0
  70. data/lib/lab/version.rb +5 -0
  71. data/lib/logger_multiplexor.rb +38 -0
  72. data/lib/tasks/lab_tasks.rake +25 -0
  73. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  74. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  75. data/lib/tasks/loaders/data/tests.csv +161 -0
  76. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  77. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  78. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  79. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  80. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  81. metadata +81 -2
@@ -0,0 +1,416 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lab::Lims::Api::RestApi
4
+ class LimsApiError < GatewayError; end
5
+
6
+ class AuthenticationTokenExpired < LimsApiError; end
7
+
8
+ class InvalidParameters < LimsApiError; end
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def create_order(order_dto)
15
+ response = in_authenticated_session do |headers|
16
+ Rails.logger.info("Pushing order ##{order_dto[:tracking_number]} to LIMS")
17
+
18
+ if order_dto['sample_type'].casecmp?('not_specified')
19
+ RestClient.post(expand_uri('request_order', api_version: 'v2'), make_create_params(order_dto), headers)
20
+ else
21
+ RestClient.post(expand_uri('create_order'), make_create_params(order_dto), headers)
22
+ end
23
+ end
24
+
25
+ data = JSON.parse(response.body)
26
+ update_order_results(order_dto) unless data['message'].casecmp?('Order already available')
27
+
28
+ ActiveSupport::HashWithIndifferentAccess.new(
29
+ id: order_dto.fetch(:_id, order_dto[:tracking_number]),
30
+ rev: 0,
31
+ tracking_number: order_dto[:tracking_number]
32
+ )
33
+ end
34
+
35
+ def update_order(_id, order_dto)
36
+ in_authenticated_session do |headers|
37
+ RestClient.post(expand_uri('update_order'), make_update_params(order_dto), headers)
38
+ end
39
+
40
+ update_order_results(order_dto)
41
+
42
+ { tracking_number: order_dto[:tracking_number] }
43
+ end
44
+
45
+ def consume_orders(*_args, patient_id: nil, **_kwargs)
46
+ orders_pending_updates(patient_id).each do |order|
47
+ order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
48
+
49
+ if order_dto['priority'].nil? || order_dto['sample_type'].casecmp?('not_specified')
50
+ patch_order_dto_with_lims_order!(order_dto, find_lims_order(order.accession_number))
51
+ end
52
+
53
+ if order_dto['test_results'].empty?
54
+ begin
55
+ patch_order_dto_with_lims_results!(order_dto, find_lims_results(order.accession_number))
56
+ rescue InvalidParameters => e # LIMS responds with a 401 when a result is not found :(
57
+ Rails.logger.error("Failed to fetch results for ##{order.accession_number}: #{e.message}")
58
+ end
59
+ end
60
+
61
+ yield order_dto, OpenStruct.new(last_seq: 0)
62
+ rescue LimsApiError => e
63
+ Rails.logger.error("Failed to fetch updates for ##{order.accession_number}: #{e.class} - #{e.message}")
64
+ sleep(1)
65
+ end
66
+ end
67
+
68
+ def delete_order(_id, order_dto)
69
+ tracking_number = order_dto.fetch('tracking_number')
70
+
71
+ order_dto['tests'].each do |test|
72
+ Rails.logger.info("Voiding test '#{test}' (#{tracking_number}) in LIMS")
73
+ in_authenticated_session do |headers|
74
+ date_voided, voided_status = find_test_status(order_dto, test, 'Voided')
75
+ params = make_void_test_params(tracking_number, test, voided_status['updated_by'], date_voided)
76
+ RestClient.post(expand_uri('update_test'), params, headers)
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :config
84
+
85
+ MAX_LIMS_RETRIES = 5 # LIMS API Calls can only fail this number of times before we give up on it
86
+
87
+ ##
88
+ # Execute LIMS API calls within an authenticated session.
89
+ #
90
+ # Method automatically checks authenticates with LIMS if necessary and passes
91
+ # down the necessary headers for authentication to the REST call being made.
92
+ #
93
+ # Example:
94
+ #
95
+ # response = in_authenticated_session do |headers|
96
+ # RestClient.get(expand_uri('query_results_by_tracking_number/XXXXXX'), headers)
97
+ # end
98
+ #
99
+ # pp JSON.parse(response.body) if response.code == 200
100
+ def in_authenticated_session
101
+ retries ||= MAX_LIMS_RETRIES
102
+
103
+ self.authentication_token = authenticate unless authentication_token
104
+
105
+ response = yield 'token' => authentication_token, 'Content-type' => 'application/json'
106
+ check_response!(response)
107
+ rescue AuthenticationTokenExpired => e
108
+ self.authentication_token = nil
109
+ retry if (retries -= 1).positive?
110
+ rescue RestClient::ExceptionWithResponse => e
111
+ Rails.logger.error("LIMS Error: #{e.response&.code} - #{e.response&.body}")
112
+ raise e unless e.response&.code == 401
113
+
114
+ self.authentication_token = nil
115
+ retry if (retries -= 1).positive?
116
+ end
117
+
118
+ def authenticate
119
+ username = config.fetch(:username)
120
+ password = config.fetch(:password)
121
+
122
+ Rails.logger.debug("Authenticating with LIMS as: #{username}")
123
+ response = RestClient.get(expand_uri("re_authenticate/#{username}/#{password}"),
124
+ headers: { 'Content-type' => 'application/json' })
125
+ response_body = JSON.parse(response.body)
126
+
127
+ if response_body['status'] == 401
128
+ Rails.logger.error("Failed to authenticate with LIMS as #{config.fetch(:username)}: #{response_body['message']}")
129
+ raise LimsApiError, 'LIMS authentication failed'
130
+ end
131
+
132
+ response_body['data']['token']
133
+ end
134
+
135
+ def authentication_token=(token)
136
+ Thread.current[:lims_authentication_token] = token
137
+ end
138
+
139
+ def authentication_token
140
+ Thread.current[:lims_authentication_token]
141
+ end
142
+
143
+ ##
144
+ # Examines a response from LIMS to check if token has expired.
145
+ #
146
+ # LIMS' doesn't properly use HTTP status codes; the codes are embedded in the
147
+ # response body. 200 is used for success responses and 401 for everything else.
148
+ # We have this work around to examine the response body and
149
+ # throw errors accordingly. The following are the errors thrown:
150
+ #
151
+ # Lims::AuthenticationTokenExpired
152
+ # Lims::InvalidParameters
153
+ # Lims::ApiError - Thrown when we couldn't make sense of the error
154
+ def check_response!(response)
155
+ body = JSON.parse(response.body)
156
+ return response if body['status'] == 200
157
+
158
+ Rails.logger.error("Lims Api Error: #{response.body}")
159
+
160
+ raise LimsApiError, "#{body['status']} - #{body['message']}" if body['status'] != 401
161
+
162
+ if body['message'].match?(/token expired/i)
163
+ raise AuthenticationTokenExpired, "Authentication token expired: #{body['message']}"
164
+ end
165
+
166
+ raise InvalidParameters, body['message']
167
+ end
168
+
169
+ ##
170
+ # Takes a LIMS API relative URI and converts it to a full URL.
171
+ def expand_uri(uri, api_version: 'v1')
172
+ protocol = config.fetch(:protocol)
173
+ host = config.fetch(:host)
174
+ port = config.fetch(:port)
175
+ uri = uri.gsub(%r{^/+}, '')
176
+
177
+ "#{protocol}://#{host}:#{port}/api/#{api_version}/#{uri}"
178
+ end
179
+
180
+ ##
181
+ # Converts an OrderDTO to parameters for POST /create_order
182
+ def make_create_params(order_dto)
183
+ {
184
+ tracking_number: order_dto.fetch(:tracking_number),
185
+ district: current_district,
186
+ health_facility_name: order_dto.fetch(:sending_facility),
187
+ first_name: order_dto.fetch(:patient).fetch(:first_name),
188
+ last_name: order_dto.fetch(:patient).fetch(:last_name),
189
+ phone_number: order_dto.fetch(:patient).fetch(:phone_number),
190
+ gender: order_dto.fetch(:patient).fetch(:gender),
191
+ arv_number: order_dto.fetch(:patient).fetch(:arv_number),
192
+ art_regimen: order_dto.fetch(:patient).fetch(:art_regimen),
193
+ art_start_date: order_dto.fetch(:patient).fetch(:art_start_date),
194
+ national_patient_id: order_dto.fetch(:patient).fetch(:id),
195
+ requesting_clinician: requesting_clinician(order_dto),
196
+ sample_type: order_dto.fetch(:sample_type),
197
+ tests: order_dto.fetch(:tests),
198
+ date_sample_drawn: sample_drawn_date(order_dto),
199
+ sample_priority: order_dto.fetch(:priority) || 'Routine',
200
+ sample_status: order_dto.fetch(:sample_status),
201
+ target_lab: order_dto.fetch(:receiving_facility),
202
+ order_location: order_dto.fetch(:order_location) || 'Unknown',
203
+ who_order_test_first_name: order_dto.fetch(:who_order_test).fetch(:first_name),
204
+ who_order_test_last_name: order_dto.fetch(:who_order_test).fetch(:last_name)
205
+ }
206
+ end
207
+
208
+ ##
209
+ # Converts an OrderDTO to parameters for POST /update_order
210
+ def make_update_params(order_dto)
211
+ date_updated, status = sample_drawn_status(order_dto)
212
+
213
+ {
214
+ tracking_number: order_dto.fetch(:tracking_number),
215
+ who_updated: status.fetch(:updated_by),
216
+ date_updated: date_updated,
217
+ specimen_type: order_dto.fetch(:sample_type),
218
+ status: 'specimen_collected'
219
+ }
220
+ end
221
+
222
+ def current_district
223
+ health_centre = Location.current_health_center
224
+ raise 'Current health centre not set' unless health_centre
225
+
226
+ district = health_centre.district || Lab::Lims::Config.application['district']
227
+
228
+ unless district
229
+ health_centre_name = "##{health_centre.id} - #{health_centre.name}"
230
+ raise "Current health centre district not set: #{health_centre_name}"
231
+ end
232
+
233
+ district
234
+ end
235
+
236
+ ##
237
+ # Extracts sample drawn status from an OrderDTO
238
+ def sample_drawn_status(order_dto)
239
+ order_dto[:sample_statuses].each do |trail_entry|
240
+ date, status = trail_entry.each_pair.find { |_date, status| status['status'].casecmp?('Drawn') }
241
+ next unless date
242
+
243
+ return Date.strptime(date, '%Y%m%d%H%M%S').strftime('%Y-%m-%d'), status
244
+ end
245
+
246
+ [order_dto['date_created'], nil]
247
+ end
248
+
249
+ ##
250
+ # Extracts a sample drawn date from a LIMS OrderDTO.
251
+ def sample_drawn_date(order_dto)
252
+ sample_drawn_status(order_dto).first
253
+ end
254
+
255
+ ##
256
+ # Extracts the requesting clinician from a LIMS OrderDTO
257
+ def requesting_clinician(order_dto)
258
+ orderer = order_dto[:who_order_test]
259
+
260
+ "#{orderer[:first_name]} #{orderer[:last_name]}"
261
+ end
262
+
263
+ def find_lims_order(tracking_number)
264
+ response = in_authenticated_session do |headers|
265
+ Rails.logger.info("Fetching order ##{tracking_number}")
266
+ RestClient.get(expand_uri("query_order_by_tracking_number/#{tracking_number}"), headers)
267
+ end
268
+
269
+ Rails.logger.info("Order ##{tracking_number} found... Parsing...")
270
+ JSON.parse(response).fetch('data')
271
+ end
272
+
273
+ def find_lims_results(tracking_number)
274
+ response = in_authenticated_session do |headers|
275
+ Rails.logger.info("Fetching results for order ##{tracking_number}")
276
+ RestClient.get(expand_uri("query_results_by_tracking_number/#{tracking_number}"), headers)
277
+ end
278
+
279
+ Rails.logger.info("Result for order ##{tracking_number} found... Parsing...")
280
+ JSON.parse(response).fetch('data').fetch('results')
281
+ end
282
+
283
+ ##
284
+ # Make a copy of the order_dto with the results from LIMS parsed
285
+ # and appended to it.
286
+ def patch_order_dto_with_lims_results!(order_dto, results)
287
+ order_dto.merge!(
288
+ '_id' => order_dto[:tracking_number],
289
+ '_rev' => 0,
290
+ 'test_results' => results.each_with_object({}) do |result, formatted_results|
291
+ test_name, measures = result
292
+ result_date = measures.delete('result_date')
293
+
294
+ formatted_results[test_name] = {
295
+ results: measures.each_with_object({}) do |measure, processed_measures|
296
+ processed_measures[measure[0]] = { 'result_value' => measure[1] }
297
+ end,
298
+ result_date: result_date,
299
+ result_entered_by: {}
300
+ }
301
+ end
302
+ )
303
+ end
304
+
305
+ def patch_order_dto_with_lims_order!(order_dto, lims_order)
306
+ order_dto.merge!(
307
+ 'sample_type' => lims_order['other']['sample_type'],
308
+ 'sample_status' => lims_order['other']['specimen_status'],
309
+ 'priority' => lims_order['other']['priority']
310
+ )
311
+ end
312
+
313
+ def update_order_results(order_dto)
314
+ return nil if order_dto['test_results'].nil? || order_dto['test_results'].empty?
315
+
316
+ order_dto['test_results'].each do |test_name, results|
317
+ Rails.logger.info("Pushing result for order ##{order_dto['tracking_number']}")
318
+ in_authenticated_session do |headers|
319
+ params = make_update_test_params(order_dto['tracking_number'], test_name, results)
320
+
321
+ RestClient.post(expand_uri('update_test'), params, headers)
322
+ end
323
+ end
324
+ end
325
+
326
+ def make_update_test_params(tracking_number, test_name, results, test_status = 'Drawn')
327
+ {
328
+ tracking_number: tracking_number,
329
+ test_name: test_name,
330
+ result_date: results['result_date'],
331
+ time_updated: results['result_date'],
332
+ who_updated: {
333
+ first_name: results[:result_entered_by][:first_name],
334
+ last_name: results[:result_entered_by][:last_name],
335
+ id_number: results[:result_entered_by][:id]
336
+ },
337
+ test_status: test_status,
338
+ results: results['results']&.each_with_object({}) do |measure, formatted_results|
339
+ measure_name, measure_value = measure
340
+
341
+ formatted_results[measure_name] = measure_value['result_value']
342
+ end
343
+ }
344
+ end
345
+
346
+ def find_test_status(order_dto, target_test, target_status)
347
+ order_dto['test_statuses'].each do |test, statuses|
348
+ next unless test.casecmp?(target_test)
349
+
350
+ statuses.each do |date, status|
351
+ next unless status['status'].casecmp?(target_status)
352
+
353
+ return [Date.strptime(date, '%Y%m%d%H%M%S'), status]
354
+ end
355
+ end
356
+
357
+ nil
358
+ end
359
+
360
+ def make_void_test_params(tracking_number, test_name, voided_by, void_date = nil)
361
+ void_date ||= Time.now
362
+
363
+ {
364
+ tracking_number: tracking_number,
365
+ test_name: test_name,
366
+ time_updated: void_date,
367
+ who_updated: {
368
+ first_name: voided_by[:first_name],
369
+ last_name: voided_by[:last_name],
370
+ id_number: voided_by[:id]
371
+ },
372
+ test_status: 'voided'
373
+ }
374
+ end
375
+
376
+ def orders_pending_updates(patient_id = nil)
377
+ Rails.logger.info('Looking for orders that need to be updated...')
378
+ orders = {}
379
+
380
+ orders_without_specimen(patient_id).each { |order| orders[order.order_id] = order }
381
+ orders_without_results(patient_id).each { |order| orders[order.order_id] = order }
382
+ orders_without_reason(patient_id).each { |order| orders[order.order_id] = order }
383
+
384
+ orders.values
385
+ end
386
+
387
+ def orders_without_specimen(patient_id = nil)
388
+ Rails.logger.debug('Looking for orders without a specimen')
389
+ unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
390
+ .select(:concept_id)
391
+ orders = Lab::LabOrder.where(concept_id: unknown_specimen)
392
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
393
+ orders = orders.where(patient_id: patient_id) if patient_id
394
+
395
+ orders
396
+ end
397
+
398
+ def orders_without_results(patient_id = nil)
399
+ Rails.logger.debug('Looking for orders without a result')
400
+ # Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
401
+ # .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id).where("pulled_at IS NULL"))
402
+ Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
403
+ .where(order_id: Lab::LimsOrderMapping.select(:order_id))
404
+ end
405
+
406
+ def orders_without_reason(patient_id = nil)
407
+ Rails.logger.debug('Looking for orders without a reason for test')
408
+ orders = Lab::LabOrder.joins(:reason_for_test)
409
+ .merge(Observation.where(value_coded: nil, value_text: nil))
410
+ .limit(1000)
411
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
412
+ orders = orders.where(patient_id: patient_id) if patient_id
413
+
414
+ orders
415
+ end
416
+ end
@@ -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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ ##
6
+ # Creates LIMS Apis based on current configuration
7
+ module ApiFactory
8
+ def self.create_api
9
+ return Lab::Lims::Api::BlackholeApi.new if Rails.env.casecmp?('test')
10
+
11
+ case Lab::Lims::Config.preferred_api
12
+ when /rest/i then Lab::Lims::Api::RestApi.new(Lab::Lims::Config.rest_api)
13
+ when /couchdb/ then Lab::Lims::Api::CouchDbApi.new(config: Lab::Lims::Config.couchdb_api)
14
+ else raise "Invalid lims_api configuration: #{Lab::Lims::Config.preferred_api}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ ##
6
+ # Load LIMS' configuration files
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
+
11
+ class ConfigNotFound < RuntimeError; end
12
+
13
+ class << self
14
+ def preferred_api
15
+ emr_api_application('lims_api')
16
+ end
17
+
18
+ ##
19
+ # Returns LIMS' couchdb configuration file for the current environment (Rails.env)
20
+ def couchdb
21
+ config_path = begin
22
+ find_config_path('couchdb.yml')
23
+ rescue ConfigNotFound => e
24
+ Rails.logger.error("Failed to find default LIMS couchdb config: #{e.message}")
25
+ find_config_path('couchdb-lims.yml') # This can be placed in HIS-EMR-API/config
26
+ end
27
+
28
+ Rails.logger.debug("Using LIMS couchdb config: #{config_path}")
29
+
30
+ YAML.load_file(config_path)[Rails.env]
31
+ end
32
+
33
+ def rest_api
34
+ @rest_api ||= {
35
+ protocol: emr_api_application('lims_protocol', 'http'),
36
+ host: emr_api_application('lims_host'),
37
+ port: emr_api_application('lims_port'),
38
+ username: emr_api_application('lims_username'),
39
+ password: emr_api_application('lims_password')
40
+ }
41
+ end
42
+
43
+ def updates_socket
44
+ @updates_socket ||= {
45
+ 'url' => emr_api_application('lims_realtime_updates_url')
46
+ }
47
+ end
48
+
49
+ def updates_poll_frequency
50
+ 30 # Seconds
51
+ end
52
+
53
+ ##
54
+ # Returns LIMS' application.yml configuration file
55
+ def application
56
+ @application ||= YAML.load_file(find_config_path('application.yml'))
57
+ end
58
+
59
+ ##
60
+ # Returns LIMS' database.yml configuration file
61
+ def database
62
+ @database ||= YAML.load_file(find_config_path('database.yml'))
63
+ end
64
+
65
+ private
66
+
67
+ def emr_api_application(param, fallback = nil)
68
+ @emr_api_application ||= YAML.load_file(Rails.root.join('config', 'application.yml'))
69
+
70
+ @emr_api_application.fetch(param) do
71
+ raise ConfigNotFound, "Missing config param: #{param}" unless fallback
72
+
73
+ fallback
74
+ end
75
+ end
76
+
77
+ ##
78
+ # Looks for a config file in various LIMS installation directories
79
+ #
80
+ # Returns: a path to a file found
81
+ def find_config_path(filename)
82
+ paths = [
83
+ "#{ENV['HOME']}/apps/nlims_controller/config/#{filename}",
84
+ "/var/www/nlims_controller/config/#{filename}",
85
+ Rails.root.parent.join("nlims_controller/config/#{filename}")
86
+ ]
87
+
88
+ paths = [Rails.root.join('config/lims-couchdb.yml'), *paths] if filename == 'couchdb.yml'
89
+
90
+ paths.each do |path|
91
+ Rails.logger.debug("Looking for LIMS couchdb config at: #{path}")
92
+ return path if File.exist?(path)
93
+ end
94
+
95
+ raise ConfigNotFound, "Could not find a configuration file, checked: #{paths.join(':')}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ class LimsException < StandardError; end
6
+ class DuplicateNHID < LimsException; end
7
+ class MissingAccessionNumber < LimsException; end
8
+ class UnknownSpecimenType < LimsException; end
9
+ class UnknownTestType < LimsException; end
10
+ end
11
+ end