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
@@ -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