his_emr_api_lab 1.1.28 → 1.1.30

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: '09cfdf606b1084b4bda331d27fa85e69b831e3c3329be923692011d403c4ab4d'
4
- data.tar.gz: 6aa364d1c6e1965df73aa05cc1a6a3dc31b160171cf9443c974aa333fbcfd9d4
3
+ metadata.gz: bc253a7a3dc9dcee98dde9b842f2c390571d3ca3fe2a677b733444a091413900
4
+ data.tar.gz: 105357ada270cc5c735fa144401b357cb362867ef9684997cddfe851e7e185d9
5
5
  SHA512:
6
- metadata.gz: 27809c7ad5e07ba4502665b4d289f007562fb159d81ba7b847d344fed25585fa8b3fe73e89def79cccd9d95a01adfdf6c82fa260e664f5cb51ad71ad27efc29f
7
- data.tar.gz: 4326993c19083cbffa2521eddb16a04e719b7406940bdbd0fead6acedb408568c94628060fb5c359c99dbb6f22edbe504fbf64a533a2d0f861d1e627e8bc162f
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|
@@ -28,11 +31,59 @@ module Lab
28
31
  render json: OrdersSearchService.find_orders(filters)
29
32
  end
30
33
 
34
+ def verify_tracking_number
35
+ tracking_number = params.require(:accession_number)
36
+ render json: { exists: OrdersService.check_tracking_number(tracking_number) }, status: :ok
37
+ end
38
+
31
39
  def destroy
32
40
  OrdersService.void_order(params[:id], params[:reason])
33
41
  Lab::VoidOrderJob.perform_later(params[:id])
34
42
 
35
43
  render status: :no_content
36
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
37
88
  end
38
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
@@ -87,6 +87,13 @@ class Lab::Lims::Api::RestApi
87
87
  end
88
88
  end
89
89
 
90
+ def verify_tracking_number(tracking_number)
91
+ find_lims_order(tracking_number)
92
+ rescue InvalidParameters => e
93
+ Rails.logger.error("Failed to verify tracking number #{tracking_number}: #{e.message}")
94
+ false
95
+ end
96
+
90
97
  private
91
98
 
92
99
  attr_reader :config
@@ -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
@@ -52,6 +52,10 @@ module Lab
52
52
  def order_test(order_params)
53
53
  Order.transaction do
54
54
  encounter = find_encounter(order_params)
55
+ if order_params[:accession_number].present? && check_tracking_number(order_params[:accession_number])
56
+ raise 'Accession number already exists'
57
+ end
58
+
55
59
  order = create_order(encounter, order_params)
56
60
 
57
61
  Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
@@ -99,13 +103,84 @@ module Lab
99
103
  order.target_lab&.void(reason)
100
104
 
101
105
  order.tests.each { |test| test.void(reason) }
102
- voided = order.void(reason)
106
+ order.void(reason)
107
+ end
103
108
 
104
- voided
109
+ def check_tracking_number(tracking_number)
110
+ accession_number_exists?(tracking_number) || nlims_accession_number_exists?(tracking_number)
111
+ end
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)
105
137
  end
106
138
 
107
139
  private
108
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
+
109
184
  ##
110
185
  # Extract an encounter from the given parameters.
111
186
  #
@@ -141,6 +216,19 @@ module Lab
141
216
  )
142
217
  end
143
218
 
219
+ def accession_number_exists?(accession_number)
220
+ Lab::LabOrder.where(accession_number: accession_number).exists?
221
+ end
222
+
223
+ def nlims_accession_number_exists?(accession_number)
224
+ config = YAML.load_file('config/application.yml')
225
+ return false unless config['lims_api']
226
+
227
+ # fetch from the rest api and check if it exists
228
+ lims_api = Lab::Lims::ApiFactory.create_api
229
+ lims_api.verify_tracking_number(accession_number).present?
230
+ end
231
+
144
232
  ##
145
233
  # Attach the requesting clinician to an order
146
234
  def add_requesting_clinician(order, params)
@@ -207,6 +295,12 @@ module Lab
207
295
  order.reason_for_test&.delete
208
296
  add_reason_for_test(order, date: order.start_date, reason_for_test_id: concept_id)
209
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
210
304
  end
211
305
  end
212
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,12 +1,18 @@
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
8
13
 
9
14
  get 'api/v1/lab/labels/order', to: 'labels#print_order_label'
15
+ get 'api/v1/lab/accession_number', to: 'orders#verify_tracking_number'
10
16
 
11
17
  # Metadata
12
18
  # TODO: Move the following to namespace /concepts
@@ -14,4 +20,9 @@ Lab::Engine.routes.draw do
14
20
  resources :test_result_indicators, only: %i[index], path: 'api/v1/lab/test_result_indicators'
15
21
  resources :test_types, only: %i[index], path: 'api/v1/lab/test_types'
16
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
17
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.28'
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.28
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-03-15 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