mahis_emr_api_lab 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +71 -0
- data/Rakefile +32 -0
- data/app/controllers/lab/application_controller.rb +6 -0
- data/app/controllers/lab/labels_controller.rb +17 -0
- data/app/controllers/lab/orders_controller.rb +78 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +20 -0
- data/app/controllers/lab/specimen_types_controller.rb +15 -0
- data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
- data/app/controllers/lab/test_types_controller.rb +15 -0
- data/app/controllers/lab/tests_controller.rb +25 -0
- data/app/controllers/lab/users_controller.rb +32 -0
- data/app/jobs/lab/application_job.rb +4 -0
- data/app/jobs/lab/push_order_job.rb +12 -0
- data/app/jobs/lab/update_patient_orders_job.rb +32 -0
- data/app/jobs/lab/void_order_job.rb +17 -0
- data/app/mailers/lab/application_mailer.rb +6 -0
- data/app/models/lab/application_record.rb +5 -0
- data/app/models/lab/lab_accession_number_counter.rb +13 -0
- data/app/models/lab/lab_acknowledgement.rb +6 -0
- data/app/models/lab/lab_encounter.rb +7 -0
- data/app/models/lab/lab_order.rb +58 -0
- data/app/models/lab/lab_result.rb +31 -0
- data/app/models/lab/lab_test.rb +19 -0
- data/app/models/lab/lims_failed_import.rb +4 -0
- data/app/models/lab/lims_order_mapping.rb +10 -0
- data/app/models/lab/order_extension.rb +14 -0
- data/app/serializers/lab/lab_order_serializer.rb +56 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +52 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/acknowledgement_service.rb +47 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/json_web_token_service.rb +20 -0
- data/app/services/lab/labelling_service/order_label.rb +106 -0
- data/app/services/lab/lims/acknowledgement_serializer.rb +29 -0
- data/app/services/lab/lims/acknowledgement_worker.rb +37 -0
- data/app/services/lab/lims/api/blackhole_api.rb +21 -0
- data/app/services/lab/lims/api/couchdb_api.rb +53 -0
- data/app/services/lab/lims/api/mysql_api.rb +316 -0
- data/app/services/lab/lims/api/rest_api.rb +434 -0
- data/app/services/lab/lims/api/ws_api.rb +121 -0
- data/app/services/lab/lims/api_factory.rb +19 -0
- data/app/services/lab/lims/config.rb +105 -0
- data/app/services/lab/lims/exceptions.rb +11 -0
- data/app/services/lab/lims/migrator.rb +216 -0
- data/app/services/lab/lims/order_dto.rb +105 -0
- data/app/services/lab/lims/order_serializer.rb +251 -0
- data/app/services/lab/lims/pull_worker.rb +314 -0
- data/app/services/lab/lims/push_worker.rb +152 -0
- data/app/services/lab/lims/utils.rb +91 -0
- data/app/services/lab/lims/worker.rb +94 -0
- data/app/services/lab/metadata.rb +26 -0
- data/app/services/lab/notification_service.rb +72 -0
- data/app/services/lab/orders_search_service.rb +72 -0
- data/app/services/lab/orders_service.rb +330 -0
- data/app/services/lab/results_service.rb +166 -0
- data/app/services/lab/tests_service.rb +105 -0
- data/app/services/lab/user_service.rb +62 -0
- data/config/routes.rb +28 -0
- data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
- data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
- data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
- data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
- data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
- data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
- data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
- data/lib/auto12epl.rb +201 -0
- data/lib/couch_bum/couch_bum.rb +92 -0
- data/lib/generators/lab/install/USAGE +9 -0
- data/lib/generators/lab/install/install_generator.rb +19 -0
- data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
- data/lib/generators/lab/install/templates/start_worker.rb +32 -0
- data/lib/generators/lab/install/templates/swagger.yaml +714 -0
- data/lib/lab/engine.rb +13 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +38 -0
- data/lib/mahis_emr_api_lab.rb +6 -0
- data/lib/tasks/lab_tasks.rake +25 -0
- data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
- data/lib/tasks/loaders/data/test-measures.csv +225 -0
- data/lib/tasks/loaders/data/tests.csv +161 -0
- data/lib/tasks/loaders/loader_mixin.rb +53 -0
- data/lib/tasks/loaders/metadata_loader.rb +26 -0
- data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
- data/lib/tasks/loaders/specimens_loader.rb +65 -0
- data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
- metadata +331 -0
@@ -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
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
# Search Lab orders.
|
5
|
+
module OrdersSearchService
|
6
|
+
class << self
|
7
|
+
def find_orders(filters)
|
8
|
+
extra_filters = pop_filters(filters, :date, :end_date, :status)
|
9
|
+
|
10
|
+
uuid = filters.delete(:patient)
|
11
|
+
patient = Patient.find(uuid) if uuid
|
12
|
+
|
13
|
+
filters.merge!(patient_id: patient.id) if patient
|
14
|
+
|
15
|
+
orders = Lab::LabOrder.prefetch_relationships
|
16
|
+
.where(filters)
|
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')
|
19
|
+
|
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])
|
22
|
+
|
23
|
+
orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_orders_without_results(patient_id: nil)
|
27
|
+
results_query = Lab::LabResult.all
|
28
|
+
results_query = results_query.where(person_id: patient_id) if patient_id
|
29
|
+
|
30
|
+
query = Lab::LabOrder.where.not(order_id: results_query.select(:order_id))
|
31
|
+
query = query.where(patient_id: patient_id) if patient_id
|
32
|
+
|
33
|
+
query
|
34
|
+
end
|
35
|
+
|
36
|
+
def filter_orders_by_date(orders:, date: nil, end_date: nil)
|
37
|
+
date = date&.to_date
|
38
|
+
end_date = end_date&.to_date
|
39
|
+
|
40
|
+
return orders.where('start_date BETWEEN ? AND ?', date, end_date + 1.day) if date && end_date
|
41
|
+
|
42
|
+
return orders.where('start_date BETWEEN ? AND ?', date, date + 1.day) if date
|
43
|
+
|
44
|
+
return orders.where('start_date < ?', end_date + 1.day) if end_date
|
45
|
+
|
46
|
+
orders
|
47
|
+
end
|
48
|
+
|
49
|
+
def filter_orders_by_status(orders:, status: nil)
|
50
|
+
case status&.downcase
|
51
|
+
when 'ordered' then orders.where(concept_id: unknown_concept_id)
|
52
|
+
when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
|
53
|
+
else orders
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def unknown_concept_id
|
58
|
+
ConceptName.find_by_name!('Unknown').concept_id
|
59
|
+
end
|
60
|
+
|
61
|
+
def pop_filters(params, *filters)
|
62
|
+
filters.each_with_object({}) do |filter, popped_params|
|
63
|
+
next unless params.key?(filter)
|
64
|
+
|
65
|
+
popped_params[filter.to_sym] = params.delete(filter)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_results(order); end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
##
|
5
|
+
# Manage lab orders.
|
6
|
+
#
|
7
|
+
# Lab orders are just ordinary openmrs orders with extra metadata that
|
8
|
+
# separates them from other orders. Lab orders have an order type of 'Lab'
|
9
|
+
# with the order's test type as the order's concept. The order's start
|
10
|
+
# date is the day the order is made. Additional information pertaining to
|
11
|
+
# the order is stored as observations that point to the order. The
|
12
|
+
# specimen types, requesting clinician, target lab, and reason for test
|
13
|
+
# are saved as observations to the order. Refer to method #order_test for
|
14
|
+
# more information.
|
15
|
+
module OrdersService
|
16
|
+
class << self
|
17
|
+
##
|
18
|
+
# Create a lab order.
|
19
|
+
#
|
20
|
+
# Parameters schema:
|
21
|
+
#
|
22
|
+
# {
|
23
|
+
# encounter_id: {
|
24
|
+
# type: :integer,
|
25
|
+
# required: :false,
|
26
|
+
# description: 'Attach order to this if program_id and patient_id are not provided'
|
27
|
+
# },
|
28
|
+
# program_id: { type: :integer, required: false },
|
29
|
+
# patient_id: { type: :integer, required: false }
|
30
|
+
# specimen: { type: :object, properties: { concept_id: :integer }, required: %i[concept_id] },
|
31
|
+
# test_type_ids: {
|
32
|
+
# type: :array,
|
33
|
+
# items: {
|
34
|
+
# type: :object,
|
35
|
+
# properties: { concept_id: :integer },
|
36
|
+
# required: %i[concept_id]
|
37
|
+
# }
|
38
|
+
# },
|
39
|
+
# start_date: { type: :datetime }
|
40
|
+
# accession_number: { type: :string }
|
41
|
+
# target_lab: { type: :string },
|
42
|
+
# reason_for_test_id: { type: :integer },
|
43
|
+
# requesting_clinician: { type: :string }
|
44
|
+
# }
|
45
|
+
#
|
46
|
+
# encounter_id: is an ID of the encounter the lab order is to be created under
|
47
|
+
# test_type_id: is a concept_id of the name of test being ordered
|
48
|
+
# specimen_type_id: is a list of IDs for the specimens to be tested (can be ommited)
|
49
|
+
# target_lab: is the name of the lab where test will be carried out
|
50
|
+
# reason_for_test_id: is a concept_id for a (standard) reason of why the test is being carried out
|
51
|
+
# requesting_clinician: Name of the clinician requesting the test (defaults to current user)
|
52
|
+
def order_test(order_params)
|
53
|
+
Order.transaction do
|
54
|
+
encounter = find_encounter(order_params)
|
55
|
+
if order_params[:accession_number].present? && check_tracking_number(order_params[:accession_number])
|
56
|
+
raise 'Accession number already exists'
|
57
|
+
end
|
58
|
+
|
59
|
+
order = create_order(encounter, order_params)
|
60
|
+
|
61
|
+
Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
|
62
|
+
|
63
|
+
Lab::LabOrderSerializer.serialize_order(
|
64
|
+
order, requesting_clinician: add_requesting_clinician(order, order_params),
|
65
|
+
reason_for_test: add_reason_for_test(order, order_params),
|
66
|
+
target_lab: add_target_lab(order, order_params)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_order(order_id, params)
|
72
|
+
specimen_id = params.dig(:specimen, :concept_id)
|
73
|
+
raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id
|
74
|
+
|
75
|
+
order = Lab::LabOrder.find(order_id)
|
76
|
+
if order.concept_id != unknown_concept_id && !params[:force_update]&.casecmp?('true')
|
77
|
+
raise ::UnprocessableEntityError, "Can't change order specimen once set"
|
78
|
+
end
|
79
|
+
|
80
|
+
if specimen_id.to_i != order.concept_id
|
81
|
+
Rails.logger.debug("Updating order ##{order.order_id}")
|
82
|
+
order.update!(concept_id: specimen_id,
|
83
|
+
discontinued: true,
|
84
|
+
discontinued_by: User.current.user_id,
|
85
|
+
discontinued_date: params[:date]&.to_date || Time.now,
|
86
|
+
discontinued_reason_non_coded: 'Sample drawn/updated')
|
87
|
+
end
|
88
|
+
|
89
|
+
reason_for_test = params[:reason_for_test] || params[:reason_for_test_id]
|
90
|
+
|
91
|
+
if reason_for_test
|
92
|
+
Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
|
93
|
+
update_reason_for_test(order, Concept.find(reason_for_test)&.id)
|
94
|
+
end
|
95
|
+
|
96
|
+
Lab::LabOrderSerializer.serialize_order(order)
|
97
|
+
end
|
98
|
+
|
99
|
+
def void_order(order_id, reason)
|
100
|
+
order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab], tests: [:result])
|
101
|
+
.find(order_id)
|
102
|
+
|
103
|
+
order.requesting_clinician&.void(reason)
|
104
|
+
order.reason_for_test&.void(reason)
|
105
|
+
order.target_lab&.void(reason)
|
106
|
+
|
107
|
+
order.tests.each { |test| test.void(reason) }
|
108
|
+
order.void(reason)
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_tracking_number(tracking_number)
|
112
|
+
accession_number_exists?(tracking_number) || nlims_accession_number_exists?(tracking_number)
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_order_status(order_params)
|
116
|
+
# find the order
|
117
|
+
order = find_order(order_params['tracking_number'])
|
118
|
+
concept = ConceptName.find_by_name Lab::Metadata::LAB_ORDER_STATUS_CONCEPT_NAME
|
119
|
+
ActiveRecord::Base.transaction do
|
120
|
+
void_order_status(order, concept)
|
121
|
+
Observation.create!(
|
122
|
+
person_id: order.patient_id,
|
123
|
+
encounter_id: order.encounter_id,
|
124
|
+
concept_id: concept.concept_id,
|
125
|
+
order_id: order.id,
|
126
|
+
obs_datetime: order_params['status_time'] || Time.now,
|
127
|
+
value_text: order_params['status'],
|
128
|
+
creator: User.current.id
|
129
|
+
)
|
130
|
+
end
|
131
|
+
create_rejection_notification(order_params) if order_params['status'] == 'test-rejected'
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_order_result(order_params)
|
135
|
+
order = find_order(order_params['tracking_number'])
|
136
|
+
order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
|
137
|
+
patch_order_dto_with_lims_results!(order_dto, order_params['results'])
|
138
|
+
Lab::Lims::PullWorker.new(nil).process_order(order_dto)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def create_rejection_notification(order_params)
|
144
|
+
order = find_order order_params['tracking_number']
|
145
|
+
data = { 'type': 'LIMS',
|
146
|
+
'accession_number': order&.accession_number,
|
147
|
+
'order_date': order&.start_date,
|
148
|
+
'arv_number': find_arv_number(order.patient_id),
|
149
|
+
'patient_id': result.person_id,
|
150
|
+
'ordered_by': order&.provider&.person&.name,
|
151
|
+
'rejection_reason': order_params['comments']
|
152
|
+
}.as_json
|
153
|
+
NotificationService.new.create_notification('LIMS', data)
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_arv_number(patient_id)
|
157
|
+
PatientIdentifier.joins(:type)
|
158
|
+
.merge(PatientIdentifierType.where(name: 'ARV Number'))
|
159
|
+
.where(patient_id: patient_id)
|
160
|
+
.first&.identifier
|
161
|
+
end
|
162
|
+
|
163
|
+
def find_order(tracking_number)
|
164
|
+
Lab::LabOrder.find_by_accession_number(tracking_number)
|
165
|
+
end
|
166
|
+
|
167
|
+
def patch_order_dto_with_lims_results!(order_dto, results)
|
168
|
+
order_dto.merge!(
|
169
|
+
'_id' => order_dto[:tracking_number],
|
170
|
+
'_rev' => 0,
|
171
|
+
'test_results' => results.each_with_object({}) do |result, formatted_results|
|
172
|
+
test_name, measures = result
|
173
|
+
result_date = measures.delete('result_date')
|
174
|
+
|
175
|
+
formatted_results[test_name] = {
|
176
|
+
results: measures.each_with_object({}) do |measure, processed_measures|
|
177
|
+
processed_measures[measure[0]] = { 'result_value' => measure[1] }
|
178
|
+
end,
|
179
|
+
result_date: result_date,
|
180
|
+
result_entered_by: {}
|
181
|
+
}
|
182
|
+
end
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Extract an encounter from the given parameters.
|
188
|
+
#
|
189
|
+
# Uses an encounter_id to retrieve an encounter if provided else
|
190
|
+
# a 'Lab' encounter is created using the provided program_id and
|
191
|
+
# patient_id.
|
192
|
+
def find_encounter(order_params)
|
193
|
+
encounter_id = order_params[:encounter_id] || order_params[:encounter]
|
194
|
+
patient_id = order_params[:patient_id] || order_params[:patient]
|
195
|
+
visit = order_params[:visit]
|
196
|
+
|
197
|
+
return Encounter.find(encounter_id) if order_params[:encounter] || order_params[:encounter_id]
|
198
|
+
raise StandardError, 'encounter_id|uuid or patient_id|uuid required' unless order_params[:patient]
|
199
|
+
|
200
|
+
|
201
|
+
encounter = Encounter.new
|
202
|
+
encounter.patient = Patient.find(patient_id)
|
203
|
+
encounter.encounter_type = EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME)
|
204
|
+
encounter.encounter_datetime = order_params[:date] || Date.today
|
205
|
+
encounter.visit = Visit.find_by_uuid(visit) if Encounter.column_names.include?('visit_id')
|
206
|
+
encounter.provider_id = User.current&.person.id if Encounter.column_names.include?('provider_id')
|
207
|
+
|
208
|
+
encounter.save!
|
209
|
+
|
210
|
+
encounter.reload
|
211
|
+
end
|
212
|
+
|
213
|
+
def create_order(encounter, params)
|
214
|
+
access_number = params[:accession_number] || next_accession_number(params[:date]&.to_date || Date.today)
|
215
|
+
raise 'Accession Number cannot be blank' unless access_number.present?
|
216
|
+
raise 'Accession cannot be this short' unless access_number.length > 6
|
217
|
+
|
218
|
+
concept = params.dig(:specimen, :concept)
|
219
|
+
concept ||= params.dig(:specimen, :concept_id)
|
220
|
+
|
221
|
+
order = Lab::LabOrder.new
|
222
|
+
order.order_type_id = OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME).id
|
223
|
+
order.concept_id = Concept.find(concept)&.id
|
224
|
+
order.encounter_id = encounter.id
|
225
|
+
order.patient_id = encounter.patient.id
|
226
|
+
order.date_created = params[:date]&.to_date || Date.today if order.respond_to?(:date_created)
|
227
|
+
order.start_date = params[:date]&.to_date || Date.today if order.respond_to?(:start_date)
|
228
|
+
order.auto_expire_date = params[:end_date]
|
229
|
+
order.accession_number = access_number
|
230
|
+
order.orderer = User.current&.user_id
|
231
|
+
|
232
|
+
order.save!
|
233
|
+
|
234
|
+
order.reload
|
235
|
+
end
|
236
|
+
|
237
|
+
def accession_number_exists?(accession_number)
|
238
|
+
Lab::LabOrder.where(accession_number: accession_number).exists?
|
239
|
+
end
|
240
|
+
|
241
|
+
def nlims_accession_number_exists?(accession_number)
|
242
|
+
config = YAML.load_file('config/application.yml')
|
243
|
+
return false unless config['lims_api']
|
244
|
+
|
245
|
+
# fetch from the rest api and check if it exists
|
246
|
+
lims_api = Lab::Lims::ApiFactory.create_api
|
247
|
+
lims_api.verify_tracking_number(accession_number).present?
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Attach the requesting clinician to an order
|
252
|
+
def add_requesting_clinician(order, params)
|
253
|
+
create_order_observation(
|
254
|
+
order,
|
255
|
+
Lab::Metadata::REQUESTING_CLINICIAN_CONCEPT_NAME,
|
256
|
+
params[:date],
|
257
|
+
value_text: params['requesting_clinician']
|
258
|
+
)
|
259
|
+
end
|
260
|
+
|
261
|
+
##
|
262
|
+
# Attach a reason for the order/test
|
263
|
+
#
|
264
|
+
# Examples of reasons include: Routine, Targeted, Confirmatory, Repeat, or Stat.
|
265
|
+
def add_reason_for_test(order, params)
|
266
|
+
|
267
|
+
reason = params[:reason_for_test_id] || params[:reason_for_test]
|
268
|
+
|
269
|
+
reason = Concept.find(reason)
|
270
|
+
create_order_observation(
|
271
|
+
order,
|
272
|
+
Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME,
|
273
|
+
params[:date],
|
274
|
+
value_coded: reason
|
275
|
+
)
|
276
|
+
end
|
277
|
+
|
278
|
+
##
|
279
|
+
# Attach the lab where the test is going to get carried out.
|
280
|
+
def add_target_lab(order, params)
|
281
|
+
return nil unless params['target_lab']
|
282
|
+
|
283
|
+
create_order_observation(
|
284
|
+
order,
|
285
|
+
Lab::Metadata::TARGET_LAB_CONCEPT_NAME,
|
286
|
+
params[:date],
|
287
|
+
value_text: params['target_lab']
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
def create_order_observation(order, concept_name, date, **values)
|
292
|
+
Observation.create!(
|
293
|
+
order: order,
|
294
|
+
encounter_id: order.encounter_id,
|
295
|
+
person_id: order.patient_id,
|
296
|
+
concept_id: ConceptName.find_by_name!(concept_name).concept_id,
|
297
|
+
obs_datetime: date&.to_time || Time.now,
|
298
|
+
**values
|
299
|
+
)
|
300
|
+
end
|
301
|
+
|
302
|
+
def next_accession_number(date = nil)
|
303
|
+
Lab::AccessionNumberService.next_accession_number(date)
|
304
|
+
end
|
305
|
+
|
306
|
+
def unknown_concept_id
|
307
|
+
ConceptName.find_by_name!('Unknown').concept
|
308
|
+
end
|
309
|
+
|
310
|
+
def update_reason_for_test(order, concept_id)
|
311
|
+
raise InvalidParameterError, "Reason for test can't be blank" if concept_id.blank?
|
312
|
+
|
313
|
+
return if order.reason_for_test&.value_coded == concept_id
|
314
|
+
|
315
|
+
raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded
|
316
|
+
|
317
|
+
order.reason_for_test&.delete
|
318
|
+
date = order.start_date if order.respond_to?(:start_date)
|
319
|
+
date ||= order.date_created
|
320
|
+
add_reason_for_test(order, date: date, reason_for_test_id: concept_id)
|
321
|
+
end
|
322
|
+
|
323
|
+
def void_order_status(order, concept)
|
324
|
+
Observation.where(order_id: order.id, concept_id: concept.concept_id).each do |obs|
|
325
|
+
obs.void('New Status Received from LIMS')
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
module ResultsService
|
5
|
+
class << self
|
6
|
+
##
|
7
|
+
# Attach results to a test
|
8
|
+
#
|
9
|
+
# Params:
|
10
|
+
# test_id: The tests id (maps to obs_id of the test's observation in OpenMRS)
|
11
|
+
# params: A hash comprising the following fields
|
12
|
+
# - encounter_id: Encounter to create result under (can be ommitted but provider_id has to specified)
|
13
|
+
# - provider_id: Specify a provider for an encounter the result is going to be created under
|
14
|
+
# - date: Retrospective date when the result was received (can be ommitted, defaults to today)
|
15
|
+
# - measures: An array of measures. A measure is an object of the following structure
|
16
|
+
# - indicator: An object that has a concept_id field (concept_id of the indicator)
|
17
|
+
# - value_type: An enum that's limited to 'numeric', 'boolean', 'text', and 'coded'
|
18
|
+
# result_enter_by: A string that specifies who created the result
|
19
|
+
def create_results(test_id, params, result_enter_by = 'LIMS')
|
20
|
+
serializer = {}
|
21
|
+
results_obs = {}
|
22
|
+
ActiveRecord::Base.transaction do
|
23
|
+
test = Lab::LabTest.find(test_id) rescue nil
|
24
|
+
test = Lab::LabTest.find_by_uuid(test_id) if test.blank?
|
25
|
+
encounter = find_encounter(test, encounter_id: params[:encounter_id],
|
26
|
+
encounter_uuid: params[:encounter],
|
27
|
+
date: params[:date]&.to_date,
|
28
|
+
provider_id: params[:provider_id])
|
29
|
+
|
30
|
+
results_obs = create_results_obs(encounter, test, params[:date], params[:comments])
|
31
|
+
params[:measures].map { |measure| add_measure_to_results(results_obs, measure, params[:date]) }
|
32
|
+
OrderExtension.create!(creator: User.current, value: result_enter_by, order_id: results_obs.order_id,
|
33
|
+
date_created: Time.now)
|
34
|
+
|
35
|
+
serializer = Lab::ResultSerializer.serialize(results_obs)
|
36
|
+
end
|
37
|
+
process_acknowledgement(results_obs, result_enter_by)
|
38
|
+
precess_notification_message(results_obs, serializer, result_enter_by)
|
39
|
+
Rails.logger.info("Lab::ResultsService: Result created for test #{test_id} #{serializer}")
|
40
|
+
serializer
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def precess_notification_message(result, values, result_enter_by)
|
46
|
+
order = Order.find(result.order_id)
|
47
|
+
data = { Type: result_enter_by,
|
48
|
+
'Test type': ConceptName.find_by(concept_id: result.test.value_coded)&.name,
|
49
|
+
'Accession number': order&.accession_number,
|
50
|
+
'Orde date': Order.columns.include?('start_date') ? order.start_date : order.date_created,
|
51
|
+
'ARV-Number': find_arv_number(result.person_id),
|
52
|
+
PatientID: result.person_id,
|
53
|
+
'Ordered By': Order.columns.include?('provider_id') ? order&.provider&.person&.name : Person.find(order.creator)&.name,
|
54
|
+
Result: values }.as_json
|
55
|
+
NotificationService.new.create_notification(result_enter_by, data)
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_acknowledgement(results, results_enter_by)
|
59
|
+
Lab::AcknowledgementService.create_acknowledgement({ order_id: results.order_id, test: results.test.id,
|
60
|
+
date_received: Time.now,
|
61
|
+
entered_by: results_enter_by })
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_arv_number(patient_id)
|
65
|
+
PatientIdentifier.joins(:type)
|
66
|
+
.merge(PatientIdentifierType.where(name: 'ARV Number'))
|
67
|
+
.where(patient_id: patient_id)
|
68
|
+
.first&.identifier
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_encounter(test, encounter_id: nil, encounter_uuid: nil, date: nil, provider_id: nil)
|
72
|
+
return Encounter.find(encounter_id) if encounter_id
|
73
|
+
return Encounter.find_by_uuid(encounter_uuid) if encounter_uuid
|
74
|
+
|
75
|
+
encounter = Encounter.new
|
76
|
+
encounter.patient_id = test.person_id
|
77
|
+
encounter.program_id = test.encounter.program_id if Encounter.column_names.include?('program_id')
|
78
|
+
encounter.visit_id = test.encounter.visit_id if Encounter.column_names.include?('visit_id')
|
79
|
+
encounter.encounter_type = EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME)
|
80
|
+
encounter.encounter_datetime = date || Date.today
|
81
|
+
encounter.provider_id = provider_id || User.current.user_id if Encounter.column_names.include?('provider_id')
|
82
|
+
|
83
|
+
encounter.save!
|
84
|
+
|
85
|
+
encounter
|
86
|
+
end
|
87
|
+
|
88
|
+
# Creates the parent observation for results to which the different measures are attached
|
89
|
+
def create_results_obs(encounter, test, date, comments = nil)
|
90
|
+
void_existing_results_obs(encounter, test)
|
91
|
+
Lab::LabResult.create!(
|
92
|
+
person_id: encounter.patient_id,
|
93
|
+
encounter_id: encounter.encounter_id,
|
94
|
+
concept_id: test_result_concept.concept_id,
|
95
|
+
order_id: test.order_id,
|
96
|
+
obs_group_id: test.obs_id,
|
97
|
+
obs_datetime: date&.to_datetime || DateTime.now,
|
98
|
+
comments: comments
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def void_existing_results_obs(encounter, test)
|
103
|
+
result = Lab::LabResult.find_by(person_id: encounter.patient_id,
|
104
|
+
concept_id: test_result_concept.concept_id,
|
105
|
+
obs_group_id: test.obs_id)
|
106
|
+
return unless result
|
107
|
+
|
108
|
+
OrderExtension.find_by(order_id: result.order_id)&.void("Updated/overwritten by #{User.current.username}")
|
109
|
+
result.measures.map { |child_obs| child_obs.void("Updated/overwritten by #{User.current.username}") }
|
110
|
+
result.void("Updated/overwritten by #{User.current.username}")
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_result_concept
|
114
|
+
ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_measure_to_results(results_obs, params, date)
|
118
|
+
validate_measure_params(params)
|
119
|
+
|
120
|
+
concept_id = params[:indicator][:concept_id] || Concept.find_concept_by_uuid(params.dig(:indicator, :concept))&.id
|
121
|
+
|
122
|
+
Observation.create!(
|
123
|
+
person_id: results_obs.person_id,
|
124
|
+
encounter_id: results_obs.encounter_id,
|
125
|
+
order_id: results_obs.order_id,
|
126
|
+
concept_id: concept_id,
|
127
|
+
obs_group_id: results_obs.obs_id,
|
128
|
+
obs_datetime: date&.to_datetime || DateTime.now,
|
129
|
+
**make_measure_value(params)
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_measure_params(params)
|
134
|
+
raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
|
135
|
+
|
136
|
+
if params[:indicator]&.[](:concept_id).blank? && params[:indicator]&.[](:concept).blank?
|
137
|
+
raise InvalidParameterError, 'measures.indicator.concept_id or concept is required'
|
138
|
+
end
|
139
|
+
|
140
|
+
params
|
141
|
+
end
|
142
|
+
|
143
|
+
# Converts user provided measure values to observation_values
|
144
|
+
def make_measure_value(params)
|
145
|
+
obs_value = { value_modifier: params[:value_modifier] }
|
146
|
+
value_type = params[:value_type] || 'text'
|
147
|
+
|
148
|
+
case value_type.downcase
|
149
|
+
when 'numeric' then obs_value.merge(value_numeric: params[:value])
|
150
|
+
when 'boolean' then obs_value.merge(value_boolean: parse_boolen_value(params[:value]))
|
151
|
+
when 'coded' then obs_value.merge(value_coded: params[:value]) # Should we be collecting value_name_coded_id?
|
152
|
+
when 'text' then obs_value.merge(value_text: params[:value])
|
153
|
+
else raise InvalidParameterError, "Invalid value_type: #{params[:value_type]}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_boolen_value(string)
|
158
|
+
case string.downcase
|
159
|
+
when 'true' then true
|
160
|
+
when 'false' then false
|
161
|
+
else raise InvalidParameterError, "Invalid boolean value: #{string}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|