mahis_his_emr_api_lab 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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/his_emr_api_lab.rb +5 -0
  78. data/lib/lab/engine.rb +15 -0
  79. data/lib/lab/version.rb +5 -0
  80. data/lib/logger_multiplexor.rb +38 -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