his_emr_api_lab 1.1.19 → 1.1.22

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. metadata +8 -86
  3. data/MIT-LICENSE +0 -20
  4. data/README.md +0 -71
  5. data/Rakefile +0 -32
  6. data/app/controllers/lab/application_controller.rb +0 -6
  7. data/app/controllers/lab/labels_controller.rb +0 -17
  8. data/app/controllers/lab/orders_controller.rb +0 -38
  9. data/app/controllers/lab/reasons_for_test_controller.rb +0 -9
  10. data/app/controllers/lab/results_controller.rb +0 -19
  11. data/app/controllers/lab/specimen_types_controller.rb +0 -15
  12. data/app/controllers/lab/test_result_indicators_controller.rb +0 -9
  13. data/app/controllers/lab/test_types_controller.rb +0 -15
  14. data/app/controllers/lab/tests_controller.rb +0 -26
  15. data/app/jobs/lab/application_job.rb +0 -4
  16. data/app/jobs/lab/push_order_job.rb +0 -12
  17. data/app/jobs/lab/update_patient_orders_job.rb +0 -32
  18. data/app/jobs/lab/void_order_job.rb +0 -17
  19. data/app/mailers/lab/application_mailer.rb +0 -6
  20. data/app/models/lab/application_record.rb +0 -5
  21. data/app/models/lab/lab_accession_number_counter.rb +0 -13
  22. data/app/models/lab/lab_encounter.rb +0 -7
  23. data/app/models/lab/lab_order.rb +0 -54
  24. data/app/models/lab/lab_result.rb +0 -31
  25. data/app/models/lab/lab_test.rb +0 -19
  26. data/app/models/lab/lims_failed_import.rb +0 -4
  27. data/app/models/lab/lims_order_mapping.rb +0 -10
  28. data/app/serializers/lab/lab_order_serializer.rb +0 -55
  29. data/app/serializers/lab/result_serializer.rb +0 -36
  30. data/app/serializers/lab/test_serializer.rb +0 -29
  31. data/app/services/lab/accession_number_service.rb +0 -77
  32. data/app/services/lab/concepts_service.rb +0 -82
  33. data/app/services/lab/labelling_service/order_label.rb +0 -106
  34. data/app/services/lab/lims/api/blackhole_api.rb +0 -21
  35. data/app/services/lab/lims/api/couchdb_api.rb +0 -53
  36. data/app/services/lab/lims/api/mysql_api.rb +0 -316
  37. data/app/services/lab/lims/api/rest_api.rb +0 -413
  38. data/app/services/lab/lims/api/ws_api.rb +0 -121
  39. data/app/services/lab/lims/api_factory.rb +0 -19
  40. data/app/services/lab/lims/config.rb +0 -100
  41. data/app/services/lab/lims/exceptions.rb +0 -11
  42. data/app/services/lab/lims/migrator.rb +0 -216
  43. data/app/services/lab/lims/order_dto.rb +0 -105
  44. data/app/services/lab/lims/order_serializer.rb +0 -216
  45. data/app/services/lab/lims/pull_worker.rb +0 -289
  46. data/app/services/lab/lims/push_worker.rb +0 -144
  47. data/app/services/lab/lims/utils.rb +0 -91
  48. data/app/services/lab/lims/worker.rb +0 -86
  49. data/app/services/lab/metadata.rb +0 -24
  50. data/app/services/lab/orders_search_service.rb +0 -66
  51. data/app/services/lab/orders_service.rb +0 -212
  52. data/app/services/lab/results_service.rb +0 -122
  53. data/app/services/lab/tests_service.rb +0 -93
  54. data/config/routes.rb +0 -17
  55. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +0 -12
  56. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +0 -15
  57. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +0 -15
  58. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +0 -5
  59. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +0 -19
  60. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +0 -20
  61. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +0 -7
  62. data/lib/auto12epl.rb +0 -201
  63. data/lib/couch_bum/couch_bum.rb +0 -92
  64. data/lib/generators/lab/install/USAGE +0 -9
  65. data/lib/generators/lab/install/install_generator.rb +0 -19
  66. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +0 -5
  67. data/lib/generators/lab/install/templates/start_worker.rb +0 -32
  68. data/lib/generators/lab/install/templates/swagger.yaml +0 -714
  69. data/lib/his_emr_api_lab.rb +0 -5
  70. data/lib/lab/engine.rb +0 -15
  71. data/lib/lab/version.rb +0 -5
  72. data/lib/logger_multiplexor.rb +0 -38
  73. data/lib/tasks/lab_tasks.rake +0 -25
  74. data/lib/tasks/loaders/data/reasons-for-test.csv +0 -7
  75. data/lib/tasks/loaders/data/test-measures.csv +0 -225
  76. data/lib/tasks/loaders/data/tests.csv +0 -161
  77. data/lib/tasks/loaders/loader_mixin.rb +0 -53
  78. data/lib/tasks/loaders/metadata_loader.rb +0 -26
  79. data/lib/tasks/loaders/reasons_for_test_loader.rb +0 -23
  80. data/lib/tasks/loaders/specimens_loader.rb +0 -65
  81. data/lib/tasks/loaders/test_result_indicators_loader.rb +0 -54
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lab
4
- module LabOrderSerializer
5
- def self.serialize_order(order, tests: nil, requesting_clinician: nil, reason_for_test: nil, target_lab: nil)
6
- tests ||= order.voided == 1 ? voided_tests(order) : order.tests
7
- requesting_clinician ||= order.requesting_clinician
8
- reason_for_test ||= order.reason_for_test
9
- target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
10
-
11
- ActiveSupport::HashWithIndifferentAccess.new(
12
- {
13
- id: order.order_id,
14
- order_id: order.order_id, # Deprecated: Link to :id
15
- encounter_id: order.encounter_id,
16
- order_date: order.start_date,
17
- patient_id: order.patient_id,
18
- accession_number: order.accession_number,
19
- specimen: {
20
- concept_id: order.concept_id,
21
- name: concept_name(order.concept_id)
22
- },
23
- requesting_clinician: requesting_clinician&.value_text,
24
- target_lab: target_lab,
25
- reason_for_test: {
26
- concept_id: reason_for_test&.value_coded,
27
- name: concept_name(reason_for_test&.value_coded)
28
- },
29
- tests: tests.map do |test|
30
- result_obs = test.children.first
31
-
32
- {
33
- id: test.obs_id,
34
- concept_id: test.value_coded,
35
- name: concept_name(test.value_coded),
36
- result: result_obs && ResultSerializer.serialize(result_obs)
37
- }
38
- end
39
- }
40
- )
41
- end
42
-
43
- def self.concept_name(concept_id)
44
- return concept_id unless concept_id
45
-
46
- ConceptName.select(:name).find_by_concept_id(concept_id)&.name
47
- end
48
-
49
- def self.voided_tests(order)
50
- concept = ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
51
- .select(:concept_id)
52
- LabTest.unscoped.where(concept: concept, order: order, voided: true)
53
- end
54
- end
55
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lab
4
- ##
5
- # Serialize a Lab order result
6
- module ResultSerializer
7
- def self.serialize(result)
8
- result.children.map do |measure|
9
- value, value_type = read_value(measure)
10
- concept_name = ConceptName.find_by_concept_id(measure.concept_id)
11
-
12
- {
13
- id: measure.obs_id,
14
- indicator: {
15
- concept_id: concept_name&.concept_id,
16
- name: concept_name&.name
17
- },
18
- date: measure.obs_datetime,
19
- value: value,
20
- value_type: value_type,
21
- value_modifier: measure.value_modifier
22
- }
23
- end
24
- end
25
-
26
- def self.read_value(measure)
27
- %w[value_numeric value_coded value_boolean value_text].each do |field|
28
- value = measure.send(field)
29
-
30
- return [value, field.split('_')[1]] if value
31
- end
32
-
33
- [nil, 'unknown']
34
- end
35
- end
36
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lab
4
- module TestSerializer
5
- def self.serialize(test, order: nil, result: nil)
6
- order ||= test.order
7
- result ||= test.result
8
-
9
- {
10
- id: test.obs_id,
11
- concept_id: test.value_coded,
12
- name: ConceptName.find_by_concept_id(test.value_coded)&.name,
13
- order: {
14
- id: order.order_id,
15
- concept_id: order.concept_id,
16
- name: ConceptName.find_by_concept_id(order.concept_id)&.name,
17
- accession_number: order.accession_number
18
- },
19
- result: if result
20
- {
21
- id: result.obs_id,
22
- modifier: result.value_modifier,
23
- value: result.value_text
24
- }
25
- end
26
- }
27
- end
28
- end
29
- end
@@ -1,77 +0,0 @@
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
@@ -1,82 +0,0 @@
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
@@ -1,106 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,53 +0,0 @@
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