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,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
|