his_emr_api_lab 1.1.29 → 1.1.30

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8830688d3c9f3a5c1eaf915060e61c89110b7602f44233d9a6d6c87628d7bfa
4
- data.tar.gz: 394f57a548c41b25303ea9cf6f7cd205094a18c2c1b471d847d7d02a7828487a
3
+ metadata.gz: bc253a7a3dc9dcee98dde9b842f2c390571d3ca3fe2a677b733444a091413900
4
+ data.tar.gz: 105357ada270cc5c735fa144401b357cb362867ef9684997cddfe851e7e185d9
5
5
  SHA512:
6
- metadata.gz: 8061088cf2210df529f524d410441f647386402ae0c706953d5dbf4b2a439b48c3d9377180b028ba5d456b5598f8b6c75c0a598f8550732785ce53853eafdb15
7
- data.tar.gz: 06a06e00a9051548ee69387fa5c9078cb86d119968a5ef81a37fbddc3e60147fe57b743232aadc20fb7f9adbfa74caddd8f8eb1f6f05bb56219f96ca0762ccdd
6
+ metadata.gz: 4c1f681d9ecc0f6f14ae6cdcf8fb7cdfe91995a3961636825eff22bb31654cb2b2f10a11ce9f090446022c9d6ee5b8f49029cab14d8454b52c3d372856daff71
7
+ data.tar.gz: ae270f2c93ae6ba49aed7122e8f92f3ef7d1a250df3101395e9bffe610b990072cc6d06ef5a0fa1560b2e2ff7fd3abe6f3fb293a371965e25664f26ea5416cb8
@@ -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
@@ -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.30'
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.30
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-06-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