his_emr_api_lab 1.1.8 → 1.1.14

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: e5421bf2b632a395fa9870ba1a3e855aea8033f8c54034bda352fdf23409812e
4
- data.tar.gz: 4cd24426fa0f0a5fe39c416bb0f35f28769c36b95eac72a8abca7579efd75c58
3
+ metadata.gz: f655e84314f6a25a8ecce1668132e93bbd1a175efd35cb832792b94cc9d33e42
4
+ data.tar.gz: 298f226847e19dd868e7caf487b00b49dc306bed4ca2c2a0f2c07765278598e5
5
5
  SHA512:
6
- metadata.gz: 54815cf740ce0c0882752a341df21acbd51bde4102b0dcd2ddd581575efb70b9aa1284295dda7e6cb78b5e6355a3c983d5b41acf5f8f40fe8318737a6cce08a5
7
- data.tar.gz: b516dd7ac285004b6d4e5125b73e159adf180a07b1cf0470ae4380d02ad274b72d2a68ec697c99deb3f4e8a5f057ec9d0682925e1d1a72b6986852285feea996
6
+ metadata.gz: 132e7e46b9c9c6468651b1ebde2ce182852cc17992a54928b49b3605191c53f81ab324e21657d1b421ac317d48c51cd2a73c24d11f94cd5ca4f0d47714c04d9b
7
+ data.tar.gz: c49d8d971697158bf1646027935ed1e95cb825a161fef18a9d658affb7e1493b24b0f70748d8c61d4a9e69d81e00d5d3f3395eec7ea4c4189401b992a27d97f5
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Push an order to LIMS.
6
+ class PushOrderJob < ApplicationJob
7
+ def perform(order_id)
8
+ push_worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
9
+ push_worker.push_order_by_id(order_id)
10
+ end
11
+ end
12
+ end
@@ -20,8 +20,7 @@ module Lab
20
20
  break false
21
21
  end
22
22
 
23
- lims_api = Lab::Lims::Api::RestApi.new(Lab::Lims::Config.rest_api)
24
- worker = Lab::Lims::PullWorker.new(lims_api)
23
+ worker = Lab::Lims::PullWorker.new(Lab::Lims::ApiFactory.create_api)
25
24
  worker.pull_orders(patient_id: patient_id)
26
25
 
27
26
  true
@@ -10,8 +10,7 @@ module Lab
10
10
  User.current = Lab::Lims::Utils.lab_user
11
11
  Location.current = Location.find_by_name('ART clinic')
12
12
 
13
- lims_api = Lab::Lims::Api::RestApi.new
14
- worker = Lab::Lims::Worker.new(lims_api)
13
+ worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
15
14
  worker.push_order(Lab::LabOrder.unscoped.find(order_id))
16
15
  end
17
16
  end
@@ -42,14 +42,21 @@ module Lab
42
42
  end
43
43
 
44
44
  scope :drawn, -> { where.not(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
45
-
46
45
  scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
47
46
 
47
+ after_save :queue_lims_push
48
+
48
49
  def self.prefetch_relationships
49
50
  includes(:reason_for_test,
50
51
  :requesting_clinician,
51
52
  :target_lab,
52
53
  tests: [:result])
53
54
  end
55
+
56
+ private
57
+
58
+ def queue_lims_push
59
+ Lab::PushOrderJob.perform_later(order_id)
60
+ end
54
61
  end
55
62
  end
@@ -6,7 +6,7 @@ module Lab
6
6
  tests ||= order.voided == 1 ? voided_tests(order) : order.tests
7
7
  requesting_clinician ||= order.requesting_clinician
8
8
  reason_for_test ||= order.reason_for_test
9
- target_lab ||= order.target_lab
9
+ target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
10
10
 
11
11
  ActiveSupport::HashWithIndifferentAccess.new(
12
12
  {
@@ -21,7 +21,7 @@ module Lab
21
21
  name: concept_name(order.concept_id)
22
22
  },
23
23
  requesting_clinician: requesting_clinician&.value_text,
24
- target_lab: target_lab&.value_text,
24
+ target_lab: target_lab,
25
25
  reason_for_test: {
26
26
  concept_id: reason_for_test&.value_coded,
27
27
  name: concept_name(reason_for_test&.value_coded)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ module Api
6
+ ##
7
+ # A LIMS Api wrappper that does nothing really.
8
+ #
9
+ # Primarily meant as a dummy for testing environments.
10
+ class BlackholeApi
11
+ def create_order(order_dto); end
12
+
13
+ def update_order(order_dto); end
14
+
15
+ def void_order(order_dto); end
16
+
17
+ def consume_orders(&_block); end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -44,9 +44,6 @@ class Lab::Lims::Api::RestApi
44
44
 
45
45
  def consume_orders(*_args, patient_id: nil, **_kwargs)
46
46
  orders_pending_updates(patient_id).each do |order|
47
- mapping = Lab::LimsOrderMapping.find_by(order_id: order.order_id)
48
- next if mapping.nil? || check_and_fix_duplicate!(mapping, order)
49
-
50
47
  order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
51
48
 
52
49
  if order_dto['priority'].nil? || order_dto['sample_type'].casecmp?('not_specified')
@@ -389,6 +386,7 @@ class Lab::Lims::Api::RestApi
389
386
  unknown_specimen = ConceptName.where(name: Lab::Metadata::UNKNOWN_SPECIMEN)
390
387
  .select(:concept_id)
391
388
  orders = Lab::LabOrder.where(concept_id: unknown_specimen)
389
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
392
390
  orders = orders.where(patient_id: patient_id) if patient_id
393
391
 
394
392
  orders
@@ -397,6 +395,7 @@ class Lab::Lims::Api::RestApi
397
395
  def orders_without_results(patient_id = nil)
398
396
  Rails.logger.debug('Looking for orders without a result')
399
397
  Lab::OrdersSearchService.find_orders_without_results(patient_id: patient_id)
398
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
400
399
  end
401
400
 
402
401
  def orders_without_reason(patient_id = nil)
@@ -404,28 +403,9 @@ class Lab::Lims::Api::RestApi
404
403
  orders = Lab::LabOrder.joins(:reason_for_test)
405
404
  .merge(Observation.where(value_coded: nil, value_text: nil))
406
405
  .limit(1000)
406
+ .where.not(accession_number: Lab::LimsOrderMapping.select(:lims_id))
407
407
  orders = orders.where(patient_id: patient_id) if patient_id
408
408
 
409
409
  orders
410
410
  end
411
-
412
- # Checks for duplicates previously created due to this proving orders that have
413
- # not been pushed to LIMS as orders awaiting updates.
414
- def check_and_fix_duplicate!(mapping, order)
415
- duplicate_orders = Lab::LabOrder.where(accession_number: mapping.lims_id)
416
- .where.not(order_id: mapping.order_id)
417
- return false if duplicate_orders.size.zero?
418
-
419
- unless order.discontinued
420
- order.void('Duplicate created due to bug in HIS-EMR-API-Lab v1.1.7')
421
- mapping.destroy
422
- return true
423
- end
424
-
425
- duplicate_orders.each do |duplicate_order|
426
- duplicate_order.void("Has duplicate that contains updates ##{order.order_id}: Duplicate was created by bug in HIS-EMR-API-Lab v1.1.7")
427
- end
428
-
429
- true
430
- end
431
411
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ ##
6
+ # Creates LIMS Apis based on current configuration
7
+ module ApiFactory
8
+ def self.create_api
9
+ return Lab::Lims::Api::BlackholeApi.new if Rails.env.casecmp?('test')
10
+
11
+ case Lab::Lims::Config.preferred_api
12
+ when /rest/i then Lab::Lims::Api::RestApi.new(Lab::Lims::Config.rest_api)
13
+ when /couchdb/ then Lab::Lims::Api::CouchDbApi.new(config: Lab::Lims::Config.couchdb_api)
14
+ else raise "Invalid lims_api configuration: #{Lab::Lims::Config.preferred_api}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -46,9 +46,20 @@ require_relative 'utils'
46
46
 
47
47
  module Lab
48
48
  module Lims
49
+ ##
50
+ # Tools for performing a bulk import of data from LIMS' databases to local OpenMRS database.
51
+ #
52
+ # Migration sources supported:
53
+ # - MySQL
54
+ # - CouchDB
55
+ #
56
+ # The sources above can be changed by setting the environment various MIGRATION_SOURCE to
57
+ # either mysql or couchdb.
49
58
  module Migrator
50
59
  MAX_THREADS = ENV.fetch('MIGRATION_WORKERS', 6).to_i
51
60
 
61
+ ##
62
+ # A Lab::Lims::Api object that supports crawling of a LIMS CouchDB instance.
52
63
  class CouchDbMigratorApi < Lab::Lims::Api::CouchDbApi
53
64
  def initialize(*args, processes: 1, on_merge_processes: nil, **kwargs)
54
65
  super(*args, **kwargs)
@@ -91,6 +102,12 @@ module Lab
91
102
  end
92
103
  end
93
104
 
105
+ ##
106
+ # Extends the PullWorker to provide pause/resume capabilities.
107
+ #
108
+ # Migrations can be take a long time to complete, in cases where something
109
+ # went wrong you wouldn't to start all over. This worker thus saves
110
+ # progress and allows for the process to continue from whether it stopped.
94
111
  class MigrationWorker < PullWorker
95
112
  LOG_FILE_PATH = Utils::LIMS_LOG_PATH.join('migration-last-id.dat')
96
113
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Lab
4
4
  module Lims
5
+ ##
6
+ # Pulls orders from a Lims API object and saves them to the local database.
5
7
  class PullWorker
6
8
  attr_reader :lims_api
7
9
 
@@ -100,9 +102,7 @@ module Lab
100
102
  .distinct(:patient_id)
101
103
  .all
102
104
 
103
- if patients.size > 1
104
- raise DuplicateNHID, "Duplicate National Health ID: #{nhid}"
105
- end
105
+ raise DuplicateNHID, "Duplicate National Health ID: #{nhid}" if patients.size > 1
106
106
 
107
107
  patients.first
108
108
  end
@@ -166,9 +166,7 @@ module Lab
166
166
  def create_order(patient, order_dto)
167
167
  logger.debug("Creating order ##{order_dto['_id']}")
168
168
  order = OrdersService.order_test(order_dto.to_order_service_params(patient_id: patient.patient_id))
169
- unless order_dto['test_results'].empty?
170
- update_results(order, order_dto['test_results'])
171
- end
169
+ update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
172
170
 
173
171
  order
174
172
  end
@@ -177,9 +175,7 @@ module Lab
177
175
  logger.debug("Updating order ##{order_dto['_id']}")
178
176
  order = OrdersService.update_order(order_id, order_dto.to_order_service_params(patient_id: patient.patient_id)
179
177
  .merge(force_update: 'true'))
180
- unless order_dto['test_results'].empty?
181
- update_results(order, order_dto['test_results'])
182
- end
178
+ update_results(order, order_dto['test_results']) unless order_dto['test_results'].empty?
183
179
 
184
180
  order
185
181
  end
@@ -283,9 +279,7 @@ module Lab
283
279
  mapping = Lab::LimsOrderMapping.find_by(lims_id: lims_id)
284
280
  return nil unless mapping
285
281
 
286
- if Lab::LabOrder.where(order_id: mapping.order_id).exists?
287
- return mapping
288
- end
282
+ return mapping if Lab::LabOrder.where(order_id: mapping.order_id).exists?
289
283
 
290
284
  mapping.destroy
291
285
  nil
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Lab
4
4
  module Lims
5
+ ##
6
+ # Pushes all local orders to a LIMS Api object.
5
7
  class PushWorker
6
8
  attr_reader :lims_api
7
9
 
@@ -17,27 +19,25 @@ module Lab
17
19
  loop do
18
20
  logger.info('Looking for new orders to push to LIMS...')
19
21
  orders = orders_pending_sync(batch_size).all
22
+
23
+ logger.debug("Found #{orders.size} orders...")
20
24
  orders.each do |order|
21
25
  push_order(order)
22
26
  rescue GatewayError => e
23
27
  logger.error("Failed to push order ##{order.accession_number}: #{e.class} - #{e.message}")
24
- sleep(Lab::Lims::Config.updates_poll_frequency)
25
28
  end
26
29
 
27
- # Doing this after .each above to stop ActiveRecord from executing
28
- # an extra request to the database (ActiveRecord's lazy evaluation
29
- # sometimes leads to unnecessary database hits for checking counts).
30
- if orders.empty? && !wait
31
- logger.info('Finished processing orders; exiting...')
32
- break
33
- end
30
+ break unless wait
34
31
 
32
+ logger.info('Waiting for orders...')
35
33
  sleep(Lab::Lims::Config.updates_poll_frequency)
36
34
  end
37
35
  end
38
36
 
39
37
  def push_order_by_id(order_id)
40
- order = Lab::LabOrder.unscoped.find(order_id)
38
+ order = Lab::LabOrder.joins(order_type: { name: 'Lab' })
39
+ .unscoped
40
+ .find(order_id)
41
41
  push_order(order)
42
42
  end
43
43
 
@@ -51,15 +51,20 @@ module Lab
51
51
 
52
52
  ActiveRecord::Base.transaction do
53
53
  if mapping && !order.voided.zero?
54
- Rails.logger.info("Deleting order ##{order_dto['accession_number']} from LIMS")
54
+ Rails.logger.info("Deleting order ##{order_dto[:accession_number]} from LIMS")
55
55
  lims_api.delete_order(mapping.lims_id, order_dto)
56
56
  mapping.destroy
57
57
  elsif mapping
58
- Rails.logger.info("Updating order ##{order_dto['accession_number']} in LIMS")
58
+ Rails.logger.info("Updating order ##{order_dto[:accession_number]} in LIMS")
59
59
  lims_api.update_order(mapping.lims_id, order_dto)
60
60
  mapping.update(pushed_at: Time.now)
61
+ elsif order_dto[:_id] && Lab::LimsOrderMapping.where(lims_id: order_dto[:_id]).exists?
62
+ # HACK: v1.1.7 had a bug where duplicates of recently created orders where being created by
63
+ # the pull worker. This here detects those duplicates and voids them.
64
+ Rails.logger.warn("Duplicate accession number found: #{order_dto[:_id]}, skipping order...")
65
+ fix_duplicates!(order)
61
66
  else
62
- Rails.logger.info("Creating order ##{order_dto['accession_number']} in LIMS")
67
+ Rails.logger.info("Creating order ##{order_dto[:accession_number]} in LIMS")
63
68
  update = lims_api.create_order(order_dto)
64
69
  Lab::LimsOrderMapping.create!(order: order, lims_id: update['id'], revision: update['rev'],
65
70
  pushed_at: Time.now)
@@ -82,6 +87,7 @@ module Lab
82
87
  def new_orders
83
88
  Rails.logger.debug('Looking for new orders that need to be created in LIMS...')
84
89
  Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
90
+ .order(date_created: :desc)
85
91
  end
86
92
 
87
93
  def updated_orders
@@ -95,6 +101,7 @@ module Lab
95
101
  OR obs.date_created > :last_updated',
96
102
  last_updated: last_updated)
97
103
  .group('orders.order_id')
104
+ .order(discontinued_date: :desc, date_created: :desc)
98
105
  end
99
106
 
100
107
  def voided_orders
@@ -103,6 +110,34 @@ module Lab
103
110
  .where(order_type: OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME),
104
111
  order_id: Lab::LimsOrderMapping.all.select(:order_id),
105
112
  voided: 1)
113
+ .order(date_voided: :desc)
114
+ end
115
+
116
+ ##
117
+ # HACK: Checks for duplicates previously created by version 1.1.7 pull worker bug due to this proving orders
118
+ # that have not been pushed to LIMS as orders awaiting updates.
119
+ def fix_duplicates!(order)
120
+ return order.void('Duplicate created by bug in HIS-EMR-API-Lab v1.1.7') unless order_has_specimen?(order)
121
+
122
+ duplicate_order = Lab::LabOrder.where(accession_number: order.accession_number)
123
+ .where.not(order_id: order.order_id)
124
+ .first
125
+ return unless duplicate_order
126
+
127
+ if !order_has_results?(order) && (order_has_results?(duplicate_order) || order_has_specimen?(duplicate_order))
128
+ order.void('DUplicate created by bug in HIS-EMR-API-Lab v1.1.7')
129
+ else
130
+ duplicate_order.void('Duplicate created by bug in HIS-EMR-API-Lab v1.1.7')
131
+ Lab::LimsOrderMapping.find_by_lims_id(order.accession_number)&.destroy
132
+ end
133
+ end
134
+
135
+ def order_has_results?(order)
136
+ order.results.exists?
137
+ end
138
+
139
+ def order_has_specimen?(order)
140
+ order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
106
141
  end
107
142
  end
108
143
  end
@@ -43,9 +43,12 @@ module Lab
43
43
  end
44
44
  end
45
45
 
46
+ LOG_FILES_TO_KEEP = 5
47
+ LOG_FILE_SIZE = 500.megabytes
48
+
46
49
  def self.start_worker(worker_name)
47
- Rails.logger = LoggerMultiplexor.new(log_path("#{worker_name}.log"), $stdout)
48
- # ActiveRecord::Base.logger = Rails.logger
50
+ Rails.logger = LoggerMultiplexor.new(file_logger(worker_name), $stdout)
51
+ ActiveRecord::Base.logger = Rails.logger
49
52
  Rails.logger.level = :debug
50
53
 
51
54
  File.open(log_path("#{worker_name}.lock"), File::RDWR | File::CREAT, 0o644) do |fout|
@@ -60,6 +63,10 @@ module Lab
60
63
  end
61
64
  end
62
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
+
63
70
  def self.log_path(filename)
64
71
  Lab::Lims::Utils::LIMS_LOG_PATH.join(filename)
65
72
  end
@@ -72,11 +79,7 @@ module Lab
72
79
  end
73
80
 
74
81
  def self.lims_api
75
- case Lims::Config.preferred_api
76
- when /couchdb/i then Api::CouchDbApi.new(config: Lab::Lims::Config.couchdb)
77
- when /rest/i then Api::RestApi.new(Lab::Lims::Config.rest_api)
78
- else raise "Invalid LIMS API in application.yml, expected 'rest' or 'couchdb'"
79
- end
82
+ Lab::Lims::ApiFactory.create_api
80
83
  end
81
84
  end
82
85
  end
@@ -169,6 +169,8 @@ module Lab
169
169
  ##
170
170
  # Attach the lab where the test is going to get carried out.
171
171
  def add_target_lab(order, params)
172
+ return nil unless params['target_lab']
173
+
172
174
  create_order_observation(
173
175
  order,
174
176
  Lab::Metadata::TARGET_LAB_CONCEPT_NAME,
data/lib/lab/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lab
4
- VERSION = '1.1.8'
4
+ VERSION = '1.1.14'
5
5
  end
@@ -68,6 +68,7 @@
68
68
  "Hepatitis B Test","Hepatitis B"
69
69
  "Hepatitis C Test","Hepatitis C"
70
70
  "Rheumatoid Factor Test","Rheumatoid Factor"
71
+ "CrAg","CrAg"
71
72
  "Cryptococcus Antigen Test","CrAg"
72
73
  "Anti Streptolysis O","ASO"
73
74
  "C-reactive protein","CRP"
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: 1.1.8
4
+ version: 1.1.14
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-08-20 00:00:00.000000000 Z
11
+ date: 2021-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: couchrest
@@ -248,6 +248,7 @@ files:
248
248
  - app/controllers/lab/test_types_controller.rb
249
249
  - app/controllers/lab/tests_controller.rb
250
250
  - app/jobs/lab/application_job.rb
251
+ - app/jobs/lab/push_order_job.rb
251
252
  - app/jobs/lab/update_patient_orders_job.rb
252
253
  - app/jobs/lab/void_order_job.rb
253
254
  - app/mailers/lab/application_mailer.rb
@@ -265,10 +266,12 @@ files:
265
266
  - app/services/lab/accession_number_service.rb
266
267
  - app/services/lab/concepts_service.rb
267
268
  - app/services/lab/labelling_service/order_label.rb
269
+ - app/services/lab/lims/api/blackhole_api.rb
268
270
  - app/services/lab/lims/api/couchdb_api.rb
269
271
  - app/services/lab/lims/api/mysql_api.rb
270
272
  - app/services/lab/lims/api/rest_api.rb
271
273
  - app/services/lab/lims/api/ws_api.rb
274
+ - app/services/lab/lims/api_factory.rb
272
275
  - app/services/lab/lims/config.rb
273
276
  - app/services/lab/lims/exceptions.rb
274
277
  - app/services/lab/lims/migrator.rb