his_emr_api_lab 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +52 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/lab/application_controller.rb +6 -0
  6. data/app/controllers/lab/orders_controller.rb +34 -0
  7. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  8. data/app/controllers/lab/results_controller.rb +19 -0
  9. data/app/controllers/lab/specimen_types_controller.rb +15 -0
  10. data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
  11. data/app/controllers/lab/test_types_controller.rb +15 -0
  12. data/app/controllers/lab/tests_controller.rb +25 -0
  13. data/app/jobs/lab/application_job.rb +4 -0
  14. data/app/mailers/lab/application_mailer.rb +6 -0
  15. data/app/models/lab/application_record.rb +5 -0
  16. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  17. data/app/models/lab/lab_encounter.rb +7 -0
  18. data/app/models/lab/lab_order.rb +47 -0
  19. data/app/models/lab/lab_result.rb +21 -0
  20. data/app/models/lab/lab_test.rb +14 -0
  21. data/app/models/lab/lims_failed_import.rb +4 -0
  22. data/app/models/lab/lims_order_mapping.rb +10 -0
  23. data/app/serializers/lab/lab_order_serializer.rb +49 -0
  24. data/app/serializers/lab/result_serializer.rb +36 -0
  25. data/app/serializers/lab/test_serializer.rb +29 -0
  26. data/app/services/lab/accession_number_service.rb +77 -0
  27. data/app/services/lab/concepts_service.rb +82 -0
  28. data/app/services/lab/lims/api.rb +46 -0
  29. data/app/services/lab/lims/config.rb +56 -0
  30. data/app/services/lab/lims/order_dto.rb +177 -0
  31. data/app/services/lab/lims/order_serializer.rb +112 -0
  32. data/app/services/lab/lims/utils.rb +27 -0
  33. data/app/services/lab/lims/worker.rb +121 -0
  34. data/app/services/lab/metadata.rb +23 -0
  35. data/app/services/lab/orders_search_service.rb +48 -0
  36. data/app/services/lab/orders_service.rb +194 -0
  37. data/app/services/lab/results_service.rb +92 -0
  38. data/app/services/lab/tests_service.rb +93 -0
  39. data/config/routes.rb +15 -0
  40. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  41. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  42. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  43. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  44. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  45. data/lib/couch_bum/couch_bum.rb +77 -0
  46. data/lib/generators/lab/install/USAGE +9 -0
  47. data/lib/generators/lab/install/install_generator.rb +19 -0
  48. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  49. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  50. data/lib/generators/lab/install/templates/swagger.yaml +682 -0
  51. data/lib/his_emr_api_lab.rb +5 -0
  52. data/lib/lab/engine.rb +15 -0
  53. data/lib/lab/version.rb +5 -0
  54. data/lib/logger_multiplexor.rb +32 -0
  55. data/lib/tasks/lab_tasks.rake +25 -0
  56. data/lib/tasks/loaders/data/reasons-for-test.csv +6 -0
  57. data/lib/tasks/loaders/data/test-measures.csv +224 -0
  58. data/lib/tasks/loaders/data/tests.csv +142 -0
  59. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  60. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  61. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  62. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  63. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  64. metadata +296 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8cc795d01dc073e4556a1070e8e14199e49adff9419dfe02837008adb1aa6556
4
+ data.tar.gz: 9a61181925a0df93287b43446f0ace3b552972859f5f61248f7b8fb0fb085f56
5
+ SHA512:
6
+ metadata.gz: 4a571f53b3c2216c9bdb60bfc98f8b02ecef5053d72ac91d9a4680d8adb893a4085cd4b91c9fb873552f4e0e84b7e6990321140e0fe9795e1c64bf45c2bf5fb2
7
+ data.tar.gz: e169300a7f5e7b4c3755107895522275eaa3b71d17495cd14cac3cd5227d6b1f2529f8a27695f5301cbd7d92f20917c9e37dbd66be0babf49cdd98c3b3be3674
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Walter Kaunda
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ ![Ruby](https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab/workflows/Ruby/badge.svg?branch=development)
2
+ # Lab
3
+
4
+ A Rails engine for [HIS-EMR-API](https://github.com/EGPAFMalawiHIS/HIS-EMR-API)
5
+ compatible applications that provides an API for managing various lab
6
+ activities.
7
+
8
+ ## Usage
9
+
10
+ The engine provides an API that provides the following functionalities:
11
+
12
+ - Search/Retrieve test types
13
+ - Search/Retrieve sample types for a given test
14
+ - Order lab tests
15
+ - Attach a result to a test
16
+
17
+ For details on how to perform these operations please see the
18
+ [API docs](https://raw.githack.com/EGPAFMalawiHIS/HIS-EMR-API-Lab/development/docs/api.html).
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'lab', git: 'https://github.com/EGPAFMalawiHIS/HIS-EMR-API-Lab', branch: 'development'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```bash
31
+ $ bundle install lab
32
+ ```
33
+
34
+ Or install it yourself as:
35
+
36
+ ```bash
37
+ $ gem install lab
38
+ ```
39
+
40
+ Finally run:
41
+
42
+ ```bash
43
+ $ bundle exec rails lab:install
44
+ ```
45
+
46
+ ## Contributing
47
+
48
+ Contribution directions go here.
49
+
50
+ ## License
51
+
52
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Lab'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'spec'
28
+ t.pattern = 'spec/**/*_spec.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class ApplicationController < ::ApplicationController
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class OrdersController < ApplicationController
5
+ def create
6
+ order_params_list = params.require(:orders)
7
+ orders = order_params_list.map do |order_params|
8
+ OrdersService.order_test(order_params)
9
+ end
10
+
11
+ render json: orders, status: :created
12
+ end
13
+
14
+ def update
15
+ specimen = params.require(:specimen).permit(:concept_id)
16
+
17
+ order = OrdersService.update_order(params[:id], specimen: specimen)
18
+
19
+ render json: order
20
+ end
21
+
22
+ def index
23
+ filters = params.permit(%i[patient_id accession_number date status])
24
+
25
+ render json: OrdersSearchService.find_orders(filters)
26
+ end
27
+
28
+ def destroy
29
+ OrdersService.void_order(params[:id], params[:reason])
30
+
31
+ render status: :no_content
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class ReasonsForTestController < ApplicationController
5
+ def index
6
+ render json: ConceptsService.reasons_for_test
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class ResultsController < ApplicationController
5
+ def create
6
+ result_params = params.require(:result)
7
+ .permit([:encounter_id,
8
+ :date,
9
+ { measures: [:value,
10
+ :value_type,
11
+ :value_modifier,
12
+ { indicator: [:concept_id] }] }])
13
+
14
+ result = Lab::ResultsService.create_results(params[:test_id], result_params)
15
+
16
+ render json: result, status: :created
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class SpecimenTypesController < ApplicationController
5
+ def index
6
+ filters = params.permit(%w[name test_type])
7
+
8
+ specimen_types = ConceptsService.specimen_types(name: filters['name'],
9
+ test_type: filters['test_type'])
10
+ .as_json(only: %w[concept_id name])
11
+
12
+ render json: specimen_types
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lab::TestResultIndicatorsController < ApplicationController
4
+ def index
5
+ test_type_id = params.require(:test_type_id)
6
+
7
+ render json: Lab::ConceptsService.test_result_indicators(test_type_id)
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class TestTypesController < ApplicationController
5
+ def index
6
+ filters = params.permit(%w[name specimen_type])
7
+
8
+ test_types = ConceptsService.test_types(name: filters['name'],
9
+ specimen_type: filters['specimen_type'])
10
+ .as_json(only: %w[concept_id name])
11
+
12
+ render json: test_types
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lab::TestsController < ::ApplicationController
4
+ def index
5
+ filters = params.permit(%i[order_date accession_number patient_id test_type_id specimen_type_id pending_results])
6
+
7
+ tests = service.find_tests(filters)
8
+ render json: tests
9
+ end
10
+
11
+ # Add a specimen to an existing order
12
+ def create
13
+ test_params = params.permit(:order_id, :date, tests: [:concept_id])
14
+ order_id, test_concepts = test_params.require(%i[order_id tests])
15
+ date = test_params[:date] || Date.today
16
+
17
+ tests = service.create_tests(Order.find(order_id), date, test_concepts)
18
+
19
+ render json: tests, status: :created
20
+ end
21
+
22
+ def service
23
+ Lab::TestsService
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module Lab
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Lab
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Lab
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # Used to keep track of counters for accession numbers.
5
+ #
6
+ # An accession number is just a prefix plus a running encounter
7
+ # that's reset at the end of the day
8
+ class LabAccessionNumberCounter < ApplicationRecord
9
+ self.table_name = :lab_accession_number_counters
10
+
11
+ validates_presence_of :date, :value
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class LabEncounter < ::Encounter
5
+ default_scope { joins(:type).merge(EncounterType.where('name LIKE ?', ENCOUNTER_TYPE_NAME)) }
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class LabOrder < ::Order
5
+ class << self
6
+ def make_obs_concept_filter(concept_name)
7
+ concept = ConceptName.where(name: concept_name).select(:concept_id)
8
+
9
+ -> { where(concept: concept) }
10
+ end
11
+ end
12
+
13
+ has_many :tests,
14
+ make_obs_concept_filter(Lab::Metadata::TEST_TYPE_CONCEPT_NAME),
15
+ class_name: '::Lab::LabTest',
16
+ foreign_key: :order_id
17
+
18
+ has_many :results,
19
+ make_obs_concept_filter(Lab::Metadata::TEST_RESULT_CONCEPT_NAME),
20
+ class_name: 'Observation',
21
+ foreign_key: :order_id
22
+
23
+ has_one :reason_for_test,
24
+ make_obs_concept_filter(Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME),
25
+ class_name: 'Observation',
26
+ foreign_key: :order_id
27
+
28
+ has_one :requesting_clinician,
29
+ make_obs_concept_filter(Lab::Metadata::REQUESTING_CLINICIAN_CONCEPT_NAME),
30
+ class_name: 'Observation',
31
+ foreign_key: :order_id
32
+
33
+ has_one :target_lab,
34
+ make_obs_concept_filter(Lab::Metadata::TARGET_LAB_CONCEPT_NAME),
35
+ class_name: 'Observation',
36
+ foreign_key: :order_id
37
+
38
+ default_scope { joins(:order_type).merge(OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME)) }
39
+
40
+ def self.prefetch_relationships
41
+ includes(:reason_for_test,
42
+ :requesting_clinician,
43
+ :target_lab,
44
+ tests: [:result])
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class LabResult < Observation
5
+ alias measures children
6
+
7
+ default_scope do
8
+ lab_test_concept = ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
9
+ .select(:concept_id)
10
+ where(voided: 0, concept: lab_test_concept)
11
+ end
12
+
13
+ belongs_to :test,
14
+ (lambda do
15
+ where(concept: ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
16
+ .select(:concept_id))
17
+ end),
18
+ class_name: 'Observation',
19
+ foreign_key: :obs_group_id
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class LabTest < ::Observation
5
+ default_scope do
6
+ where(concept: ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME))
7
+ end
8
+
9
+ has_one :result,
10
+ -> { where(concept: ConceptName.where(name: Lab::Metadata::TEST_RESULT_CONCEPT_NAME)) },
11
+ class_name: '::Lab::LabResult',
12
+ foreign_key: :obs_group_id
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ module Lab
2
+ class LimsFailedImport < ApplicationRecord
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class LimsOrderMapping < ApplicationRecord
5
+ belongs_to :order, class_name: 'LabOrder', foreign_key: 'order_id'
6
+
7
+ validates_presence_of :lims_id, :order_id
8
+ validates_uniqueness_of :lims_id, :order_id
9
+ end
10
+ end
@@ -0,0 +1,49 @@
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.tests
7
+ requesting_clinician ||= order.requesting_clinician
8
+ reason_for_test ||= order.reason_for_test
9
+ target_lab ||= order.target_lab
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&.value_text,
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
+ end
49
+ end