his_emr_api_lab 1.1.29 → 1.1.31

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: e8830688d3c9f3a5c1eaf915060e61c89110b7602f44233d9a6d6c87628d7bfa
4
- data.tar.gz: 394f57a548c41b25303ea9cf6f7cd205094a18c2c1b471d847d7d02a7828487a
3
+ metadata.gz: 88d310815f8d7efad226f84e50963754893da11220df2af7aedf173361452091
4
+ data.tar.gz: d01062b0a5ec36c1a1710d795a128ca810c265451f46c95b663cd47158667263
5
5
  SHA512:
6
- metadata.gz: 8061088cf2210df529f524d410441f647386402ae0c706953d5dbf4b2a439b48c3d9377180b028ba5d456b5598f8b6c75c0a598f8550732785ce53853eafdb15
7
- data.tar.gz: 06a06e00a9051548ee69387fa5c9078cb86d119968a5ef81a37fbddc3e60147fe57b743232aadc20fb7f9adbfa74caddd8f8eb1f6f05bb56219f96ca0762ccdd
6
+ metadata.gz: 881a7f78e83513207822d69980a884d19dd4b328b89192caa96b8d536e79a637e6c21bf1a07231e7e03a19464ec3693d03233dfbda1b0974cf41a7195a5882ff
7
+ data.tar.gz: 186e472eeb17f75bf7be3cac3478701f6b9b34d092da3368b96e9410f7522e0bdfaaec26099d919ed2a88366de8e57e755f492d52a16b50a584e2193e82f4887
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Lab
4
4
  class OrdersController < ApplicationController
5
+ skip_before_action :authenticate, only: %i[order_status order_result]
6
+ before_action :authenticate_request, only: %i[order_status order_result]
7
+
5
8
  def create
6
9
  order_params_list = params.require(:orders)
7
10
  orders = order_params_list.map do |order_params|
@@ -39,5 +42,48 @@ module Lab
39
42
 
40
43
  render status: :no_content
41
44
  end
45
+
46
+ def order_status
47
+ order_params = params.permit(:tracking_number, :status, :status_time, :comments)
48
+ OrdersService.update_order_status(order_params)
49
+ render json: { message: "Status for order #{order_params['tracking_number']} successfully updated" }, status: :ok
50
+ end
51
+
52
+ def order_result
53
+ params.permit!
54
+ order_params = params[:data].to_h
55
+ OrdersService.update_order_result(order_params)
56
+ render json: { message: 'Results processed successfully' }, status: :ok
57
+ end
58
+
59
+ private
60
+
61
+ def authenticate_request
62
+ header = request.headers['Authorization']
63
+ content = header.split(' ')
64
+ auth_scheme = content.first
65
+ unless header
66
+ errors = ['Authorization token required']
67
+ render json: { errors: errors }, status: :unauthorized
68
+ return false
69
+ end
70
+ unless auth_scheme == 'Bearer'
71
+ errors = ['Authorization token bearer scheme required']
72
+ render json: { errors: errors }, status: :unauthorized
73
+ return false
74
+ end
75
+
76
+ process_token(content.last)
77
+ end
78
+
79
+ def process_token(token)
80
+ browser = Browser.new(request.user_agent)
81
+ decoded = Lab::JsonWebTokenService.decode(token, request.remote_ip + browser.name + browser.version)
82
+ user(decoded)
83
+ end
84
+
85
+ def user(decoded)
86
+ User.current = User.find decoded[:user_id]
87
+ end
42
88
  end
43
89
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This controller handles creation and authentication of LIMS User
4
+ module Lab
5
+ class UsersController < ::ApplicationController
6
+ skip_before_action :authenticate
7
+ # create a LIMS User that will be responsible for sending lab results
8
+ def create
9
+ user_params = params.permit(:username, :password)
10
+ service.create_lims_user(username: user_params['username'], password: user_params['password'])
11
+ render json: { message: 'User successfully created' }, status: 200
12
+ end
13
+
14
+ # authenticate the lims user
15
+ def login
16
+ user_params = params.permit(:username, :password)
17
+ result = service.authenticate_user(username: user_params['username'], password: user_params['password'],
18
+ user_agent: request.user_agent, request_ip: request.remote_ip)
19
+ if result.present?
20
+ render json: result, status: 200
21
+ else
22
+ render json: { message: 'Invalid Credentials Provided' }, status: 401
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def service
29
+ Lab::UserService
30
+ end
31
+ end
32
+ end
@@ -26,6 +26,7 @@ module Lab
26
26
  concept_id: reason_for_test&.value_coded,
27
27
  name: concept_name(reason_for_test&.value_coded)
28
28
  },
29
+ delivery_mode: order&.lims_acknowledgement_status&.acknowledgement_type,
29
30
  tests: tests.map do |test|
30
31
  result_obs = test.children.first
31
32
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # This class is used to encode and decode the JWT token
5
+ module JsonWebTokenService
6
+ class << self
7
+ SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
8
+
9
+ def encode(payload, request_ip, exp = 18.hours.from_now)
10
+ payload[:exp] = exp.to_i
11
+ JWT.encode(payload, SECRET_KEY + request_ip)
12
+ end
13
+
14
+ def decode(token, request_ip)
15
+ decoded = JWT.decode(token, SECRET_KEY + request_ip)[0]
16
+ HashWithIndifferentAccess.new decoded
17
+ end
18
+ end
19
+ end
20
+ end
@@ -57,6 +57,29 @@ module Lab
57
57
  end
58
58
  end
59
59
 
60
+ def process_order(order_dto)
61
+ patient = find_patient_by_nhid(order_dto[:patient][:id])
62
+ unless patient
63
+ logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
64
+ order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
65
+ return
66
+ end
67
+
68
+ if order_dto[:tests].empty?
69
+ logger.debug("Discarding order: Missing tests on order ##{order_dto[:tracking_number]}")
70
+ order_rejected(order_dto, 'Order is missing tests')
71
+ return
72
+ end
73
+
74
+ diff = match_patient_demographics(patient, order_dto['patient'])
75
+ if diff.empty?
76
+ save_order(patient, order_dto)
77
+ order_saved(order_dto)
78
+ else
79
+ save_failed_import(order_dto, 'Demographics not matching', diff)
80
+ end
81
+ end
82
+
60
83
  protected
61
84
 
62
85
  def order_saved(order_dto); end
@@ -182,30 +205,32 @@ module Lab
182
205
 
183
206
  def update_results(order, lims_results)
184
207
  logger.debug("Updating results for order ##{order[:accession_number]}: #{lims_results}")
185
-
208
+
186
209
  lims_results.each do |test_name, test_results|
187
210
  test = find_test(order['id'], test_name)
188
211
  unless test
189
212
  logger.warn("Couldn't find test, #{test_name}, in order ##{order[:id]}")
190
213
  next
191
214
  end
192
-
193
- next if test.result || test_results['results'].blank?
194
215
 
216
+ next if test.result || test_results['results'].blank?
217
+
218
+ result_date = Time.now
195
219
  measures = test_results['results'].map do |indicator, value|
196
220
  measure = find_measure(order, indicator, value)
221
+ result_date = value['result_date'] || result_date
197
222
  next nil unless measure
198
223
 
199
224
  measure
200
225
  end
201
-
226
+
202
227
  measures = measures.compact
203
228
  next if measures.empty?
204
229
 
205
230
  creator = format_result_entered_by(test_results['result_entered_by'])
206
231
 
207
232
  ResultsService.create_results(test.id, { provider_id: User.current.person_id,
208
- date: Utils.parse_date(test_results['date_result_entered'], order[:order_date].to_s),
233
+ date: Utils.parse_date(test_results['date_result_entered'], result_date),
209
234
  comments: "LIMS import: Entered by: #{creator}",
210
235
  measures: measures } )
211
236
  end
@@ -10,6 +10,7 @@ module Lab
10
10
  TEST_RESULT_CONCEPT_NAME = 'Lab test result'
11
11
  TEST_RESULT_INDICATOR_CONCEPT_NAME = 'Lab test result indicator'
12
12
  TEST_TYPE_CONCEPT_NAME = 'Test type'
13
+ LAB_ORDER_STATUS_CONCEPT_NAME = 'lab order status'
13
14
  UNKNOWN_SPECIMEN = 'Unknown'
14
15
 
15
16
  # Encounter
@@ -103,17 +103,84 @@ module Lab
103
103
  order.target_lab&.void(reason)
104
104
 
105
105
  order.tests.each { |test| test.void(reason) }
106
- voided = order.void(reason)
107
-
108
- voided
106
+ order.void(reason)
109
107
  end
110
108
 
111
109
  def check_tracking_number(tracking_number)
112
110
  accession_number_exists?(tracking_number) || nlims_accession_number_exists?(tracking_number)
113
111
  end
114
112
 
113
+ def update_order_status(order_params)
114
+ # find the order
115
+ order = find_order(order_params['tracking_number'])
116
+ concept = ConceptName.find_by_name Lab::Metadata::LAB_ORDER_STATUS_CONCEPT_NAME
117
+ ActiveRecord::Base.transaction do
118
+ void_order_status(order, concept)
119
+ Observation.create!(
120
+ person_id: order.patient_id,
121
+ encounter_id: order.encounter_id,
122
+ concept_id: concept.concept_id,
123
+ order_id: order.id,
124
+ obs_datetime: order_params['status_time'] || Time.now,
125
+ value_text: order_params['status'],
126
+ creator: User.current.id
127
+ )
128
+ end
129
+ create_rejection_notification(order_params) if order_params['status'] == 'test-rejected'
130
+ end
131
+
132
+ def update_order_result(order_params)
133
+ order = find_order(order_params['tracking_number'])
134
+ order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
135
+ patch_order_dto_with_lims_results!(order_dto, order_params['results'])
136
+ Lab::Lims::PullWorker.new(nil).process_order(order_dto)
137
+ end
138
+
115
139
  private
116
140
 
141
+ def create_rejection_notification(order_params)
142
+ order = find_order order_params['tracking_number']
143
+ data = { 'type': 'LIMS',
144
+ 'accession_number': order&.accession_number,
145
+ 'order_date': order&.start_date,
146
+ 'arv_number': find_arv_number(order.patient_id),
147
+ 'patient_id': result.person_id,
148
+ 'ordered_by': order&.provider&.person&.name,
149
+ 'rejection_reason': order_params['comments']
150
+ }.as_json
151
+ NotificationService.new.create_notification('LIMS', data)
152
+ end
153
+
154
+ def find_arv_number(patient_id)
155
+ PatientIdentifier.joins(:type)
156
+ .merge(PatientIdentifierType.where(name: 'ARV Number'))
157
+ .where(patient_id: patient_id)
158
+ .first&.identifier
159
+ end
160
+
161
+ def find_order(tracking_number)
162
+ Lab::LabOrder.find_by_accession_number(tracking_number)
163
+ end
164
+
165
+ def patch_order_dto_with_lims_results!(order_dto, results)
166
+ order_dto.merge!(
167
+ '_id' => order_dto[:tracking_number],
168
+ '_rev' => 0,
169
+ 'test_results' => results.each_with_object({}) do |result, formatted_results|
170
+ test_name, measures = result
171
+ result_date = measures.delete('result_date')
172
+
173
+ formatted_results[test_name] = {
174
+ results: measures.each_with_object({}) do |measure, processed_measures|
175
+ processed_measures[measure[0]] = { 'result_value' => measure[1] }
176
+ end,
177
+ result_date: result_date,
178
+ result_entered_by: {}
179
+ }
180
+ end
181
+ )
182
+ end
183
+
117
184
  ##
118
185
  # Extract an encounter from the given parameters.
119
186
  #
@@ -228,6 +295,12 @@ module Lab
228
295
  order.reason_for_test&.delete
229
296
  add_reason_for_test(order, date: order.start_date, reason_for_test_id: concept_id)
230
297
  end
298
+
299
+ def void_order_status(order, concept)
300
+ Observation.where(order_id: order.id, concept_id: concept.concept_id).each do |obs|
301
+ obs.void('New Status Received from LIMS')
302
+ end
303
+ end
231
304
  end
232
305
  end
233
306
  end
@@ -45,6 +45,7 @@ module Lab
45
45
  data = { Type: result_enter_by,
46
46
  'Test type': ConceptName.find_by(concept_id: result.test.value_coded)&.name,
47
47
  'Accession number': order&.accession_number,
48
+ 'Orde date': order&.start_date,
48
49
  'ARV-Number': find_arv_number(result.person_id),
49
50
  PatientID: result.person_id,
50
51
  'Ordered By': order&.provider&.person&.name,
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # Service for managing LIMS users
5
+ module UserService
6
+ class << self
7
+ include BCrypt
8
+
9
+ def create_lims_user(username:, password:)
10
+ validate username: username
11
+ ActiveRecord::Base.transaction do
12
+ person = create_lims_person
13
+ create_user username: username, password: password, person: person
14
+ end
15
+ end
16
+
17
+ def authenticate_user(username:, password:, user_agent:, request_ip:)
18
+ user = User.find_by_username username
19
+ encrypted_pass = Password.new(user.password)
20
+ if encrypted_pass == password
21
+ generate_token(user, user_agent, request_ip)
22
+ else
23
+ # throw authentication error
24
+ nil
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ ##
31
+ # Validate that the username doesn't already exists
32
+ def validate(username:)
33
+ raise UnprocessableEntityError, 'Username already exists' if User.find_by_username username
34
+ end
35
+
36
+ def create_lims_person
37
+ god_user = User.first
38
+ person = Person.create!(creator: god_user.id)
39
+ PersonName.create!(given_name: 'Lims', family_name: 'User', creator: god_user.id, person: person)
40
+ person
41
+ end
42
+
43
+ def create_user(username:, password:, person:)
44
+ salt = SecureRandom.base64
45
+ user = User.create!(
46
+ username: username,
47
+ password: Password.create(password),
48
+ salt: salt,
49
+ person: person,
50
+ creator: User.first.id
51
+ )
52
+ end
53
+
54
+ def generate_token(user, user_agent, request_ip)
55
+ browser = Browser.new(user_agent)
56
+ key_supplement = request_ip + browser.name + browser.version
57
+ token = Lab::JsonWebTokenService.encode({ user_id: user.id }, key_supplement)
58
+ { auth_token: token }
59
+ end
60
+ end
61
+ end
62
+ end
data/config/routes.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Lab::Engine.routes.draw do
4
- resources :orders, path: 'api/v1/lab/orders'
4
+ resources :orders, path: 'api/v1/lab/orders' do
5
+ collection do
6
+ post :order_status
7
+ post :order_result
8
+ end
9
+ end
5
10
  resources :tests, path: 'api/v1/lab/tests', except: %i[update] do # ?pending=true to select tests without results?
6
11
  resources :results, only: %i[index create destroy]
7
12
  end
@@ -15,4 +20,9 @@ Lab::Engine.routes.draw do
15
20
  resources :test_result_indicators, only: %i[index], path: 'api/v1/lab/test_result_indicators'
16
21
  resources :test_types, only: %i[index], path: 'api/v1/lab/test_types'
17
22
  resources :reasons_for_test, only: %i[index], path: 'api/v1/lab/reasons_for_test'
23
+ resources :users, only: %i[create], path: 'api/v1/lab/users' do
24
+ collection do
25
+ post :login
26
+ end
27
+ end
18
28
  end
data/lib/lab/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lab
4
- VERSION = '1.1.29'
4
+ VERSION = '1.1.31'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: his_emr_api_lab
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.29
4
+ version: 1.1.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elizabeth Glaser Pediatric Foundation Malawi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-19 00:00:00.000000000 Z
11
+ date: 2023-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: couchrest
@@ -247,6 +247,7 @@ files:
247
247
  - app/controllers/lab/test_result_indicators_controller.rb
248
248
  - app/controllers/lab/test_types_controller.rb
249
249
  - app/controllers/lab/tests_controller.rb
250
+ - app/controllers/lab/users_controller.rb
250
251
  - app/jobs/lab/application_job.rb
251
252
  - app/jobs/lab/push_order_job.rb
252
253
  - app/jobs/lab/update_patient_orders_job.rb
@@ -267,6 +268,7 @@ files:
267
268
  - app/services/lab/accession_number_service.rb
268
269
  - app/services/lab/acknowledgement_service.rb
269
270
  - app/services/lab/concepts_service.rb
271
+ - app/services/lab/json_web_token_service.rb
270
272
  - app/services/lab/labelling_service/order_label.rb
271
273
  - app/services/lab/lims/acknowledgement_serializer.rb
272
274
  - app/services/lab/lims/acknowledgement_worker.rb
@@ -290,6 +292,7 @@ files:
290
292
  - app/services/lab/orders_service.rb
291
293
  - app/services/lab/results_service.rb
292
294
  - app/services/lab/tests_service.rb
295
+ - app/services/lab/user_service.rb
293
296
  - config/routes.rb
294
297
  - db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb
295
298
  - db/migrate/20210310115457_create_lab_lims_order_mappings.rb
@@ -338,8 +341,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
338
341
  - !ruby/object:Gem::Version
339
342
  version: '0'
340
343
  requirements: []
341
- rubyforge_project:
342
- rubygems_version: 2.7.6
344
+ rubygems_version: 3.4.1
343
345
  signing_key:
344
346
  specification_version: 4
345
347
  summary: Lab extension for the HIS-EMR-API