his_emr_api_lab 1.1.22 → 1.1.23

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