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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +20 -0
- data/README.md +71 -0
- data/Rakefile +32 -0
- data/app/controllers/lab/application_controller.rb +6 -0
- data/app/controllers/lab/labels_controller.rb +17 -0
- data/app/controllers/lab/orders_controller.rb +38 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +19 -0
- data/app/controllers/lab/specimen_types_controller.rb +15 -0
- data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
- data/app/controllers/lab/test_types_controller.rb +15 -0
- data/app/controllers/lab/tests_controller.rb +26 -0
- data/app/jobs/lab/application_job.rb +4 -0
- data/app/jobs/lab/push_order_job.rb +12 -0
- data/app/jobs/lab/update_patient_orders_job.rb +32 -0
- data/app/jobs/lab/void_order_job.rb +17 -0
- data/app/mailers/lab/application_mailer.rb +6 -0
- data/app/models/lab/application_record.rb +5 -0
- data/app/models/lab/lab_accession_number_counter.rb +13 -0
- data/app/models/lab/lab_encounter.rb +7 -0
- data/app/models/lab/lab_order.rb +58 -0
- data/app/models/lab/lab_result.rb +31 -0
- data/app/models/lab/lab_test.rb +19 -0
- data/app/models/lab/lims_failed_import.rb +4 -0
- data/app/models/lab/lims_order_mapping.rb +10 -0
- data/app/serializers/lab/lab_order_serializer.rb +55 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +29 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/labelling_service/order_label.rb +106 -0
- data/app/services/lab/lims/api/blackhole_api.rb +21 -0
- data/app/services/lab/lims/api/couchdb_api.rb +53 -0
- data/app/services/lab/lims/api/mysql_api.rb +316 -0
- data/app/services/lab/lims/api/rest_api.rb +416 -0
- data/app/services/lab/lims/api/ws_api.rb +121 -0
- data/app/services/lab/lims/api_factory.rb +19 -0
- data/app/services/lab/lims/config.rb +100 -0
- data/app/services/lab/lims/exceptions.rb +11 -0
- data/app/services/lab/lims/migrator.rb +216 -0
- data/app/services/lab/lims/order_dto.rb +105 -0
- data/app/services/lab/lims/order_serializer.rb +244 -0
- data/app/services/lab/lims/pull_worker.rb +289 -0
- data/app/services/lab/lims/push_worker.rb +149 -0
- data/app/services/lab/lims/utils.rb +91 -0
- data/app/services/lab/lims/worker.rb +86 -0
- data/app/services/lab/metadata.rb +24 -0
- data/app/services/lab/orders_search_service.rb +66 -0
- data/app/services/lab/orders_service.rb +212 -0
- data/app/services/lab/results_service.rb +149 -0
- data/app/services/lab/tests_service.rb +93 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
- data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
- data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
- data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
- data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
- data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
- data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
- data/lib/auto12epl.rb +201 -0
- data/lib/couch_bum/couch_bum.rb +92 -0
- data/lib/generators/lab/install/USAGE +9 -0
- data/lib/generators/lab/install/install_generator.rb +19 -0
- data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
- data/lib/generators/lab/install/templates/start_worker.rb +32 -0
- data/lib/generators/lab/install/templates/swagger.yaml +714 -0
- data/lib/his_emr_api_lab.rb +5 -0
- data/lib/lab/engine.rb +15 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +38 -0
- data/lib/tasks/lab_tasks.rake +25 -0
- data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
- data/lib/tasks/loaders/data/test-measures.csv +225 -0
- data/lib/tasks/loaders/data/tests.csv +161 -0
- data/lib/tasks/loaders/loader_mixin.rb +53 -0
- data/lib/tasks/loaders/metadata_loader.rb +26 -0
- data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
- data/lib/tasks/loaders/specimens_loader.rb +65 -0
- data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
- 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
|