his_emr_api_lab 2.1.1 → 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.
- checksums.yaml +4 -4
- data/app/controllers/lab/labels_controller.rb +2 -1
- data/app/controllers/lab/orders_controller.rb +14 -25
- data/app/controllers/lab/results_controller.rb +2 -1
- data/app/controllers/lab/specimen_types_controller.rb +0 -1
- data/app/controllers/lab/test_methods_controller.rb +9 -0
- data/app/controllers/lab/test_types_controller.rb +1 -1
- data/app/controllers/lab/tests_controller.rb +1 -3
- data/app/controllers/lab/users_controller.rb +2 -1
- data/app/models/lab/lab_result.rb +4 -0
- data/app/models/lab/lab_test.rb +1 -1
- data/app/models/lab/order_extension.rb +14 -0
- data/app/serializers/lab/lab_order_serializer.rb +20 -4
- data/app/serializers/lab/result_serializer.rb +1 -1
- data/app/serializers/lab/test_serializer.rb +24 -1
- data/app/services/lab/acknowledgement_service.rb +4 -1
- data/app/services/lab/concepts_service.rb +77 -22
- data/app/services/lab/lims/acknowledgement_worker.rb +1 -1
- data/app/services/lab/lims/api/rest_api.rb +543 -78
- data/app/services/lab/lims/config.rb +7 -2
- data/app/services/lab/lims/exceptions.rb +7 -6
- data/app/services/lab/lims/migrator.rb +3 -3
- data/app/services/lab/lims/order_dto.rb +1 -1
- data/app/services/lab/lims/order_serializer.rb +28 -7
- data/app/services/lab/lims/push_worker.rb +3 -3
- data/app/services/lab/lims/utils.rb +3 -4
- data/app/services/lab/lims/worker.rb +1 -1
- data/app/services/lab/metadata.rb +3 -0
- data/app/services/lab/notification_service.rb +72 -0
- data/app/services/lab/orders_search_service.rb +11 -5
- data/app/services/lab/orders_service.rb +79 -38
- data/app/services/lab/results_service.rb +23 -15
- data/app/services/lab/tests_service.rb +15 -3
- data/config/routes.rb +1 -0
- data/db/migrate/20260119104240_add_fulfiller_fields_to_orders.rb +11 -0
- data/db/migrate/20260119104241_create_comment_to_fulfiller_concept.rb +50 -0
- data/lib/lab/version.rb +1 -1
- data/lib/mahis_emr_api_lab.rb +6 -0
- metadata +11 -5
- /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 <
|
|
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('
|
|
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('
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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:
|
|
326
|
-
|
|
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' =>
|
|
338
|
-
'sample_status' =>
|
|
339
|
-
'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('
|
|
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(
|
|
454
|
+
def make_update_test_params(_tracking_number, test, results, test_status = 'Drawn')
|
|
357
455
|
{
|
|
358
|
-
|
|
359
|
-
test_name:,
|
|
360
|
-
result_date: results['result_date'],
|
|
456
|
+
test_status:,
|
|
361
457
|
time_updated: results['result_date'],
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
395
|
-
test_name:,
|
|
497
|
+
test_status: 'voided',
|
|
396
498
|
time_updated: void_date,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|