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.
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
@@ -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,12 @@
1
+ class CreateLabLabAccessionNumberCounters < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :lab_accession_number_counters do |t|
4
+ t.date :date
5
+ t.bigint :value
6
+
7
+ t.timestamps
8
+
9
+ t.index %i[date], unique: true
10
+ end
11
+ end
12
+ 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,5 @@
1
+ class AddOrderRevisionToLimsOrderMapping < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :lab_lims_order_mappings, :revision, :string
4
+ end
5
+ 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,9 @@
1
+ Description:
2
+ Installs the Lab engine.
3
+
4
+ Example:
5
+ rails generate lab:install
6
+
7
+ This will create:
8
+ config/initializers/rswag-ui-lab.rb
9
+ swagger/lab/v1/swagger.yaml
@@ -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,5 @@
1
+ require 'rswag/ui'
2
+
3
+ Rswag::Ui.configure do |c|
4
+ c.swagger_endpoint '/api-docs/lab/v1/swagger.yaml', 'Lab API V1 Docs'
5
+ 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