his_emr_api_lab 2.1.0 → 2.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/lab/labels_controller.rb +2 -1
  3. data/app/controllers/lab/orders_controller.rb +14 -25
  4. data/app/controllers/lab/results_controller.rb +2 -1
  5. data/app/controllers/lab/specimen_types_controller.rb +0 -1
  6. data/app/controllers/lab/test_methods_controller.rb +9 -0
  7. data/app/controllers/lab/test_types_controller.rb +1 -1
  8. data/app/controllers/lab/tests_controller.rb +1 -3
  9. data/app/controllers/lab/users_controller.rb +2 -1
  10. data/app/jobs/lab/process_lab_result_job.rb +13 -0
  11. data/app/models/lab/lab_order.rb +4 -1
  12. data/app/models/lab/lab_result.rb +4 -0
  13. data/app/models/lab/lab_test.rb +1 -1
  14. data/app/models/lab/order_extension.rb +14 -0
  15. data/app/serializers/lab/lab_order_serializer.rb +27 -4
  16. data/app/serializers/lab/result_serializer.rb +10 -2
  17. data/app/serializers/lab/test_serializer.rb +24 -1
  18. data/app/services/lab/accession_number_service.rb +2 -2
  19. data/app/services/lab/acknowledgement_service.rb +4 -1
  20. data/app/services/lab/concepts_service.rb +77 -22
  21. data/app/services/lab/lims/acknowledgement_worker.rb +1 -1
  22. data/app/services/lab/lims/api/rest_api.rb +543 -78
  23. data/app/services/lab/lims/config.rb +7 -2
  24. data/app/services/lab/lims/exceptions.rb +7 -6
  25. data/app/services/lab/lims/migrator.rb +3 -3
  26. data/app/services/lab/lims/order_dto.rb +1 -1
  27. data/app/services/lab/lims/order_serializer.rb +28 -7
  28. data/app/services/lab/lims/pull_worker.rb +6 -6
  29. data/app/services/lab/lims/push_worker.rb +3 -3
  30. data/app/services/lab/lims/utils.rb +3 -4
  31. data/app/services/lab/lims/worker.rb +2 -2
  32. data/app/services/lab/metadata.rb +4 -0
  33. data/app/services/lab/notification_service.rb +72 -0
  34. data/app/services/lab/orders_search_service.rb +11 -5
  35. data/app/services/lab/orders_service.rb +82 -36
  36. data/app/services/lab/results_service.rb +32 -17
  37. data/app/services/lab/tests_service.rb +15 -3
  38. data/config/routes.rb +1 -0
  39. data/db/migrate/20260119104240_add_fulfiller_fields_to_orders.rb +11 -0
  40. data/db/migrate/20260119104241_create_comment_to_fulfiller_concept.rb +50 -0
  41. data/lib/lab/version.rb +1 -1
  42. data/lib/mahis_emr_api_lab.rb +6 -0
  43. metadata +12 -5
  44. /data/app/services/lab/lims/api/{couchdb_api.rb → couch_db_api.rb} +0 -0
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rest-client'
4
+
3
5
  module Lab
4
6
  module Lims
5
7
  module Api
6
8
  class RestApi
7
- class LimsApiError < GatewayError; end
8
-
9
+ class LimsApiError < StandardError; end
9
10
  class AuthenticationTokenExpired < LimsApiError; end
10
-
11
11
  class InvalidParameters < LimsApiError; end
12
12
 
13
+ START_DATE = Time.parse('2024-09-03').freeze
14
+
13
15
  def initialize(config)
14
16
  @config = config
15
17
  end
@@ -17,11 +19,11 @@ module Lab
17
19
  def create_order(order_dto)
18
20
  response = in_authenticated_session do |headers|
19
21
  Rails.logger.info("Pushing order ##{order_dto[:tracking_number]} to LIMS")
20
-
21
22
  if order_dto['sample_type'].casecmp?('not_specified')
22
- RestClient.post(expand_uri('request_order', api_version: 'v2'), make_create_params(order_dto), headers)
23
+ RestClient.post(expand_uri('orders/requests', api_version: 'v2'), make_create_params(order_dto),
24
+ headers)
23
25
  else
24
- RestClient.post(expand_uri('create_order'), make_create_params(order_dto), headers)
26
+ RestClient.post(expand_uri('orders', api_version: 'v2'), make_create_params(order_dto), headers)
25
27
  end
26
28
  end
27
29
 
@@ -37,16 +39,34 @@ module Lab
37
39
 
38
40
  def acknowledge(acknowledgement_dto)
39
41
  Rails.logger.info("Acknowledging order ##{acknowledgement_dto} in LIMS")
42
+ test_concept = ::ConceptName.find_by_name(acknowledgement_dto.fetch(:test))&.concept
43
+ if test_concept.nil?
44
+ return { 'status' => 400,
45
+ 'message' => "Test concept not found for '#{acknowledgement_dto.fetch(:test)}'" }
46
+ end
47
+
40
48
  response = in_authenticated_session do |headers|
41
- RestClient.post(expand_uri('/acknowledge/test/results/recipient'), acknowledgement_dto, headers)
49
+ RestClient.post(expand_uri("tests/#{acknowledgement_dto[:tracking_number]}/acknowledge_test_results_receipt", api_version: 'v2'), {
50
+ test_type: {
51
+ name: test_concept.test_catalogue_name,
52
+ nlims_code: test_concept.nlims_code
53
+ },
54
+ date_acknowledged: acknowledgement_dto[:date_acknowledged],
55
+ recipient_type: acknowledgement_dto[:recipient_type],
56
+ acknowledged_by: 'emr_at_facility'
57
+ }, headers)
42
58
  end
43
59
  Rails.logger.info("Acknowledged order ##{acknowledgement_dto} in LIMS. Response: #{response}")
44
60
  JSON.parse(response)
61
+ rescue StandardError => e
62
+ Rails.logger.error("Failed to acknowledge order ##{acknowledgement_dto} in LIMS: #{e.class}")
63
+ { 'status' => 500, 'message' => e.message }
45
64
  end
46
65
 
47
66
  def update_order(_id, order_dto)
48
67
  in_authenticated_session do |headers|
49
- RestClient.post(expand_uri('update_order'), make_update_params(order_dto), headers)
68
+ RestClient.put(expand_uri("orders/#{order_dto[:tracking_number]}", api_version: 'v2'),
69
+ make_update_params(order_dto), headers)
50
70
  end
51
71
 
52
72
  update_order_results(order_dto)
@@ -57,35 +77,39 @@ module Lab
57
77
  def consume_orders(*_args, patient_id: nil, **_kwargs)
58
78
  orders_pending_updates(patient_id).each do |order|
59
79
  order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
60
-
61
80
  if order_dto['priority'].nil? || order_dto['sample_type'].casecmp?('not_specified')
62
81
  patch_order_dto_with_lims_order!(order_dto, find_lims_order(order.accession_number))
63
82
  end
64
-
65
83
  if order_dto['test_results'].empty?
66
84
  begin
67
85
  patch_order_dto_with_lims_results!(order_dto, find_lims_results(order.accession_number))
68
86
  rescue InvalidParameters => e # LIMS responds with a 401 when a result is not found :(
69
87
  Rails.logger.error("Failed to fetch results for ##{order.accession_number}: #{e.message}")
88
+ next
70
89
  end
71
90
  end
72
91
 
73
92
  yield order_dto, OpenStruct.new(last_seq: 0)
93
+ rescue RestClient::NotFound
94
+ Rails.logger.error("Order ##{order.accession_number} not found in LIMS")
95
+ next
74
96
  rescue LimsApiError => e
75
97
  Rails.logger.error("Failed to fetch updates for ##{order.accession_number}: #{e.class} - #{e.message}")
76
98
  sleep(1)
99
+ rescue StandardError => e
100
+ Rails.logger.error("Failed to fetch updates for ##{order.accession_number}: #{e.class} - #{e.message}")
101
+ next
77
102
  end
78
103
  end
79
104
 
80
105
  def delete_order(_id, order_dto)
81
106
  tracking_number = order_dto.fetch('tracking_number')
82
-
83
- order_dto['tests'].each do |test|
84
- Rails.logger.info("Voiding test '#{test}' (#{tracking_number}) in LIMS")
107
+ order_dto['tests_map'].each do |test|
108
+ Rails.logger.info("Voiding test '#{test.name}' (#{tracking_number}) in LIMS")
85
109
  in_authenticated_session do |headers|
86
- date_voided, voided_status = find_test_status(order_dto, test, 'Voided')
110
+ date_voided, voided_status = find_test_status(order_dto, test.name, 'Voided')
87
111
  params = make_void_test_params(tracking_number, test, voided_status['updated_by'], date_voided)
88
- RestClient.post(expand_uri('update_test'), params, headers)
112
+ RestClient.put(expand_uri("tests/#{tracking_number}", api_version: 'v2'), params.to_json, headers)
89
113
  end
90
114
  end
91
115
  end
@@ -172,7 +196,8 @@ module Lab
172
196
  # Lims::ApiError - Thrown when we couldn't make sense of the error
173
197
  def check_response!(response)
174
198
  body = JSON.parse(response.body)
175
- return response if body['status'] == 200
199
+
200
+ return response if [200, 201].include?(response.code)
176
201
 
177
202
  Rails.logger.error("Lims Api Error: #{response.body}")
178
203
 
@@ -200,28 +225,51 @@ module Lab
200
225
  # Converts an OrderDto to parameters for POST /create_order
201
226
  def make_create_params(order_dto)
202
227
  {
203
- tracking_number: order_dto.fetch(:tracking_number),
204
- district: current_district,
205
- health_facility_name: order_dto.fetch(:sending_facility),
206
- first_name: order_dto.fetch(:patient).fetch(:first_name),
207
- last_name: order_dto.fetch(:patient).fetch(:last_name),
208
- phone_number: order_dto.fetch(:patient).fetch(:phone_number),
209
- gender: order_dto.fetch(:patient).fetch(:gender),
210
- arv_number: order_dto.fetch(:patient).fetch(:arv_number),
211
- art_regimen: order_dto.fetch(:patient).fetch(:art_regimen),
212
- art_start_date: order_dto.fetch(:patient).fetch(:art_start_date),
213
- date_of_birth: order_dto.fetch(:patient).fetch(:dob),
214
- national_patient_id: order_dto.fetch(:patient).fetch(:id),
215
- requesting_clinician: requesting_clinician(order_dto),
216
- sample_type: order_dto.fetch(:sample_type),
217
- tests: order_dto.fetch(:tests),
218
- date_sample_drawn: sample_drawn_date(order_dto),
219
- sample_priority: order_dto.fetch(:priority) || 'Routine',
220
- sample_status: order_dto.fetch(:sample_status),
221
- target_lab: order_dto.fetch(:receiving_facility),
222
- order_location: order_dto.fetch(:order_location) || 'Unknown',
223
- who_order_test_first_name: order_dto.fetch(:who_order_test).fetch(:first_name),
224
- who_order_test_last_name: order_dto.fetch(:who_order_test).fetch(:last_name)
228
+ order: {
229
+ tracking_number: order_dto.fetch(:tracking_number),
230
+ district: current_district,
231
+ health_facility_name: order_dto.fetch(:sending_facility),
232
+ sending_facility: order_dto.fetch(:sending_facility),
233
+ arv_number: order_dto.fetch(:patient).fetch(:arv_number),
234
+ art_regimen: order_dto.fetch(:patient).fetch(:art_regimen),
235
+ art_start_date: order_dto.fetch(:patient).fetch(:art_start_date),
236
+ requesting_clinician: requesting_clinician(order_dto),
237
+ sample_type: order_dto.fetch(:sample_type_map),
238
+ date_sample_drawn: sample_drawn_date(order_dto),
239
+ date_created: sample_drawn_date(order_dto),
240
+ priority: order_dto.fetch(:priority) || 'Routine',
241
+ sample_status: {
242
+ name: order_dto.fetch(:sample_status)
243
+ },
244
+ target_lab: order_dto.fetch(:receiving_facility),
245
+ order_location: order_dto.fetch(:order_location) || 'Unknown',
246
+ who_order_test_first_name: order_dto.fetch(:who_order_test).fetch(:first_name),
247
+ who_order_test_last_name: order_dto.fetch(:who_order_test).fetch(:last_name),
248
+ requested_by: "#{order_dto.fetch(:who_order_test).fetch(:first_name)} #{order_dto.fetch(:who_order_test).fetch(:last_name)}",
249
+ drawn_by: {
250
+ id: order_dto.fetch(:who_order_test).fetch(:id),
251
+ name: "#{order_dto.fetch(:who_order_test).fetch(:first_name)} #{order_dto.fetch(:who_order_test).fetch(:last_name)}"
252
+ },
253
+ clinical_history: order_dto.fetch(:clinical_history)
254
+ },
255
+ patient: {
256
+ national_patient_id: order_dto.fetch(:patient).fetch(:id),
257
+ first_name: order_dto.fetch(:patient).fetch(:first_name),
258
+ last_name: order_dto.fetch(:patient).fetch(:last_name),
259
+ phone_number: order_dto.fetch(:patient).fetch(:phone_number),
260
+ gender: order_dto.fetch(:patient).fetch(:gender),
261
+ date_of_birth: order_dto.fetch(:patient).fetch(:dob)
262
+ },
263
+ tests: order_dto.fetch(:tests_map).map do |test|
264
+ concept = ::Concept.find(test.concept_id)
265
+ {
266
+ test_type: {
267
+ name: concept.test_catalogue_name,
268
+ nlims_code: concept.nlims_code,
269
+ method_of_testing: test&.test_method&.name
270
+ }
271
+ }
272
+ end
225
273
  }
226
274
  end
227
275
 
@@ -229,13 +277,11 @@ module Lab
229
277
  # Converts an OrderDto to parameters for POST /update_order
230
278
  def make_update_params(order_dto)
231
279
  date_updated, status = sample_drawn_status(order_dto)
232
-
233
280
  {
234
- tracking_number: order_dto.fetch(:tracking_number),
235
- who_updated: status.fetch(:updated_by),
236
- date_updated:,
237
- specimen_type: order_dto.fetch(:sample_type),
238
- status: 'specimen_collected'
281
+ status: 'specimen_collected',
282
+ time_updated: date_updated,
283
+ sample_type: order_dto.fetch(:sample_type_map),
284
+ updated_by: status.fetch(:updated_by)
239
285
  }
240
286
  end
241
287
 
@@ -283,17 +329,19 @@ module Lab
283
329
  def find_lims_order(tracking_number)
284
330
  response = in_authenticated_session do |headers|
285
331
  Rails.logger.info("Fetching order ##{tracking_number}")
286
- RestClient.get(expand_uri("query_order_by_tracking_number/#{tracking_number}"), headers)
332
+ RestClient.get(expand_uri("orders/#{tracking_number}", api_version: 'v2'), headers)
287
333
  end
288
334
 
289
335
  Rails.logger.info("Order ##{tracking_number} found... Parsing...")
290
336
  JSON.parse(response).fetch('data')
337
+ rescue RestClient::NotFound
338
+ raise RestClient::NotFound, "Order ##{tracking_number} not found in LIMS"
291
339
  end
292
340
 
293
341
  def nlims_order_exists?(tracking_number)
294
342
  response = in_authenticated_session do |headers|
295
343
  Rails.logger.info("Verifying order ##{tracking_number}")
296
- RestClient.get(expand_uri("verify_order_tracking_number_exist/#{tracking_number}"), headers)
344
+ RestClient.get(expand_uri("orders/#{tracking_number}", api_version: 'v2'), headers)
297
345
  end
298
346
 
299
347
  Rails.logger.info("Order ##{tracking_number} verified... Parsing...")
@@ -303,11 +351,21 @@ module Lab
303
351
  def find_lims_results(tracking_number)
304
352
  response = in_authenticated_session do |headers|
305
353
  Rails.logger.info("Fetching results for order ##{tracking_number}")
306
- RestClient.get(expand_uri("query_results_by_tracking_number/#{tracking_number}"), headers)
354
+ RestClient.get(expand_uri("orders/#{tracking_number}", api_version: 'v2'), headers)
355
+ end
356
+
357
+ Rails.logger.info("Found order ##{tracking_number}")
358
+ data = JSON.parse(response)
359
+
360
+ if data.fetch('data').fetch('tests').all? do |test|
361
+ test.fetch('test_results').blank?
307
362
  end
363
+ raise InvalidParameters, "No results found for order ##{tracking_number}"
364
+ end
365
+
366
+ Rails.logger.info("Result for order ##{tracking_number} found...")
308
367
 
309
- Rails.logger.info("Result for order ##{tracking_number} found... Parsing...")
310
- JSON.parse(response).fetch('data').fetch('results')
368
+ data.fetch('data').fetch('tests')
311
369
  end
312
370
 
313
371
  ##
@@ -318,25 +376,65 @@ module Lab
318
376
  '_id' => order_dto[:tracking_number],
319
377
  '_rev' => 0,
320
378
  'test_results' => results.each_with_object({}) do |result, formatted_results|
321
- test_name, measures = result
322
- result_date = measures.delete('result_date')
379
+ test_type = result.delete('test_type')
380
+ if test_type.nil?
381
+ Rails.logger.error("ERROR: test_type is nil for result: #{result.inspect}")
382
+ next
383
+ end
384
+ results = result.delete('test_results')
385
+ if results.nil?
386
+ Rails.logger.error("ERROR: test_results is nil for result: #{result.inspect}")
387
+ next
388
+ end
389
+
390
+ test_name = test_type['name']
391
+ Rails.logger.debug("test_name: #{test_name}")
392
+
393
+ if test_name.nil?
394
+ Rails.logger.error("ERROR: test_name is nil, test_type was: #{test_type.inspect}")
395
+ next
396
+ end
323
397
 
324
398
  formatted_results[test_name] = {
325
- results: measures.each_with_object({}) do |measure, processed_measures|
326
- processed_measures[measure[0]] = { 'result_value' => measure[1] }
399
+ results: results.each_with_object({}) do |result, processed_measures|
400
+ measure = result['measure']
401
+ if measure.nil?
402
+ Rails.logger.error(" ERROR: measure is nil for result: #{result.inspect}")
403
+ next
404
+ end
405
+ result_data = result['result']
406
+ if result_data.nil?
407
+ Rails.logger.error(" ERROR: result_data is nil for result: #{result.inspect}")
408
+ next
409
+ end
410
+
411
+ measure_name = measure['name']
412
+ result_value = result_data['value']
413
+
414
+ if measure_name.nil?
415
+ Rails.logger.error(" ERROR: measure_name is nil, measure was: #{measure.inspect}")
416
+ next
417
+ end
418
+
419
+ processed_measures[measure_name] = { 'result_value' => result_value }
327
420
  end,
328
- result_date:,
421
+ result_date: results&.first&.dig('result', 'result_date'),
329
422
  result_entered_by: {}
330
423
  }
331
424
  end
332
425
  )
426
+ rescue StandardError => e
427
+ Rails.logger.error("EXCEPTION in patch_order_dto_with_lims_results!: #{e.class} - #{e.message}")
428
+ Rails.logger.error("Backtrace: #{e.backtrace.first(10).join("\n")}")
429
+ raise
333
430
  end
334
431
 
335
432
  def patch_order_dto_with_lims_order!(order_dto, lims_order)
433
+ order = lims_order['order']
336
434
  order_dto.merge!(
337
- 'sample_type' => lims_order['other']['sample_type'],
338
- 'sample_status' => lims_order['other']['specimen_status'],
339
- 'priority' => lims_order['other']['priority']
435
+ 'sample_type' => order['sample_type']['name'],
436
+ 'sample_status' => order['sample_status']['name'],
437
+ 'priority' => order['priority']
340
438
  )
341
439
  end
342
440
 
@@ -348,27 +446,32 @@ module Lab
348
446
  in_authenticated_session do |headers|
349
447
  params = make_update_test_params(order_dto['tracking_number'], test_name, results)
350
448
 
351
- RestClient.post(expand_uri('update_test'), params, headers)
449
+ RestClient.post(expand_uri("tests/#{order_dto['tracking_number']}", api_version: 'v2'), params, headers)
352
450
  end
353
451
  end
354
452
  end
355
453
 
356
- def make_update_test_params(tracking_number, test_name, results, test_status = 'Drawn')
454
+ def make_update_test_params(_tracking_number, test, results, test_status = 'Drawn')
357
455
  {
358
- tracking_number:,
359
- test_name:,
360
- result_date: results['result_date'],
456
+ test_status:,
361
457
  time_updated: results['result_date'],
362
- who_updated: {
363
- first_name: results[:result_entered_by][:first_name],
364
- last_name: results[:result_entered_by][:last_name],
365
- id_number: results[:result_entered_by][:id]
458
+ test_type: {
459
+ name: ::Concept.find(test.concept_id).test_catalogue_name,
460
+ nlims_code: ::Concept.find(test.concept_id).nlims_code
366
461
  },
367
- test_status:,
368
- results: results['results']&.each_with_object({}) do |measure, formatted_results|
462
+ test_results: results['results'].map do |measure, _value|
369
463
  measure_name, measure_value = measure
370
-
371
- formatted_results[measure_name] = measure_value['result_value']
464
+ {
465
+ measure: {
466
+ name: measure_name,
467
+ nlims_code: ::ConceptAttribute.find_by(value_reference: measure_name,
468
+ attribute_type: ConceptAttributeType.nlims_code)&.value_reference
469
+ },
470
+ result: {
471
+ value: measure_value,
472
+ result_date: results['result_date']
473
+ }
474
+ }
372
475
  end
373
476
  }
374
477
  end
@@ -387,19 +490,23 @@ module Lab
387
490
  nil
388
491
  end
389
492
 
390
- def make_void_test_params(tracking_number, test_name, voided_by, void_date = nil)
493
+ def make_void_test_params(_tracking_number, test, voided_by, void_date = nil)
391
494
  void_date ||= Time.now
392
495
 
393
496
  {
394
- tracking_number:,
395
- test_name:,
497
+ test_status: 'voided',
396
498
  time_updated: void_date,
397
- who_updated: {
398
- first_name: voided_by[:first_name],
399
- last_name: voided_by[:last_name],
400
- id_number: voided_by[:id]
499
+ test_type: {
500
+ name: ::Concept.find(test.concept_id).test_catalogue_name,
501
+ nlims_code: ::Concept.find(test.concept_id).nlims_code
401
502
  },
402
- test_status: 'voided'
503
+ status_trail: [
504
+ updated_by: {
505
+ first_name: voided_by[:first_name],
506
+ last_name: voided_by[:last_name],
507
+ id_number: voided_by[:id]
508
+ }
509
+ ]
403
510
  }
404
511
  end
405
512
 
@@ -446,4 +553,362 @@ module Lab
446
553
  end
447
554
  end
448
555
  end
556
+
557
+ def delete_order(_id, order_dto)
558
+ tracking_number = order_dto.fetch('tracking_number')
559
+
560
+ order_dto['tests'].each do |test|
561
+ Rails.logger.info("Voiding test '#{test}' (#{tracking_number}) in LIMS")
562
+ in_authenticated_session do |headers|
563
+ date_voided, voided_status = find_test_status(order_dto, test, 'Voided')
564
+ params = make_void_test_params(tracking_number, test, voided_status['updated_by'], date_voided)
565
+ RestClient.post(expand_uri('update_test'), params, headers)
566
+ end
567
+ end
568
+ end
569
+
570
+ def verify_tracking_number(tracking_number)
571
+ find_lims_order(tracking_number)
572
+ rescue InvalidParameters => e
573
+ Rails.logger.error("Failed to verify tracking number #{tracking_number}: #{e.message}")
574
+ false
575
+ end
576
+
577
+ private
578
+
579
+ attr_reader :config
580
+
581
+ MAX_LIMS_RETRIES = 5 # LIMS API Calls can only fail this number of times before we give up on it
582
+
583
+ ##
584
+ # Execute LIMS API calls within an authenticated session.
585
+ #
586
+ # Method automatically checks authenticates with LIMS if necessary and passes
587
+ # down the necessary headers for authentication to the REST call being made.
588
+ #
589
+ # Example:
590
+ #
591
+ # response = in_authenticated_session do |headers|
592
+ # RestClient.get(expand_uri('query_results_by_tracking_number/XXXXXX'), headers)
593
+ # end
594
+ #
595
+ # pp JSON.parse(response.body) if response.code == 200
596
+ def in_authenticated_session
597
+ retries ||= MAX_LIMS_RETRIES
598
+
599
+ self.authentication_token = authenticate unless authentication_token
600
+
601
+ response = yield 'token' => authentication_token, 'Content-type' => 'application/json'
602
+ check_response!(response)
603
+ rescue AuthenticationTokenExpired => e
604
+ self.authentication_token = nil
605
+ retry if (retries -= 1).positive?
606
+ rescue RestClient::ExceptionWithResponse => e
607
+ Rails.logger.error("LIMS Error: #{e.response&.code} - #{e.response&.body}")
608
+ raise e unless e.response&.code == 401
609
+
610
+ self.authentication_token = nil
611
+ retry if (retries -= 1).positive?
612
+ end
613
+
614
+ def authenticate
615
+ username = config.fetch(:username)
616
+ password = config.fetch(:password)
617
+
618
+ Rails.logger.debug("Authenticating with LIMS as: #{username}")
619
+ response = RestClient.get(expand_uri("re_authenticate/#{username}/#{password}"),
620
+ headers: { 'Content-type' => 'application/json' })
621
+ response_body = JSON.parse(response.body)
622
+
623
+ if response_body['status'] == 401
624
+ Rails.logger.error("Failed to authenticate with LIMS as #{config.fetch(:username)}: #{response_body['message']}")
625
+ raise LimsApiError, 'LIMS authentication failed'
626
+ end
627
+
628
+ response_body['data']['token']
629
+ end
630
+
631
+ def authentication_token=(token)
632
+ Thread.current[:lims_authentication_token] = token
633
+ end
634
+
635
+ def authentication_token
636
+ Thread.current[:lims_authentication_token]
637
+ end
638
+
639
+ ##
640
+ # Examines a response from LIMS to check if token has expired.
641
+ #
642
+ # LIMS' doesn't properly use HTTP status codes; the codes are embedded in the
643
+ # response body. 200 is used for success responses and 401 for everything else.
644
+ # We have this work around to examine the response body and
645
+ # throw errors accordingly. The following are the errors thrown:
646
+ #
647
+ # Lims::AuthenticationTokenExpired
648
+ # Lims::InvalidParameters
649
+ # Lims::ApiError - Thrown when we couldn't make sense of the error
650
+ def check_response!(response)
651
+ body = JSON.parse(response.body)
652
+ return response if body['status'] == 200
653
+
654
+ Rails.logger.error("Lims Api Error: #{response.body}")
655
+
656
+ raise LimsApiError, "#{body['status']} - #{body['message']}" if body['status'] != 401
657
+
658
+ if body['message'].match?(/token expired/i)
659
+ raise AuthenticationTokenExpired, "Authentication token expired: #{body['message']}"
660
+ end
661
+
662
+ raise InvalidParameters, body['message']
663
+ end
664
+
665
+ ##
666
+ # Takes a LIMS API relative URI and converts it to a full URL.
667
+ def expand_uri(uri, api_version: 'v1')
668
+ protocol = config.fetch(:protocol)
669
+ host = config.fetch(:host)
670
+ port = config.fetch(:port)
671
+ uri = uri.gsub(%r{^/+}, '')
672
+
673
+ "#{protocol}://#{host}:#{port}/api/#{api_version}/#{uri}"
674
+ end
675
+
676
+ ##
677
+ # Converts an OrderDto to parameters for POST /create_order
678
+ def make_create_params(order_dto)
679
+ {
680
+ tracking_number: order_dto.fetch(:tracking_number),
681
+ district: current_district,
682
+ health_facility_name: order_dto.fetch(:sending_facility),
683
+ first_name: order_dto.fetch(:patient).fetch(:first_name),
684
+ last_name: order_dto.fetch(:patient).fetch(:last_name),
685
+ phone_number: order_dto.fetch(:patient).fetch(:phone_number),
686
+ gender: order_dto.fetch(:patient).fetch(:gender),
687
+ arv_number: order_dto.fetch(:patient).fetch(:arv_number),
688
+ art_regimen: order_dto.fetch(:patient).fetch(:art_regimen),
689
+ art_start_date: order_dto.fetch(:patient).fetch(:art_start_date),
690
+ date_of_birth: order_dto.fetch(:patient).fetch(:dob),
691
+ national_patient_id: order_dto.fetch(:patient).fetch(:id),
692
+ requesting_clinician: requesting_clinician(order_dto),
693
+ sample_type: order_dto.fetch(:sample_type),
694
+ tests: order_dto.fetch(:tests),
695
+ date_sample_drawn: sample_drawn_date(order_dto),
696
+ sample_priority: order_dto.fetch(:priority) || 'Routine',
697
+ sample_status: order_dto.fetch(:sample_status),
698
+ target_lab: order_dto.fetch(:receiving_facility),
699
+ order_location: order_dto.fetch(:order_location) || 'Unknown',
700
+ who_order_test_first_name: order_dto.fetch(:who_order_test).fetch(:first_name),
701
+ who_order_test_last_name: order_dto.fetch(:who_order_test).fetch(:last_name)
702
+ }
703
+ end
704
+
705
+ ##
706
+ # Converts an OrderDto to parameters for POST /update_order
707
+ def make_update_params(order_dto)
708
+ date_updated, status = sample_drawn_status(order_dto)
709
+
710
+ {
711
+ tracking_number: order_dto.fetch(:tracking_number),
712
+ who_updated: status.fetch(:updated_by),
713
+ date_updated: date_updated,
714
+ specimen_type: order_dto.fetch(:sample_type),
715
+ status: 'specimen_collected'
716
+ }
717
+ end
718
+
719
+ def current_district
720
+ health_centre = Location.current_health_center
721
+ raise 'Current health centre not set' unless health_centre
722
+
723
+ district = health_centre.district || Lab::Lims::Config.application['district']
724
+
725
+ unless district
726
+ health_centre_name = "##{health_centre.id} - #{health_centre.name}"
727
+ raise "Current health centre district not set: #{health_centre_name}"
728
+ end
729
+
730
+ district
731
+ end
732
+
733
+ ##
734
+ # Extracts sample drawn status from an OrderDto
735
+ def sample_drawn_status(order_dto)
736
+ order_dto[:sample_statuses].each do |trail_entry|
737
+ date, status = trail_entry.each_pair.find { |_date, status| status['status'].casecmp?('Drawn') }
738
+ next unless date
739
+
740
+ return Date.strptime(date, '%Y%m%d%H%M%S').strftime('%Y-%m-%d'), status
741
+ end
742
+
743
+ [order_dto['date_created'], nil]
744
+ end
745
+
746
+ ##
747
+ # Extracts a sample drawn date from a LIMS OrderDto.
748
+ def sample_drawn_date(order_dto)
749
+ sample_drawn_status(order_dto).first
750
+ end
751
+
752
+ ##
753
+ # Extracts the requesting clinician from a LIMS OrderDto
754
+ def requesting_clinician(order_dto)
755
+ orderer = order_dto[:who_order_test]
756
+
757
+ "#{orderer[:first_name]} #{orderer[:last_name]}"
758
+ end
759
+
760
+ def find_lims_order(tracking_number)
761
+ response = in_authenticated_session do |headers|
762
+ Rails.logger.info("Fetching order ##{tracking_number}")
763
+ RestClient.get(expand_uri("query_order_by_tracking_number/#{tracking_number}"), headers)
764
+ end
765
+
766
+ Rails.logger.info("Order ##{tracking_number} found... Parsing...")
767
+ JSON.parse(response).fetch('data')
768
+ end
769
+
770
+ def find_lims_results(tracking_number)
771
+ response = in_authenticated_session do |headers|
772
+ Rails.logger.info("Fetching results for order ##{tracking_number}")
773
+ RestClient.get(expand_uri("query_results_by_tracking_number/#{tracking_number}"), headers)
774
+ end
775
+
776
+ Rails.logger.info("Result for order ##{tracking_number} found... Parsing...")
777
+ JSON.parse(response).fetch('data').fetch('results')
778
+ end
779
+
780
+ ##
781
+ # Make a copy of the order_dto with the results from LIMS parsed
782
+ # and appended to it.
783
+ def patch_order_dto_with_lims_results!(order_dto, results)
784
+ order_dto.merge!(
785
+ '_id' => order_dto[:tracking_number],
786
+ '_rev' => 0,
787
+ 'test_results' => results.each_with_object({}) do |result, formatted_results|
788
+ test_name, measures = result
789
+ result_date = measures.delete('result_date')
790
+
791
+ formatted_results[test_name] = {
792
+ results: measures.each_with_object({}) do |measure, processed_measures|
793
+ processed_measures[measure[0]] = { 'result_value' => measure[1] }
794
+ end,
795
+ result_date: result_date,
796
+ result_entered_by: {}
797
+ }
798
+ end
799
+ )
800
+ end
801
+
802
+ def patch_order_dto_with_lims_order!(order_dto, lims_order)
803
+ order_dto.merge!(
804
+ 'sample_type' => lims_order['other']['sample_type'],
805
+ 'sample_status' => lims_order['other']['specimen_status'],
806
+ 'priority' => lims_order['other']['priority']
807
+ )
808
+ end
809
+
810
+ def update_order_results(order_dto)
811
+ return nil if order_dto['test_results'].nil? || order_dto['test_results'].empty?
812
+
813
+ order_dto['test_results'].each do |test_name, results|
814
+ Rails.logger.info("Pushing result for order ##{order_dto['tracking_number']}")
815
+ in_authenticated_session do |headers|
816
+ params = make_update_test_params(order_dto['tracking_number'], test_name, results)
817
+
818
+ RestClient.post(expand_uri('update_test'), params, headers)
819
+ end
820
+ end
821
+ end
822
+
823
+ def make_update_test_params(tracking_number, test_name, results, test_status = 'Drawn')
824
+ {
825
+ tracking_number: tracking_number,
826
+ test_name: test_name,
827
+ result_date: results['result_date'],
828
+ time_updated: results['result_date'],
829
+ who_updated: {
830
+ first_name: results[:result_entered_by][:first_name],
831
+ last_name: results[:result_entered_by][:last_name],
832
+ id_number: results[:result_entered_by][:id]
833
+ },
834
+ test_status: test_status,
835
+ results: results['results']&.each_with_object({}) do |measure, formatted_results|
836
+ measure_name, measure_value = measure
837
+
838
+ formatted_results[measure_name] = measure_value['result_value']
839
+ end
840
+ }
841
+ end
842
+
843
+ def find_test_status(order_dto, target_test, target_status)
844
+ order_dto['test_statuses'].each do |test, statuses|
845
+ next unless test.casecmp?(target_test)
846
+
847
+ statuses.each do |date, status|
848
+ next unless status['status'].casecmp?(target_status)
849
+
850
+ return [Date.strptime(date, '%Y%m%d%H%M%S'), status]
851
+ end
852
+ end
853
+
854
+ nil
855
+ end
856
+
857
+ def make_void_test_params(tracking_number, test_name, voided_by, void_date = nil)
858
+ void_date ||= Time.now
859
+
860
+ {
861
+ tracking_number: tracking_number,
862
+ test_name: test_name,
863
+ time_updated: void_date,
864
+ who_updated: {
865
+ first_name: voided_by[:first_name],
866
+ last_name: voided_by[:last_name],
867
+ id_number: voided_by[:id]
868
+ },
869
+ test_status: 'voided'
870
+ }
871
+ end
872
+
873
+ def orders_pending_updates(patient_id = nil)
874
+ Rails.logger.info('Looking for orders that need to be updated...')
875
+ orders = {}
876
+
877
+ orders_without_specimen(patient_id).each { |order| orders[order.order_id] = order }
878
+ orders_without_results(patient_id).each { |order| orders[order.order_id] = order }
879
+ orders_without_reason(patient_id).each { |order| orders[order.order_id] = order }
880
+
881
+ orders.values
882
+ end
883
+
884
+ def orders_without_specimen(patient_id = nil)
885
+ Rails.logger.debug('Looking for orders without a specimen')
886
+ unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
887
+ .select(:concept_id)
888
+ orders = Lab::LabOrder.where(concept_id: unknown_specimen, date_created: START_DATE..(Date.today + 1.day))
889
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
890
+ orders = orders.where(patient_id: patient_id) if patient_id
891
+
892
+ orders
893
+ end
894
+
895
+ def orders_without_results(patient_id = nil)
896
+ Rails.logger.debug('Looking for orders without a result')
897
+ # Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
898
+ # .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id).where("pulled_at IS NULL"))
899
+ Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
900
+ .where(order_id: Lab::LimsOrderMapping.select(:order_id), date_created: START_DATE..(Date.today + 1.day))
901
+ end
902
+
903
+ def orders_without_reason(patient_id = nil)
904
+ Rails.logger.debug('Looking for orders without a reason for test')
905
+ orders = Lab::LabOrder.joins(:reason_for_test)
906
+ .merge(Observation.where(value_coded: nil, value_text: nil))
907
+ .where(date_created: START_DATE..(Date.today + 1.day))
908
+ .limit(1000)
909
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
910
+ orders = orders.where(patient_id: patient_id, date_created: START_DATE..(Date.today + 1.day)) if patient_id
911
+
912
+ orders
913
+ end
449
914
  end