his_emr_api_lab 2.1.9.pre.alpha → 2.1.9.pre.beta
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 +4 -17
- data/app/controllers/lab/orders_controller.rb +1 -2
- data/app/jobs/lab/process_lab_result_job.rb +4 -1
- data/app/jobs/lab/update_patient_orders_job.rb +50 -1
- data/app/jobs/lab/void_order_job.rb +6 -2
- data/app/models/lab/lab_order.rb +8 -23
- data/app/models/lab/lab_result.rb +2 -2
- data/app/models/lab/lab_test.rb +2 -21
- data/app/serializers/lab/lab_order_serializer.rb +4 -74
- data/app/services/lab/lims/api/rest_api.rb +36 -124
- data/app/services/lab/lims/order_serializer.rb +39 -29
- data/app/services/lab/lims/pull_worker.rb +0 -169
- data/app/services/lab/lims/utils.rb +2 -1
- data/app/services/lab/metadata.rb +0 -1
- data/app/services/lab/notification_service.rb +2 -1
- data/app/services/lab/orders_service.rb +24 -223
- data/app/services/lab/results_service.rb +1 -1
- data/app/services/lab/tests_service.rb +4 -48
- data/app/services/lab/user_service.rb +3 -2
- data/lib/lab/version.rb +1 -1
- metadata +1 -2
- data/db/migrate/20260226065149_create_lab_status_concepts.rb +0 -80
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6c20f1ba07edd60ba5795eaaef1ca3f34395060b1f9ed0160a5513777d657d0
|
|
4
|
+
data.tar.gz: d16b695b3592233474fa9bbc2f90c85db7d04161a02f8107c56f92fd4e82a9fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 035cf4578ee6f60ba3406a66f88ea33cc54ea7d85ab38524b7e6ae54c9b4b80e03fe2938fc34d047e72df9423b71b8eba1f40004a8fd688555b56c3aebb81605
|
|
7
|
+
data.tar.gz: feda70dff8797923f46078a42429b274d7562eb5105a5d43c1d746f744436c6c516847aa2ef68d7b235f1d1a216981e10d4c707bc6f4fb45372d6b867c412a2a
|
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 '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 lab
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Or install it yourself as:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
$ gem install
|
|
37
|
+
$ gem install lab
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Finally run:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
$ bundle exec rails
|
|
43
|
+
$ bundle exec rails lab:install
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
## Configuration
|
|
@@ -66,19 +66,6 @@ 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
|
-
|
|
82
69
|
## License
|
|
83
70
|
|
|
84
71
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -52,8 +52,7 @@ module Lab
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def order_status
|
|
55
|
-
order_params = params.permit(:tracking_number, :status, :status_time, :comments
|
|
56
|
-
updated_by: [:first_name, :last_name, :id, :phone_number])
|
|
55
|
+
order_params = params.permit(:tracking_number, :status, :status_time, :comments)
|
|
57
56
|
OrdersService.update_order_status(order_params)
|
|
58
57
|
render json: { message: "Status for order #{order_params['tracking_number']} successfully updated" }, status: :ok
|
|
59
58
|
end
|
|
@@ -7,8 +7,11 @@ module Lab
|
|
|
7
7
|
queue_as :default
|
|
8
8
|
def perform(results_obs_id, serializer, result_enter_by)
|
|
9
9
|
Rails.logger.info("Lab::ProcessLabResultJob: Processing result completion for #{serializer}")
|
|
10
|
+
# set location context for the job based on the order's encounter to ensure proper context for any operations performed in the job
|
|
10
11
|
results_obs = Lab::LabResult.unscoped.find(results_obs_id)
|
|
12
|
+
encounter = Encounter.unscoped.find_by(encounter_id: results_obs.encounter_id)
|
|
13
|
+
Location.current = Location.find(encounter.location_id) if encounter&.location_id
|
|
11
14
|
Lab::ResultsService.process_result_completion(results_obs, serializer, result_enter_by)
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
|
-
end
|
|
17
|
+
end
|
|
@@ -10,7 +10,8 @@ module Lab
|
|
|
10
10
|
Rails.logger.info('Initialising LIMS REST API...')
|
|
11
11
|
|
|
12
12
|
User.current = Lab::Lims::Utils.lab_user
|
|
13
|
-
|
|
13
|
+
# Set location from patient's most recent encounter to ensure proper context
|
|
14
|
+
set_location_from_patient_encounter(patient_id)
|
|
14
15
|
|
|
15
16
|
lockfile = Rails.root.join('tmp', "update-patient-orders-#{patient_id}.lock")
|
|
16
17
|
|
|
@@ -28,5 +29,53 @@ module Lab
|
|
|
28
29
|
|
|
29
30
|
File.unlink(lockfile) if done
|
|
30
31
|
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def set_location_from_patient_encounter(patient_id)
|
|
36
|
+
Rails.logger.info("Setting location context for patient #{patient_id}")
|
|
37
|
+
|
|
38
|
+
# Strategy 1: Find location from patient's most recent order (ANY order type)
|
|
39
|
+
recent_order = Order.unscoped
|
|
40
|
+
.where(patient_id: patient_id)
|
|
41
|
+
.order(start_date: :desc)
|
|
42
|
+
.first
|
|
43
|
+
|
|
44
|
+
if recent_order
|
|
45
|
+
encounter = Encounter.unscoped.find_by(encounter_id: recent_order.encounter_id)
|
|
46
|
+
if encounter&.location_id
|
|
47
|
+
Location.current = Location.find(encounter.location_id)
|
|
48
|
+
Rails.logger.info("Location set from patient's recent order: #{Location.current.name} (ID: #{Location.current.location_id})")
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Strategy 2: Find location from patient's most recent encounter
|
|
54
|
+
recent_encounter = Encounter.unscoped
|
|
55
|
+
.where(patient_id: patient_id)
|
|
56
|
+
.order(encounter_datetime: :desc)
|
|
57
|
+
.first
|
|
58
|
+
|
|
59
|
+
if recent_encounter&.location_id
|
|
60
|
+
Location.current = Location.find(recent_encounter.location_id)
|
|
61
|
+
Rails.logger.info("Location set from patient's recent encounter: #{Location.current.name} (ID: #{Location.current.location_id})")
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Fallback chain: Try multiple options to ensure location is ALWAYS set
|
|
66
|
+
Location.current ||= begin
|
|
67
|
+
Location.current_health_center
|
|
68
|
+
rescue StandardError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
Location.current ||= Location.first
|
|
72
|
+
|
|
73
|
+
if Location.current
|
|
74
|
+
Rails.logger.info("Location set to fallback: #{Location.current.name} (ID: #{Location.current.location_id})")
|
|
75
|
+
else
|
|
76
|
+
Rails.logger.error('CRITICAL: Could not set Location.current - no locations found in database!')
|
|
77
|
+
raise 'No locations available in database'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
31
80
|
end
|
|
32
81
|
end
|
|
@@ -8,10 +8,14 @@ module Lab
|
|
|
8
8
|
Rails.logger.info("Voiding order ##{order_id} in LIMS")
|
|
9
9
|
|
|
10
10
|
User.current = Lab::Lims::Utils.lab_user
|
|
11
|
-
|
|
11
|
+
# Set location from order's encounter to ensure proper context
|
|
12
|
+
order = Lab::LabOrder.unscoped.find(order_id)
|
|
13
|
+
encounter = Encounter.unscoped.find_by(encounter_id: order.encounter_id)
|
|
14
|
+
Location.current = Location.find(encounter.location_id) if encounter&.location_id
|
|
15
|
+
Location.current ||= Location.find_by_name('ART clinic')
|
|
12
16
|
|
|
13
17
|
worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
|
|
14
|
-
worker.push_order(
|
|
18
|
+
worker.push_order(order)
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
end
|
data/app/models/lab/lab_order.rb
CHANGED
|
@@ -8,11 +8,6 @@ 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
|
|
16
11
|
end
|
|
17
12
|
|
|
18
13
|
has_many :tests,
|
|
@@ -49,20 +44,12 @@ module Lab
|
|
|
49
44
|
class_name: '::Lab::LimsOrderMapping',
|
|
50
45
|
foreign_key: :order_id
|
|
51
46
|
|
|
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
|
-
|
|
60
47
|
default_scope do
|
|
61
48
|
joins(:order_type)
|
|
62
49
|
.merge(OrderType.where(name: [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
Lab::Metadata::ORDER_TYPE_NAME,
|
|
51
|
+
Lab::Metadata::HTS_ORDER_TYPE_NAME
|
|
52
|
+
]))
|
|
66
53
|
.where.not(concept_id: ConceptName.where(name: 'Tests ordered').select(:concept_id))
|
|
67
54
|
end
|
|
68
55
|
|
|
@@ -70,13 +57,11 @@ module Lab
|
|
|
70
57
|
scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
|
|
71
58
|
|
|
72
59
|
def self.prefetch_relationships
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:comment_to_fulfiller,
|
|
79
|
-
:tests)
|
|
60
|
+
includes(:reason_for_test,
|
|
61
|
+
:requesting_clinician,
|
|
62
|
+
:target_lab,
|
|
63
|
+
:comment_to_fulfiller,
|
|
64
|
+
tests: [:result])
|
|
80
65
|
end
|
|
81
66
|
end
|
|
82
67
|
end
|
data/app/models/lab/lab_test.rb
CHANGED
|
@@ -6,33 +6,14 @@ 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
|
-
|
|
18
9
|
has_one :result,
|
|
19
|
-
-> {
|
|
10
|
+
-> { where(concept: ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)) },
|
|
20
11
|
class_name: 'Lab::LabResult',
|
|
21
12
|
foreign_key: :obs_group_id
|
|
22
13
|
|
|
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
|
-
|
|
33
14
|
def void(reason)
|
|
34
15
|
result&.void(reason)
|
|
35
|
-
super
|
|
16
|
+
super(reason)
|
|
36
17
|
end
|
|
37
18
|
end
|
|
38
19
|
end
|
|
@@ -9,7 +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
|
-
encounter
|
|
12
|
+
# Use unscoped to find encounter across all locations
|
|
13
|
+
encounter = Encounter.unscoped.find_by_encounter_id(order.encounter_id)
|
|
13
14
|
program = Program.find_by_program_id(encounter.program_id)
|
|
14
15
|
|
|
15
16
|
ActiveSupport::HashWithIndifferentAccess.new(
|
|
@@ -36,10 +37,8 @@ module Lab
|
|
|
36
37
|
name: concept_name(reason_for_test&.value_coded)
|
|
37
38
|
},
|
|
38
39
|
delivery_mode: order&.lims_acknowledgement_status&.acknowledgement_type,
|
|
39
|
-
order_status: latest_order_status(order),
|
|
40
|
-
order_status_trail: serialize_order_status_trail(order),
|
|
41
40
|
tests: tests.map do |test|
|
|
42
|
-
result_obs = test.
|
|
41
|
+
result_obs = test.children.first
|
|
43
42
|
|
|
44
43
|
{
|
|
45
44
|
id: test.obs_id,
|
|
@@ -47,9 +46,7 @@ module Lab
|
|
|
47
46
|
uuid: test.uuid,
|
|
48
47
|
name: concept_name(test.value_coded),
|
|
49
48
|
test_method: test_method(order, test.value_coded),
|
|
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
|
+
result: result_obs && ResultSerializer.serialize(result_obs)
|
|
53
50
|
}
|
|
54
51
|
end
|
|
55
52
|
}
|
|
@@ -78,72 +75,5 @@ module Lab
|
|
|
78
75
|
.select(:concept_id)
|
|
79
76
|
LabTest.unscoped.where(concept:, order:, voided: true)
|
|
80
77
|
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
|
|
148
78
|
end
|
|
149
79
|
end
|
|
@@ -77,82 +77,15 @@ module Lab
|
|
|
77
77
|
def consume_orders(*_args, patient_id: nil, **_kwargs)
|
|
78
78
|
orders_pending_updates(patient_id).each do |order|
|
|
79
79
|
order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
|
|
80
|
-
|
|
81
|
-
|
|
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")
|
|
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))
|
|
144
82
|
end
|
|
145
|
-
|
|
146
|
-
# Try to fetch results if available
|
|
147
83
|
if order_dto['test_results'].empty?
|
|
148
84
|
begin
|
|
149
85
|
patch_order_dto_with_lims_results!(order_dto, find_lims_results(order.accession_number))
|
|
150
|
-
rescue InvalidParameters => e
|
|
151
|
-
Rails.logger.
|
|
152
|
-
|
|
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
|
|
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
|
|
156
89
|
end
|
|
157
90
|
end
|
|
158
91
|
|
|
@@ -294,7 +227,7 @@ module Lab
|
|
|
294
227
|
{
|
|
295
228
|
order: {
|
|
296
229
|
tracking_number: order_dto.fetch(:tracking_number),
|
|
297
|
-
district:
|
|
230
|
+
district: order_dto.fetch(:districy),
|
|
298
231
|
health_facility_name: order_dto.fetch(:sending_facility),
|
|
299
232
|
sending_facility: order_dto.fetch(:sending_facility),
|
|
300
233
|
arv_number: order_dto.fetch(:patient).fetch(:arv_number),
|
|
@@ -348,24 +281,17 @@ module Lab
|
|
|
348
281
|
status: 'specimen_collected',
|
|
349
282
|
time_updated: date_updated,
|
|
350
283
|
sample_type: order_dto.fetch(:sample_type_map),
|
|
351
|
-
updated_by: status.fetch(:updated_by)
|
|
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
|
+
]
|
|
352
292
|
}
|
|
353
293
|
end
|
|
354
294
|
|
|
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
|
-
|
|
369
295
|
##
|
|
370
296
|
# Extracts sample drawn status from an OrderDto
|
|
371
297
|
def sample_drawn_status(order_dto)
|
|
@@ -511,23 +437,25 @@ module Lab
|
|
|
511
437
|
order_dto['test_results'].each do |test_name, results|
|
|
512
438
|
Rails.logger.info("Pushing result for order ##{order_dto['tracking_number']}")
|
|
513
439
|
in_authenticated_session do |headers|
|
|
514
|
-
params = make_update_test_params(order_dto
|
|
440
|
+
params = make_update_test_params(order_dto, test_name, results)
|
|
515
441
|
|
|
516
|
-
RestClient.
|
|
442
|
+
RestClient.put(expand_uri("tests/#{order_dto['tracking_number']}", api_version: 'v2'), params, headers)
|
|
517
443
|
end
|
|
518
444
|
end
|
|
519
445
|
end
|
|
520
446
|
|
|
521
|
-
def make_update_test_params(
|
|
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
|
|
522
450
|
{
|
|
523
451
|
test_status:,
|
|
524
452
|
time_updated: results['result_date'],
|
|
525
453
|
test_type: {
|
|
526
|
-
name:
|
|
527
|
-
nlims_code:
|
|
454
|
+
name: concept&.test_catalogue_name,
|
|
455
|
+
nlims_code: concept&.nlims_code
|
|
528
456
|
},
|
|
529
|
-
test_results: results['results'].map do |
|
|
530
|
-
|
|
457
|
+
test_results: results['results'].map do |measure_name, value|
|
|
458
|
+
measure_value = value['result_value']
|
|
531
459
|
{
|
|
532
460
|
measure: {
|
|
533
461
|
name: measure_name,
|
|
@@ -539,6 +467,18 @@ module Lab
|
|
|
539
467
|
result_date: results['result_date']
|
|
540
468
|
}
|
|
541
469
|
}
|
|
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
|
+
}
|
|
542
482
|
end
|
|
543
483
|
}
|
|
544
484
|
end
|
|
@@ -617,20 +557,6 @@ module Lab
|
|
|
617
557
|
|
|
618
558
|
orders
|
|
619
559
|
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
|
|
634
560
|
end
|
|
635
561
|
end
|
|
636
562
|
end
|
|
@@ -759,7 +685,7 @@ module Lab
|
|
|
759
685
|
def make_create_params(order_dto)
|
|
760
686
|
{
|
|
761
687
|
tracking_number: order_dto.fetch(:tracking_number),
|
|
762
|
-
district:
|
|
688
|
+
district: order_dto.fetch(:districy),
|
|
763
689
|
health_facility_name: order_dto.fetch(:sending_facility),
|
|
764
690
|
first_name: order_dto.fetch(:patient).fetch(:first_name),
|
|
765
691
|
last_name: order_dto.fetch(:patient).fetch(:last_name),
|
|
@@ -797,20 +723,6 @@ module Lab
|
|
|
797
723
|
}
|
|
798
724
|
end
|
|
799
725
|
|
|
800
|
-
def current_district
|
|
801
|
-
health_centre = Location.current_health_center
|
|
802
|
-
raise 'Current health centre not set' unless health_centre
|
|
803
|
-
|
|
804
|
-
district = health_centre.district || Lab::Lims::Config.application['district']
|
|
805
|
-
|
|
806
|
-
unless district
|
|
807
|
-
health_centre_name = "##{health_centre.id} - #{health_centre.name}"
|
|
808
|
-
raise "Current health centre district not set: #{health_centre_name}"
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
district
|
|
812
|
-
end
|
|
813
|
-
|
|
814
726
|
##
|
|
815
727
|
# Extracts sample drawn status from an OrderDto
|
|
816
728
|
def sample_drawn_status(order_dto)
|