his_emr_api_lab 2.1.0 → 2.1.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/app/controllers/lab/labels_controller.rb +2 -1
- data/app/controllers/lab/orders_controller.rb +14 -25
- data/app/controllers/lab/results_controller.rb +2 -1
- data/app/controllers/lab/specimen_types_controller.rb +0 -1
- data/app/controllers/lab/test_methods_controller.rb +9 -0
- data/app/controllers/lab/test_types_controller.rb +1 -1
- data/app/controllers/lab/tests_controller.rb +1 -3
- data/app/controllers/lab/users_controller.rb +2 -1
- data/app/jobs/lab/process_lab_result_job.rb +13 -0
- data/app/models/lab/lab_order.rb +4 -1
- data/app/models/lab/lab_result.rb +4 -0
- data/app/models/lab/lab_test.rb +1 -1
- data/app/models/lab/order_extension.rb +14 -0
- data/app/serializers/lab/lab_order_serializer.rb +27 -4
- data/app/serializers/lab/result_serializer.rb +10 -2
- data/app/serializers/lab/test_serializer.rb +24 -1
- data/app/services/lab/accession_number_service.rb +2 -2
- data/app/services/lab/acknowledgement_service.rb +4 -1
- data/app/services/lab/concepts_service.rb +77 -22
- data/app/services/lab/lims/acknowledgement_worker.rb +1 -1
- data/app/services/lab/lims/api/rest_api.rb +543 -78
- data/app/services/lab/lims/config.rb +7 -2
- data/app/services/lab/lims/exceptions.rb +7 -6
- data/app/services/lab/lims/migrator.rb +3 -3
- data/app/services/lab/lims/order_dto.rb +1 -1
- data/app/services/lab/lims/order_serializer.rb +28 -7
- data/app/services/lab/lims/pull_worker.rb +6 -6
- data/app/services/lab/lims/push_worker.rb +3 -3
- data/app/services/lab/lims/utils.rb +3 -4
- data/app/services/lab/lims/worker.rb +2 -2
- data/app/services/lab/metadata.rb +4 -0
- data/app/services/lab/notification_service.rb +72 -0
- data/app/services/lab/orders_search_service.rb +11 -5
- data/app/services/lab/orders_service.rb +82 -36
- data/app/services/lab/results_service.rb +32 -17
- data/app/services/lab/tests_service.rb +15 -3
- data/config/routes.rb +1 -0
- data/db/migrate/20260119104240_add_fulfiller_fields_to_orders.rb +11 -0
- data/db/migrate/20260119104241_create_comment_to_fulfiller_concept.rb +50 -0
- data/lib/lab/version.rb +1 -1
- data/lib/mahis_emr_api_lab.rb +6 -0
- metadata +12 -5
- /data/app/services/lab/lims/api/{couchdb_api.rb → couch_db_api.rb} +0 -0
|
@@ -50,10 +50,15 @@ module Lab
|
|
|
50
50
|
30 # Seconds
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def config_file_name
|
|
54
|
+
# if nlims.yml exists return it, else return application.yml
|
|
55
|
+
File.exist?(Rails.root.join('config', 'nlims.yml')) ? 'nlims.yml' : 'application.yml'
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
##
|
|
54
59
|
# Returns LIMS' application.yml configuration file
|
|
55
60
|
def application
|
|
56
|
-
@application ||= YAML.load_file(find_config_path(
|
|
61
|
+
@application ||= YAML.load_file(find_config_path(config_file_name))
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
##
|
|
@@ -65,7 +70,7 @@ module Lab
|
|
|
65
70
|
private
|
|
66
71
|
|
|
67
72
|
def emr_api_application(param, fallback = nil)
|
|
68
|
-
@emr_api_application ||= YAML.load_file(Rails.root.join('config',
|
|
73
|
+
@emr_api_application ||= YAML.load_file(Rails.root.join('config', config_file_name))
|
|
69
74
|
|
|
70
75
|
@emr_api_application.fetch(param) do
|
|
71
76
|
raise ConfigNotFound, "Missing config param: #{param}" unless fallback
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Lab
|
|
4
4
|
module Lims
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
module Exceptions
|
|
6
|
+
class LimsException < StandardError; end
|
|
7
|
+
class DuplicateNHID < LimsException; end
|
|
8
|
+
class MissingAccessionNumber < LimsException; end
|
|
9
|
+
class UnknownSpecimenType < LimsException; end
|
|
10
|
+
class UnknownTestType < LimsException; end
|
|
11
|
+
end
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -29,7 +29,7 @@ require 'lab/lab_test'
|
|
|
29
29
|
require 'lab/lims_order_mapping'
|
|
30
30
|
require 'lab/lims_failed_import'
|
|
31
31
|
|
|
32
|
-
require_relative 'api/
|
|
32
|
+
require_relative 'api/couch_db_api'
|
|
33
33
|
require_relative 'config'
|
|
34
34
|
require_relative 'pull_worker'
|
|
35
35
|
require_relative 'utils'
|
|
@@ -60,8 +60,8 @@ module Lab
|
|
|
60
60
|
##
|
|
61
61
|
# A Lab::Lims::Api object that supports crawling of a LIMS CouchDB instance.
|
|
62
62
|
class CouchDbMigratorApi < Lab::Lims::Api::CouchdbApi
|
|
63
|
-
def initialize(
|
|
64
|
-
super(
|
|
63
|
+
def initialize(*, processes: 1, on_merge_processes: nil, **)
|
|
64
|
+
super(*, **)
|
|
65
65
|
|
|
66
66
|
@processes = processes
|
|
67
67
|
@on_merge_processes = on_merge_processes
|
|
@@ -17,7 +17,7 @@ module Lab
|
|
|
17
17
|
accession_number: self['tracking_number'],
|
|
18
18
|
patient_id:,
|
|
19
19
|
specimen: { concept_id: specimen_type_id },
|
|
20
|
-
tests: self['
|
|
20
|
+
tests: self['tests_map']&.map { |test| { concept_id: test.concept_id } },
|
|
21
21
|
requesting_clinician:,
|
|
22
22
|
date: start_date,
|
|
23
23
|
target_lab: facility_name(self['receiving_facility']),
|
|
@@ -14,16 +14,20 @@ module Lab
|
|
|
14
14
|
|
|
15
15
|
def serialize_order(order)
|
|
16
16
|
serialized_order = Lims::Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
|
|
17
|
-
|
|
18
17
|
Lims::OrderDto.new(
|
|
19
|
-
_id: Lab::LimsOrderMapping.find_by(order:)&.lims_id || serialized_order.accession_number,
|
|
18
|
+
_id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
|
|
20
19
|
tracking_number: serialized_order.accession_number,
|
|
21
20
|
sending_facility: current_facility_name,
|
|
22
21
|
receiving_facility: serialized_order.target_lab,
|
|
23
22
|
tests: serialized_order.tests.map { |test| format_test_name(test.name) },
|
|
23
|
+
tests_map: serialized_order.tests,
|
|
24
24
|
patient: format_patient(serialized_order.patient_id),
|
|
25
25
|
order_location: format_order_location(serialized_order.encounter_id),
|
|
26
26
|
sample_type: format_sample_type(serialized_order.specimen.name),
|
|
27
|
+
sample_type_map: {
|
|
28
|
+
name: format_sample_type(serialized_order.specimen.name),
|
|
29
|
+
nlims_code: Concept.find(serialized_order.specimen.concept_id).nlims_code
|
|
30
|
+
},
|
|
27
31
|
sample_status: format_sample_status(serialized_order.specimen.name),
|
|
28
32
|
sample_statuses: format_sample_status_trail(order),
|
|
29
33
|
test_statuses: format_test_status_trail(order),
|
|
@@ -32,7 +36,8 @@ module Lab
|
|
|
32
36
|
priority: format_sample_priority(serialized_order.reason_for_test.name),
|
|
33
37
|
date_created: serialized_order.order_date,
|
|
34
38
|
test_results: format_test_results(serialized_order),
|
|
35
|
-
type: 'Order'
|
|
39
|
+
type: 'Order',
|
|
40
|
+
clinical_history: serialized_order.comment_to_fulfiller
|
|
36
41
|
)
|
|
37
42
|
end
|
|
38
43
|
|
|
@@ -81,6 +86,8 @@ module Lab
|
|
|
81
86
|
return nil if regimen_data.blank?
|
|
82
87
|
|
|
83
88
|
regimen_data['regimen']
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
def find_arv_number(patient_id)
|
|
@@ -97,6 +104,8 @@ module Lab
|
|
|
97
104
|
return nil if start_date.blank?
|
|
98
105
|
|
|
99
106
|
start_date['earliest_date']
|
|
107
|
+
rescue StandardError
|
|
108
|
+
nil
|
|
100
109
|
end
|
|
101
110
|
|
|
102
111
|
def format_sample_type(name)
|
|
@@ -114,9 +123,12 @@ module Lab
|
|
|
114
123
|
def format_sample_status_trail(order)
|
|
115
124
|
return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
|
|
116
125
|
|
|
117
|
-
user = User.find(order.
|
|
126
|
+
user = User.find(order.creator)
|
|
127
|
+
user = User.find(order.discontinued_by) if Order.columns_hash.key?('discontinued_by') && user.blank?
|
|
128
|
+
|
|
118
129
|
drawn_by = PersonName.find_by_person_id(user.user_id)
|
|
119
|
-
drawn_date = order.discontinued_date || order.start_date
|
|
130
|
+
drawn_date = order.discontinued_date || order.start_date if ['discontinued_date', 'start_date'].all? { |column| order.respond_to?(column) }
|
|
131
|
+
drawn_date ||= order.date_created
|
|
120
132
|
|
|
121
133
|
[
|
|
122
134
|
drawn_date.strftime('%Y%m%d%H%M%S') => {
|
|
@@ -132,7 +144,7 @@ module Lab
|
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
def format_test_status_trail(order)
|
|
135
|
-
tests = order.voided
|
|
147
|
+
tests = order.voided ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
|
|
136
148
|
|
|
137
149
|
tests.each_with_object({}) do |test, trail|
|
|
138
150
|
test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
|
|
@@ -168,7 +180,13 @@ module Lab
|
|
|
168
180
|
order.tests&.each_with_object({}) do |test, results|
|
|
169
181
|
next if test.result.nil? || test.result.empty?
|
|
170
182
|
|
|
171
|
-
|
|
183
|
+
result_obs = Observation.find_by(obs_id: test.result.first.id)
|
|
184
|
+
unless result_obs
|
|
185
|
+
Rails.logger.warn("Observation with obs_id=#{test.result.first.id} not found for test #{test.name} in order #{order.accession_number}")
|
|
186
|
+
next
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
test_creator = User.find(result_obs.creator)
|
|
172
190
|
test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
|
|
173
191
|
|
|
174
192
|
results[format_test_name(test.name)] = {
|
|
@@ -184,6 +202,9 @@ module Lab
|
|
|
184
202
|
id: test_creator.username
|
|
185
203
|
}
|
|
186
204
|
}
|
|
205
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
206
|
+
Rails.logger.error("Failed to format test results for test #{test.name} in order #{order.accession_number}: #{e.message}")
|
|
207
|
+
next
|
|
187
208
|
end
|
|
188
209
|
end
|
|
189
210
|
|
|
@@ -23,7 +23,7 @@ module Lab
|
|
|
23
23
|
lims_api.consume_orders(from: last_seq, limit: batch_size, **) do |order_dto, context|
|
|
24
24
|
logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
|
|
25
25
|
|
|
26
|
-
patient = find_patient_by_nhid(order_dto[:patient][:id])
|
|
26
|
+
patient = find_patient_by_nhid(order_dto[:patient][:id], order_dto[:tracking_number])
|
|
27
27
|
unless patient
|
|
28
28
|
logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
|
|
29
29
|
order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
|
|
@@ -58,7 +58,7 @@ module Lab
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def process_order(order_dto)
|
|
61
|
-
patient = find_patient_by_nhid(order_dto[:patient][:id])
|
|
61
|
+
patient = find_patient_by_nhid(order_dto[:patient][:id], order_dto[:tracking_number])
|
|
62
62
|
unless patient
|
|
63
63
|
logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
|
|
64
64
|
order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
|
|
@@ -107,7 +107,7 @@ module Lab
|
|
|
107
107
|
|
|
108
108
|
private
|
|
109
109
|
|
|
110
|
-
def find_patient_by_nhid(nhid)
|
|
110
|
+
def find_patient_by_nhid(nhid, accession_number)
|
|
111
111
|
national_id_type = PatientIdentifierType.where(name: ['National id', 'Old Identification Number'])
|
|
112
112
|
identifiers = PatientIdentifier.where(type: national_id_type, identifier: nhid)
|
|
113
113
|
.joins('INNER JOIN person ON person.person_id = patient_identifier.patient_id AND person.voided = 0')
|
|
@@ -126,10 +126,10 @@ module Lab
|
|
|
126
126
|
patients = Patient.where(patient_id: identifiers.select(:patient_id))
|
|
127
127
|
.distinct(:patient_id)
|
|
128
128
|
.all
|
|
129
|
+
order = Order.find_by(patient_id: patients.select(:patient_id), accession_number: accession_number)
|
|
130
|
+
raise Lab::Lims::LimsException, "Order #{accession_number} does not exists for patient #{nhid}" if order.nil?
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
patients.first
|
|
132
|
+
order.patient
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
##
|
|
@@ -10,6 +10,7 @@ module Lab
|
|
|
10
10
|
include Utils # for logger
|
|
11
11
|
|
|
12
12
|
SECONDS_TO_WAIT_FOR_ORDERS = 30
|
|
13
|
+
START_DATE = Time.parse('2024-09-03').freeze
|
|
13
14
|
|
|
14
15
|
def initialize(lims_api)
|
|
15
16
|
@lims_api = lims_api
|
|
@@ -23,8 +24,6 @@ module Lab
|
|
|
23
24
|
logger.debug("Found #{orders.size} orders...")
|
|
24
25
|
orders.each do |order|
|
|
25
26
|
push_order(order)
|
|
26
|
-
rescue GatewayError => e
|
|
27
|
-
logger.error("Failed to push order ##{order.accession_number}: #{e.class} - #{e.message}")
|
|
28
27
|
rescue StandardError => e
|
|
29
28
|
logger.error("Failed to push order ##{order.id}: #{order&.accession_number} : #{e.class} - #{e.message}")
|
|
30
29
|
end
|
|
@@ -53,7 +52,7 @@ module Lab
|
|
|
53
52
|
|
|
54
53
|
ActiveRecord::Base.transaction do
|
|
55
54
|
if mapping && !order.voided.zero?
|
|
56
|
-
Rails.logger.info("Deleting order ##{order_dto[
|
|
55
|
+
Rails.logger.info("Deleting order ##{order_dto['accession_number']} from LIMS")
|
|
57
56
|
lims_api.delete_order(mapping.lims_id, order_dto)
|
|
58
57
|
mapping.destroy
|
|
59
58
|
elsif mapping
|
|
@@ -103,6 +102,7 @@ module Lab
|
|
|
103
102
|
Rails.logger.debug('Looking for new orders that need to be created in LIMS...')
|
|
104
103
|
Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
|
|
105
104
|
.where("accession_number IS NOT NULL AND accession_number !=''")
|
|
105
|
+
.where(date_created: START_DATE..(Date.today + 1.day))
|
|
106
106
|
.order(date_created: :desc)
|
|
107
107
|
end
|
|
108
108
|
|
|
@@ -47,10 +47,9 @@ module Lab
|
|
|
47
47
|
return user if user
|
|
48
48
|
|
|
49
49
|
god_user = User.first
|
|
50
|
-
|
|
51
|
-
person = Person.create!(creator: god_user.user_id)
|
|
52
|
-
PersonName.create!(person
|
|
53
|
-
|
|
50
|
+
User.current = god_user
|
|
51
|
+
person = Person.create!(creator: god_user.user_id, birthdate: '1980-01-01')
|
|
52
|
+
PersonName.create!(person: person, given_name: 'Lab', family_name: 'Daemon', creator: god_user.user_id)
|
|
54
53
|
User.create!(username: 'lab_daemon', person:, creator: god_user.user_id)
|
|
55
54
|
end
|
|
56
55
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'logger_multiplexor'
|
|
4
4
|
|
|
5
|
-
require_relative 'api/couchdb_api'
|
|
5
|
+
# require_relative 'api/couchdb_api'
|
|
6
6
|
|
|
7
7
|
module Lab
|
|
8
8
|
module Lims
|
|
@@ -13,7 +13,7 @@ module Lab
|
|
|
13
13
|
User.current = Utils.lab_user
|
|
14
14
|
|
|
15
15
|
fork(&method(:start_push_worker))
|
|
16
|
-
|
|
16
|
+
fork(&method(:start_pull_worker))
|
|
17
17
|
fork(&method(:start_acknowledgement_worker))
|
|
18
18
|
fork(&method(:start_realtime_pull_worker)) if realtime_updates_enabled?
|
|
19
19
|
|
|
@@ -5,6 +5,7 @@ module Lab
|
|
|
5
5
|
# Concepts
|
|
6
6
|
REASON_FOR_TEST_CONCEPT_NAME = 'Reason for test'
|
|
7
7
|
REQUESTING_CLINICIAN_CONCEPT_NAME = 'Person making request'
|
|
8
|
+
COMMENT_TO_FULFILLER_CONCEPT_NAME = 'Comment to fulfiller'
|
|
8
9
|
SPECIMEN_TYPE_CONCEPT_NAME = 'Specimen type'
|
|
9
10
|
TARGET_LAB_CONCEPT_NAME = 'Lab'
|
|
10
11
|
TEST_RESULT_CONCEPT_NAME = 'Lab test result'
|
|
@@ -12,12 +13,15 @@ module Lab
|
|
|
12
13
|
TEST_TYPE_CONCEPT_NAME = 'Test type'
|
|
13
14
|
LAB_ORDER_STATUS_CONCEPT_NAME = 'lab order status'
|
|
14
15
|
UNKNOWN_SPECIMEN = 'Unknown'
|
|
16
|
+
VISIT_TYPE_UUID = '8a4a4488-c2cc-11de-8d13-0010c6dffd0f'
|
|
17
|
+
TEST_METHOD_CONCEPT_NAME = 'Recommended test method'
|
|
15
18
|
|
|
16
19
|
# Encounter
|
|
17
20
|
ENCOUNTER_TYPE_NAME = 'Lab'
|
|
18
21
|
|
|
19
22
|
# Order types
|
|
20
23
|
ORDER_TYPE_NAME = 'Lab'
|
|
24
|
+
HTS_ORDER_TYPE_NAME = 'HTS Lab'
|
|
21
25
|
|
|
22
26
|
# Programs
|
|
23
27
|
LAB_PROGRAM_NAME = 'Laboratory Program'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# notification service
|
|
4
|
+
class Lab::NotificationService
|
|
5
|
+
# this gets all uncleared notifications of the user
|
|
6
|
+
def uncleared
|
|
7
|
+
NotificationAlert.joins(:notification_alert_recipients).where(
|
|
8
|
+
'notification_alert_recipient.user_id = ?', User.current.user_id
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def clear(alert_id)
|
|
13
|
+
alert = NotificationAlert.find(alert_id)
|
|
14
|
+
# update the notification alert recipient to cleared and read only for the current user
|
|
15
|
+
alert.notification_alert_recipients.where(user_id: User.current.user_id).update_all(cleared: true, alert_read: true)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# this updates the notification to read
|
|
19
|
+
def read(alerts)
|
|
20
|
+
alerts.each do |alert|
|
|
21
|
+
notification = NotificationAlertRecipient.where(user_id: User.current.user_id, alert_id: alert,
|
|
22
|
+
alert_read: false).first
|
|
23
|
+
next if notification.blank?
|
|
24
|
+
|
|
25
|
+
notification.alert_read = true
|
|
26
|
+
notification.save
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_notification(alert_type, alert_message)
|
|
31
|
+
return if alert_type != 'LIMS'
|
|
32
|
+
|
|
33
|
+
lab = User.find_by(username: 'lab_daemon')
|
|
34
|
+
ActiveRecord::Base.transaction do
|
|
35
|
+
alert = NotificationAlert.create!(text: alert_message.to_json, date_to_expire: Time.now + not_period.days,
|
|
36
|
+
creator: lab, changed_by: lab, date_created: Time.now)
|
|
37
|
+
notify(alert, User.joins(:roles).uniq)
|
|
38
|
+
# ActionCable.server.broadcast('nlims_channel', alert)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def not_period
|
|
43
|
+
result = GlobalProperty.where(property: 'notification.period')&.first
|
|
44
|
+
return result.property_value.to_i if result.present?
|
|
45
|
+
|
|
46
|
+
7 # default to 7 days
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def notify(notification_alert, recipients)
|
|
50
|
+
recipients.each do |recipient|
|
|
51
|
+
recipient.notification_alert_recipients.create(
|
|
52
|
+
alert_id: notification_alert.id
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def notify_all(notification_alert, users)
|
|
58
|
+
users.each do |user|
|
|
59
|
+
user.notification_alert_recipients.create(
|
|
60
|
+
alert_id: notification_alert.id
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def notify_all_users(notification_alert)
|
|
66
|
+
User.all.each do |user|
|
|
67
|
+
user.notification_alert_recipients.create!(
|
|
68
|
+
alert_id: notification_alert.id
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -7,12 +7,18 @@ module Lab
|
|
|
7
7
|
def find_orders(filters)
|
|
8
8
|
extra_filters = pop_filters(filters, :date, :end_date, :status)
|
|
9
9
|
|
|
10
|
+
uuid = filters.delete(:patient)
|
|
11
|
+
patient = Patient.find(uuid) if uuid
|
|
12
|
+
|
|
13
|
+
filters.merge!(patient_id: patient.id) if patient
|
|
14
|
+
|
|
10
15
|
orders = Lab::LabOrder.prefetch_relationships
|
|
11
16
|
.where(filters)
|
|
12
|
-
|
|
17
|
+
orders = orders.order(start_date: :desc) if Order.column_names.include?('start_date')
|
|
18
|
+
orders = orders.order(date_created: :desc) unless Order.column_names.include?('start_date')
|
|
13
19
|
|
|
14
|
-
orders = filter_orders_by_status(orders
|
|
15
|
-
orders = filter_orders_by_date(orders,
|
|
20
|
+
orders = filter_orders_by_status(orders: orders, status: extra_filters[:status])
|
|
21
|
+
orders = filter_orders_by_date(orders: orders, date: extra_filters[:date], end_date: extra_filters[:end_date])
|
|
16
22
|
|
|
17
23
|
orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
|
|
18
24
|
end
|
|
@@ -27,7 +33,7 @@ module Lab
|
|
|
27
33
|
query
|
|
28
34
|
end
|
|
29
35
|
|
|
30
|
-
def filter_orders_by_date(orders
|
|
36
|
+
def filter_orders_by_date(orders:, date: nil, end_date: nil)
|
|
31
37
|
date = date&.to_date
|
|
32
38
|
end_date = end_date&.to_date
|
|
33
39
|
|
|
@@ -40,7 +46,7 @@ module Lab
|
|
|
40
46
|
orders
|
|
41
47
|
end
|
|
42
48
|
|
|
43
|
-
def filter_orders_by_status(orders
|
|
49
|
+
def filter_orders_by_status(orders:, status: nil)
|
|
44
50
|
case status&.downcase
|
|
45
51
|
when 'ordered' then orders.where(concept_id: unknown_concept_id)
|
|
46
52
|
when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
|
|
@@ -58,16 +58,28 @@ module Lab
|
|
|
58
58
|
|
|
59
59
|
order = create_order(encounter, order_params)
|
|
60
60
|
|
|
61
|
+
attach_test_method(order, order_params) if order_params[:test_method]
|
|
62
|
+
|
|
61
63
|
Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
|
|
62
64
|
|
|
63
65
|
Lab::LabOrderSerializer.serialize_order(
|
|
64
66
|
order, requesting_clinician: add_requesting_clinician(order, order_params),
|
|
65
67
|
reason_for_test: add_reason_for_test(order, order_params),
|
|
66
|
-
target_lab: add_target_lab(order, order_params)
|
|
68
|
+
target_lab: add_target_lab(order, order_params),
|
|
69
|
+
comment_to_fulfiller: add_comment_to_fulfiller(order, order_params)
|
|
67
70
|
)
|
|
68
71
|
end
|
|
69
72
|
end
|
|
70
73
|
|
|
74
|
+
def attach_test_method(order, order_params)
|
|
75
|
+
create_order_observation(
|
|
76
|
+
order,
|
|
77
|
+
Lab::Metadata::TEST_METHOD_CONCEPT_NAME,
|
|
78
|
+
order_params[:date],
|
|
79
|
+
value_coded: order_params[:test_method]
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
def update_order(order_id, params)
|
|
72
84
|
specimen_id = params.dig(:specimen, :concept_id)
|
|
73
85
|
raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id
|
|
@@ -86,20 +98,23 @@ module Lab
|
|
|
86
98
|
discontinued_reason_non_coded: 'Sample drawn/updated')
|
|
87
99
|
end
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
reason_for_test = params[:reason_for_test] || params[:reason_for_test_id]
|
|
102
|
+
|
|
103
|
+
if reason_for_test
|
|
90
104
|
Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
|
|
91
|
-
update_reason_for_test(order, params
|
|
105
|
+
update_reason_for_test(order, Concept.find(reason_for_test)&.id, force_update: params.fetch('force_update', false))
|
|
92
106
|
end
|
|
93
107
|
|
|
94
108
|
Lab::LabOrderSerializer.serialize_order(order)
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
def void_order(order_id, reason)
|
|
98
|
-
order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab], tests: [:result])
|
|
112
|
+
order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab comment_to_fulfiller], tests: [:result])
|
|
99
113
|
.find(order_id)
|
|
100
114
|
|
|
101
115
|
order.requesting_clinician&.void(reason)
|
|
102
116
|
order.reason_for_test&.void(reason)
|
|
117
|
+
order.comment_to_fulfiller&.void(reason)
|
|
103
118
|
order.target_lab&.void(reason)
|
|
104
119
|
|
|
105
120
|
order.tests.each { |test| test.void(reason) }
|
|
@@ -160,13 +175,13 @@ module Lab
|
|
|
160
175
|
def create_rejection_notification(order_params)
|
|
161
176
|
order = find_order order_params['tracking_number']
|
|
162
177
|
data = { 'type': 'LIMS',
|
|
178
|
+
'specimen': ConceptName.find_by(concept_id: order.concept_id)&.name,
|
|
163
179
|
'accession_number': order&.accession_number,
|
|
164
180
|
'order_date': order&.start_date,
|
|
165
181
|
'arv_number': find_arv_number(order.patient_id),
|
|
166
182
|
'patient_id': result.person_id,
|
|
167
183
|
'ordered_by': order&.provider&.person&.name,
|
|
168
|
-
'rejection_reason': order_params['comments']
|
|
169
|
-
}.as_json
|
|
184
|
+
'rejection_reason': order_params['comments'] }.as_json
|
|
170
185
|
NotificationService.new.create_notification('LIMS', data)
|
|
171
186
|
end
|
|
172
187
|
|
|
@@ -207,35 +222,50 @@ module Lab
|
|
|
207
222
|
# a 'Lab' encounter is created using the provided program_id and
|
|
208
223
|
# patient_id.
|
|
209
224
|
def find_encounter(order_params)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
225
|
+
encounter_id = order_params[:encounter_id] || order_params[:encounter]
|
|
226
|
+
patient_id = order_params[:patient_id] || order_params[:patient]
|
|
227
|
+
visit = order_params[:visit]
|
|
228
|
+
|
|
229
|
+
return Encounter.find(encounter_id) if order_params[:encounter] || order_params[:encounter_id]
|
|
230
|
+
raise StandardError, 'encounter_id|uuid or patient_id|uuid required' unless order_params[:patient]
|
|
231
|
+
|
|
232
|
+
encounter = Encounter.new
|
|
233
|
+
encounter.patient = Patient.find(patient_id)
|
|
234
|
+
encounter.encounter_type = EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME)
|
|
235
|
+
encounter.encounter_datetime = order_params[:date] || Date.today
|
|
236
|
+
encounter.visit = Visit.find_by_uuid(visit) if Encounter.column_names.include?('visit_id')
|
|
237
|
+
encounter.provider_id = User.current&.person.id if Encounter.column_names.include?('provider_id')
|
|
238
|
+
encounter.program_id = order_params[:program_id] if Encounter.column_names.include?('program_id') && order_params[:program_id].present?
|
|
239
|
+
encounter.save!
|
|
240
|
+
encounter.reload
|
|
223
241
|
end
|
|
224
242
|
|
|
225
243
|
def create_order(encounter, params)
|
|
226
244
|
access_number = params[:accession_number] || next_accession_number(params[:date]&.to_date || Date.today)
|
|
227
245
|
raise 'Accession Number cannot be blank' unless access_number.present?
|
|
228
246
|
raise 'Accession cannot be this short' unless access_number.length > 6
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
247
|
+
|
|
248
|
+
concept = params.dig(:specimen, :concept)
|
|
249
|
+
concept ||= params.dig(:specimen, :concept_id)
|
|
250
|
+
|
|
251
|
+
order_type = nil
|
|
252
|
+
order_type = OrderType.find_by_order_type_id!(params[:order_type_id])&.id if params[:order_type_id].present?
|
|
253
|
+
|
|
254
|
+
order = Lab::LabOrder.new
|
|
255
|
+
order.order_type_id = order_type || OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME).id
|
|
256
|
+
order.concept_id = Concept.find(concept)&.id
|
|
257
|
+
order.encounter_id = encounter.id
|
|
258
|
+
order.patient_id = encounter.patient.id
|
|
259
|
+
order.date_created = params[:date]&.to_date || Date.today if order.respond_to?(:date_created)
|
|
260
|
+
order.start_date = params[:date]&.to_date || Date.today if order.respond_to?(:start_date)
|
|
261
|
+
order.auto_expire_date = params[:end_date]
|
|
262
|
+
order.comment_to_fulfiller = params[:comment_to_fulfiller] if params[:comment_to_fulfiller]
|
|
263
|
+
order.accession_number = access_number
|
|
264
|
+
order.orderer = User.current&.user_id
|
|
265
|
+
|
|
266
|
+
order.save!
|
|
267
|
+
|
|
268
|
+
order.reload
|
|
239
269
|
end
|
|
240
270
|
|
|
241
271
|
def accession_number_exists?(accession_number)
|
|
@@ -262,16 +292,28 @@ module Lab
|
|
|
262
292
|
)
|
|
263
293
|
end
|
|
264
294
|
|
|
295
|
+
def add_comment_to_fulfiller(order, params)
|
|
296
|
+
create_order_observation(
|
|
297
|
+
order,
|
|
298
|
+
Lab::Metadata::COMMENT_TO_FULFILLER_CONCEPT_NAME,
|
|
299
|
+
params[:date],
|
|
300
|
+
value_text: params['comment_to_fulfiller']
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
|
|
265
304
|
##
|
|
266
305
|
# Attach a reason for the order/test
|
|
267
306
|
#
|
|
268
307
|
# Examples of reasons include: Routine, Targeted, Confirmatory, Repeat, or Stat.
|
|
269
308
|
def add_reason_for_test(order, params)
|
|
309
|
+
reason = params[:reason_for_test_id] || params[:reason_for_test]
|
|
310
|
+
|
|
311
|
+
reason = Concept.find(reason)
|
|
270
312
|
create_order_observation(
|
|
271
313
|
order,
|
|
272
314
|
Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME,
|
|
273
315
|
params[:date],
|
|
274
|
-
value_coded:
|
|
316
|
+
value_coded: reason
|
|
275
317
|
)
|
|
276
318
|
end
|
|
277
319
|
|
|
@@ -288,14 +330,16 @@ module Lab
|
|
|
288
330
|
)
|
|
289
331
|
end
|
|
290
332
|
|
|
291
|
-
def create_order_observation(order, concept_name, date, **)
|
|
333
|
+
def create_order_observation(order, concept_name, date, **values)
|
|
334
|
+
creator = User.find_by(username: 'lab_daemon')
|
|
335
|
+
User.current ||= creator
|
|
292
336
|
Observation.create!(
|
|
293
337
|
order:,
|
|
294
338
|
encounter_id: order.encounter_id,
|
|
295
339
|
person_id: order.patient_id,
|
|
296
340
|
concept_id: ConceptName.find_by_name!(concept_name).concept_id,
|
|
297
341
|
obs_datetime: date&.to_time || Time.now,
|
|
298
|
-
**
|
|
342
|
+
**values
|
|
299
343
|
)
|
|
300
344
|
end
|
|
301
345
|
|
|
@@ -304,18 +348,20 @@ module Lab
|
|
|
304
348
|
end
|
|
305
349
|
|
|
306
350
|
def unknown_concept_id
|
|
307
|
-
ConceptName.find_by_name!('Unknown').
|
|
351
|
+
ConceptName.find_by_name!('Unknown').concept
|
|
308
352
|
end
|
|
309
353
|
|
|
310
|
-
def update_reason_for_test(order, concept_id)
|
|
354
|
+
def update_reason_for_test(order, concept_id, force_update: false)
|
|
311
355
|
raise InvalidParameterError, "Reason for test can't be blank" if concept_id.blank?
|
|
312
356
|
|
|
313
357
|
return if order.reason_for_test&.value_coded == concept_id
|
|
314
358
|
|
|
315
|
-
raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded
|
|
359
|
+
raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded && !force_update
|
|
316
360
|
|
|
317
361
|
order.reason_for_test&.delete
|
|
318
|
-
|
|
362
|
+
date = order.start_date if order.respond_to?(:start_date)
|
|
363
|
+
date ||= order.date_created
|
|
364
|
+
add_reason_for_test(order, date: date, reason_for_test_id: concept_id)
|
|
319
365
|
end
|
|
320
366
|
|
|
321
367
|
def void_order_status(order, concept)
|