mahis_emr_api_lab 1.2.0

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 (90) hide show
  1. checksums.yaml +7 -0
  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 +78 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +20 -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 +25 -0
  14. data/app/controllers/lab/users_controller.rb +32 -0
  15. data/app/jobs/lab/application_job.rb +4 -0
  16. data/app/jobs/lab/push_order_job.rb +12 -0
  17. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  18. data/app/jobs/lab/void_order_job.rb +17 -0
  19. data/app/mailers/lab/application_mailer.rb +6 -0
  20. data/app/models/lab/application_record.rb +5 -0
  21. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  22. data/app/models/lab/lab_acknowledgement.rb +6 -0
  23. data/app/models/lab/lab_encounter.rb +7 -0
  24. data/app/models/lab/lab_order.rb +58 -0
  25. data/app/models/lab/lab_result.rb +31 -0
  26. data/app/models/lab/lab_test.rb +19 -0
  27. data/app/models/lab/lims_failed_import.rb +4 -0
  28. data/app/models/lab/lims_order_mapping.rb +10 -0
  29. data/app/models/lab/order_extension.rb +14 -0
  30. data/app/serializers/lab/lab_order_serializer.rb +56 -0
  31. data/app/serializers/lab/result_serializer.rb +36 -0
  32. data/app/serializers/lab/test_serializer.rb +52 -0
  33. data/app/services/lab/accession_number_service.rb +77 -0
  34. data/app/services/lab/acknowledgement_service.rb +47 -0
  35. data/app/services/lab/concepts_service.rb +82 -0
  36. data/app/services/lab/json_web_token_service.rb +20 -0
  37. data/app/services/lab/labelling_service/order_label.rb +106 -0
  38. data/app/services/lab/lims/acknowledgement_serializer.rb +29 -0
  39. data/app/services/lab/lims/acknowledgement_worker.rb +37 -0
  40. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  41. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  42. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  43. data/app/services/lab/lims/api/rest_api.rb +434 -0
  44. data/app/services/lab/lims/api/ws_api.rb +121 -0
  45. data/app/services/lab/lims/api_factory.rb +19 -0
  46. data/app/services/lab/lims/config.rb +105 -0
  47. data/app/services/lab/lims/exceptions.rb +11 -0
  48. data/app/services/lab/lims/migrator.rb +216 -0
  49. data/app/services/lab/lims/order_dto.rb +105 -0
  50. data/app/services/lab/lims/order_serializer.rb +251 -0
  51. data/app/services/lab/lims/pull_worker.rb +314 -0
  52. data/app/services/lab/lims/push_worker.rb +152 -0
  53. data/app/services/lab/lims/utils.rb +91 -0
  54. data/app/services/lab/lims/worker.rb +94 -0
  55. data/app/services/lab/metadata.rb +26 -0
  56. data/app/services/lab/notification_service.rb +72 -0
  57. data/app/services/lab/orders_search_service.rb +72 -0
  58. data/app/services/lab/orders_service.rb +330 -0
  59. data/app/services/lab/results_service.rb +166 -0
  60. data/app/services/lab/tests_service.rb +105 -0
  61. data/app/services/lab/user_service.rb +62 -0
  62. data/config/routes.rb +28 -0
  63. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  64. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  65. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  66. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  67. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  68. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  69. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  70. data/lib/auto12epl.rb +201 -0
  71. data/lib/couch_bum/couch_bum.rb +92 -0
  72. data/lib/generators/lab/install/USAGE +9 -0
  73. data/lib/generators/lab/install/install_generator.rb +19 -0
  74. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  75. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  76. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  77. data/lib/lab/engine.rb +13 -0
  78. data/lib/lab/version.rb +5 -0
  79. data/lib/logger_multiplexor.rb +38 -0
  80. data/lib/mahis_emr_api_lab.rb +6 -0
  81. data/lib/tasks/lab_tasks.rake +25 -0
  82. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  83. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  84. data/lib/tasks/loaders/data/tests.csv +161 -0
  85. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  86. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  87. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  88. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  89. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  90. metadata +331 -0
@@ -0,0 +1,56 @@
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
+ ActiveSupport::HashWithIndifferentAccess.new(
11
+ {
12
+ id: order.order_id,
13
+ order_id: order.order_id, # Deprecated: Link to :id
14
+ encounter_id: order.encounter_id,
15
+ order_date: order.date_created,
16
+ patient_id: order.patient_id,
17
+ accession_number: order.accession_number,
18
+ specimen: {
19
+ concept_id: order.concept_id,
20
+ name: concept_name(order.concept_id)
21
+ },
22
+ requesting_clinician: requesting_clinician&.value_text,
23
+ target_lab: target_lab,
24
+ reason_for_test: {
25
+ concept_id: reason_for_test&.value_coded,
26
+ name: concept_name(reason_for_test&.value_coded)
27
+ },
28
+ delivery_mode: order&.lims_acknowledgement_status&.acknowledgement_type,
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
+ uuid: test.uuid,
36
+ name: concept_name(test.value_coded),
37
+ result: result_obs && ResultSerializer.serialize(result_obs)
38
+ }
39
+ end
40
+ }
41
+ )
42
+ end
43
+
44
+ def self.concept_name(concept_id)
45
+ return concept_id unless concept_id
46
+
47
+ ConceptName.select(:name).find_by_concept_id(concept_id)&.name
48
+ end
49
+
50
+ def self.voided_tests(order)
51
+ concept = ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
52
+ .select(:concept_id)
53
+ LabTest.unscoped.where(concept: concept, order: order, voided: true)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
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) if measure.respond_to?(field)
29
+
30
+ return [value, field.split('_')[1]] if value
31
+ end
32
+
33
+ [nil, 'unknown']
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ # rubocop:disable Lint/UnreachableLoop
2
+ # frozen_string_literal: true
3
+
4
+ module Lab
5
+ module TestSerializer
6
+ def self.serialize(test, order: nil, result: nil)
7
+ order ||= test.order
8
+ result ||= test.result
9
+ {
10
+ id: test.obs_id,
11
+ test_uuid: test.uuid,
12
+ concept_id: test.value_coded,
13
+ concept_uuid: test.value_coded ? Concept.find(test.value_coded)&.uuid : nil,
14
+ name: ConceptName.find_by_concept_id(test.value_coded)&.name,
15
+ order: {
16
+ id: order.order_id,
17
+ concept_id: order.concept_id,
18
+ concept_uuid: order.concept_id ? Concept.find(order.concept_id)&.uuid : nil,
19
+ name: ConceptName.find_by_concept_id(order.concept_id)&.name,
20
+ accession_number: order.accession_number
21
+ },
22
+ measures: result_mesures(result),
23
+ result: if result
24
+ {
25
+ id: result.obs_id,
26
+ uuid: result.uuid,
27
+ modifier: result.value_modifier,
28
+ value: result.value_text
29
+ }
30
+ end
31
+ }
32
+ end
33
+
34
+ def self.result_mesures(result)
35
+ if result&.measures.present?
36
+ return result&.measures&.map do |measure|
37
+ m = {}
38
+ m[:uuid] = measure.uuid
39
+ m[:concept_id] = measure.concept_id
40
+ m[:name] = ConceptName.find_by_concept_id(measure.concept_id)&.name
41
+ m[:modifier] = measure.value_modifier
42
+ m[:value] = measure&.value_text || measure&.value_numeric || measure&.value_boolean || measure&.value_coded || measure&.value_datetime || measure&.value_drug || measure&.value_complex || measure&.value_group
43
+ m
44
+ end
45
+ end
46
+
47
+ nil
48
+ end
49
+ end
50
+ end
51
+
52
+ # rubocop:enable Lint/UnreachableLoop
@@ -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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # acknoledgement service for creating lab acknowledgements
5
+ module AcknowledgementService
6
+ class << self
7
+ def create_acknowledgement(params)
8
+ order = Order.find(params[:order_id])
9
+
10
+ Lab::LabAcknowledgement.find_by(order_id: order.id, test: params[:test])&.destroy
11
+
12
+ Lab::LabAcknowledgement.create!(order_id: order.id, test: params[:test], pushed: false,
13
+ acknowledgement_type: params[:entered_by] == 'LIMS' ? 'test_results_delivered_to_site_electronically' : 'test_results_delivered_to_site_manually',
14
+ date_received: params[:date_received])
15
+ end
16
+
17
+ def acknowledgements_pending_sync(batch_size)
18
+ Lab::LabAcknowledgement.where(pushed: false)
19
+ .limit(batch_size)
20
+ end
21
+
22
+ def push_acknowledgement(acknowledgement, lims_api)
23
+ Rails.logger.info("Pushing acknowledgement ##{acknowledgement.order_id}")
24
+
25
+ acknowledgement_dto = Lab::Lims::AcknowledgementSerializer.serialize_acknowledgement(acknowledgement)
26
+ mapping = Lab::LimsOrderMapping.find_by(order_id: acknowledgement.order_id)
27
+
28
+ ActiveRecord::Base.transaction do
29
+ if mapping
30
+ Rails.logger.info("Updating acknowledgement ##{acknowledgement_dto[:tracking_number]} in LIMS")
31
+ response = lims_api.acknowledge(acknowledgement_dto)
32
+ Rails.logger.info("Info #{response}")
33
+ if response['status'] == 200 || response['message'] == 'results already delivered for test name given'
34
+ acknowledgement.pushed = true
35
+ acknowledgement.date_pushed = Time.now
36
+ acknowledgement.save!
37
+ else
38
+ Rails.logger.error("Failed to process acknowledgement for tracking number ##{acknowledgement_dto[:tracking_number]} in LIMS")
39
+ end
40
+ else
41
+ Rails.logger.info("No mapping found for acknowledgement ##{acknowledgement_dto[:tracking_number]}")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ 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 AND concept_name.voided = 0 AND concept_name.locale_preferred = 1')
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: Concept.find(test_type_id)&.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, measure.uuid')
71
+ .group('measure.concept_id')
72
+ .map { |concept| { name: concept.name, concept_id: concept.concept_id, uuid: concept.uuid } }
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, concept_name.uuid')
79
+ .map { |concept| { name: concept.name, concept_id: concept.concept_id, uuid: concept.uuid } }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # This class is used to encode and decode the JWT token
5
+ module JsonWebTokenService
6
+ class << self
7
+ SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
8
+
9
+ def encode(payload, request_ip, exp = 18.hours.from_now)
10
+ payload[:exp] = exp.to_i
11
+ JWT.encode(payload, SECRET_KEY + request_ip)
12
+ end
13
+
14
+ def decode(token, request_ip)
15
+ decoded = JWT.decode(token, SECRET_KEY + request_ip)[0]
16
+ HashWithIndifferentAccess.new decoded
17
+ end
18
+ end
19
+ end
20
+ 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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ ##
6
+ # Serialize a Lab::LabResult to LIMS' acknowledgement format
7
+ module AcknowledgementSerializer
8
+ class << self
9
+ include Utils
10
+
11
+ def serialize_acknowledgement(acknowledgement)
12
+ serialized_acknowledgement = Lims::Utils.structify(acknowledgement)
13
+ {
14
+ tracking_number: Lab::LabOrder.find(serialized_acknowledgement.order_id).accession_number,
15
+ test: ::ConceptName.where(concept_id: serialized_acknowledgement.test).first.name,
16
+ date_acknowledged: format_date(serialized_acknowledgement.date_received),
17
+ recipient_type: serialized_acknowledgement.acknowledgement_type
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def format_date(date)
24
+ date.strftime('%Y-%m-%d %H:%M:%S')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module Lims
5
+ # This class is responsible for handling the acknowledgement of lab orders
6
+ class AcknowledgementWorker
7
+ attr_reader :lims_api
8
+
9
+ include Utils # for logger
10
+
11
+ SECONDS_TO_WAIT_FOR_ORDERS = 30
12
+
13
+ def initialize(lims_api)
14
+ @lims_api = lims_api
15
+ end
16
+
17
+ def push_acknowledgement(batch_size: 1000, wait: false)
18
+ loop do
19
+ logger.info('Looking for new acknowledgements to push to LIMS...')
20
+ acknowledgements = Lab::AcknowledgementService.acknowledgements_pending_sync(batch_size).all
21
+
22
+ logger.debug("Found #{acknowledgements.size} acknowledgements...")
23
+ acknowledgements.each do |acknowledgement|
24
+ Lab::AcknowledgementService.push_acknowledgement(acknowledgement, @lims_api)
25
+ rescue GatewayError => e
26
+ logger.error("Failed to push acknowledgement ##{acknowledgement.order_id}: #{e.class} - #{e.message}")
27
+ end
28
+
29
+ break unless wait
30
+
31
+ logger.info('Waiting for acknowledgements...')
32
+ sleep(Lab::Lims::Config.updates_poll_frequency)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ 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