his_emr_api_lab 1.1.22 → 1.1.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +71 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/lab/application_controller.rb +6 -0
  6. data/app/controllers/lab/labels_controller.rb +17 -0
  7. data/app/controllers/lab/orders_controller.rb +38 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +19 -0
  10. data/app/controllers/lab/specimen_types_controller.rb +15 -0
  11. data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
  12. data/app/controllers/lab/test_types_controller.rb +15 -0
  13. data/app/controllers/lab/tests_controller.rb +26 -0
  14. data/app/jobs/lab/application_job.rb +4 -0
  15. data/app/jobs/lab/push_order_job.rb +12 -0
  16. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  17. data/app/jobs/lab/void_order_job.rb +17 -0
  18. data/app/mailers/lab/application_mailer.rb +6 -0
  19. data/app/models/lab/application_record.rb +5 -0
  20. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  21. data/app/models/lab/lab_encounter.rb +7 -0
  22. data/app/models/lab/lab_order.rb +58 -0
  23. data/app/models/lab/lab_result.rb +31 -0
  24. data/app/models/lab/lab_test.rb +19 -0
  25. data/app/models/lab/lims_failed_import.rb +4 -0
  26. data/app/models/lab/lims_order_mapping.rb +10 -0
  27. data/app/serializers/lab/lab_order_serializer.rb +55 -0
  28. data/app/serializers/lab/result_serializer.rb +36 -0
  29. data/app/serializers/lab/test_serializer.rb +29 -0
  30. data/app/services/lab/accession_number_service.rb +77 -0
  31. data/app/services/lab/concepts_service.rb +82 -0
  32. data/app/services/lab/labelling_service/order_label.rb +106 -0
  33. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  34. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  35. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  36. data/app/services/lab/lims/api/rest_api.rb +416 -0
  37. data/app/services/lab/lims/api/ws_api.rb +121 -0
  38. data/app/services/lab/lims/api_factory.rb +19 -0
  39. data/app/services/lab/lims/config.rb +100 -0
  40. data/app/services/lab/lims/exceptions.rb +11 -0
  41. data/app/services/lab/lims/migrator.rb +216 -0
  42. data/app/services/lab/lims/order_dto.rb +105 -0
  43. data/app/services/lab/lims/order_serializer.rb +244 -0
  44. data/app/services/lab/lims/pull_worker.rb +289 -0
  45. data/app/services/lab/lims/push_worker.rb +149 -0
  46. data/app/services/lab/lims/utils.rb +91 -0
  47. data/app/services/lab/lims/worker.rb +86 -0
  48. data/app/services/lab/metadata.rb +24 -0
  49. data/app/services/lab/orders_search_service.rb +66 -0
  50. data/app/services/lab/orders_service.rb +212 -0
  51. data/app/services/lab/results_service.rb +149 -0
  52. data/app/services/lab/tests_service.rb +93 -0
  53. data/config/routes.rb +17 -0
  54. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  55. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  56. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  57. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  58. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  59. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  60. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  61. data/lib/auto12epl.rb +201 -0
  62. data/lib/couch_bum/couch_bum.rb +92 -0
  63. data/lib/generators/lab/install/USAGE +9 -0
  64. data/lib/generators/lab/install/install_generator.rb +19 -0
  65. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  66. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  67. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  68. data/lib/his_emr_api_lab.rb +5 -0
  69. data/lib/lab/engine.rb +15 -0
  70. data/lib/lab/version.rb +5 -0
  71. data/lib/logger_multiplexor.rb +38 -0
  72. data/lib/tasks/lab_tasks.rake +25 -0
  73. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  74. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  75. data/lib/tasks/loaders/data/tests.csv +161 -0
  76. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  77. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  78. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  79. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  80. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  81. metadata +81 -2
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ ##
6
+ # Pulls orders from a Lims API object and saves them to the local database.
7
+ class PullWorker
8
+ attr_reader :lims_api
9
+
10
+ include Utils # for logger
11
+
12
+ LIMS_LOG_PATH = Rails.root.join('log', 'lims')
13
+
14
+ def initialize(lims_api)
15
+ @lims_api = lims_api
16
+ end
17
+
18
+ ##
19
+ # Pulls orders from the LIMS queue and writes them to the local database
20
+ def pull_orders(batch_size: 10_000, **kwargs)
21
+ logger.info("Retrieving LIMS orders starting from #{last_seq}")
22
+
23
+ lims_api.consume_orders(from: last_seq, limit: batch_size, **kwargs) do |order_dto, context|
24
+ logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
25
+
26
+ patient = find_patient_by_nhid(order_dto[:patient][:id])
27
+ unless patient
28
+ logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
29
+ order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
30
+ next
31
+ end
32
+
33
+ if order_dto[:tests].empty?
34
+ logger.debug("Discarding order: Missing tests on order ##{order_dto[:tracking_number]}")
35
+ order_rejected(order_dto, 'Order is missing tests')
36
+ next
37
+ end
38
+
39
+ diff = match_patient_demographics(patient, order_dto['patient'])
40
+ if diff.empty?
41
+ save_order(patient, order_dto)
42
+ order_saved(order_dto)
43
+ else
44
+ save_failed_import(order_dto, 'Demographics not matching', diff)
45
+ end
46
+
47
+ update_last_seq(context.current_seq)
48
+ rescue Lab::Lims::DuplicateNHID
49
+ logger.warn("Failed to import order due to duplicate patient NHID: #{order_dto[:patient][:id]}")
50
+ save_failed_import(order_dto, "Duplicate local patient NHID: #{order_dto[:patient][:id]}")
51
+ rescue MissingAccessionNumber
52
+ logger.warn("Failed to import order due to missing accession number: #{order_dto[:_id]}")
53
+ save_failed_import(order_dto, 'Order missing tracking number')
54
+ rescue LimsException => e
55
+ logger.warn("Failed to import order due to #{e.class} - #{e.message}")
56
+ save_failed_import(order_dto, e.message)
57
+ end
58
+ end
59
+
60
+ protected
61
+
62
+ def order_saved(order_dto); end
63
+
64
+ def order_rejected(order_dto, message); end
65
+
66
+ def last_seq
67
+ File.open(last_seq_path, File::RDONLY | File::CREAT, 0o644) do |fin|
68
+ data = fin.read&.strip
69
+ return nil if data.blank?
70
+
71
+ return data
72
+ end
73
+ end
74
+
75
+ def update_last_seq(last_seq)
76
+ File.open(last_seq_path, File::WRONLY | File::CREAT, 0o644) do |fout|
77
+ fout.flock(File::LOCK_EX)
78
+
79
+ fout.write(last_seq.to_s)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def find_patient_by_nhid(nhid)
86
+ national_id_type = PatientIdentifierType.where(name: ['National id', 'Old Identification Number'])
87
+ identifiers = PatientIdentifier.where(type: national_id_type, identifier: nhid)
88
+ .joins('INNER JOIN person ON person.person_id = patient_identifier.patient_id AND person.voided = 0')
89
+ if identifiers.count.zero?
90
+ identifiers = PatientIdentifier.unscoped
91
+ .where(voided: 1, type: national_id_type, identifier: nhid)
92
+ .joins('INNER JOIN person ON person.person_id = patient_identifier.patient_id AND person.voided = 0')
93
+ end
94
+
95
+ # Joining to person above to ensure that the person is not voided,
96
+ # it was noted at one site that there were some people that were voided
97
+ # upon merging but the patient and patient_identifier was not voided
98
+
99
+ return nil if identifiers.count.zero?
100
+
101
+ patients = Patient.where(patient_id: identifiers.select(:patient_id))
102
+ .distinct(:patient_id)
103
+ .all
104
+
105
+ raise Lab::Lims::DuplicateNHID, "Duplicate National Health ID: #{nhid}" if patients.size > 1
106
+
107
+ patients.first
108
+ end
109
+
110
+ ##
111
+ # Matches a local patient's demographics to a LIMS patient's demographics
112
+ def match_patient_demographics(local_patient, lims_patient)
113
+ diff = {}
114
+ person = Person.find(local_patient.id)
115
+ person_name = PersonName.find_by_person_id(local_patient.id)
116
+
117
+ unless (person.gender.blank? && lims_patient['gender'].blank?)\
118
+ || person.gender&.first&.casecmp?(lims_patient['gender']&.first)
119
+ diff[:gender] = { local: person.gender, lims: lims_patient['gender'] }
120
+ end
121
+
122
+ unless names_match?(person_name&.given_name, lims_patient['first_name'])
123
+ diff[:given_name] = { local: person_name&.given_name, lims: lims_patient['first_name'] }
124
+ end
125
+
126
+ unless names_match?(person_name&.family_name, lims_patient['last_name'])
127
+ diff[:family_name] = { local: person_name&.family_name, lims: lims_patient['last_name'] }
128
+ end
129
+
130
+ diff
131
+ end
132
+
133
+ def names_match?(name1, name2)
134
+ name1 = name1&.gsub(/'/, '')&.strip
135
+ name2 = name2&.gsub(/'/, '')&.strip
136
+
137
+ return true if name1.blank? && name2.blank?
138
+
139
+ return false if name1.blank? || name2.blank?
140
+
141
+ name1.casecmp?(name2)
142
+ end
143
+
144
+ def save_order(patient, order_dto)
145
+ raise MissingAccessionNumber if order_dto[:tracking_number].blank?
146
+
147
+ logger.info("Importing LIMS order ##{order_dto[:tracking_number]}")
148
+ mapping = find_order_mapping_by_lims_id(order_dto[:_id])
149
+
150
+ ActiveRecord::Base.transaction do
151
+ if mapping
152
+ order = update_order(patient, mapping.order_id, order_dto)
153
+ mapping.update(pulled_at: Time.now)
154
+ else
155
+ order = create_order(patient, order_dto)
156
+ mapping = LimsOrderMapping.create(lims_id: order_dto[:_id],
157
+ order_id: order['id'],
158
+ pulled_at: Time.now,
159
+ revision: order_dto['_rev'])
160
+ end
161
+
162
+ order
163
+ end
164
+ end
165
+
166
+ def create_order(patient, order_dto)
167
+ logger.debug("Creating order ##{order_dto['_id']}")
168
+ order = OrdersService.order_test(order_dto.to_order_service_params(patient_id: patient.patient_id))
169
+ update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
170
+
171
+ order
172
+ end
173
+
174
+ def update_order(patient, order_id, order_dto)
175
+ logger.debug("Updating order ##{order_dto['_id']}")
176
+ order = OrdersService.update_order(order_id, order_dto.to_order_service_params(patient_id: patient.patient_id)
177
+ .merge(force_update: 'true'))
178
+ update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
179
+
180
+ order
181
+ end
182
+
183
+ def update_results(order, lims_results)
184
+ logger.debug("Updating results for order ##{order[:accession_number]}: #{lims_results}")
185
+
186
+ lims_results.each do |test_name, test_results|
187
+ test = find_test(order['id'], test_name)
188
+ unless test
189
+ logger.warn("Couldn't find test, #{test_name}, in order ##{order[:id]}")
190
+ next
191
+ end
192
+
193
+ next if test.result || test_results['results'].blank?
194
+
195
+ measures = test_results['results'].map do |indicator, value|
196
+ measure = find_measure(order, indicator, value)
197
+ next nil unless measure
198
+
199
+ measure
200
+ end
201
+
202
+ measures = measures.compact
203
+ next if measures.empty?
204
+
205
+ creator = format_result_entered_by(test_results['result_entered_by'])
206
+
207
+ ResultsService.create_results(test.id, { provider_id: User.current.person_id,
208
+ date: Utils.parse_date(test_results['date_result_entered'], order[:order_date].to_s),
209
+ comments: "LIMS import: Entered by: #{creator}",
210
+ measures: measures } )
211
+ end
212
+ end
213
+
214
+ def find_test(order_id, test_name)
215
+ test_name = Utils.translate_test_name(test_name)
216
+ test_concept = Utils.find_concept_by_name(test_name)
217
+ raise "Unknown test name, #{test_name}!" unless test_concept
218
+
219
+ LabTest.find_by(order_id: order_id, value_coded: test_concept.concept_id)
220
+ end
221
+
222
+ def find_measure(_order, indicator_name, value)
223
+ indicator = Utils.find_concept_by_name(indicator_name)
224
+ unless indicator
225
+ logger.warn("Result indicator #{indicator_name} not found in concepts list")
226
+ return nil
227
+ end
228
+
229
+ value_modifier, value, value_type = parse_lims_result_value(value)
230
+ return nil if value.blank?
231
+
232
+ ActiveSupport::HashWithIndifferentAccess.new(
233
+ indicator: { concept_id: indicator.concept_id },
234
+ value_type: value_type,
235
+ value: value_type == 'numeric' ? value.to_f : value,
236
+ value_modifier: value_modifier.blank? ? '=' : value_modifier
237
+ )
238
+ end
239
+
240
+ def parse_lims_result_value(value)
241
+ value = value['result_value']&.strip
242
+ return nil, nil, nil if value.blank?
243
+
244
+ match = value&.match(/^(>|=|<|<=|>=)(.*)$/)
245
+ return nil, value, guess_result_datatype(value) unless match
246
+
247
+ [match[1], match[2].strip, guess_result_datatype(match[2])]
248
+ end
249
+
250
+ def guess_result_datatype(result)
251
+ return 'numeric' if result.strip.match?(/^[+-]?((\d+(\.\d+)?)|\.\d+)$/)
252
+
253
+ 'text'
254
+ end
255
+
256
+ def format_result_entered_by(result_entered_by)
257
+ first_name = result_entered_by['first_name']
258
+ last_name = result_entered_by['last_name']
259
+ phone_number = result_entered_by['phone_number']
260
+ id = result_entered_by['id'] # Looks like a user_id of some sort
261
+
262
+ "#{id}:#{first_name} #{last_name}:#{phone_number}"
263
+ end
264
+
265
+ def save_failed_import(order_dto, reason, diff = nil)
266
+ logger.info("Failed to import LIMS order ##{order_dto[:tracking_number]} due to '#{reason}'")
267
+ LimsFailedImport.create!(lims_id: order_dto[:_id],
268
+ tracking_number: order_dto[:tracking_number],
269
+ patient_nhid: order_dto[:patient][:id],
270
+ reason: reason,
271
+ diff: diff&.to_json)
272
+ end
273
+
274
+ def last_seq_path
275
+ LIMS_LOG_PATH.join('last_seq.dat')
276
+ end
277
+
278
+ def find_order_mapping_by_lims_id(lims_id)
279
+ mapping = Lab::LimsOrderMapping.find_by(lims_id: lims_id)
280
+ return nil unless mapping
281
+
282
+ return mapping if Lab::LabOrder.where(order_id: mapping.order_id).exists?
283
+
284
+ mapping.destroy
285
+ nil
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ ##
6
+ # Pushes all local orders to a LIMS Api object.
7
+ class PushWorker
8
+ attr_reader :lims_api
9
+
10
+ include Utils # for logger
11
+
12
+ SECONDS_TO_WAIT_FOR_ORDERS = 30
13
+
14
+ def initialize(lims_api)
15
+ @lims_api = lims_api
16
+ end
17
+
18
+ def push_orders(batch_size: 1000, wait: false)
19
+ loop do
20
+ logger.info('Looking for new orders to push to LIMS...')
21
+ orders = orders_pending_sync(batch_size).all
22
+
23
+ logger.debug("Found #{orders.size} orders...")
24
+ orders.each do |order|
25
+ push_order(order)
26
+ rescue GatewayError => e
27
+ logger.error("Failed to push order ##{order.accession_number}: #{e.class} - #{e.message}")
28
+ end
29
+
30
+ break unless wait
31
+
32
+ logger.info('Waiting for orders...')
33
+ sleep(Lab::Lims::Config.updates_poll_frequency)
34
+ end
35
+ end
36
+
37
+ def push_order_by_id(order_id)
38
+ order = Lab::LabOrder.joins(order_type: { name: 'Lab' })
39
+ .unscoped
40
+ .find(order_id)
41
+ push_order(order)
42
+ end
43
+
44
+ ##
45
+ # Pushes given order to LIMS queue
46
+ def push_order(order)
47
+ logger.info("Pushing order ##{order.order_id}")
48
+
49
+ order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
50
+ mapping = Lab::LimsOrderMapping.find_by(order_id: order.order_id)
51
+
52
+ ActiveRecord::Base.transaction do
53
+ if mapping && !order.voided.zero?
54
+ Rails.logger.info("Deleting order ##{order_dto[:accession_number]} from LIMS")
55
+ lims_api.delete_order(mapping.lims_id, order_dto)
56
+ mapping.destroy
57
+ elsif mapping
58
+ Rails.logger.info("Updating order ##{order_dto[:accession_number]} in LIMS")
59
+ lims_api.update_order(mapping.lims_id, order_dto)
60
+ if order_dto['test_results'].nil? || order_dto['test_results'].empty?
61
+ mapping.update(pushed_at: Time.now)
62
+ else
63
+ mapping.update(pushed_at: Time.now, result_push_status: true)
64
+ end
65
+ elsif order_dto[:_id] && Lab::LimsOrderMapping.where(lims_id: order_dto[:_id]).exists?
66
+ # HACK: v1.1.7 had a bug where duplicates of recently created orders where being created by
67
+ # the pull worker. This here detects those duplicates and voids them.
68
+ Rails.logger.warn("Duplicate accession number found: #{order_dto[:_id]}, skipping order...")
69
+ fix_duplicates!(order)
70
+ else
71
+ Rails.logger.info("Creating order ##{order_dto[:accession_number]} in LIMS")
72
+ update = lims_api.create_order(order_dto)
73
+ Lab::LimsOrderMapping.create!(order: order, lims_id: update['id'], revision: update['rev'],
74
+ pushed_at: Time.now, result_push_status: false)
75
+ end
76
+ end
77
+
78
+ order_dto
79
+ end
80
+
81
+ private
82
+
83
+ def orders_pending_sync(batch_size)
84
+ return new_orders.limit(batch_size) if new_orders.exists?
85
+
86
+ return voided_orders.limit(batch_size) if voided_orders.exists?
87
+
88
+ updated_orders.limit(batch_size)
89
+ end
90
+
91
+ def new_orders
92
+ Rails.logger.debug('Looking for new orders that need to be created in LIMS...')
93
+ Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
94
+ .order(date_created: :desc)
95
+ end
96
+
97
+ def updated_orders
98
+ Rails.logger.debug('Looking for recently updated orders that need to be pushed to LIMS...')
99
+ last_updated = Lab::LimsOrderMapping.select('MAX(updated_at) AS last_updated')
100
+ .first
101
+ .last_updated
102
+
103
+ Lab::LabOrder.left_joins(:results)
104
+ .joins(:mapping)
105
+ .where('orders.discontinued_date > :last_updated
106
+ OR obs.date_created > orders.date_created AND lab_lims_order_mappings.result_push_status = 0',
107
+ last_updated: last_updated)
108
+ .group('orders.order_id')
109
+ .order(discontinued_date: :desc, date_created: :desc)
110
+ end
111
+
112
+ def voided_orders
113
+ Rails.logger.debug('Looking for voided orders that are being tracked by LIMS...')
114
+ Lab::LabOrder.unscoped
115
+ .where(order_type: OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME),
116
+ order_id: Lab::LimsOrderMapping.all.select(:order_id),
117
+ voided: 1)
118
+ .order(date_voided: :desc)
119
+ end
120
+
121
+ ##
122
+ # HACK: Checks for duplicates previously created by version 1.1.7 pull worker bug due to this proving orders
123
+ # that have not been pushed to LIMS as orders awaiting updates.
124
+ def fix_duplicates!(order)
125
+ return order.void('Duplicate created by bug in HIS-EMR-API-Lab v1.1.7') unless order_has_specimen?(order)
126
+
127
+ duplicate_order = Lab::LabOrder.where(accession_number: order.accession_number)
128
+ .where.not(order_id: order.order_id)
129
+ .first
130
+ return unless duplicate_order
131
+
132
+ if !order_has_results?(order) && (order_has_results?(duplicate_order) || order_has_specimen?(duplicate_order))
133
+ order.void('DUplicate created by bug in HIS-EMR-API-Lab v1.1.7')
134
+ else
135
+ duplicate_order.void('Duplicate created by bug in HIS-EMR-API-Lab v1.1.7')
136
+ Lab::LimsOrderMapping.find_by_lims_id(order.accession_number)&.destroy
137
+ end
138
+ end
139
+
140
+ def order_has_results?(order)
141
+ order.results.exists?
142
+ end
143
+
144
+ def order_has_specimen?(order)
145
+ order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi/util'
4
+
5
+ module Lab
6
+ module Lims
7
+ ##
8
+ # Various helper methods for modules in the Lims namespaces...
9
+ module Utils
10
+ LIMS_LOG_PATH = Rails.root.join('log', 'lims')
11
+ FileUtils.mkdir_p(LIMS_LOG_PATH) unless File.exist?(LIMS_LOG_PATH)
12
+
13
+ def logger
14
+ Rails.logger
15
+ end
16
+
17
+ TEST_NAME_MAPPINGS = {
18
+ # For some weird reason(s) some tests have multiple names in LIMS,
19
+ # this is used to sanitize those names.
20
+ 'hiv_viral_load' => 'HIV Viral Load',
21
+ 'viral laod' => 'HIV Viral Load',
22
+ 'viral load' => 'HIV Viral Load',
23
+ 'i/ink' => 'India ink',
24
+ 'indian ink' => 'India ink'
25
+ }.freeze
26
+
27
+ def self.translate_test_name(test_name)
28
+ TEST_NAME_MAPPINGS.fetch(test_name.downcase, test_name)
29
+ end
30
+
31
+ def self.structify(object)
32
+ if object.is_a?(Hash)
33
+ object.each_with_object(OpenStruct.new) do |kv_pair, struct|
34
+ key, value = kv_pair
35
+
36
+ struct[key] = structify(value)
37
+ end
38
+ elsif object.respond_to?(:map)
39
+ object.map { |item| structify(item) }
40
+ else
41
+ object
42
+ end
43
+ end
44
+
45
+ def self.lab_user
46
+ user = User.find_by_username('lab_daemon')
47
+ return user if user
48
+
49
+ god_user = User.first
50
+
51
+ person = Person.create!(creator: god_user.user_id)
52
+ PersonName.create!(person: person, given_name: 'Lab', family_name: 'Daemon', creator: god_user.user_id)
53
+
54
+ User.create!(username: 'lab_daemon', person: person, creator: god_user.user_id)
55
+ end
56
+
57
+ def self.parse_date(str_date, fallback_date = nil)
58
+ str_date = str_date&.to_s
59
+
60
+ if str_date.blank? && fallback_date.blank?
61
+ raise "Can't parse blank date"
62
+ end
63
+
64
+ return parse_date(fallback_date) if str_date.blank?
65
+
66
+ str_date = str_date.gsub(/^00/, '20').gsub(/^180/, '20')
67
+
68
+ case str_date
69
+ when /\d{4}-\d{2}-\d{2}/
70
+ str_date
71
+ when /\d{2}-\d{2}-\d{2}/
72
+ Date.strptime(str_date, '%d-%m-%Y').strftime('%Y-%m-%d')
73
+ when /(\d{4}\d{2}\d{2})\d+/
74
+ Date.strptime(str_date, '%Y%m%d').strftime('%Y-%m-%d')
75
+ when %r{\d{2}/\d{2}/\d{4}}
76
+ str_date.to_date.to_s
77
+ else
78
+ Rails.logger.warn("Invalid date: #{str_date}")
79
+ parse_date(fallback_date)
80
+ end
81
+ end
82
+
83
+ def self.find_concept_by_name(name)
84
+ ConceptName.joins(:concept)
85
+ .merge(Concept.all) # Filter out voided
86
+ .where(name: CGI.unescapeHTML(name))
87
+ .first
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger_multiplexor'
4
+
5
+ require_relative './api/couchdb_api'
6
+
7
+ module Lab
8
+ module Lims
9
+ ##
10
+ # Pull/Push orders from/to the LIMS queue (Oops meant CouchDB).
11
+ module Worker
12
+ def self.start
13
+ User.current = Utils.lab_user
14
+
15
+ fork(&method(:start_push_worker))
16
+ fork(&method(:start_pull_worker))
17
+ fork(&method(:start_realtime_pull_worker)) if realtime_updates_enabled?
18
+
19
+ Process.waitall
20
+ end
21
+
22
+ def self.start_push_worker
23
+ start_worker('push_worker') do
24
+ worker = PushWorker.new(lims_api)
25
+
26
+ worker.push_orders # (wait: true)
27
+ end
28
+ end
29
+
30
+ def self.start_pull_worker
31
+ start_worker('pull_worker') do
32
+ worker = PullWorker.new(lims_api)
33
+
34
+ worker.pull_orders
35
+ end
36
+ end
37
+
38
+ def self.start_realtime_pull_worker
39
+ start_worker('realtime_pull_worker') do
40
+ worker = PullWorker.new(Lims::Api::WsApi.new(Lab::Lims::Config.updates_socket))
41
+
42
+ worker.pull_orders
43
+ end
44
+ end
45
+
46
+ LOG_FILES_TO_KEEP = 5
47
+ LOG_FILE_SIZE = 500.megabytes
48
+
49
+ def self.start_worker(worker_name)
50
+ Rails.logger = LoggerMultiplexor.new(file_logger(worker_name), $stdout)
51
+ ActiveRecord::Base.logger = Rails.logger
52
+ Rails.logger.level = :debug
53
+
54
+ File.open(log_path("#{worker_name}.lock"), File::RDWR | File::CREAT, 0o644) do |fout|
55
+ unless fout.flock(File::LOCK_EX | File::LOCK_NB)
56
+ Rails.logger.warn("Another process already holds lock #{worker_name} (#{fout.read}), exiting...")
57
+ break
58
+ end
59
+
60
+ fout.write("Locked by process ##{Process.pid} under process group ##{Process.ppid} at #{Time.now}")
61
+ fout.flush
62
+ yield
63
+ end
64
+ end
65
+
66
+ def self.file_logger(worker_name)
67
+ Logger.new(log_path("#{worker_name}.log"), LOG_FILES_TO_KEEP, LOG_FILE_SIZE)
68
+ end
69
+
70
+ def self.log_path(filename)
71
+ Lab::Lims::Utils::LIMS_LOG_PATH.join(filename)
72
+ end
73
+
74
+ def self.realtime_updates_enabled?
75
+ Lims::Config.updates_socket.key?('url')
76
+ rescue Lab::Lims::Config::ConfigNotFound => e
77
+ Rails.logger.warn("Check for realtime updates failed: #{e.message}")
78
+ false
79
+ end
80
+
81
+ def self.lims_api
82
+ Lab::Lims::ApiFactory.create_api
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Metadata
5
+ # Concepts
6
+ REASON_FOR_TEST_CONCEPT_NAME = 'Reason for test'
7
+ REQUESTING_CLINICIAN_CONCEPT_NAME = 'Person making request'
8
+ SPECIMEN_TYPE_CONCEPT_NAME = 'Specimen type'
9
+ TARGET_LAB_CONCEPT_NAME = 'Lab'
10
+ TEST_RESULT_CONCEPT_NAME = 'Lab test result'
11
+ TEST_RESULT_INDICATOR_CONCEPT_NAME = 'Lab test result indicator'
12
+ TEST_TYPE_CONCEPT_NAME = 'Test type'
13
+ UNKNOWN_SPECIMEN = 'Unknown'
14
+
15
+ # Encounter
16
+ ENCOUNTER_TYPE_NAME = 'Lab'
17
+
18
+ # Order types
19
+ ORDER_TYPE_NAME = 'Lab'
20
+
21
+ # Programs
22
+ LAB_PROGRAM_NAME = 'Laboratory Program'
23
+ end
24
+ end