his_emr_api_lab 1.1.22 → 1.1.23
Sign up to get free protection for your applications and to get access to all the features.
- 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,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
# Responsible for the generation of tracking numbers
|
5
|
+
module AccessionNumberService
|
6
|
+
class << self
|
7
|
+
# Returns the next accession number on the given date or today.
|
8
|
+
#
|
9
|
+
# Throws:
|
10
|
+
# RangeError - If date is greater than system date
|
11
|
+
def next_accession_number(date = nil)
|
12
|
+
date = validate_date(date || Date.today)
|
13
|
+
counter = find_counter(date)
|
14
|
+
|
15
|
+
counter.with_lock do
|
16
|
+
accession_number = format_accession_number(date, counter.value)
|
17
|
+
counter.value += 1
|
18
|
+
counter.save!
|
19
|
+
|
20
|
+
return accession_number
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def find_counter(date)
|
27
|
+
counter = Lab::LabAccessionNumberCounter.find_by(date: date)
|
28
|
+
return counter if counter
|
29
|
+
|
30
|
+
Lab::LabAccessionNumberCounter.create(date: date, value: 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Checks if date does not exceed system date
|
34
|
+
def validate_date(date)
|
35
|
+
return date unless date > Date.today
|
36
|
+
|
37
|
+
raise RangeError, "Specified date exceeds system date: #{date} > #{Date.today}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def format_accession_number(date, counter)
|
41
|
+
year = format_year(date.year)
|
42
|
+
month = format_month(date.month)
|
43
|
+
day = format_day(date.day)
|
44
|
+
|
45
|
+
"X#{site_code}#{year}#{month}#{day}#{counter}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_year(year)
|
49
|
+
(year % 100).to_s.rjust(2, '0')
|
50
|
+
end
|
51
|
+
|
52
|
+
# It's base 32 that uses letters for values 10+ but the letters
|
53
|
+
# are ordered in a way that seems rather arbitrary
|
54
|
+
# (see #get_day in https://github.com/HISMalawi/nlims_controller/blob/3c0faf1cb6572a11cb3b9bd1ea8444f457d01fd7/lib/tracking_number_service.rb#L58)
|
55
|
+
DAY_NUMBERING_SYSTEM = %w[1 2 3 4 5 6 7 8 9 A B C E F G H Y J K Z M N O P Q R S T V W X].freeze
|
56
|
+
|
57
|
+
def format_day(day)
|
58
|
+
DAY_NUMBERING_SYSTEM[day - 1]
|
59
|
+
end
|
60
|
+
|
61
|
+
def format_month(month)
|
62
|
+
# Months use a base 13 numbering system that's just a subset of the
|
63
|
+
# numbering system used for days
|
64
|
+
format_day(month)
|
65
|
+
end
|
66
|
+
|
67
|
+
def site_code
|
68
|
+
property = GlobalProperty.find_by(property: 'site_prefix')
|
69
|
+
value = property&.property_value&.strip
|
70
|
+
|
71
|
+
raise "Global property 'site_prefix' not set" unless value
|
72
|
+
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
# A read-only repository of sort for all lab-centric concepts.
|
5
|
+
module ConceptsService
|
6
|
+
def self.test_types(name: nil, specimen_type: nil)
|
7
|
+
test_types = ConceptSet.find_members_by_name(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
8
|
+
test_types = test_types.filter_members(name: name) if name
|
9
|
+
|
10
|
+
unless specimen_type
|
11
|
+
return test_types.joins('INNER JOIN concept_name ON concept_set.concept_id = concept_name.concept_id')
|
12
|
+
.select('concept_name.name, concept_name.concept_id')
|
13
|
+
.group('concept_name.concept_id')
|
14
|
+
end
|
15
|
+
|
16
|
+
# Filter out only those test types that have the specified specimen
|
17
|
+
# type.
|
18
|
+
specimen_types = ConceptSet.find_members_by_name(Lab::Metadata::SPECIMEN_TYPE_CONCEPT_NAME)
|
19
|
+
.filter_members(name: specimen_type)
|
20
|
+
.select(:concept_id)
|
21
|
+
|
22
|
+
concept_set = ConceptSet.where(
|
23
|
+
concept_id: specimen_types,
|
24
|
+
concept_set: test_types.select(:concept_id)
|
25
|
+
)
|
26
|
+
|
27
|
+
concept_set.joins('INNER JOIN concept_name ON concept_set.concept_set = concept_name.concept_id')
|
28
|
+
.select('concept_name.concept_id, concept_name.name')
|
29
|
+
.group('concept_name.concept_id')
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.specimen_types(name: nil, test_type: nil)
|
33
|
+
specimen_types = ConceptSet.find_members_by_name(Lab::Metadata::SPECIMEN_TYPE_CONCEPT_NAME)
|
34
|
+
specimen_types = specimen_types.filter_members(name: name) if name
|
35
|
+
|
36
|
+
unless test_type
|
37
|
+
return specimen_types.select('concept_name.concept_id, concept_name.name')
|
38
|
+
.joins('INNER JOIN concept_name ON concept_name.concept_id = concept_set.concept_id')
|
39
|
+
.group('concept_name.concept_id')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Retrieve only those specimen types that belong to concept
|
43
|
+
# set of the selected test_type
|
44
|
+
test_types = ConceptSet.find_members_by_name(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
45
|
+
.filter_members(name: test_type)
|
46
|
+
.select(:concept_id)
|
47
|
+
|
48
|
+
concept_set = ConceptSet.where(
|
49
|
+
concept_id: specimen_types.select(:concept_id),
|
50
|
+
concept_set: test_types
|
51
|
+
)
|
52
|
+
|
53
|
+
concept_set.select('concept_name.concept_id, concept_name.name')
|
54
|
+
.joins('INNER JOIN concept_name ON concept_name.concept_id = concept_set.concept_id')
|
55
|
+
.group('concept_name.concept_id')
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.test_result_indicators(test_type_id)
|
59
|
+
# Verify that the specified test_type is indeed a test_type
|
60
|
+
test = ConceptSet.find_members_by_name(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
61
|
+
.where(concept_id: test_type_id)
|
62
|
+
.select(:concept_id)
|
63
|
+
|
64
|
+
# From the members above, filter out only those concepts that are result indicators
|
65
|
+
measures = ConceptSet.find_members_by_name(Lab::Metadata::TEST_RESULT_INDICATOR_CONCEPT_NAME)
|
66
|
+
.select(:concept_id)
|
67
|
+
|
68
|
+
ConceptSet.where(concept_set: measures, concept_id: test)
|
69
|
+
.joins('INNER JOIN concept_name AS measure ON measure.concept_id = concept_set.concept_set')
|
70
|
+
.select('measure.concept_id, measure.name')
|
71
|
+
.group('measure.concept_id')
|
72
|
+
.map { |concept| { name: concept.name, concept_id: concept.concept_id } }
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.reasons_for_test
|
76
|
+
ConceptSet.find_members_by_name(Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME)
|
77
|
+
.joins('INNER JOIN concept_name ON concept_name.concept_id = concept_set.concept_id')
|
78
|
+
.select('concept_name.concept_id, concept_name.name')
|
79
|
+
.map { |concept| { name: concept.name, concept_id: concept.concept_id } }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'auto12epl'
|
4
|
+
|
5
|
+
module Lab
|
6
|
+
module LabellingService
|
7
|
+
##
|
8
|
+
# Prints an order label for order with given accession number.
|
9
|
+
class OrderLabel
|
10
|
+
attr_reader :order
|
11
|
+
|
12
|
+
def initialize(order_id)
|
13
|
+
@order = Lab::LabOrder.find(order_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def print
|
17
|
+
# NOTE: The arguments are passed into the method below not in the order
|
18
|
+
# the method expects (eg patient_id is passed to middle_name field)
|
19
|
+
# to retain compatibility with labels generated by the `lab test controller`
|
20
|
+
# application of the NLIMS suite.
|
21
|
+
auto12epl.generate_epl(patient.given_name,
|
22
|
+
patient.family_name,
|
23
|
+
patient.nhid,
|
24
|
+
patient.birthdate.strftime('%d/%^b/%Y'),
|
25
|
+
'',
|
26
|
+
patient.gender,
|
27
|
+
'',
|
28
|
+
drawer,
|
29
|
+
'',
|
30
|
+
tests,
|
31
|
+
reason_for_test,
|
32
|
+
order.accession_number,
|
33
|
+
order.accession_number)
|
34
|
+
end
|
35
|
+
|
36
|
+
def reason_for_test
|
37
|
+
return 'Unknown' unless order.reason_for_test
|
38
|
+
|
39
|
+
short_concept_name(order.reason_for_test.value_coded) || 'Unknown'
|
40
|
+
end
|
41
|
+
|
42
|
+
def patient
|
43
|
+
return @patient if @patient
|
44
|
+
|
45
|
+
person = Person.find(order.patient_id)
|
46
|
+
person_name = PersonName.find_by_person_id(order.patient_id)
|
47
|
+
patient_identifier = PatientIdentifier.where(type: PatientIdentifierType.where(name: 'National id'),
|
48
|
+
patient_id: order.patient_id)
|
49
|
+
.first
|
50
|
+
|
51
|
+
@patient = OpenStruct.new(
|
52
|
+
given_name: person_name.given_name,
|
53
|
+
family_name: person_name.family_name,
|
54
|
+
birthdate: person.birthdate,
|
55
|
+
gender: person.gender,
|
56
|
+
nhid: patient_identifier&.identifier || 'Unknown'
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def drawer
|
61
|
+
return 'N/A' if order.concept_id == unknown_concept.concept_id
|
62
|
+
|
63
|
+
drawer_id = User.find(order.discontinued_by || order.creator).person_id
|
64
|
+
draw_date = (order.discontinued_date || order.start_date).strftime('%d/%^b/%Y %H:%M:%S')
|
65
|
+
|
66
|
+
name = PersonName.find_by_person_id(drawer_id)
|
67
|
+
return "#{name.given_name} #{name.family_name} #{draw_date}" if name
|
68
|
+
|
69
|
+
user = User.find_by_user_id(drawer_id)
|
70
|
+
user ? "#{user.username} #{draw_date}" : 'N/A'
|
71
|
+
end
|
72
|
+
|
73
|
+
def specimen
|
74
|
+
return 'N/A' if order.concept_id == unknown_concept.concept_id
|
75
|
+
|
76
|
+
ConceptName.find_by_concept_id(order.concept_id)&.name || 'Unknown'
|
77
|
+
end
|
78
|
+
|
79
|
+
def tests
|
80
|
+
tests = order.tests.map do |test|
|
81
|
+
name = short_concept_name(test.value_coded) || 'Unknown'
|
82
|
+
|
83
|
+
next 'VL' if name.match?(/Viral load/i)
|
84
|
+
|
85
|
+
name.size > 7 ? name[0..6] : name
|
86
|
+
end
|
87
|
+
|
88
|
+
tests.join(', ')
|
89
|
+
end
|
90
|
+
|
91
|
+
def short_concept_name(concept_id)
|
92
|
+
ConceptName.where(concept_id: concept_id)
|
93
|
+
.min_by { |concept| concept.name.size }
|
94
|
+
&.name
|
95
|
+
end
|
96
|
+
|
97
|
+
def unknown_concept
|
98
|
+
ConceptName.find_by_name('Unknown')
|
99
|
+
end
|
100
|
+
|
101
|
+
def auto12epl
|
102
|
+
Auto12Epl.new
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
module Lims
|
5
|
+
module Api
|
6
|
+
##
|
7
|
+
# A LIMS Api wrappper that does nothing really.
|
8
|
+
#
|
9
|
+
# Primarily meant as a dummy for testing environments.
|
10
|
+
class BlackholeApi
|
11
|
+
def create_order(order_dto); end
|
12
|
+
|
13
|
+
def update_order(order_dto); end
|
14
|
+
|
15
|
+
def void_order(order_dto); end
|
16
|
+
|
17
|
+
def consume_orders(&_block); end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'couch_bum/couch_bum'
|
4
|
+
|
5
|
+
require_relative '../config'
|
6
|
+
|
7
|
+
module Lab
|
8
|
+
module Lims
|
9
|
+
module Api
|
10
|
+
##
|
11
|
+
# Talk to LIMS like a boss
|
12
|
+
class CouchDbApi
|
13
|
+
attr_reader :bum
|
14
|
+
|
15
|
+
def initialize(config: nil)
|
16
|
+
config ||= Config.couchdb
|
17
|
+
|
18
|
+
@bum = CouchBum.new(protocol: config['protocol'],
|
19
|
+
host: config['host'],
|
20
|
+
port: config['port'],
|
21
|
+
database: "#{config['prefix']}_order_#{config['suffix']}",
|
22
|
+
username: config['username'],
|
23
|
+
password: config['password'])
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Consume orders from the LIMS queue.
|
28
|
+
#
|
29
|
+
# Retrieves orders from the LIMS queue and passes each order to
|
30
|
+
# given block until the queue is empty or connection is terminated
|
31
|
+
# by calling method +choke+.
|
32
|
+
def consume_orders(from: 0, limit: 30)
|
33
|
+
bum.binge_changes(since: from, limit: limit, include_docs: true) do |change|
|
34
|
+
next unless change['doc']['type']&.casecmp?('Order')
|
35
|
+
|
36
|
+
yield OrderDTO.new(change['doc']), self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_order(order)
|
41
|
+
order = order.dup
|
42
|
+
order.delete('_id')
|
43
|
+
|
44
|
+
bum.couch_rest :post, '/', order
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_order(id, order)
|
48
|
+
bum.couch_rest :put, "/#{id}", order
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
module Lims
|
5
|
+
module Api
|
6
|
+
class MysqlApi
|
7
|
+
def self.start
|
8
|
+
instance = MysqlApi.new
|
9
|
+
orders_processed = 0
|
10
|
+
instance.consume_orders(from: 0, limit: 1000) do |order|
|
11
|
+
puts "Order ##{orders_processed}"
|
12
|
+
pp order
|
13
|
+
orders_processed += 1
|
14
|
+
puts
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(processes: 1, on_merge_processes: nil)
|
19
|
+
@processes = processes
|
20
|
+
@on_merge_processes = on_merge_processes
|
21
|
+
@mysql_connection_pool = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def multiprocessed?
|
25
|
+
@processes > 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def consume_orders(from: nil, limit: 1000)
|
29
|
+
loop do
|
30
|
+
specimens_to_process = specimens(from, limit)
|
31
|
+
break if specimens_to_process.size.zero?
|
32
|
+
|
33
|
+
processes = multiprocessed? ? @processes : 0
|
34
|
+
on_merge_processes = ->(_item, index, _result) { @on_merge_processes&.call(from + index) }
|
35
|
+
|
36
|
+
Parallel.map(specimens_to_process, in_processes: processes, finish: on_merge_processes) do |specimen|
|
37
|
+
User.current ||= Utils.lab_user
|
38
|
+
|
39
|
+
tests = specimen_tests(specimen['specimen_id'])
|
40
|
+
results = tests.each_with_object({}) do |test, object|
|
41
|
+
object[test['test_name']] = test_results(test['test_id'])
|
42
|
+
end
|
43
|
+
|
44
|
+
dto = make_order_dto(
|
45
|
+
specimen: specimen,
|
46
|
+
patient: specimen_patient(specimen['specimen_id']),
|
47
|
+
test_results: results,
|
48
|
+
specimen_status_trail: specimen_status_trail(specimen['specimen_id']),
|
49
|
+
test_status_trail: tests.each_with_object({}) do |test, trails|
|
50
|
+
trails[test['test_name']] = test_status_trail(test['test_id'])
|
51
|
+
end
|
52
|
+
)
|
53
|
+
|
54
|
+
yield dto, OpenStruct.new(last_seq: from)
|
55
|
+
end
|
56
|
+
|
57
|
+
from += limit
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parallel_map(items, on_merge: nil, &block); end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def specimens(start_id, limit)
|
66
|
+
query = <<~SQL
|
67
|
+
SELECT specimen.id AS specimen_id,
|
68
|
+
specimen.couch_id AS doc_id,
|
69
|
+
specimen_types.name AS specimen_name,
|
70
|
+
specimen.tracking_number,
|
71
|
+
specimen.priority,
|
72
|
+
specimen.target_lab,
|
73
|
+
specimen.sending_facility,
|
74
|
+
specimen.drawn_by_id,
|
75
|
+
specimen.drawn_by_name,
|
76
|
+
specimen.drawn_by_phone_number,
|
77
|
+
specimen.ward_id,
|
78
|
+
specimen_statuses.name AS specimen_status,
|
79
|
+
specimen.district,
|
80
|
+
specimen.date_created AS order_date
|
81
|
+
FROM specimen
|
82
|
+
INNER JOIN specimen_types ON specimen_types.id = specimen.specimen_type_id
|
83
|
+
INNER JOIN specimen_statuses ON specimen_statuses.id = specimen.specimen_status_id
|
84
|
+
SQL
|
85
|
+
|
86
|
+
query = "#{query} WHERE specimen.id > #{sql_escape(start_id)}" if start_id
|
87
|
+
query = "#{query} LIMIT #{limit.to_i}"
|
88
|
+
|
89
|
+
Rails.logger.debug(query)
|
90
|
+
query(query)
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Pull patient associated with given specimen
|
95
|
+
def specimen_patient(specimen_id)
|
96
|
+
results = query <<~SQL
|
97
|
+
SELECT patients.patient_number AS nhid,
|
98
|
+
patients.name,
|
99
|
+
patients.gender,
|
100
|
+
DATE(patients.dob) AS birthdate
|
101
|
+
FROM patients
|
102
|
+
INNER JOIN tests
|
103
|
+
ON tests.patient_id = patients.id
|
104
|
+
AND tests.specimen_id = #{sql_escape(specimen_id)}
|
105
|
+
LIMIT 1
|
106
|
+
SQL
|
107
|
+
|
108
|
+
results.first
|
109
|
+
end
|
110
|
+
|
111
|
+
def specimen_tests(specimen_id)
|
112
|
+
query <<~SQL
|
113
|
+
SELECT tests.id AS test_id,
|
114
|
+
test_types.name AS test_name,
|
115
|
+
tests.created_by AS drawn_by_name
|
116
|
+
FROM tests
|
117
|
+
INNER JOIN test_types ON test_types.id = tests.test_type_id
|
118
|
+
WHERE tests.specimen_id = #{sql_escape(specimen_id)}
|
119
|
+
SQL
|
120
|
+
end
|
121
|
+
|
122
|
+
def specimen_status_trail(specimen_id)
|
123
|
+
query <<~SQL
|
124
|
+
SELECT specimen_statuses.name AS status_name,
|
125
|
+
specimen_status_trails.who_updated_id AS updated_by_id,
|
126
|
+
specimen_status_trails.who_updated_name AS updated_by_name,
|
127
|
+
specimen_status_trails.who_updated_phone_number AS updated_by_phone_number,
|
128
|
+
specimen_status_trails.time_updated AS date
|
129
|
+
FROM specimen_status_trails
|
130
|
+
INNER JOIN specimen_statuses
|
131
|
+
ON specimen_statuses.id = specimen_status_trails.specimen_status_id
|
132
|
+
WHERE specimen_status_trails.specimen_id = #{sql_escape(specimen_id)}
|
133
|
+
SQL
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_status_trail(test_id)
|
137
|
+
query <<~SQL
|
138
|
+
SELECT test_statuses.name AS status_name,
|
139
|
+
test_status_trails.who_updated_id AS updated_by_id,
|
140
|
+
test_status_trails.who_updated_name AS updated_by_name,
|
141
|
+
test_status_trails.who_updated_phone_number AS updated_by_phone_number,
|
142
|
+
COALESCE(test_status_trails.time_updated, test_status_trails.created_at) AS date
|
143
|
+
FROM test_status_trails
|
144
|
+
INNER JOIN test_statuses
|
145
|
+
ON test_statuses.id = test_status_trails.test_status_id
|
146
|
+
WHERE test_status_trails.test_id = #{sql_escape(test_id)}
|
147
|
+
SQL
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_results(test_id)
|
151
|
+
query <<~SQL
|
152
|
+
SELECT measures.name AS measure_name,
|
153
|
+
test_results.result,
|
154
|
+
test_results.time_entered AS date
|
155
|
+
FROM test_results
|
156
|
+
INNER JOIN measures ON measures.id = test_results.measure_id
|
157
|
+
WHERE test_results.test_id = #{sql_escape(test_id)}
|
158
|
+
SQL
|
159
|
+
end
|
160
|
+
|
161
|
+
def make_order_dto(specimen:, patient:, test_status_trail:, specimen_status_trail:, test_results:)
|
162
|
+
drawn_by_first_name, drawn_by_last_name = specimen['drawn_by_name']&.split
|
163
|
+
patient_first_name, patient_last_name = patient['name'].split
|
164
|
+
|
165
|
+
OrderDTO.new(
|
166
|
+
_id: specimen['doc_id'].blank? ? SecureRandom.uuid : specimen['doc_id'],
|
167
|
+
_rev: '0',
|
168
|
+
tracking_number: specimen['tracking_number'],
|
169
|
+
date_created: specimen['order_date'],
|
170
|
+
sample_type: specimen['specimen_name'],
|
171
|
+
tests: test_status_trail.keys,
|
172
|
+
districy: specimen['district'], # districy [sic] - That's how it's named
|
173
|
+
order_location: specimen['ward_id'],
|
174
|
+
sending_facility: specimen['sending_facility'],
|
175
|
+
receiving_facility: specimen['target_lab'],
|
176
|
+
priority: specimen['priority'],
|
177
|
+
patient: {
|
178
|
+
id: patient['nhid'],
|
179
|
+
first_name: patient_first_name,
|
180
|
+
last_name: patient_last_name,
|
181
|
+
gender: patient['gender'],
|
182
|
+
birthdate: patient['birthdate'],
|
183
|
+
email: nil,
|
184
|
+
phone_number: nil
|
185
|
+
},
|
186
|
+
type: 'Order',
|
187
|
+
who_order_test: {
|
188
|
+
first_name: drawn_by_first_name,
|
189
|
+
last_name: drawn_by_last_name,
|
190
|
+
id: specimen['drawn_by_id'],
|
191
|
+
phone_number: specimen['drawn_by_phone_number']
|
192
|
+
},
|
193
|
+
sample_status: specimen['specimen_status'],
|
194
|
+
sample_statuses: specimen_status_trail.each_with_object({}) do |trail_entry, object|
|
195
|
+
first_name, last_name = trail_entry['updated_by_name'].split
|
196
|
+
|
197
|
+
object[format_date(trail_entry['date'])] = {
|
198
|
+
status: trail_entry['status_name'],
|
199
|
+
updated_by: {
|
200
|
+
first_name: first_name,
|
201
|
+
last_name: last_name,
|
202
|
+
phone_number: trail_entry['updated_by_phone_number'],
|
203
|
+
id: trail_entry['updated_by_id']
|
204
|
+
}
|
205
|
+
}
|
206
|
+
end,
|
207
|
+
test_statuses: test_status_trail.each_with_object({}) do |trail_entry, formatted_trail|
|
208
|
+
test_name, test_statuses = trail_entry
|
209
|
+
|
210
|
+
formatted_trail[test_name] = test_statuses.each_with_object({}) do |test_status, formatted_statuses|
|
211
|
+
updated_by_first_name, updated_by_last_name = test_status['updated_by_name'].split
|
212
|
+
|
213
|
+
formatted_statuses[format_date(test_status['date'])] = {
|
214
|
+
status: test_status['status_name'],
|
215
|
+
updated_by: {
|
216
|
+
first_name: updated_by_first_name,
|
217
|
+
last_name: updated_by_last_name,
|
218
|
+
phone_number: test_status['updated_by_phone_number'],
|
219
|
+
id: test_status['updated_by_id']
|
220
|
+
}
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end,
|
224
|
+
test_results: test_results.each_with_object({}) do |results_entry, formatted_results|
|
225
|
+
test_name, results = results_entry
|
226
|
+
|
227
|
+
formatted_results[test_name] = format_test_result_for_dto(test_name, specimen, results, test_status_trail)
|
228
|
+
end
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
def format_test_result_for_dto(test_name, specimen, results, test_status_trail)
|
233
|
+
return {} if results.size.zero?
|
234
|
+
|
235
|
+
result_create_event = test_status_trail[test_name]&.find do |trail_entry|
|
236
|
+
trail_entry['status_name'].casecmp?('drawn')
|
237
|
+
end
|
238
|
+
|
239
|
+
result_creator_first_name, result_creator_last_name = result_create_event&.fetch('updated_by_name')&.split
|
240
|
+
unless result_creator_first_name
|
241
|
+
result_creator_first_name, result_creator_last_name = specimen['drawn_by_name']&.split
|
242
|
+
end
|
243
|
+
|
244
|
+
{
|
245
|
+
results: results.each_with_object({}) do |result, formatted_measures|
|
246
|
+
formatted_measures[result['measure_name']] = {
|
247
|
+
result_value: result['result']
|
248
|
+
}
|
249
|
+
end,
|
250
|
+
date_result_entered: format_date(result_create_event&.fetch('date') || specimen['order_date'], :iso),
|
251
|
+
result_entered_by: {
|
252
|
+
first_name: result_creator_first_name,
|
253
|
+
last_name: result_creator_last_name,
|
254
|
+
phone_number: result_create_event&.fetch('updated_by_phone_number') || specimen['drawn_by_phone_number'],
|
255
|
+
id: result_create_event&.fetch('updated_by_id') || specimen['updated_by_id']
|
256
|
+
}
|
257
|
+
}
|
258
|
+
end
|
259
|
+
|
260
|
+
def mysql
|
261
|
+
return mysql_connection if mysql_connection
|
262
|
+
|
263
|
+
config = lambda do |key|
|
264
|
+
@config ||= Lab::Lims::Config.database
|
265
|
+
@config['default'][key] || @config['development'][key]
|
266
|
+
end
|
267
|
+
|
268
|
+
connection = Mysql2::Client.new(host: config['host'] || 'localhost',
|
269
|
+
username: config['username'] || 'root',
|
270
|
+
password: config['password'],
|
271
|
+
port: config['port'] || '3306',
|
272
|
+
database: config['database'],
|
273
|
+
reconnect: true)
|
274
|
+
|
275
|
+
self.mysql_connection = connection
|
276
|
+
end
|
277
|
+
|
278
|
+
def pid
|
279
|
+
return -1 if Parallel.worker_number.nil?
|
280
|
+
|
281
|
+
Parallel.worker_number
|
282
|
+
end
|
283
|
+
|
284
|
+
def mysql_connection=(connection)
|
285
|
+
@mysql_connection_pool[pid] = connection
|
286
|
+
end
|
287
|
+
|
288
|
+
def mysql_connection
|
289
|
+
@mysql_connection_pool[pid]
|
290
|
+
end
|
291
|
+
|
292
|
+
def query(sql)
|
293
|
+
Rails.logger.debug("#{MysqlApi}: #{sql}")
|
294
|
+
mysql.query(sql)
|
295
|
+
end
|
296
|
+
|
297
|
+
def sql_escape(value)
|
298
|
+
mysql.escape(value.to_s)
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Lims has some weird date formatting standards...
|
303
|
+
def format_date(date, format = nil)
|
304
|
+
date = date&.to_time
|
305
|
+
|
306
|
+
case format
|
307
|
+
when :iso
|
308
|
+
date&.strftime('%Y-%m-%d %H:%M:%S')
|
309
|
+
else
|
310
|
+
date&.strftime('%Y%m%d%H%M%S')
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|