mahis_his_emr_api_lab 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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/his_emr_api_lab.rb +5 -0
  78. data/lib/lab/engine.rb +15 -0
  79. data/lib/lab/version.rb +5 -0
  80. data/lib/logger_multiplexor.rb +38 -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