his_emr_api_lab 2.1.9.pre.beta → 2.2.1

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: c6c20f1ba07edd60ba5795eaaef1ca3f34395060b1f9ed0160a5513777d657d0
4
- data.tar.gz: d16b695b3592233474fa9bbc2f90c85db7d04161a02f8107c56f92fd4e82a9fb
3
+ metadata.gz: c59744a50921b0f2244c94e052d4d66cbf39096485f99755e84f9cb66c311a0b
4
+ data.tar.gz: 989cc056aa59acbcd9d24977d3fc34b330cd1793db5d35908ccce44ca08dab34
5
5
  SHA512:
6
- metadata.gz: 035cf4578ee6f60ba3406a66f88ea33cc54ea7d85ab38524b7e6ae54c9b4b80e03fe2938fc34d047e72df9423b71b8eba1f40004a8fd688555b56c3aebb81605
7
- data.tar.gz: feda70dff8797923f46078a42429b274d7562eb5105a5d43c1d746f744436c6c516847aa2ef68d7b235f1d1a216981e10d4c707bc6f4fb45372d6b867c412a2a
6
+ metadata.gz: 22429cb56bfb9c5af91e5b44e2da11b66551ac89d084eeea45f5b16bb06daf5cc4f99d41cb5f2e4f3d5a38443fb62c15e5a30a14c5e5fff85006a3233d8f71de
7
+ data.tar.gz: 622a040596dc790f1d5a3c05469f7afe131badeca6313eb2412f8da378e632b7b28643c6a12eba6f02c618b9bed680722a635ce6e2e57ea6863872eb5e9b0966
data/README.md CHANGED
@@ -22,25 +22,25 @@ For details on how to perform these operations please see the
22
22
  Add this line to your application's Gemfile:
23
23
 
24
24
  ```ruby
25
- gem 'lab', git: 'https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab', branch: 'development'
25
+ gem 'his_emr_api_lab', git: 'https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab', branch: 'development'
26
26
  ```
27
27
 
28
28
  And then execute:
29
29
 
30
30
  ```bash
31
- $ bundle install lab
31
+ $ bundle install
32
32
  ```
33
33
 
34
34
  Or install it yourself as:
35
35
 
36
36
  ```bash
37
- $ gem install lab
37
+ $ gem install his_emr_api_lab
38
38
  ```
39
39
 
40
40
  Finally run:
41
41
 
42
42
  ```bash
43
- $ bundle exec rails lab:install
43
+ $ bundle exec rails his_emr_api_lab:install
44
44
  ```
45
45
 
46
46
  ## Configuration
@@ -66,6 +66,19 @@ but too much a departure from it is frowned upon. For example, you will be forgi
66
66
  for writing a method with 15 to 20 lines if you clearly justify why you couldn't
67
67
  break that method into multiple smaller methods.
68
68
 
69
+ ## Publishing
70
+
71
+ To publish a new version of the gem, first update the version number in
72
+ `lib/lab/version.rb` and then run the following command:
73
+
74
+ ```bash
75
+ $ gem build his_emr_api_lab.gemspec
76
+ $ gem push his_emr_api_lab-<version>.gem
77
+ ```
78
+ Make sure to replace `<version>` with the version number you set in `lib/lab/version.rb`.
79
+
80
+ NB: You need to have an account on [rubygems.org](https://rubygems.org/) and permission to publish gems.
81
+
69
82
  ## License
70
83
 
71
84
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Lab
4
4
  class OrdersController < ApplicationController
5
+ skip_before_action :authenticate, only: %i[order_status order_result summary]
5
6
  before_action :authenticate_request, only: %i[order_status order_result summary]
6
7
 
7
8
  def create
@@ -52,7 +53,8 @@ module Lab
52
53
  end
53
54
 
54
55
  def order_status
55
- order_params = params.permit(:tracking_number, :status, :status_time, :comments)
56
+ order_params = params.permit(:tracking_number, :status, :status_time, :comments, :status_id,
57
+ updated_by: [:first_name, :last_name, :id, :phone_number])
56
58
  OrdersService.update_order_status(order_params)
57
59
  render json: { message: "Status for order #{order_params['tracking_number']} successfully updated" }, status: :ok
58
60
  end
@@ -76,8 +78,26 @@ module Lab
76
78
  private
77
79
 
78
80
  def authenticate_request
79
- decoded_user = authorize_request
80
- user(decoded_user)
81
+ header = request.headers['Authorization']
82
+ content = header.split(' ')
83
+ auth_scheme = content.first
84
+ unless header
85
+ errors = ['Authorization token required']
86
+ render json: { errors: errors }, status: :unauthorized
87
+ return false
88
+ end
89
+ unless auth_scheme == 'Bearer'
90
+ errors = ['Authorization token bearer scheme required']
91
+ render json: { errors: errors }, status: :unauthorized
92
+ return false
93
+ end
94
+ process_token(content.last)
95
+ end
96
+
97
+ def process_token(token)
98
+ browser = Browser.new(request.user_agent)
99
+ decoded = Lab::JsonWebTokenService.decode(token, request.remote_ip + browser.name + browser.version)
100
+ user(decoded)
81
101
  end
82
102
 
83
103
  def user(decoded)
@@ -7,11 +7,8 @@ module Lab
7
7
  queue_as :default
8
8
  def perform(results_obs_id, serializer, result_enter_by)
9
9
  Rails.logger.info("Lab::ProcessLabResultJob: Processing result completion for #{serializer}")
10
- # set location context for the job based on the order's encounter to ensure proper context for any operations performed in the job
11
10
  results_obs = Lab::LabResult.unscoped.find(results_obs_id)
12
- encounter = Encounter.unscoped.find_by(encounter_id: results_obs.encounter_id)
13
- Location.current = Location.find(encounter.location_id) if encounter&.location_id
14
11
  Lab::ResultsService.process_result_completion(results_obs, serializer, result_enter_by)
15
12
  end
16
13
  end
17
- end
14
+ end
@@ -10,8 +10,7 @@ module Lab
10
10
  Rails.logger.info('Initialising LIMS REST API...')
11
11
 
12
12
  User.current = Lab::Lims::Utils.lab_user
13
- # Set location from patient's most recent encounter to ensure proper context
14
- set_location_from_patient_encounter(patient_id)
13
+ Location.current = Location.find_by_name('ART clinic')
15
14
 
16
15
  lockfile = Rails.root.join('tmp', "update-patient-orders-#{patient_id}.lock")
17
16
 
@@ -29,53 +28,5 @@ module Lab
29
28
 
30
29
  File.unlink(lockfile) if done
31
30
  end
32
-
33
- private
34
-
35
- def set_location_from_patient_encounter(patient_id)
36
- Rails.logger.info("Setting location context for patient #{patient_id}")
37
-
38
- # Strategy 1: Find location from patient's most recent order (ANY order type)
39
- recent_order = Order.unscoped
40
- .where(patient_id: patient_id)
41
- .order(start_date: :desc)
42
- .first
43
-
44
- if recent_order
45
- encounter = Encounter.unscoped.find_by(encounter_id: recent_order.encounter_id)
46
- if encounter&.location_id
47
- Location.current = Location.find(encounter.location_id)
48
- Rails.logger.info("Location set from patient's recent order: #{Location.current.name} (ID: #{Location.current.location_id})")
49
- return
50
- end
51
- end
52
-
53
- # Strategy 2: Find location from patient's most recent encounter
54
- recent_encounter = Encounter.unscoped
55
- .where(patient_id: patient_id)
56
- .order(encounter_datetime: :desc)
57
- .first
58
-
59
- if recent_encounter&.location_id
60
- Location.current = Location.find(recent_encounter.location_id)
61
- Rails.logger.info("Location set from patient's recent encounter: #{Location.current.name} (ID: #{Location.current.location_id})")
62
- return
63
- end
64
-
65
- # Fallback chain: Try multiple options to ensure location is ALWAYS set
66
- Location.current ||= begin
67
- Location.current_health_center
68
- rescue StandardError
69
- nil
70
- end
71
- Location.current ||= Location.first
72
-
73
- if Location.current
74
- Rails.logger.info("Location set to fallback: #{Location.current.name} (ID: #{Location.current.location_id})")
75
- else
76
- Rails.logger.error('CRITICAL: Could not set Location.current - no locations found in database!')
77
- raise 'No locations available in database'
78
- end
79
- end
80
31
  end
81
32
  end
@@ -8,14 +8,10 @@ module Lab
8
8
  Rails.logger.info("Voiding order ##{order_id} in LIMS")
9
9
 
10
10
  User.current = Lab::Lims::Utils.lab_user
11
- # Set location from order's encounter to ensure proper context
12
- order = Lab::LabOrder.unscoped.find(order_id)
13
- encounter = Encounter.unscoped.find_by(encounter_id: order.encounter_id)
14
- Location.current = Location.find(encounter.location_id) if encounter&.location_id
15
- Location.current ||= Location.find_by_name('ART clinic')
11
+ Location.current = Location.find_by_name('ART clinic')
16
12
 
17
13
  worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
18
- worker.push_order(order)
14
+ worker.push_order(Lab::LabOrder.unscoped.find(order_id))
19
15
  end
20
16
  end
21
17
  end
@@ -8,6 +8,11 @@ module Lab
8
8
 
9
9
  -> { where(concept:) }
10
10
  end
11
+
12
+ # Cache the concept ID to avoid lookups in association scopes
13
+ def order_status_concept_id
14
+ @order_status_concept_id ||= ConceptName.find_by(name: 'Lab Order Status')&.concept_id
15
+ end
11
16
  end
12
17
 
13
18
  has_many :tests,
@@ -44,12 +49,20 @@ module Lab
44
49
  class_name: '::Lab::LimsOrderMapping',
45
50
  foreign_key: :order_id
46
51
 
52
+ # Status trails are stored as observations with concept 'Lab Order Status'
53
+ has_many :status_trail_observations,
54
+ lambda {
55
+ unscoped.where(voided: 0, concept_id: Lab::LabOrder.order_status_concept_id).order(obs_datetime: :asc)
56
+ },
57
+ class_name: 'Observation',
58
+ foreign_key: :order_id
59
+
47
60
  default_scope do
48
61
  joins(:order_type)
49
62
  .merge(OrderType.where(name: [
50
- Lab::Metadata::ORDER_TYPE_NAME,
51
- Lab::Metadata::HTS_ORDER_TYPE_NAME
52
- ]))
63
+ Lab::Metadata::ORDER_TYPE_NAME,
64
+ Lab::Metadata::HTS_ORDER_TYPE_NAME
65
+ ]))
53
66
  .where.not(concept_id: ConceptName.where(name: 'Tests ordered').select(:concept_id))
54
67
  end
55
68
 
@@ -57,11 +70,13 @@ module Lab
57
70
  scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
58
71
 
59
72
  def self.prefetch_relationships
60
- includes(:reason_for_test,
61
- :requesting_clinician,
62
- :target_lab,
63
- :comment_to_fulfiller,
64
- tests: [:result])
73
+ # NOTE: status_trail_observations and test results are not preloaded due to
74
+ # Rails limitations with eager loading unscoped associations. They load on-demand instead.
75
+ preload(:reason_for_test,
76
+ :requesting_clinician,
77
+ :target_lab,
78
+ :comment_to_fulfiller,
79
+ :tests)
65
80
  end
66
81
  end
67
82
  end
@@ -3,9 +3,9 @@
3
3
  module Lab
4
4
  class LabResult < Observation
5
5
  def children
6
- Observation.where(obs_group_id: obs_id, voided: 0)
6
+ Observation.unscoped.where(obs_group_id: obs_id, voided: 0)
7
7
  end
8
-
8
+
9
9
  alias measures children
10
10
 
11
11
  default_scope do
@@ -6,14 +6,33 @@ module Lab
6
6
  where(concept: ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME))
7
7
  end
8
8
 
9
+ # Cache the concept IDs as class methods to avoid lookups in association scopes
10
+ def self.test_status_concept_id
11
+ @test_status_concept_id ||= ConceptName.find_by(name: 'Lab Test Status')&.concept_id
12
+ end
13
+
14
+ def self.test_result_concept_id
15
+ @test_result_concept_id ||= ConceptName.find_by(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)&.concept_id
16
+ end
17
+
9
18
  has_one :result,
10
- -> { where(concept: ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)) },
19
+ -> { unscoped.where(voided: 0, concept_id: Lab::LabTest.test_result_concept_id) },
11
20
  class_name: 'Lab::LabResult',
12
21
  foreign_key: :obs_group_id
13
22
 
23
+ # Status trails are stored as observations with concept 'Lab Test Status'
24
+ # They are linked via obs_group_id (this test obs is the parent)
25
+ has_many :status_trail_observations,
26
+ lambda {
27
+ unscoped.where(voided: 0, concept_id: Lab::LabTest.test_status_concept_id).order(obs_datetime: :asc)
28
+ },
29
+ class_name: 'Observation',
30
+ foreign_key: :obs_group_id,
31
+ primary_key: :obs_id
32
+
14
33
  def void(reason)
15
34
  result&.void(reason)
16
- super(reason)
35
+ super
17
36
  end
18
37
  end
19
38
  end
@@ -9,9 +9,8 @@ module Lab
9
9
  reason_for_test ||= order.reason_for_test
10
10
  target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
11
11
 
12
- # Use unscoped to find encounter across all locations
13
- encounter = Encounter.unscoped.find_by_encounter_id(order.encounter_id)
14
- program = Program.find_by_program_id(encounter.program_id)
12
+ encounter = Encounter.find_by_encounter_id(order.encounter_id)
13
+ program = Program.find_by_program_id(encounter&.program_id)
15
14
 
16
15
  ActiveSupport::HashWithIndifferentAccess.new(
17
16
  {
@@ -20,8 +19,8 @@ module Lab
20
19
  order_id: order.order_id, # Deprecated: Link to :id
21
20
  encounter_id: order.encounter_id,
22
21
  order_date: order.start_date,
23
- location_id: encounter.location_id,
24
- program_id: encounter.program_id,
22
+ location_id: encounter&.location_id,
23
+ program_id: encounter&.program_id,
25
24
  program_name: program&.name,
26
25
  patient_id: order.patient_id,
27
26
  accession_number: order.accession_number,
@@ -37,8 +36,10 @@ module Lab
37
36
  name: concept_name(reason_for_test&.value_coded)
38
37
  },
39
38
  delivery_mode: order&.lims_acknowledgement_status&.acknowledgement_type,
39
+ order_status: latest_order_status(order),
40
+ order_status_trail: serialize_order_status_trail(order),
40
41
  tests: tests.map do |test|
41
- result_obs = test.children.first
42
+ result_obs = test.result
42
43
 
43
44
  {
44
45
  id: test.obs_id,
@@ -46,7 +47,9 @@ module Lab
46
47
  uuid: test.uuid,
47
48
  name: concept_name(test.value_coded),
48
49
  test_method: test_method(order, test.value_coded),
49
- result: result_obs && ResultSerializer.serialize(result_obs)
50
+ result: result_obs && ResultSerializer.serialize(result_obs),
51
+ test_status: latest_test_status(test),
52
+ test_status_trail: serialize_test_status_trail(test)
50
53
  }
51
54
  end
52
55
  }
@@ -75,5 +78,72 @@ module Lab
75
78
  .select(:concept_id)
76
79
  LabTest.unscoped.where(concept:, order:, voided: true)
77
80
  end
81
+
82
+ def self.latest_order_status(order)
83
+ # Query obs table for latest order status
84
+ latest_obs = order.status_trail_observations.last
85
+ return nil unless latest_obs
86
+
87
+ updated_by = parse_comments_json(latest_obs.comments)
88
+
89
+ {
90
+ status_id: 0, # status_id not used with text values
91
+ status: latest_obs.value_text,
92
+ timestamp: latest_obs.obs_datetime,
93
+ updated_by: updated_by
94
+ }
95
+ end
96
+
97
+ def self.serialize_order_status_trail(order)
98
+ # Query obs table for order status trail
99
+ order.status_trail_observations.map do |obs|
100
+ updated_by = parse_comments_json(obs.comments)
101
+
102
+ {
103
+ status_id: 0, # status_id not used with text values
104
+ status: obs.value_text,
105
+ timestamp: obs.obs_datetime,
106
+ updated_by: updated_by
107
+ }
108
+ end
109
+ end
110
+
111
+ def self.latest_test_status(test)
112
+ # Query obs table for latest test status
113
+ latest_obs = test.status_trail_observations.last
114
+ return nil unless latest_obs
115
+
116
+ updated_by = parse_comments_json(latest_obs.comments)
117
+
118
+ {
119
+ status_id: 0, # status_id not used with text values
120
+ status: latest_obs.value_text,
121
+ timestamp: latest_obs.obs_datetime,
122
+ updated_by: updated_by
123
+ }
124
+ end
125
+
126
+ def self.serialize_test_status_trail(test)
127
+ # Query obs table for test status trail
128
+ test.status_trail_observations.map do |obs|
129
+ updated_by = parse_comments_json(obs.comments)
130
+
131
+ {
132
+ status_id: 0, # status_id not used with text values
133
+ status: obs.value_text,
134
+ timestamp: obs.obs_datetime,
135
+ updated_by: updated_by
136
+ }
137
+ end
138
+ end
139
+
140
+ # Helper to parse updated_by from obs comments field
141
+ def self.parse_comments_json(comments)
142
+ return {} if comments.blank?
143
+
144
+ JSON.parse(comments)
145
+ rescue JSON::ParserError
146
+ {}
147
+ end
78
148
  end
79
149
  end
@@ -14,9 +14,12 @@ module Lab
14
14
  date_received: params[:date_received])
15
15
  end
16
16
 
17
- def acknowledgements_pending_sync(batch_size)
18
- Lab::LabAcknowledgement.where(pushed: false)
19
- .limit(batch_size)
17
+ def acknowledgements_pending_sync(batch_size, start_date: nil)
18
+ query = Lab::LabAcknowledgement.joins(:order).where(pushed: false)
19
+
20
+ query = query.where('orders.date_created >= ?', start_date) if start_date
21
+
22
+ query.limit(batch_size)
20
23
  end
21
24
 
22
25
  def push_acknowledgement(acknowledgement, lims_api)
@@ -30,7 +33,8 @@ module Lab
30
33
  Rails.logger.info("Updating acknowledgement ##{acknowledgement_dto[:tracking_number]} in LIMS")
31
34
  response = lims_api.acknowledge(acknowledgement_dto)
32
35
  Rails.logger.info("Info #{response}")
33
- if ['results already delivered for test name given', 'test result acknowledged successfully', 'test result already acknowledged electronically at facility'].include?(response['message'])
36
+ if ['results already delivered for test name given', 'test result acknowledged successfully',
37
+ 'test result already acknowledged electronically at facility'].include?(response['message'])
34
38
  acknowledgement.pushed = true
35
39
  acknowledgement.date_pushed = Time.now
36
40
  acknowledgement.save!
@@ -4,20 +4,22 @@ module Lab
4
4
  module Lims
5
5
  # This class is responsible for handling the acknowledgement of lab orders
6
6
  class AcknowledgementWorker
7
- attr_reader :lims_api
7
+ attr_reader :lims_api, :start_date
8
8
 
9
9
  include Utils # for logger
10
10
 
11
11
  SECONDS_TO_WAIT_FOR_ORDERS = 30
12
12
 
13
- def initialize(lims_api)
13
+ def initialize(lims_api, start_date: nil)
14
14
  @lims_api = lims_api
15
+ @start_date = start_date
15
16
  end
16
17
 
17
18
  def push_acknowledgement(batch_size: 1000, wait: false)
18
19
  loop do
19
20
  logger.info('Looking for new acknowledgements to push to LIMS...')
20
- acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size).all
21
+ acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size,
22
+ start_date: start_date).all
21
23
 
22
24
  logger.debug("Found #{acknowledgements.size} acknowledgements...")
23
25
  acknowledgements.each do |acknowledgement|