his_emr_api_lab 2.1.0 → 2.1.2

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/lab/labels_controller.rb +2 -1
  3. data/app/controllers/lab/orders_controller.rb +14 -25
  4. data/app/controllers/lab/results_controller.rb +2 -1
  5. data/app/controllers/lab/specimen_types_controller.rb +0 -1
  6. data/app/controllers/lab/test_methods_controller.rb +9 -0
  7. data/app/controllers/lab/test_types_controller.rb +1 -1
  8. data/app/controllers/lab/tests_controller.rb +1 -3
  9. data/app/controllers/lab/users_controller.rb +2 -1
  10. data/app/jobs/lab/process_lab_result_job.rb +13 -0
  11. data/app/models/lab/lab_order.rb +4 -1
  12. data/app/models/lab/lab_result.rb +4 -0
  13. data/app/models/lab/lab_test.rb +1 -1
  14. data/app/models/lab/order_extension.rb +14 -0
  15. data/app/serializers/lab/lab_order_serializer.rb +27 -4
  16. data/app/serializers/lab/result_serializer.rb +10 -2
  17. data/app/serializers/lab/test_serializer.rb +24 -1
  18. data/app/services/lab/accession_number_service.rb +2 -2
  19. data/app/services/lab/acknowledgement_service.rb +4 -1
  20. data/app/services/lab/concepts_service.rb +77 -22
  21. data/app/services/lab/lims/acknowledgement_worker.rb +1 -1
  22. data/app/services/lab/lims/api/rest_api.rb +543 -78
  23. data/app/services/lab/lims/config.rb +7 -2
  24. data/app/services/lab/lims/exceptions.rb +7 -6
  25. data/app/services/lab/lims/migrator.rb +3 -3
  26. data/app/services/lab/lims/order_dto.rb +1 -1
  27. data/app/services/lab/lims/order_serializer.rb +28 -7
  28. data/app/services/lab/lims/pull_worker.rb +6 -6
  29. data/app/services/lab/lims/push_worker.rb +3 -3
  30. data/app/services/lab/lims/utils.rb +3 -4
  31. data/app/services/lab/lims/worker.rb +2 -2
  32. data/app/services/lab/metadata.rb +4 -0
  33. data/app/services/lab/notification_service.rb +72 -0
  34. data/app/services/lab/orders_search_service.rb +11 -5
  35. data/app/services/lab/orders_service.rb +82 -36
  36. data/app/services/lab/results_service.rb +32 -17
  37. data/app/services/lab/tests_service.rb +15 -3
  38. data/config/routes.rb +1 -0
  39. data/db/migrate/20260119104240_add_fulfiller_fields_to_orders.rb +11 -0
  40. data/db/migrate/20260119104241_create_comment_to_fulfiller_concept.rb +50 -0
  41. data/lib/lab/version.rb +1 -1
  42. data/lib/mahis_emr_api_lab.rb +6 -0
  43. metadata +12 -5
  44. /data/app/services/lab/lims/api/{couchdb_api.rb → couch_db_api.rb} +0 -0
@@ -50,10 +50,15 @@ module Lab
50
50
  30 # Seconds
51
51
  end
52
52
 
53
+ def config_file_name
54
+ # if nlims.yml exists return it, else return application.yml
55
+ File.exist?(Rails.root.join('config', 'nlims.yml')) ? 'nlims.yml' : 'application.yml'
56
+ end
57
+
53
58
  ##
54
59
  # Returns LIMS' application.yml configuration file
55
60
  def application
56
- @application ||= YAML.load_file(find_config_path('application.yml'))
61
+ @application ||= YAML.load_file(find_config_path(config_file_name))
57
62
  end
58
63
 
59
64
  ##
@@ -65,7 +70,7 @@ module Lab
65
70
  private
66
71
 
67
72
  def emr_api_application(param, fallback = nil)
68
- @emr_api_application ||= YAML.load_file(Rails.root.join('config', 'application.yml'))
73
+ @emr_api_application ||= YAML.load_file(Rails.root.join('config', config_file_name))
69
74
 
70
75
  @emr_api_application.fetch(param) do
71
76
  raise ConfigNotFound, "Missing config param: #{param}" unless fallback
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Lab
4
4
  module Lims
5
- class Exceptions < StandardError; end
6
- class LimsException < StandardError; end
7
- class DuplicateNHID < LimsException; end
8
- class MissingAccessionNumber < LimsException; end
9
- class UnknownSpecimenType < LimsException; end
10
- class UnknownTestType < LimsException; end
5
+ module Exceptions
6
+ class LimsException < StandardError; end
7
+ class DuplicateNHID < LimsException; end
8
+ class MissingAccessionNumber < LimsException; end
9
+ class UnknownSpecimenType < LimsException; end
10
+ class UnknownTestType < LimsException; end
11
+ end
11
12
  end
12
13
  end
@@ -29,7 +29,7 @@ require 'lab/lab_test'
29
29
  require 'lab/lims_order_mapping'
30
30
  require 'lab/lims_failed_import'
31
31
 
32
- require_relative 'api/couchdb_api'
32
+ require_relative 'api/couch_db_api'
33
33
  require_relative 'config'
34
34
  require_relative 'pull_worker'
35
35
  require_relative 'utils'
@@ -60,8 +60,8 @@ module Lab
60
60
  ##
61
61
  # A Lab::Lims::Api object that supports crawling of a LIMS CouchDB instance.
62
62
  class CouchDbMigratorApi < Lab::Lims::Api::CouchdbApi
63
- def initialize(*args, processes: 1, on_merge_processes: nil, **kwargs)
64
- super(*args, **kwargs)
63
+ def initialize(*, processes: 1, on_merge_processes: nil, **)
64
+ super(*, **)
65
65
 
66
66
  @processes = processes
67
67
  @on_merge_processes = on_merge_processes
@@ -17,7 +17,7 @@ module Lab
17
17
  accession_number: self['tracking_number'],
18
18
  patient_id:,
19
19
  specimen: { concept_id: specimen_type_id },
20
- tests: self['tests']&.map { |test| { concept_id: test_type_id(test) } },
20
+ tests: self['tests_map']&.map { |test| { concept_id: test.concept_id } },
21
21
  requesting_clinician:,
22
22
  date: start_date,
23
23
  target_lab: facility_name(self['receiving_facility']),
@@ -14,16 +14,20 @@ module Lab
14
14
 
15
15
  def serialize_order(order)
16
16
  serialized_order = Lims::Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
17
-
18
17
  Lims::OrderDto.new(
19
- _id: Lab::LimsOrderMapping.find_by(order:)&.lims_id || serialized_order.accession_number,
18
+ _id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
20
19
  tracking_number: serialized_order.accession_number,
21
20
  sending_facility: current_facility_name,
22
21
  receiving_facility: serialized_order.target_lab,
23
22
  tests: serialized_order.tests.map { |test| format_test_name(test.name) },
23
+ tests_map: serialized_order.tests,
24
24
  patient: format_patient(serialized_order.patient_id),
25
25
  order_location: format_order_location(serialized_order.encounter_id),
26
26
  sample_type: format_sample_type(serialized_order.specimen.name),
27
+ sample_type_map: {
28
+ name: format_sample_type(serialized_order.specimen.name),
29
+ nlims_code: Concept.find(serialized_order.specimen.concept_id).nlims_code
30
+ },
27
31
  sample_status: format_sample_status(serialized_order.specimen.name),
28
32
  sample_statuses: format_sample_status_trail(order),
29
33
  test_statuses: format_test_status_trail(order),
@@ -32,7 +36,8 @@ module Lab
32
36
  priority: format_sample_priority(serialized_order.reason_for_test.name),
33
37
  date_created: serialized_order.order_date,
34
38
  test_results: format_test_results(serialized_order),
35
- type: 'Order'
39
+ type: 'Order',
40
+ clinical_history: serialized_order.comment_to_fulfiller
36
41
  )
37
42
  end
38
43
 
@@ -81,6 +86,8 @@ module Lab
81
86
  return nil if regimen_data.blank?
82
87
 
83
88
  regimen_data['regimen']
89
+ rescue StandardError
90
+ nil
84
91
  end
85
92
 
86
93
  def find_arv_number(patient_id)
@@ -97,6 +104,8 @@ module Lab
97
104
  return nil if start_date.blank?
98
105
 
99
106
  start_date['earliest_date']
107
+ rescue StandardError
108
+ nil
100
109
  end
101
110
 
102
111
  def format_sample_type(name)
@@ -114,9 +123,12 @@ module Lab
114
123
  def format_sample_status_trail(order)
115
124
  return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
116
125
 
117
- user = User.find(order.discontinued_by || order.creator)
126
+ user = User.find(order.creator)
127
+ user = User.find(order.discontinued_by) if Order.columns_hash.key?('discontinued_by') && user.blank?
128
+
118
129
  drawn_by = PersonName.find_by_person_id(user.user_id)
119
- drawn_date = order.discontinued_date || order.start_date
130
+ drawn_date = order.discontinued_date || order.start_date if ['discontinued_date', 'start_date'].all? { |column| order.respond_to?(column) }
131
+ drawn_date ||= order.date_created
120
132
 
121
133
  [
122
134
  drawn_date.strftime('%Y%m%d%H%M%S') => {
@@ -132,7 +144,7 @@ module Lab
132
144
  end
133
145
 
134
146
  def format_test_status_trail(order)
135
- tests = order.voided.zero? ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
147
+ tests = order.voided ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
136
148
 
137
149
  tests.each_with_object({}) do |test, trail|
138
150
  test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
@@ -168,7 +180,13 @@ module Lab
168
180
  order.tests&.each_with_object({}) do |test, results|
169
181
  next if test.result.nil? || test.result.empty?
170
182
 
171
- test_creator = User.find(Observation.find(test.result.first.id).creator)
183
+ result_obs = Observation.find_by(obs_id: test.result.first.id)
184
+ unless result_obs
185
+ Rails.logger.warn("Observation with obs_id=#{test.result.first.id} not found for test #{test.name} in order #{order.accession_number}")
186
+ next
187
+ end
188
+
189
+ test_creator = User.find(result_obs.creator)
172
190
  test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
173
191
 
174
192
  results[format_test_name(test.name)] = {
@@ -184,6 +202,9 @@ module Lab
184
202
  id: test_creator.username
185
203
  }
186
204
  }
205
+ rescue ActiveRecord::RecordNotFound => e
206
+ Rails.logger.error("Failed to format test results for test #{test.name} in order #{order.accession_number}: #{e.message}")
207
+ next
187
208
  end
188
209
  end
189
210
 
@@ -23,7 +23,7 @@ module Lab
23
23
  lims_api.consume_orders(from: last_seq, limit: batch_size, **) do |order_dto, context|
24
24
  logger.debug("Retrieved order ##{order_dto[:tracking_number]}: #{order_dto}")
25
25
 
26
- patient = find_patient_by_nhid(order_dto[:patient][:id])
26
+ patient = find_patient_by_nhid(order_dto[:patient][:id], order_dto[:tracking_number])
27
27
  unless patient
28
28
  logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
29
29
  order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
@@ -58,7 +58,7 @@ module Lab
58
58
  end
59
59
 
60
60
  def process_order(order_dto)
61
- patient = find_patient_by_nhid(order_dto[:patient][:id])
61
+ patient = find_patient_by_nhid(order_dto[:patient][:id], order_dto[:tracking_number])
62
62
  unless patient
63
63
  logger.debug("Discarding order: Non local patient ##{order_dto[:patient][:id]} on order ##{order_dto[:tracking_number]}")
64
64
  order_rejected(order_dto, "Patient NPID, '#{order_dto[:patient][:id]}', didn't match any local NPIDs")
@@ -107,7 +107,7 @@ module Lab
107
107
 
108
108
  private
109
109
 
110
- def find_patient_by_nhid(nhid)
110
+ def find_patient_by_nhid(nhid, accession_number)
111
111
  national_id_type = PatientIdentifierType.where(name: ['National id', 'Old Identification Number'])
112
112
  identifiers = PatientIdentifier.where(type: national_id_type, identifier: nhid)
113
113
  .joins('INNER JOIN person ON person.person_id = patient_identifier.patient_id AND person.voided = 0')
@@ -126,10 +126,10 @@ module Lab
126
126
  patients = Patient.where(patient_id: identifiers.select(:patient_id))
127
127
  .distinct(:patient_id)
128
128
  .all
129
+ order = Order.find_by(patient_id: patients.select(:patient_id), accession_number: accession_number)
130
+ raise Lab::Lims::LimsException, "Order #{accession_number} does not exists for patient #{nhid}" if order.nil?
129
131
 
130
- raise Lab::Lims::DuplicateNHID, "Duplicate National Health ID: #{nhid}" if patients.size > 1
131
-
132
- patients.first
132
+ order.patient
133
133
  end
134
134
 
135
135
  ##
@@ -10,6 +10,7 @@ module Lab
10
10
  include Utils # for logger
11
11
 
12
12
  SECONDS_TO_WAIT_FOR_ORDERS = 30
13
+ START_DATE = Time.parse('2024-09-03').freeze
13
14
 
14
15
  def initialize(lims_api)
15
16
  @lims_api = lims_api
@@ -23,8 +24,6 @@ module Lab
23
24
  logger.debug("Found #{orders.size} orders...")
24
25
  orders.each do |order|
25
26
  push_order(order)
26
- rescue GatewayError => e
27
- logger.error("Failed to push order ##{order.accession_number}: #{e.class} - #{e.message}")
28
27
  rescue StandardError => e
29
28
  logger.error("Failed to push order ##{order.id}: #{order&.accession_number} : #{e.class} - #{e.message}")
30
29
  end
@@ -53,7 +52,7 @@ module Lab
53
52
 
54
53
  ActiveRecord::Base.transaction do
55
54
  if mapping && !order.voided.zero?
56
- Rails.logger.info("Deleting order ##{order_dto[:accession_number]} from LIMS")
55
+ Rails.logger.info("Deleting order ##{order_dto['accession_number']} from LIMS")
57
56
  lims_api.delete_order(mapping.lims_id, order_dto)
58
57
  mapping.destroy
59
58
  elsif mapping
@@ -103,6 +102,7 @@ module Lab
103
102
  Rails.logger.debug('Looking for new orders that need to be created in LIMS...')
104
103
  Lab::LabOrder.where.not(order_id: Lab::LimsOrderMapping.all.select(:order_id))
105
104
  .where("accession_number IS NOT NULL AND accession_number !=''")
105
+ .where(date_created: START_DATE..(Date.today + 1.day))
106
106
  .order(date_created: :desc)
107
107
  end
108
108
 
@@ -47,10 +47,9 @@ module Lab
47
47
  return user if user
48
48
 
49
49
  god_user = User.first
50
-
51
- person = Person.create!(creator: god_user.user_id)
52
- PersonName.create!(person:, given_name: 'Lab', family_name: 'Daemon', creator: god_user.user_id)
53
-
50
+ User.current = god_user
51
+ person = Person.create!(creator: god_user.user_id, birthdate: '1980-01-01')
52
+ PersonName.create!(person: person, given_name: 'Lab', family_name: 'Daemon', creator: god_user.user_id)
54
53
  User.create!(username: 'lab_daemon', person:, creator: god_user.user_id)
55
54
  end
56
55
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'logger_multiplexor'
4
4
 
5
- require_relative 'api/couchdb_api'
5
+ # require_relative 'api/couchdb_api'
6
6
 
7
7
  module Lab
8
8
  module Lims
@@ -13,7 +13,7 @@ module Lab
13
13
  User.current = Utils.lab_user
14
14
 
15
15
  fork(&method(:start_push_worker))
16
- # fork(&method(:start_pull_worker))
16
+ fork(&method(:start_pull_worker))
17
17
  fork(&method(:start_acknowledgement_worker))
18
18
  fork(&method(:start_realtime_pull_worker)) if realtime_updates_enabled?
19
19
 
@@ -5,6 +5,7 @@ module Lab
5
5
  # Concepts
6
6
  REASON_FOR_TEST_CONCEPT_NAME = 'Reason for test'
7
7
  REQUESTING_CLINICIAN_CONCEPT_NAME = 'Person making request'
8
+ COMMENT_TO_FULFILLER_CONCEPT_NAME = 'Comment to fulfiller'
8
9
  SPECIMEN_TYPE_CONCEPT_NAME = 'Specimen type'
9
10
  TARGET_LAB_CONCEPT_NAME = 'Lab'
10
11
  TEST_RESULT_CONCEPT_NAME = 'Lab test result'
@@ -12,12 +13,15 @@ module Lab
12
13
  TEST_TYPE_CONCEPT_NAME = 'Test type'
13
14
  LAB_ORDER_STATUS_CONCEPT_NAME = 'lab order status'
14
15
  UNKNOWN_SPECIMEN = 'Unknown'
16
+ VISIT_TYPE_UUID = '8a4a4488-c2cc-11de-8d13-0010c6dffd0f'
17
+ TEST_METHOD_CONCEPT_NAME = 'Recommended test method'
15
18
 
16
19
  # Encounter
17
20
  ENCOUNTER_TYPE_NAME = 'Lab'
18
21
 
19
22
  # Order types
20
23
  ORDER_TYPE_NAME = 'Lab'
24
+ HTS_ORDER_TYPE_NAME = 'HTS Lab'
21
25
 
22
26
  # Programs
23
27
  LAB_PROGRAM_NAME = 'Laboratory Program'
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # notification service
4
+ class Lab::NotificationService
5
+ # this gets all uncleared notifications of the user
6
+ def uncleared
7
+ NotificationAlert.joins(:notification_alert_recipients).where(
8
+ 'notification_alert_recipient.user_id = ?', User.current.user_id
9
+ )
10
+ end
11
+
12
+ def clear(alert_id)
13
+ alert = NotificationAlert.find(alert_id)
14
+ # update the notification alert recipient to cleared and read only for the current user
15
+ alert.notification_alert_recipients.where(user_id: User.current.user_id).update_all(cleared: true, alert_read: true)
16
+ end
17
+
18
+ # this updates the notification to read
19
+ def read(alerts)
20
+ alerts.each do |alert|
21
+ notification = NotificationAlertRecipient.where(user_id: User.current.user_id, alert_id: alert,
22
+ alert_read: false).first
23
+ next if notification.blank?
24
+
25
+ notification.alert_read = true
26
+ notification.save
27
+ end
28
+ end
29
+
30
+ def create_notification(alert_type, alert_message)
31
+ return if alert_type != 'LIMS'
32
+
33
+ lab = User.find_by(username: 'lab_daemon')
34
+ ActiveRecord::Base.transaction do
35
+ alert = NotificationAlert.create!(text: alert_message.to_json, date_to_expire: Time.now + not_period.days,
36
+ creator: lab, changed_by: lab, date_created: Time.now)
37
+ notify(alert, User.joins(:roles).uniq)
38
+ # ActionCable.server.broadcast('nlims_channel', alert)
39
+ end
40
+ end
41
+
42
+ def not_period
43
+ result = GlobalProperty.where(property: 'notification.period')&.first
44
+ return result.property_value.to_i if result.present?
45
+
46
+ 7 # default to 7 days
47
+ end
48
+
49
+ def notify(notification_alert, recipients)
50
+ recipients.each do |recipient|
51
+ recipient.notification_alert_recipients.create(
52
+ alert_id: notification_alert.id
53
+ )
54
+ end
55
+ end
56
+
57
+ def notify_all(notification_alert, users)
58
+ users.each do |user|
59
+ user.notification_alert_recipients.create(
60
+ alert_id: notification_alert.id
61
+ )
62
+ end
63
+ end
64
+
65
+ def notify_all_users(notification_alert)
66
+ User.all.each do |user|
67
+ user.notification_alert_recipients.create!(
68
+ alert_id: notification_alert.id
69
+ )
70
+ end
71
+ end
72
+ end
@@ -7,12 +7,18 @@ module Lab
7
7
  def find_orders(filters)
8
8
  extra_filters = pop_filters(filters, :date, :end_date, :status)
9
9
 
10
+ uuid = filters.delete(:patient)
11
+ patient = Patient.find(uuid) if uuid
12
+
13
+ filters.merge!(patient_id: patient.id) if patient
14
+
10
15
  orders = Lab::LabOrder.prefetch_relationships
11
16
  .where(filters)
12
- .order(start_date: :desc)
17
+ orders = orders.order(start_date: :desc) if Order.column_names.include?('start_date')
18
+ orders = orders.order(date_created: :desc) unless Order.column_names.include?('start_date')
13
19
 
14
- orders = filter_orders_by_status(orders, **pop_filters(extra_filters, :status))
15
- orders = filter_orders_by_date(orders, **extra_filters)
20
+ orders = filter_orders_by_status(orders: orders, status: extra_filters[:status])
21
+ orders = filter_orders_by_date(orders: orders, date: extra_filters[:date], end_date: extra_filters[:end_date])
16
22
 
17
23
  orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
18
24
  end
@@ -27,7 +33,7 @@ module Lab
27
33
  query
28
34
  end
29
35
 
30
- def filter_orders_by_date(orders, date: nil, end_date: nil)
36
+ def filter_orders_by_date(orders:, date: nil, end_date: nil)
31
37
  date = date&.to_date
32
38
  end_date = end_date&.to_date
33
39
 
@@ -40,7 +46,7 @@ module Lab
40
46
  orders
41
47
  end
42
48
 
43
- def filter_orders_by_status(orders, status: nil)
49
+ def filter_orders_by_status(orders:, status: nil)
44
50
  case status&.downcase
45
51
  when 'ordered' then orders.where(concept_id: unknown_concept_id)
46
52
  when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
@@ -58,16 +58,28 @@ module Lab
58
58
 
59
59
  order = create_order(encounter, order_params)
60
60
 
61
+ attach_test_method(order, order_params) if order_params[:test_method]
62
+
61
63
  Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
62
64
 
63
65
  Lab::LabOrderSerializer.serialize_order(
64
66
  order, requesting_clinician: add_requesting_clinician(order, order_params),
65
67
  reason_for_test: add_reason_for_test(order, order_params),
66
- target_lab: add_target_lab(order, order_params)
68
+ target_lab: add_target_lab(order, order_params),
69
+ comment_to_fulfiller: add_comment_to_fulfiller(order, order_params)
67
70
  )
68
71
  end
69
72
  end
70
73
 
74
+ def attach_test_method(order, order_params)
75
+ create_order_observation(
76
+ order,
77
+ Lab::Metadata::TEST_METHOD_CONCEPT_NAME,
78
+ order_params[:date],
79
+ value_coded: order_params[:test_method]
80
+ )
81
+ end
82
+
71
83
  def update_order(order_id, params)
72
84
  specimen_id = params.dig(:specimen, :concept_id)
73
85
  raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id
@@ -86,20 +98,23 @@ module Lab
86
98
  discontinued_reason_non_coded: 'Sample drawn/updated')
87
99
  end
88
100
 
89
- if params[:reason_for_test_id]
101
+ reason_for_test = params[:reason_for_test] || params[:reason_for_test_id]
102
+
103
+ if reason_for_test
90
104
  Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
91
- update_reason_for_test(order, params[:reason_for_test_id])
105
+ update_reason_for_test(order, Concept.find(reason_for_test)&.id, force_update: params.fetch('force_update', false))
92
106
  end
93
107
 
94
108
  Lab::LabOrderSerializer.serialize_order(order)
95
109
  end
96
110
 
97
111
  def void_order(order_id, reason)
98
- order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab], tests: [:result])
112
+ order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab comment_to_fulfiller], tests: [:result])
99
113
  .find(order_id)
100
114
 
101
115
  order.requesting_clinician&.void(reason)
102
116
  order.reason_for_test&.void(reason)
117
+ order.comment_to_fulfiller&.void(reason)
103
118
  order.target_lab&.void(reason)
104
119
 
105
120
  order.tests.each { |test| test.void(reason) }
@@ -160,13 +175,13 @@ module Lab
160
175
  def create_rejection_notification(order_params)
161
176
  order = find_order order_params['tracking_number']
162
177
  data = { 'type': 'LIMS',
178
+ 'specimen': ConceptName.find_by(concept_id: order.concept_id)&.name,
163
179
  'accession_number': order&.accession_number,
164
180
  'order_date': order&.start_date,
165
181
  'arv_number': find_arv_number(order.patient_id),
166
182
  'patient_id': result.person_id,
167
183
  'ordered_by': order&.provider&.person&.name,
168
- 'rejection_reason': order_params['comments']
169
- }.as_json
184
+ 'rejection_reason': order_params['comments'] }.as_json
170
185
  NotificationService.new.create_notification('LIMS', data)
171
186
  end
172
187
 
@@ -207,35 +222,50 @@ module Lab
207
222
  # a 'Lab' encounter is created using the provided program_id and
208
223
  # patient_id.
209
224
  def find_encounter(order_params)
210
- return Encounter.find(order_params[:encounter_id]) if order_params[:encounter_id]
211
-
212
- raise InvalidParameterError, 'encounter_id or patient_id required' unless order_params[:patient_id]
213
-
214
- program_id = order_params[:program_id] || Program.find_by_name!(Lab::Metadata::LAB_PROGRAM_NAME).program_id
215
-
216
- Encounter.create!(
217
- patient_id: order_params[:patient_id],
218
- program_id:,
219
- type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
220
- encounter_datetime: order_params[:date] || Date.today,
221
- provider_id: order_params[:provider_id] || User.current.person.person_id
222
- )
225
+ encounter_id = order_params[:encounter_id] || order_params[:encounter]
226
+ patient_id = order_params[:patient_id] || order_params[:patient]
227
+ visit = order_params[:visit]
228
+
229
+ return Encounter.find(encounter_id) if order_params[:encounter] || order_params[:encounter_id]
230
+ raise StandardError, 'encounter_id|uuid or patient_id|uuid required' unless order_params[:patient]
231
+
232
+ encounter = Encounter.new
233
+ encounter.patient = Patient.find(patient_id)
234
+ encounter.encounter_type = EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME)
235
+ encounter.encounter_datetime = order_params[:date] || Date.today
236
+ encounter.visit = Visit.find_by_uuid(visit) if Encounter.column_names.include?('visit_id')
237
+ encounter.provider_id = User.current&.person.id if Encounter.column_names.include?('provider_id')
238
+ encounter.program_id = order_params[:program_id] if Encounter.column_names.include?('program_id') && order_params[:program_id].present?
239
+ encounter.save!
240
+ encounter.reload
223
241
  end
224
242
 
225
243
  def create_order(encounter, params)
226
244
  access_number = params[:accession_number] || next_accession_number(params[:date]&.to_date || Date.today)
227
245
  raise 'Accession Number cannot be blank' unless access_number.present?
228
246
  raise 'Accession cannot be this short' unless access_number.length > 6
229
- Lab::LabOrder.create!(
230
- order_type: OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME),
231
- concept_id: params.dig(:specimen, :concept_id) || unknown_concept_id,
232
- encounter_id: encounter.encounter_id,
233
- patient_id: encounter.patient_id,
234
- start_date: params[:date]&.to_date || Date.today,
235
- auto_expire_date: params[:end_date],
236
- accession_number: access_number,
237
- orderer: User.current&.user_id
238
- )
247
+
248
+ concept = params.dig(:specimen, :concept)
249
+ concept ||= params.dig(:specimen, :concept_id)
250
+
251
+ order_type = nil
252
+ order_type = OrderType.find_by_order_type_id!(params[:order_type_id])&.id if params[:order_type_id].present?
253
+
254
+ order = Lab::LabOrder.new
255
+ order.order_type_id = order_type || OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME).id
256
+ order.concept_id = Concept.find(concept)&.id
257
+ order.encounter_id = encounter.id
258
+ order.patient_id = encounter.patient.id
259
+ order.date_created = params[:date]&.to_date || Date.today if order.respond_to?(:date_created)
260
+ order.start_date = params[:date]&.to_date || Date.today if order.respond_to?(:start_date)
261
+ order.auto_expire_date = params[:end_date]
262
+ order.comment_to_fulfiller = params[:comment_to_fulfiller] if params[:comment_to_fulfiller]
263
+ order.accession_number = access_number
264
+ order.orderer = User.current&.user_id
265
+
266
+ order.save!
267
+
268
+ order.reload
239
269
  end
240
270
 
241
271
  def accession_number_exists?(accession_number)
@@ -262,16 +292,28 @@ module Lab
262
292
  )
263
293
  end
264
294
 
295
+ def add_comment_to_fulfiller(order, params)
296
+ create_order_observation(
297
+ order,
298
+ Lab::Metadata::COMMENT_TO_FULFILLER_CONCEPT_NAME,
299
+ params[:date],
300
+ value_text: params['comment_to_fulfiller']
301
+ )
302
+ end
303
+
265
304
  ##
266
305
  # Attach a reason for the order/test
267
306
  #
268
307
  # Examples of reasons include: Routine, Targeted, Confirmatory, Repeat, or Stat.
269
308
  def add_reason_for_test(order, params)
309
+ reason = params[:reason_for_test_id] || params[:reason_for_test]
310
+
311
+ reason = Concept.find(reason)
270
312
  create_order_observation(
271
313
  order,
272
314
  Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME,
273
315
  params[:date],
274
- value_coded: params[:reason_for_test_id]
316
+ value_coded: reason
275
317
  )
276
318
  end
277
319
 
@@ -288,14 +330,16 @@ module Lab
288
330
  )
289
331
  end
290
332
 
291
- def create_order_observation(order, concept_name, date, **)
333
+ def create_order_observation(order, concept_name, date, **values)
334
+ creator = User.find_by(username: 'lab_daemon')
335
+ User.current ||= creator
292
336
  Observation.create!(
293
337
  order:,
294
338
  encounter_id: order.encounter_id,
295
339
  person_id: order.patient_id,
296
340
  concept_id: ConceptName.find_by_name!(concept_name).concept_id,
297
341
  obs_datetime: date&.to_time || Time.now,
298
- **
342
+ **values
299
343
  )
300
344
  end
301
345
 
@@ -304,18 +348,20 @@ module Lab
304
348
  end
305
349
 
306
350
  def unknown_concept_id
307
- ConceptName.find_by_name!('Unknown').concept_id
351
+ ConceptName.find_by_name!('Unknown').concept
308
352
  end
309
353
 
310
- def update_reason_for_test(order, concept_id)
354
+ def update_reason_for_test(order, concept_id, force_update: false)
311
355
  raise InvalidParameterError, "Reason for test can't be blank" if concept_id.blank?
312
356
 
313
357
  return if order.reason_for_test&.value_coded == concept_id
314
358
 
315
- raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded
359
+ raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded && !force_update
316
360
 
317
361
  order.reason_for_test&.delete
318
- add_reason_for_test(order, date: order.start_date, reason_for_test_id: concept_id)
362
+ date = order.start_date if order.respond_to?(:start_date)
363
+ date ||= order.date_created
364
+ add_reason_for_test(order, date: date, reason_for_test_id: concept_id)
319
365
  end
320
366
 
321
367
  def void_order_status(order, concept)