his_emr_api_lab 2.1.9.pre.beta → 2.2.1
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/README.md +17 -4
- data/app/controllers/lab/orders_controller.rb +23 -3
- data/app/jobs/lab/process_lab_result_job.rb +1 -4
- data/app/jobs/lab/update_patient_orders_job.rb +1 -50
- data/app/jobs/lab/void_order_job.rb +2 -6
- data/app/models/lab/lab_order.rb +23 -8
- data/app/models/lab/lab_result.rb +2 -2
- data/app/models/lab/lab_test.rb +21 -2
- data/app/serializers/lab/lab_order_serializer.rb +77 -7
- data/app/services/lab/acknowledgement_service.rb +8 -4
- data/app/services/lab/lims/acknowledgement_worker.rb +5 -3
- data/app/services/lab/lims/api/rest_api.rb +139 -47
- data/app/services/lab/lims/order_serializer.rb +29 -39
- data/app/services/lab/lims/pull_worker.rb +173 -3
- data/app/services/lab/lims/push_worker.rb +15 -8
- data/app/services/lab/lims/utils.rb +1 -2
- data/app/services/lab/lims/worker.rb +13 -13
- data/app/services/lab/metadata.rb +1 -0
- data/app/services/lab/notification_service.rb +1 -2
- data/app/services/lab/orders_service.rb +225 -25
- data/app/services/lab/results_service.rb +1 -1
- data/app/services/lab/tests_service.rb +48 -4
- data/app/services/lab/user_service.rb +2 -3
- data/db/migrate/20260226065149_create_lab_status_concepts.rb +80 -0
- data/lib/lab/version.rb +1 -1
- metadata +5 -4
|
@@ -5,15 +5,16 @@ module Lab
|
|
|
5
5
|
##
|
|
6
6
|
# Pushes all local orders to a LIMS Api object.
|
|
7
7
|
class PushWorker
|
|
8
|
-
attr_reader :lims_api
|
|
8
|
+
attr_reader :lims_api, :start_date
|
|
9
9
|
|
|
10
10
|
include Utils # for logger
|
|
11
11
|
|
|
12
12
|
SECONDS_TO_WAIT_FOR_ORDERS = 30
|
|
13
13
|
START_DATE = Time.parse('2024-09-03').freeze
|
|
14
14
|
|
|
15
|
-
def initialize(lims_api)
|
|
15
|
+
def initialize(lims_api, start_date: nil)
|
|
16
16
|
@lims_api = lims_api
|
|
17
|
+
@start_date = start_date
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def push_orders(batch_size: 1000, wait: false)
|
|
@@ -81,8 +82,8 @@ module Lab
|
|
|
81
82
|
|
|
82
83
|
def void_order_in_lims(order_id)
|
|
83
84
|
order = Lab::LabOrder.joins(order_type: { name: 'Lab' })
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
.unscoped
|
|
86
|
+
.find(order_id)
|
|
86
87
|
order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
|
|
87
88
|
Rails.logger.info("Deleting order ##{order_dto[:accession_number]} from LIMS")
|
|
88
89
|
lims_api.delete_order('', order_dto)
|
|
@@ -100,10 +101,16 @@ module Lab
|
|
|
100
101
|
|
|
101
102
|
def new_orders
|
|
102
103
|
Rails.logger.debug('Looking for new orders that need to be created in LIMS...')
|
|
103
|
-
Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
query = Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
|
|
105
|
+
.where("accession_number IS NOT NULL AND accession_number !=''")
|
|
106
|
+
|
|
107
|
+
query = if start_date
|
|
108
|
+
query.where('orders.date_created >= ?', start_date)
|
|
109
|
+
else
|
|
110
|
+
query.where('orders.date_created >= ? AND orders.date_created <= ?', START_DATE, Date.today + 1.day)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
query.order(date_created: :desc)
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
def updated_orders
|
|
@@ -9,43 +9,43 @@ module Lab
|
|
|
9
9
|
##
|
|
10
10
|
# Pull/Push orders from/to the LIMS queue (Oops meant CouchDB).
|
|
11
11
|
module Worker
|
|
12
|
-
def self.start
|
|
12
|
+
def self.start(start_date: nil)
|
|
13
13
|
User.current = Utils.lab_user
|
|
14
14
|
|
|
15
|
-
fork(
|
|
16
|
-
fork(
|
|
17
|
-
fork(
|
|
18
|
-
fork(
|
|
15
|
+
fork { start_push_worker(start_date: start_date) }
|
|
16
|
+
fork { start_pull_worker(start_date: start_date) }
|
|
17
|
+
fork { start_acknowledgement_worker(start_date: start_date) }
|
|
18
|
+
fork { start_realtime_pull_worker(start_date: start_date) } if realtime_updates_enabled?
|
|
19
19
|
|
|
20
20
|
Process.waitall
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def self.start_push_worker
|
|
23
|
+
def self.start_push_worker(start_date: nil)
|
|
24
24
|
start_worker('push_worker') do
|
|
25
|
-
worker = PushWorker.new(lims_api)
|
|
25
|
+
worker = PushWorker.new(lims_api, start_date: start_date)
|
|
26
26
|
|
|
27
27
|
worker.push_orders # (wait: true)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def self.start_acknowledgement_worker
|
|
31
|
+
def self.start_acknowledgement_worker(start_date: nil)
|
|
32
32
|
start_worker('acknowledgement_worker') do
|
|
33
|
-
worker = AcknowledgementWorker.new(lims_api)
|
|
33
|
+
worker = AcknowledgementWorker.new(lims_api, start_date: start_date)
|
|
34
34
|
worker.push_acknowledgement
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def self.start_pull_worker
|
|
38
|
+
def self.start_pull_worker(start_date: nil)
|
|
39
39
|
start_worker('pull_worker') do
|
|
40
|
-
worker = PullWorker.new(lims_api)
|
|
40
|
+
worker = PullWorker.new(lims_api, start_date: start_date)
|
|
41
41
|
|
|
42
42
|
worker.pull_orders
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def self.start_realtime_pull_worker
|
|
46
|
+
def self.start_realtime_pull_worker(start_date: nil)
|
|
47
47
|
start_worker('realtime_pull_worker') do
|
|
48
|
-
worker = PullWorker.new(Lims::Api::WsApi.new(Lab::Lims::Config.updates_socket))
|
|
48
|
+
worker = PullWorker.new(Lims::Api::WsApi.new(Lab::Lims::Config.updates_socket), start_date: start_date)
|
|
49
49
|
|
|
50
50
|
worker.pull_orders
|
|
51
51
|
end
|
|
@@ -12,6 +12,7 @@ module Lab
|
|
|
12
12
|
TEST_RESULT_INDICATOR_CONCEPT_NAME = 'Lab test result indicator'
|
|
13
13
|
TEST_TYPE_CONCEPT_NAME = 'Test type'
|
|
14
14
|
LAB_ORDER_STATUS_CONCEPT_NAME = 'lab order status'
|
|
15
|
+
LAB_TEST_STATUS_CONCEPT_NAME = 'lab test status'
|
|
15
16
|
UNKNOWN_SPECIMEN = 'Unknown'
|
|
16
17
|
VISIT_TYPE_UUID = '8a4a4488-c2cc-11de-8d13-0010c6dffd0f'
|
|
17
18
|
TEST_METHOD_CONCEPT_NAME = 'Recommended test method'
|
|
@@ -30,8 +30,7 @@ class Lab::NotificationService
|
|
|
30
30
|
def create_notification(alert_type, alert_message)
|
|
31
31
|
return if alert_type != 'LIMS'
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
lab = User.unscoped.find_by(username: 'lab_daemon')
|
|
33
|
+
lab = User.find_by(username: 'lab_daemon')
|
|
35
34
|
ActiveRecord::Base.transaction do
|
|
36
35
|
alert = NotificationAlert.create!(text: alert_message.to_json, date_to_expire: Time.now + not_period.days,
|
|
37
36
|
creator: lab, changed_by: lab, date_created: Time.now)
|
|
@@ -60,8 +60,14 @@ module Lab
|
|
|
60
60
|
|
|
61
61
|
attach_test_method(order, order_params) if order_params[:test_method]
|
|
62
62
|
|
|
63
|
+
# Create initial order status trail
|
|
64
|
+
create_initial_order_status_trail(order)
|
|
65
|
+
|
|
63
66
|
Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
|
|
64
67
|
|
|
68
|
+
# Reload order to include status trails and tests
|
|
69
|
+
order = Lab::LabOrder.prefetch_relationships.find(order.order_id)
|
|
70
|
+
|
|
65
71
|
Lab::LabOrderSerializer.serialize_order(
|
|
66
72
|
order, requesting_clinician: add_requesting_clinician(order, order_params),
|
|
67
73
|
reason_for_test: add_reason_for_test(order, order_params),
|
|
@@ -102,14 +108,16 @@ module Lab
|
|
|
102
108
|
|
|
103
109
|
if reason_for_test
|
|
104
110
|
Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
|
|
105
|
-
update_reason_for_test(order, Concept.find(reason_for_test)&.id,
|
|
111
|
+
update_reason_for_test(order, Concept.find(reason_for_test)&.id,
|
|
112
|
+
force_update: params.fetch('force_update', false))
|
|
106
113
|
end
|
|
107
114
|
|
|
108
115
|
Lab::LabOrderSerializer.serialize_order(order)
|
|
109
116
|
end
|
|
110
117
|
|
|
111
118
|
def void_order(order_id, reason)
|
|
112
|
-
order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab comment_to_fulfiller],
|
|
119
|
+
order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab comment_to_fulfiller],
|
|
120
|
+
tests: [:result])
|
|
113
121
|
.find(order_id)
|
|
114
122
|
|
|
115
123
|
order.requesting_clinician&.void(reason)
|
|
@@ -140,14 +148,55 @@ module Lab
|
|
|
140
148
|
value_text: order_params['status'],
|
|
141
149
|
creator: User.current.id
|
|
142
150
|
)
|
|
151
|
+
|
|
152
|
+
# Save order status trail if available
|
|
153
|
+
save_order_status_trail(order, order_params) if order_params['status']
|
|
143
154
|
end
|
|
144
155
|
create_rejection_notification(order_params) if order_params['status'] == 'test-rejected'
|
|
145
156
|
end
|
|
146
157
|
|
|
147
158
|
def update_order_result(order_params)
|
|
148
|
-
|
|
159
|
+
# Extract tracking number from nested structure if present
|
|
160
|
+
tracking_number = order_params['tracking_number'] || order_params.dig('order', 'tracking_number')
|
|
161
|
+
order = find_order(tracking_number)
|
|
162
|
+
|
|
149
163
|
order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
|
|
150
|
-
|
|
164
|
+
|
|
165
|
+
# Handle results if present in the old format
|
|
166
|
+
patch_order_dto_with_lims_results!(order_dto, order_params['results']) if order_params['results']
|
|
167
|
+
|
|
168
|
+
# Handle test results in NLIMS format
|
|
169
|
+
if order_params['tests']
|
|
170
|
+
# Extract test results from NLIMS tests payload
|
|
171
|
+
test_results = {}
|
|
172
|
+
order_params['tests'].each do |test_data|
|
|
173
|
+
test_name = test_data.dig('test_type', 'name')
|
|
174
|
+
next unless test_name && test_data['test_results']
|
|
175
|
+
|
|
176
|
+
test_results[test_name] = {
|
|
177
|
+
'results' => test_data['test_results'].each_with_object({}) do |result, formatted|
|
|
178
|
+
measure_name = result.dig('measure', 'name')
|
|
179
|
+
result_value = result.dig('result', 'value')
|
|
180
|
+
next unless measure_name && result_value
|
|
181
|
+
|
|
182
|
+
formatted[measure_name] = { 'result_value' => result_value }
|
|
183
|
+
end,
|
|
184
|
+
'result_date' => test_data['test_results'].first&.dig('result', 'result_date'),
|
|
185
|
+
'result_entered_by' => {}
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
patch_order_dto_with_lims_results!(order_dto, test_results) unless test_results.empty?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Save order status trail if available from NLIMS
|
|
193
|
+
if order_params['order'] && order_params['order']['status_trail']
|
|
194
|
+
save_order_status_trails_from_nlims(order, order_params['order']['status_trail'])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Save test status trails if available from NLIMS
|
|
198
|
+
save_test_status_trails_from_nlims(order, order_params['tests']) if order_params['tests']
|
|
199
|
+
|
|
151
200
|
Lab::Lims::PullWorker.new(nil).process_order(order_dto)
|
|
152
201
|
end
|
|
153
202
|
|
|
@@ -160,13 +209,15 @@ module Lab
|
|
|
160
209
|
last_order_date: Lab::LabOrder.last&.start_date&.to_date,
|
|
161
210
|
lab_orders: []
|
|
162
211
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
if include_data
|
|
213
|
+
data[:lab_orders] = orders.map do |order|
|
|
214
|
+
Lab::LabOrderSerializer.serialize_order(
|
|
215
|
+
order, requesting_clinician: order.requesting_clinician,
|
|
216
|
+
reason_for_test: order.reason_for_test,
|
|
217
|
+
target_lab: order.target_lab
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
170
221
|
data
|
|
171
222
|
end
|
|
172
223
|
|
|
@@ -174,14 +225,14 @@ module Lab
|
|
|
174
225
|
|
|
175
226
|
def create_rejection_notification(order_params)
|
|
176
227
|
order = find_order order_params['tracking_number']
|
|
177
|
-
data = {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
data = { type: 'LIMS',
|
|
229
|
+
specimen: ConceptName.find_by(concept_id: order.concept_id)&.name,
|
|
230
|
+
accession_number: order&.accession_number,
|
|
231
|
+
order_date: order&.start_date,
|
|
232
|
+
arv_number: find_arv_number(order.patient_id),
|
|
233
|
+
patient_id: result.person_id,
|
|
234
|
+
ordered_by: order&.provider&.person&.name,
|
|
235
|
+
rejection_reason: order_params['comments'] }.as_json
|
|
185
236
|
NotificationService.new.create_notification('LIMS', data)
|
|
186
237
|
end
|
|
187
238
|
|
|
@@ -235,7 +286,9 @@ module Lab
|
|
|
235
286
|
encounter.encounter_datetime = order_params[:date] || Date.today
|
|
236
287
|
encounter.visit = Visit.find_by_uuid(visit) if Encounter.column_names.include?('visit_id')
|
|
237
288
|
encounter.provider_id = User.current&.person&.id if Encounter.column_names.include?('provider_id')
|
|
238
|
-
|
|
289
|
+
if Encounter.column_names.include?('program_id') && order_params[:program_id].present?
|
|
290
|
+
encounter.program_id = order_params[:program_id]
|
|
291
|
+
end
|
|
239
292
|
encounter.save!
|
|
240
293
|
encounter.reload
|
|
241
294
|
end
|
|
@@ -247,6 +300,7 @@ module Lab
|
|
|
247
300
|
|
|
248
301
|
concept = params.dig(:specimen, :concept)
|
|
249
302
|
concept ||= params.dig(:specimen, :concept_id)
|
|
303
|
+
concept ||= unknown_concept_id
|
|
250
304
|
|
|
251
305
|
order_type = nil
|
|
252
306
|
order_type = OrderType.find_by_order_type_id!(params[:order_type_id])&.id if params[:order_type_id].present?
|
|
@@ -259,7 +313,7 @@ module Lab
|
|
|
259
313
|
order.date_created = params[:date]&.to_date || Date.today if order.respond_to?(:date_created)
|
|
260
314
|
order.start_date = params[:date]&.to_date || Date.today if order.respond_to?(:start_date)
|
|
261
315
|
order.auto_expire_date = params[:end_date]
|
|
262
|
-
#
|
|
316
|
+
# NOTE: comment_to_fulfiller is a has_one association, not a field
|
|
263
317
|
# It will be created via add_comment_to_fulfiller method
|
|
264
318
|
order.accession_number = access_number
|
|
265
319
|
order.orderer = User.current&.user_id
|
|
@@ -331,8 +385,7 @@ module Lab
|
|
|
331
385
|
end
|
|
332
386
|
|
|
333
387
|
def create_order_observation(order, concept_name, date, **values)
|
|
334
|
-
|
|
335
|
-
creator = User.unscoped.find_by(username: 'lab_daemon')
|
|
388
|
+
creator = User.find_by(username: 'lab_daemon')
|
|
336
389
|
User.current ||= creator
|
|
337
390
|
Observation.create!(
|
|
338
391
|
order:,
|
|
@@ -349,7 +402,7 @@ module Lab
|
|
|
349
402
|
end
|
|
350
403
|
|
|
351
404
|
def unknown_concept_id
|
|
352
|
-
ConceptName.find_by_name!('Unknown').
|
|
405
|
+
ConceptName.find_by_name!('Unknown').concept_id
|
|
353
406
|
end
|
|
354
407
|
|
|
355
408
|
def update_reason_for_test(order, concept_id, force_update: false)
|
|
@@ -357,7 +410,10 @@ module Lab
|
|
|
357
410
|
|
|
358
411
|
return if order.reason_for_test&.value_coded == concept_id
|
|
359
412
|
|
|
360
|
-
|
|
413
|
+
if order.reason_for_test&.value_coded && !force_update
|
|
414
|
+
raise InvalidParameterError,
|
|
415
|
+
"Can't change reason for test once set"
|
|
416
|
+
end
|
|
361
417
|
|
|
362
418
|
order.reason_for_test&.delete
|
|
363
419
|
date = order.start_date if order.respond_to?(:start_date)
|
|
@@ -370,6 +426,150 @@ module Lab
|
|
|
370
426
|
obs.void('New Status Received from LIMS')
|
|
371
427
|
end
|
|
372
428
|
end
|
|
429
|
+
|
|
430
|
+
def create_initial_order_status_trail(order)
|
|
431
|
+
create_order_status_observation(
|
|
432
|
+
order: order,
|
|
433
|
+
status: 'Drawn',
|
|
434
|
+
timestamp: order.start_date || order.date_created || Time.now,
|
|
435
|
+
updated_by: {
|
|
436
|
+
'first_name' => User.current&.person&.names&.first&.given_name,
|
|
437
|
+
'last_name' => User.current&.person&.names&.first&.family_name,
|
|
438
|
+
'id' => User.current&.user_id&.to_s,
|
|
439
|
+
'phone_number' => nil
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
rescue StandardError => e
|
|
443
|
+
Rails.logger.warn("Failed to create initial order status trail: #{e.message}")
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def save_order_status_trail(order, status_params)
|
|
447
|
+
create_order_status_observation(
|
|
448
|
+
order: order,
|
|
449
|
+
status: status_params['status'],
|
|
450
|
+
timestamp: status_params['status_time'] || Time.now,
|
|
451
|
+
updated_by: status_params['updated_by'] || {}
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def save_order_status_trails_from_nlims(order, status_trail)
|
|
456
|
+
return unless status_trail.is_a?(Array)
|
|
457
|
+
|
|
458
|
+
status_trail.each do |trail_entry|
|
|
459
|
+
create_order_status_observation(
|
|
460
|
+
order: order,
|
|
461
|
+
status: trail_entry['status'],
|
|
462
|
+
timestamp: trail_entry['timestamp'],
|
|
463
|
+
updated_by: trail_entry['updated_by'] || {}
|
|
464
|
+
)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def save_test_status_trails_from_nlims(order, tests)
|
|
469
|
+
return unless tests.is_a?(Array)
|
|
470
|
+
|
|
471
|
+
tests.each do |test_data|
|
|
472
|
+
next unless test_data['status_trail'].is_a?(Array)
|
|
473
|
+
|
|
474
|
+
# Find the test by test_type
|
|
475
|
+
test_name = test_data.dig('test_type', 'name')
|
|
476
|
+
next unless test_name
|
|
477
|
+
|
|
478
|
+
# Find concept for this test
|
|
479
|
+
concept = Lab::Lims::Utils.find_concept_by_name(test_name)
|
|
480
|
+
next unless concept
|
|
481
|
+
|
|
482
|
+
# Find the test observation
|
|
483
|
+
test = order.tests.find_by(value_coded: concept.concept_id)
|
|
484
|
+
next unless test
|
|
485
|
+
|
|
486
|
+
# Save each status trail entry
|
|
487
|
+
test_data['status_trail'].each do |trail_entry|
|
|
488
|
+
create_test_status_observation(
|
|
489
|
+
test: test,
|
|
490
|
+
status: trail_entry['status'],
|
|
491
|
+
timestamp: trail_entry['timestamp'],
|
|
492
|
+
updated_by: trail_entry['updated_by'] || {}
|
|
493
|
+
)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Creates an observation for order status trail using obs table
|
|
499
|
+
def create_order_status_observation(order:, status:, timestamp:, updated_by: {})
|
|
500
|
+
# Find concept
|
|
501
|
+
order_status_concept = ConceptName.find_by(name: 'Lab Order Status')&.concept
|
|
502
|
+
|
|
503
|
+
unless order_status_concept
|
|
504
|
+
Rails.logger.warn('Missing Lab Order Status concept')
|
|
505
|
+
return
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Check if this exact status already exists
|
|
509
|
+
return if Observation.unscoped.exists?(
|
|
510
|
+
person_id: order.patient_id,
|
|
511
|
+
order_id: order.order_id,
|
|
512
|
+
concept_id: order_status_concept.concept_id,
|
|
513
|
+
value_text: status,
|
|
514
|
+
obs_datetime: timestamp,
|
|
515
|
+
voided: 0
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Create status observation
|
|
519
|
+
Observation.create!(
|
|
520
|
+
person_id: order.patient_id,
|
|
521
|
+
encounter_id: order.encounter_id,
|
|
522
|
+
order_id: order.order_id,
|
|
523
|
+
concept_id: order_status_concept.concept_id,
|
|
524
|
+
value_text: status, # Store status as text
|
|
525
|
+
obs_datetime: timestamp,
|
|
526
|
+
comments: updated_by.to_json,
|
|
527
|
+
creator: User.current&.user_id || 1,
|
|
528
|
+
date_created: Time.now,
|
|
529
|
+
uuid: SecureRandom.uuid
|
|
530
|
+
)
|
|
531
|
+
rescue StandardError => e
|
|
532
|
+
Rails.logger.error("Failed to create order status observation: #{e.message}")
|
|
533
|
+
Rails.logger.error(e.backtrace.first(5).join("\n"))
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Creates an observation for test status trail using obs table
|
|
537
|
+
def create_test_status_observation(test:, status:, timestamp:, updated_by: {})
|
|
538
|
+
# Find concept
|
|
539
|
+
test_status_concept = ConceptName.find_by(name: 'Lab Test Status')&.concept
|
|
540
|
+
|
|
541
|
+
unless test_status_concept
|
|
542
|
+
Rails.logger.warn('Missing Lab Test Status concept')
|
|
543
|
+
return
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Check if this exact status already exists
|
|
547
|
+
return if Observation.unscoped.exists?(
|
|
548
|
+
person_id: test.person_id,
|
|
549
|
+
obs_group_id: test.obs_id,
|
|
550
|
+
concept_id: test_status_concept.concept_id,
|
|
551
|
+
value_text: status,
|
|
552
|
+
obs_datetime: timestamp,
|
|
553
|
+
voided: 0
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Create status observation
|
|
557
|
+
Observation.create!(
|
|
558
|
+
person_id: test.person_id,
|
|
559
|
+
encounter_id: test.encounter_id,
|
|
560
|
+
obs_group_id: test.obs_id, # Link to parent test observation
|
|
561
|
+
concept_id: test_status_concept.concept_id,
|
|
562
|
+
value_text: status, # Store status as text
|
|
563
|
+
obs_datetime: timestamp,
|
|
564
|
+
comments: updated_by.to_json,
|
|
565
|
+
creator: User.current&.user_id || 1,
|
|
566
|
+
date_created: Time.now,
|
|
567
|
+
uuid: SecureRandom.uuid
|
|
568
|
+
)
|
|
569
|
+
rescue StandardError => e
|
|
570
|
+
Rails.logger.error("Failed to create test status observation: #{e.message}")
|
|
571
|
+
Rails.logger.error(e.backtrace.first(5).join("\n"))
|
|
572
|
+
end
|
|
373
573
|
end
|
|
374
574
|
end
|
|
375
575
|
end
|
|
@@ -65,7 +65,7 @@ module Lab
|
|
|
65
65
|
order_date: Order.columns.include?('start_date') ? order.start_date : order.date_created,
|
|
66
66
|
'ARV-Number': find_arv_number(result.person_id),
|
|
67
67
|
PatientID: result.person_id,
|
|
68
|
-
'Ordered By': Order.columns.include?('provider_id') ? order&.provider&.person&.name : Person.find(
|
|
68
|
+
'Ordered By': Order.columns.include?('provider_id') ? order&.provider&.person&.name : Person.find(order.creator)&.name,
|
|
69
69
|
Result: values }.as_json
|
|
70
70
|
NotificationService.new.create_notification(result_enter_by, data)
|
|
71
71
|
end
|
|
@@ -14,8 +14,7 @@ module Lab
|
|
|
14
14
|
|
|
15
15
|
tests = filter_tests(tests, test_type_id: filters.delete(:test_type_id),
|
|
16
16
|
patient_id: filters.delete(:patient_id),
|
|
17
|
-
patient: filters.delete(:patient)
|
|
18
|
-
)
|
|
17
|
+
patient: filters.delete(:patient))
|
|
19
18
|
|
|
20
19
|
tests = filter_tests_by_results(tests) if %w[1 true].include?(filters[:pending_results]&.downcase)
|
|
21
20
|
|
|
@@ -29,12 +28,11 @@ module Lab
|
|
|
29
28
|
def create_tests(order, date, tests_params)
|
|
30
29
|
raise InvalidParameterError, 'tests are required' if tests_params.nil? || tests_params.empty?
|
|
31
30
|
|
|
32
|
-
|
|
33
31
|
Lab::LabTest.transaction do
|
|
34
32
|
tests_params.map do |params|
|
|
35
33
|
concept_id = params[:concept_id]
|
|
36
34
|
concept_id = Concept.find_concept_by_uuid(params[:concept]).id if concept_id.nil?
|
|
37
|
-
|
|
35
|
+
|
|
38
36
|
test = Lab::LabTest.create!(
|
|
39
37
|
concept_id: ConceptName.find_by_name!(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
|
40
38
|
.concept_id,
|
|
@@ -45,6 +43,9 @@ module Lab
|
|
|
45
43
|
value_coded: concept_id
|
|
46
44
|
)
|
|
47
45
|
|
|
46
|
+
# Create initial test status trail
|
|
47
|
+
create_initial_test_status_trail(test, date)
|
|
48
|
+
|
|
48
49
|
Lab::TestSerializer.serialize(test, order:)
|
|
49
50
|
end
|
|
50
51
|
end
|
|
@@ -100,6 +101,49 @@ module Lab
|
|
|
100
101
|
value_coded: test_type_id
|
|
101
102
|
)
|
|
102
103
|
end
|
|
104
|
+
|
|
105
|
+
def create_initial_test_status_trail(test, date)
|
|
106
|
+
# Find concept
|
|
107
|
+
test_status_concept = ConceptName.find_by(name: 'Lab Test Status')&.concept
|
|
108
|
+
|
|
109
|
+
unless test_status_concept
|
|
110
|
+
Rails.logger.warn('Missing Lab Test Status concept')
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
timestamp = date || test.obs_datetime || Time.now
|
|
115
|
+
|
|
116
|
+
# Check if 'Drawn' status already exists for this test
|
|
117
|
+
return if Observation.unscoped.exists?(
|
|
118
|
+
person_id: test.person_id,
|
|
119
|
+
obs_group_id: test.obs_id,
|
|
120
|
+
concept_id: test_status_concept.concept_id,
|
|
121
|
+
value_text: 'Drawn',
|
|
122
|
+
voided: 0
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Create status observation with 'Drawn' as initial status
|
|
126
|
+
Observation.create!(
|
|
127
|
+
person_id: test.person_id,
|
|
128
|
+
encounter_id: test.encounter_id,
|
|
129
|
+
obs_group_id: test.obs_id, # Link to parent test observation
|
|
130
|
+
concept_id: test_status_concept.concept_id,
|
|
131
|
+
value_text: 'Drawn', # Store status as text
|
|
132
|
+
obs_datetime: timestamp,
|
|
133
|
+
status: 'FINAL',
|
|
134
|
+
comments: {
|
|
135
|
+
'first_name' => User.current&.person&.names&.first&.given_name,
|
|
136
|
+
'last_name' => User.current&.person&.names&.first&.family_name,
|
|
137
|
+
'id' => User.current&.user_id&.to_s,
|
|
138
|
+
'phone_number' => nil
|
|
139
|
+
}.to_json,
|
|
140
|
+
creator: User.current&.user_id || 1,
|
|
141
|
+
date_created: Time.now,
|
|
142
|
+
uuid: SecureRandom.uuid
|
|
143
|
+
)
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
Rails.logger.warn("Failed to create initial test status trail: #{e.message}")
|
|
146
|
+
end
|
|
103
147
|
end
|
|
104
148
|
end
|
|
105
149
|
end
|
|
@@ -15,8 +15,7 @@ module Lab
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def authenticate_user(username:, password:, user_agent:, request_ip:)
|
|
18
|
-
|
|
19
|
-
user = User.unscoped.find_by_username username
|
|
18
|
+
user = User.find_by_username username
|
|
20
19
|
encrypted_pass = Password.new(user.password)
|
|
21
20
|
if encrypted_pass == password
|
|
22
21
|
generate_token(user, user_agent, request_ip)
|
|
@@ -31,7 +30,7 @@ module Lab
|
|
|
31
30
|
##
|
|
32
31
|
# Validate that the username doesn't already exists
|
|
33
32
|
def validate(username:)
|
|
34
|
-
raise UnprocessableEntityError, 'Username already exists' if User.
|
|
33
|
+
raise UnprocessableEntityError, 'Username already exists' if User.find_by_username username
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
def create_lims_person
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This migration creates concepts for lab order and test status tracking using obs table
|
|
4
|
+
# Status values are stored as text (value_text), not coded values
|
|
5
|
+
class CreateLabStatusConcepts < ActiveRecord::Migration[5.2]
|
|
6
|
+
def up
|
|
7
|
+
ActiveRecord::Base.transaction do
|
|
8
|
+
# Find concept class and datatype
|
|
9
|
+
concept_class = ConceptClass.find_by(name: 'Finding') || ConceptClass.find_by(name: 'Misc')
|
|
10
|
+
text_datatype = ConceptDatatype.find_by(name: 'Text')
|
|
11
|
+
|
|
12
|
+
# Create Lab Order Status concept (stores status as value_text)
|
|
13
|
+
unless ConceptName.exists?(name: 'Lab Order Status')
|
|
14
|
+
order_status_concept = create_concept(
|
|
15
|
+
name: 'Lab Order Status',
|
|
16
|
+
class_id: concept_class.id,
|
|
17
|
+
datatype_id: text_datatype.id,
|
|
18
|
+
is_set: 0
|
|
19
|
+
)
|
|
20
|
+
puts "Created 'Lab Order Status' concept with ID: #{order_status_concept.concept_id}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create Lab Test Status concept (stores status as value_text)
|
|
24
|
+
unless ConceptName.exists?(name: 'Lab Test Status')
|
|
25
|
+
test_status_concept = create_concept(
|
|
26
|
+
name: 'Lab Test Status',
|
|
27
|
+
class_id: concept_class.id,
|
|
28
|
+
datatype_id: text_datatype.id,
|
|
29
|
+
is_set: 0
|
|
30
|
+
)
|
|
31
|
+
puts "Created 'Lab Test Status' concept with ID: #{test_status_concept.concept_id}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts 'Lab status concepts created successfully'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def down
|
|
39
|
+
ActiveRecord::Base.transaction do
|
|
40
|
+
# Remove concepts
|
|
41
|
+
concepts_to_remove = ['Lab Order Status', 'Lab Test Status']
|
|
42
|
+
|
|
43
|
+
concepts_to_remove.each do |name|
|
|
44
|
+
concept = ConceptName.find_by(name: name)&.concept
|
|
45
|
+
next unless concept
|
|
46
|
+
|
|
47
|
+
ConceptName.where(concept: concept).destroy_all
|
|
48
|
+
concept.destroy
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def create_concept(name:, class_id:, datatype_id:, is_set:)
|
|
56
|
+
concept = Concept.create!(
|
|
57
|
+
class_id: class_id,
|
|
58
|
+
datatype_id: datatype_id,
|
|
59
|
+
short_name: name,
|
|
60
|
+
retired: 0,
|
|
61
|
+
is_set: is_set,
|
|
62
|
+
creator: 1,
|
|
63
|
+
date_created: Time.current,
|
|
64
|
+
uuid: SecureRandom.uuid
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
ConceptName.create!(
|
|
68
|
+
concept: concept,
|
|
69
|
+
name: name,
|
|
70
|
+
locale: 'en',
|
|
71
|
+
locale_preferred: 1,
|
|
72
|
+
concept_name_type: 'FULLY_SPECIFIED',
|
|
73
|
+
creator: 1,
|
|
74
|
+
date_created: Time.current,
|
|
75
|
+
uuid: SecureRandom.uuid
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
concept
|
|
79
|
+
end
|
|
80
|
+
end
|