his_emr_api_lab 1.1.22 → 1.1.23

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 (81) hide show
  1. checksums.yaml +4 -4
  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 +38 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +19 -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 +26 -0
  14. data/app/jobs/lab/application_job.rb +4 -0
  15. data/app/jobs/lab/push_order_job.rb +12 -0
  16. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  17. data/app/jobs/lab/void_order_job.rb +17 -0
  18. data/app/mailers/lab/application_mailer.rb +6 -0
  19. data/app/models/lab/application_record.rb +5 -0
  20. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  21. data/app/models/lab/lab_encounter.rb +7 -0
  22. data/app/models/lab/lab_order.rb +58 -0
  23. data/app/models/lab/lab_result.rb +31 -0
  24. data/app/models/lab/lab_test.rb +19 -0
  25. data/app/models/lab/lims_failed_import.rb +4 -0
  26. data/app/models/lab/lims_order_mapping.rb +10 -0
  27. data/app/serializers/lab/lab_order_serializer.rb +55 -0
  28. data/app/serializers/lab/result_serializer.rb +36 -0
  29. data/app/serializers/lab/test_serializer.rb +29 -0
  30. data/app/services/lab/accession_number_service.rb +77 -0
  31. data/app/services/lab/concepts_service.rb +82 -0
  32. data/app/services/lab/labelling_service/order_label.rb +106 -0
  33. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  34. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  35. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  36. data/app/services/lab/lims/api/rest_api.rb +416 -0
  37. data/app/services/lab/lims/api/ws_api.rb +121 -0
  38. data/app/services/lab/lims/api_factory.rb +19 -0
  39. data/app/services/lab/lims/config.rb +100 -0
  40. data/app/services/lab/lims/exceptions.rb +11 -0
  41. data/app/services/lab/lims/migrator.rb +216 -0
  42. data/app/services/lab/lims/order_dto.rb +105 -0
  43. data/app/services/lab/lims/order_serializer.rb +244 -0
  44. data/app/services/lab/lims/pull_worker.rb +289 -0
  45. data/app/services/lab/lims/push_worker.rb +149 -0
  46. data/app/services/lab/lims/utils.rb +91 -0
  47. data/app/services/lab/lims/worker.rb +86 -0
  48. data/app/services/lab/metadata.rb +24 -0
  49. data/app/services/lab/orders_search_service.rb +66 -0
  50. data/app/services/lab/orders_service.rb +212 -0
  51. data/app/services/lab/results_service.rb +149 -0
  52. data/app/services/lab/tests_service.rb +93 -0
  53. data/config/routes.rb +17 -0
  54. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  55. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  56. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  57. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  58. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  59. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  60. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  61. data/lib/auto12epl.rb +201 -0
  62. data/lib/couch_bum/couch_bum.rb +92 -0
  63. data/lib/generators/lab/install/USAGE +9 -0
  64. data/lib/generators/lab/install/install_generator.rb +19 -0
  65. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  66. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  67. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  68. data/lib/his_emr_api_lab.rb +5 -0
  69. data/lib/lab/engine.rb +15 -0
  70. data/lib/lab/version.rb +5 -0
  71. data/lib/logger_multiplexor.rb +38 -0
  72. data/lib/tasks/lab_tasks.rake +25 -0
  73. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  74. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  75. data/lib/tasks/loaders/data/tests.csv +161 -0
  76. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  77. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  78. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  79. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  80. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  81. metadata +81 -2
@@ -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,244 @@
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
+
18
+ Lims::OrderDTO.new(
19
+ _id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
20
+ tracking_number: serialized_order.accession_number,
21
+ sending_facility: current_facility_name,
22
+ receiving_facility: serialized_order.target_lab,
23
+ tests: serialized_order.tests.map { |test| format_test_name(test.name) },
24
+ patient: format_patient(serialized_order.patient_id),
25
+ order_location: format_order_location(serialized_order.encounter_id),
26
+ sample_type: format_sample_type(serialized_order.specimen.name),
27
+ sample_status: format_sample_status(serialized_order.specimen.name),
28
+ sample_statuses: format_sample_status_trail(order),
29
+ test_statuses: format_test_status_trail(order),
30
+ who_order_test: format_orderer(order),
31
+ districy: current_district, # yes districy [sic]...
32
+ priority: format_sample_priority(serialized_order.reason_for_test.name),
33
+ date_created: serialized_order.order_date,
34
+ test_results: format_test_results(serialized_order),
35
+ type: 'Order'
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def format_order_location(encounter_id)
42
+ location_id = Encounter.select(:location_id).where(encounter_id: encounter_id)
43
+ location = Location.select(:name)
44
+ .where(location_id: location_id)
45
+ .first
46
+
47
+ location&.name
48
+ end
49
+
50
+ # Format patient into a structure that LIMS expects
51
+ def format_patient(patient_id)
52
+ person = Person.find(patient_id)
53
+ name = PersonName.find_by_person_id(patient_id)
54
+ national_id = PatientIdentifier.joins(:type)
55
+ .merge(PatientIdentifierType.where(name: 'National ID'))
56
+ .where(patient_id: patient_id)
57
+ .first
58
+ phone_number = PersonAttribute.joins(:type)
59
+ .merge(PersonAttributeType.where(name: 'Cell phone Number'))
60
+ .where(person_id: patient_id)
61
+ .first
62
+
63
+ {
64
+ first_name: name&.given_name,
65
+ last_name: name&.family_name,
66
+ id: national_id&.identifier,
67
+ arv_number: find_arv_number(patient_id),
68
+ art_regimen: find_current_regimen(patient_id),
69
+ art_start_date: find_art_start_date(patient_id),
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
+ end
84
+
85
+ def find_arv_number(patient_id)
86
+ PatientIdentifier.joins(:type)
87
+ .merge(PatientIdentifierType.where(name: 'ARV Number'))
88
+ .where(patient_id: patient_id)
89
+ .first&.identifier
90
+ end
91
+
92
+ def find_art_start_date(patient_id)
93
+ start_date = ActiveRecord::Base.connection.select_one <<~SQL
94
+ SELECT date_antiretrovirals_started(#{patient_id}, current_date()) AS earliest_date
95
+ SQL
96
+ return nil if start_date.blank?
97
+
98
+ start_date['earliest_date']
99
+ end
100
+
101
+ def format_sample_type(name)
102
+ return 'not_specified' if name.casecmp?('Unknown')
103
+
104
+ return 'CSF' if name.casecmp?('Cerebrospinal Fluid')
105
+
106
+ name.titleize
107
+ end
108
+
109
+ def format_sample_status(name)
110
+ name.casecmp?('Unknown') ? 'specimen_not_collected' : 'specimen_collected'
111
+ end
112
+
113
+ def format_sample_status_trail(order)
114
+ return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
115
+
116
+ user = User.find(order.discontinued_by || order.creator)
117
+ drawn_by = PersonName.find_by_person_id(user.user_id)
118
+ drawn_date = order.discontinued_date || order.start_date
119
+
120
+ [
121
+ drawn_date.strftime('%Y%m%d%H%M%S') => {
122
+ 'status' => 'Drawn',
123
+ 'updated_by' => {
124
+ 'first_name' => drawn_by&.given_name || user.username,
125
+ 'last_name' => drawn_by&.family_name,
126
+ 'phone_number' => nil,
127
+ 'id' => user.username
128
+ }
129
+ }
130
+ ]
131
+ end
132
+
133
+ def format_test_status_trail(order)
134
+ tests = order.voided.zero? ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
135
+
136
+ tests.each_with_object({}) do |test, trail|
137
+ test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
138
+
139
+ current_test_trail = trail[test_name] = {}
140
+
141
+ current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
142
+ status: 'Drawn',
143
+ updated_by: find_user(test.creator)
144
+ }
145
+
146
+ unless test.voided.zero?
147
+ current_test_trail[test.date_voided.strftime('%Y%m%d%H%M%S')] = {
148
+ status: 'Voided',
149
+ updated_by: find_user(test.voided_by)
150
+ }
151
+ end
152
+
153
+ next unless test.result
154
+
155
+ current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
156
+ status: 'Verified',
157
+ updated_by: find_user(test.result.creator)
158
+ }
159
+ end
160
+ end
161
+
162
+ def format_orderer(order)
163
+ find_user(order.creator)
164
+ end
165
+
166
+ def format_test_results(order)
167
+ order.tests&.each_with_object({}) do |test, results|
168
+ next if test.result.nil? || test.result.empty?
169
+
170
+ test_creator = User.find(Observation.find(test.result.first.id).creator)
171
+ test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
172
+
173
+ results[format_test_name(test.name)] = {
174
+ results: test.result.each_with_object({}) do |measure, measures|
175
+ measures[format_test_name(measure.indicator.name)] = {
176
+ result_value: "#{measure.value_modifier}#{measure.value}"
177
+ }
178
+ end,
179
+ result_date: test.result.first&.date,
180
+ result_entered_by: {
181
+ first_name: test_creator_name&.given_name,
182
+ last_name: test_creator_name&.family_name,
183
+ id: test_creator.username
184
+ }
185
+ }
186
+ end
187
+ end
188
+
189
+ def format_test_name(test_name)
190
+ return 'Viral Load' if test_name.casecmp?('HIV Viral load')
191
+
192
+ return 'TB' if test_name.casecmp?('TB Program')
193
+
194
+ test_name.titleize
195
+ end
196
+
197
+ def format_sample_priority(priority)
198
+ return 'Routine' if priority&.casecmp?('Medical examination, routine')
199
+
200
+ priority&.titleize
201
+ end
202
+
203
+ def current_health_center
204
+ health_center = Location.current_health_center
205
+ raise 'Current health center not set' unless health_center
206
+
207
+ health_center
208
+ end
209
+
210
+ def current_district
211
+ district = current_health_center.city_village\
212
+ || current_health_center.parent&.name\
213
+ || GlobalProperty.find_by_property('current_health_center_district')&.property_value
214
+
215
+ return district if district
216
+
217
+ GlobalProperty.create(property: 'current_health_center_district',
218
+ property_value: Lims::Config.application['district'],
219
+ uuid: SecureRandom.uuid)
220
+
221
+ Config.application['district']
222
+ end
223
+
224
+ def current_facility_name
225
+ current_health_center.name
226
+ end
227
+
228
+ def find_user(user_id)
229
+ user = User.find(user_id)
230
+ person_name = PersonName.find_by(person_id: user.person_id)
231
+ phone_number = PersonAttribute.find_by(type: PersonAttributeType.where(name: 'Cell phone number'),
232
+ person_id: user.person_id)
233
+
234
+ {
235
+ first_name: person_name&.given_name,
236
+ last_name: person_name&.family_name,
237
+ phone_number: phone_number&.value,
238
+ id: user.username
239
+ }
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end