mahis_emr_api_lab 1.2.0

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 (90) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +71 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/lab/application_controller.rb +6 -0
  6. data/app/controllers/lab/labels_controller.rb +17 -0
  7. data/app/controllers/lab/orders_controller.rb +78 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +20 -0
  10. data/app/controllers/lab/specimen_types_controller.rb +15 -0
  11. data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
  12. data/app/controllers/lab/test_types_controller.rb +15 -0
  13. data/app/controllers/lab/tests_controller.rb +25 -0
  14. data/app/controllers/lab/users_controller.rb +32 -0
  15. data/app/jobs/lab/application_job.rb +4 -0
  16. data/app/jobs/lab/push_order_job.rb +12 -0
  17. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  18. data/app/jobs/lab/void_order_job.rb +17 -0
  19. data/app/mailers/lab/application_mailer.rb +6 -0
  20. data/app/models/lab/application_record.rb +5 -0
  21. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  22. data/app/models/lab/lab_acknowledgement.rb +6 -0
  23. data/app/models/lab/lab_encounter.rb +7 -0
  24. data/app/models/lab/lab_order.rb +58 -0
  25. data/app/models/lab/lab_result.rb +31 -0
  26. data/app/models/lab/lab_test.rb +19 -0
  27. data/app/models/lab/lims_failed_import.rb +4 -0
  28. data/app/models/lab/lims_order_mapping.rb +10 -0
  29. data/app/models/lab/order_extension.rb +14 -0
  30. data/app/serializers/lab/lab_order_serializer.rb +56 -0
  31. data/app/serializers/lab/result_serializer.rb +36 -0
  32. data/app/serializers/lab/test_serializer.rb +52 -0
  33. data/app/services/lab/accession_number_service.rb +77 -0
  34. data/app/services/lab/acknowledgement_service.rb +47 -0
  35. data/app/services/lab/concepts_service.rb +82 -0
  36. data/app/services/lab/json_web_token_service.rb +20 -0
  37. data/app/services/lab/labelling_service/order_label.rb +106 -0
  38. data/app/services/lab/lims/acknowledgement_serializer.rb +29 -0
  39. data/app/services/lab/lims/acknowledgement_worker.rb +37 -0
  40. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  41. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  42. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  43. data/app/services/lab/lims/api/rest_api.rb +434 -0
  44. data/app/services/lab/lims/api/ws_api.rb +121 -0
  45. data/app/services/lab/lims/api_factory.rb +19 -0
  46. data/app/services/lab/lims/config.rb +105 -0
  47. data/app/services/lab/lims/exceptions.rb +11 -0
  48. data/app/services/lab/lims/migrator.rb +216 -0
  49. data/app/services/lab/lims/order_dto.rb +105 -0
  50. data/app/services/lab/lims/order_serializer.rb +251 -0
  51. data/app/services/lab/lims/pull_worker.rb +314 -0
  52. data/app/services/lab/lims/push_worker.rb +152 -0
  53. data/app/services/lab/lims/utils.rb +91 -0
  54. data/app/services/lab/lims/worker.rb +94 -0
  55. data/app/services/lab/metadata.rb +26 -0
  56. data/app/services/lab/notification_service.rb +72 -0
  57. data/app/services/lab/orders_search_service.rb +72 -0
  58. data/app/services/lab/orders_service.rb +330 -0
  59. data/app/services/lab/results_service.rb +166 -0
  60. data/app/services/lab/tests_service.rb +105 -0
  61. data/app/services/lab/user_service.rb +62 -0
  62. data/config/routes.rb +28 -0
  63. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  64. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  65. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  66. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  67. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  68. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  69. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  70. data/lib/auto12epl.rb +201 -0
  71. data/lib/couch_bum/couch_bum.rb +92 -0
  72. data/lib/generators/lab/install/USAGE +9 -0
  73. data/lib/generators/lab/install/install_generator.rb +19 -0
  74. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  75. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  76. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  77. data/lib/lab/engine.rb +13 -0
  78. data/lib/lab/version.rb +5 -0
  79. data/lib/logger_multiplexor.rb +38 -0
  80. data/lib/mahis_emr_api_lab.rb +6 -0
  81. data/lib/tasks/lab_tasks.rake +25 -0
  82. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  83. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  84. data/lib/tasks/loaders/data/tests.csv +161 -0
  85. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  86. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  87. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  88. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  89. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  90. metadata +331 -0
@@ -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
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # Search Lab orders.
5
+ module OrdersSearchService
6
+ class << self
7
+ def find_orders(filters)
8
+ extra_filters = pop_filters(filters, :date, :end_date, :status)
9
+
10
+ uuid = filters.delete(:patient)
11
+ patient = Patient.find(uuid) if uuid
12
+
13
+ filters.merge!(patient_id: patient.id) if patient
14
+
15
+ orders = Lab::LabOrder.prefetch_relationships
16
+ .where(filters)
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')
19
+
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])
22
+
23
+ orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
24
+ end
25
+
26
+ def find_orders_without_results(patient_id: nil)
27
+ results_query = Lab::LabResult.all
28
+ results_query = results_query.where(person_id: patient_id) if patient_id
29
+
30
+ query = Lab::LabOrder.where.not(order_id: results_query.select(:order_id))
31
+ query = query.where(patient_id: patient_id) if patient_id
32
+
33
+ query
34
+ end
35
+
36
+ def filter_orders_by_date(orders:, date: nil, end_date: nil)
37
+ date = date&.to_date
38
+ end_date = end_date&.to_date
39
+
40
+ return orders.where('start_date BETWEEN ? AND ?', date, end_date + 1.day) if date && end_date
41
+
42
+ return orders.where('start_date BETWEEN ? AND ?', date, date + 1.day) if date
43
+
44
+ return orders.where('start_date < ?', end_date + 1.day) if end_date
45
+
46
+ orders
47
+ end
48
+
49
+ def filter_orders_by_status(orders:, status: nil)
50
+ case status&.downcase
51
+ when 'ordered' then orders.where(concept_id: unknown_concept_id)
52
+ when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
53
+ else orders
54
+ end
55
+ end
56
+
57
+ def unknown_concept_id
58
+ ConceptName.find_by_name!('Unknown').concept_id
59
+ end
60
+
61
+ def pop_filters(params, *filters)
62
+ filters.each_with_object({}) do |filter, popped_params|
63
+ next unless params.key?(filter)
64
+
65
+ popped_params[filter.to_sym] = params.delete(filter)
66
+ end
67
+ end
68
+
69
+ def fetch_results(order); end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Manage lab orders.
6
+ #
7
+ # Lab orders are just ordinary openmrs orders with extra metadata that
8
+ # separates them from other orders. Lab orders have an order type of 'Lab'
9
+ # with the order's test type as the order's concept. The order's start
10
+ # date is the day the order is made. Additional information pertaining to
11
+ # the order is stored as observations that point to the order. The
12
+ # specimen types, requesting clinician, target lab, and reason for test
13
+ # are saved as observations to the order. Refer to method #order_test for
14
+ # more information.
15
+ module OrdersService
16
+ class << self
17
+ ##
18
+ # Create a lab order.
19
+ #
20
+ # Parameters schema:
21
+ #
22
+ # {
23
+ # encounter_id: {
24
+ # type: :integer,
25
+ # required: :false,
26
+ # description: 'Attach order to this if program_id and patient_id are not provided'
27
+ # },
28
+ # program_id: { type: :integer, required: false },
29
+ # patient_id: { type: :integer, required: false }
30
+ # specimen: { type: :object, properties: { concept_id: :integer }, required: %i[concept_id] },
31
+ # test_type_ids: {
32
+ # type: :array,
33
+ # items: {
34
+ # type: :object,
35
+ # properties: { concept_id: :integer },
36
+ # required: %i[concept_id]
37
+ # }
38
+ # },
39
+ # start_date: { type: :datetime }
40
+ # accession_number: { type: :string }
41
+ # target_lab: { type: :string },
42
+ # reason_for_test_id: { type: :integer },
43
+ # requesting_clinician: { type: :string }
44
+ # }
45
+ #
46
+ # encounter_id: is an ID of the encounter the lab order is to be created under
47
+ # test_type_id: is a concept_id of the name of test being ordered
48
+ # specimen_type_id: is a list of IDs for the specimens to be tested (can be ommited)
49
+ # target_lab: is the name of the lab where test will be carried out
50
+ # reason_for_test_id: is a concept_id for a (standard) reason of why the test is being carried out
51
+ # requesting_clinician: Name of the clinician requesting the test (defaults to current user)
52
+ def order_test(order_params)
53
+ Order.transaction do
54
+ encounter = find_encounter(order_params)
55
+ if order_params[:accession_number].present? && check_tracking_number(order_params[:accession_number])
56
+ raise 'Accession number already exists'
57
+ end
58
+
59
+ order = create_order(encounter, order_params)
60
+
61
+ Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
62
+
63
+ Lab::LabOrderSerializer.serialize_order(
64
+ order, requesting_clinician: add_requesting_clinician(order, order_params),
65
+ reason_for_test: add_reason_for_test(order, order_params),
66
+ target_lab: add_target_lab(order, order_params)
67
+ )
68
+ end
69
+ end
70
+
71
+ def update_order(order_id, params)
72
+ specimen_id = params.dig(:specimen, :concept_id)
73
+ raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id
74
+
75
+ order = Lab::LabOrder.find(order_id)
76
+ if order.concept_id != unknown_concept_id && !params[:force_update]&.casecmp?('true')
77
+ raise ::UnprocessableEntityError, "Can't change order specimen once set"
78
+ end
79
+
80
+ if specimen_id.to_i != order.concept_id
81
+ Rails.logger.debug("Updating order ##{order.order_id}")
82
+ order.update!(concept_id: specimen_id,
83
+ discontinued: true,
84
+ discontinued_by: User.current.user_id,
85
+ discontinued_date: params[:date]&.to_date || Time.now,
86
+ discontinued_reason_non_coded: 'Sample drawn/updated')
87
+ end
88
+
89
+ reason_for_test = params[:reason_for_test] || params[:reason_for_test_id]
90
+
91
+ if reason_for_test
92
+ Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
93
+ update_reason_for_test(order, Concept.find(reason_for_test)&.id)
94
+ end
95
+
96
+ Lab::LabOrderSerializer.serialize_order(order)
97
+ end
98
+
99
+ def void_order(order_id, reason)
100
+ order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab], tests: [:result])
101
+ .find(order_id)
102
+
103
+ order.requesting_clinician&.void(reason)
104
+ order.reason_for_test&.void(reason)
105
+ order.target_lab&.void(reason)
106
+
107
+ order.tests.each { |test| test.void(reason) }
108
+ order.void(reason)
109
+ end
110
+
111
+ def check_tracking_number(tracking_number)
112
+ accession_number_exists?(tracking_number) || nlims_accession_number_exists?(tracking_number)
113
+ end
114
+
115
+ def update_order_status(order_params)
116
+ # find the order
117
+ order = find_order(order_params['tracking_number'])
118
+ concept = ConceptName.find_by_name Lab::Metadata::LAB_ORDER_STATUS_CONCEPT_NAME
119
+ ActiveRecord::Base.transaction do
120
+ void_order_status(order, concept)
121
+ Observation.create!(
122
+ person_id: order.patient_id,
123
+ encounter_id: order.encounter_id,
124
+ concept_id: concept.concept_id,
125
+ order_id: order.id,
126
+ obs_datetime: order_params['status_time'] || Time.now,
127
+ value_text: order_params['status'],
128
+ creator: User.current.id
129
+ )
130
+ end
131
+ create_rejection_notification(order_params) if order_params['status'] == 'test-rejected'
132
+ end
133
+
134
+ def update_order_result(order_params)
135
+ order = find_order(order_params['tracking_number'])
136
+ order_dto = Lab::Lims::OrderSerializer.serialize_order(order)
137
+ patch_order_dto_with_lims_results!(order_dto, order_params['results'])
138
+ Lab::Lims::PullWorker.new(nil).process_order(order_dto)
139
+ end
140
+
141
+ private
142
+
143
+ def create_rejection_notification(order_params)
144
+ order = find_order order_params['tracking_number']
145
+ data = { 'type': 'LIMS',
146
+ 'accession_number': order&.accession_number,
147
+ 'order_date': order&.start_date,
148
+ 'arv_number': find_arv_number(order.patient_id),
149
+ 'patient_id': result.person_id,
150
+ 'ordered_by': order&.provider&.person&.name,
151
+ 'rejection_reason': order_params['comments']
152
+ }.as_json
153
+ NotificationService.new.create_notification('LIMS', data)
154
+ end
155
+
156
+ def find_arv_number(patient_id)
157
+ PatientIdentifier.joins(:type)
158
+ .merge(PatientIdentifierType.where(name: 'ARV Number'))
159
+ .where(patient_id: patient_id)
160
+ .first&.identifier
161
+ end
162
+
163
+ def find_order(tracking_number)
164
+ Lab::LabOrder.find_by_accession_number(tracking_number)
165
+ end
166
+
167
+ def patch_order_dto_with_lims_results!(order_dto, results)
168
+ order_dto.merge!(
169
+ '_id' => order_dto[:tracking_number],
170
+ '_rev' => 0,
171
+ 'test_results' => results.each_with_object({}) do |result, formatted_results|
172
+ test_name, measures = result
173
+ result_date = measures.delete('result_date')
174
+
175
+ formatted_results[test_name] = {
176
+ results: measures.each_with_object({}) do |measure, processed_measures|
177
+ processed_measures[measure[0]] = { 'result_value' => measure[1] }
178
+ end,
179
+ result_date: result_date,
180
+ result_entered_by: {}
181
+ }
182
+ end
183
+ )
184
+ end
185
+
186
+ ##
187
+ # Extract an encounter from the given parameters.
188
+ #
189
+ # Uses an encounter_id to retrieve an encounter if provided else
190
+ # a 'Lab' encounter is created using the provided program_id and
191
+ # patient_id.
192
+ def find_encounter(order_params)
193
+ encounter_id = order_params[:encounter_id] || order_params[:encounter]
194
+ patient_id = order_params[:patient_id] || order_params[:patient]
195
+ visit = order_params[:visit]
196
+
197
+ return Encounter.find(encounter_id) if order_params[:encounter] || order_params[:encounter_id]
198
+ raise StandardError, 'encounter_id|uuid or patient_id|uuid required' unless order_params[:patient]
199
+
200
+
201
+ encounter = Encounter.new
202
+ encounter.patient = Patient.find(patient_id)
203
+ encounter.encounter_type = EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME)
204
+ encounter.encounter_datetime = order_params[:date] || Date.today
205
+ encounter.visit = Visit.find_by_uuid(visit) if Encounter.column_names.include?('visit_id')
206
+ encounter.provider_id = User.current&.person.id if Encounter.column_names.include?('provider_id')
207
+
208
+ encounter.save!
209
+
210
+ encounter.reload
211
+ end
212
+
213
+ def create_order(encounter, params)
214
+ access_number = params[:accession_number] || next_accession_number(params[:date]&.to_date || Date.today)
215
+ raise 'Accession Number cannot be blank' unless access_number.present?
216
+ raise 'Accession cannot be this short' unless access_number.length > 6
217
+
218
+ concept = params.dig(:specimen, :concept)
219
+ concept ||= params.dig(:specimen, :concept_id)
220
+
221
+ order = Lab::LabOrder.new
222
+ order.order_type_id = OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME).id
223
+ order.concept_id = Concept.find(concept)&.id
224
+ order.encounter_id = encounter.id
225
+ order.patient_id = encounter.patient.id
226
+ order.date_created = params[:date]&.to_date || Date.today if order.respond_to?(:date_created)
227
+ order.start_date = params[:date]&.to_date || Date.today if order.respond_to?(:start_date)
228
+ order.auto_expire_date = params[:end_date]
229
+ order.accession_number = access_number
230
+ order.orderer = User.current&.user_id
231
+
232
+ order.save!
233
+
234
+ order.reload
235
+ end
236
+
237
+ def accession_number_exists?(accession_number)
238
+ Lab::LabOrder.where(accession_number: accession_number).exists?
239
+ end
240
+
241
+ def nlims_accession_number_exists?(accession_number)
242
+ config = YAML.load_file('config/application.yml')
243
+ return false unless config['lims_api']
244
+
245
+ # fetch from the rest api and check if it exists
246
+ lims_api = Lab::Lims::ApiFactory.create_api
247
+ lims_api.verify_tracking_number(accession_number).present?
248
+ end
249
+
250
+ ##
251
+ # Attach the requesting clinician to an order
252
+ def add_requesting_clinician(order, params)
253
+ create_order_observation(
254
+ order,
255
+ Lab::Metadata::REQUESTING_CLINICIAN_CONCEPT_NAME,
256
+ params[:date],
257
+ value_text: params['requesting_clinician']
258
+ )
259
+ end
260
+
261
+ ##
262
+ # Attach a reason for the order/test
263
+ #
264
+ # Examples of reasons include: Routine, Targeted, Confirmatory, Repeat, or Stat.
265
+ def add_reason_for_test(order, params)
266
+
267
+ reason = params[:reason_for_test_id] || params[:reason_for_test]
268
+
269
+ reason = Concept.find(reason)
270
+ create_order_observation(
271
+ order,
272
+ Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME,
273
+ params[:date],
274
+ value_coded: reason
275
+ )
276
+ end
277
+
278
+ ##
279
+ # Attach the lab where the test is going to get carried out.
280
+ def add_target_lab(order, params)
281
+ return nil unless params['target_lab']
282
+
283
+ create_order_observation(
284
+ order,
285
+ Lab::Metadata::TARGET_LAB_CONCEPT_NAME,
286
+ params[:date],
287
+ value_text: params['target_lab']
288
+ )
289
+ end
290
+
291
+ def create_order_observation(order, concept_name, date, **values)
292
+ Observation.create!(
293
+ order: order,
294
+ encounter_id: order.encounter_id,
295
+ person_id: order.patient_id,
296
+ concept_id: ConceptName.find_by_name!(concept_name).concept_id,
297
+ obs_datetime: date&.to_time || Time.now,
298
+ **values
299
+ )
300
+ end
301
+
302
+ def next_accession_number(date = nil)
303
+ Lab::AccessionNumberService.next_accession_number(date)
304
+ end
305
+
306
+ def unknown_concept_id
307
+ ConceptName.find_by_name!('Unknown').concept
308
+ end
309
+
310
+ def update_reason_for_test(order, concept_id)
311
+ raise InvalidParameterError, "Reason for test can't be blank" if concept_id.blank?
312
+
313
+ return if order.reason_for_test&.value_coded == concept_id
314
+
315
+ raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded
316
+
317
+ order.reason_for_test&.delete
318
+ date = order.start_date if order.respond_to?(:start_date)
319
+ date ||= order.date_created
320
+ add_reason_for_test(order, date: date, reason_for_test_id: concept_id)
321
+ end
322
+
323
+ def void_order_status(order, concept)
324
+ Observation.where(order_id: order.id, concept_id: concept.concept_id).each do |obs|
325
+ obs.void('New Status Received from LIMS')
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module ResultsService
5
+ class << self
6
+ ##
7
+ # Attach results to a test
8
+ #
9
+ # Params:
10
+ # test_id: The tests id (maps to obs_id of the test's observation in OpenMRS)
11
+ # params: A hash comprising the following fields
12
+ # - encounter_id: Encounter to create result under (can be ommitted but provider_id has to specified)
13
+ # - provider_id: Specify a provider for an encounter the result is going to be created under
14
+ # - date: Retrospective date when the result was received (can be ommitted, defaults to today)
15
+ # - measures: An array of measures. A measure is an object of the following structure
16
+ # - indicator: An object that has a concept_id field (concept_id of the indicator)
17
+ # - value_type: An enum that's limited to 'numeric', 'boolean', 'text', and 'coded'
18
+ # result_enter_by: A string that specifies who created the result
19
+ def create_results(test_id, params, result_enter_by = 'LIMS')
20
+ serializer = {}
21
+ results_obs = {}
22
+ ActiveRecord::Base.transaction do
23
+ test = Lab::LabTest.find(test_id) rescue nil
24
+ test = Lab::LabTest.find_by_uuid(test_id) if test.blank?
25
+ encounter = find_encounter(test, encounter_id: params[:encounter_id],
26
+ encounter_uuid: params[:encounter],
27
+ date: params[:date]&.to_date,
28
+ provider_id: params[:provider_id])
29
+
30
+ results_obs = create_results_obs(encounter, test, params[:date], params[:comments])
31
+ params[:measures].map { |measure| add_measure_to_results(results_obs, measure, params[:date]) }
32
+ OrderExtension.create!(creator: User.current, value: result_enter_by, order_id: results_obs.order_id,
33
+ date_created: Time.now)
34
+
35
+ serializer = Lab::ResultSerializer.serialize(results_obs)
36
+ end
37
+ process_acknowledgement(results_obs, result_enter_by)
38
+ precess_notification_message(results_obs, serializer, result_enter_by)
39
+ Rails.logger.info("Lab::ResultsService: Result created for test #{test_id} #{serializer}")
40
+ serializer
41
+ end
42
+
43
+ private
44
+
45
+ def precess_notification_message(result, values, result_enter_by)
46
+ order = Order.find(result.order_id)
47
+ data = { Type: result_enter_by,
48
+ 'Test type': ConceptName.find_by(concept_id: result.test.value_coded)&.name,
49
+ 'Accession number': order&.accession_number,
50
+ 'Orde date': Order.columns.include?('start_date') ? order.start_date : order.date_created,
51
+ 'ARV-Number': find_arv_number(result.person_id),
52
+ PatientID: result.person_id,
53
+ 'Ordered By': Order.columns.include?('provider_id') ? order&.provider&.person&.name : Person.find(order.creator)&.name,
54
+ Result: values }.as_json
55
+ NotificationService.new.create_notification(result_enter_by, data)
56
+ end
57
+
58
+ def process_acknowledgement(results, results_enter_by)
59
+ Lab::AcknowledgementService.create_acknowledgement({ order_id: results.order_id, test: results.test.id,
60
+ date_received: Time.now,
61
+ entered_by: results_enter_by })
62
+ end
63
+
64
+ def find_arv_number(patient_id)
65
+ PatientIdentifier.joins(:type)
66
+ .merge(PatientIdentifierType.where(name: 'ARV Number'))
67
+ .where(patient_id: patient_id)
68
+ .first&.identifier
69
+ end
70
+
71
+ def find_encounter(test, encounter_id: nil, encounter_uuid: nil, date: nil, provider_id: nil)
72
+ return Encounter.find(encounter_id) if encounter_id
73
+ return Encounter.find_by_uuid(encounter_uuid) if encounter_uuid
74
+
75
+ encounter = Encounter.new
76
+ encounter.patient_id = test.person_id
77
+ encounter.program_id = test.encounter.program_id if Encounter.column_names.include?('program_id')
78
+ encounter.visit_id = test.encounter.visit_id if Encounter.column_names.include?('visit_id')
79
+ encounter.encounter_type = EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME)
80
+ encounter.encounter_datetime = date || Date.today
81
+ encounter.provider_id = provider_id || User.current.user_id if Encounter.column_names.include?('provider_id')
82
+
83
+ encounter.save!
84
+
85
+ encounter
86
+ end
87
+
88
+ # Creates the parent observation for results to which the different measures are attached
89
+ def create_results_obs(encounter, test, date, comments = nil)
90
+ void_existing_results_obs(encounter, test)
91
+ Lab::LabResult.create!(
92
+ person_id: encounter.patient_id,
93
+ encounter_id: encounter.encounter_id,
94
+ concept_id: test_result_concept.concept_id,
95
+ order_id: test.order_id,
96
+ obs_group_id: test.obs_id,
97
+ obs_datetime: date&.to_datetime || DateTime.now,
98
+ comments: comments
99
+ )
100
+ end
101
+
102
+ def void_existing_results_obs(encounter, test)
103
+ result = Lab::LabResult.find_by(person_id: encounter.patient_id,
104
+ concept_id: test_result_concept.concept_id,
105
+ obs_group_id: test.obs_id)
106
+ return unless result
107
+
108
+ OrderExtension.find_by(order_id: result.order_id)&.void("Updated/overwritten by #{User.current.username}")
109
+ result.measures.map { |child_obs| child_obs.void("Updated/overwritten by #{User.current.username}") }
110
+ result.void("Updated/overwritten by #{User.current.username}")
111
+ end
112
+
113
+ def test_result_concept
114
+ ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
115
+ end
116
+
117
+ def add_measure_to_results(results_obs, params, date)
118
+ validate_measure_params(params)
119
+
120
+ concept_id = params[:indicator][:concept_id] || Concept.find_concept_by_uuid(params.dig(:indicator, :concept))&.id
121
+
122
+ Observation.create!(
123
+ person_id: results_obs.person_id,
124
+ encounter_id: results_obs.encounter_id,
125
+ order_id: results_obs.order_id,
126
+ concept_id: concept_id,
127
+ obs_group_id: results_obs.obs_id,
128
+ obs_datetime: date&.to_datetime || DateTime.now,
129
+ **make_measure_value(params)
130
+ )
131
+ end
132
+
133
+ def validate_measure_params(params)
134
+ raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
135
+
136
+ if params[:indicator]&.[](:concept_id).blank? && params[:indicator]&.[](:concept).blank?
137
+ raise InvalidParameterError, 'measures.indicator.concept_id or concept is required'
138
+ end
139
+
140
+ params
141
+ end
142
+
143
+ # Converts user provided measure values to observation_values
144
+ def make_measure_value(params)
145
+ obs_value = { value_modifier: params[:value_modifier] }
146
+ value_type = params[:value_type] || 'text'
147
+
148
+ case value_type.downcase
149
+ when 'numeric' then obs_value.merge(value_numeric: params[:value])
150
+ when 'boolean' then obs_value.merge(value_boolean: parse_boolen_value(params[:value]))
151
+ when 'coded' then obs_value.merge(value_coded: params[:value]) # Should we be collecting value_name_coded_id?
152
+ when 'text' then obs_value.merge(value_text: params[:value])
153
+ else raise InvalidParameterError, "Invalid value_type: #{params[:value_type]}"
154
+ end
155
+ end
156
+
157
+ def parse_boolen_value(string)
158
+ case string.downcase
159
+ when 'true' then true
160
+ when 'false' then false
161
+ else raise InvalidParameterError, "Invalid boolean value: #{string}"
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end