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