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.
@@ -74,18 +74,85 @@ module Lab
74
74
  { tracking_number: order_dto[:tracking_number] }
75
75
  end
76
76
 
77
- def consume_orders(*_args, patient_id: nil, **_kwargs)
78
- orders_pending_updates(patient_id).each do |order|
77
+ def consume_orders(*_args, patient_id: nil, start_date: nil, **_kwargs)
78
+ orders_pending_updates(patient_id, start_date: start_date).each do |order|
79
79
  order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
80
- if order_dto['priority'].nil? || order_dto['sample_type'].casecmp?('not_specified')
81
- patch_order_dto_with_lims_order!(order_dto, find_lims_order(order.accession_number))
80
+
81
+ # Always fetch the full order from NLIMS to get status trails
82
+ begin
83
+ lims_order = find_lims_order(order.accession_number)
84
+ patch_order_dto_with_lims_order!(order_dto, lims_order)
85
+
86
+ Rails.logger.debug("NLIMS order structure for #{order.accession_number}:")
87
+ Rails.logger.debug(" Has 'order' key: #{lims_order.key?('order')}")
88
+ Rails.logger.debug(" Has 'data' key: #{lims_order.key?('data')}")
89
+ Rails.logger.debug(" Top level keys: #{lims_order.keys.inspect}")
90
+
91
+ # Also extract status trails from the NLIMS order
92
+ # Note: NLIMS might return order data under 'order' or 'data.order'
93
+ order_data = lims_order['order'] || lims_order.dig('data', 'order') || lims_order
94
+
95
+ if order_data && order_data['status_trail']
96
+ Rails.logger.info("Found #{order_data['status_trail'].size} order status trail entries from NLIMS")
97
+ order_dto[:sample_statuses] ||= []
98
+ # Convert NLIMS status trail to the format expected by PullWorker
99
+ # Note: sample_statuses must be an array of single-key hashes
100
+ order_data['status_trail'].each do |trail|
101
+ # Convert ISO 8601 timestamp to YYYYMMDDHHmmss format
102
+ timestamp_key = convert_timestamp_to_key(trail['timestamp'])
103
+ order_dto[:sample_statuses] << {
104
+ timestamp_key => {
105
+ 'status_id' => trail['status_id'],
106
+ 'status' => trail['status'],
107
+ 'updated_by' => trail['updated_by']
108
+ }
109
+ }
110
+ Rails.logger.debug(" Added order status: #{trail['status']} at #{timestamp_key}")
111
+ end
112
+ Rails.logger.debug("Final sample_statuses: #{order_dto[:sample_statuses].inspect}")
113
+ else
114
+ Rails.logger.warn("No order status_trail found in NLIMS response for #{order.accession_number}")
115
+ Rails.logger.debug("Order data keys: #{order_data&.keys&.inspect}")
116
+ end
117
+
118
+ # Extract test status trails from NLIMS tests
119
+ tests_data = lims_order['tests'] || lims_order.dig('data', 'tests') || []
120
+ if tests_data.is_a?(Array)
121
+ Rails.logger.debug("Processing #{tests_data.size} tests from NLIMS")
122
+ order_dto['test_statuses'] ||= {}
123
+ tests_data.each do |test|
124
+ next unless test['status_trail'].is_a?(Array)
125
+
126
+ test_name = test.dig('test_type', 'name')
127
+ next unless test_name
128
+
129
+ Rails.logger.debug(" Found #{test['status_trail'].size} status trail entries for test #{test_name}")
130
+ order_dto['test_statuses'][test_name] ||= {}
131
+ test['status_trail'].each do |trail|
132
+ # Convert ISO 8601 timestamp to YYYYMMDDHHmmss format
133
+ timestamp_key = convert_timestamp_to_key(trail['timestamp'])
134
+ order_dto['test_statuses'][test_name][timestamp_key] = {
135
+ 'status_id' => trail['status_id'],
136
+ 'status' => trail['status'],
137
+ 'updated_by' => trail['updated_by']
138
+ }
139
+ end
140
+ end
141
+ end
142
+ rescue RestClient::NotFound
143
+ Rails.logger.warn("Order ##{order.accession_number} not found in NLIMS, using local data only")
82
144
  end
145
+
146
+ # Try to fetch results if available
83
147
  if order_dto['test_results'].empty?
84
148
  begin
85
149
  patch_order_dto_with_lims_results!(order_dto, find_lims_results(order.accession_number))
86
- rescue InvalidParameters => e # LIMS responds with a 401 when a result is not found :(
87
- Rails.logger.error("Failed to fetch results for ##{order.accession_number}: #{e.message}")
88
- next
150
+ rescue InvalidParameters => e
151
+ Rails.logger.info("No results available for ##{order.accession_number}: #{e.message}")
152
+ # Don't skip - continue processing to save status trails
153
+ rescue RestClient::NotFound
154
+ Rails.logger.info("No results found for ##{order.accession_number}")
155
+ # Don't skip - continue processing to save status trails
89
156
  end
90
157
  end
91
158
 
@@ -227,7 +294,7 @@ module Lab
227
294
  {
228
295
  order: {
229
296
  tracking_number: order_dto.fetch(:tracking_number),
230
- district: order_dto.fetch(:districy),
297
+ district: current_district,
231
298
  health_facility_name: order_dto.fetch(:sending_facility),
232
299
  sending_facility: order_dto.fetch(:sending_facility),
233
300
  arv_number: order_dto.fetch(:patient).fetch(:arv_number),
@@ -281,17 +348,24 @@ module Lab
281
348
  status: 'specimen_collected',
282
349
  time_updated: date_updated,
283
350
  sample_type: order_dto.fetch(:sample_type_map),
284
- updated_by: status.fetch(:updated_by),
285
- status_trail: [
286
- updated_by: {
287
- first_name: status.fetch(:updated_by).fetch(:first_name),
288
- last_name: status.fetch(:updated_by).fetch(:last_name),
289
- id_number: status.fetch(:updated_by).fetch(:id)
290
- }
291
- ]
351
+ updated_by: status.fetch(:updated_by)
292
352
  }
293
353
  end
294
354
 
355
+ def current_district
356
+ health_centre = Location.current_health_center
357
+ raise 'Current health centre not set' unless health_centre
358
+
359
+ district = health_centre.district || Lab::Lims::Config.application['district']
360
+
361
+ unless district
362
+ health_centre_name = "##{health_centre.id} - #{health_centre.name}"
363
+ raise "Current health centre district not set: #{health_centre_name}"
364
+ end
365
+
366
+ district
367
+ end
368
+
295
369
  ##
296
370
  # Extracts sample drawn status from an OrderDto
297
371
  def sample_drawn_status(order_dto)
@@ -437,25 +511,23 @@ module Lab
437
511
  order_dto['test_results'].each do |test_name, results|
438
512
  Rails.logger.info("Pushing result for order ##{order_dto['tracking_number']}")
439
513
  in_authenticated_session do |headers|
440
- params = make_update_test_params(order_dto, test_name, results)
514
+ params = make_update_test_params(order_dto['tracking_number'], test_name, results)
441
515
 
442
- RestClient.put(expand_uri("tests/#{order_dto['tracking_number']}", api_version: 'v2'), params, headers)
516
+ RestClient.post(expand_uri("tests/#{order_dto['tracking_number']}", api_version: 'v2'), params, headers)
443
517
  end
444
518
  end
445
519
  end
446
520
 
447
- def make_update_test_params(order_dto, test, results, test_status = 'Drawn')
448
- # Find the concept from the test name (which is a string)
449
- concept = ::ConceptName.find_by(name: test)&.concept
521
+ def make_update_test_params(_tracking_number, test, results, test_status = 'Drawn')
450
522
  {
451
523
  test_status:,
452
524
  time_updated: results['result_date'],
453
525
  test_type: {
454
- name: concept&.test_catalogue_name,
455
- nlims_code: concept&.nlims_code
526
+ name: ::Concept.find(test.concept_id).test_catalogue_name,
527
+ nlims_code: ::Concept.find(test.concept_id).nlims_code
456
528
  },
457
- test_results: results['results'].map do |measure_name, value|
458
- measure_value = value['result_value']
529
+ test_results: results['results'].map do |measure, _value|
530
+ measure_name, measure_value = measure
459
531
  {
460
532
  measure: {
461
533
  name: measure_name,
@@ -467,18 +539,6 @@ module Lab
467
539
  result_date: results['result_date']
468
540
  }
469
541
  }
470
- end,
471
- status_trail: order_dto['sample_statuses'].map do |trail_entry|
472
- date, status = trail_entry.each_pair.first
473
- {
474
- status: status['status'],
475
- timestamp: date,
476
- updated_by: {
477
- first_name: status.fetch('updated_by').fetch('first_name'),
478
- last_name: status.fetch('updated_by').fetch('last_name'),
479
- id_number: status.fetch('updated_by').fetch('id')
480
- }
481
- }
482
542
  end
483
543
  }
484
544
  end
@@ -517,46 +577,64 @@ module Lab
517
577
  }
518
578
  end
519
579
 
520
- def orders_pending_updates(patient_id = nil)
580
+ def orders_pending_updates(patient_id = nil, start_date: nil)
521
581
  Rails.logger.info('Looking for orders that need to be updated...')
522
582
  orders = {}
523
583
 
524
- orders_without_specimen(patient_id).each { |order| orders[order.order_id] = order }
525
- orders_without_results(patient_id).each { |order| orders[order.order_id] = order }
526
- orders_without_reason(patient_id).each { |order| orders[order.order_id] = order }
584
+ orders_without_specimen(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
585
+ orders_without_results(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
586
+ orders_without_reason(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
527
587
 
528
588
  orders.values
529
589
  end
530
590
 
531
- def orders_without_specimen(patient_id = nil)
591
+ def orders_without_specimen(patient_id = nil, start_date: nil)
532
592
  Rails.logger.debug('Looking for orders without a specimen')
533
593
  unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
534
594
  .select(:concept_id)
535
595
  orders = Lab::LabOrder.where(concept_id: unknown_specimen)
536
596
  .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
537
597
  orders = orders.where(patient_id:) if patient_id
598
+ orders = orders.where('orders.date_created >= ?', start_date) if start_date
538
599
 
539
600
  orders
540
601
  end
541
602
 
542
- def orders_without_results(patient_id = nil)
603
+ def orders_without_results(patient_id = nil, start_date: nil)
543
604
  Rails.logger.debug('Looking for orders without a result')
544
605
  # Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
545
606
  # .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id).where("pulled_at IS NULL"))
546
- Lab::OrdersSearchService.find_orders_without_results(patient_id:)
547
- .where(order_id: Lab::LimsOrderMapping.select(:order_id))
607
+ orders = Lab::OrdersSearchService.find_orders_without_results(patient_id:)
608
+ .where(order_id: Lab::LimsOrderMapping.select(:order_id))
609
+ orders = orders.where('orders.date_created >= ?', start_date) if start_date
610
+ orders
548
611
  end
549
612
 
550
- def orders_without_reason(patient_id = nil)
613
+ def orders_without_reason(patient_id = nil, start_date: nil)
551
614
  Rails.logger.debug('Looking for orders without a reason for test')
552
615
  orders = Lab::LabOrder.joins(:reason_for_test)
553
616
  .merge(Observation.where(value_coded: nil, value_text: nil))
554
617
  .limit(1000)
555
618
  .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
556
619
  orders = orders.where(patient_id:) if patient_id
620
+ orders = orders.where('orders.date_created >= ?', start_date) if start_date
557
621
 
558
622
  orders
559
623
  end
624
+
625
+ # Converts ISO 8601 timestamp to YYYYMMDDHHmmss format
626
+ def convert_timestamp_to_key(timestamp)
627
+ return timestamp if timestamp.nil? || timestamp.empty?
628
+
629
+ begin
630
+ # Parse ISO 8601 timestamp and format as YYYYMMDDHHmmss
631
+ Time.parse(timestamp).strftime('%Y%m%d%H%M%S')
632
+ rescue StandardError => e
633
+ Rails.logger.warn("Failed to parse timestamp '#{timestamp}': #{e.message}")
634
+ # Fallback: remove all non-digits
635
+ timestamp.to_s.gsub(/\D/, '')
636
+ end
637
+ end
560
638
  end
561
639
  end
562
640
  end
@@ -685,7 +763,7 @@ module Lab
685
763
  def make_create_params(order_dto)
686
764
  {
687
765
  tracking_number: order_dto.fetch(:tracking_number),
688
- district: order_dto.fetch(:districy),
766
+ district: current_district,
689
767
  health_facility_name: order_dto.fetch(:sending_facility),
690
768
  first_name: order_dto.fetch(:patient).fetch(:first_name),
691
769
  last_name: order_dto.fetch(:patient).fetch(:last_name),
@@ -723,6 +801,20 @@ module Lab
723
801
  }
724
802
  end
725
803
 
804
+ def current_district
805
+ health_centre = Location.current_health_center
806
+ raise 'Current health centre not set' unless health_centre
807
+
808
+ district = health_centre.district || Lab::Lims::Config.application['district']
809
+
810
+ unless district
811
+ health_centre_name = "##{health_centre.id} - #{health_centre.name}"
812
+ raise "Current health centre district not set: #{health_centre_name}"
813
+ end
814
+
815
+ district
816
+ end
817
+
726
818
  ##
727
819
  # Extracts sample drawn status from an OrderDto
728
820
  def sample_drawn_status(order_dto)
@@ -14,17 +14,15 @@ module Lab
14
14
 
15
15
  def serialize_order(order)
16
16
  serialized_order = Lims::Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
17
- location = get_location(serialized_order.location_id, order)
18
-
19
17
  Lims::OrderDto.new(
20
18
  _id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
21
19
  tracking_number: serialized_order.accession_number,
22
- sending_facility: location.name,
20
+ sending_facility: current_facility_name,
23
21
  receiving_facility: serialized_order.target_lab,
24
22
  tests: serialized_order.tests.map { |test| format_test_name(test.name) },
25
23
  tests_map: serialized_order.tests,
26
24
  patient: format_patient(serialized_order.patient_id),
27
- order_location: location.name,
25
+ order_location: format_order_location(serialized_order.encounter_id),
28
26
  sample_type: format_sample_type(serialized_order.specimen.name),
29
27
  sample_type_map: {
30
28
  name: format_sample_type(serialized_order.specimen.name),
@@ -34,7 +32,7 @@ module Lab
34
32
  sample_statuses: format_sample_status_trail(order),
35
33
  test_statuses: format_test_status_trail(order),
36
34
  who_order_test: format_orderer(order),
37
- districy: get_district(location), # yes districy [sic]...
35
+ districy: current_district, # yes districy [sic]...
38
36
  priority: format_sample_priority(serialized_order.reason_for_test.name),
39
37
  date_created: serialized_order.order_date,
40
38
  test_results: format_test_results(serialized_order),
@@ -45,26 +43,13 @@ module Lab
45
43
 
46
44
  private
47
45
 
48
- def get_location(location_id, order)
49
- # Try to get location from the location_id first
50
- location = Location.find_by(location_id: location_id) if location_id.present?
51
-
52
- # Fallback to current health center
53
- location ||= Location.current_health_center
54
-
55
- # Last fallback: try to get from order's observation encounter
56
- # Use unscoped to find observations/encounters across all locations
57
- if location.nil? && order.present?
58
- obs = Observation.unscoped.find_by(order_id: order.order_id)
59
- if obs.respond_to?(:encounter) && obs.encounter.respond_to?(:location)
60
- encounter = Encounter.unscoped.find_by(encounter_id: obs.encounter_id)
61
- location = encounter&.location
62
- end
63
- end
64
-
65
- raise 'Current health center not set' unless location
46
+ def format_order_location(encounter_id)
47
+ location_id = Encounter.select(:location_id).where(encounter_id:)
48
+ location = Location.select(:name)
49
+ .where(location_id:)
50
+ .first
66
51
 
67
- location
52
+ location&.name
68
53
  end
69
54
 
70
55
  # Format patient into a structure that LIMS expects
@@ -138,13 +123,11 @@ module Lab
138
123
  def format_sample_status_trail(order)
139
124
  return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
140
125
 
141
- user = User.unscoped.find(order.creator)
142
- user = User.unscoped.find(order.discontinued_by) if Order.columns_hash.key?('discontinued_by') && user.blank?
126
+ user = User.find(order.creator)
127
+ user = User.find(order.discontinued_by) if Order.columns_hash.key?('discontinued_by') && user.blank?
143
128
 
144
129
  drawn_by = PersonName.find_by_person_id(user.user_id)
145
- drawn_date = order.discontinued_date || order.start_date if %w[discontinued_date start_date].all? do |column|
146
- order.respond_to?(column)
147
- end
130
+ drawn_date = order.discontinued_date || order.start_date if ['discontinued_date', 'start_date'].all? { |column| order.respond_to?(column) }
148
131
  drawn_date ||= order.date_created
149
132
 
150
133
  [
@@ -196,15 +179,13 @@ module Lab
196
179
  order.tests&.each_with_object({}) do |test, results|
197
180
  next if test.result.nil? || test.result.empty?
198
181
 
199
- # Use unscoped to find observations across locations
200
- result_obs = Observation.unscoped.find_by(obs_id: test.result.first.id)
182
+ result_obs = Observation.find_by(obs_id: test.result.first.id)
201
183
  unless result_obs
202
184
  Rails.logger.warn("Observation with obs_id=#{test.result.first.id} not found for test #{test.name} in order #{order.accession_number}")
203
185
  next
204
186
  end
205
187
 
206
- # Use unscoped to find user regardless of location
207
- test_creator = User.unscoped.find(result_obs.creator)
188
+ test_creator = User.find(result_obs.creator)
208
189
  test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
209
190
 
210
191
  results[format_test_name(test.name)] = {
@@ -227,7 +208,7 @@ module Lab
227
208
  end
228
209
 
229
210
  def format_test_name(test_name)
230
- test_name
211
+ return test_name
231
212
  end
232
213
 
233
214
  def format_sample_priority(priority)
@@ -236,11 +217,16 @@ module Lab
236
217
  priority&.titleize
237
218
  end
238
219
 
239
- def get_district(location)
240
- return nil unless location
220
+ def current_health_center
221
+ health_center = Location.current_health_center
222
+ raise 'Current health center not set' unless health_center
223
+
224
+ health_center
225
+ end
241
226
 
242
- district = location.city_village \
243
- || location.parent&.name \
227
+ def current_district
228
+ district = current_health_center.city_village \
229
+ || current_health_center.parent&.name \
244
230
  || GlobalProperty.find_by_property('current_health_center_district')&.property_value
245
231
 
246
232
  return district if district
@@ -252,8 +238,12 @@ module Lab
252
238
  Config.application['district']
253
239
  end
254
240
 
241
+ def current_facility_name
242
+ current_health_center.name
243
+ end
244
+
255
245
  def find_user(user_id)
256
- user = User.unscoped.find(user_id)
246
+ user = User.find(user_id)
257
247
  person_name = PersonName.find_by(person_id: user.person_id)
258
248
  phone_number = PersonAttribute.find_by(type: PersonAttributeType.where(name: 'Cell phone number'),
259
249
  person_id: user.person_id)
@@ -5,14 +5,15 @@ 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
8
+ attr_reader :lims_api, :start_date
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)
14
+ def initialize(lims_api, start_date: nil)
15
15
  @lims_api = lims_api
16
+ @start_date = start_date
16
17
  end
17
18
 
18
19
  ##
@@ -20,7 +21,7 @@ module Lab
20
21
  def pull_orders(batch_size: 10_000, **)
21
22
  logger.info("Retrieving LIMS orders starting from #{last_seq}")
22
23
 
23
- lims_api.consume_orders(from: last_seq, limit: batch_size, **) do |order_dto, context|
24
+ lims_api.consume_orders(from: last_seq, limit: batch_size, start_date: start_date, **) do |order_dto, context|
24
25
  logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
25
26
 
26
27
  patient = find_patient_by_nhid(order_dto[:patient][:id], order_dto[:tracking_number])
@@ -191,6 +192,11 @@ module Lab
191
192
  def create_order(patient, order_dto)
192
193
  logger.debug("Creating order ##{order_dto['_id']}")
193
194
  order = OrdersService.order_test(order_dto.to_order_service_params(patient_id: patient.patient_id))
195
+
196
+ # Extract and save status trails from NLIMS
197
+ save_status_trails_from_nlims(order, order_dto)
198
+
199
+ # Update results if present
194
200
  update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
195
201
 
196
202
  order
@@ -200,6 +206,11 @@ module Lab
200
206
  logger.debug("Updating order ##{order_dto['_id']}")
201
207
  order = OrdersService.update_order(order_id, order_dto.to_order_service_params(patient_id: patient.patient_id)
202
208
  .merge(force_update: 'true'))
209
+
210
+ # Extract and save status trails from NLIMS
211
+ save_status_trails_from_nlims(order, order_dto)
212
+
213
+ # Update results if present
203
214
  update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
204
215
 
205
216
  order
@@ -290,6 +301,165 @@ module Lab
290
301
  "#{id}:#{first_name} #{last_name}:#{phone_number}"
291
302
  end
292
303
 
304
+ def save_status_trails_from_nlims(order, order_dto)
305
+ logger.debug("Saving status trails from NLIMS for order ##{order['order_id'] || order[:order_id]}")
306
+ logger.debug("Order DTO keys: #{order_dto.keys.inspect}")
307
+ logger.debug("Order DTO sample_statuses type: #{order_dto[:sample_statuses].class}")
308
+ logger.debug("Order DTO sample_statuses content: #{order_dto[:sample_statuses].inspect}")
309
+
310
+ # Extract order status trail from sample_statuses (NLIMS uses sample_statuses for order status)
311
+ # Note: sample_statuses is an array of single-key hashes
312
+ if order_dto[:sample_statuses].is_a?(Array)
313
+ logger.debug("Found sample_statuses: #{order_dto[:sample_statuses].size} entries")
314
+ save_order_status_trails(order, order_dto[:sample_statuses])
315
+ else
316
+ logger.warn("No sample_statuses found or not an Array: #{order_dto[:sample_statuses].class}")
317
+ end
318
+
319
+ # Extract test status trails from test_statuses
320
+ if order_dto['test_statuses'].is_a?(Hash)
321
+ logger.debug("Found test_statuses: #{order_dto['test_statuses'].keys.size} entries")
322
+ save_test_status_trails(order, order_dto['test_statuses'])
323
+ else
324
+ logger.debug("No test_statuses found or not a Hash: #{order_dto['test_statuses'].class}")
325
+ end
326
+ end
327
+
328
+ def save_order_status_trails(order, sample_statuses)
329
+ logger.debug("Saving order status trails for order ##{order['order_id'] || order[:order_id]}")
330
+ logger.debug("Sample statuses: #{sample_statuses.inspect}")
331
+
332
+ # Find concept
333
+ order_status_concept = ConceptName.find_by(name: 'Lab Order Status')&.concept
334
+ unless order_status_concept
335
+ logger.error('Lab Order Status concept not found')
336
+ return
337
+ end
338
+
339
+ order_id = order['order_id'] || order[:order_id] || order['id'] || order[:id]
340
+ lab_order = Lab::LabOrder.find_by(order_id: order_id)
341
+ unless lab_order
342
+ logger.error("Order not found: #{order_id}")
343
+ return
344
+ end
345
+
346
+ # sample_statuses is an array of single-key hashes like:
347
+ # [{ "20260225120000" => { "status" => "Drawn", ... } }, { "20260225130000" => { ... } }]
348
+ sample_statuses.each do |trail_entry|
349
+ # Each trail_entry is a hash with one timestamp key
350
+ trail_entry.each_pair do |timestamp_key, status_data|
351
+ next unless status_data.is_a?(Hash) && status_data['status']
352
+
353
+ # Parse timestamp (format: YYYYMMDDHHmmss) - already in local timezone from NLIMS conversion
354
+ # Use Time.zone.strptime to prevent Rails from converting timezone during save
355
+ begin
356
+ timestamp = Time.zone.strptime(timestamp_key, '%Y%m%d%H%M%S')
357
+ rescue StandardError => e
358
+ logger.warn("Failed to parse timestamp '#{timestamp_key}': #{e.message}")
359
+ next
360
+ end
361
+
362
+ # Skip if this status has already been recorded for this order (regardless of timestamp)
363
+ if Observation.unscoped.exists?(
364
+ person_id: lab_order.patient_id,
365
+ order_id: order_id,
366
+ concept_id: order_status_concept.concept_id,
367
+ value_text: status_data['status'],
368
+ voided: 0
369
+ )
370
+ logger.debug("Order status already recorded: #{status_data['status']} for order ##{order_id}")
371
+ next
372
+ end
373
+
374
+ updated_by = status_data['updated_by'] || {}
375
+
376
+ begin
377
+ Observation.create!(
378
+ person_id: lab_order.patient_id,
379
+ encounter_id: lab_order.encounter_id,
380
+ order_id: order_id,
381
+ concept_id: order_status_concept.concept_id,
382
+ value_text: status_data['status'], # Store status as text
383
+ obs_datetime: timestamp,
384
+ comments: updated_by.to_json,
385
+ creator: User.current&.user_id || 1,
386
+ date_created: Time.now,
387
+ uuid: SecureRandom.uuid
388
+ )
389
+ logger.info("Created order status trail: #{status_data['status']} at #{timestamp}")
390
+ rescue StandardError => e
391
+ logger.error("Failed to save order status trail: #{e.message}")
392
+ logger.error(" Order ID: #{order_id}")
393
+ logger.error(" Status: #{status_data['status']}")
394
+ logger.error(" Timestamp: #{timestamp}")
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ def save_test_status_trails(order, test_statuses)
401
+ # Find test status concept
402
+ test_status_concept = ConceptName.find_by(name: 'Lab Test Status')&.concept
403
+ unless test_status_concept
404
+ logger.error('Lab Test Status concept not found')
405
+ return
406
+ end
407
+
408
+ test_statuses.each do |test_name, statuses|
409
+ next unless statuses.is_a?(Hash)
410
+
411
+ # Find the test by name
412
+ test_concept = Utils.find_concept_by_name(Utils.translate_test_name(test_name))
413
+ next unless test_concept
414
+
415
+ test = Lab::LabTest.find_by(order_id: order['order_id'], value_coded: test_concept.concept_id)
416
+ next unless test
417
+
418
+ # Process each status in the trail
419
+ statuses.each do |timestamp_key, status_data|
420
+ next unless status_data.is_a?(Hash) && status_data['status']
421
+
422
+ # Parse timestamp (format: YYYYMMDDHHmmss) - already in local timezone from NLIMS conversion
423
+ # Use Time.zone.strptime to prevent Rails from converting timezone during save
424
+ begin
425
+ timestamp = Time.zone.strptime(timestamp_key, '%Y%m%d%H%M%S')
426
+ rescue StandardError => e
427
+ logger.warn("Failed to parse timestamp '#{timestamp_key}': #{e.message}")
428
+ next
429
+ end
430
+
431
+ # Skip if this status has already been recorded for this test (regardless of timestamp)
432
+ next if Observation.unscoped.exists?(
433
+ person_id: test.person_id,
434
+ obs_group_id: test.obs_id,
435
+ concept_id: test_status_concept.concept_id,
436
+ value_text: status_data['status'],
437
+ voided: 0
438
+ )
439
+
440
+ updated_by = status_data['updated_by'] || {}
441
+
442
+ begin
443
+ Observation.create!(
444
+ person_id: test.person_id,
445
+ encounter_id: test.encounter_id,
446
+ obs_group_id: test.obs_id,
447
+ concept_id: test_status_concept.concept_id,
448
+ value_text: status_data['status'], # Store status as text
449
+ obs_datetime: timestamp,
450
+ comments: updated_by.to_json,
451
+ creator: User.current&.user_id || 1,
452
+ date_created: Time.now,
453
+ uuid: SecureRandom.uuid
454
+ )
455
+ logger.info("Created test status trail: #{status_data['status']} at #{timestamp} for #{test_name}")
456
+ rescue StandardError => e
457
+ logger.error("Failed to save test status trail for #{test_name}: #{e.message}")
458
+ end
459
+ end
460
+ end
461
+ end
462
+
293
463
  def save_failed_import(order_dto, reason, diff = nil)
294
464
  logger.info("Failed to import LIMS order ##{order_dto[:tracking_number]} due to '#{reason}'")
295
465
  LimsFailedImport.create!(lims_id: order_dto[:_id],