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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +71 -0
- data/Rakefile +32 -0
- data/app/controllers/lab/application_controller.rb +6 -0
- data/app/controllers/lab/labels_controller.rb +17 -0
- data/app/controllers/lab/orders_controller.rb +78 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +20 -0
- data/app/controllers/lab/specimen_types_controller.rb +15 -0
- data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
- data/app/controllers/lab/test_types_controller.rb +15 -0
- data/app/controllers/lab/tests_controller.rb +25 -0
- data/app/controllers/lab/users_controller.rb +32 -0
- data/app/jobs/lab/application_job.rb +4 -0
- data/app/jobs/lab/push_order_job.rb +12 -0
- data/app/jobs/lab/update_patient_orders_job.rb +32 -0
- data/app/jobs/lab/void_order_job.rb +17 -0
- data/app/mailers/lab/application_mailer.rb +6 -0
- data/app/models/lab/application_record.rb +5 -0
- data/app/models/lab/lab_accession_number_counter.rb +13 -0
- data/app/models/lab/lab_acknowledgement.rb +6 -0
- data/app/models/lab/lab_encounter.rb +7 -0
- data/app/models/lab/lab_order.rb +58 -0
- data/app/models/lab/lab_result.rb +31 -0
- data/app/models/lab/lab_test.rb +19 -0
- data/app/models/lab/lims_failed_import.rb +4 -0
- data/app/models/lab/lims_order_mapping.rb +10 -0
- data/app/models/lab/order_extension.rb +14 -0
- data/app/serializers/lab/lab_order_serializer.rb +56 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +52 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/acknowledgement_service.rb +47 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/json_web_token_service.rb +20 -0
- data/app/services/lab/labelling_service/order_label.rb +106 -0
- data/app/services/lab/lims/acknowledgement_serializer.rb +29 -0
- data/app/services/lab/lims/acknowledgement_worker.rb +37 -0
- data/app/services/lab/lims/api/blackhole_api.rb +21 -0
- data/app/services/lab/lims/api/couchdb_api.rb +53 -0
- data/app/services/lab/lims/api/mysql_api.rb +316 -0
- data/app/services/lab/lims/api/rest_api.rb +434 -0
- data/app/services/lab/lims/api/ws_api.rb +121 -0
- data/app/services/lab/lims/api_factory.rb +19 -0
- data/app/services/lab/lims/config.rb +105 -0
- data/app/services/lab/lims/exceptions.rb +11 -0
- data/app/services/lab/lims/migrator.rb +216 -0
- data/app/services/lab/lims/order_dto.rb +105 -0
- data/app/services/lab/lims/order_serializer.rb +251 -0
- data/app/services/lab/lims/pull_worker.rb +314 -0
- data/app/services/lab/lims/push_worker.rb +152 -0
- data/app/services/lab/lims/utils.rb +91 -0
- data/app/services/lab/lims/worker.rb +94 -0
- data/app/services/lab/metadata.rb +26 -0
- data/app/services/lab/notification_service.rb +72 -0
- data/app/services/lab/orders_search_service.rb +72 -0
- data/app/services/lab/orders_service.rb +330 -0
- data/app/services/lab/results_service.rb +166 -0
- data/app/services/lab/tests_service.rb +105 -0
- data/app/services/lab/user_service.rb +62 -0
- data/config/routes.rb +28 -0
- data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
- data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
- data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
- data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
- data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
- data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
- data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
- data/lib/auto12epl.rb +201 -0
- data/lib/couch_bum/couch_bum.rb +92 -0
- data/lib/generators/lab/install/USAGE +9 -0
- data/lib/generators/lab/install/install_generator.rb +19 -0
- data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
- data/lib/generators/lab/install/templates/start_worker.rb +32 -0
- data/lib/generators/lab/install/templates/swagger.yaml +714 -0
- data/lib/lab/engine.rb +13 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +38 -0
- data/lib/mahis_emr_api_lab.rb +6 -0
- data/lib/tasks/lab_tasks.rake +25 -0
- data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
- data/lib/tasks/loaders/data/test-measures.csv +225 -0
- data/lib/tasks/loaders/data/tests.csv +161 -0
- data/lib/tasks/loaders/loader_mixin.rb +53 -0
- data/lib/tasks/loaders/metadata_loader.rb +26 -0
- data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
- data/lib/tasks/loaders/specimens_loader.rb +65 -0
- data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
- metadata +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
|