his_emr_api_lab 1.0.4 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6142a8351eea3dc6cc2dde34b014948bd95c5d4398355ddc29d2aa0ddff8c095
4
- data.tar.gz: ca4493c370000ab13cae02629c8cc3ea840f9bf03a70dee35a3f8bc3574e12e3
3
+ metadata.gz: e2202cc31522b4ae92847b3102841cfcf0c65a9e389fde480e3e787f1ef49ec7
4
+ data.tar.gz: a82291c66cdd6fb651e0917dc68e5b28b6dcedeb124973ab57f9f60187b4c920
5
5
  SHA512:
6
- metadata.gz: b18abf5a9f7e00c213ff41bea37c924f7c5abafbfefd5f63727c76093d26e057391ddb047e4a2583fbfdb9d98bd05895387cc6b26aacfe54df4a290cb804f9b9
7
- data.tar.gz: e7e23b2a1eb5d387855e7acc7e1bd60dcdfca08938245693a07833a6fc43314e1127a9108eaaf8e1536fa3e16acaaa74ecf104d107b3fb71a98ad9b0c4b8a875
6
+ metadata.gz: 7da2c231dba9fa4b013890becd128b0a8694fe797b510e9baa37638d1437133cc11e27e2479938eda921ccfe6c23583544092cb477372b5db298922913035c72
7
+ data.tar.gz: cb8fbf9e9c71d187e0085b42f0fe5299976dc4309d672c815c231cdb096548d9b53b9b0f222f453b66a41a8d18b759f2d8d295c46c899206d7c7ffcee500bb6c
data/README.md CHANGED
@@ -43,9 +43,28 @@ Finally run:
43
43
  $ bundle exec rails lab:install
44
44
  ```
45
45
 
46
+ ## Configuration
47
+
48
+ This module in most cases should work without any configuration, however to enable
49
+ certain features some configuration may be required. Visit the
50
+ [configuration](./docs/configuration.md) page to learn how to configure the
51
+ application.
52
+
46
53
  ## Contributing
47
54
 
48
- Contribution directions go here.
55
+ Fork this application create a branch for the contribution you want to make,
56
+ push your changes to the branch and then issue a pull request. You may want
57
+ to create a new first on our repository, so that your pull request references
58
+ this issue.
59
+
60
+ If you are fixing a bug, it will be nice to add a unit test that exposes
61
+ the bug. Although this is not a requirement in most cases.
62
+
63
+ Be sure to follow [this](https://github.com/rubocop/ruby-style-guide) Ruby
64
+ style guide. We don't necessarily look for strict adherence to the guidelines
65
+ but too much a departure from it is frowned upon. For example, you will be forgiven
66
+ for writing a method with 15 to 20 lines if you clearly justify why you couldn't
67
+ break that method into multiple smaller methods.
49
68
 
50
69
  ## License
51
70
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Fetches updates on a patient's orders from external sources.
6
+ class UpdatePatientOrdersJob < ApplicationJob
7
+ queue_as :default
8
+
9
+ def perform(patient_id)
10
+ Rails.logger.info('Initialising LIMS REST API...')
11
+
12
+ User.current = Lab::Lims::Utils.lab_user
13
+ Location.current = Location.find_by_name('ART clinic')
14
+
15
+ lockfile = Rails.root.join('tmp', "update-patient-orders-#{patient_id}.lock")
16
+
17
+ done = File.open(lockfile, File::RDWR | File::CREAT) do |lock|
18
+ unless lock.flock(File::LOCK_NB | File::LOCK_EX)
19
+ Rails.logger.info('Another update patient job is already running...')
20
+ break false
21
+ end
22
+
23
+ lims_api = Lab::Lims::Api::RestApi.new(Lab::Lims::Config.rest_api)
24
+ worker = Lab::Lims::PullWorker.new(lims_api)
25
+ worker.pull_orders(patient_id: patient_id)
26
+
27
+ true
28
+ end
29
+
30
+ File.unlink(lockfile) if done
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class VoidOrderJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(order_id)
8
+ Rails.logger.info("Voiding order ##{order_id} in LIMS")
9
+
10
+ User.current = Lab::Lims::Utils.lab_user
11
+ Location.current = Location.find_by_name('ART clinic')
12
+
13
+ lims_api = Lab::Lims::Api::RestApi.new
14
+ worker = Lab::Lims::Worker.new(lims_api)
15
+ worker.push_order(Lab::LabOrder.unscoped.find(order_id))
16
+ end
17
+ end
18
+ end
@@ -41,6 +41,10 @@ module Lab
41
41
  .where.not(concept_id: ConceptName.where(name: 'Tests ordered').select(:concept_id))
42
42
  end
43
43
 
44
+ scope :drawn, -> { where.not(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
45
+
46
+ scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
47
+
44
48
  def self.prefetch_relationships
45
49
  includes(:reason_for_test,
46
50
  :requesting_clinician,
@@ -3,7 +3,7 @@
3
3
  module Lab
4
4
  module LabOrderSerializer
5
5
  def self.serialize_order(order, tests: nil, requesting_clinician: nil, reason_for_test: nil, target_lab: nil)
6
- tests ||= order.tests
6
+ tests ||= order.voided == 1 ? voided_tests(order) : order.tests
7
7
  requesting_clinician ||= order.requesting_clinician
8
8
  reason_for_test ||= order.reason_for_test
9
9
  target_lab ||= order.target_lab
@@ -45,5 +45,11 @@ module Lab
45
45
 
46
46
  ConceptName.select(:name).find_by_concept_id(concept_id)&.name
47
47
  end
48
+
49
+ def self.voided_tests(order)
50
+ concept = ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
51
+ .select(:concept_id)
52
+ LabTest.unscoped.where(concept: concept, order: order, voided: true)
53
+ end
48
54
  end
49
55
  end
@@ -0,0 +1,405 @@
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
+ end
63
+ end
64
+
65
+ def delete_order(_id, order_dto)
66
+ tracking_number = order_dto.fetch('tracking_number')
67
+
68
+ order_dto['tests'].each do |test|
69
+ Rails.logger.info("Voiding test '#{test}' (#{tracking_number}) in LIMS")
70
+ in_authenticated_session do |headers|
71
+ date_voided, voided_status = find_test_status(order_dto, test, 'Voided')
72
+ params = make_void_test_params(tracking_number, test, voided_status['updated_by'], date_voided)
73
+ RestClient.post(expand_uri('update_test'), params, headers)
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :config
81
+
82
+ MAX_LIMS_RETRIES = 5 # LIMS API Calls can only fail this number of times before we give up on it
83
+
84
+ ##
85
+ # Execute LIMS API calls within an authenticated session.
86
+ #
87
+ # Method automatically checks authenticates with LIMS if necessary and passes
88
+ # down the necessary headers for authentication to the REST call being made.
89
+ #
90
+ # Example:
91
+ #
92
+ # response = in_authenticated_session do |headers|
93
+ # RestClient.get(expand_uri('query_results_by_tracking_number/XXXXXX'), headers)
94
+ # end
95
+ #
96
+ # pp JSON.parse(response.body) if response.code == 200
97
+ def in_authenticated_session
98
+ retries ||= MAX_LIMS_RETRIES
99
+
100
+ self.authentication_token = authenticate unless authentication_token
101
+
102
+ response = yield 'token' => authentication_token, 'Content-type' => 'application/json'
103
+ check_response!(response)
104
+ rescue AuthenticationTokenExpired => e
105
+ self.authentication_token = nil
106
+ retry if (retries -= 1).positive?
107
+ rescue RestClient::ExceptionWithResponse => e
108
+ Rails.logger.error("LIMS Error: #{e.response&.code} - #{e.response&.body}")
109
+ raise e unless e.response&.code == 401
110
+
111
+ self.authentication_token = nil
112
+ retry if (retries -= 1).positive?
113
+ end
114
+
115
+ def authenticate
116
+ username = config.fetch(:username)
117
+ password = config.fetch(:password)
118
+
119
+ Rails.logger.debug("Authenticating with LIMS as: #{username}")
120
+ response = RestClient.get(expand_uri("re_authenticate/#{username}/#{password}"),
121
+ headers: { 'Content-type' => 'application/json' })
122
+ response_body = JSON.parse(response.body)
123
+
124
+ if response_body['status'] == 401
125
+ Rails.logger.error("Failed to authenticate with LIMS as #{config.fetch(:username)}: #{response_body['message']}")
126
+ raise LimsApiError, 'LIMS authentication failed'
127
+ end
128
+
129
+ response_body['data']['token']
130
+ end
131
+
132
+ def authentication_token=(token)
133
+ Thread.current[:lims_authentication_token] = token
134
+ end
135
+
136
+ def authentication_token
137
+ Thread.current[:lims_authentication_token]
138
+ end
139
+
140
+ ##
141
+ # Examines a response from LIMS to check if token has expired.
142
+ #
143
+ # LIMS' doesn't properly use HTTP status codes; the codes are embedded in the
144
+ # response body. 200 is used for success responses and 401 for everything else.
145
+ # We have this work around to examine the response body and
146
+ # throw errors accordingly. The following are the errors thrown:
147
+ #
148
+ # Lims::AuthenticationTokenExpired
149
+ # Lims::InvalidParameters
150
+ # Lims::ApiError - Thrown when we couldn't make sense of the error
151
+ def check_response!(response)
152
+ body = JSON.parse(response.body)
153
+ return response if body['status'] == 200
154
+
155
+ Rails.logger.error("Lims Api Error: #{response.body}")
156
+
157
+ raise LimsApiError, "#{body['status']} - #{body['message']}" if body['status'] != 401
158
+
159
+ if body['message'].match?(/token expired/i)
160
+ raise AuthenticationTokenExpired, "Authentication token expired: #{body['message']}"
161
+ end
162
+
163
+ raise InvalidParameters, body['message']
164
+ end
165
+
166
+ ##
167
+ # Takes a LIMS API relative URI and converts it to a full URL.
168
+ def expand_uri(uri, api_version: 'v1')
169
+ protocol = config.fetch(:protocol)
170
+ host = config.fetch(:host)
171
+ port = config.fetch(:port)
172
+ uri = uri.gsub(%r{^/+}, '')
173
+
174
+ "#{protocol}://#{host}:#{port}/api/#{api_version}/#{uri}"
175
+ end
176
+
177
+ ##
178
+ # Converts an OrderDTO to parameters for POST /create_order
179
+ def make_create_params(order_dto)
180
+ {
181
+ tracking_number: order_dto.fetch(:tracking_number),
182
+ district: current_district,
183
+ health_facility_name: order_dto.fetch(:sending_facility),
184
+ first_name: order_dto.fetch(:patient).fetch(:first_name),
185
+ last_name: order_dto.fetch(:patient).fetch(:last_name),
186
+ phone_number: order_dto.fetch(:patient).fetch(:phone_number),
187
+ gender: order_dto.fetch(:patient).fetch(:gender),
188
+ national_patient_id: order_dto.fetch(:patient).fetch(:id),
189
+ requesting_clinician: requesting_clinician(order_dto),
190
+ sample_type: order_dto.fetch(:sample_type),
191
+ tests: order_dto.fetch(:tests),
192
+ date_sample_drawn: sample_drawn_date(order_dto),
193
+ sample_priority: order_dto.fetch(:priority) || 'Routine',
194
+ sample_status: order_dto.fetch(:sample_status),
195
+ target_lab: order_dto.fetch(:receiving_facility),
196
+ order_location: order_dto.fetch(:order_location) || 'Unknown',
197
+ who_order_test_first_name: order_dto.fetch(:who_order_test).fetch(:first_name),
198
+ who_order_test_last_name: order_dto.fetch(:who_order_test).fetch(:last_name)
199
+ }
200
+ end
201
+
202
+ ##
203
+ # Converts an OrderDTO to parameters for POST /update_order
204
+ def make_update_params(order_dto)
205
+ date_updated, status = sample_drawn_status(order_dto)
206
+
207
+ {
208
+ tracking_number: order_dto.fetch(:tracking_number),
209
+ who_updated: status.fetch(:updated_by),
210
+ date_updated: date_updated,
211
+ specimen_type: order_dto.fetch(:sample_type),
212
+ status: 'specimen_collected'
213
+ }
214
+ end
215
+
216
+ def current_district
217
+ health_centre = Location.current_health_center
218
+ raise 'Current health centre not set' unless health_centre
219
+
220
+ district = health_centre.district || Lab::Lims::Config.application['district']
221
+
222
+ unless district
223
+ health_centre_name = "##{health_centre.id} - #{health_centre.name}"
224
+ raise "Current health centre district not set: #{health_centre_name}"
225
+ end
226
+
227
+ district
228
+ end
229
+
230
+ ##
231
+ # Extracts sample drawn status from an OrderDTO
232
+ def sample_drawn_status(order_dto)
233
+ order_dto[:sample_statuses].each do |trail_entry|
234
+ date, status = trail_entry.each_pair.find { |_date, status| status['status'].casecmp?('Drawn') }
235
+ next unless date
236
+
237
+ return Date.strptime(date, '%Y%m%d%H%M%S').strftime('%Y-%m-%d'), status
238
+ end
239
+
240
+ [order_dto['date_created'], nil]
241
+ end
242
+
243
+ ##
244
+ # Extracts a sample drawn date from a LIMS OrderDTO.
245
+ def sample_drawn_date(order_dto)
246
+ sample_drawn_status(order_dto).first
247
+ end
248
+
249
+ ##
250
+ # Extracts the requesting clinician from a LIMS OrderDTO
251
+ def requesting_clinician(order_dto)
252
+ orderer = order_dto[:who_order_test]
253
+
254
+ "#{orderer[:first_name]} #{orderer[:last_name]}"
255
+ end
256
+
257
+ def find_lims_order(tracking_number)
258
+ response = in_authenticated_session do |headers|
259
+ Rails.logger.info("Fetching order ##{tracking_number}")
260
+ RestClient.get(expand_uri("query_order_by_tracking_number/#{tracking_number}"), headers)
261
+ end
262
+
263
+ Rails.logger.info("Order ##{tracking_number} found... Parsing...")
264
+ JSON.parse(response).fetch('data')
265
+ end
266
+
267
+ def find_lims_results(tracking_number)
268
+ response = in_authenticated_session do |headers|
269
+ Rails.logger.info("Fetching results for order ##{tracking_number}")
270
+ RestClient.get(expand_uri("query_results_by_tracking_number/#{tracking_number}"), headers)
271
+ end
272
+
273
+ Rails.logger.info("Result for order ##{tracking_number} found... Parsing...")
274
+ JSON.parse(response).fetch('data').fetch('results')
275
+ end
276
+
277
+ ##
278
+ # Make a copy of the order_dto with the results from LIMS parsed
279
+ # and appended to it.
280
+ def patch_order_dto_with_lims_results!(order_dto, results)
281
+ order_dto.merge!(
282
+ '_id' => order_dto[:tracking_number],
283
+ '_rev' => 0,
284
+ 'test_results' => results.each_with_object({}) do |result, formatted_results|
285
+ test_name, measures = result
286
+ result_date = measures.delete('result_date')
287
+
288
+ formatted_results[test_name] = {
289
+ results: measures.each_with_object({}) do |measure, processed_measures|
290
+ processed_measures[measure[0]] = { 'result_value' => measure[1] }
291
+ end,
292
+ result_date: result_date,
293
+ result_entered_by: {}
294
+ }
295
+ end
296
+ )
297
+ end
298
+
299
+ def patch_order_dto_with_lims_order!(order_dto, lims_order)
300
+ order_dto.merge!(
301
+ 'sample_type' => lims_order['other']['sample_type'],
302
+ 'sample_status' => lims_order['other']['specimen_status'],
303
+ 'priority' => lims_order['other']['priority']
304
+ )
305
+ end
306
+
307
+ def update_order_results(order_dto)
308
+ return nil if order_dto['test_results'].nil? || order_dto['test_results'].empty?
309
+
310
+ order_dto['test_results'].each do |test_name, results|
311
+ Rails.logger.info("Pushing result for order ##{order_dto['tracking_number']}")
312
+ in_authenticated_session do |headers|
313
+ params = make_update_test_params(order_dto['tracking_number'], test_name, results)
314
+
315
+ RestClient.post(expand_uri('update_test'), params, headers)
316
+ end
317
+ end
318
+ end
319
+
320
+ def make_update_test_params(tracking_number, test_name, results, test_status = 'Drawn')
321
+ {
322
+ tracking_number: tracking_number,
323
+ test_name: test_name,
324
+ result_date: results['result_date'],
325
+ time_updated: results['result_date'],
326
+ who_updated: {
327
+ first_name: results[:result_entered_by][:first_name],
328
+ last_name: results[:result_entered_by][:last_name],
329
+ id_number: results[:result_entered_by][:id]
330
+ },
331
+ test_status: test_status,
332
+ results: results['results']&.each_with_object({}) do |measure, formatted_results|
333
+ measure_name, measure_value = measure
334
+
335
+ formatted_results[measure_name] = measure_value['result_value']
336
+ end
337
+ }
338
+ end
339
+
340
+ def find_test_status(order_dto, target_test, target_status)
341
+ order_dto['test_statuses'].each do |test, statuses|
342
+ next unless test.casecmp?(target_test)
343
+
344
+ statuses.each do |date, status|
345
+ next unless status['status'].casecmp?(target_status)
346
+
347
+ return [Date.strptime(date, '%Y%m%d%H%M%S'), status]
348
+ end
349
+ end
350
+
351
+ nil
352
+ end
353
+
354
+ def make_void_test_params(tracking_number, test_name, voided_by, void_date = nil)
355
+ void_date ||= Time.now
356
+
357
+ {
358
+ tracking_number: tracking_number,
359
+ test_name: test_name,
360
+ time_updated: void_date,
361
+ who_updated: {
362
+ first_name: voided_by[:first_name],
363
+ last_name: voided_by[:last_name],
364
+ id_number: voided_by[:id]
365
+ },
366
+ test_status: 'voided'
367
+ }
368
+ end
369
+
370
+ def orders_pending_updates(patient_id = nil)
371
+ Rails.logger.info('Looking for orders that need to be updated...')
372
+ orders = {}
373
+
374
+ orders_without_specimen(patient_id).each { |order| orders[order.order_id] = order }
375
+ orders_without_results(patient_id).each { |order| orders[order.order_id] = order }
376
+ orders_without_reason(patient_id).each { |order| orders[order.order_id] = order }
377
+
378
+ orders.values
379
+ end
380
+
381
+ def orders_without_specimen(patient_id = nil)
382
+ Rails.logger.debug('Looking for orders without a specimen')
383
+ unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
384
+ .select(:concept_id)
385
+ orders = Lab::LabOrder.where(concept_id: unknown_specimen)
386
+ orders = orders.where(patient_id: patient_id) if patient_id
387
+
388
+ orders
389
+ end
390
+
391
+ def orders_without_results(patient_id = nil)
392
+ Rails.logger.debug('Looking for orders without a result')
393
+ Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
394
+ end
395
+
396
+ def orders_without_reason(patient_id = nil)
397
+ Rails.logger.debug('Looking for orders without a reason for test')
398
+ orders = Lab::LabOrder.joins(:reason_for_test)
399
+ .merge(Observation.where(value_coded: nil, value_text: nil))
400
+ .limit(1000)
401
+ orders = orders.where(patient_id: patient_id) if patient_id
402
+
403
+ orders
404
+ end
405
+ end