his_emr_api_lab 1.1.22 → 1.1.23
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/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 +38 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +19 -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 +26 -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_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/serializers/lab/lab_order_serializer.rb +55 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +29 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/labelling_service/order_label.rb +106 -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 +416 -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 +100 -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 +244 -0
- data/app/services/lab/lims/pull_worker.rb +289 -0
- data/app/services/lab/lims/push_worker.rb +149 -0
- data/app/services/lab/lims/utils.rb +91 -0
- data/app/services/lab/lims/worker.rb +86 -0
- data/app/services/lab/metadata.rb +24 -0
- data/app/services/lab/orders_search_service.rb +66 -0
- data/app/services/lab/orders_service.rb +212 -0
- data/app/services/lab/results_service.rb +149 -0
- data/app/services/lab/tests_service.rb +93 -0
- data/config/routes.rb +17 -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/his_emr_api_lab.rb +5 -0
- data/lib/lab/engine.rb +15 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +38 -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 +81 -2
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
orders = Lab::LabOrder.prefetch_relationships
|
|
11
|
+
.where(filters)
|
|
12
|
+
.order(start_date: :desc)
|
|
13
|
+
|
|
14
|
+
orders = filter_orders_by_status(orders, pop_filters(extra_filters, :status))
|
|
15
|
+
orders = filter_orders_by_date(orders, extra_filters)
|
|
16
|
+
|
|
17
|
+
orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_orders_without_results(patient_id: nil)
|
|
21
|
+
results_query = Lab::LabResult.all
|
|
22
|
+
results_query = results_query.where(person_id: patient_id) if patient_id
|
|
23
|
+
|
|
24
|
+
query = Lab::LabOrder.where.not(order_id: results_query.select(:order_id))
|
|
25
|
+
query = query.where(patient_id: patient_id) if patient_id
|
|
26
|
+
|
|
27
|
+
query
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def filter_orders_by_date(orders, date: nil, end_date: nil)
|
|
31
|
+
date = date&.to_date
|
|
32
|
+
end_date = end_date&.to_date
|
|
33
|
+
|
|
34
|
+
return orders.where('start_date BETWEEN ? AND ?', date, end_date + 1.day) if date && end_date
|
|
35
|
+
|
|
36
|
+
return orders.where('start_date BETWEEN ? AND ?', date, date + 1.day) if date
|
|
37
|
+
|
|
38
|
+
return orders.where('start_date < ?', end_date + 1.day) if end_date
|
|
39
|
+
|
|
40
|
+
orders
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def filter_orders_by_status(orders, status: nil)
|
|
44
|
+
case status&.downcase
|
|
45
|
+
when 'ordered' then orders.where(concept_id: unknown_concept_id)
|
|
46
|
+
when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
|
|
47
|
+
else orders
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unknown_concept_id
|
|
52
|
+
ConceptName.find_by_name!('Unknown').concept_id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def pop_filters(params, *filters)
|
|
56
|
+
filters.each_with_object({}) do |filter, popped_params|
|
|
57
|
+
next unless params.key?(filter)
|
|
58
|
+
|
|
59
|
+
popped_params[filter.to_sym] = params.delete(filter)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_results(order); end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
order = create_order(encounter, order_params)
|
|
56
|
+
|
|
57
|
+
Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
|
|
58
|
+
|
|
59
|
+
Lab::LabOrderSerializer.serialize_order(
|
|
60
|
+
order, requesting_clinician: add_requesting_clinician(order, order_params),
|
|
61
|
+
reason_for_test: add_reason_for_test(order, order_params),
|
|
62
|
+
target_lab: add_target_lab(order, order_params)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update_order(order_id, params)
|
|
68
|
+
specimen_id = params.dig(:specimen, :concept_id)
|
|
69
|
+
raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id
|
|
70
|
+
|
|
71
|
+
order = Lab::LabOrder.find(order_id)
|
|
72
|
+
if order.concept_id != unknown_concept_id && !params[:force_update]&.casecmp?('true')
|
|
73
|
+
raise ::UnprocessableEntityError, "Can't change order specimen once set"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if specimen_id.to_i != order.concept_id
|
|
77
|
+
Rails.logger.debug("Updating order ##{order.order_id}")
|
|
78
|
+
order.update!(concept_id: specimen_id,
|
|
79
|
+
discontinued: true,
|
|
80
|
+
discontinued_by: User.current.user_id,
|
|
81
|
+
discontinued_date: params[:date]&.to_date || Time.now,
|
|
82
|
+
discontinued_reason_non_coded: 'Sample drawn/updated')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if params[:reason_for_test_id]
|
|
86
|
+
Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
|
|
87
|
+
update_reason_for_test(order, params[:reason_for_test_id])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Lab::LabOrderSerializer.serialize_order(order)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def void_order(order_id, reason)
|
|
94
|
+
order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab], tests: [:result])
|
|
95
|
+
.find(order_id)
|
|
96
|
+
|
|
97
|
+
order.requesting_clinician&.void(reason)
|
|
98
|
+
order.reason_for_test&.void(reason)
|
|
99
|
+
order.target_lab&.void(reason)
|
|
100
|
+
|
|
101
|
+
order.tests.each { |test| test.void(reason) }
|
|
102
|
+
voided = order.void(reason)
|
|
103
|
+
|
|
104
|
+
voided
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Extract an encounter from the given parameters.
|
|
111
|
+
#
|
|
112
|
+
# Uses an encounter_id to retrieve an encounter if provided else
|
|
113
|
+
# a 'Lab' encounter is created using the provided program_id and
|
|
114
|
+
# patient_id.
|
|
115
|
+
def find_encounter(order_params)
|
|
116
|
+
return Encounter.find(order_params[:encounter_id]) if order_params[:encounter_id]
|
|
117
|
+
|
|
118
|
+
raise InvalidParameterError, 'encounter_id or patient_id required' unless order_params[:patient_id]
|
|
119
|
+
|
|
120
|
+
program_id = order_params[:program_id] || Program.find_by_name!(Lab::Metadata::LAB_PROGRAM_NAME).program_id
|
|
121
|
+
|
|
122
|
+
Encounter.create!(
|
|
123
|
+
patient_id: order_params[:patient_id],
|
|
124
|
+
program_id: program_id,
|
|
125
|
+
type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
|
|
126
|
+
encounter_datetime: order_params[:date] || Date.today,
|
|
127
|
+
provider_id: order_params[:provider_id] || User.current.person.person_id
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def create_order(encounter, params)
|
|
132
|
+
Lab::LabOrder.create!(
|
|
133
|
+
order_type: OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME),
|
|
134
|
+
concept_id: params.dig(:specimen, :concept_id) || unknown_concept_id,
|
|
135
|
+
encounter_id: encounter.encounter_id,
|
|
136
|
+
patient_id: encounter.patient_id,
|
|
137
|
+
start_date: params[:date]&.to_date || Date.today,
|
|
138
|
+
auto_expire_date: params[:end_date],
|
|
139
|
+
accession_number: params[:accession_number] || next_accession_number,
|
|
140
|
+
orderer: User.current&.user_id
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Attach the requesting clinician to an order
|
|
146
|
+
def add_requesting_clinician(order, params)
|
|
147
|
+
create_order_observation(
|
|
148
|
+
order,
|
|
149
|
+
Lab::Metadata::REQUESTING_CLINICIAN_CONCEPT_NAME,
|
|
150
|
+
params[:date],
|
|
151
|
+
value_text: params['requesting_clinician']
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
##
|
|
156
|
+
# Attach a reason for the order/test
|
|
157
|
+
#
|
|
158
|
+
# Examples of reasons include: Routine, Targeted, Confirmatory, Repeat, or Stat.
|
|
159
|
+
def add_reason_for_test(order, params)
|
|
160
|
+
create_order_observation(
|
|
161
|
+
order,
|
|
162
|
+
Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME,
|
|
163
|
+
params[:date],
|
|
164
|
+
value_coded: params[:reason_for_test_id]
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
##
|
|
169
|
+
# Attach the lab where the test is going to get carried out.
|
|
170
|
+
def add_target_lab(order, params)
|
|
171
|
+
return nil unless params['target_lab']
|
|
172
|
+
|
|
173
|
+
create_order_observation(
|
|
174
|
+
order,
|
|
175
|
+
Lab::Metadata::TARGET_LAB_CONCEPT_NAME,
|
|
176
|
+
params[:date],
|
|
177
|
+
value_text: params['target_lab']
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def create_order_observation(order, concept_name, date, **values)
|
|
182
|
+
Observation.create!(
|
|
183
|
+
order: order,
|
|
184
|
+
encounter_id: order.encounter_id,
|
|
185
|
+
person_id: order.patient_id,
|
|
186
|
+
concept_id: ConceptName.find_by_name!(concept_name).concept_id,
|
|
187
|
+
obs_datetime: date&.to_time || Time.now,
|
|
188
|
+
**values
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def next_accession_number(date = nil)
|
|
193
|
+
Lab::AccessionNumberService.next_accession_number(date)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def unknown_concept_id
|
|
197
|
+
ConceptName.find_by_name!('Unknown').concept_id
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def update_reason_for_test(order, concept_id)
|
|
201
|
+
raise InvalidParameterError, "Reason for test can't be blank" if concept_id.blank?
|
|
202
|
+
|
|
203
|
+
return if order.reason_for_test&.value_coded == concept_id
|
|
204
|
+
|
|
205
|
+
raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded
|
|
206
|
+
|
|
207
|
+
order.reason_for_test&.delete
|
|
208
|
+
add_reason_for_test(order, date: order.start_date, reason_for_test_id: concept_id)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
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)
|
|
24
|
+
encounter = find_encounter(test, encounter_id: params[:encounter_id],
|
|
25
|
+
date: params[:date]&.to_date,
|
|
26
|
+
provider_id: params[:provider_id])
|
|
27
|
+
|
|
28
|
+
results_obs = create_results_obs(encounter, test, params[:date], params[:comments])
|
|
29
|
+
params[:measures].map { |measure| add_measure_to_results(results_obs, measure, params[:date]) }
|
|
30
|
+
OrderExtension.create!(creator: User.current, value: result_enter_by, order_id: results_obs.order_id,
|
|
31
|
+
date_created: Time.now)
|
|
32
|
+
|
|
33
|
+
serializer = Lab::ResultSerializer.serialize(results_obs)
|
|
34
|
+
end
|
|
35
|
+
NotificationService.new.create_notification(result_enter_by, prepare_notification_message(results_obs, serializer, result_enter_by))
|
|
36
|
+
Rails.logger.info("Lab::ResultsService: Result created for test #{test_id} #{serializer}")
|
|
37
|
+
serializer
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def prepare_notification_message(result, values, result_enter_by)
|
|
43
|
+
order = Order.find(result.order_id)
|
|
44
|
+
{ Type: result_enter_by,
|
|
45
|
+
'Test type': ConceptName.find_by(concept_id: result.test.value_coded)&.name,
|
|
46
|
+
'Accession number': order&.accession_number,
|
|
47
|
+
'ARV-Number': find_arv_number(result.person_id),
|
|
48
|
+
PatientID: result.person_id,
|
|
49
|
+
'Ordered By': order&.provider&.person&.name,
|
|
50
|
+
Result: values }.as_json
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_arv_number(patient_id)
|
|
54
|
+
PatientIdentifier.joins(:type)
|
|
55
|
+
.merge(PatientIdentifierType.where(name: 'ARV Number'))
|
|
56
|
+
.where(patient_id: patient_id)
|
|
57
|
+
.first&.identifier
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def find_encounter(test, encounter_id: nil, date: nil, provider_id: nil)
|
|
61
|
+
return Encounter.find(encounter_id) if encounter_id
|
|
62
|
+
|
|
63
|
+
Encounter.create!(
|
|
64
|
+
patient_id: test.person_id,
|
|
65
|
+
program_id: test.encounter.program_id,
|
|
66
|
+
type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
|
|
67
|
+
encounter_datetime: date || Date.today,
|
|
68
|
+
provider_id: provider_id || User.current.user_id
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Creates the parent observation for results to which the different measures are attached
|
|
73
|
+
def create_results_obs(encounter, test, date, comments = nil)
|
|
74
|
+
void_existing_results_obs(encounter, test)
|
|
75
|
+
|
|
76
|
+
Lab::LabResult.create!(
|
|
77
|
+
person_id: encounter.patient_id,
|
|
78
|
+
encounter_id: encounter.encounter_id,
|
|
79
|
+
concept_id: test_result_concept.concept_id,
|
|
80
|
+
order_id: test.order_id,
|
|
81
|
+
obs_group_id: test.obs_id,
|
|
82
|
+
obs_datetime: date&.to_datetime || DateTime.now,
|
|
83
|
+
comments: comments
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def void_existing_results_obs(encounter, test)
|
|
88
|
+
result = Lab::LabResult.find_by(person_id: encounter.patient_id,
|
|
89
|
+
concept_id: test_result_concept.concept_id,
|
|
90
|
+
obs_group_id: test.obs_id)
|
|
91
|
+
return unless result
|
|
92
|
+
|
|
93
|
+
OrderExtension.find_by(order_id: result.order_id)&.void("Updated/overwritten by #{User.current.username}")
|
|
94
|
+
result.measures.map { |child_obs| child_obs.void("Updated/overwritten by #{User.current.username}") }
|
|
95
|
+
result.void("Updated/overwritten by #{User.current.username}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_result_concept
|
|
99
|
+
ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_measure_to_results(results_obs, params, date)
|
|
103
|
+
validate_measure_params(params)
|
|
104
|
+
|
|
105
|
+
Observation.create!(
|
|
106
|
+
person_id: results_obs.person_id,
|
|
107
|
+
encounter_id: results_obs.encounter_id,
|
|
108
|
+
order_id: results_obs.order_id,
|
|
109
|
+
concept_id: params[:indicator][:concept_id],
|
|
110
|
+
obs_group_id: results_obs.obs_id,
|
|
111
|
+
obs_datetime: date&.to_datetime || DateTime.now,
|
|
112
|
+
**make_measure_value(params)
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_measure_params(params)
|
|
117
|
+
raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
|
|
118
|
+
|
|
119
|
+
if params[:indicator]&.[](:concept_id).blank?
|
|
120
|
+
raise InvalidParameterError, 'measures.indicator.concept_id is required'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
params
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Converts user provided measure values to observation_values
|
|
127
|
+
def make_measure_value(params)
|
|
128
|
+
obs_value = { value_modifier: params[:value_modifier] }
|
|
129
|
+
value_type = params[:value_type] || 'text'
|
|
130
|
+
|
|
131
|
+
case value_type.downcase
|
|
132
|
+
when 'numeric' then obs_value.merge(value_numeric: params[:value])
|
|
133
|
+
when 'boolean' then obs_value.merge(value_boolean: parse_boolen_value(params[:value]))
|
|
134
|
+
when 'coded' then obs_value.merge(value_coded: params[:value]) # Should we be collecting value_name_coded_id?
|
|
135
|
+
when 'text' then obs_value.merge(value_text: params[:value])
|
|
136
|
+
else raise InvalidParameterError, "Invalid value_type: #{params[:value_type]}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_boolen_value(string)
|
|
141
|
+
case string.downcase
|
|
142
|
+
when 'true' then true
|
|
143
|
+
when 'false' then false
|
|
144
|
+
else raise InvalidParameterError, "Invalid boolean value: #{string}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lab
|
|
4
|
+
##
|
|
5
|
+
# Manage tests that have been ordered through the ordering service.
|
|
6
|
+
module TestsService
|
|
7
|
+
class << self
|
|
8
|
+
def find_tests(filters)
|
|
9
|
+
tests = Lab::LabTest.all
|
|
10
|
+
|
|
11
|
+
tests = filter_tests(tests, test_type_id: filters.delete(:test_type_id),
|
|
12
|
+
patient_id: filters.delete(:patient_id))
|
|
13
|
+
|
|
14
|
+
tests = filter_tests_by_results(tests) if %w[1 true].include?(filters[:pending_results]&.downcase)
|
|
15
|
+
|
|
16
|
+
tests = filter_tests_by_order(tests, accession_number: filters.delete(:accession_number),
|
|
17
|
+
order_date: filters.delete(:order_date),
|
|
18
|
+
specimen_type_id: filters.delete(:specimen_type_id))
|
|
19
|
+
|
|
20
|
+
tests.map { |test| Lab::TestSerializer.serialize(test) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_tests(order, date, tests_params)
|
|
24
|
+
raise InvalidParameterError, 'tests are required' if tests_params.nil? || tests_params.empty?
|
|
25
|
+
|
|
26
|
+
Lab::LabTest.transaction do
|
|
27
|
+
tests_params.map do |params|
|
|
28
|
+
test = Lab::LabTest.create!(
|
|
29
|
+
concept_id: ConceptName.find_by_name!(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
|
30
|
+
.concept_id,
|
|
31
|
+
encounter_id: order.encounter_id,
|
|
32
|
+
order_id: order.order_id,
|
|
33
|
+
person_id: order.patient_id,
|
|
34
|
+
obs_datetime: date&.to_time || Time.now,
|
|
35
|
+
value_coded: params[:concept_id]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
Lab::TestSerializer.serialize(test, order: order)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Filter a LabTests Relation.
|
|
47
|
+
def filter_tests(tests, test_type_id: nil, patient_id: nil)
|
|
48
|
+
tests = tests.where(value_coded: test_type_id) if test_type_id
|
|
49
|
+
tests = tests.where(person_id: patient_id) if patient_id
|
|
50
|
+
|
|
51
|
+
tests
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Filter out any tests having results
|
|
56
|
+
def filter_tests_by_results(tests)
|
|
57
|
+
tests.where.not(obs_id: Lab::LabResult.all.select(:obs_group_id))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Filter LabTests Relation using their parent orders parameters.
|
|
62
|
+
def filter_tests_by_order(tests, accession_number: nil, order_date: nil, specimen_type_id: nil)
|
|
63
|
+
return tests unless accession_number || order_date || specimen_type_id
|
|
64
|
+
|
|
65
|
+
lab_orders = filter_orders(Lab::LabOrder.all, accession_number: accession_number,
|
|
66
|
+
order_date: order_date,
|
|
67
|
+
specimen_type_id: specimen_type_id)
|
|
68
|
+
tests.joins(:order).merge(lab_orders)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def filter_orders(orders, accession_number: nil, order_date: nil, specimen_type_id: nil)
|
|
72
|
+
if order_date
|
|
73
|
+
order_date = order_date.to_date
|
|
74
|
+
orders = orders.where('start_date >= ? AND start_date < ?', order_date, order_date + 1.day)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
orders = orders.where(accession_number: accession_number) if accession_number
|
|
78
|
+
orders = orders.where(concept_id: specimen_type_id) if specimen_type_id
|
|
79
|
+
|
|
80
|
+
orders
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def create_test(order, date, test_type_id)
|
|
84
|
+
create_order_observation(
|
|
85
|
+
order,
|
|
86
|
+
Lab::Metadata::TEST_TYPE_CONCEPT_NAME,
|
|
87
|
+
date,
|
|
88
|
+
value_coded: test_type_id
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Lab::Engine.routes.draw do
|
|
4
|
+
resources :orders, path: 'api/v1/lab/orders'
|
|
5
|
+
resources :tests, path: 'api/v1/lab/tests', except: %i[update] do # ?pending=true to select tests without results?
|
|
6
|
+
resources :results, only: %i[index create destroy]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
get 'api/v1/lab/labels/order', to: 'labels#print_order_label'
|
|
10
|
+
|
|
11
|
+
# Metadata
|
|
12
|
+
# TODO: Move the following to namespace /concepts
|
|
13
|
+
resources :specimen_types, only: %i[index], path: 'api/v1/lab/specimen_types'
|
|
14
|
+
resources :test_result_indicators, only: %i[index], path: 'api/v1/lab/test_result_indicators'
|
|
15
|
+
resources :test_types, only: %i[index], path: 'api/v1/lab/test_types'
|
|
16
|
+
resources :reasons_for_test, only: %i[index], path: 'api/v1/lab/reasons_for_test'
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateLabLimsOrderMappings < ActiveRecord::Migration[5.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :lab_lims_order_mappings do |t|
|
|
4
|
+
t.integer :lims_id, null: false, unique: true
|
|
5
|
+
t.integer :order_id, null: false, unique: true
|
|
6
|
+
t.datetime :pushed_at
|
|
7
|
+
t.datetime :pulled_at
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
|
|
11
|
+
t.foreign_key :orders, primary_key: :order_id, column: :order_id
|
|
12
|
+
t.index :lims_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ChangeLimsIdToStringInLimsOrderMapping < ActiveRecord::Migration[5.2]
|
|
4
|
+
def change
|
|
5
|
+
reversible do |direction|
|
|
6
|
+
direction.up do
|
|
7
|
+
change_column :lab_lims_order_mappings, :lims_id, :string, null: false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
direction.down do
|
|
11
|
+
change_column :lab_lims_order_mappings, :lims_id, :integer, null: false
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateLabLimsFailedImports < ActiveRecord::Migration[5.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :lab_lims_failed_imports do |t|
|
|
6
|
+
t.string :lims_id, null: false
|
|
7
|
+
t.string :tracking_number, null: false
|
|
8
|
+
t.string :patient_nhid, null: false
|
|
9
|
+
t.string :reason, null: false
|
|
10
|
+
t.string :diff, limit: 2048
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
|
|
14
|
+
t.index :lims_id
|
|
15
|
+
t.index :patient_nhid
|
|
16
|
+
t.index :tracking_number
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class FixNumericResultsValueType < ActiveRecord::Migration[5.2]
|
|
2
|
+
def up
|
|
3
|
+
results = Lab::LabResult.all.includes(:children)
|
|
4
|
+
|
|
5
|
+
ActiveRecord::Base.connection.transaction do
|
|
6
|
+
results.each do |result|
|
|
7
|
+
result.children.each do |measure|
|
|
8
|
+
next unless measure.value_text&.match?(/^[+-]?((\d+(\.\d+)?)|\.\d+)$/)
|
|
9
|
+
|
|
10
|
+
puts "Updating result value type for result measure ##{measure.obs_id}"
|
|
11
|
+
measure.value_numeric = measure.value_text
|
|
12
|
+
measure.value_text = nil
|
|
13
|
+
measure.save!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def down; end
|
|
20
|
+
end
|