mahis_emr_api_lab 1.2.0
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 +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
|