his_emr_api_lab 0.0.14 → 0.0.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb42401918667c79dc2f78a1ce8f32cd611abd12650686142546e28396eb7cf4
4
- data.tar.gz: 76fd320373a81bf3b10b42aeca93a381e63d112f66ee977618778b9f56c1b8f7
3
+ metadata.gz: 2a7b072fd83de5ea6c03054521d68b82ac2c01237097a8ea9748113b5446a285
4
+ data.tar.gz: 5a67e4211a1d0b61d6f8a51357ee2a9fb898bd1c7973d2fc224039451f0a00d1
5
5
  SHA512:
6
- metadata.gz: dfcb51b5aeb60f0910ab62fdbca15e7cf13bcf450c625755eb29a15556122d5de40e92d4ffd7d7a1141b2bebd1d285bc567fa0ed87280c5bc501a0d52ee6cdb1
7
- data.tar.gz: 9cb56acdbcc7559dd0106b8af3eab31141cbe1e9ca13d36094a3cb25dcc0d632a1fb910db9b6312000ca83563e2c118acf819816fecb29288e3390d74cecbdb1
6
+ metadata.gz: ea314a6c8598617ae9f8ef0cb272d3258ecc50d60f2226103625edd2a11ae7a49bbb08915816ffe3650c3bd78339db7373ee96b63e0a7072c3a703c2e62ecca7
7
+ data.tar.gz: ba1049dbb93d3d842cb31eae9a88e939e0e687e0a2d866432c07713bc670d75c92760dbdd2a22c89f733f00ccc2382b757cd0a4f32563a590fedfe253c6af5a2
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'couch_bum/couch_bum'
4
+
5
+ require_relative '../config'
6
+
7
+ module Lab
8
+ module Lims
9
+ module Api
10
+ ##
11
+ # Talk to LIMS like a boss
12
+ class CouchDbApi
13
+ attr_reader :bum
14
+
15
+ def initialize(config: nil)
16
+ config ||= Config.couchdb
17
+
18
+ @bum = CouchBum.new(protocol: config['protocol'],
19
+ host: config['host'],
20
+ port: config['port'],
21
+ database: "#{config['prefix']}_order_#{config['suffix']}",
22
+ username: config['username'],
23
+ password: config['password'])
24
+ end
25
+
26
+ ##
27
+ # Consume orders from the LIMS queue.
28
+ #
29
+ # Retrieves orders from the LIMS queue and passes each order to
30
+ # given block until the queue is empty or connection is terminated
31
+ # by calling method +choke+.
32
+ def consume_orders(from: 0, limit: 30)
33
+ bum.binge_changes(since: from, limit: limit, include_docs: true) do |change|
34
+ next unless change['doc']['type']&.casecmp?('Order')
35
+
36
+ yield OrderDTO.new(change['doc']), self
37
+ end
38
+ end
39
+
40
+ def create_order(order)
41
+ bum.couch_rest :post, '/', order
42
+ end
43
+
44
+ def update_order(id, order)
45
+ bum.couch_rest :put, "/#{id}", order
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -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
@@ -29,6 +29,12 @@ module Lab
29
29
  YAML.load_file(find_config_path('application.yml'))
30
30
  end
31
31
 
32
+ ##
33
+ # Returns LIMS' database.yml configuration file
34
+ def database
35
+ YAML.load_file(find_config_path('database.yml'))
36
+ end
37
+
32
38
  private
33
39
 
34
40
  ##
@@ -29,6 +29,7 @@ require 'lab/lab_test'
29
29
  require 'lab/lims_order_mapping'
30
30
  require 'lab/lims_failed_import'
31
31
 
32
+ require_relative './config'
32
33
  require_relative './worker'
33
34
  require_relative '../orders_service'
34
35
  require_relative '../results_service'
@@ -43,53 +44,73 @@ require_relative 'utils'
43
44
  module Lab
44
45
  module Lims
45
46
  module Migrator
46
- class MigratorApi < Api
47
- MAX_THREADS = (ENV.fetch('MIGRATION_WORKERS') { 6 }).to_i
47
+ MAX_THREADS = ENV.fetch('MIGRATION_WORKERS', 6).to_i
48
48
 
49
- attr_reader :rejections
49
+ class CouchDbMigratorApi < Api::CouchDbApi
50
+ def initialize(*args, processes: 1, on_merge_processes: nil, **kwargs)
51
+ super(*args, **kwargs)
52
+
53
+ @processes = processes
54
+ @on_merge_processes = on_merge_processes
55
+ end
50
56
 
51
57
  def consume_orders(from: nil, **_kwargs)
52
- limit = 50_000
58
+ limit = 25_000
53
59
 
54
- Parallel.each(read_orders(from, limit),
55
- in_processes: MAX_THREADS,
56
- finish: order_pmap_post_processor(from)) do |row|
57
- next unless row['doc']['type']&.casecmp?('Order')
60
+ loop do
61
+ on_merge_processes = ->(_item, index, _result) { @on_merge_processes&.call(from + index) }
62
+ processes = @processes > 1 ? @processes : 0
58
63
 
59
- User.current = Utils.lab_user
60
- yield OrderDTO.new(row['doc']), OpenStruct.new(last_seq: (from || 0) + limit, current_seq: from)
61
- end
62
- end
64
+ orders = read_orders(from, limit)
65
+ break if orders.empty?
63
66
 
64
- def last_seq
65
- return 0 unless File.exist?(last_seq_path)
67
+ Parallel.each(orders, in_processes: processes, finish: on_merge_processes) do |row|
68
+ next unless row['doc']['type']&.casecmp?('Order')
66
69
 
67
- File.open(last_seq_path, File::RDONLY) do |file|
68
- last_seq = file.read&.strip
69
- return last_seq.blank? ? nil : last_seq&.to_i
70
+ User.current = Utils.lab_user
71
+ yield OrderDTO.new(row['doc']), OpenStruct.new(last_seq: (from || 0) + limit, current_seq: from)
72
+ end
73
+
74
+ from += orders.size
70
75
  end
71
76
  end
72
77
 
73
78
  private
74
79
 
75
- def last_seq_path
76
- LIMS_LOG_PATH.join('migration-last-id.dat')
80
+ def read_orders(from, batch_size)
81
+ start_key_param = from ? "&skip=#{from}" : ''
82
+ url = "_all_docs?include_docs=true&limit=#{batch_size}#{start_key_param}"
83
+
84
+ Rails.logger.debug("#{CouchDbMigratorApi}: Pulling orders from LIMS CouchDB: #{url}")
85
+ response = bum.couch_rest :get, url
86
+
87
+ response['rows']
88
+ end
89
+ end
90
+
91
+ class MigrationWorker < Worker
92
+ LOG_FILE_PATH = LIMS_LOG_PATH.join('migration-last-id.dat')
93
+
94
+ attr_reader :rejections
95
+
96
+ def initialize(api_class)
97
+ api = api_class.new(processes: MAX_THREADS, on_merge_processes: method(:save_seq))
98
+ super(api)
77
99
  end
78
100
 
79
- def order_pmap_post_processor(last_seq)
80
- lambda do |item, index, result|
81
- save_last_seq(last_seq + index)
82
- status, reason = result
83
- next unless status == :rejected
101
+ def last_seq
102
+ return 0 unless File.exist?(LOG_FILE_PATH)
84
103
 
85
- (@rejections ||= []) << OpenStruct.new(order: OrderDTO.new(item['doc']), reason: reason)
104
+ File.open(LOG_FILE_PATH, File::RDONLY) do |file|
105
+ last_seq = file.read&.strip
106
+ return last_seq.blank? ? nil : last_seq&.to_i
86
107
  end
87
108
  end
88
109
 
89
- def save_last_seq(last_seq)
90
- return unless last_seq
110
+ private
91
111
 
92
- File.open(last_seq_path, File::WRONLY | File::CREAT, 0o644) do |file|
112
+ def save_seq(last_seq)
113
+ File.open(LOG_FILE_PATH, File::WRONLY | File::CREAT, 0o644) do |file|
93
114
  Rails.logger.debug("Process ##{Parallel.worker_number}: Saving last seq: #{last_seq}")
94
115
  file.flock(File::LOCK_EX)
95
116
  file.write(last_seq.to_s)
@@ -97,39 +118,11 @@ module Lab
97
118
  end
98
119
  end
99
120
 
100
- def save_rejection(order_dto, reason); end
101
-
102
- def read_orders(from, batch_size)
103
- Enumerator.new do |enum|
104
- loop do
105
- start_key_param = from ? "&skip=#{from}" : ''
106
- url = "_all_docs?include_docs=true&limit=#{batch_size}#{start_key_param}"
107
-
108
- Rails.logger.debug("#{MigratorApi}: Pulling orders from LIMS CouchDB: #{url}")
109
- response = bum.couch_rest :get, url
110
-
111
- from ||= 0
112
-
113
- break from if response['rows'].empty?
114
-
115
- response['rows'].each do |row|
116
- enum.yield(row)
117
- end
118
-
119
- from += response['rows'].size
120
- end
121
- end
122
- end
123
- end
124
-
125
- class MigrationWorker < Worker
126
- protected
121
+ def order_rejected(order_dto, reason)
122
+ @rejections ||= []
127
123
 
128
- def last_seq
129
- lims_api.last_seq
124
+ @rejections << OpenStruct.new(order: order_dto, reason: reason)
130
125
  end
131
-
132
- def update_last_seq(_last_seq); end
133
126
  end
134
127
 
135
128
  def self.save_csv(filename, rows:, headers: nil)
@@ -177,8 +170,7 @@ module Lab
177
170
  MIGRATION_LOG_PATH = LIMS_LOG_PATH.join('migration.log')
178
171
 
179
172
  def self.start_migration
180
- log_dir = Rails.root.join('log/lims')
181
- Dir.mkdir(log_dir) unless File.exist?(log_dir)
173
+ Dir.mkdir(LIMS_LOG_PATH) unless File.exist?(LIMS_LOG_PATH)
182
174
 
183
175
  logger = LoggerMultiplexor.new(Logger.new($stdout), MIGRATION_LOG_PATH)
184
176
  logger.level = :debug
@@ -186,12 +178,17 @@ module Lab
186
178
  ActiveRecord::Base.logger = logger
187
179
  # CouchBum.logger = logger
188
180
 
189
- api = MigratorApi.new
190
- worker = MigrationWorker.new(api)
181
+ api_class = case ENV.fetch('MIGRATION_SOURCE', 'couchdb').downcase
182
+ when 'couchdb' then CouchDbMigratorApi
183
+ when 'mysql' then Api::MysqlApi
184
+ else raise "Invalid MIGRATION_SOURCE: #{ENV['MIGRATION_SOURCE']}"
185
+ end
186
+
187
+ worker = MigrationWorker.new(api_class)
191
188
 
192
- worker.pull_orders
189
+ worker.pull_orders(batch_size: 10_000)
193
190
  ensure
194
- api && export_rejections(api.rejections)
191
+ worker && export_rejections(worker.rejections)
195
192
  export_failures
196
193
  end
197
194
  end
@@ -69,9 +69,7 @@ module Lab
69
69
  end
70
70
 
71
71
  def start_date
72
- if self['date_created'].blank?
73
- raise LimsException, 'Order missing created date'
74
- end
72
+ raise LimsException, 'Order missing created date' if self['date_created'].blank?
75
73
 
76
74
  Utils.parse_date(self['date_created'])
77
75
  end
@@ -52,21 +52,22 @@ module Lab
52
52
  end
53
53
 
54
54
  def self.parse_date(str_date, fallback_date = nil)
55
- if str_date.blank? && fallback_date.blank?
56
- raise "Can't parse blank date"
57
- end
55
+ str_date = str_date&.to_s
56
+
57
+ raise "Can't parse blank date" if str_date.blank? && fallback_date.blank?
58
58
 
59
59
  return parse_date(fallback_date) if str_date.blank?
60
60
 
61
61
  str_date = str_date.gsub(/^00/, '20').gsub(/^180/, '20')
62
62
 
63
- if str_date.match?(/\d{4}-\d{2}-\d{2}/)
63
+ case str_date
64
+ when /\d{4}-\d{2}-\d{2}/
64
65
  str_date
65
- elsif str_date.match?(/\d{2}-\d{2}-\d{2}/)
66
+ when /\d{2}-\d{2}-\d{2}/
66
67
  Date.strptime(str_date, '%d-%m-%Y').strftime('%Y-%m-%d')
67
- elsif str_date.match?(/(\d{4}\d{2}\d{2})\d+/)
68
+ when /(\d{4}\d{2}\d{2})\d+/
68
69
  Date.strptime(str_date, '%Y%m%d').strftime('%Y-%m-%d')
69
- elsif str_date.match?(%r{\d{2}/\d{2}/\d{4}})
70
+ when %r{\d{2}/\d{2}/\d{4}}
70
71
  str_date.to_date.to_s
71
72
  else
72
73
  Rails.logger.warn("Invalid date: #{str_date}")
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'cgi/util'
4
4
 
5
- require_relative './api'
5
+ require_relative './api/couchdb_api'
6
6
  require_relative './exceptions'
7
7
  require_relative './order_serializer'
8
8
  require_relative './utils'
@@ -26,7 +26,7 @@ module Lab
26
26
  User.current = Utils.lab_user
27
27
 
28
28
  fout.write("Worker ##{Process.pid} started at #{Time.now}")
29
- worker = new(Api.new)
29
+ worker = new(CouchDbApi.new)
30
30
  worker.pull_orders
31
31
  # TODO: Verify that names being pushed to LIMS are of the correct format (ie matching
32
32
  # LIMS naming conventions). Enable pushing when that is done
@@ -81,28 +81,34 @@ module Lab
81
81
 
82
82
  ##
83
83
  # Pulls orders from the LIMS queue and writes them to the local database
84
- def pull_orders
84
+ def pull_orders(batch_size: 10_000)
85
85
  logger.info("Retrieving LIMS orders starting from #{last_seq}")
86
86
 
87
- lims_api.consume_orders(from: last_seq, limit: 100) do |order_dto, context|
87
+ lims_api.consume_orders(from: last_seq, limit: batch_size) do |order_dto, context|
88
88
  logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
89
89
 
90
90
  patient = find_patient_by_nhid(order_dto[:patient][:id])
91
91
  unless patient
92
92
  logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
93
- next [:rejected, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs"]
93
+ order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
94
+ next
95
+ end
96
+
97
+ if order_dto[:tests].empty?
98
+ logger.debug("Discarding order: Missing tests on order ##{order_dto[:tracking_number]}")
99
+ order_rejected(order_dto, 'Order is missing tests')
100
+ next
94
101
  end
95
102
 
96
103
  diff = match_patient_demographics(patient, order_dto['patient'])
97
104
  if diff.empty?
98
105
  save_order(patient, order_dto)
106
+ order_saved(order_dto)
99
107
  else
100
108
  save_failed_import(order_dto, 'Demographics not matching', diff)
101
109
  end
102
110
 
103
111
  update_last_seq(context.current_seq)
104
-
105
- [:accepted, "Patient NPID, '#{order_dto[:patient][:id]}', matched"]
106
112
  rescue DuplicateNHID
107
113
  logger.warn("Failed to import order due to duplicate patient NHID: #{order_dto[:patient][:id]}")
108
114
  save_failed_import(order_dto, "Duplicate local patient NHID: #{order_dto[:patient][:id]}")
@@ -134,6 +140,10 @@ module Lab
134
140
  end
135
141
  end
136
142
 
143
+ def order_saved(order_dto); end
144
+
145
+ def order_rejected(order_dto, message); end
146
+
137
147
  private
138
148
 
139
149
  def find_patient_by_nhid(nhid)
@@ -156,9 +166,7 @@ module Lab
156
166
  .distinct(:patient_id)
157
167
  .all
158
168
 
159
- if patients.size > 1
160
- raise DuplicateNHID, "Duplicate National Health ID: #{nhid}"
161
- end
169
+ raise DuplicateNHID, "Duplicate National Health ID: #{nhid}" if patients.size > 1
162
170
 
163
171
  patients.first
164
172
  end
@@ -209,10 +217,11 @@ module Lab
209
217
  mapping.update(pulled_at: Time.now)
210
218
  else
211
219
  order = create_order(patient, order_dto)
212
- LimsOrderMapping.create!(lims_id: order_dto[:_id],
213
- order_id: order['id'],
214
- pulled_at: Time.now,
215
- revision: order_dto['_rev'])
220
+ mapping = LimsOrderMapping.create(lims_id: order_dto[:_id],
221
+ order_id: order['id'],
222
+ pulled_at: Time.now,
223
+ revision: order_dto['_rev'])
224
+ byebug unless mapping.errors.empty?
216
225
  end
217
226
 
218
227
  order
@@ -222,9 +231,7 @@ module Lab
222
231
  def create_order(patient, order_dto)
223
232
  logger.debug("Creating order ##{order_dto['_id']}")
224
233
  order = OrdersService.order_test(order_dto.to_order_service_params(patient_id: patient.patient_id))
225
- unless order_dto['test_results'].empty?
226
- update_results(order, order_dto['test_results'])
227
- end
234
+ update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
228
235
 
229
236
  order
230
237
  end
@@ -233,9 +240,7 @@ module Lab
233
240
  logger.debug("Updating order ##{order_dto['_id']}")
234
241
  order = OrdersService.update_order(order_id, order_dto.to_order_service_params(patient_id: patient.patient_id)
235
242
  .merge(force_update: true))
236
- unless order_dto['test_results'].empty?
237
- update_results(order, order_dto['test_results'])
238
- end
243
+ update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
239
244
 
240
245
  order
241
246
  end
@@ -250,6 +255,8 @@ module Lab
250
255
  next
251
256
  end
252
257
 
258
+ next unless test_results['results']
259
+
253
260
  measures = test_results['results'].map do |indicator, value|
254
261
  measure = find_measure(order, indicator, value)
255
262
  next nil unless measure
@@ -258,7 +265,6 @@ module Lab
258
265
  end
259
266
 
260
267
  measures = measures.compact
261
-
262
268
  next if measures.empty?
263
269
 
264
270
  creator = format_result_entered_by(test_results['result_entered_by'])
@@ -87,9 +87,7 @@ module Lab
87
87
  end
88
88
 
89
89
  def validate_measure_params(params)
90
- if params[:value].blank?
91
- raise InvalidParameterError, 'measures.value is required'
92
- end
90
+ raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
93
91
 
94
92
  if params[:indicator]&.[](:concept_id).blank?
95
93
  raise InvalidParameterError, 'measures.indicator.concept_id is required'
data/lib/lab/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lab
4
- VERSION = '0.0.14'
4
+ VERSION = '0.0.15'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: his_emr_api_lab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elizabeth Glaser Pediatric Foundation Malawi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-10 00:00:00.000000000 Z
11
+ date: 2021-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: couchrest
@@ -249,7 +249,8 @@ files:
249
249
  - app/services/lab/accession_number_service.rb
250
250
  - app/services/lab/concepts_service.rb
251
251
  - app/services/lab/labelling_service/order_label.rb
252
- - app/services/lab/lims/api.rb
252
+ - app/services/lab/lims/api/couchdb_api.rb
253
+ - app/services/lab/lims/api/mysql_api.rb
253
254
  - app/services/lab/lims/config.rb
254
255
  - app/services/lab/lims/exceptions.rb
255
256
  - app/services/lab/lims/failed_imports.rb
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'couch_bum/couch_bum'
4
-
5
- require_relative './config'
6
-
7
- module Lab
8
- module Lims
9
- ##
10
- # Talk to LIMS like a boss
11
- class Api
12
- attr_reader :bum
13
-
14
- def initialize(config: nil)
15
- config ||= Config.couchdb
16
-
17
- @bum = CouchBum.new(protocol: config['protocol'],
18
- host: config['host'],
19
- port: config['port'],
20
- database: "#{config['prefix']}_order_#{config['suffix']}",
21
- username: config['username'],
22
- password: config['password'])
23
- end
24
-
25
- ##
26
- # Consume orders from the LIMS queue.
27
- #
28
- # Retrieves orders from the LIMS queue and passes each order to
29
- # given block until the queue is empty or connection is terminated
30
- # by calling method +choke+.
31
- def consume_orders(from: 0, limit: 30)
32
- bum.binge_changes(since: from, limit: limit, include_docs: true) do |change|
33
- next unless change['doc']['type']&.casecmp?('Order')
34
-
35
- yield OrderDTO.new(change['doc']), self
36
- end
37
- end
38
-
39
- def create_order(order)
40
- bum.couch_rest :post, '/', order
41
- end
42
-
43
- def update_order(id, order)
44
- bum.couch_rest :put, "/#{id}", order
45
- end
46
- end
47
- end
48
- end