his_emr_api_lab 1.1.29 → 1.1.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/lab/orders_controller.rb +46 -0
- data/app/controllers/lab/users_controller.rb +32 -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 +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc253a7a3dc9dcee98dde9b842f2c390571d3ca3fe2a677b733444a091413900
|
4
|
+
data.tar.gz: 105357ada270cc5c735fa144401b357cb362867ef9684997cddfe851e7e185d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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'],
|
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.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-
|
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
|