his_emr_api_lab 1.1.22 → 1.1.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  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 +38 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +19 -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 +26 -0
  14. data/app/jobs/lab/application_job.rb +4 -0
  15. data/app/jobs/lab/push_order_job.rb +12 -0
  16. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  17. data/app/jobs/lab/void_order_job.rb +17 -0
  18. data/app/mailers/lab/application_mailer.rb +6 -0
  19. data/app/models/lab/application_record.rb +5 -0
  20. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  21. data/app/models/lab/lab_encounter.rb +7 -0
  22. data/app/models/lab/lab_order.rb +58 -0
  23. data/app/models/lab/lab_result.rb +31 -0
  24. data/app/models/lab/lab_test.rb +19 -0
  25. data/app/models/lab/lims_failed_import.rb +4 -0
  26. data/app/models/lab/lims_order_mapping.rb +10 -0
  27. data/app/serializers/lab/lab_order_serializer.rb +55 -0
  28. data/app/serializers/lab/result_serializer.rb +36 -0
  29. data/app/serializers/lab/test_serializer.rb +29 -0
  30. data/app/services/lab/accession_number_service.rb +77 -0
  31. data/app/services/lab/concepts_service.rb +82 -0
  32. data/app/services/lab/labelling_service/order_label.rb +106 -0
  33. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  34. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  35. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  36. data/app/services/lab/lims/api/rest_api.rb +416 -0
  37. data/app/services/lab/lims/api/ws_api.rb +121 -0
  38. data/app/services/lab/lims/api_factory.rb +19 -0
  39. data/app/services/lab/lims/config.rb +100 -0
  40. data/app/services/lab/lims/exceptions.rb +11 -0
  41. data/app/services/lab/lims/migrator.rb +216 -0
  42. data/app/services/lab/lims/order_dto.rb +105 -0
  43. data/app/services/lab/lims/order_serializer.rb +244 -0
  44. data/app/services/lab/lims/pull_worker.rb +289 -0
  45. data/app/services/lab/lims/push_worker.rb +149 -0
  46. data/app/services/lab/lims/utils.rb +91 -0
  47. data/app/services/lab/lims/worker.rb +86 -0
  48. data/app/services/lab/metadata.rb +24 -0
  49. data/app/services/lab/orders_search_service.rb +66 -0
  50. data/app/services/lab/orders_service.rb +212 -0
  51. data/app/services/lab/results_service.rb +149 -0
  52. data/app/services/lab/tests_service.rb +93 -0
  53. data/config/routes.rb +17 -0
  54. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  55. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  56. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  57. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  58. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  59. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  60. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  61. data/lib/auto12epl.rb +201 -0
  62. data/lib/couch_bum/couch_bum.rb +92 -0
  63. data/lib/generators/lab/install/USAGE +9 -0
  64. data/lib/generators/lab/install/install_generator.rb +19 -0
  65. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  66. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  67. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  68. data/lib/his_emr_api_lab.rb +5 -0
  69. data/lib/lab/engine.rb +15 -0
  70. data/lib/lab/version.rb +5 -0
  71. data/lib/logger_multiplexor.rb +38 -0
  72. data/lib/tasks/lab_tasks.rake +25 -0
  73. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  74. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  75. data/lib/tasks/loaders/data/tests.csv +161 -0
  76. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  77. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  78. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  79. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  80. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  81. metadata +81 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 074eeccc38e08701c9ad01db23814ea8b6add3b17ddc863d67a66c04d0b15488
4
- data.tar.gz: 9be44ee134d3ad50ad73ccaf9443c9bd7fe98d46c0a5459e80c62b1f7e3fc6a8
3
+ metadata.gz: 629116134d63ff0d2d3da97b80b0b9f4a3249483e5d57f43d4f332a645ce35e5
4
+ data.tar.gz: 414d3a1a4647e666599f71fca7e9026a034f210fbf6080da5cc501bb47c2e24f
5
5
  SHA512:
6
- metadata.gz: d34ffbf3a18d56a77eec06be6bcd7827a405d505ebb25de2c0c5624259f7e7c29e75fb13e912a47e474b8f261ed8568f47f7c0537e9afdbbfc7bef51563b98f0
7
- data.tar.gz: 567f5258355b56afd28da1f418d4524318cf41afef7994515be342e06f8f610369ff51317d1749f36db0d0d4ee2639dbee77482439cc6bc0dfc25467c81e9361
6
+ metadata.gz: b5bf3f01293464ab6f0be71a8707ab6b57801a371375f1bd6a2fae9812da72d4cba323a5b783e68f9b770db0f6e21c9a39162d89d3004add6b6a8327f7033cf1
7
+ data.tar.gz: 6c09429e8520fc49459c40737214e1c538079cdacf2b715a9dc856d32bf2b562a8e68a4d5ad9f97bfbe3ead532484f510cc944986475978090755787d118c6f2
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,71 @@
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
+ ## Configuration
47
+
48
+ This module in most cases should work without any configuration, however to enable
49
+ certain features some configuration may be required. Visit the
50
+ [configuration](./docs/configuration.md) page to learn how to configure the
51
+ application.
52
+
53
+ ## Contributing
54
+
55
+ Fork this application create a branch for the contribution you want to make,
56
+ push your changes to the branch and then issue a pull request. You may want
57
+ to create a new first on our repository, so that your pull request references
58
+ this issue.
59
+
60
+ If you are fixing a bug, it will be nice to add a unit test that exposes
61
+ the bug. Although this is not a requirement in most cases.
62
+
63
+ Be sure to follow [this](https://github.com/rubocop/ruby-style-guide) Ruby
64
+ style guide. We don't necessarily look for strict adherence to the guidelines
65
+ but too much a departure from it is frowned upon. For example, you will be forgiven
66
+ for writing a method with 15 to 20 lines if you clearly justify why you couldn't
67
+ break that method into multiple smaller methods.
68
+
69
+ ## License
70
+
71
+ 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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class LabelsController < ApplicationController
5
+ skip_before_action :authenticate
6
+
7
+ def print_order_label
8
+ order_id = params.require(:order_id)
9
+
10
+ label = LabellingService::OrderLabel.new(order_id)
11
+ send_data(label.print, type: 'application/label; charset=utf-8',
12
+ stream: false,
13
+ filename: "#{SecureRandom.hex(24)}.lbl",
14
+ disposition: 'inline')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
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
+ orders.each { |order| Lab::PushOrderJob.perform_later(order.fetch(:order_id)) }
12
+
13
+ render json: orders, status: :created
14
+ end
15
+
16
+ def update
17
+ specimen = params.require(:specimen).permit(:concept_id)
18
+ order = OrdersService.update_order(params[:id], specimen: specimen, force_update: params[:force_update])
19
+ Lab::PushOrderJob.perform_later(order.fetch(:order_id))
20
+
21
+ render json: order
22
+ end
23
+
24
+ def index
25
+ filters = params.permit(%i[patient_id accession_number date status])
26
+
27
+ Lab::UpdatePatientOrdersJob.perform_later(filters[:patient_id]) if filters[:patient_id]
28
+ render json: OrdersSearchService.find_orders(filters)
29
+ end
30
+
31
+ def destroy
32
+ OrdersService.void_order(params[:id], params[:reason])
33
+ Lab::VoidOrderJob.perform_later(params[:id])
34
+
35
+ render status: :no_content
36
+ end
37
+ end
38
+ 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, 'user entered')
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,26 @@
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(Lab::LabOrder.find(order_id), date, test_concepts)
18
+ Lab::PushOrderJob.perform_later(order_id)
19
+
20
+ render json: tests, status: :created
21
+ end
22
+
23
+ def service
24
+ Lab::TestsService
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ module Lab
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Push an order to LIMS.
6
+ class PushOrderJob < ApplicationJob
7
+ def perform(order_id)
8
+ push_worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
9
+ push_worker.push_order_by_id(order_id)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Fetches updates on a patient's orders from external sources.
6
+ class UpdatePatientOrdersJob < ApplicationJob
7
+ queue_as :default
8
+
9
+ def perform(patient_id)
10
+ Rails.logger.info('Initialising LIMS REST API...')
11
+
12
+ User.current = Lab::Lims::Utils.lab_user
13
+ Location.current = Location.find_by_name('ART clinic')
14
+
15
+ lockfile = Rails.root.join('tmp', "update-patient-orders-#{patient_id}.lock")
16
+
17
+ done = File.open(lockfile, File::RDWR | File::CREAT) do |lock|
18
+ unless lock.flock(File::LOCK_NB | File::LOCK_EX)
19
+ Rails.logger.info('Another update patient job is already running...')
20
+ break false
21
+ end
22
+
23
+ worker = Lab::Lims::PullWorker.new(Lab::Lims::ApiFactory.create_api)
24
+ worker.pull_orders(patient_id: patient_id)
25
+
26
+ true
27
+ end
28
+
29
+ File.unlink(lockfile) if done
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ class VoidOrderJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(order_id)
8
+ Rails.logger.info("Voiding order ##{order_id} in LIMS")
9
+
10
+ User.current = Lab::Lims::Utils.lab_user
11
+ Location.current = Location.find_by_name('ART clinic')
12
+
13
+ worker = Lab::Lims::PushWorker.new(Lab::Lims::ApiFactory.create_api)
14
+ worker.push_order(Lab::LabOrder.unscoped.find(order_id))
15
+ end
16
+ end
17
+ 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,58 @@
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
+ has_one :mapping,
39
+ class_name: '::Lab::LimsOrderMapping',
40
+ foreign_key: :order_id
41
+
42
+ default_scope do
43
+ joins(:order_type)
44
+ .merge(OrderType.where(name: Lab::Metadata::ORDER_TYPE_NAME))
45
+ .where.not(concept_id: ConceptName.where(name: 'Tests ordered').select(:concept_id))
46
+ end
47
+
48
+ scope :drawn, -> { where.not(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
49
+ scope :not_drawn, -> { where(concept_id: ConceptName.where(name: 'Unknown').select(:concept_id)) }
50
+
51
+ def self.prefetch_relationships
52
+ includes(:reason_for_test,
53
+ :requesting_clinician,
54
+ :target_lab,
55
+ tests: [:result])
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,31 @@
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
+
21
+ def void(reason)
22
+ children.each do |measure|
23
+ # Need to have a LabResultMeasure model that privately handles it's children
24
+ measure.children.each { |provider| provider.void(reason) }
25
+ measure.void(reason)
26
+ end
27
+
28
+ super(reason)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
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
+
14
+ def void(reason)
15
+ result&.void(reason)
16
+ super(reason)
17
+ end
18
+ end
19
+ 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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module LabOrderSerializer
5
+ def self.serialize_order(order, tests: nil, requesting_clinician: nil, reason_for_test: nil, target_lab: nil)
6
+ tests ||= order.voided == 1 ? voided_tests(order) : order.tests
7
+ requesting_clinician ||= order.requesting_clinician
8
+ reason_for_test ||= order.reason_for_test
9
+ target_lab = target_lab&.value_text || order.target_lab&.value_text || Location.current_health_center&.name
10
+
11
+ ActiveSupport::HashWithIndifferentAccess.new(
12
+ {
13
+ id: order.order_id,
14
+ order_id: order.order_id, # Deprecated: Link to :id
15
+ encounter_id: order.encounter_id,
16
+ order_date: order.start_date,
17
+ patient_id: order.patient_id,
18
+ accession_number: order.accession_number,
19
+ specimen: {
20
+ concept_id: order.concept_id,
21
+ name: concept_name(order.concept_id)
22
+ },
23
+ requesting_clinician: requesting_clinician&.value_text,
24
+ target_lab: target_lab,
25
+ reason_for_test: {
26
+ concept_id: reason_for_test&.value_coded,
27
+ name: concept_name(reason_for_test&.value_coded)
28
+ },
29
+ tests: tests.map do |test|
30
+ result_obs = test.children.first
31
+
32
+ {
33
+ id: test.obs_id,
34
+ concept_id: test.value_coded,
35
+ name: concept_name(test.value_coded),
36
+ result: result_obs && ResultSerializer.serialize(result_obs)
37
+ }
38
+ end
39
+ }
40
+ )
41
+ end
42
+
43
+ def self.concept_name(concept_id)
44
+ return concept_id unless concept_id
45
+
46
+ ConceptName.select(:name).find_by_concept_id(concept_id)&.name
47
+ end
48
+
49
+ def self.voided_tests(order)
50
+ concept = ConceptName.where(name: Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
51
+ .select(:concept_id)
52
+ LabTest.unscoped.where(concept: concept, order: order, voided: true)
53
+ end
54
+ end
55
+ end
@@ -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)
29
+
30
+ return [value, field.split('_')[1]] if value
31
+ end
32
+
33
+ [nil, 'unknown']
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module TestSerializer
5
+ def self.serialize(test, order: nil, result: nil)
6
+ order ||= test.order
7
+ result ||= test.result
8
+
9
+ {
10
+ id: test.obs_id,
11
+ concept_id: test.value_coded,
12
+ name: ConceptName.find_by_concept_id(test.value_coded)&.name,
13
+ order: {
14
+ id: order.order_id,
15
+ concept_id: order.concept_id,
16
+ name: ConceptName.find_by_concept_id(order.concept_id)&.name,
17
+ accession_number: order.accession_number
18
+ },
19
+ result: if result
20
+ {
21
+ id: result.obs_id,
22
+ modifier: result.value_modifier,
23
+ value: result.value_text
24
+ }
25
+ end
26
+ }
27
+ end
28
+ end
29
+ end