his_emr_api_lab 1.1.22 → 1.1.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +71 -0
  4. data/Rakefile +32 -0
  5. data/app/controllers/lab/application_controller.rb +6 -0
  6. data/app/controllers/lab/labels_controller.rb +17 -0
  7. data/app/controllers/lab/orders_controller.rb +38 -0
  8. data/app/controllers/lab/reasons_for_test_controller.rb +9 -0
  9. data/app/controllers/lab/results_controller.rb +19 -0
  10. data/app/controllers/lab/specimen_types_controller.rb +15 -0
  11. data/app/controllers/lab/test_result_indicators_controller.rb +9 -0
  12. data/app/controllers/lab/test_types_controller.rb +15 -0
  13. data/app/controllers/lab/tests_controller.rb +26 -0
  14. data/app/jobs/lab/application_job.rb +4 -0
  15. data/app/jobs/lab/push_order_job.rb +12 -0
  16. data/app/jobs/lab/update_patient_orders_job.rb +32 -0
  17. data/app/jobs/lab/void_order_job.rb +17 -0
  18. data/app/mailers/lab/application_mailer.rb +6 -0
  19. data/app/models/lab/application_record.rb +5 -0
  20. data/app/models/lab/lab_accession_number_counter.rb +13 -0
  21. data/app/models/lab/lab_encounter.rb +7 -0
  22. data/app/models/lab/lab_order.rb +58 -0
  23. data/app/models/lab/lab_result.rb +31 -0
  24. data/app/models/lab/lab_test.rb +19 -0
  25. data/app/models/lab/lims_failed_import.rb +4 -0
  26. data/app/models/lab/lims_order_mapping.rb +10 -0
  27. data/app/serializers/lab/lab_order_serializer.rb +55 -0
  28. data/app/serializers/lab/result_serializer.rb +36 -0
  29. data/app/serializers/lab/test_serializer.rb +29 -0
  30. data/app/services/lab/accession_number_service.rb +77 -0
  31. data/app/services/lab/concepts_service.rb +82 -0
  32. data/app/services/lab/labelling_service/order_label.rb +106 -0
  33. data/app/services/lab/lims/api/blackhole_api.rb +21 -0
  34. data/app/services/lab/lims/api/couchdb_api.rb +53 -0
  35. data/app/services/lab/lims/api/mysql_api.rb +316 -0
  36. data/app/services/lab/lims/api/rest_api.rb +416 -0
  37. data/app/services/lab/lims/api/ws_api.rb +121 -0
  38. data/app/services/lab/lims/api_factory.rb +19 -0
  39. data/app/services/lab/lims/config.rb +100 -0
  40. data/app/services/lab/lims/exceptions.rb +11 -0
  41. data/app/services/lab/lims/migrator.rb +216 -0
  42. data/app/services/lab/lims/order_dto.rb +105 -0
  43. data/app/services/lab/lims/order_serializer.rb +244 -0
  44. data/app/services/lab/lims/pull_worker.rb +289 -0
  45. data/app/services/lab/lims/push_worker.rb +149 -0
  46. data/app/services/lab/lims/utils.rb +91 -0
  47. data/app/services/lab/lims/worker.rb +86 -0
  48. data/app/services/lab/metadata.rb +24 -0
  49. data/app/services/lab/orders_search_service.rb +66 -0
  50. data/app/services/lab/orders_service.rb +212 -0
  51. data/app/services/lab/results_service.rb +149 -0
  52. data/app/services/lab/tests_service.rb +93 -0
  53. data/config/routes.rb +17 -0
  54. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +12 -0
  55. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +15 -0
  56. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +15 -0
  57. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +5 -0
  58. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +19 -0
  59. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +20 -0
  60. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +7 -0
  61. data/lib/auto12epl.rb +201 -0
  62. data/lib/couch_bum/couch_bum.rb +92 -0
  63. data/lib/generators/lab/install/USAGE +9 -0
  64. data/lib/generators/lab/install/install_generator.rb +19 -0
  65. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +5 -0
  66. data/lib/generators/lab/install/templates/start_worker.rb +32 -0
  67. data/lib/generators/lab/install/templates/swagger.yaml +714 -0
  68. data/lib/his_emr_api_lab.rb +5 -0
  69. data/lib/lab/engine.rb +15 -0
  70. data/lib/lab/version.rb +5 -0
  71. data/lib/logger_multiplexor.rb +38 -0
  72. data/lib/tasks/lab_tasks.rake +25 -0
  73. data/lib/tasks/loaders/data/reasons-for-test.csv +7 -0
  74. data/lib/tasks/loaders/data/test-measures.csv +225 -0
  75. data/lib/tasks/loaders/data/tests.csv +161 -0
  76. data/lib/tasks/loaders/loader_mixin.rb +53 -0
  77. data/lib/tasks/loaders/metadata_loader.rb +26 -0
  78. data/lib/tasks/loaders/reasons_for_test_loader.rb +23 -0
  79. data/lib/tasks/loaders/specimens_loader.rb +65 -0
  80. data/lib/tasks/loaders/test_result_indicators_loader.rb +54 -0
  81. metadata +81 -2
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ # Search Lab orders.
5
+ module OrdersSearchService
6
+ class << self
7
+ def find_orders(filters)
8
+ extra_filters = pop_filters(filters, :date, :end_date, :status)
9
+
10
+ orders = Lab::LabOrder.prefetch_relationships
11
+ .where(filters)
12
+ .order(start_date: :desc)
13
+
14
+ orders = filter_orders_by_status(orders, pop_filters(extra_filters, :status))
15
+ orders = filter_orders_by_date(orders, extra_filters)
16
+
17
+ orders.map { |order| Lab::LabOrderSerializer.serialize_order(order) }
18
+ end
19
+
20
+ def find_orders_without_results(patient_id: nil)
21
+ results_query = Lab::LabResult.all
22
+ results_query = results_query.where(person_id: patient_id) if patient_id
23
+
24
+ query = Lab::LabOrder.where.not(order_id: results_query.select(:order_id))
25
+ query = query.where(patient_id: patient_id) if patient_id
26
+
27
+ query
28
+ end
29
+
30
+ def filter_orders_by_date(orders, date: nil, end_date: nil)
31
+ date = date&.to_date
32
+ end_date = end_date&.to_date
33
+
34
+ return orders.where('start_date BETWEEN ? AND ?', date, end_date + 1.day) if date && end_date
35
+
36
+ return orders.where('start_date BETWEEN ? AND ?', date, date + 1.day) if date
37
+
38
+ return orders.where('start_date < ?', end_date + 1.day) if end_date
39
+
40
+ orders
41
+ end
42
+
43
+ def filter_orders_by_status(orders, status: nil)
44
+ case status&.downcase
45
+ when 'ordered' then orders.where(concept_id: unknown_concept_id)
46
+ when 'drawn' then orders.where.not(concept_id: unknown_concept_id)
47
+ else orders
48
+ end
49
+ end
50
+
51
+ def unknown_concept_id
52
+ ConceptName.find_by_name!('Unknown').concept_id
53
+ end
54
+
55
+ def pop_filters(params, *filters)
56
+ filters.each_with_object({}) do |filter, popped_params|
57
+ next unless params.key?(filter)
58
+
59
+ popped_params[filter.to_sym] = params.delete(filter)
60
+ end
61
+ end
62
+
63
+ def fetch_results(order); end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ ##
5
+ # Manage lab orders.
6
+ #
7
+ # Lab orders are just ordinary openmrs orders with extra metadata that
8
+ # separates them from other orders. Lab orders have an order type of 'Lab'
9
+ # with the order's test type as the order's concept. The order's start
10
+ # date is the day the order is made. Additional information pertaining to
11
+ # the order is stored as observations that point to the order. The
12
+ # specimen types, requesting clinician, target lab, and reason for test
13
+ # are saved as observations to the order. Refer to method #order_test for
14
+ # more information.
15
+ module OrdersService
16
+ class << self
17
+ ##
18
+ # Create a lab order.
19
+ #
20
+ # Parameters schema:
21
+ #
22
+ # {
23
+ # encounter_id: {
24
+ # type: :integer,
25
+ # required: :false,
26
+ # description: 'Attach order to this if program_id and patient_id are not provided'
27
+ # },
28
+ # program_id: { type: :integer, required: false },
29
+ # patient_id: { type: :integer, required: false }
30
+ # specimen: { type: :object, properties: { concept_id: :integer }, required: %i[concept_id] },
31
+ # test_type_ids: {
32
+ # type: :array,
33
+ # items: {
34
+ # type: :object,
35
+ # properties: { concept_id: :integer },
36
+ # required: %i[concept_id]
37
+ # }
38
+ # },
39
+ # start_date: { type: :datetime }
40
+ # accession_number: { type: :string }
41
+ # target_lab: { type: :string },
42
+ # reason_for_test_id: { type: :integer },
43
+ # requesting_clinician: { type: :string }
44
+ # }
45
+ #
46
+ # encounter_id: is an ID of the encounter the lab order is to be created under
47
+ # test_type_id: is a concept_id of the name of test being ordered
48
+ # specimen_type_id: is a list of IDs for the specimens to be tested (can be ommited)
49
+ # target_lab: is the name of the lab where test will be carried out
50
+ # reason_for_test_id: is a concept_id for a (standard) reason of why the test is being carried out
51
+ # requesting_clinician: Name of the clinician requesting the test (defaults to current user)
52
+ def order_test(order_params)
53
+ Order.transaction do
54
+ encounter = find_encounter(order_params)
55
+ order = create_order(encounter, order_params)
56
+
57
+ Lab::TestsService.create_tests(order, order_params[:date], order_params[:tests])
58
+
59
+ Lab::LabOrderSerializer.serialize_order(
60
+ order, requesting_clinician: add_requesting_clinician(order, order_params),
61
+ reason_for_test: add_reason_for_test(order, order_params),
62
+ target_lab: add_target_lab(order, order_params)
63
+ )
64
+ end
65
+ end
66
+
67
+ def update_order(order_id, params)
68
+ specimen_id = params.dig(:specimen, :concept_id)
69
+ raise ::InvalidParameterError, 'Specimen concept_id is required' unless specimen_id
70
+
71
+ order = Lab::LabOrder.find(order_id)
72
+ if order.concept_id != unknown_concept_id && !params[:force_update]&.casecmp?('true')
73
+ raise ::UnprocessableEntityError, "Can't change order specimen once set"
74
+ end
75
+
76
+ if specimen_id.to_i != order.concept_id
77
+ Rails.logger.debug("Updating order ##{order.order_id}")
78
+ order.update!(concept_id: specimen_id,
79
+ discontinued: true,
80
+ discontinued_by: User.current.user_id,
81
+ discontinued_date: params[:date]&.to_date || Time.now,
82
+ discontinued_reason_non_coded: 'Sample drawn/updated')
83
+ end
84
+
85
+ if params[:reason_for_test_id]
86
+ Rails.logger.debug("Updating reason for test on order ##{order.order_id}")
87
+ update_reason_for_test(order, params[:reason_for_test_id])
88
+ end
89
+
90
+ Lab::LabOrderSerializer.serialize_order(order)
91
+ end
92
+
93
+ def void_order(order_id, reason)
94
+ order = Lab::LabOrder.includes(%i[requesting_clinician reason_for_test target_lab], tests: [:result])
95
+ .find(order_id)
96
+
97
+ order.requesting_clinician&.void(reason)
98
+ order.reason_for_test&.void(reason)
99
+ order.target_lab&.void(reason)
100
+
101
+ order.tests.each { |test| test.void(reason) }
102
+ voided = order.void(reason)
103
+
104
+ voided
105
+ end
106
+
107
+ private
108
+
109
+ ##
110
+ # Extract an encounter from the given parameters.
111
+ #
112
+ # Uses an encounter_id to retrieve an encounter if provided else
113
+ # a 'Lab' encounter is created using the provided program_id and
114
+ # patient_id.
115
+ def find_encounter(order_params)
116
+ return Encounter.find(order_params[:encounter_id]) if order_params[:encounter_id]
117
+
118
+ raise InvalidParameterError, 'encounter_id or patient_id required' unless order_params[:patient_id]
119
+
120
+ program_id = order_params[:program_id] || Program.find_by_name!(Lab::Metadata::LAB_PROGRAM_NAME).program_id
121
+
122
+ Encounter.create!(
123
+ patient_id: order_params[:patient_id],
124
+ program_id: program_id,
125
+ type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
126
+ encounter_datetime: order_params[:date] || Date.today,
127
+ provider_id: order_params[:provider_id] || User.current.person.person_id
128
+ )
129
+ end
130
+
131
+ def create_order(encounter, params)
132
+ Lab::LabOrder.create!(
133
+ order_type: OrderType.find_by_name!(Lab::Metadata::ORDER_TYPE_NAME),
134
+ concept_id: params.dig(:specimen, :concept_id) || unknown_concept_id,
135
+ encounter_id: encounter.encounter_id,
136
+ patient_id: encounter.patient_id,
137
+ start_date: params[:date]&.to_date || Date.today,
138
+ auto_expire_date: params[:end_date],
139
+ accession_number: params[:accession_number] || next_accession_number,
140
+ orderer: User.current&.user_id
141
+ )
142
+ end
143
+
144
+ ##
145
+ # Attach the requesting clinician to an order
146
+ def add_requesting_clinician(order, params)
147
+ create_order_observation(
148
+ order,
149
+ Lab::Metadata::REQUESTING_CLINICIAN_CONCEPT_NAME,
150
+ params[:date],
151
+ value_text: params['requesting_clinician']
152
+ )
153
+ end
154
+
155
+ ##
156
+ # Attach a reason for the order/test
157
+ #
158
+ # Examples of reasons include: Routine, Targeted, Confirmatory, Repeat, or Stat.
159
+ def add_reason_for_test(order, params)
160
+ create_order_observation(
161
+ order,
162
+ Lab::Metadata::REASON_FOR_TEST_CONCEPT_NAME,
163
+ params[:date],
164
+ value_coded: params[:reason_for_test_id]
165
+ )
166
+ end
167
+
168
+ ##
169
+ # Attach the lab where the test is going to get carried out.
170
+ def add_target_lab(order, params)
171
+ return nil unless params['target_lab']
172
+
173
+ create_order_observation(
174
+ order,
175
+ Lab::Metadata::TARGET_LAB_CONCEPT_NAME,
176
+ params[:date],
177
+ value_text: params['target_lab']
178
+ )
179
+ end
180
+
181
+ def create_order_observation(order, concept_name, date, **values)
182
+ Observation.create!(
183
+ order: order,
184
+ encounter_id: order.encounter_id,
185
+ person_id: order.patient_id,
186
+ concept_id: ConceptName.find_by_name!(concept_name).concept_id,
187
+ obs_datetime: date&.to_time || Time.now,
188
+ **values
189
+ )
190
+ end
191
+
192
+ def next_accession_number(date = nil)
193
+ Lab::AccessionNumberService.next_accession_number(date)
194
+ end
195
+
196
+ def unknown_concept_id
197
+ ConceptName.find_by_name!('Unknown').concept_id
198
+ end
199
+
200
+ def update_reason_for_test(order, concept_id)
201
+ raise InvalidParameterError, "Reason for test can't be blank" if concept_id.blank?
202
+
203
+ return if order.reason_for_test&.value_coded == concept_id
204
+
205
+ raise InvalidParameterError, "Can't change reason for test once set" if order.reason_for_test&.value_coded
206
+
207
+ order.reason_for_test&.delete
208
+ add_reason_for_test(order, date: order.start_date, reason_for_test_id: concept_id)
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lab
4
+ module ResultsService
5
+ class << self
6
+ ##
7
+ # Attach results to a test
8
+ #
9
+ # Params:
10
+ # test_id: The tests id (maps to obs_id of the test's observation in OpenMRS)
11
+ # params: A hash comprising the following fields
12
+ # - encounter_id: Encounter to create result under (can be ommitted but provider_id has to specified)
13
+ # - provider_id: Specify a provider for an encounter the result is going to be created under
14
+ # - date: Retrospective date when the result was received (can be ommitted, defaults to today)
15
+ # - measures: An array of measures. A measure is an object of the following structure
16
+ # - indicator: An object that has a concept_id field (concept_id of the indicator)
17
+ # - value_type: An enum that's limited to 'numeric', 'boolean', 'text', and 'coded'
18
+ # result_enter_by: A string that specifies who created the result
19
+ def create_results(test_id, params, result_enter_by = 'LIMS')
20
+ serializer = {}
21
+ results_obs = {}
22
+ ActiveRecord::Base.transaction do
23
+ test = Lab::LabTest.find(test_id)
24
+ encounter = find_encounter(test, encounter_id: params[:encounter_id],
25
+ date: params[:date]&.to_date,
26
+ provider_id: params[:provider_id])
27
+
28
+ results_obs = create_results_obs(encounter, test, params[:date], params[:comments])
29
+ params[:measures].map { |measure| add_measure_to_results(results_obs, measure, params[:date]) }
30
+ OrderExtension.create!(creator: User.current, value: result_enter_by, order_id: results_obs.order_id,
31
+ date_created: Time.now)
32
+
33
+ serializer = Lab::ResultSerializer.serialize(results_obs)
34
+ end
35
+ NotificationService.new.create_notification(result_enter_by, prepare_notification_message(results_obs, serializer, result_enter_by))
36
+ Rails.logger.info("Lab::ResultsService: Result created for test #{test_id} #{serializer}")
37
+ serializer
38
+ end
39
+
40
+ private
41
+
42
+ def prepare_notification_message(result, values, result_enter_by)
43
+ order = Order.find(result.order_id)
44
+ { Type: result_enter_by,
45
+ 'Test type': ConceptName.find_by(concept_id: result.test.value_coded)&.name,
46
+ 'Accession number': order&.accession_number,
47
+ 'ARV-Number': find_arv_number(result.person_id),
48
+ PatientID: result.person_id,
49
+ 'Ordered By': order&.provider&.person&.name,
50
+ Result: values }.as_json
51
+ end
52
+
53
+ def find_arv_number(patient_id)
54
+ PatientIdentifier.joins(:type)
55
+ .merge(PatientIdentifierType.where(name: 'ARV Number'))
56
+ .where(patient_id: patient_id)
57
+ .first&.identifier
58
+ end
59
+
60
+ def find_encounter(test, encounter_id: nil, date: nil, provider_id: nil)
61
+ return Encounter.find(encounter_id) if encounter_id
62
+
63
+ Encounter.create!(
64
+ patient_id: test.person_id,
65
+ program_id: test.encounter.program_id,
66
+ type: EncounterType.find_by_name!(Lab::Metadata::ENCOUNTER_TYPE_NAME),
67
+ encounter_datetime: date || Date.today,
68
+ provider_id: provider_id || User.current.user_id
69
+ )
70
+ end
71
+
72
+ # Creates the parent observation for results to which the different measures are attached
73
+ def create_results_obs(encounter, test, date, comments = nil)
74
+ void_existing_results_obs(encounter, test)
75
+
76
+ Lab::LabResult.create!(
77
+ person_id: encounter.patient_id,
78
+ encounter_id: encounter.encounter_id,
79
+ concept_id: test_result_concept.concept_id,
80
+ order_id: test.order_id,
81
+ obs_group_id: test.obs_id,
82
+ obs_datetime: date&.to_datetime || DateTime.now,
83
+ comments: comments
84
+ )
85
+ end
86
+
87
+ def void_existing_results_obs(encounter, test)
88
+ result = Lab::LabResult.find_by(person_id: encounter.patient_id,
89
+ concept_id: test_result_concept.concept_id,
90
+ obs_group_id: test.obs_id)
91
+ return unless result
92
+
93
+ OrderExtension.find_by(order_id: result.order_id)&.void("Updated/overwritten by #{User.current.username}")
94
+ result.measures.map { |child_obs| child_obs.void("Updated/overwritten by #{User.current.username}") }
95
+ result.void("Updated/overwritten by #{User.current.username}")
96
+ end
97
+
98
+ def test_result_concept
99
+ ConceptName.find_by_name!(Lab::Metadata::TEST_RESULT_CONCEPT_NAME)
100
+ end
101
+
102
+ def add_measure_to_results(results_obs, params, date)
103
+ validate_measure_params(params)
104
+
105
+ Observation.create!(
106
+ person_id: results_obs.person_id,
107
+ encounter_id: results_obs.encounter_id,
108
+ order_id: results_obs.order_id,
109
+ concept_id: params[:indicator][:concept_id],
110
+ obs_group_id: results_obs.obs_id,
111
+ obs_datetime: date&.to_datetime || DateTime.now,
112
+ **make_measure_value(params)
113
+ )
114
+ end
115
+
116
+ def validate_measure_params(params)
117
+ raise InvalidParameterError, 'measures.value is required' if params[:value].blank?
118
+
119
+ if params[:indicator]&.[](:concept_id).blank?
120
+ raise InvalidParameterError, 'measures.indicator.concept_id is required'
121
+ end
122
+
123
+ params
124
+ end
125
+
126
+ # Converts user provided measure values to observation_values
127
+ def make_measure_value(params)
128
+ obs_value = { value_modifier: params[:value_modifier] }
129
+ value_type = params[:value_type] || 'text'
130
+
131
+ case value_type.downcase
132
+ when 'numeric' then obs_value.merge(value_numeric: params[:value])
133
+ when 'boolean' then obs_value.merge(value_boolean: parse_boolen_value(params[:value]))
134
+ when 'coded' then obs_value.merge(value_coded: params[:value]) # Should we be collecting value_name_coded_id?
135
+ when 'text' then obs_value.merge(value_text: params[:value])
136
+ else raise InvalidParameterError, "Invalid value_type: #{params[:value_type]}"
137
+ end
138
+ end
139
+
140
+ def parse_boolen_value(string)
141
+ case string.downcase
142
+ when 'true' then true
143
+ when 'false' then false
144
+ else raise InvalidParameterError, "Invalid boolean value: #{string}"
145
+ end
146
+ end
147
+ end
148
+ end
149
+ 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,17 @@
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
+ get 'api/v1/lab/labels/order', to: 'labels#print_order_label'
10
+
11
+ # Metadata
12
+ # TODO: Move the following to namespace /concepts
13
+ resources :specimen_types, only: %i[index], path: 'api/v1/lab/specimen_types'
14
+ resources :test_result_indicators, only: %i[index], path: 'api/v1/lab/test_result_indicators'
15
+ resources :test_types, only: %i[index], path: 'api/v1/lab/test_types'
16
+ resources :reasons_for_test, only: %i[index], path: 'api/v1/lab/reasons_for_test'
17
+ 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,20 @@
1
+ class FixNumericResultsValueType < ActiveRecord::Migration[5.2]
2
+ def up
3
+ results = Lab::LabResult.all.includes(:children)
4
+
5
+ ActiveRecord::Base.connection.transaction do
6
+ results.each do |result|
7
+ result.children.each do |measure|
8
+ next unless measure.value_text&.match?(/^[+-]?((\d+(\.\d+)?)|\.\d+)$/)
9
+
10
+ puts "Updating result value type for result measure ##{measure.obs_id}"
11
+ measure.value_numeric = measure.value_text
12
+ measure.value_text = nil
13
+ measure.save!
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def down; end
20
+ end
@@ -0,0 +1,7 @@
1
+ class AddDefaultToLimsOrderMapping < ActiveRecord::Migration[5.2]
2
+ def up
3
+ change_column :lab_lims_order_mappings, :revision, :string, limit: 256, default: nil, null: true
4
+ end
5
+
6
+ def down; end
7
+ end