his_emr_api_lab 0.0.2
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 +52 -0
- data/Rakefile +32 -0
- data/app/controllers/lab/application_controller.rb +6 -0
- data/app/controllers/lab/orders_controller.rb +34 -0
- data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
- data/app/controllers/lab/results_controller.rb +19 -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/jobs/lab/application_job.rb +4 -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_encounter.rb +7 -0
- data/app/models/lab/lab_order.rb +47 -0
- data/app/models/lab/lab_result.rb +21 -0
- data/app/models/lab/lab_test.rb +14 -0
- data/app/models/lab/lims_failed_import.rb +4 -0
- data/app/models/lab/lims_order_mapping.rb +10 -0
- data/app/serializers/lab/lab_order_serializer.rb +49 -0
- data/app/serializers/lab/result_serializer.rb +36 -0
- data/app/serializers/lab/test_serializer.rb +29 -0
- data/app/services/lab/accession_number_service.rb +77 -0
- data/app/services/lab/concepts_service.rb +82 -0
- data/app/services/lab/lims/api.rb +46 -0
- data/app/services/lab/lims/config.rb +56 -0
- data/app/services/lab/lims/order_dto.rb +177 -0
- data/app/services/lab/lims/order_serializer.rb +112 -0
- data/app/services/lab/lims/utils.rb +27 -0
- data/app/services/lab/lims/worker.rb +121 -0
- data/app/services/lab/metadata.rb +23 -0
- data/app/services/lab/orders_search_service.rb +48 -0
- data/app/services/lab/orders_service.rb +194 -0
- data/app/services/lab/results_service.rb +92 -0
- data/app/services/lab/tests_service.rb +93 -0
- data/config/routes.rb +15 -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/lib/couch_bum/couch_bum.rb +77 -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 +682 -0
- data/lib/his_emr_api_lab.rb +5 -0
- data/lib/lab/engine.rb +15 -0
- data/lib/lab/version.rb +5 -0
- data/lib/logger_multiplexor.rb +32 -0
- data/lib/tasks/lab_tasks.rake +25 -0
- data/lib/tasks/loaders/data/reasons-for-test.csv +6 -0
- data/lib/tasks/loaders/data/test-measures.csv +224 -0
- data/lib/tasks/loaders/data/tests.csv +142 -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 +296 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
module ResultsService
|
5
|
+
class << self
|
6
|
+
def create_results(test_id, params)
|
7
|
+
ActiveRecord::Base.transaction do
|
8
|
+
test = Lab::LabTest.find(test_id)
|
9
|
+
encounter = find_encounter(test, encounter_id: params[:encounter_id],
|
10
|
+
date: params[:date],
|
11
|
+
provider_id: params[:provider_id])
|
12
|
+
|
13
|
+
results_obs = create_results_obs(encounter, test, params[:date])
|
14
|
+
params[:measures].map { |measure| add_measure_to_results(results_obs, measure, params[:date]) }
|
15
|
+
|
16
|
+
Lab::ResultSerializer.serialize(results_obs)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def find_encounter(test, encounter_id: nil, date: nil, provider_id: nil)
|
23
|
+
return Encounter.find(encounter_id) if encounter_id
|
24
|
+
|
25
|
+
Encounter.create!(
|
26
|
+
patient_id: test.person_id,
|
27
|
+
program_id: test.encounter.program_id,
|
28
|
+
type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
|
29
|
+
encounter_datetime: date || Date.today,
|
30
|
+
provider_id: provider_id || User.current.user_id
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates the parent observation for results to which the different measures are attached
|
35
|
+
def create_results_obs(encounter, test, date)
|
36
|
+
Lab::LabResult.create!(
|
37
|
+
person_id: encounter.patient_id,
|
38
|
+
encounter_id: encounter.encounter_id,
|
39
|
+
concept_id: ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME).concept_id,
|
40
|
+
order_id: test.order_id,
|
41
|
+
obs_group_id: test.obs_id,
|
42
|
+
obs_datetime: date&.to_datetime || DateTime.now
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_measure_to_results(results_obs, params, date)
|
47
|
+
validate_measure_params(params)
|
48
|
+
|
49
|
+
Observation.create!(
|
50
|
+
person_id: results_obs.person_id,
|
51
|
+
encounter_id: results_obs.encounter_id,
|
52
|
+
concept_id: params[:indicator][:concept_id],
|
53
|
+
obs_group_id: results_obs.obs_id,
|
54
|
+
obs_datetime: date&.to_datetime || DateTime.now,
|
55
|
+
**make_measure_value(params)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_measure_params(params)
|
60
|
+
raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
|
61
|
+
|
62
|
+
if params[:indicator]&.[](:concept_id).blank?
|
63
|
+
raise InvalidParameterError, 'measures.indicator.concept_id is required'
|
64
|
+
end
|
65
|
+
|
66
|
+
params
|
67
|
+
end
|
68
|
+
|
69
|
+
# Converts user provided measure values to observation_values
|
70
|
+
def make_measure_value(params)
|
71
|
+
obs_value = { value_modifier: params[:value_modifier] }
|
72
|
+
value_type = params[:value_type] || 'text'
|
73
|
+
|
74
|
+
case value_type.downcase
|
75
|
+
when 'numeric' then obs_value.merge(value_numeric: params[:value])
|
76
|
+
when 'boolean' then obs_value.merge(value_boolean: parse_boolen_value(params[:value]))
|
77
|
+
when 'coded' then obs_value.merge(value_coded: params[:value]) # Should we be collecting value_name_coded_id?
|
78
|
+
when 'text' then obs_value.merge(value_text: params[:value])
|
79
|
+
else raise InvalidParameterError, "Invalid value_type: #{params[:value_type]}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_boolen_value(string)
|
84
|
+
case string.downcase
|
85
|
+
when 'true' then true
|
86
|
+
when 'false' then false
|
87
|
+
else raise InvalidParameterError, "Invalid boolean value: #{string}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
##
|
5
|
+
# Manage tests that have been ordered through the ordering service.
|
6
|
+
module TestsService
|
7
|
+
class << self
|
8
|
+
def find_tests(filters)
|
9
|
+
tests = Lab::LabTest.all
|
10
|
+
|
11
|
+
tests = filter_tests(tests, test_type_id: filters.delete(:test_type_id),
|
12
|
+
patient_id: filters.delete(:patient_id))
|
13
|
+
|
14
|
+
tests = filter_tests_by_results(tests) if %w[1 true].include?(filters[:pending_results]&.downcase)
|
15
|
+
|
16
|
+
tests = filter_tests_by_order(tests, accession_number: filters.delete(:accession_number),
|
17
|
+
order_date: filters.delete(:order_date),
|
18
|
+
specimen_type_id: filters.delete(:specimen_type_id))
|
19
|
+
|
20
|
+
tests.map { |test| Lab::TestSerializer.serialize(test) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_tests(order, date, tests_params)
|
24
|
+
raise InvalidParameterError, 'tests are required' if tests_params.nil? || tests_params.empty?
|
25
|
+
|
26
|
+
Lab::LabTest.transaction do
|
27
|
+
tests_params.map do |params|
|
28
|
+
test = Lab::LabTest.create!(
|
29
|
+
concept_id: ConceptName.find_by_name!(Lab::Metadata::TEST_TYPE_CONCEPT_NAME)
|
30
|
+
.concept_id,
|
31
|
+
encounter_id: order.encounter_id,
|
32
|
+
order_id: order.order_id,
|
33
|
+
person_id: order.patient_id,
|
34
|
+
obs_datetime: date&.to_time || Time.now,
|
35
|
+
value_coded: params[:concept_id]
|
36
|
+
)
|
37
|
+
|
38
|
+
Lab::TestSerializer.serialize(test, order: order)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
##
|
46
|
+
# Filter a LabTests Relation.
|
47
|
+
def filter_tests(tests, test_type_id: nil, patient_id: nil)
|
48
|
+
tests = tests.where(value_coded: test_type_id) if test_type_id
|
49
|
+
tests = tests.where(person_id: patient_id) if patient_id
|
50
|
+
|
51
|
+
tests
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Filter out any tests having results
|
56
|
+
def filter_tests_by_results(tests)
|
57
|
+
tests.where.not(obs_id: Lab::LabResult.all.select(:obs_group_id))
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Filter LabTests Relation using their parent orders parameters.
|
62
|
+
def filter_tests_by_order(tests, accession_number: nil, order_date: nil, specimen_type_id: nil)
|
63
|
+
return tests unless accession_number || order_date || specimen_type_id
|
64
|
+
|
65
|
+
lab_orders = filter_orders(Lab::LabOrder.all, accession_number: accession_number,
|
66
|
+
order_date: order_date,
|
67
|
+
specimen_type_id: specimen_type_id)
|
68
|
+
tests.joins(:order).merge(lab_orders)
|
69
|
+
end
|
70
|
+
|
71
|
+
def filter_orders(orders, accession_number: nil, order_date: nil, specimen_type_id: nil)
|
72
|
+
if order_date
|
73
|
+
order_date = order_date.to_date
|
74
|
+
orders = orders.where('start_date >= ? AND start_date < ?', order_date, order_date + 1.day)
|
75
|
+
end
|
76
|
+
|
77
|
+
orders = orders.where(accession_number: accession_number) if accession_number
|
78
|
+
orders = orders.where(concept_id: specimen_type_id) if specimen_type_id
|
79
|
+
|
80
|
+
orders
|
81
|
+
end
|
82
|
+
|
83
|
+
def create_test(order, date, test_type_id)
|
84
|
+
create_order_observation(
|
85
|
+
order,
|
86
|
+
Lab::Metadata::TEST_TYPE_CONCEPT_NAME,
|
87
|
+
date,
|
88
|
+
value_coded: test_type_id
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Lab::Engine.routes.draw do
|
4
|
+
resources :orders, path: 'api/v1/lab/orders'
|
5
|
+
resources :tests, path: 'api/v1/lab/tests', except: %i[update] do # ?pending=true to select tests without results?
|
6
|
+
resources :results, only: %i[index create destroy]
|
7
|
+
end
|
8
|
+
|
9
|
+
# Metadata
|
10
|
+
# TODO: Move the following to namespace /concepts
|
11
|
+
resources :specimen_types, only: %i[index], path: 'api/v1/lab/specimen_types'
|
12
|
+
resources :test_result_indicators, only: %i[index], path: 'api/v1/lab/test_result_indicators'
|
13
|
+
resources :test_types, only: %i[index], path: 'api/v1/lab/test_types'
|
14
|
+
resources :reasons_for_test, only: %i[index], path: 'api/v1/lab/reasons_for_test'
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateLabLimsOrderMappings < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :lab_lims_order_mappings do |t|
|
4
|
+
t.integer :lims_id, null: false, unique: true
|
5
|
+
t.integer :order_id, null: false, unique: true
|
6
|
+
t.datetime :pushed_at
|
7
|
+
t.datetime :pulled_at
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
|
11
|
+
t.foreign_key :orders, primary_key: :order_id, column: :order_id
|
12
|
+
t.index :lims_id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ChangeLimsIdToStringInLimsOrderMapping < ActiveRecord::Migration[5.2]
|
4
|
+
def change
|
5
|
+
reversible do |direction|
|
6
|
+
direction.up do
|
7
|
+
change_column :lab_lims_order_mappings, :lims_id, :string, null: false
|
8
|
+
end
|
9
|
+
|
10
|
+
direction.down do
|
11
|
+
change_column :lab_lims_order_mappings, :lims_id, :integer, null: false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateLabLimsFailedImports < ActiveRecord::Migration[5.2]
|
4
|
+
def change
|
5
|
+
create_table :lab_lims_failed_imports do |t|
|
6
|
+
t.string :lims_id, null: false
|
7
|
+
t.string :tracking_number, null: false
|
8
|
+
t.string :patient_nhid, null: false
|
9
|
+
t.string :reason, null: false
|
10
|
+
t.string :diff, limit: 2048
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
|
14
|
+
t.index :lims_id
|
15
|
+
t.index :patient_nhid
|
16
|
+
t.index :tracking_number
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'couchrest'
|
5
|
+
|
6
|
+
##
|
7
|
+
# A CouchRest wrapper for the changes API.
|
8
|
+
#
|
9
|
+
# See: https://github.com/couchrest/couchrest
|
10
|
+
class CouchBum
|
11
|
+
def initialize(database:, protocol: 'http', host: 'localhost', port: 5984, username: nil, password: nil)
|
12
|
+
@connection_string = make_connection_string(protocol, username, password, host, port, database)
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Attaches to the Changes API and streams the updates to passed block.
|
17
|
+
#
|
18
|
+
# This is a blocking call that only stops when there are no more
|
19
|
+
# changes to pull or is explicitly terminated by calling +choke+
|
20
|
+
# within the passed block.
|
21
|
+
def binge_changes(since: 0, limit: nil, include_docs: nil, &block)
|
22
|
+
catch(:choke) do
|
23
|
+
extra_params = stringify_params(limit: limit, include_docs: include_docs)
|
24
|
+
|
25
|
+
changes = couch_rest(:get, "_changes?since=#{since}&#{extra_params}")
|
26
|
+
context = BingeContext.new(changes)
|
27
|
+
changes['results'].each { |change| context.instance_exec(change, &block) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def couch_rest(method, route, *args, **kwargs)
|
32
|
+
CouchRest.send(method, expand_route(route), *args, **kwargs)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Context under which the callback passed to binge_changes is executed.
|
38
|
+
class BingeContext
|
39
|
+
def initialize(changes)
|
40
|
+
@changes = changes
|
41
|
+
end
|
42
|
+
|
43
|
+
def choke
|
44
|
+
throw :choke
|
45
|
+
end
|
46
|
+
|
47
|
+
def last_seq
|
48
|
+
@changes['last_seq']
|
49
|
+
end
|
50
|
+
|
51
|
+
def pending
|
52
|
+
@changes['pending']
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def make_connection_string(protocol, username, password, host, port, database)
|
57
|
+
auth = username ? "#{CGI.escape(username)}:#{CGI.escape(password)}@" : ''
|
58
|
+
|
59
|
+
"#{protocol}://#{auth}#{host}:#{port}/#{database}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def expand_route(route)
|
63
|
+
route = route.gsub(%r{^/+}, '')
|
64
|
+
|
65
|
+
"#{@connection_string}/#{route}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def stringify_params(params)
|
69
|
+
params.reduce('') do |str_params, entry|
|
70
|
+
name, value = entry
|
71
|
+
next params unless value
|
72
|
+
|
73
|
+
param = "#{name}=#{value}"
|
74
|
+
str_params.empty? ? param : "#{str_params}&#{param}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
source_root File.expand_path('templates', __dir__)
|
6
|
+
|
7
|
+
def copy_openapi_docs
|
8
|
+
copy_file('swagger.yaml', 'swagger/lab/v1/swagger.yaml')
|
9
|
+
end
|
10
|
+
|
11
|
+
def copy_rswag_initializer
|
12
|
+
copy_file('rswag-ui-lab.rb', 'config/initializers/rswag-ui-lab.rb')
|
13
|
+
end
|
14
|
+
|
15
|
+
def copy_worker
|
16
|
+
copy_file('start_worker.rb', 'bin/lab/start_worker.rb')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger_multiplexor'
|
4
|
+
|
5
|
+
Rails.logger = LoggerMultiplexor.new(Rails.root.join('log/lims-push.log'), $stdout)
|
6
|
+
api = Lab::Lims::Api.new
|
7
|
+
worker = Lab::Lims::Worker.new(api)
|
8
|
+
|
9
|
+
def with_lock(lock_file)
|
10
|
+
File.open("log/#{lock_file}", File::RDWR | File::CREAT, 0o644) do |file|
|
11
|
+
unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
12
|
+
Rails.logger.warn("Failed to start new process due to lock: #{lock_file}")
|
13
|
+
exit 2
|
14
|
+
end
|
15
|
+
|
16
|
+
file.rewind
|
17
|
+
file.puts("Process ##{Process.pid} started at #{Time.now}")
|
18
|
+
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
case ARGV[0]&.downcase
|
24
|
+
when 'push'
|
25
|
+
with_lock('lims-push.lock') { worker.push_orders }
|
26
|
+
when 'pull'
|
27
|
+
with_lock('lims-pull.lock') { worker.pull_orders }
|
28
|
+
else
|
29
|
+
warn 'Error: No or invalid action specified: Valid actions are push and pull'
|
30
|
+
warn 'USAGE: rails runner start_worker.rb push'
|
31
|
+
exit 1
|
32
|
+
end
|
@@ -0,0 +1,682 @@
|
|
1
|
+
---
|
2
|
+
openapi: 3.0.1
|
3
|
+
info:
|
4
|
+
title: API V1
|
5
|
+
version: v1
|
6
|
+
paths:
|
7
|
+
"/api/v1/lab/orders":
|
8
|
+
post:
|
9
|
+
summary: Create order
|
10
|
+
tags:
|
11
|
+
- Orders
|
12
|
+
description: |
|
13
|
+
Create a lab order for a test.
|
14
|
+
|
15
|
+
Broadly a lab order consists of a test type and a number of specimens.
|
16
|
+
To each specimen is assigned a tracking number which can be used
|
17
|
+
to query the status and results of the specimen.
|
18
|
+
parameters: []
|
19
|
+
security:
|
20
|
+
- api_key: []
|
21
|
+
responses:
|
22
|
+
'201':
|
23
|
+
description: Created
|
24
|
+
content:
|
25
|
+
application/json:
|
26
|
+
schema:
|
27
|
+
type: array
|
28
|
+
items:
|
29
|
+
type: object
|
30
|
+
properties:
|
31
|
+
id:
|
32
|
+
type: integer
|
33
|
+
patient_id:
|
34
|
+
type: integer
|
35
|
+
encounter_id:
|
36
|
+
type: integer
|
37
|
+
order_date:
|
38
|
+
type: string
|
39
|
+
format: datetime
|
40
|
+
accession_number:
|
41
|
+
type: string
|
42
|
+
specimen:
|
43
|
+
type: object
|
44
|
+
properties:
|
45
|
+
concept_id:
|
46
|
+
type: integer
|
47
|
+
name:
|
48
|
+
type: string
|
49
|
+
required:
|
50
|
+
- concept_id
|
51
|
+
- name
|
52
|
+
requesting_clinician:
|
53
|
+
type: string
|
54
|
+
nullable: true
|
55
|
+
target_lab:
|
56
|
+
type: string
|
57
|
+
reason_for_test:
|
58
|
+
type: object
|
59
|
+
properties:
|
60
|
+
concept_id:
|
61
|
+
type: integer
|
62
|
+
name:
|
63
|
+
type: string
|
64
|
+
required:
|
65
|
+
- concept_id
|
66
|
+
- name
|
67
|
+
tests:
|
68
|
+
type: array
|
69
|
+
items:
|
70
|
+
type: object
|
71
|
+
properties:
|
72
|
+
id:
|
73
|
+
type: integer
|
74
|
+
concept_id:
|
75
|
+
type: integer
|
76
|
+
name:
|
77
|
+
type: string
|
78
|
+
result:
|
79
|
+
type: object
|
80
|
+
nullable: true
|
81
|
+
properties:
|
82
|
+
id:
|
83
|
+
type: integer
|
84
|
+
value:
|
85
|
+
type: string
|
86
|
+
nullable: true
|
87
|
+
date:
|
88
|
+
type: string
|
89
|
+
format: datetime
|
90
|
+
nullable: true
|
91
|
+
required:
|
92
|
+
- id
|
93
|
+
- value
|
94
|
+
- date
|
95
|
+
required:
|
96
|
+
- id
|
97
|
+
- concept_id
|
98
|
+
- name
|
99
|
+
required:
|
100
|
+
- id
|
101
|
+
- specimen
|
102
|
+
- reason_for_test
|
103
|
+
- accession_number
|
104
|
+
- patient_id
|
105
|
+
- order_date
|
106
|
+
requestBody:
|
107
|
+
content:
|
108
|
+
application/json:
|
109
|
+
schema:
|
110
|
+
type: object
|
111
|
+
properties:
|
112
|
+
orders:
|
113
|
+
type: array
|
114
|
+
items:
|
115
|
+
properties:
|
116
|
+
encounter_id:
|
117
|
+
type: integer
|
118
|
+
specimen:
|
119
|
+
type: object
|
120
|
+
properties:
|
121
|
+
concept_id:
|
122
|
+
type: integer
|
123
|
+
description: Specimen type concept ID (see GET /lab/test_types)
|
124
|
+
tests:
|
125
|
+
type: array
|
126
|
+
items:
|
127
|
+
type: object
|
128
|
+
properties:
|
129
|
+
concept_id:
|
130
|
+
type: integer
|
131
|
+
description: Test type concept ID (see GET /lab/test_types)
|
132
|
+
requesting_clinician:
|
133
|
+
type: string
|
134
|
+
description: Fullname of the clinician requesting the test
|
135
|
+
(defaults to orderer)
|
136
|
+
target_lab:
|
137
|
+
type: string
|
138
|
+
reason_for_test_id:
|
139
|
+
type: string
|
140
|
+
description: One of routine, targeted, or confirmatory
|
141
|
+
required:
|
142
|
+
- encounter_id
|
143
|
+
- tests
|
144
|
+
- target_lab
|
145
|
+
- reason_for_test_id
|
146
|
+
get:
|
147
|
+
summary: Retrieve lab orders
|
148
|
+
tags:
|
149
|
+
- Orders
|
150
|
+
description: Search/retrieve for lab orders.
|
151
|
+
security:
|
152
|
+
- api_key: []
|
153
|
+
parameters:
|
154
|
+
- name: patient_id
|
155
|
+
in: query
|
156
|
+
required: false
|
157
|
+
description: Filter orders using patient_id
|
158
|
+
schema:
|
159
|
+
type: integer
|
160
|
+
- name: accession_number
|
161
|
+
in: query
|
162
|
+
required: false
|
163
|
+
description: Filter orders using sample accession number
|
164
|
+
schema:
|
165
|
+
type: integer
|
166
|
+
- name: date
|
167
|
+
in: query
|
168
|
+
required: false
|
169
|
+
description: Select results falling on a specific date
|
170
|
+
schema:
|
171
|
+
type: date
|
172
|
+
- name: status
|
173
|
+
in: query
|
174
|
+
required: false
|
175
|
+
description: 'Filter by sample status: ordered, drawn'
|
176
|
+
schema:
|
177
|
+
type: string
|
178
|
+
responses:
|
179
|
+
'200':
|
180
|
+
description: Success
|
181
|
+
content:
|
182
|
+
application/json:
|
183
|
+
schema:
|
184
|
+
type: array
|
185
|
+
items:
|
186
|
+
type: object
|
187
|
+
properties:
|
188
|
+
id:
|
189
|
+
type: integer
|
190
|
+
patient_id:
|
191
|
+
type: integer
|
192
|
+
encounter_id:
|
193
|
+
type: integer
|
194
|
+
order_date:
|
195
|
+
type: string
|
196
|
+
format: datetime
|
197
|
+
accession_number:
|
198
|
+
type: string
|
199
|
+
specimen:
|
200
|
+
type: object
|
201
|
+
properties:
|
202
|
+
concept_id:
|
203
|
+
type: integer
|
204
|
+
name:
|
205
|
+
type: string
|
206
|
+
required:
|
207
|
+
- concept_id
|
208
|
+
- name
|
209
|
+
requesting_clinician:
|
210
|
+
type: string
|
211
|
+
nullable: true
|
212
|
+
target_lab:
|
213
|
+
type: string
|
214
|
+
reason_for_test:
|
215
|
+
type: object
|
216
|
+
properties:
|
217
|
+
concept_id:
|
218
|
+
type: integer
|
219
|
+
name:
|
220
|
+
type: string
|
221
|
+
required:
|
222
|
+
- concept_id
|
223
|
+
- name
|
224
|
+
tests:
|
225
|
+
type: array
|
226
|
+
items:
|
227
|
+
type: object
|
228
|
+
properties:
|
229
|
+
id:
|
230
|
+
type: integer
|
231
|
+
concept_id:
|
232
|
+
type: integer
|
233
|
+
name:
|
234
|
+
type: string
|
235
|
+
result:
|
236
|
+
type: object
|
237
|
+
nullable: true
|
238
|
+
properties:
|
239
|
+
id:
|
240
|
+
type: integer
|
241
|
+
value:
|
242
|
+
type: string
|
243
|
+
nullable: true
|
244
|
+
date:
|
245
|
+
type: string
|
246
|
+
format: datetime
|
247
|
+
nullable: true
|
248
|
+
required:
|
249
|
+
- id
|
250
|
+
- value
|
251
|
+
- date
|
252
|
+
required:
|
253
|
+
- id
|
254
|
+
- concept_id
|
255
|
+
- name
|
256
|
+
required:
|
257
|
+
- id
|
258
|
+
- specimen
|
259
|
+
- reason_for_test
|
260
|
+
- accession_number
|
261
|
+
- patient_id
|
262
|
+
- order_date
|
263
|
+
"/api/v1/lab/orders/{order_id}":
|
264
|
+
put:
|
265
|
+
summary: Update order
|
266
|
+
tags:
|
267
|
+
- Orders
|
268
|
+
description: Update an existing order
|
269
|
+
security:
|
270
|
+
- api_key: []
|
271
|
+
parameters:
|
272
|
+
- name: order_id
|
273
|
+
in: path
|
274
|
+
required: true
|
275
|
+
schema:
|
276
|
+
type: integer
|
277
|
+
responses:
|
278
|
+
'200':
|
279
|
+
description: Ok
|
280
|
+
content:
|
281
|
+
application/json:
|
282
|
+
schema:
|
283
|
+
type: object
|
284
|
+
properties:
|
285
|
+
type: object
|
286
|
+
properties:
|
287
|
+
id:
|
288
|
+
type: integer
|
289
|
+
patient_id:
|
290
|
+
type: integer
|
291
|
+
encounter_id:
|
292
|
+
type: integer
|
293
|
+
order_date:
|
294
|
+
type: string
|
295
|
+
format: datetime
|
296
|
+
accession_number:
|
297
|
+
type: string
|
298
|
+
specimen:
|
299
|
+
type: object
|
300
|
+
properties:
|
301
|
+
concept_id:
|
302
|
+
type: integer
|
303
|
+
name:
|
304
|
+
type: string
|
305
|
+
required:
|
306
|
+
- concept_id
|
307
|
+
- name
|
308
|
+
requesting_clinician:
|
309
|
+
type: string
|
310
|
+
nullable: true
|
311
|
+
target_lab:
|
312
|
+
type: string
|
313
|
+
reason_for_test:
|
314
|
+
type: object
|
315
|
+
properties:
|
316
|
+
concept_id:
|
317
|
+
type: integer
|
318
|
+
name:
|
319
|
+
type: string
|
320
|
+
required:
|
321
|
+
- concept_id
|
322
|
+
- name
|
323
|
+
tests:
|
324
|
+
type: array
|
325
|
+
items:
|
326
|
+
type: object
|
327
|
+
properties:
|
328
|
+
id:
|
329
|
+
type: integer
|
330
|
+
concept_id:
|
331
|
+
type: integer
|
332
|
+
name:
|
333
|
+
type: string
|
334
|
+
result:
|
335
|
+
type: object
|
336
|
+
nullable: true
|
337
|
+
properties:
|
338
|
+
id:
|
339
|
+
type: integer
|
340
|
+
value:
|
341
|
+
type: string
|
342
|
+
nullable: true
|
343
|
+
date:
|
344
|
+
type: string
|
345
|
+
format: datetime
|
346
|
+
nullable: true
|
347
|
+
required:
|
348
|
+
- id
|
349
|
+
- value
|
350
|
+
- date
|
351
|
+
required:
|
352
|
+
- id
|
353
|
+
- concept_id
|
354
|
+
- name
|
355
|
+
required:
|
356
|
+
- id
|
357
|
+
- specimen
|
358
|
+
- reason_for_test
|
359
|
+
- accession_number
|
360
|
+
- patient_id
|
361
|
+
- order_date
|
362
|
+
requestBody:
|
363
|
+
content:
|
364
|
+
application/json:
|
365
|
+
schema:
|
366
|
+
type: object
|
367
|
+
properties:
|
368
|
+
specimen:
|
369
|
+
type: object
|
370
|
+
properties:
|
371
|
+
concept_id:
|
372
|
+
type: integer
|
373
|
+
required:
|
374
|
+
- concept_id
|
375
|
+
delete:
|
376
|
+
summary: Void lab order
|
377
|
+
tags:
|
378
|
+
- Orders
|
379
|
+
description: |
|
380
|
+
Void a lab order and all it's associated records
|
381
|
+
|
382
|
+
This action voids an order, all it's linked tests and results.
|
383
|
+
security:
|
384
|
+
- api_key: []
|
385
|
+
parameters:
|
386
|
+
- name: order_id
|
387
|
+
in: path
|
388
|
+
required: true
|
389
|
+
schema:
|
390
|
+
type: integer
|
391
|
+
- name: reason
|
392
|
+
in: query
|
393
|
+
required: true
|
394
|
+
schema:
|
395
|
+
type: string
|
396
|
+
responses:
|
397
|
+
'204':
|
398
|
+
description: No Content
|
399
|
+
"/api/v1/lab/tests/{test_id}/results":
|
400
|
+
post:
|
401
|
+
summary: Add results to order
|
402
|
+
tags:
|
403
|
+
- Results
|
404
|
+
description: Attach results to specimens on order
|
405
|
+
parameters:
|
406
|
+
- name: test_id
|
407
|
+
in: path
|
408
|
+
required: true
|
409
|
+
schema:
|
410
|
+
type: integer
|
411
|
+
security:
|
412
|
+
- api_key: []
|
413
|
+
responses:
|
414
|
+
'201':
|
415
|
+
description: Created
|
416
|
+
requestBody:
|
417
|
+
content:
|
418
|
+
application/json:
|
419
|
+
schema:
|
420
|
+
type: object
|
421
|
+
properties:
|
422
|
+
encounter_id:
|
423
|
+
type: integer
|
424
|
+
provider_id:
|
425
|
+
type: integer
|
426
|
+
date:
|
427
|
+
type: string
|
428
|
+
measures:
|
429
|
+
type: array
|
430
|
+
items:
|
431
|
+
type: object
|
432
|
+
properties:
|
433
|
+
indicator:
|
434
|
+
type: object
|
435
|
+
properties:
|
436
|
+
concept_id:
|
437
|
+
type: integer
|
438
|
+
description: Concept ID of a test result indicator for
|
439
|
+
this test (see GET /test_result_indicators)
|
440
|
+
required:
|
441
|
+
- concept_id
|
442
|
+
value:
|
443
|
+
type: string
|
444
|
+
example: LDL
|
445
|
+
value_modifier:
|
446
|
+
type: string
|
447
|
+
example: "="
|
448
|
+
value_type:
|
449
|
+
type: string
|
450
|
+
enum:
|
451
|
+
- text
|
452
|
+
- boolean
|
453
|
+
- numeric
|
454
|
+
- coded
|
455
|
+
description: Determines under what column the value is to
|
456
|
+
be saved under in the obs table (defaults to text)
|
457
|
+
example: text
|
458
|
+
required:
|
459
|
+
- indicator
|
460
|
+
- value
|
461
|
+
required:
|
462
|
+
- measures
|
463
|
+
"/api/v1/lab/specimen_types":
|
464
|
+
get:
|
465
|
+
summary: Specimen types
|
466
|
+
tags:
|
467
|
+
- Concepts
|
468
|
+
description: Retrieve all specimen types
|
469
|
+
security:
|
470
|
+
- api_key: []
|
471
|
+
parameters:
|
472
|
+
- name: test_type
|
473
|
+
in: query
|
474
|
+
required: false
|
475
|
+
description: Select specimen types having this test type only
|
476
|
+
schema:
|
477
|
+
type: string
|
478
|
+
responses:
|
479
|
+
'200':
|
480
|
+
description: Success
|
481
|
+
content:
|
482
|
+
application/json:
|
483
|
+
schema:
|
484
|
+
type: array
|
485
|
+
items:
|
486
|
+
type: object
|
487
|
+
properties:
|
488
|
+
concept_id:
|
489
|
+
type: integer
|
490
|
+
name:
|
491
|
+
type: string
|
492
|
+
required:
|
493
|
+
- concept_id
|
494
|
+
- name
|
495
|
+
"/api/v1/lab/test_result_indicators":
|
496
|
+
get:
|
497
|
+
summary: Test Result Indicators
|
498
|
+
tags:
|
499
|
+
- Concepts
|
500
|
+
description: Retrieve all result indicators for a given test
|
501
|
+
security:
|
502
|
+
- api_key: []
|
503
|
+
parameters:
|
504
|
+
- name: test_type_id
|
505
|
+
in: query
|
506
|
+
required: true
|
507
|
+
description: Concept ID for the desired test
|
508
|
+
schema:
|
509
|
+
type: integer
|
510
|
+
responses:
|
511
|
+
'200':
|
512
|
+
description: Ok
|
513
|
+
content:
|
514
|
+
application/json:
|
515
|
+
schema:
|
516
|
+
type: array
|
517
|
+
items:
|
518
|
+
type: object
|
519
|
+
properties:
|
520
|
+
concept_id:
|
521
|
+
type: integer
|
522
|
+
name:
|
523
|
+
type: string
|
524
|
+
required:
|
525
|
+
- concept_id
|
526
|
+
- name
|
527
|
+
"/api/v1/lab/test_types":
|
528
|
+
get:
|
529
|
+
summary: Test types
|
530
|
+
tags:
|
531
|
+
- Concepts
|
532
|
+
description: Retrieve all test types
|
533
|
+
security:
|
534
|
+
- api_key: []
|
535
|
+
parameters:
|
536
|
+
- name: specimen_type
|
537
|
+
in: query
|
538
|
+
required: false
|
539
|
+
description: Select test types having this specimen type only
|
540
|
+
schema:
|
541
|
+
type: string
|
542
|
+
responses:
|
543
|
+
'200':
|
544
|
+
description: Success
|
545
|
+
content:
|
546
|
+
application/json:
|
547
|
+
schema:
|
548
|
+
type: array
|
549
|
+
items:
|
550
|
+
type: object
|
551
|
+
properties:
|
552
|
+
concept_id:
|
553
|
+
type: integer
|
554
|
+
name:
|
555
|
+
type: string
|
556
|
+
required:
|
557
|
+
- concept_id
|
558
|
+
- name
|
559
|
+
"/api/v1/lab/tests":
|
560
|
+
get:
|
561
|
+
summary: Search for tests
|
562
|
+
tags:
|
563
|
+
- Tests
|
564
|
+
description: 'Search for tests by accession number, date and other parameters.
|
565
|
+
|
566
|
+
'
|
567
|
+
parameters:
|
568
|
+
- name: accession_number
|
569
|
+
in: query
|
570
|
+
required: false
|
571
|
+
schema:
|
572
|
+
type: string
|
573
|
+
- name: test_type_id
|
574
|
+
in: query
|
575
|
+
required: false
|
576
|
+
schema:
|
577
|
+
type: integer
|
578
|
+
- name: specimen_type_id
|
579
|
+
in: query
|
580
|
+
required: false
|
581
|
+
schema:
|
582
|
+
type: integer
|
583
|
+
- name: patient_id
|
584
|
+
in: query
|
585
|
+
required: false
|
586
|
+
schema:
|
587
|
+
type: integer
|
588
|
+
- name: order_date
|
589
|
+
in: query
|
590
|
+
required: false
|
591
|
+
schema:
|
592
|
+
type: boolean
|
593
|
+
responses:
|
594
|
+
'200':
|
595
|
+
description: Okay
|
596
|
+
content:
|
597
|
+
application/json:
|
598
|
+
schema:
|
599
|
+
type: array
|
600
|
+
items:
|
601
|
+
type: object
|
602
|
+
properties:
|
603
|
+
id:
|
604
|
+
type: integer
|
605
|
+
concept_id:
|
606
|
+
type: integer
|
607
|
+
name:
|
608
|
+
type: string
|
609
|
+
order:
|
610
|
+
order_id:
|
611
|
+
type: string
|
612
|
+
accession_number:
|
613
|
+
type: string
|
614
|
+
post:
|
615
|
+
summary: Add tests to an existing order
|
616
|
+
tags:
|
617
|
+
- Tests
|
618
|
+
description: |
|
619
|
+
Add tests to an existing order.
|
620
|
+
|
621
|
+
An order can be created without specifying tests.
|
622
|
+
This endpoint allows one to add tests to that order.
|
623
|
+
parameters: []
|
624
|
+
security:
|
625
|
+
- api_key: []
|
626
|
+
responses:
|
627
|
+
'201':
|
628
|
+
description: Created
|
629
|
+
content:
|
630
|
+
application/json:
|
631
|
+
schema:
|
632
|
+
type: array
|
633
|
+
items:
|
634
|
+
type: object
|
635
|
+
properties:
|
636
|
+
id:
|
637
|
+
type: integer
|
638
|
+
concept_id:
|
639
|
+
type: integer
|
640
|
+
name:
|
641
|
+
type: string
|
642
|
+
order:
|
643
|
+
order_id:
|
644
|
+
type: string
|
645
|
+
accession_number:
|
646
|
+
type: string
|
647
|
+
requestBody:
|
648
|
+
content:
|
649
|
+
application/json:
|
650
|
+
schema:
|
651
|
+
type: object
|
652
|
+
properties:
|
653
|
+
order_id:
|
654
|
+
type: integer
|
655
|
+
tests:
|
656
|
+
type: array
|
657
|
+
items:
|
658
|
+
type: object
|
659
|
+
properties:
|
660
|
+
concept_id:
|
661
|
+
type: integer
|
662
|
+
description: Test type concept ID
|
663
|
+
required:
|
664
|
+
- concept_id
|
665
|
+
required:
|
666
|
+
- order_id
|
667
|
+
- tests
|
668
|
+
servers:
|
669
|
+
- url: http://{defaultHost}
|
670
|
+
variables:
|
671
|
+
defaultHost:
|
672
|
+
default: localhost:3000
|
673
|
+
components:
|
674
|
+
securitySchemes:
|
675
|
+
api_key:
|
676
|
+
type: apiKey
|
677
|
+
name: Authorization
|
678
|
+
in: header
|
679
|
+
bearer:
|
680
|
+
type: bearer
|
681
|
+
name: Authorization
|
682
|
+
in: header
|