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,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require 'parallel'
5
+
6
+ require 'couch_bum/couch_bum'
7
+ require 'logger_multiplexor'
8
+
9
+ require 'concept'
10
+ require 'concept_name'
11
+ require 'drug_order'
12
+ require 'encounter'
13
+ require 'encounter_type'
14
+ require 'observation'
15
+ require 'order'
16
+ require 'order_type'
17
+ require 'patient'
18
+ require 'patient_identifier'
19
+ require 'patient_identifier_type'
20
+ require 'person'
21
+ require 'person_name'
22
+ require 'program'
23
+ require 'user'
24
+
25
+ require 'lab/lab_encounter'
26
+ require 'lab/lab_order'
27
+ require 'lab/lab_result'
28
+ require 'lab/lab_test'
29
+ require 'lab/lims_order_mapping'
30
+ require 'lab/lims_failed_import'
31
+
32
+ require_relative './api/couchdb_api'
33
+ require_relative './config'
34
+ require_relative './pull_worker'
35
+ require_relative './utils'
36
+
37
+ require_relative '../orders_service'
38
+ require_relative '../results_service'
39
+ require_relative '../tests_service'
40
+ require_relative '../../../serializers/lab/lab_order_serializer'
41
+ require_relative '../../../serializers/lab/result_serializer'
42
+ require_relative '../../../serializers/lab/test_serializer'
43
+
44
+ require_relative 'order_dto'
45
+ require_relative 'utils'
46
+
47
+ module Lab
48
+ module Lims
49
+ ##
50
+ # Tools for performing a bulk import of data from LIMS' databases to local OpenMRS database.
51
+ #
52
+ # Migration sources supported:
53
+ # - MySQL
54
+ # - CouchDB
55
+ #
56
+ # The sources above can be changed by setting the environment various MIGRATION_SOURCE to
57
+ # either mysql or couchdb.
58
+ module Migrator
59
+ MAX_THREADS = ENV.fetch('MIGRATION_WORKERS', 6).to_i
60
+
61
+ ##
62
+ # A Lab::Lims::Api object that supports crawling of a LIMS CouchDB instance.
63
+ class CouchDbMigratorApi < Lab::Lims::Api::CouchDbApi
64
+ def initialize(*args, processes: 1, on_merge_processes: nil, **kwargs)
65
+ super(*args, **kwargs)
66
+
67
+ @processes = processes
68
+ @on_merge_processes = on_merge_processes
69
+ end
70
+
71
+ def consume_orders(from: nil, **_kwargs)
72
+ limit = 25_000
73
+
74
+ loop do
75
+ on_merge_processes = ->(_item, index, _result) { @on_merge_processes&.call(from + index) }
76
+ processes = @processes > 1 ? @processes : 0
77
+
78
+ orders = read_orders(from, limit)
79
+ break if orders.empty?
80
+
81
+ Parallel.each(orders, in_processes: processes, finish: on_merge_processes) do |row|
82
+ next unless row['doc']['type']&.casecmp?('Order')
83
+
84
+ User.current = Utils.lab_user
85
+ yield OrderDTO.new(row['doc']), OpenStruct.new(last_seq: (from || 0) + limit, current_seq: from)
86
+ end
87
+
88
+ from += orders.size
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def read_orders(from, batch_size)
95
+ start_key_param = from ? "&skip=#{from}" : ''
96
+ url = "_all_docs?include_docs=true&limit=#{batch_size}#{start_key_param}"
97
+
98
+ Rails.logger.debug("#{CouchDbMigratorApi}: Pulling orders from LIMS CouchDB: #{url}")
99
+ response = bum.couch_rest :get, url
100
+
101
+ response['rows']
102
+ end
103
+ end
104
+
105
+ ##
106
+ # Extends the PullWorker to provide pause/resume capabilities.
107
+ #
108
+ # Migrations can be take a long time to complete, in cases where something
109
+ # went wrong you wouldn't to start all over. This worker thus saves
110
+ # progress and allows for the process to continue from whether it stopped.
111
+ class MigrationWorker < PullWorker
112
+ LOG_FILE_PATH = Utils::LIMS_LOG_PATH.join('migration-last-id.dat')
113
+
114
+ attr_reader :rejections
115
+
116
+ def initialize(api_class)
117
+ api = api_class.new(processes: MAX_THREADS, on_merge_processes: method(:save_seq))
118
+ super(api)
119
+ end
120
+
121
+ def last_seq
122
+ return 0 unless File.exist?(LOG_FILE_PATH)
123
+
124
+ File.open(LOG_FILE_PATH, File::RDONLY) do |file|
125
+ last_seq = file.read&.strip
126
+ return last_seq.blank? ? nil : last_seq&.to_i
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def save_seq(last_seq)
133
+ File.open(LOG_FILE_PATH, File::WRONLY | File::CREAT, 0o644) do |file|
134
+ Rails.logger.debug("Process ##{Parallel.worker_number}: Saving last seq: #{last_seq}")
135
+ file.flock(File::LOCK_EX)
136
+ file.write(last_seq.to_s)
137
+ file.flush
138
+ end
139
+ end
140
+
141
+ def order_rejected(order_dto, reason)
142
+ @rejections ||= []
143
+
144
+ @rejections << OpenStruct.new(order: order_dto, reason: reason)
145
+ end
146
+ end
147
+
148
+ def self.save_csv(filename, rows:, headers: nil)
149
+ CSV.open(filename, File::WRONLY | File::CREAT) do |csv|
150
+ csv << headers if headers
151
+ rows.each { |row| csv << row }
152
+ end
153
+ end
154
+
155
+ # NOTE: LIMS_LOG_PATH below is defined in worker.rb
156
+ MIGRATION_REJECTIONS_CSV_PATH = Utils::LIMS_LOG_PATH.join('migration-rejections.csv')
157
+
158
+ def self.export_rejections(rejections)
159
+ headers = ['doc_id', 'Accession number', 'NHID', 'First name', 'Last name', 'Reason']
160
+ rows = (rejections || []).map do |rejection|
161
+ [
162
+ rejection.order[:_id],
163
+ rejection.order[:tracking_number],
164
+ rejection.order[:patient][:id],
165
+ rejection.order[:patient][:first_name],
166
+ rejection.order[:patient][:last_name],
167
+ rejection.reason
168
+ ]
169
+ end
170
+
171
+ save_csv(MIGRATION_REJECTIONS_CSV_PATH, headers: headers, rows: rows)
172
+ end
173
+
174
+ MIGRATION_FAILURES_CSV_PATH = Utils::LIMS_LOG_PATH.join('migration-failures.csv')
175
+
176
+ def self.export_failures
177
+ headers = ['doc_id', 'Accession number', 'NHID', 'Reason', 'Difference']
178
+ rows = Lab::LimsFailedImport.all.map do |failure|
179
+ [
180
+ failure.lims_id,
181
+ failure.tracking_number,
182
+ failure.patient_nhid,
183
+ failure.reason,
184
+ failure.diff
185
+ ]
186
+ end
187
+
188
+ save_csv(MIGRATION_FAILURES_CSV_PATH, headers: headers, rows: rows)
189
+ end
190
+
191
+ MIGRATION_LOG_PATH = Utils::LIMS_LOG_PATH.join('migration.log')
192
+
193
+ def self.start_migration
194
+ Dir.mkdir(Utils::LIMS_LOG_PATH) unless File.exist?(Utils::LIMS_LOG_PATH)
195
+
196
+ logger = LoggerMultiplexor.new(Logger.new($stdout), MIGRATION_LOG_PATH)
197
+ logger.level = :debug
198
+ Rails.logger = logger
199
+ ActiveRecord::Base.logger = logger
200
+ # CouchBum.logger = logger
201
+
202
+ api_class = case ENV.fetch('MIGRATION_SOURCE', 'couchdb').downcase
203
+ when 'couchdb' then CouchDbMigratorApi
204
+ when 'mysql' then Api::MysqlApi
205
+ else raise "Invalid MIGRATION_SOURCE: #{ENV['MIGRATION_SOURCE']}"
206
+ end
207
+
208
+ worker = MigrationWorker.new(api_class)
209
+ worker.pull_orders(batch_size: 10_000)
210
+ ensure
211
+ worker && export_rejections(worker.rejections)
212
+ export_failures
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './exceptions'
4
+
5
+ module Lab
6
+ module Lims
7
+ ##
8
+ # LIMS' Data Transfer Object for orders
9
+ class OrderDTO < ActiveSupport::HashWithIndifferentAccess
10
+ include Utils
11
+
12
+ ##
13
+ # Unpacks a LIMS order into an object that OrdersService can handle
14
+ def to_order_service_params(patient_id:)
15
+ ActiveSupport::HashWithIndifferentAccess.new(
16
+ program_id: lab_program.program_id,
17
+ accession_number: self['tracking_number'],
18
+ patient_id: patient_id,
19
+ specimen: { concept_id: specimen_type_id },
20
+ tests: self['tests']&.map { |test| { concept_id: test_type_id(test) } },
21
+ requesting_clinician: requesting_clinician,
22
+ date: start_date,
23
+ target_lab: facility_name(self['receiving_facility']),
24
+ order_location: facility_name(self['sending_facility']),
25
+ reason_for_test_id: reason_for_test
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ # Translates a LIMS specimen name to an OpenMRS concept_id
32
+ def specimen_type_id
33
+ lims_specimen_name = self['sample_type']&.strip&.downcase
34
+
35
+ if lims_specimen_name.nil? || %w[specimen_not_collected not_assigned not_specified].include?(lims_specimen_name)
36
+ return ConceptName.select(:concept_id).find_by_name!('Unknown').concept_id
37
+ end
38
+
39
+ concept = Utils.find_concept_by_name(lims_specimen_name)
40
+ return concept.concept_id if concept
41
+
42
+ raise UnknownSpecimenType, "Unknown specimen name: #{lims_specimen_name}"
43
+ end
44
+
45
+ # Translates a LIMS test type name to an OpenMRS concept_id
46
+ def test_type_id(lims_test_name)
47
+ lims_test_name = Utils.translate_test_name(lims_test_name)
48
+ concept = Utils.find_concept_by_name(lims_test_name)
49
+ return concept.concept_id if concept
50
+
51
+ raise UnknownTestType, "Unknown test type: #{lims_test_name}"
52
+ end
53
+
54
+ # Extract requesting clinician name from LIMS
55
+ def requesting_clinician
56
+ return 'Unknown' unless self['who_order_test']
57
+
58
+ # TODO: Extend requesting clinician to an obs tree having extra parameters
59
+ # like phone number and ID to closely match the lims user.
60
+ first_name = self['who_order_test']['first_name'] || ''
61
+ last_name = self['who_order_test']['last_name'] || ''
62
+
63
+ if first_name.blank? && last_name.blank?
64
+ logger.warn('Missing requesting clinician name')
65
+ return ''
66
+ end
67
+
68
+ "#{first_name} #{last_name}"
69
+ end
70
+
71
+ def start_date
72
+ raise LimsException, 'Order missing created date' if self['date_created'].blank?
73
+
74
+ Utils.parse_date(self['date_created'])
75
+ end
76
+
77
+ # Parses a LIMS facility name
78
+ def facility_name(lims_target_lab)
79
+ return 'Unknown' if lims_target_lab == 'not_assigned'
80
+
81
+ lims_target_lab
82
+ end
83
+
84
+ # Translates a LIMS sample priority to a concept_id
85
+ def reason_for_test
86
+ return nil unless self['priority']
87
+
88
+ name = case self['priority']
89
+ when %r{Reapet / Missing}i then 'Repeat / Missing'
90
+ else self['priority']
91
+ end
92
+
93
+ ConceptName.find_by_name!(name).concept_id
94
+ end
95
+
96
+ def lab_program
97
+ Program.find_by_name!(Lab::Metadata::LAB_PROGRAM_NAME)
98
+ end
99
+
100
+ def unknown_concept
101
+ ConceptName.find_by_name!('Unknown')
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './config'
4
+ require_relative './order_dto'
5
+ require_relative './utils'
6
+
7
+ module Lab
8
+ module Lims
9
+ ##
10
+ # Serializes a LabOrder into a LIMS OrderDTO.
11
+ module OrderSerializer
12
+ class << self
13
+ include Utils
14
+
15
+ def serialize_order(order)
16
+ serialized_order = Lims::Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
17
+ Lims::OrderDTO.new(
18
+ _id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
19
+ tracking_number: serialized_order.accession_number,
20
+ sending_facility: current_facility_name,
21
+ receiving_facility: serialized_order.target_lab,
22
+ tests: serialized_order.tests.map { |test| format_test_name(test.name) },
23
+ patient: format_patient(serialized_order.patient_id),
24
+ order_location: format_order_location(serialized_order.encounter_id),
25
+ sample_type: format_sample_type(serialized_order.specimen.name),
26
+ sample_status: format_sample_status(serialized_order.specimen.name),
27
+ sample_statuses: format_sample_status_trail(order),
28
+ test_statuses: format_test_status_trail(order),
29
+ who_order_test: format_orderer(order),
30
+ districy: current_district, # yes districy [sic]...
31
+ priority: format_sample_priority(serialized_order.reason_for_test.name),
32
+ date_created: serialized_order.order_date,
33
+ test_results: format_test_results(serialized_order),
34
+ type: 'Order'
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def format_order_location(encounter_id)
41
+ location_id = Encounter.select(:location_id).where(encounter_id: encounter_id)
42
+ location = Location.select(:name)
43
+ .where(location_id: location_id)
44
+ .first
45
+
46
+ location&.name
47
+ end
48
+
49
+ # Format patient into a structure that LIMS expects
50
+ def format_patient(patient_id)
51
+ person = Person.find(patient_id)
52
+ name = PersonName.find_by_person_id(patient_id)
53
+ national_id = PatientIdentifier.joins(:type)
54
+ .merge(PatientIdentifierType.where(name: 'National ID'))
55
+ .where(patient_id: patient_id)
56
+ .first
57
+ phone_number = PersonAttribute.joins(:type)
58
+ .merge(PersonAttributeType.where(name: 'Cell phone Number'))
59
+ .where(person_id: patient_id)
60
+ .first
61
+
62
+ {
63
+ first_name: name&.given_name,
64
+ last_name: name&.family_name,
65
+ id: national_id&.identifier,
66
+ arv_number: find_arv_number(patient_id),
67
+ art_regimen: find_current_regimen(patient_id),
68
+ art_start_date: find_art_start_date(patient_id),
69
+ dob: person.birthdate,
70
+ phone_number: phone_number&.value || 'Unknown',
71
+ gender: person.gender,
72
+ email: nil
73
+ }
74
+ end
75
+
76
+ def find_current_regimen(patient_id)
77
+ regimen_data = ActiveRecord::Base.connection.select_one <<~SQL
78
+ SELECT patient_current_regimen(#{patient_id}, current_date()) regimen
79
+ SQL
80
+ return nil if regimen_data.blank?
81
+
82
+ regimen_data['regimen']
83
+ rescue StandardError
84
+ nil
85
+ end
86
+
87
+ def find_arv_number(patient_id)
88
+ PatientIdentifier.joins(:type)
89
+ .merge(PatientIdentifierType.where(name: 'ARV Number'))
90
+ .where(patient_id: patient_id)
91
+ .first&.identifier
92
+ end
93
+
94
+ def find_art_start_date(patient_id)
95
+ start_date = ActiveRecord::Base.connection.select_one <<~SQL
96
+ SELECT date_antiretrovirals_started(#{patient_id}, current_date()) AS earliest_date
97
+ SQL
98
+ return nil if start_date.blank?
99
+
100
+ start_date['earliest_date']
101
+ rescue StandardError
102
+ nil
103
+ end
104
+
105
+ def format_sample_type(name)
106
+ return 'not_specified' if name.casecmp?('Unknown')
107
+
108
+ return 'CSF' if name.casecmp?('Cerebrospinal Fluid')
109
+
110
+ name.titleize
111
+ end
112
+
113
+ def format_sample_status(name)
114
+ name.casecmp?('Unknown') ? 'specimen_not_collected' : 'specimen_collected'
115
+ end
116
+
117
+ def format_sample_status_trail(order)
118
+ return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
119
+
120
+ user = User.find(order.creator)
121
+ user = User.find(order.discontinued_by) if Order.columns_hash.key?('discontinued_by') && user.blank?
122
+
123
+ drawn_by = PersonName.find_by_person_id(user.user_id)
124
+ drawn_date = order.discontinued_date || order.start_date if ['discontinued_date', 'start_date'].all? { |column| order.respond_to?(column) }
125
+ drawn_date ||= order.date_created
126
+
127
+ [
128
+ drawn_date.strftime('%Y%m%d%H%M%S') => {
129
+ 'status' => 'Drawn',
130
+ 'updated_by' => {
131
+ 'first_name' => drawn_by&.given_name || user.username,
132
+ 'last_name' => drawn_by&.family_name,
133
+ 'phone_number' => nil,
134
+ 'id' => user.username
135
+ }
136
+ }
137
+ ]
138
+ end
139
+
140
+ def format_test_status_trail(order)
141
+ tests = order.voided ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
142
+
143
+ tests.each_with_object({}) do |test, trail|
144
+ test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
145
+
146
+ current_test_trail = trail[test_name] = {}
147
+
148
+ current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
149
+ status: 'Drawn',
150
+ updated_by: find_user(test.creator)
151
+ }
152
+
153
+ unless test.voided.zero?
154
+ current_test_trail[test.date_voided.strftime('%Y%m%d%H%M%S')] = {
155
+ status: 'Voided',
156
+ updated_by: find_user(test.voided_by)
157
+ }
158
+ end
159
+
160
+ next unless test.result
161
+
162
+ current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
163
+ status: 'Verified',
164
+ updated_by: find_user(test.result.creator)
165
+ }
166
+ end
167
+ end
168
+
169
+ def format_orderer(order)
170
+ find_user(order.creator)
171
+ end
172
+
173
+ def format_test_results(order)
174
+ order.tests&.each_with_object({}) do |test, results|
175
+ next if test.result.nil? || test.result.empty?
176
+
177
+ test_creator = User.find(Observation.find(test.result.first.id).creator)
178
+ test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
179
+
180
+ results[format_test_name(test.name)] = {
181
+ results: test.result.each_with_object({}) do |measure, measures|
182
+ measures[format_test_name(measure.indicator.name)] = {
183
+ result_value: "#{measure.value_modifier}#{measure.value}"
184
+ }
185
+ end,
186
+ result_date: test.result.first&.date,
187
+ result_entered_by: {
188
+ first_name: test_creator_name&.given_name,
189
+ last_name: test_creator_name&.family_name,
190
+ id: test_creator.username
191
+ }
192
+ }
193
+ end
194
+ end
195
+
196
+ def format_test_name(test_name)
197
+ return 'Viral Load' if test_name.casecmp?('HIV Viral load')
198
+
199
+ return 'TB' if test_name.casecmp?('TB Program')
200
+
201
+ test_name.titleize
202
+ end
203
+
204
+ def format_sample_priority(priority)
205
+ return 'Routine' if priority&.casecmp?('Medical examination, routine')
206
+
207
+ priority&.titleize
208
+ end
209
+
210
+ def current_health_center
211
+ health_center = Location.current_health_center
212
+ raise 'Current health center not set' unless health_center
213
+
214
+ health_center
215
+ end
216
+
217
+ def current_district
218
+ district = current_health_center.city_village\
219
+ || current_health_center.parent&.name\
220
+ || GlobalProperty.find_by_property('current_health_center_district')&.property_value
221
+
222
+ return district if district
223
+
224
+ GlobalProperty.create(property: 'current_health_center_district',
225
+ property_value: Lims::Config.application['district'],
226
+ uuid: SecureRandom.uuid)
227
+
228
+ Config.application['district']
229
+ end
230
+
231
+ def current_facility_name
232
+ current_health_center.name
233
+ end
234
+
235
+ def find_user(user_id)
236
+ user = User.find(user_id)
237
+ person_name = PersonName.find_by(person_id: user.person_id)
238
+ phone_number = PersonAttribute.find_by(type: PersonAttributeType.where(name: 'Cell phone number'),
239
+ person_id: user.person_id)
240
+
241
+ {
242
+ first_name: person_name&.given_name,
243
+ last_name: person_name&.family_name,
244
+ phone_number: phone_number&.value,
245
+ id: user.username
246
+ }
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end