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,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lab
|
|
4
|
+
module Lims
|
|
5
|
+
module Api
|
|
6
|
+
class MysqlApi
|
|
7
|
+
def self.start
|
|
8
|
+
instance = MysqlApi.new
|
|
9
|
+
orders_processed = 0
|
|
10
|
+
instance.consume_orders(from: 0, limit: 1000) do |order|
|
|
11
|
+
puts "Order ##{orders_processed}"
|
|
12
|
+
pp order
|
|
13
|
+
orders_processed += 1
|
|
14
|
+
puts
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(processes: 1, on_merge_processes: nil)
|
|
19
|
+
@processes = processes
|
|
20
|
+
@on_merge_processes = on_merge_processes
|
|
21
|
+
@mysql_connection_pool = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def multiprocessed?
|
|
25
|
+
@processes > 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def consume_orders(from: nil, limit: 1000)
|
|
29
|
+
loop do
|
|
30
|
+
specimens_to_process = specimens(from, limit)
|
|
31
|
+
break if specimens_to_process.size.zero?
|
|
32
|
+
|
|
33
|
+
processes = multiprocessed? ? @processes : 0
|
|
34
|
+
on_merge_processes = ->(_item, index, _result) { @on_merge_processes&.call(from + index) }
|
|
35
|
+
|
|
36
|
+
Parallel.map(specimens_to_process, in_processes: processes, finish: on_merge_processes) do |specimen|
|
|
37
|
+
User.current ||= Utils.lab_user
|
|
38
|
+
|
|
39
|
+
tests = specimen_tests(specimen['specimen_id'])
|
|
40
|
+
results = tests.each_with_object({}) do |test, object|
|
|
41
|
+
object[test['test_name']] = test_results(test['test_id'])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
dto = make_order_dto(
|
|
45
|
+
specimen: specimen,
|
|
46
|
+
patient: specimen_patient(specimen['specimen_id']),
|
|
47
|
+
test_results: results,
|
|
48
|
+
specimen_status_trail: specimen_status_trail(specimen['specimen_id']),
|
|
49
|
+
test_status_trail: tests.each_with_object({}) do |test, trails|
|
|
50
|
+
trails[test['test_name']] = test_status_trail(test['test_id'])
|
|
51
|
+
end
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
yield dto, OpenStruct.new(last_seq: from)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
from += limit
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parallel_map(items, on_merge: nil, &block); end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def specimens(start_id, limit)
|
|
66
|
+
query = <<~SQL
|
|
67
|
+
SELECT specimen.id AS specimen_id,
|
|
68
|
+
specimen.couch_id AS doc_id,
|
|
69
|
+
specimen_types.name AS specimen_name,
|
|
70
|
+
specimen.tracking_number,
|
|
71
|
+
specimen.priority,
|
|
72
|
+
specimen.target_lab,
|
|
73
|
+
specimen.sending_facility,
|
|
74
|
+
specimen.drawn_by_id,
|
|
75
|
+
specimen.drawn_by_name,
|
|
76
|
+
specimen.drawn_by_phone_number,
|
|
77
|
+
specimen.ward_id,
|
|
78
|
+
specimen_statuses.name AS specimen_status,
|
|
79
|
+
specimen.district,
|
|
80
|
+
specimen.date_created AS order_date
|
|
81
|
+
FROM specimen
|
|
82
|
+
INNER JOIN specimen_types ON specimen_types.id = specimen.specimen_type_id
|
|
83
|
+
INNER JOIN specimen_statuses ON specimen_statuses.id = specimen.specimen_status_id
|
|
84
|
+
SQL
|
|
85
|
+
|
|
86
|
+
query = "#{query} WHERE specimen.id > #{sql_escape(start_id)}" if start_id
|
|
87
|
+
query = "#{query} LIMIT #{limit.to_i}"
|
|
88
|
+
|
|
89
|
+
Rails.logger.debug(query)
|
|
90
|
+
query(query)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Pull patient associated with given specimen
|
|
95
|
+
def specimen_patient(specimen_id)
|
|
96
|
+
results = query <<~SQL
|
|
97
|
+
SELECT patients.patient_number AS nhid,
|
|
98
|
+
patients.name,
|
|
99
|
+
patients.gender,
|
|
100
|
+
DATE(patients.dob) AS birthdate
|
|
101
|
+
FROM patients
|
|
102
|
+
INNER JOIN tests
|
|
103
|
+
ON tests.patient_id = patients.id
|
|
104
|
+
AND tests.specimen_id = #{sql_escape(specimen_id)}
|
|
105
|
+
LIMIT 1
|
|
106
|
+
SQL
|
|
107
|
+
|
|
108
|
+
results.first
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def specimen_tests(specimen_id)
|
|
112
|
+
query <<~SQL
|
|
113
|
+
SELECT tests.id AS test_id,
|
|
114
|
+
test_types.name AS test_name,
|
|
115
|
+
tests.created_by AS drawn_by_name
|
|
116
|
+
FROM tests
|
|
117
|
+
INNER JOIN test_types ON test_types.id = tests.test_type_id
|
|
118
|
+
WHERE tests.specimen_id = #{sql_escape(specimen_id)}
|
|
119
|
+
SQL
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def specimen_status_trail(specimen_id)
|
|
123
|
+
query <<~SQL
|
|
124
|
+
SELECT specimen_statuses.name AS status_name,
|
|
125
|
+
specimen_status_trails.who_updated_id AS updated_by_id,
|
|
126
|
+
specimen_status_trails.who_updated_name AS updated_by_name,
|
|
127
|
+
specimen_status_trails.who_updated_phone_number AS updated_by_phone_number,
|
|
128
|
+
specimen_status_trails.time_updated AS date
|
|
129
|
+
FROM specimen_status_trails
|
|
130
|
+
INNER JOIN specimen_statuses
|
|
131
|
+
ON specimen_statuses.id = specimen_status_trails.specimen_status_id
|
|
132
|
+
WHERE specimen_status_trails.specimen_id = #{sql_escape(specimen_id)}
|
|
133
|
+
SQL
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_status_trail(test_id)
|
|
137
|
+
query <<~SQL
|
|
138
|
+
SELECT test_statuses.name AS status_name,
|
|
139
|
+
test_status_trails.who_updated_id AS updated_by_id,
|
|
140
|
+
test_status_trails.who_updated_name AS updated_by_name,
|
|
141
|
+
test_status_trails.who_updated_phone_number AS updated_by_phone_number,
|
|
142
|
+
COALESCE(test_status_trails.time_updated, test_status_trails.created_at) AS date
|
|
143
|
+
FROM test_status_trails
|
|
144
|
+
INNER JOIN test_statuses
|
|
145
|
+
ON test_statuses.id = test_status_trails.test_status_id
|
|
146
|
+
WHERE test_status_trails.test_id = #{sql_escape(test_id)}
|
|
147
|
+
SQL
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_results(test_id)
|
|
151
|
+
query <<~SQL
|
|
152
|
+
SELECT measures.name AS measure_name,
|
|
153
|
+
test_results.result,
|
|
154
|
+
test_results.time_entered AS date
|
|
155
|
+
FROM test_results
|
|
156
|
+
INNER JOIN measures ON measures.id = test_results.measure_id
|
|
157
|
+
WHERE test_results.test_id = #{sql_escape(test_id)}
|
|
158
|
+
SQL
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def make_order_dto(specimen:, patient:, test_status_trail:, specimen_status_trail:, test_results:)
|
|
162
|
+
drawn_by_first_name, drawn_by_last_name = specimen['drawn_by_name']&.split
|
|
163
|
+
patient_first_name, patient_last_name = patient['name'].split
|
|
164
|
+
|
|
165
|
+
OrderDTO.new(
|
|
166
|
+
_id: specimen['doc_id'].blank? ? SecureRandom.uuid : specimen['doc_id'],
|
|
167
|
+
_rev: '0',
|
|
168
|
+
tracking_number: specimen['tracking_number'],
|
|
169
|
+
date_created: specimen['order_date'],
|
|
170
|
+
sample_type: specimen['specimen_name'],
|
|
171
|
+
tests: test_status_trail.keys,
|
|
172
|
+
districy: specimen['district'], # districy [sic] - That's how it's named
|
|
173
|
+
order_location: specimen['ward_id'],
|
|
174
|
+
sending_facility: specimen['sending_facility'],
|
|
175
|
+
receiving_facility: specimen['target_lab'],
|
|
176
|
+
priority: specimen['priority'],
|
|
177
|
+
patient: {
|
|
178
|
+
id: patient['nhid'],
|
|
179
|
+
first_name: patient_first_name,
|
|
180
|
+
last_name: patient_last_name,
|
|
181
|
+
gender: patient['gender'],
|
|
182
|
+
birthdate: patient['birthdate'],
|
|
183
|
+
email: nil,
|
|
184
|
+
phone_number: nil
|
|
185
|
+
},
|
|
186
|
+
type: 'Order',
|
|
187
|
+
who_order_test: {
|
|
188
|
+
first_name: drawn_by_first_name,
|
|
189
|
+
last_name: drawn_by_last_name,
|
|
190
|
+
id: specimen['drawn_by_id'],
|
|
191
|
+
phone_number: specimen['drawn_by_phone_number']
|
|
192
|
+
},
|
|
193
|
+
sample_status: specimen['specimen_status'],
|
|
194
|
+
sample_statuses: specimen_status_trail.each_with_object({}) do |trail_entry, object|
|
|
195
|
+
first_name, last_name = trail_entry['updated_by_name'].split
|
|
196
|
+
|
|
197
|
+
object[format_date(trail_entry['date'])] = {
|
|
198
|
+
status: trail_entry['status_name'],
|
|
199
|
+
updated_by: {
|
|
200
|
+
first_name: first_name,
|
|
201
|
+
last_name: last_name,
|
|
202
|
+
phone_number: trail_entry['updated_by_phone_number'],
|
|
203
|
+
id: trail_entry['updated_by_id']
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
end,
|
|
207
|
+
test_statuses: test_status_trail.each_with_object({}) do |trail_entry, formatted_trail|
|
|
208
|
+
test_name, test_statuses = trail_entry
|
|
209
|
+
|
|
210
|
+
formatted_trail[test_name] = test_statuses.each_with_object({}) do |test_status, formatted_statuses|
|
|
211
|
+
updated_by_first_name, updated_by_last_name = test_status['updated_by_name'].split
|
|
212
|
+
|
|
213
|
+
formatted_statuses[format_date(test_status['date'])] = {
|
|
214
|
+
status: test_status['status_name'],
|
|
215
|
+
updated_by: {
|
|
216
|
+
first_name: updated_by_first_name,
|
|
217
|
+
last_name: updated_by_last_name,
|
|
218
|
+
phone_number: test_status['updated_by_phone_number'],
|
|
219
|
+
id: test_status['updated_by_id']
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
end,
|
|
224
|
+
test_results: test_results.each_with_object({}) do |results_entry, formatted_results|
|
|
225
|
+
test_name, results = results_entry
|
|
226
|
+
|
|
227
|
+
formatted_results[test_name] = format_test_result_for_dto(test_name, specimen, results, test_status_trail)
|
|
228
|
+
end
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def format_test_result_for_dto(test_name, specimen, results, test_status_trail)
|
|
233
|
+
return {} if results.size.zero?
|
|
234
|
+
|
|
235
|
+
result_create_event = test_status_trail[test_name]&.find do |trail_entry|
|
|
236
|
+
trail_entry['status_name'].casecmp?('drawn')
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
result_creator_first_name, result_creator_last_name = result_create_event&.fetch('updated_by_name')&.split
|
|
240
|
+
unless result_creator_first_name
|
|
241
|
+
result_creator_first_name, result_creator_last_name = specimen['drawn_by_name']&.split
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
results: results.each_with_object({}) do |result, formatted_measures|
|
|
246
|
+
formatted_measures[result['measure_name']] = {
|
|
247
|
+
result_value: result['result']
|
|
248
|
+
}
|
|
249
|
+
end,
|
|
250
|
+
date_result_entered: format_date(result_create_event&.fetch('date') || specimen['order_date'], :iso),
|
|
251
|
+
result_entered_by: {
|
|
252
|
+
first_name: result_creator_first_name,
|
|
253
|
+
last_name: result_creator_last_name,
|
|
254
|
+
phone_number: result_create_event&.fetch('updated_by_phone_number') || specimen['drawn_by_phone_number'],
|
|
255
|
+
id: result_create_event&.fetch('updated_by_id') || specimen['updated_by_id']
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def mysql
|
|
261
|
+
return mysql_connection if mysql_connection
|
|
262
|
+
|
|
263
|
+
config = lambda do |key|
|
|
264
|
+
@config ||= Lab::Lims::Config.database
|
|
265
|
+
@config['default'][key] || @config['development'][key]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
connection = Mysql2::Client.new(host: config['host'] || 'localhost',
|
|
269
|
+
username: config['username'] || 'root',
|
|
270
|
+
password: config['password'],
|
|
271
|
+
port: config['port'] || '3306',
|
|
272
|
+
database: config['database'],
|
|
273
|
+
reconnect: true)
|
|
274
|
+
|
|
275
|
+
self.mysql_connection = connection
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def pid
|
|
279
|
+
return -1 if Parallel.worker_number.nil?
|
|
280
|
+
|
|
281
|
+
Parallel.worker_number
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def mysql_connection=(connection)
|
|
285
|
+
@mysql_connection_pool[pid] = connection
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def mysql_connection
|
|
289
|
+
@mysql_connection_pool[pid]
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def query(sql)
|
|
293
|
+
Rails.logger.debug("#{MysqlApi}: #{sql}")
|
|
294
|
+
mysql.query(sql)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def sql_escape(value)
|
|
298
|
+
mysql.escape(value.to_s)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
##
|
|
302
|
+
# Lims has some weird date formatting standards...
|
|
303
|
+
def format_date(date, format = nil)
|
|
304
|
+
date = date&.to_time
|
|
305
|
+
|
|
306
|
+
case format
|
|
307
|
+
when :iso
|
|
308
|
+
date&.strftime('%Y-%m-%d %H:%M:%S')
|
|
309
|
+
else
|
|
310
|
+
date&.strftime('%Y%m%d%H%M%S')
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|