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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 666ee2ba3fbc370e272c06156d5f90fa2b8a8bedba647dcc1bbfbfbec347a42b
4
- data.tar.gz: ceedca5090c613d217af7f85b3d52cb5a519cb8dfea24a5452e321d50efa2a3d
3
+ metadata.gz: cfef9418fc9d94d233f24d04883623d67e0c59c2aacea66bc15d3cca61192f78
4
+ data.tar.gz: 2d6324bf11f30c87398819042e3a0835275596961bfd838d088a63ae7a4f93c2
5
5
  SHA512:
6
- metadata.gz: f10ed2e39e3fabcdb6219a13b553dd4a1acc29318256f6977ffa05230917265afd90ba4487bb350508ffb5d323cb0783ffc04cc0a3dcdb591692e18cdd78ed4b
7
- data.tar.gz: 46a28575116934b1a8bdce291dd5661b965f5d8ec428a00edbe2530626a7be5bc22b2e96c4e7a09989ba000048d5c252d4aa1303f4d943c044f4415ead92ca54
6
+ metadata.gz: c7eaa989ae6027aaba3bcd29e90136d1f93eda66854f7ee5bf84453420e0c66a40a928e460e00ee48bdee3fac8e2de05b7c030a7c71c8d4a6bf9457704dc7c6f
7
+ data.tar.gz: 16048c52a25d2876d79d6b3526b13625c67de47c44717de48910f905a75bb02dbd1cd48d7ce22e8b58947e61a1cb603340ea3bbdb1cce7536bb1774f41a72d65
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Lab
4
4
  class OrdersController < ApplicationController
5
- skip_before_action :authenticate, only: %i[order_status order_result summary]
6
5
  before_action :authenticate_request, only: %i[order_status order_result summary]
7
6
 
8
7
  def create
@@ -53,7 +52,8 @@ module Lab
53
52
  end
54
53
 
55
54
  def order_status
56
- order_params = params.permit(:tracking_number, :status, :status_time, :comments)
55
+ order_params = params.permit(:tracking_number, :status, :status_time, :comments, :status_id,
56
+ updated_by: [:first_name, :last_name, :id, :phone_number])
57
57
  OrdersService.update_order_status(order_params)
58
58
  render json: { message: "Status for order #{order_params['tracking_number']} successfully updated" }, status: :ok
59
59
  end
@@ -77,26 +77,8 @@ module Lab
77
77
  private
78
78
 
79
79
  def authenticate_request
80
- header = request.headers['Authorization']
81
- content = header.split(' ')
82
- auth_scheme = content.first
83
- unless header
84
- errors = ['Authorization token required']
85
- render json: { errors: errors }, status: :unauthorized
86
- return false
87
- end
88
- unless auth_scheme == 'Bearer'
89
- errors = ['Authorization token bearer scheme required']
90
- render json: { errors: errors }, status: :unauthorized
91
- return false
92
- end
93
- process_token(content.last)
94
- end
95
-
96
- def process_token(token)
97
- browser = Browser.new(request.user_agent)
98
- decoded = Lab::JsonWebTokenService.decode(token, request.remote_ip + browser.name + browser.version)
99
- user(decoded)
80
+ decoded_user = authorize_request
81
+ user(decoded_user)
100
82
  end
101
83
 
102
84
  def user(decoded)
@@ -8,6 +8,11 @@ module Lab
8
8
 
9
9
  -> { where(concept:) }
10
10
  end
11
+
12
+ # Cache the concept ID to avoid lookups in association scopes
13
+ def order_status_concept_id
14
+ @order_status_concept_id ||= ConceptName.find_by(name: 'Lab Order Status')&.concept_id
15
+ end
11
16
  end
12
17
 
13
18
  has_many :tests,
@@ -44,12 +49,20 @@ module Lab
44
49
  class_name: '::Lab::LimsOrderMapping',
45
50
  foreign_key: :order_id
46
51
 
52
+ # Status trails are stored as observations with concept 'Lab Order Status'
53
+ has_many :status_trail_observations,
54
+ lambda {
55
+ unscoped.where(voided: 0, concept_id: Lab::LabOrder.order_status_concept_id).order(obs_datetime: :asc)
56
+ },
57
+ class_name: 'Observation',
58
+ foreign_key: :order_id
59
+
47
60
  default_scope do
48
61
  joins(:order_type)
49
62
  .merge(OrderType.where(name: [
50
- Lab::Metadata::ORDER_TYPE_NAME,
51
- Lab::Metadata::HTS_ORDER_TYPE_NAME
52
- ]))
63
+ Lab::Metadata::ORDER_TYPE_NAME,
64
+ Lab::Metadata::HTS_ORDER_TYPE_NAME
65
+ ]))
53
66
  .where.not(concept_id: ConceptName.where(name: 'Tests ordered').select(:concept_id))
54
67
  end
55
68
 
@@ -57,11 +70,13 @@ module Lab
57
70
  scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
58
71
 
59
72
  def self.prefetch_relationships
60
- includes(:reason_for_test,
61
- :requesting_clinician,
62
- :target_lab,
63
- :comment_to_fulfiller,
64
- tests: [:result])
73
+ # NOTE: status_trail_observations and test results are not preloaded due to
74
+ # Rails limitations with eager loading unscoped associations. They load on-demand instead.
75
+ preload(:reason_for_test,
76
+ :requesting_clinician,
77
+ :target_lab,
78
+ :comment_to_fulfiller,
79
+ :tests)
65
80
  end
66
81
  end
67
82
  end
@@ -3,9 +3,9 @@
3
3
  module Lab
4
4
  class LabResult < Observation
5
5
  def children
6
- Observation.where(obs_group_id: obs_id, voided: 0)
6
+ Observation.unscoped.where(obs_group_id: obs_id, voided: 0)
7
7
  end
8
-
8
+
9
9
  alias measures children
10
10
 
11
11
  default_scope do
@@ -6,14 +6,33 @@ module Lab
6
6
  where(concept: ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME))
7
7
  end
8
8
 
9
+ # Cache the concept IDs as class methods to avoid lookups in association scopes
10
+ def self.test_status_concept_id
11
+ @test_status_concept_id ||= ConceptName.find_by(name: 'Lab Test Status')&.concept_id
12
+ end
13
+
14
+ def self.test_result_concept_id
15
+ @test_result_concept_id ||= ConceptName.find_by(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)&.concept_id
16
+ end
17
+
9
18
  has_one :result,
10
- -> { where(concept: ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)) },
19
+ -> { unscoped.where(voided: 0, concept_id: Lab::LabTest.test_result_concept_id) },
11
20
  class_name: 'Lab::LabResult',
12
21
  foreign_key: :obs_group_id
13
22
 
23
+ # Status trails are stored as observations with concept 'Lab Test Status'
24
+ # They are linked via obs_group_id (this test obs is the parent)
25
+ has_many :status_trail_observations,
26
+ lambda {
27
+ unscoped.where(voided: 0, concept_id: Lab::LabTest.test_status_concept_id).order(obs_datetime: :asc)
28
+ },
29
+ class_name: 'Observation',
30
+ foreign_key: :obs_group_id,
31
+ primary_key: :obs_id
32
+
14
33
  def void(reason)
15
34
  result&.void(reason)
16
- super(reason)
35
+ super
17
36
  end
18
37
  end
19
38
  end
@@ -10,7 +10,7 @@ module Lab
10
10
  target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
11
11
 
12
12
  encounter = Encounter.find_by_encounter_id(order.encounter_id)
13
- program = Program.find_by_program_id(encounter&.program_id)
13
+ program = Program.find_by_program_id(encounter.program_id)
14
14
 
15
15
  ActiveSupport::HashWithIndifferentAccess.new(
16
16
  {
@@ -19,8 +19,8 @@ module Lab
19
19
  order_id: order.order_id, # Deprecated: Link to :id
20
20
  encounter_id: order.encounter_id,
21
21
  order_date: order.start_date,
22
- location_id: encounter&.location_id,
23
- program_id: encounter&.program_id,
22
+ location_id: encounter.location_id,
23
+ program_id: encounter.program_id,
24
24
  program_name: program&.name,
25
25
  patient_id: order.patient_id,
26
26
  accession_number: order.accession_number,
@@ -36,8 +36,10 @@ module Lab
36
36
  name: concept_name(reason_for_test&.value_coded)
37
37
  },
38
38
  delivery_mode: order&.lims_acknowledgement_status&.acknowledgement_type,
39
+ order_status: latest_order_status(order),
40
+ order_status_trail: serialize_order_status_trail(order),
39
41
  tests: tests.map do |test|
40
- result_obs = test.children.first
42
+ result_obs = test.result
41
43
 
42
44
  {
43
45
  id: test.obs_id,
@@ -45,7 +47,9 @@ module Lab
45
47
  uuid: test.uuid,
46
48
  name: concept_name(test.value_coded),
47
49
  test_method: test_method(order, test.value_coded),
48
- result: result_obs && ResultSerializer.serialize(result_obs)
50
+ result: result_obs && ResultSerializer.serialize(result_obs),
51
+ test_status: latest_test_status(test),
52
+ test_status_trail: serialize_test_status_trail(test)
49
53
  }
50
54
  end
51
55
  }
@@ -66,8 +70,7 @@ module Lab
66
70
  def self.concept_name(concept_id)
67
71
  return concept_id unless concept_id
68
72
 
69
- c_name = ::ConceptAttribute.find_by(concept_id:, attribute_type: ConceptAttributeType.test_catalogue_name)&.value_reference
70
- c_name || ConceptName.find_by_concept_id(concept_id)&.name
73
+ ::ConceptAttribute.find_by(concept_id:, attribute_type: ConceptAttributeType.test_catalogue_name)&.value_reference
71
74
  end
72
75
 
73
76
  def self.voided_tests(order)
@@ -75,5 +78,72 @@ module Lab
75
78
  .select(:concept_id)
76
79
  LabTest.unscoped.where(concept:, order:, voided: true)
77
80
  end
81
+
82
+ def self.latest_order_status(order)
83
+ # Query obs table for latest order status
84
+ latest_obs = order.status_trail_observations.last
85
+ return nil unless latest_obs
86
+
87
+ updated_by = parse_comments_json(latest_obs.comments)
88
+
89
+ {
90
+ status_id: 0, # status_id not used with text values
91
+ status: latest_obs.value_text,
92
+ timestamp: latest_obs.obs_datetime,
93
+ updated_by: updated_by
94
+ }
95
+ end
96
+
97
+ def self.serialize_order_status_trail(order)
98
+ # Query obs table for order status trail
99
+ order.status_trail_observations.map do |obs|
100
+ updated_by = parse_comments_json(obs.comments)
101
+
102
+ {
103
+ status_id: 0, # status_id not used with text values
104
+ status: obs.value_text,
105
+ timestamp: obs.obs_datetime,
106
+ updated_by: updated_by
107
+ }
108
+ end
109
+ end
110
+
111
+ def self.latest_test_status(test)
112
+ # Query obs table for latest test status
113
+ latest_obs = test.status_trail_observations.last
114
+ return nil unless latest_obs
115
+
116
+ updated_by = parse_comments_json(latest_obs.comments)
117
+
118
+ {
119
+ status_id: 0, # status_id not used with text values
120
+ status: latest_obs.value_text,
121
+ timestamp: latest_obs.obs_datetime,
122
+ updated_by: updated_by
123
+ }
124
+ end
125
+
126
+ def self.serialize_test_status_trail(test)
127
+ # Query obs table for test status trail
128
+ test.status_trail_observations.map do |obs|
129
+ updated_by = parse_comments_json(obs.comments)
130
+
131
+ {
132
+ status_id: 0, # status_id not used with text values
133
+ status: obs.value_text,
134
+ timestamp: obs.obs_datetime,
135
+ updated_by: updated_by
136
+ }
137
+ end
138
+ end
139
+
140
+ # Helper to parse updated_by from obs comments field
141
+ def self.parse_comments_json(comments)
142
+ return {} if comments.blank?
143
+
144
+ JSON.parse(comments)
145
+ rescue JSON::ParserError
146
+ {}
147
+ end
78
148
  end
79
149
  end
@@ -12,8 +12,8 @@ module Lab
12
12
  program_id = ''
13
13
  if measure.obs_id.present?
14
14
  obs = Observation.unscope(where: :obs_group_id).find(measure.obs_id)
15
- encounter = Encounter.find_by(encounter_id: obs&.encounter_id)
16
- program_id = encounter&.program_id
15
+ encounter = Encounter.find(obs.encounter_id)
16
+ program_id = encounter.program_id
17
17
  end
18
18
 
19
19
  {
@@ -41,8 +41,8 @@ module Lab
41
41
  year = format_year(date.year)
42
42
  month = format_month(date.month)
43
43
  day = format_day(date.day)
44
- #time_acc_generated = Time.now.strftime('%H%M')
45
- "X#{site_code}#{year}#{month}#{day}#{counter}"
44
+ time_acc_generated = Time.now.strftime('%H%M')
45
+ "X#{site_code}#{year}#{month}#{day}#{counter}#{time_acc_generated}"
46
46
  end
47
47
 
48
48
  def format_year(year)
@@ -14,12 +14,9 @@ module Lab
14
14
  date_received: params[:date_received])
15
15
  end
16
16
 
17
- def acknowledgements_pending_sync(batch_size, start_date: nil)
18
- query = Lab::LabAcknowledgement.joins(:order).where(pushed: false)
19
-
20
- query = query.where('orders.date_created >= ?', start_date) if start_date
21
-
22
- query.limit(batch_size)
17
+ def acknowledgements_pending_sync(batch_size)
18
+ Lab::LabAcknowledgement.where(pushed: false)
19
+ .limit(batch_size)
23
20
  end
24
21
 
25
22
  def push_acknowledgement(acknowledgement, lims_api)
@@ -33,8 +30,7 @@ module Lab
33
30
  Rails.logger.info("Updating acknowledgement ##{acknowledgement_dto[:tracking_number]} in LIMS")
34
31
  response = lims_api.acknowledge(acknowledgement_dto)
35
32
  Rails.logger.info("Info #{response}")
36
- if ['results already delivered for test name given', 'test result acknowledged successfully',
37
- 'test result already acknowledged electronically at facility'].include?(response['message'])
33
+ if ['results already delivered for test name given', 'test result acknowledged successfully', 'test result already acknowledged electronically at facility'].include?(response['message'])
38
34
  acknowledgement.pushed = true
39
35
  acknowledgement.date_pushed = Time.now
40
36
  acknowledgement.save!
@@ -4,22 +4,20 @@ module Lab
4
4
  module Lims
5
5
  # This class is responsible for handling the acknowledgement of lab orders
6
6
  class AcknowledgementWorker
7
- attr_reader :lims_api, :start_date
7
+ attr_reader :lims_api
8
8
 
9
9
  include Utils # for logger
10
10
 
11
11
  SECONDS_TO_WAIT_FOR_ORDERS = 30
12
12
 
13
- def initialize(lims_api, start_date: nil)
13
+ def initialize(lims_api)
14
14
  @lims_api = lims_api
15
- @start_date = start_date
16
15
  end
17
16
 
18
17
  def push_acknowledgement(batch_size: 1000, wait: false)
19
18
  loop do
20
19
  logger.info('Looking for new acknowledgements to push to LIMS...')
21
- acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size,
22
- start_date: start_date).all
20
+ acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size).all
23
21
 
24
22
  logger.debug("Found #{acknowledgements.size} acknowledgements...")
25
23
  acknowledgements.each do |acknowledgement|
@@ -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, start_date: nil, **_kwargs)
78
- orders_pending_updates(patient_id, start_date: start_date).each do |order|
77
+ def consume_orders(*_args, patient_id: nil, **_kwargs)
78
+ orders_pending_updates(patient_id).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
 
@@ -510,52 +577,60 @@ module Lab
510
577
  }
511
578
  end
512
579
 
513
- def orders_pending_updates(patient_id = nil, start_date: nil)
580
+ def orders_pending_updates(patient_id = nil)
514
581
  Rails.logger.info('Looking for orders that need to be updated...')
515
582
  orders = {}
516
583
 
517
- orders_without_specimen(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
518
- orders_without_results(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
519
- orders_without_reason(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
584
+ orders_without_specimen(patient_id).each { |order| orders[order.order_id] = order }
585
+ orders_without_results(patient_id).each { |order| orders[order.order_id] = order }
586
+ orders_without_reason(patient_id).each { |order| orders[order.order_id] = order }
520
587
 
521
588
  orders.values
522
589
  end
523
590
 
524
- def orders_without_specimen(patient_id = nil, start_date: nil)
591
+ def orders_without_specimen(patient_id = nil)
525
592
  Rails.logger.debug('Looking for orders without a specimen')
526
593
  unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
527
- .pluck(:concept_id)
594
+ .select(:concept_id)
528
595
  orders = Lab::LabOrder.where(concept_id: unknown_specimen)
529
- .joins('LEFT JOIN lab_lims_order_mappings ON lab_lims_order_mappings.lims_id = orders.accession_number')
530
- .where('lab_lims_order_mappings.lims_id IS NULL')
596
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
531
597
  orders = orders.where(patient_id:) if patient_id
532
- orders = orders.where('orders.date_created >= ?', start_date) if start_date
533
598
 
534
599
  orders
535
600
  end
536
601
 
537
- def orders_without_results(patient_id = nil, start_date: nil)
602
+ def orders_without_results(patient_id = nil)
538
603
  Rails.logger.debug('Looking for orders without a result')
539
604
  # Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
540
605
  # .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id).where("pulled_at IS NULL"))
541
- orders = Lab::OrdersSearchService.find_orders_without_results(patient_id:)
542
- .joins('INNER JOIN lab_lims_order_mappings ON lab_lims_order_mappings.order_id = orders.order_id')
543
- orders = orders.where('orders.date_created >= ?', start_date) if start_date
544
- orders
606
+ Lab::OrdersSearchService.find_orders_without_results(patient_id:)
607
+ .where(order_id: Lab::LimsOrderMapping.select(:order_id))
545
608
  end
546
609
 
547
- def orders_without_reason(patient_id = nil, start_date: nil)
610
+ def orders_without_reason(patient_id = nil)
548
611
  Rails.logger.debug('Looking for orders without a reason for test')
549
612
  orders = Lab::LabOrder.joins(:reason_for_test)
550
613
  .merge(Observation.where(value_coded: nil, value_text: nil))
551
614
  .limit(1000)
552
- .joins('LEFT JOIN lab_lims_order_mappings ON lab_lims_order_mappings.lims_id = orders.accession_number')
553
- .where('lab_lims_order_mappings.lims_id IS NULL')
615
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
554
616
  orders = orders.where(patient_id:) if patient_id
555
- orders = orders.where('orders.date_created >= ?', start_date) if start_date
556
617
 
557
618
  orders
558
619
  end
620
+
621
+ # Converts ISO 8601 timestamp to YYYYMMDDHHmmss format
622
+ def convert_timestamp_to_key(timestamp)
623
+ return timestamp if timestamp.nil? || timestamp.empty?
624
+
625
+ begin
626
+ # Parse ISO 8601 timestamp and format as YYYYMMDDHHmmss
627
+ Time.parse(timestamp).strftime('%Y%m%d%H%M%S')
628
+ rescue StandardError => e
629
+ Rails.logger.warn("Failed to parse timestamp '#{timestamp}': #{e.message}")
630
+ # Fallback: remove all non-digits
631
+ timestamp.to_s.gsub(/\D/, '')
632
+ end
633
+ end
559
634
  end
560
635
  end
561
636
  end
@@ -109,7 +109,6 @@ module Lab
109
109
  end
110
110
 
111
111
  def format_sample_type(name)
112
- return 'not_specified' if name.nil?
113
112
  return 'not_specified' if name.casecmp?('Unknown')
114
113
 
115
114
  return 'CSF' if name.casecmp?('Cerebrospinal Fluid')