his_emr_api_lab 2.1.8.7 → 2.1.9.pre.alpha

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.
@@ -5,15 +5,14 @@ module Lab
5
5
  ##
6
6
  # Pulls orders from a Lims API object and saves them to the local database.
7
7
  class PullWorker
8
- attr_reader :lims_api, :start_date
8
+ attr_reader :lims_api
9
9
 
10
10
  include Utils # for logger
11
11
 
12
12
  LIMS_LOG_PATH = Rails.root.join('log', 'lims')
13
13
 
14
- def initialize(lims_api, start_date: nil)
14
+ def initialize(lims_api)
15
15
  @lims_api = lims_api
16
- @start_date = start_date
17
16
  end
18
17
 
19
18
  ##
@@ -21,7 +20,7 @@ module Lab
21
20
  def pull_orders(batch_size: 10_000, **)
22
21
  logger.info("Retrieving LIMS orders starting from #{last_seq}")
23
22
 
24
- lims_api.consume_orders(from: last_seq, limit: batch_size, start_date: start_date, **) do |order_dto, context|
23
+ lims_api.consume_orders(from: last_seq, limit: batch_size, **) do |order_dto, context|
25
24
  logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
26
25
 
27
26
  patient = find_patient_by_nhid(order_dto[:patient][:id], order_dto[:tracking_number])
@@ -192,6 +191,11 @@ module Lab
192
191
  def create_order(patient, order_dto)
193
192
  logger.debug("Creating order ##{order_dto['_id']}")
194
193
  order = OrdersService.order_test(order_dto.to_order_service_params(patient_id: patient.patient_id))
194
+
195
+ # Extract and save status trails from NLIMS
196
+ save_status_trails_from_nlims(order, order_dto)
197
+
198
+ # Update results if present
195
199
  update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
196
200
 
197
201
  order
@@ -201,6 +205,11 @@ module Lab
201
205
  logger.debug("Updating order ##{order_dto['_id']}")
202
206
  order = OrdersService.update_order(order_id, order_dto.to_order_service_params(patient_id: patient.patient_id)
203
207
  .merge(force_update: 'true'))
208
+
209
+ # Extract and save status trails from NLIMS
210
+ save_status_trails_from_nlims(order, order_dto)
211
+
212
+ # Update results if present
204
213
  update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
205
214
 
206
215
  order
@@ -291,6 +300,165 @@ module Lab
291
300
  "#{id}:#{first_name} #{last_name}:#{phone_number}"
292
301
  end
293
302
 
303
+ def save_status_trails_from_nlims(order, order_dto)
304
+ logger.debug("Saving status trails from NLIMS for order ##{order['order_id'] || order[:order_id]}")
305
+ logger.debug("Order DTO keys: #{order_dto.keys.inspect}")
306
+ logger.debug("Order DTO sample_statuses type: #{order_dto[:sample_statuses].class}")
307
+ logger.debug("Order DTO sample_statuses content: #{order_dto[:sample_statuses].inspect}")
308
+
309
+ # Extract order status trail from sample_statuses (NLIMS uses sample_statuses for order status)
310
+ # Note: sample_statuses is an array of single-key hashes
311
+ if order_dto[:sample_statuses].is_a?(Array)
312
+ logger.debug("Found sample_statuses: #{order_dto[:sample_statuses].size} entries")
313
+ save_order_status_trails(order, order_dto[:sample_statuses])
314
+ else
315
+ logger.warn("No sample_statuses found or not an Array: #{order_dto[:sample_statuses].class}")
316
+ end
317
+
318
+ # Extract test status trails from test_statuses
319
+ if order_dto['test_statuses'].is_a?(Hash)
320
+ logger.debug("Found test_statuses: #{order_dto['test_statuses'].keys.size} entries")
321
+ save_test_status_trails(order, order_dto['test_statuses'])
322
+ else
323
+ logger.debug("No test_statuses found or not a Hash: #{order_dto['test_statuses'].class}")
324
+ end
325
+ end
326
+
327
+ def save_order_status_trails(order, sample_statuses)
328
+ logger.debug("Saving order status trails for order ##{order['order_id'] || order[:order_id]}")
329
+ logger.debug("Sample statuses: #{sample_statuses.inspect}")
330
+
331
+ # Find concept
332
+ order_status_concept = ConceptName.find_by(name: 'Lab Order Status')&.concept
333
+ unless order_status_concept
334
+ logger.error('Lab Order Status concept not found')
335
+ return
336
+ end
337
+
338
+ order_id = order['order_id'] || order[:order_id] || order['id'] || order[:id]
339
+ lab_order = Lab::LabOrder.find_by(order_id: order_id)
340
+ unless lab_order
341
+ logger.error("Order not found: #{order_id}")
342
+ return
343
+ end
344
+
345
+ # sample_statuses is an array of single-key hashes like:
346
+ # [{ "20260225120000" => { "status" => "Drawn", ... } }, { "20260225130000" => { ... } }]
347
+ sample_statuses.each do |trail_entry|
348
+ # Each trail_entry is a hash with one timestamp key
349
+ trail_entry.each_pair do |timestamp_key, status_data|
350
+ next unless status_data.is_a?(Hash) && status_data['status']
351
+
352
+ # Parse timestamp (format: YYYYMMDDHHmmss) - already in local timezone from NLIMS conversion
353
+ # Use Time.zone.strptime to prevent Rails from converting timezone during save
354
+ begin
355
+ timestamp = Time.zone.strptime(timestamp_key, '%Y%m%d%H%M%S')
356
+ rescue StandardError => e
357
+ logger.warn("Failed to parse timestamp '#{timestamp_key}': #{e.message}")
358
+ next
359
+ end
360
+
361
+ # Skip if this status has already been recorded for this order (regardless of timestamp)
362
+ if Observation.unscoped.exists?(
363
+ person_id: lab_order.patient_id,
364
+ order_id: order_id,
365
+ concept_id: order_status_concept.concept_id,
366
+ value_text: status_data['status'],
367
+ voided: 0
368
+ )
369
+ logger.debug("Order status already recorded: #{status_data['status']} for order ##{order_id}")
370
+ next
371
+ end
372
+
373
+ updated_by = status_data['updated_by'] || {}
374
+
375
+ begin
376
+ Observation.create!(
377
+ person_id: lab_order.patient_id,
378
+ encounter_id: lab_order.encounter_id,
379
+ order_id: order_id,
380
+ concept_id: order_status_concept.concept_id,
381
+ value_text: status_data['status'], # Store status as text
382
+ obs_datetime: timestamp,
383
+ comments: updated_by.to_json,
384
+ creator: User.current&.user_id || 1,
385
+ date_created: Time.now,
386
+ uuid: SecureRandom.uuid
387
+ )
388
+ logger.info("Created order status trail: #{status_data['status']} at #{timestamp}")
389
+ rescue StandardError => e
390
+ logger.error("Failed to save order status trail: #{e.message}")
391
+ logger.error(" Order ID: #{order_id}")
392
+ logger.error(" Status: #{status_data['status']}")
393
+ logger.error(" Timestamp: #{timestamp}")
394
+ end
395
+ end
396
+ end
397
+ end
398
+
399
+ def save_test_status_trails(order, test_statuses)
400
+ # Find test status concept
401
+ test_status_concept = ConceptName.find_by(name: 'Lab Test Status')&.concept
402
+ unless test_status_concept
403
+ logger.error('Lab Test Status concept not found')
404
+ return
405
+ end
406
+
407
+ test_statuses.each do |test_name, statuses|
408
+ next unless statuses.is_a?(Hash)
409
+
410
+ # Find the test by name
411
+ test_concept = Utils.find_concept_by_name(Utils.translate_test_name(test_name))
412
+ next unless test_concept
413
+
414
+ test = Lab::LabTest.find_by(order_id: order['order_id'], value_coded: test_concept.concept_id)
415
+ next unless test
416
+
417
+ # Process each status in the trail
418
+ statuses.each do |timestamp_key, status_data|
419
+ next unless status_data.is_a?(Hash) && status_data['status']
420
+
421
+ # Parse timestamp (format: YYYYMMDDHHmmss) - already in local timezone from NLIMS conversion
422
+ # Use Time.zone.strptime to prevent Rails from converting timezone during save
423
+ begin
424
+ timestamp = Time.zone.strptime(timestamp_key, '%Y%m%d%H%M%S')
425
+ rescue StandardError => e
426
+ logger.warn("Failed to parse timestamp '#{timestamp_key}': #{e.message}")
427
+ next
428
+ end
429
+
430
+ # Skip if this status has already been recorded for this test (regardless of timestamp)
431
+ next if Observation.unscoped.exists?(
432
+ person_id: test.person_id,
433
+ obs_group_id: test.obs_id,
434
+ concept_id: test_status_concept.concept_id,
435
+ value_text: status_data['status'],
436
+ voided: 0
437
+ )
438
+
439
+ updated_by = status_data['updated_by'] || {}
440
+
441
+ begin
442
+ Observation.create!(
443
+ person_id: test.person_id,
444
+ encounter_id: test.encounter_id,
445
+ obs_group_id: test.obs_id,
446
+ concept_id: test_status_concept.concept_id,
447
+ value_text: status_data['status'], # Store status as text
448
+ obs_datetime: timestamp,
449
+ comments: updated_by.to_json,
450
+ creator: User.current&.user_id || 1,
451
+ date_created: Time.now,
452
+ uuid: SecureRandom.uuid
453
+ )
454
+ logger.info("Created test status trail: #{status_data['status']} at #{timestamp} for #{test_name}")
455
+ rescue StandardError => e
456
+ logger.error("Failed to save test status trail for #{test_name}: #{e.message}")
457
+ end
458
+ end
459
+ end
460
+ end
461
+
294
462
  def save_failed_import(order_dto, reason, diff = nil)
295
463
  logger.info("Failed to import LIMS order ##{order_dto[:tracking_number]} due to '#{reason}'")
296
464
  LimsFailedImport.create!(lims_id: order_dto[:_id],
@@ -5,16 +5,15 @@ module Lab
5
5
  ##
6
6
  # Pushes all local orders to a LIMS Api object.
7
7
  class PushWorker
8
- attr_reader :lims_api, :start_date
8
+ attr_reader :lims_api
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, start_date: nil)
15
+ def initialize(lims_api)
16
16
  @lims_api = lims_api
17
- @start_date = start_date
18
17
  end
19
18
 
20
19
  def push_orders(batch_size: 1000, wait: false)
@@ -82,8 +81,8 @@ module Lab
82
81
 
83
82
  def void_order_in_lims(order_id)
84
83
  order = Lab::LabOrder.joins(order_type: { name: 'Lab' })
85
- .unscoped
86
- .find(order_id)
84
+ .unscoped
85
+ .find(order_id)
87
86
  order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
88
87
  Rails.logger.info("Deleting order ##{order_dto[:accession_number]} from LIMS")
89
88
  lims_api.delete_order('', order_dto)
@@ -101,17 +100,10 @@ module Lab
101
100
 
102
101
  def new_orders
103
102
  Rails.logger.debug('Looking for new orders that need to be created in LIMS...')
104
- query = Lab::LabOrder.joins('LEFT JOIN lab_lims_order_mappings ON lab_lims_order_mappings.order_id = orders.order_id')
105
- .where('lab_lims_order_mappings.order_id IS NULL')
106
- .where("accession_number IS NOT NULL AND accession_number !=''")
107
-
108
- query = if start_date
109
- query.where('orders.date_created >= ?', start_date)
110
- else
111
- query.where('orders.date_created >= ? AND orders.date_created <= ?', START_DATE, Date.today + 1.day)
112
- end
113
-
114
- query.order(date_created: :desc)
103
+ Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
104
+ .where("accession_number IS NOT NULL AND accession_number !=''")
105
+ .where(date_created: START_DATE..(Date.today + 1.day))
106
+ .order(date_created: :desc)
115
107
  end
116
108
 
117
109
  def updated_orders
@@ -119,26 +111,23 @@ module Lab
119
111
  last_updated = Lab::LimsOrderMapping.select('MAX(updated_at) AS last_updated')
120
112
  .first
121
113
  .last_updated
122
- last_updated = start_date if start_date && last_updated < start_date
123
114
 
124
115
  Lab::LabOrder.left_joins(:results)
125
116
  .joins(:mapping)
126
- .where('(orders.discontinued_date > :last_updated
127
- OR (obs.date_created > orders.date_created AND orders.date_created >= :last_updated)) AND lab_lims_order_mappings.result_push_status = 0',
117
+ .where('orders.discontinued_date > :last_updated
118
+ OR obs.date_created > orders.date_created AND lab_lims_order_mappings.result_push_status = 0',
128
119
  last_updated:)
129
120
  .group('orders.order_id')
130
121
  .order(discontinued_date: :desc, date_created: :desc)
131
122
  end
132
123
 
133
124
  def voided_orders
134
- # add date filter to avoid pushing voided orders that were created a long time ago
135
125
  Rails.logger.debug('Looking for voided orders that are being tracked by LIMS...')
136
- orders = Lab::LabOrder.unscoped
137
- .joins('INNER JOIN lab_lims_order_mappings ON lab_lims_order_mappings.order_id = orders.order_id')
138
- .where(order_type: OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME),
139
- voided: 1)
140
- orders = orders.where('orders.date_created >= ?', start_date) if start_date
141
- orders.order(date_voided: :desc)
126
+ Lab::LabOrder.unscoped
127
+ .where(order_type: OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME),
128
+ order_id: Lab::LimsOrderMapping.all.select(:order_id),
129
+ voided: 1)
130
+ .order(date_voided: :desc)
142
131
  end
143
132
 
144
133
  ##
@@ -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(start_date: nil)
12
+ def self.start
13
13
  User.current = Utils.lab_user
14
14
 
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?
15
+ fork(&method(:start_push_worker))
16
+ fork(&method(:start_pull_worker))
17
+ fork(&method(:start_acknowledgement_worker))
18
+ fork(&method(:start_realtime_pull_worker)) if realtime_updates_enabled?
19
19
 
20
20
  Process.waitall
21
21
  end
22
22
 
23
- def self.start_push_worker(start_date: nil)
23
+ def self.start_push_worker
24
24
  start_worker('push_worker') do
25
- worker = PushWorker.new(lims_api, start_date: start_date)
25
+ worker = PushWorker.new(lims_api)
26
26
 
27
27
  worker.push_orders # (wait: true)
28
28
  end
29
29
  end
30
30
 
31
- def self.start_acknowledgement_worker(start_date: nil)
31
+ def self.start_acknowledgement_worker
32
32
  start_worker('acknowledgement_worker') do
33
- worker = AcknowledgementWorker.new(lims_api, start_date: start_date)
33
+ worker = AcknowledgementWorker.new(lims_api)
34
34
  worker.push_acknowledgement
35
35
  end
36
36
  end
37
37
 
38
- def self.start_pull_worker(start_date: nil)
38
+ def self.start_pull_worker
39
39
  start_worker('pull_worker') do
40
- worker = PullWorker.new(lims_api, start_date: start_date)
40
+ worker = PullWorker.new(lims_api)
41
41
 
42
42
  worker.pull_orders
43
43
  end
44
44
  end
45
45
 
46
- def self.start_realtime_pull_worker(start_date: nil)
46
+ def self.start_realtime_pull_worker
47
47
  start_worker('realtime_pull_worker') do
48
- worker = PullWorker.new(Lims::Api::WsApi.new(Lab::Lims::Config.updates_socket), start_date: start_date)
48
+ worker = PullWorker.new(Lims::Api::WsApi.new(Lab::Lims::Config.updates_socket))
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'