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



100
101
102
103
104
105
106
107
# File 'app/services/lab/orders_service.rb', line 100

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



152
153
154
# File 'app/services/lab/orders_service.rb', line 152

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



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'app/services/lab/orders_service.rb', line 233

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
# 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? && check_tracking_number(order_params[:accession_number])
      raise 'Accession number already exists'
    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)


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/services/lab/orders_service.rb', line 109

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



188
189
190
191
192
193
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
# File 'app/services/lab/orders_service.rb', line 188

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



156
157
158
159
160
161
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
# File 'app/services/lab/orders_service.rb', line 156

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



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/services/lab/orders_service.rb', line 138

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