his_emr_api_lab 1.0.5 → 1.1.0

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: cecc609c02db9bc3bfef3b86458006055944bf3bc7729446490c5375c9ceabd4
4
- data.tar.gz: 9992f6d05a17f52c725dc014876a29faadc1b96ba2711f26851e73c917e44e61
3
+ metadata.gz: 69ffcbf9ab0cea24c83a5e18f9298af7599090009eea8f17f111c37333902b64
4
+ data.tar.gz: 2e4dab1daa94bfa9af75510691c40505468bfb4faaa5410eaaf6cf43ee47e8f8
5
5
  SHA512:
6
- metadata.gz: 4d37c96cfad06c9d7c8a62dab06ab9c4a2ecbb7ffed973287d09abfe5f7f3fb9d721ae5f473edc1aab7ba100cf4f525af329f8797e319cfb534f7141e96d12c3
7
- data.tar.gz: 2e88ceb111fa7a74be37c78d8b7df7a6b179f28c38ee9824d829097e8568c058ea9996afd2105fff44d61e51aa37184d495f77fd6ed6ee84e3ffd86419cc8e9f
6
+ metadata.gz: 951f366fed5cc8ac190a7b73bcf958a9f93eb233d724fb9b30d42b9f4b5de78985cd20a01cc262fa85a795fb4fdf86d3d8e912e11256b25b980605493319f736
7
+ data.tar.gz: da971e7cb0f69ea705fcf2b8f9a52d2101959183b20004c0330c2103ec49d03a3c16492b8e3908d3f8ffd13f128a444f6b011c54fdfd37fb982ab392b840a0f0
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,20 @@
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
+ lims_api = Lab::Lims::Api::RestApi.new
16
+ worker = Lab::Lims::Worker.new(lims_api)
17
+ worker.pull_orders(patient_id: patient_id)
18
+ end
19
+ end
20
+ 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,344 @@
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
+ update_order_results(order_dto)
26
+
27
+ data = JSON.parse(response.body)['data']
28
+
29
+ ActiveSupport::HashWithIndifferentAccess.new(
30
+ id: data['tracking_number'],
31
+ rev: 0,
32
+ tracking_number: data['tracking_number']
33
+ )
34
+ end
35
+
36
+ def update_order(_id, order_dto)
37
+ in_authenticated_session do |headers|
38
+ RestClient.post(expand_uri('update_order'), make_update_params(order_dto), headers)
39
+ end
40
+
41
+ update_order_results(order_dto)
42
+
43
+ { tracking_number: order_dto[:tracking_number] }
44
+ end
45
+
46
+ def consume_orders(*_args, patient_id: nil, **_kwargs)
47
+ Rails.logger.info('Looking for orders without results...')
48
+ orders = Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
49
+
50
+ orders.each do |order|
51
+ response = in_authenticated_session do |headers|
52
+ Rails.logger.info("Fetching results for order ##{order.accession_number}")
53
+ RestClient.get(expand_uri("query_results_by_tracking_number/#{order.accession_number}"), headers)
54
+ end
55
+
56
+ Rails.logger.info("Result for order ##{order.accession_number} found... Parsing...")
57
+ results = JSON.parse(response).fetch('data').fetch('results')
58
+ order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
59
+ yield lims_results_to_order_dto(results, order_dto), OpenStruct.new(last_seq: 0)
60
+ rescue InvalidParameters => e # LIMS responds with a 401 when a result is not found :(
61
+ Rails.logger.error("Failed to fetch results for ##{order.accession_number}: #{e.message}")
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}"), headers: { 'Content-type' => 'application/json' })
121
+ response_body = JSON.parse(response.body)
122
+
123
+ if response_body['status'] == 401
124
+ Rails.logger.error("Failed to authenticate with LIMS as #{config.fetch(:username)}: #{response_body['message']}")
125
+ raise LimsApiError, 'LIMS authentication failed'
126
+ end
127
+
128
+ response_body['data']['token']
129
+ end
130
+
131
+ def authentication_token=(token)
132
+ Thread.current[:lims_authentication_token] = token
133
+ end
134
+
135
+ def authentication_token
136
+ Thread.current[:lims_authentication_token]
137
+ end
138
+
139
+ ##
140
+ # Examines a response from LIMS to check if token has expired.
141
+ #
142
+ # LIMS' doesn't properly use HTTP status codes; the codes are embedded in the
143
+ # response body. 200 is used for success responses and 401 for everything else.
144
+ # We have this work around to examine the response body and
145
+ # throw errors accordingly. The following are the errors thrown:
146
+ #
147
+ # Lims::AuthenticationTokenExpired
148
+ # Lims::InvalidParameters
149
+ # Lims::ApiError - Thrown when we couldn't make sense of the error
150
+ def check_response!(response)
151
+ body = JSON.parse(response.body)
152
+ return response if body['status'] == 200
153
+
154
+ Rails.logger.error("Lims Api Error: #{response.body}")
155
+
156
+ if body['status'] != 401
157
+ raise LimsApiError, "#{body['status']} - #{body['message']}"
158
+ end
159
+
160
+ if body['message'].match?(/token expired/i)
161
+ raise AuthenticationTokenExpired, "Authentication token expired: #{body['message']}"
162
+ end
163
+
164
+ raise InvalidParameters, body['message']
165
+ end
166
+
167
+ ##
168
+ # Takes a LIMS API relative URI and converts it to a full URL.
169
+ def expand_uri(uri, api_version: 'v1')
170
+ protocol = config.fetch(:protocol)
171
+ host = config.fetch(:host)
172
+ port = config.fetch(:port)
173
+ uri = uri.gsub(%r{^/+}, '')
174
+
175
+ "#{protocol}://#{host}:#{port}/api/#{api_version}/#{uri}"
176
+ end
177
+
178
+ ##
179
+ # Converts an OrderDTO to parameters for POST /create_order
180
+ def make_create_params(order_dto)
181
+ {
182
+ tracking_number: order_dto.fetch(:tracking_number),
183
+ district: current_district,
184
+ health_facility_name: order_dto.fetch(:sending_facility),
185
+ first_name: order_dto.fetch(:patient).fetch(:first_name),
186
+ last_name: order_dto.fetch(:patient).fetch(:last_name),
187
+ phone_number: order_dto.fetch(:patient).fetch(:phone_number),
188
+ gender: order_dto.fetch(:patient).fetch(:gender),
189
+ national_patient_id: order_dto.fetch(:patient).fetch(:id),
190
+ requesting_clinician: requesting_clinician(order_dto),
191
+ sample_type: order_dto.fetch(:sample_type),
192
+ tests: order_dto.fetch(:tests),
193
+ date_sample_drawn: sample_drawn_date(order_dto),
194
+ sample_priority: order_dto.fetch(:priority) || 'Routine',
195
+ sample_status: order_dto.fetch(:sample_status),
196
+ target_lab: order_dto.fetch(:receiving_facility),
197
+ order_location: order_dto.fetch(:order_location) || 'Unknown',
198
+ who_order_test_first_name: order_dto.fetch(:who_order_test).fetch(:first_name),
199
+ who_order_test_last_name: order_dto.fetch(:who_order_test).fetch(:last_name)
200
+ }
201
+ end
202
+
203
+ ##
204
+ # Converts an OrderDTO to parameters for POST /update_order
205
+ def make_update_params(order_dto)
206
+ date_updated, status = sample_drawn_status(order_dto)
207
+
208
+ {
209
+ tracking_number: order_dto.fetch(:tracking_number),
210
+ who_updated: status.fetch(:updated_by),
211
+ date_updated: date_updated,
212
+ specimen_type: order_dto.fetch(:sample_type),
213
+ status: 'specimen_collected'
214
+ }
215
+ end
216
+
217
+ def current_district
218
+ health_centre = Location.current_health_center
219
+ raise 'Current health centre not set' unless health_centre
220
+
221
+ district = health_centre.district || Lab::Lims::Config.application['district']
222
+
223
+ unless district
224
+ health_centre_name = "##{health_centre.id} - #{health_centre.name}"
225
+ raise "Current health centre district not set: #{health_centre_name}"
226
+ end
227
+
228
+ district
229
+ end
230
+
231
+ ##
232
+ # Extracts sample drawn status from an OrderDTO
233
+ def sample_drawn_status(order_dto)
234
+ order_dto[:sample_statuses].each do |trail_entry|
235
+ date, status = trail_entry.each_pair.find { |_date, status| status['status'].casecmp?('Drawn') }
236
+ next unless date
237
+
238
+ return Date.strptime(date, '%Y%m%d%H%M%S').strftime('%Y-%m-%d'), status
239
+ end
240
+
241
+ [order_dto['date_created'], nil]
242
+ end
243
+
244
+ ##
245
+ # Extracts a sample drawn date from a LIMS OrderDTO.
246
+ def sample_drawn_date(order_dto)
247
+ sample_drawn_status(order_dto).first
248
+ end
249
+
250
+ ##
251
+ # Extracts the requesting clinician from a LIMS OrderDTO
252
+ def requesting_clinician(order_dto)
253
+ orderer = order_dto[:who_order_test]
254
+
255
+ "#{orderer[:first_name]} #{orderer[:last_name]}"
256
+ end
257
+
258
+ ##
259
+ # Make a copy of the order_dto with the results from LIMS parsed
260
+ # and appended to it.
261
+ def lims_results_to_order_dto(results, order_dto)
262
+ order_dto.merge(
263
+ '_id' => order_dto[:tracking_number],
264
+ '_rev' => 0,
265
+ 'test_results' => results.each_with_object({}) do |result, formatted_results|
266
+ test_name, measures = result
267
+ result_date = measures.delete('result_date')
268
+
269
+ formatted_results[test_name] = {
270
+ results: measures.each_with_object({}) do |measure, processed_measures|
271
+ processed_measures[measure[0]] = { 'result_value' => measure[1] }
272
+ end,
273
+ result_date: result_date,
274
+ result_entered_by: {}
275
+ }
276
+ end
277
+ )
278
+ end
279
+
280
+ def update_order_results(order_dto)
281
+ if order_dto['test_results'].nil? || order_dto['test_results'].empty?
282
+ return nil
283
+ end
284
+
285
+ order_dto['test_results'].each do |test_name, results|
286
+ Rails.logger.info("Pushing result for order ##{order_dto['tracking_number']}")
287
+ in_authenticated_session do |headers|
288
+ params = make_update_test_params(order_dto['tracking_number'], test_name, results)
289
+
290
+ RestClient.post(expand_uri('update_test'), params, headers)
291
+ end
292
+ end
293
+ end
294
+
295
+ def make_update_test_params(tracking_number, test_name, results, test_status = 'Drawn')
296
+ {
297
+ tracking_number: tracking_number,
298
+ test_name: test_name,
299
+ result_date: results['result_date'],
300
+ time_updated: results['result_date'],
301
+ who_updated: {
302
+ first_name: results[:result_entered_by][:first_name],
303
+ last_name: results[:result_entered_by][:last_name],
304
+ id_number: results[:result_entered_by][:id]
305
+ },
306
+ test_status: test_status,
307
+ results: results['results']&.each_with_object({}) do |measure, formatted_results|
308
+ measure_name, measure_value = measure
309
+
310
+ formatted_results[measure_name] = measure_value['result_value']
311
+ end
312
+ }
313
+ end
314
+
315
+ def find_test_status(order_dto, target_test, target_status)
316
+ order_dto['test_statuses'].each do |test, statuses|
317
+ next unless test.casecmp?(target_test)
318
+
319
+ statuses.each do |date, status|
320
+ next unless status['status'].casecmp?(target_status)
321
+
322
+ return [Date.strptime(date, '%Y%m%d%H%M%S'), status]
323
+ end
324
+ end
325
+
326
+ nil
327
+ end
328
+
329
+ def make_void_test_params(tracking_number, test_name, voided_by, void_date = nil)
330
+ void_date ||= Time.now
331
+
332
+ {
333
+ tracking_number: tracking_number,
334
+ test_name: test_name,
335
+ time_updated: void_date,
336
+ who_updated: {
337
+ first_name: voided_by[:first_name],
338
+ last_name: voided_by[:last_name],
339
+ id_number: voided_by[:id]
340
+ },
341
+ test_status: 'voided'
342
+ }
343
+ end
344
+ end