Module: Lab::OrdersService

Defined in:
app/services/lab/orders_service.rb

Overview

Manage lab orders.

Lab orders are just ordinary openmrs orders with extra metadata that separates them from other orders. Lab orders have an order type of ‘Lab’ with the order’s test type as the order’s concept. The order’s start date is the day the order is made. Additional information pertaining to the order is stored as observations that point to the order. The specimen types, requesting clinician, target lab, and reason for test are saved as observations to the order. Refer to method #order_test for more information.

Class Method Summary collapse

Class Method Details

.attach_test_method(order, order_params) ⇒ Object



106
107
108
109
110
111
112
113
# File 'app/services/lab/orders_service.rb', line 106

def attach_test_method(order, order_params)
  create_order_observation(
    order,
    Lab::Metadata::TEST_METHOD_CONCEPT_NAME,
    order_params[:date],
    value_coded: order_params[:test_method]
  )
end

.check_tracking_number(tracking_number) ⇒ Object



158
159
160
# File 'app/services/lab/orders_service.rb', line 158

def check_tracking_number(tracking_number)
  accession_number_exists?(tracking_number) || nlims_accession_number_exists?(tracking_number)
end

.lab_orders(start_date, end_date, concept_id = nil, include_data: false) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'app/services/lab/orders_service.rb', line 239

def lab_orders(start_date, end_date, concept_id = nil, include_data: false)
  tests = Lab::LabTest.where('date_created >= ? AND date_created <= ?', start_date, end_date)
  tests = tests.where(value_coded: concept_id) if concept_id
  orders = Lab::LabOrder.where(order_id: tests.pluck(:order_id))
  data = {
    count: orders.count,
    last_order_date: Lab::LabOrder.last&.start_date&.to_date,
    lab_orders: []
  }
  if include_data
    data[:lab_orders] = orders.map do |order|
      Lab::LabOrderSerializer.serialize_order(
        order, requesting_clinician: order.requesting_clinician,
               reason_for_test: order.reason_for_test,
               target_lab: order.target_lab
      )
    end
  end
  data
end

.order_test(order_params) ⇒ Object

Create a lab order.

Parameters schema:

{
  encounter_id: {
    type: :integer,
    required: :false,
    description: 'Attach order to this if program_id and patient_id are not provided'
  },
  program_id: { type: :integer, required: false },
  patient_id: { type: :integer, required: false }
  specimen: { type: :object, properties: { concept_id: :integer }, required: %i[concept_id] },
  test_type_ids: {
    type: :array,
    items: {
      type: :object,
      properties: { concept_id: :integer },
      required: %i[concept_id]
    }
  },
  start_date: { type: :datetime }
  accession_number: { type: :string }
  target_lab: { type: :string },
  reason_for_test_id: { type: :integer },
  requesting_clinician: { type: :string }
}

encounter_id: is an ID of the encounter the lab order is to be created under test_type_id: is a concept_id of the name of test being ordered specimen_type_id: is a list of IDs for the specimens to be tested (can be ommited) target_lab: is the name of the lab where test will be carried out reason_for_test_id: is a concept_id for a (standard) reason of why the test is being carried out requesting_clinician: Name of the clinician requesting the test (defaults to current user)



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'app/services/lab/orders_service.rb', line 52

def order_test(order_params)
  serialized_order = nil
  order_id = nil
  patient_id = nil

  Order.transaction do
    encounter = find_encounter(order_params)
    if order_params[:accession_number].present?
      begin
        raise 'Accession number already exists' if check_tracking_number(order_params[:accession_number])
      rescue Lab::Lims::ValidationUnavailable => e
        # Log warning but allow order creation to proceed
        # User should have been prompted to confirm on the frontend
        Rails.logger.warn("Creating order with unvalidated accession number: #{e.message}")
      end
    end

    order = create_order(encounter, order_params)

    attach_test_method(order, order_params) if order_params[:test_method]

    # Create initial order status trail
    create_initial_order_status_trail(order)

    Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])

    # Reload order to include status trails and tests
    order = Lab::LabOrder.prefetch_relationships.find(order.order_id)

    serialized_order = Lab::LabOrderSerializer.serialize_order(
      order, requesting_clinician: add_requesting_clinician(order, order_params),
             reason_for_test: add_reason_for_test(order, order_params),
             target_lab: add_target_lab(order, order_params),
             comment_to_fulfiller: add_comment_to_fulfiller(order, order_params)
    )

    # Store IDs for notification after transaction commits
    order_id = order.order_id
    patient_id = order.patient_id
  end

  # Publish notification AFTER transaction commits
  # This ensures the order is visible in the database before rebuilding
  ActiveSupport::Notifications.instrument(
    'lab.order_created',
    patient_id: patient_id,
    order_id: order_id,
    accession_number: serialized_order[:accession_number],
    timestamp: Time.current
  )

  serialized_order
end

.update_order(order_id, params) ⇒ Object

Raises:

  • (::InvalidParameterError)


115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/services/lab/orders_service.rb', line 115

def update_order(order_id, params)
  specimen_id = params.dig(:specimen, :concept_id)
  raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id

  order = Lab::LabOrder.find(order_id)
  if order.concept_id != unknown_concept_id && !params[:force_update]&.casecmp?('true')
    raise ::UnprocessableEntityError, "Can't change order specimen once set"
  end

  if specimen_id.to_i != order.concept_id
    Rails.logger.debug("Updating order ##{order.order_id}")
    order.update!(concept_id: specimen_id,
                  discontinued: true,
                  discontinued_by: User.current.user_id,
                  discontinued_date: params[:date]&.to_date || Time.now,
                  discontinued_reason_non_coded: 'Sample drawn/updated')
  end

  reason_for_test = params[:reason_for_test] || params[:reason_for_test_id]

  if reason_for_test
    Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
    update_reason_for_test(order, Concept.find(reason_for_test)&.id,
                           force_update: params.fetch('force_update', false))
  end

  Lab::LabOrderSerializer.serialize_order(order)
end

.update_order_result(order_params) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/services/lab/orders_service.rb', line 194

def update_order_result(order_params)
  # Extract tracking number from nested structure if present
  tracking_number = order_params['tracking_number'] || order_params.dig('order', 'tracking_number')
  order = find_order(tracking_number)

  order_dto = Lab::Lims::OrderSerializer.serialize_order(order)

  # Handle results if present in the old format
  patch_order_dto_with_lims_results!(order_dto, order_params['results']) if order_params['results']

  # Handle test results in NLIMS format
  if order_params['tests']
    # Extract test results from NLIMS tests payload
    test_results = {}
    order_params['tests'].each do |test_data|
      test_name = test_data.dig('test_type', 'name')
      next unless test_name && test_data['test_results']

      test_results[test_name] = {
        'results' => test_data['test_results'].each_with_object({}) do |result, formatted|
          measure_name = result.dig('measure', 'name')
          result_value = result.dig('result', 'value')
          next unless measure_name && result_value

          formatted[measure_name] = { 'result_value' => result_value }
        end,
        'result_date' => test_data['test_results'].first&.dig('result', 'result_date'),
        'result_entered_by' => {}
      }
    end

    patch_order_dto_with_lims_results!(order_dto, test_results) unless test_results.empty?
  end

  # Save order status trail if available from NLIMS
  if order_params['order'] && order_params['order']['status_trail']
    save_order_status_trails_from_nlims(order, order_params['order']['status_trail'])
  end

  # Save test status trails if available from NLIMS
  save_test_status_trails_from_nlims(order, order_params['tests']) if order_params['tests']

  Lab::Lims::PullWorker.new(nil).process_order(order_dto)
end

.update_order_status(order_params) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'app/services/lab/orders_service.rb', line 162

def update_order_status(order_params)
  # find the order
  order = find_order(order_params['tracking_number'])
  concept = ConceptName.find_by_name Lab::Metadata::LAB_ORDER_STATUS_CONCEPT_NAME
  ActiveRecord::Base.transaction do
    void_order_status(order, concept)
    Observation.create!(
      person_id: order.patient_id,
      encounter_id: order.encounter_id,
      concept_id: concept.concept_id,
      order_id: order.id,
      obs_datetime: order_params['status_time'] || Time.now,
      value_text: order_params['status'],
      creator: User.current.id
    )

    # Save order status trail if available
    save_order_status_trail(order, order_params) if order_params['status']
  end
  create_rejection_notification(order_params) if order_params['status'] == 'test-rejected'

  # Publish notification that order status has changed
  ActiveSupport::Notifications.instrument(
    'lab.order_status_changed',
    patient_id: order.patient_id,
    order_id: order.order_id,
    new_status: order_params['status'],
    tracking_number: order_params['tracking_number'],
    timestamp: Time.current
  )
end

.void_order(order_id, reason) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'app/services/lab/orders_service.rb', line 144

def void_order(order_id, reason)
  order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab comment_to_fulfiller],
                                 tests: [:result])
                       .find(order_id)

  order.requesting_clinician&.void(reason)
  order.reason_for_test&.void(reason)
  order.comment_to_fulfiller&.void(reason)
  order.target_lab&.void(reason)

  order.tests.each { |test| test.void(reason) }
  order.void(reason)
end