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 +4 -4
- data/app/controllers/lab/orders_controller.rb +4 -22
- 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/serializers/lab/result_serializer.rb +2 -2
- data/app/services/lab/accession_number_service.rb +2 -2
- data/app/services/lab/acknowledgement_service.rb +4 -8
- data/app/services/lab/lims/acknowledgement_worker.rb +3 -5
- data/app/services/lab/lims/api/rest_api.rb +100 -25
- data/app/services/lab/lims/order_serializer.rb +0 -1
- data/app/services/lab/lims/pull_worker.rb +172 -4
- data/app/services/lab/lims/push_worker.rb +15 -26
- data/app/services/lab/lims/worker.rb +13 -13
- data/app/services/lab/metadata.rb +1 -0
- data/app/services/lab/orders_service.rb +223 -24
- data/app/services/lab/results_service.rb +1 -5
- data/app/services/lab/tests_service.rb +48 -4
- data/db/migrate/20260226065149_create_lab_status_concepts.rb +80 -0
- data/lib/lab/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfef9418fc9d94d233f24d04883623d67e0c59c2aacea66bc15d3cca61192f78
|
|
4
|
+
data.tar.gz: 2d6324bf11f30c87398819042e3a0835275596961bfd838d088a63ae7a4f93c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
81
|
-
|
|
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)
|
data/app/models/lab/lab_order.rb
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
data/app/models/lab/lab_test.rb
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
23
|
-
program_id: encounter
|
|
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.
|
|
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
|
-
|
|
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.
|
|
16
|
-
program_id = encounter
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
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
|
|
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
|
|
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,
|
|
78
|
-
orders_pending_updates(patient_id
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
87
|
-
Rails.logger.
|
|
88
|
-
|
|
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
|
|
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
|
|
518
|
-
orders_without_results(patient_id
|
|
519
|
-
orders_without_reason(patient_id
|
|
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
|
|
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
|
-
.
|
|
594
|
+
.select(:concept_id)
|
|
528
595
|
orders = Lab::LabOrder.where(concept_id: unknown_specimen)
|
|
529
|
-
.
|
|
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
|
|
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
|
-
|
|
542
|
-
|
|
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
|
|
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
|
-
.
|
|
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
|