his_emr_api_lab 2.1.9.pre.beta → 2.2.2
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/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 +3 -3
- 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 +101 -16
- 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/worker.rb +13 -13
- data/app/services/lab/metadata.rb +1 -0
- data/app/services/lab/orders_service.rb +224 -23
- data/app/services/lab/tests_service.rb +47 -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: 5430975d278d3f06b169b906ca5c471f0ad632dc158540f358545f4da2aa05d1
|
|
4
|
+
data.tar.gz: e75574b9ac3023e974d70ef021ae158ded0614a8d379eec120124f9e45891479
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3f869dd69e746a19f35be7741f717c1b657ae6d8df6a55cf72a427f70fbdee62c76d61af605ac81f248a376428db8fab67bd6ef9fa623ceb2ca0bec45678cd6
|
|
7
|
+
data.tar.gz: a7c3ef0003f803d83f47337f3d4ba4f9388a4b40962f6b95b36d14c904ab361280971e6689f8fcbb3cd48f41ba4863722d422c6d884e44e346f0e48626cacedc
|
data/README.md
CHANGED
|
@@ -22,25 +22,25 @@ For details on how to perform these operations please see the
|
|
|
22
22
|
Add this line to your application's Gemfile:
|
|
23
23
|
|
|
24
24
|
```ruby
|
|
25
|
-
gem '
|
|
25
|
+
gem 'his_emr_api_lab', git: 'https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab', branch: 'development'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
And then execute:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
$ bundle install
|
|
31
|
+
$ bundle install
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Or install it yourself as:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
$ gem install
|
|
37
|
+
$ gem install his_emr_api_lab
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Finally run:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
$ bundle exec rails
|
|
43
|
+
$ bundle exec rails his_emr_api_lab:install
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
## Configuration
|
|
@@ -66,6 +66,19 @@ but too much a departure from it is frowned upon. For example, you will be forgi
|
|
|
66
66
|
for writing a method with 15 to 20 lines if you clearly justify why you couldn't
|
|
67
67
|
break that method into multiple smaller methods.
|
|
68
68
|
|
|
69
|
+
## Publishing
|
|
70
|
+
|
|
71
|
+
To publish a new version of the gem, first update the version number in
|
|
72
|
+
`lib/lab/version.rb` and then run the following command:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ gem build his_emr_api_lab.gemspec
|
|
76
|
+
$ gem push his_emr_api_lab-<version>.gem
|
|
77
|
+
```
|
|
78
|
+
Make sure to replace `<version>` with the version number you set in `lib/lab/version.rb`.
|
|
79
|
+
|
|
80
|
+
NB: You need to have an account on [rubygems.org](https://rubygems.org/) and permission to publish gems.
|
|
81
|
+
|
|
69
82
|
## License
|
|
70
83
|
|
|
71
84
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Lab
|
|
4
4
|
class OrdersController < ApplicationController
|
|
5
|
+
skip_before_action :authenticate, only: %i[order_status order_result summary]
|
|
5
6
|
before_action :authenticate_request, only: %i[order_status order_result summary]
|
|
6
7
|
|
|
7
8
|
def create
|
|
@@ -52,7 +53,8 @@ module Lab
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def order_status
|
|
55
|
-
order_params = params.permit(:tracking_number, :status, :status_time, :comments
|
|
56
|
+
order_params = params.permit(:tracking_number, :status, :status_time, :comments, :status_id,
|
|
57
|
+
updated_by: [:first_name, :last_name, :id, :phone_number])
|
|
56
58
|
OrdersService.update_order_status(order_params)
|
|
57
59
|
render json: { message: "Status for order #{order_params['tracking_number']} successfully updated" }, status: :ok
|
|
58
60
|
end
|
|
@@ -76,8 +78,26 @@ module Lab
|
|
|
76
78
|
private
|
|
77
79
|
|
|
78
80
|
def authenticate_request
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
header = request.headers['Authorization']
|
|
82
|
+
content = header.split(' ')
|
|
83
|
+
auth_scheme = content.first
|
|
84
|
+
unless header
|
|
85
|
+
errors = ['Authorization token required']
|
|
86
|
+
render json: { errors: errors }, status: :unauthorized
|
|
87
|
+
return false
|
|
88
|
+
end
|
|
89
|
+
unless auth_scheme == 'Bearer'
|
|
90
|
+
errors = ['Authorization token bearer scheme required']
|
|
91
|
+
render json: { errors: errors }, status: :unauthorized
|
|
92
|
+
return false
|
|
93
|
+
end
|
|
94
|
+
process_token(content.last)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def process_token(token)
|
|
98
|
+
browser = Browser.new(request.user_agent)
|
|
99
|
+
decoded = Lab::JsonWebTokenService.decode(token, request.remote_ip + browser.name + browser.version)
|
|
100
|
+
user(decoded)
|
|
81
101
|
end
|
|
82
102
|
|
|
83
103
|
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
|
|
@@ -9,9 +9,8 @@ module Lab
|
|
|
9
9
|
reason_for_test ||= order.reason_for_test
|
|
10
10
|
target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
program = Program.find_by_program_id(encounter.program_id)
|
|
12
|
+
encounter = Encounter.find_by_encounter_id(order.encounter_id)
|
|
13
|
+
program = Program.find_by_program_id(encounter&.program_id)
|
|
15
14
|
|
|
16
15
|
ActiveSupport::HashWithIndifferentAccess.new(
|
|
17
16
|
{
|
|
@@ -20,8 +19,8 @@ module Lab
|
|
|
20
19
|
order_id: order.order_id, # Deprecated: Link to :id
|
|
21
20
|
encounter_id: order.encounter_id,
|
|
22
21
|
order_date: order.start_date,
|
|
23
|
-
location_id: encounter
|
|
24
|
-
program_id: encounter
|
|
22
|
+
location_id: encounter&.location_id,
|
|
23
|
+
program_id: encounter&.program_id,
|
|
25
24
|
program_name: program&.name,
|
|
26
25
|
patient_id: order.patient_id,
|
|
27
26
|
accession_number: order.accession_number,
|
|
@@ -37,8 +36,10 @@ module Lab
|
|
|
37
36
|
name: concept_name(reason_for_test&.value_coded)
|
|
38
37
|
},
|
|
39
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),
|
|
40
41
|
tests: tests.map do |test|
|
|
41
|
-
result_obs = test.
|
|
42
|
+
result_obs = test.result
|
|
42
43
|
|
|
43
44
|
{
|
|
44
45
|
id: test.obs_id,
|
|
@@ -46,7 +47,9 @@ module Lab
|
|
|
46
47
|
uuid: test.uuid,
|
|
47
48
|
name: concept_name(test.value_coded),
|
|
48
49
|
test_method: test_method(order, test.value_coded),
|
|
49
|
-
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)
|
|
50
53
|
}
|
|
51
54
|
end
|
|
52
55
|
}
|
|
@@ -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
|
|
@@ -11,9 +11,9 @@ module Lab
|
|
|
11
11
|
concept_name = get_test_catalog_concept_name(measure.concept_id)
|
|
12
12
|
program_id = ''
|
|
13
13
|
if measure.obs_id.present?
|
|
14
|
-
obs = Observation.unscope(where: :obs_group_id).
|
|
15
|
-
encounter = Encounter.
|
|
16
|
-
program_id = encounter
|
|
14
|
+
obs = Observation.unscope(where: :obs_group_id).find_by(obs_id: measure.obs_id)
|
|
15
|
+
encounter = Encounter.find_by(encounter_id: obs&.encounter_id)
|
|
16
|
+
program_id = encounter&.program_id
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
{
|
|
@@ -14,9 +14,12 @@ module Lab
|
|
|
14
14
|
date_received: params[:date_received])
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def acknowledgements_pending_sync(batch_size)
|
|
18
|
-
Lab::LabAcknowledgement.where(pushed: false)
|
|
19
|
-
|
|
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)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def push_acknowledgement(acknowledgement, lims_api)
|
|
@@ -30,7 +33,8 @@ module Lab
|
|
|
30
33
|
Rails.logger.info("Updating acknowledgement ##{acknowledgement_dto[:tracking_number]} in LIMS")
|
|
31
34
|
response = lims_api.acknowledge(acknowledgement_dto)
|
|
32
35
|
Rails.logger.info("Info #{response}")
|
|
33
|
-
if ['results already delivered for test name given', 'test result acknowledged successfully',
|
|
36
|
+
if ['results already delivered for test name given', 'test result acknowledged successfully',
|
|
37
|
+
'test result already acknowledged electronically at facility'].include?(response['message'])
|
|
34
38
|
acknowledgement.pushed = true
|
|
35
39
|
acknowledgement.date_pushed = Time.now
|
|
36
40
|
acknowledgement.save!
|
|
@@ -4,20 +4,22 @@ 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, :start_date
|
|
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, start_date: nil)
|
|
14
14
|
@lims_api = lims_api
|
|
15
|
+
@start_date = start_date
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def push_acknowledgement(batch_size: 1000, wait: false)
|
|
18
19
|
loop do
|
|
19
20
|
logger.info('Looking for new acknowledgements to push to LIMS...')
|
|
20
|
-
acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size
|
|
21
|
+
acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size,
|
|
22
|
+
start_date: start_date).all
|
|
21
23
|
|
|
22
24
|
logger.debug("Found #{acknowledgements.size} acknowledgements...")
|
|
23
25
|
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, **_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
|
-
|
|
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
|
|
|
@@ -517,46 +584,64 @@ module Lab
|
|
|
517
584
|
}
|
|
518
585
|
end
|
|
519
586
|
|
|
520
|
-
def orders_pending_updates(patient_id = nil)
|
|
587
|
+
def orders_pending_updates(patient_id = nil, start_date: nil)
|
|
521
588
|
Rails.logger.info('Looking for orders that need to be updated...')
|
|
522
589
|
orders = {}
|
|
523
590
|
|
|
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 }
|
|
591
|
+
orders_without_specimen(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
|
|
592
|
+
orders_without_results(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
|
|
593
|
+
orders_without_reason(patient_id, start_date: start_date).each { |order| orders[order.order_id] = order }
|
|
527
594
|
|
|
528
595
|
orders.values
|
|
529
596
|
end
|
|
530
597
|
|
|
531
|
-
def orders_without_specimen(patient_id = nil)
|
|
598
|
+
def orders_without_specimen(patient_id = nil, start_date: nil)
|
|
532
599
|
Rails.logger.debug('Looking for orders without a specimen')
|
|
533
600
|
unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
|
|
534
601
|
.select(:concept_id)
|
|
535
602
|
orders = Lab::LabOrder.where(concept_id: unknown_specimen)
|
|
536
603
|
.where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
|
|
537
604
|
orders = orders.where(patient_id:) if patient_id
|
|
605
|
+
orders = orders.where('orders.date_created >= ?', start_date) if start_date
|
|
538
606
|
|
|
539
607
|
orders
|
|
540
608
|
end
|
|
541
609
|
|
|
542
|
-
def orders_without_results(patient_id = nil)
|
|
610
|
+
def orders_without_results(patient_id = nil, start_date: nil)
|
|
543
611
|
Rails.logger.debug('Looking for orders without a result')
|
|
544
612
|
# Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
|
|
545
613
|
# .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id).where("pulled_at IS NULL"))
|
|
546
|
-
Lab::OrdersSearchService.find_orders_without_results(patient_id:)
|
|
547
|
-
|
|
614
|
+
orders = Lab::OrdersSearchService.find_orders_without_results(patient_id:)
|
|
615
|
+
.where(order_id: Lab::LimsOrderMapping.select(:order_id))
|
|
616
|
+
orders = orders.where('orders.date_created >= ?', start_date) if start_date
|
|
617
|
+
orders
|
|
548
618
|
end
|
|
549
619
|
|
|
550
|
-
def orders_without_reason(patient_id = nil)
|
|
620
|
+
def orders_without_reason(patient_id = nil, start_date: nil)
|
|
551
621
|
Rails.logger.debug('Looking for orders without a reason for test')
|
|
552
622
|
orders = Lab::LabOrder.joins(:reason_for_test)
|
|
553
623
|
.merge(Observation.where(value_coded: nil, value_text: nil))
|
|
554
624
|
.limit(1000)
|
|
555
625
|
.where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
|
|
556
626
|
orders = orders.where(patient_id:) if patient_id
|
|
627
|
+
orders = orders.where('orders.date_created >= ?', start_date) if start_date
|
|
557
628
|
|
|
558
629
|
orders
|
|
559
630
|
end
|
|
631
|
+
|
|
632
|
+
# Converts ISO 8601 timestamp to YYYYMMDDHHmmss format
|
|
633
|
+
def convert_timestamp_to_key(timestamp)
|
|
634
|
+
return timestamp if timestamp.nil? || timestamp.empty?
|
|
635
|
+
|
|
636
|
+
begin
|
|
637
|
+
# Parse ISO 8601 timestamp and format as YYYYMMDDHHmmss
|
|
638
|
+
Time.parse(timestamp).strftime('%Y%m%d%H%M%S')
|
|
639
|
+
rescue StandardError => e
|
|
640
|
+
Rails.logger.warn("Failed to parse timestamp '#{timestamp}': #{e.message}")
|
|
641
|
+
# Fallback: remove all non-digits
|
|
642
|
+
timestamp.to_s.gsub(/\D/, '')
|
|
643
|
+
end
|
|
644
|
+
end
|
|
560
645
|
end
|
|
561
646
|
end
|
|
562
647
|
end
|