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,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