his_emr_api_lab 1.1.29 → 1.1.31
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 +4 -4
- data/app/controllers/lab/orders_controller.rb +46 -0
- data/app/controllers/lab/users_controller.rb +32 -0
- data/app/serializers/lab/lab_order_serializer.rb +1 -0
- data/app/services/lab/json_web_token_service.rb +20 -0
- data/app/services/lab/lims/pull_worker.rb +30 -5
- data/app/services/lab/metadata.rb +1 -0
- data/app/services/lab/orders_service.rb +76 -3
- data/app/services/lab/results_service.rb +1 -0
- data/app/services/lab/user_service.rb +62 -0
- data/config/routes.rb +11 -1
- data/lib/lab/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88d310815f8d7efad226f84e50963754893da11220df2af7aedf173361452091
|
4
|
+
data.tar.gz: d01062b0a5ec36c1a1710d795a128ca810c265451f46c95b663cd47158667263
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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'],
|
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
|
-
|
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
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.
|
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-
|
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
|
-
|
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
|