davinci_crd_test_kit 0.9.0

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/davinci_crd_test_kit/card_responses/companions_prerequisites.json +58 -0
  4. data/lib/davinci_crd_test_kit/card_responses/create_update_coverage_information.json +20 -0
  5. data/lib/davinci_crd_test_kit/card_responses/external_reference.json +21 -0
  6. data/lib/davinci_crd_test_kit/card_responses/instructions.json +14 -0
  7. data/lib/davinci_crd_test_kit/card_responses/launch_smart_app.json +21 -0
  8. data/lib/davinci_crd_test_kit/card_responses/propose_alternate_request.json +71 -0
  9. data/lib/davinci_crd_test_kit/card_responses/request_form_completion.json +227 -0
  10. data/lib/davinci_crd_test_kit/cards_validation.rb +234 -0
  11. data/lib/davinci_crd_test_kit/client_fhir_api_group.rb +762 -0
  12. data/lib/davinci_crd_test_kit/client_hook_request_validation.rb +15 -0
  13. data/lib/davinci_crd_test_kit/client_hooks_group.rb +706 -0
  14. data/lib/davinci_crd_test_kit/client_tests/appointment_book_receive_request_test.rb +71 -0
  15. data/lib/davinci_crd_test_kit/client_tests/client_display_cards_attest.rb +48 -0
  16. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_create_test.rb +40 -0
  17. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_read_test.rb +39 -0
  18. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_search_test.rb +232 -0
  19. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_update_test.rb +40 -0
  20. data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_validation_test.rb +60 -0
  21. data/lib/davinci_crd_test_kit/client_tests/decode_auth_token_test.rb +40 -0
  22. data/lib/davinci_crd_test_kit/client_tests/encounter_discharge_receive_request_test.rb +68 -0
  23. data/lib/davinci_crd_test_kit/client_tests/encounter_start_receive_request_test.rb +68 -0
  24. data/lib/davinci_crd_test_kit/client_tests/hook_request_optional_fields_test.rb +41 -0
  25. data/lib/davinci_crd_test_kit/client_tests/hook_request_required_fields_test.rb +40 -0
  26. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_context_test.rb +63 -0
  27. data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_prefetch_test.rb +151 -0
  28. data/lib/davinci_crd_test_kit/client_tests/order_dispatch_receive_request_test.rb +79 -0
  29. data/lib/davinci_crd_test_kit/client_tests/order_select_receive_request_test.rb +76 -0
  30. data/lib/davinci_crd_test_kit/client_tests/order_sign_receive_request_test.rb +79 -0
  31. data/lib/davinci_crd_test_kit/client_tests/retrieve_jwks_test.rb +65 -0
  32. data/lib/davinci_crd_test_kit/client_tests/token_header_test.rb +34 -0
  33. data/lib/davinci_crd_test_kit/client_tests/token_payload_test.rb +61 -0
  34. data/lib/davinci_crd_test_kit/crd_client_suite.rb +156 -0
  35. data/lib/davinci_crd_test_kit/crd_jwks.json +59 -0
  36. data/lib/davinci_crd_test_kit/crd_options.rb +9 -0
  37. data/lib/davinci_crd_test_kit/crd_server_suite.rb +115 -0
  38. data/lib/davinci_crd_test_kit/ext/inferno_core/runnable.rb +22 -0
  39. data/lib/davinci_crd_test_kit/hook_request_field_validation.rb +410 -0
  40. data/lib/davinci_crd_test_kit/jwks.rb +25 -0
  41. data/lib/davinci_crd_test_kit/jwt_helper.rb +74 -0
  42. data/lib/davinci_crd_test_kit/mock_service_response.rb +421 -0
  43. data/lib/davinci_crd_test_kit/routes/cds-services.json +74 -0
  44. data/lib/davinci_crd_test_kit/routes/cds_services_discovery_handler.rb +18 -0
  45. data/lib/davinci_crd_test_kit/routes/hook_request_endpoint.rb +93 -0
  46. data/lib/davinci_crd_test_kit/routes/jwk_set_endpoint_handler.rb +15 -0
  47. data/lib/davinci_crd_test_kit/server_appointment_book_group.rb +173 -0
  48. data/lib/davinci_crd_test_kit/server_discovery_group.rb +59 -0
  49. data/lib/davinci_crd_test_kit/server_encounter_discharge_group.rb +144 -0
  50. data/lib/davinci_crd_test_kit/server_encounter_start_group.rb +144 -0
  51. data/lib/davinci_crd_test_kit/server_hook_request_validation.rb +15 -0
  52. data/lib/davinci_crd_test_kit/server_hooks_group.rb +69 -0
  53. data/lib/davinci_crd_test_kit/server_order_dispatch_group.rb +173 -0
  54. data/lib/davinci_crd_test_kit/server_order_select_group.rb +169 -0
  55. data/lib/davinci_crd_test_kit/server_order_sign_group.rb +198 -0
  56. data/lib/davinci_crd_test_kit/server_required_card_response_validation_group.rb +23 -0
  57. data/lib/davinci_crd_test_kit/server_tests/additional_orders_validation_test.rb +70 -0
  58. data/lib/davinci_crd_test_kit/server_tests/card_optional_fields_validation_test.rb +47 -0
  59. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_across_hooks_validation_test.rb +32 -0
  60. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_received_test.rb +58 -0
  61. data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_validation_test.rb +121 -0
  62. data/lib/davinci_crd_test_kit/server_tests/create_or_update_coverage_info_response_validation_test.rb +72 -0
  63. data/lib/davinci_crd_test_kit/server_tests/discovery_endpoint_test.rb +88 -0
  64. data/lib/davinci_crd_test_kit/server_tests/discovery_services_validation_test.rb +65 -0
  65. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_across_hooks_validation_test.rb +28 -0
  66. data/lib/davinci_crd_test_kit/server_tests/external_reference_card_validation_test.rb +36 -0
  67. data/lib/davinci_crd_test_kit/server_tests/form_completion_response_validation_test.rb +79 -0
  68. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_across_hooks_test.rb +25 -0
  69. data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_test.rb +28 -0
  70. data/lib/davinci_crd_test_kit/server_tests/launch_smart_app_card_validation_test.rb +38 -0
  71. data/lib/davinci_crd_test_kit/server_tests/propose_alternate_request_card_validation_test.rb +65 -0
  72. data/lib/davinci_crd_test_kit/server_tests/service_call_test.rb +86 -0
  73. data/lib/davinci_crd_test_kit/server_tests/service_request_context_validation_test.rb +30 -0
  74. data/lib/davinci_crd_test_kit/server_tests/service_request_optional_fields_validation_test.rb +41 -0
  75. data/lib/davinci_crd_test_kit/server_tests/service_request_required_fields_validation_test.rb +43 -0
  76. data/lib/davinci_crd_test_kit/server_tests/service_response_validation_test.rb +82 -0
  77. data/lib/davinci_crd_test_kit/suggestion_actions_validation.rb +123 -0
  78. data/lib/davinci_crd_test_kit/tags.rb +8 -0
  79. data/lib/davinci_crd_test_kit/test_helper.rb +23 -0
  80. data/lib/davinci_crd_test_kit/urls.rb +52 -0
  81. data/lib/davinci_crd_test_kit/version.rb +3 -0
  82. data/lib/davinci_crd_test_kit.rb +2 -0
  83. metadata +170 -0
@@ -0,0 +1,421 @@
1
+ module DaVinciCRDTestKit
2
+ # Serve responses to CRD hook invocations
3
+ module MockServiceResponse
4
+ def current_time
5
+ Time.now.utc
6
+ end
7
+
8
+ def coverage_information_required_hooks
9
+ ['appointment-book', 'order-dispatch', 'order-sign']
10
+ end
11
+
12
+ def get_card_json(filename)
13
+ json = JSON.parse(File.read(File.join(__dir__, 'card_responses', filename)))
14
+ return json unless filename == 'launch_smart_app.json'
15
+
16
+ json['links'].first['url'] = "#{Inferno::Application['base_url']}/custom/smart/launch"
17
+
18
+ json
19
+ end
20
+
21
+ def format_missing_response_types(missing_response_types)
22
+ missing_response_types
23
+ .map do |response_type|
24
+ response_type_string =
25
+ response_type.split('_')
26
+ .map(&:capitalize)
27
+ .join(' ')
28
+ .sub('Smart', 'SMART')
29
+ .sub('Create Update', 'Create/Update')
30
+ .sub('Companions Prerequisites', 'Companions/Prerequisites')
31
+ response_type_string
32
+ end
33
+ end
34
+
35
+ def missing_response_type_filter(response_type, hook_card_response)
36
+ if response_type == 'coverage information'
37
+ hook_card_response['systemActions'].nil? ||
38
+ hook_card_response['systemActions'].none? do |card|
39
+ card['description'].include?('coverage information')
40
+ end
41
+ else
42
+ hook_card_response['cards'].present? &&
43
+ hook_card_response['cards'].none? { |card| card['summary'].downcase.include?(response_type) }
44
+ end
45
+ end
46
+
47
+ def get_missing_response_types(selected_response_types, hook_card_response, hook_name)
48
+ if coverage_information_required_hooks.include?(hook_name)
49
+ selected_response_types.append('coverage_information').uniq!
50
+ end
51
+
52
+ selected_response_types
53
+ .select do |response_type|
54
+ response_type = response_type
55
+ .split('_')
56
+ .join(' ')
57
+ .sub('create update', 'create/update')
58
+ .sub('companions prerequisites', 'companions/prerequisites')
59
+ missing_response_type_filter(response_type, hook_card_response)
60
+ end
61
+ end
62
+
63
+ def create_warning_messages(selected_response_types, hook_card_response, hook_name)
64
+ missing_response_types = if hook_card_response.nil?
65
+ selected_response_types
66
+ else
67
+ get_missing_response_types(selected_response_types, hook_card_response, hook_name)
68
+ end
69
+
70
+ return if missing_response_types.empty?
71
+
72
+ missing_response_types = format_missing_response_types(missing_response_types)
73
+ missing_response_types.each do |missing_response_type|
74
+ Inferno::Repositories::Messages.new.create(result_id: result.id, type: 'warning',
75
+ message: %(Unable to return response type: `#{missing_response_type}`
76
+ for #{hook_name} hook))
77
+ end
78
+ end
79
+
80
+ def create_card_response(hook_card_response)
81
+ if hook_card_response.nil?
82
+ response.headers.merge!({ 'Access-Control-Allow-Origin' => '*' })
83
+ response.status = 400
84
+ response.body = 'Invalid Request: Incorrect format for hook request body'
85
+ else
86
+ response.body = hook_card_response.to_json
87
+ response.headers.merge!({ 'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*' })
88
+ response.status = 200
89
+ response.format = :json
90
+ end
91
+ end
92
+
93
+ def update_specific_hook_card_info(card_response, hook_name)
94
+ return if card_response.nil?
95
+
96
+ hook_display = hook_name.split('-').map(&:capitalize).join(' ')
97
+ card_response['cards'].map do |card|
98
+ card['summary'].prepend("#{hook_display} ")
99
+ card['uuid'] = SecureRandom.uuid
100
+ end
101
+ card_response
102
+ end
103
+
104
+ def appointment_book_response(selected_response_types)
105
+ cards_response = create_cards_and_system_actions(selected_response_types, 'appointment-book', 'appointments')
106
+ hook_card_response = update_specific_hook_card_info(cards_response, 'appointment-book')
107
+ create_warning_messages(selected_response_types, hook_card_response, 'appointment-book')
108
+ create_card_response(hook_card_response)
109
+ end
110
+
111
+ def encounter_start_response(selected_response_types)
112
+ cards_response = create_cards_and_system_actions(selected_response_types, 'encounter-start', 'encounterId',
113
+ 'Encounter')
114
+ hook_card_response = update_specific_hook_card_info(cards_response, 'encounter-start')
115
+ create_warning_messages(selected_response_types, hook_card_response, 'encounter-start')
116
+ create_card_response(hook_card_response)
117
+ end
118
+
119
+ def encounter_discharge_response(selected_response_types)
120
+ cards_response = create_cards_and_system_actions(selected_response_types, 'encounter-discharge', 'encounterId',
121
+ 'Encounter')
122
+ hook_card_response = update_specific_hook_card_info(cards_response, 'encounter-discharge')
123
+ create_warning_messages(selected_response_types, hook_card_response, 'encounter-discharge')
124
+ create_card_response(hook_card_response)
125
+ end
126
+
127
+ def order_dispatch_response(selected_response_types)
128
+ cards_response = create_cards_and_system_actions(selected_response_types, 'order-dispatch', 'order')
129
+ hook_card_response = update_specific_hook_card_info(cards_response, 'order-dispatch')
130
+ create_warning_messages(selected_response_types, hook_card_response, 'order-dispatch')
131
+ create_card_response(hook_card_response)
132
+ end
133
+
134
+ def order_select_response(selected_response_types)
135
+ cards_response = create_cards_and_system_actions(selected_response_types, 'order-select', 'draftOrders')
136
+ hook_card_response = update_specific_hook_card_info(cards_response, 'order-select')
137
+ create_warning_messages(selected_response_types, hook_card_response, 'order-select')
138
+ create_card_response(hook_card_response)
139
+ end
140
+
141
+ def order_sign_response(selected_response_types)
142
+ cards_response = create_cards_and_system_actions(selected_response_types, 'order-sign', 'draftOrders')
143
+ hook_card_response = update_specific_hook_card_info(cards_response, 'order-sign')
144
+ create_warning_messages(selected_response_types, hook_card_response, 'order-sign')
145
+ create_card_response(hook_card_response)
146
+ end
147
+
148
+ def make_resource_request(uri, access_token)
149
+ response = Faraday.get(uri, nil, { 'Authorization' => "Bearer #{access_token}" })
150
+ return unless response.status == 200
151
+
152
+ resource = FHIR.from_contents(response.body)
153
+ return resource unless resource.resourceType == 'Bundle'
154
+ return if resource.entry.empty?
155
+
156
+ resource.entry.first.resource
157
+ end
158
+
159
+ def get_patient_coverage(request_body)
160
+ prefetch = request_body['prefetch']
161
+ if prefetch.present? && prefetch['coverage']
162
+ FHIR.from_contents(prefetch['coverage'].to_json)
163
+ else
164
+ fhir_server = request_body['fhirServer']
165
+ if fhir_server.present?
166
+ access_token = request_body['fhirAuthorization']['access_token'] if request_body['fhirAuthorization']
167
+ patient_id = request_body['context']['patientId']
168
+
169
+ make_resource_request(
170
+ "#{fhir_server}/Coverage?patient=#{patient_id}&status=active",
171
+ access_token
172
+ )
173
+ end
174
+ end
175
+ end
176
+
177
+ def get_context_resource(request_body, resource_type, update_resource_id)
178
+ update_resource_id = "#{resource_type}/#{update_resource_id}" unless update_resource_id.include? '/'
179
+ fhir_server = request_body['fhirServer']
180
+ return unless fhir_server.present?
181
+
182
+ access_token = request_body['fhirAuthorization']['access_token'] if request_body['fhirAuthorization']
183
+ make_resource_request(
184
+ "#{fhir_server}/#{update_resource_id}",
185
+ access_token
186
+ )
187
+ end
188
+
189
+ def add_coverage_cards?(selected_response_types, hook_name)
190
+ (['coverage_information', 'create_update_coverage_info'].any? { |x| selected_response_types.include?(x) }) ||
191
+ coverage_information_required_hooks.include?(hook_name)
192
+ end
193
+
194
+ def create_cards_and_system_actions(selected_response_types, hook_name, update_resource_name, resource_type = nil)
195
+ request_body = JSON.parse(request.params.to_json)
196
+ context = request_body['context']
197
+ return if context.nil?
198
+
199
+ cards = []
200
+
201
+ add_basic_cards(selected_response_types, cards, context)
202
+
203
+ add_order_hook_cards(selected_response_types, cards, request_body, hook_name)
204
+
205
+ system_actions = add_coverage_cards(selected_response_types, cards, request_body, hook_name,
206
+ update_resource_name, resource_type)
207
+
208
+ cards.append(get_card_json('instructions.json')) if selected_response_types.include?('instructions') ||
209
+ (cards.empty? && system_actions.nil?)
210
+ cards_response = { 'cards' => cards }
211
+ cards_response['systemActions'] = system_actions if system_actions.present?
212
+ cards_response
213
+ rescue StandardError
214
+ nil
215
+ end
216
+
217
+ def add_order_hook_cards(selected_response_types, cards, request_body, hook_name)
218
+ if selected_response_types.include?('companions_prerequisites')
219
+ cards.append(create_companions_prerequisites_card(request_body['context']))
220
+ end
221
+
222
+ return unless selected_response_types.include?('propose_alternate_request')
223
+
224
+ cards.append(create_alternate_request_card(request_body, hook_name))
225
+ end
226
+
227
+ def add_basic_cards(selected_response_types, cards, context)
228
+ cards.append(create_form_completion_card(context)) if selected_response_types.include?('request_form_completion')
229
+ cards.append(get_card_json('launch_smart_app.json')) if selected_response_types.include?('launch_smart_app')
230
+ cards.append(get_card_json('external_reference.json')) if selected_response_types.include?('external_reference')
231
+ end
232
+
233
+ def add_coverage_cards(selected_response_types, cards, request_body, hook_name, update_resource_name,
234
+ resource_type = nil)
235
+ return unless add_coverage_cards?(selected_response_types, hook_name)
236
+
237
+ coverage = get_patient_coverage(request_body)
238
+ if coverage.present?
239
+ if selected_response_types.include?('coverage_information') ||
240
+ coverage_information_required_hooks.include?(hook_name)
241
+ system_actions = create_coverage_extension_system_actions(request_body, update_resource_name,
242
+ coverage.id, resource_type)
243
+ end
244
+
245
+ if selected_response_types.include?('create_update_coverage_info')
246
+ cards.append(create_or_update_coverage(coverage, request_body['context']))
247
+ end
248
+ end
249
+ system_actions
250
+ end
251
+
252
+ def create_coverage_extension_system_actions(request_body, update_resource_name, coverage_id, resource_type = nil)
253
+ context = request_body['context']
254
+ update_resource = context[update_resource_name]
255
+ prefetch_id = update_resource_name.split(/(?=[A-Z])/).first
256
+
257
+ fhir_resource = if update_resource.is_a? Hash
258
+ FHIR.from_contents(update_resource.to_json)
259
+ elsif request_body['prefetch'] && request_body['prefetch'][prefetch_id]
260
+ FHIR.from_contents(request_body['prefetch'][prefetch_id].to_json)
261
+ else
262
+ get_context_resource(request_body, resource_type, update_resource)
263
+ end
264
+ create_system_actions(fhir_resource, coverage_id)
265
+ rescue StandardError
266
+ nil
267
+ end
268
+
269
+ def create_system_actions(resource, coverage_id)
270
+ return if resource.nil?
271
+
272
+ system_actions = []
273
+ if resource.resourceType == 'Bundle'
274
+ resource.entry.each do |entry|
275
+ entry_resource = entry.resource
276
+ add_coverage_extension(entry_resource, coverage_id)
277
+ system_actions.append({ 'type' => 'update',
278
+ 'description' =>
279
+ "Added coverage information to #{entry_resource.resourceType} resource.",
280
+ 'resource' => entry_resource })
281
+ end
282
+ else
283
+ add_coverage_extension(resource, coverage_id)
284
+ system_actions.append({ 'type' => 'update',
285
+ 'description' => "Added coverage information to #{resource.resourceType} resource.",
286
+ 'resource' => resource })
287
+ end
288
+ system_actions
289
+ end
290
+
291
+ def add_coverage_extension(resource, coverage_id)
292
+ resource.extension = [
293
+ FHIR::Extension.new(
294
+ url: 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information',
295
+ extension: [
296
+ FHIR::Extension.new(
297
+ url: 'coverage',
298
+ valueReference: FHIR::Reference.new(
299
+ reference: "Coverage/#{coverage_id}"
300
+ )
301
+ ),
302
+ FHIR::Extension.new(
303
+ url: 'covered',
304
+ valueCode: 'conditional'
305
+ ),
306
+ FHIR::Extension.new(
307
+ url: 'date',
308
+ valueDate: current_time.strftime('%Y-%m-%d')
309
+ ),
310
+ FHIR::Extension.new(
311
+ url: 'coverage-assertion-id',
312
+ valueString: SecureRandom.hex(32)
313
+ )
314
+ ]
315
+ )
316
+ ]
317
+ end
318
+
319
+ def create_coverage_resource(patient_id)
320
+ FHIR::Coverage.new(
321
+ id: SecureRandom.uuid,
322
+ status: 'draft',
323
+ beneficiary: FHIR::Reference.new(
324
+ reference: "Patient/#{patient_id}"
325
+ ),
326
+ subscriber: FHIR::Reference.new(
327
+ reference: "Patient/#{patient_id}"
328
+ ),
329
+ relationship: FHIR::CodeableConcept.new(
330
+ coding: [
331
+ FHIR::Coding.new(
332
+ system: 'http://terminology.hl7.org/CodeSystem/subscriber-relationship',
333
+ code: 'self'
334
+ )
335
+ ]
336
+ ),
337
+ payor: FHIR::Reference.new(
338
+ reference: "Patient/#{patient_id}"
339
+ )
340
+ )
341
+ end
342
+
343
+ def create_or_update_coverage(coverage, context)
344
+ return if context.nil?
345
+
346
+ if coverage.present?
347
+ action = { 'type' => 'update', 'description' => 'Update current coverage record' }
348
+ coverage.period = FHIR::Period.new(start: current_time.strftime('%Y-%m-%d'),
349
+ end: (current_time + 1.month).strftime('%Y-%m-%d'))
350
+ action['resource'] = coverage
351
+ else
352
+ action = { 'type' => 'create', 'description' => 'Create coverage record' }
353
+ new_coverage = create_coverage_resource(context['patientId'])
354
+ action['resource'] = new_coverage
355
+ end
356
+ coverage_info_card = get_card_json('create_update_coverage_information.json')
357
+ coverage_info_card['suggestions'][0]['actions'] = [action]
358
+ coverage_info_card
359
+ end
360
+
361
+ def create_form_completion_card(context)
362
+ return if context.nil?
363
+
364
+ request_form_completion_card = get_card_json('request_form_completion.json')
365
+ form_completion_task = request_form_completion_card['suggestions'][0]['actions'].find do |action|
366
+ action['resource']['resourceType'] == 'Task'
367
+ end['resource']
368
+
369
+ form_completion_task['for']['reference'] = "Patient/#{context['patientId']}"
370
+ form_completion_task['authoredOn'] = current_time.strftime('%Y-%m-%d')
371
+ request_form_completion_card
372
+ end
373
+
374
+ def update_service_request(service_request, context)
375
+ return if context.nil?
376
+
377
+ service_request['subject']['reference'] = "Patient/#{context['patientId']}"
378
+ service_request['requester']['reference'] = context['userId']
379
+ service_request['authoredOn'] = current_time.strftime('%Y-%m-%d')
380
+ end
381
+
382
+ def create_companions_prerequisites_card(context)
383
+ return if context.nil?
384
+
385
+ companions_prerequisites_card = get_card_json('companions_prerequisites.json')
386
+ card_service_request = companions_prerequisites_card['suggestions'][0]['actions'][0]['resource']
387
+ update_service_request(card_service_request, context)
388
+ companions_prerequisites_card
389
+ end
390
+
391
+ def create_alternate_request_card(request_body, hook_name)
392
+ context = request_body['context']
393
+ return if context.nil?
394
+
395
+ propose_alternate_request_card = get_card_json('propose_alternate_request.json')
396
+
397
+ if hook_name == 'order-dispatch'
398
+ order_resource = get_context_resource(request_body, nil, context['order'])
399
+ else
400
+ draft_orders = context['draftOrders']['entry']
401
+ draft_order_resource = draft_orders[0]['resource']
402
+ order_resource = FHIR.from_contents(draft_order_resource.to_json)
403
+ end
404
+ return if order_resource.nil?
405
+
406
+ order_resource_type = order_resource.resourceType
407
+ order_resource_id = order_resource.id
408
+
409
+ card_actions = propose_alternate_request_card['suggestions'][0]['actions']
410
+ card_actions.append(
411
+ {
412
+ 'type' => 'delete',
413
+ 'description' => 'Remove current order until health assessment has been done',
414
+ 'resourceId' => ["#{order_resource_type}/#{order_resource_id}"]
415
+ }
416
+ )
417
+ update_service_request(card_actions[0]['resource'], context)
418
+ propose_alternate_request_card
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,74 @@
1
+
2
+ {
3
+ "services": [
4
+ {
5
+ "hook": "appointment-book",
6
+ "title": "Appointment Booking CDS Service",
7
+ "description": "An example of a CDS Service that is invoked when user of a CRD Client books a future appointment for a patient",
8
+ "id": "appointment-book-service",
9
+ "prefetch": {
10
+ "user": "{{context.userId}}",
11
+ "patient": "Patient/{{context.patientId}}",
12
+ "coverage": "Coverage?patient={{context.patientId}}&status=active"
13
+ }
14
+ },
15
+ {
16
+ "hook": "encounter-start",
17
+ "title": "Encounter Start CDS Service",
18
+ "description": "An example of a CDS Service that is invoked when the user is initiating a new encounter.",
19
+ "id": "encounter-start-service",
20
+ "prefetch": {
21
+ "user": "{{context.userId}}",
22
+ "patient": "Patient/{{context.patientId}}",
23
+ "encounter": "Encounter/{{context.encounterId}}",
24
+ "coverage": "Coverage?patient={{context.patientId}}&status=active"
25
+ }
26
+ },
27
+ {
28
+ "hook": "encounter-discharge",
29
+ "title": "Encounter Disharge CDS Service Example",
30
+ "description": "An example of a CDS Service that is invoked when the user is performing the discharge process for an encounter - typically an inpatient encounter.",
31
+ "id": "encounter-discharge-service",
32
+ "prefetch": {
33
+ "user": "{{context.userId}}",
34
+ "patient": "Patient/{{context.patientId}}",
35
+ "encounter": "Encounter/{{context.encounterId}}",
36
+ "coverage": "Coverage?patient={{context.patientId}}&status=active"
37
+ }
38
+ },
39
+ {
40
+ "hook": "order-dispatch",
41
+ "title": "Order Dispatch CDS Service Example",
42
+ "description": "An example of a CDS Service that fires when a practitioner is selecting a candidate performer for a pre-existing order that was not tied to a specific performer",
43
+ "id": "order-dispatch-service",
44
+ "prefetch": {
45
+ "patient": "Patient/{{context.patientId}}",
46
+ "performer": "{{context.performer}}",
47
+ "order": "{{context.order}}",
48
+ "coverage": "Coverage?patient={{context.patientId}}&status=active"
49
+ }
50
+ },
51
+ {
52
+ "hook": "order-select",
53
+ "title": "Order Select CDS Service",
54
+ "description": "An example of a CDS Service that fires when a clinician selects one or more orders to place for a patient",
55
+ "id": "order-select-service",
56
+ "prefetch": {
57
+ "user": "{{context.userId}}",
58
+ "patient": "Patient/{{context.patientId}}",
59
+ "coverage": "Coverage?patient={{context.patientId}}&status=active"
60
+ }
61
+ },
62
+ {
63
+ "hook": "order-sign",
64
+ "title": "Order Sign CDS Service",
65
+ "description": "An example of a CDS Service that fires when a clinician is ready to sign one or more orders for a patient",
66
+ "id": "order-sign-service",
67
+ "prefetch": {
68
+ "user": "{{context.userId}}",
69
+ "patient": "Patient/{{context.patientId}}",
70
+ "coverage": "Coverage?patient={{context.patientId}}&status=active"
71
+ }
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,18 @@
1
+ module DaVinciCRDTestKit
2
+ module Routes
3
+ class CDSServicesDiscoveryHandler
4
+ def self.call(...)
5
+ new.call(...)
6
+ end
7
+
8
+ def self.cds_services
9
+ @cds_services ||= File.read(File.join(__dir__, 'cds-services.json'))
10
+ end
11
+
12
+ def call(_env)
13
+ # Check authorization header
14
+ [200, { 'Content-Type' => 'application/json' }, [self.class.cds_services]]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,93 @@
1
+ require_relative '../mock_service_response'
2
+ require_relative '../tags'
3
+ module DaVinciCRDTestKit
4
+ class HookRequestEndpoint < Inferno::DSL::SuiteEndpoint
5
+ include DaVinciCRDTestKit::MockServiceResponse
6
+
7
+ def selected_response_types
8
+ inputs = JSON.parse(result.input_json)
9
+ selected_response_types_input = inputs.detect { |input| input['name'].include?('selected_response_types') }
10
+ selected_response_types_input['value']
11
+ end
12
+
13
+ def test_run_identifier
14
+ extract_iss_claim_and_hook(request)
15
+ end
16
+
17
+ def extract_iss_claim_and_hook(request)
18
+ hook = extract_hook_name(request).to_s
19
+ iss = extract_iss_claim_from_token(request).to_s
20
+
21
+ "#{hook} #{iss}"
22
+ end
23
+
24
+ def extract_iss_claim_from_token(request)
25
+ token = extract_bearer_token(request)
26
+ begin
27
+ payload, = JWT.decode(token, nil, false)
28
+ payload['iss']
29
+ rescue JWT::DecodeError
30
+ nil
31
+ end
32
+ end
33
+
34
+ # Header expected to be a bearer token of the form "Bearer <token>"
35
+ def extract_bearer_token(request)
36
+ request.headers['authorization']&.delete_prefix('Bearer ')
37
+ end
38
+
39
+ def extract_hook_name(request)
40
+ request.params[:hook]
41
+ end
42
+
43
+ def make_response
44
+ hook_name = extract_hook_name(request)
45
+ case hook_name
46
+ when 'appointment-book'
47
+ appointment_book_response(selected_response_types)
48
+ when 'encounter-start'
49
+ encounter_start_response(selected_response_types)
50
+ when 'encounter-discharge'
51
+ encounter_discharge_response(selected_response_types)
52
+ when 'order-select'
53
+ order_select_response(selected_response_types)
54
+ when 'order-sign'
55
+ order_sign_response(selected_response_types)
56
+ when 'order-dispatch'
57
+ order_dispatch_response(selected_response_types)
58
+ else
59
+ response.status = 400
60
+ response.body = 'Invalid Request: Request did not contain a valid hook in the `hook` field.'
61
+ end
62
+ end
63
+
64
+ def tags
65
+ hook_name = extract_hook_name(request)
66
+ case hook_name
67
+ when 'appointment-book'
68
+ [APPOINTMENT_BOOK_TAG]
69
+ when 'encounter-start'
70
+ [ENCOUNTER_START_TAG]
71
+ when 'encounter-discharge'
72
+ [ENCOUNTER_DISCHARGE_TAG]
73
+ when 'order-select'
74
+ [ORDER_SELECT_TAG]
75
+ when 'order-sign'
76
+ [ORDER_SIGN_TAG]
77
+ when 'order-dispatch'
78
+ [ORDER_DISPATCH_TAG]
79
+ else
80
+ response.status = 400
81
+ response.body = 'Invalid Request: Request did not contain a valid hook in the `hook` field.'
82
+ end
83
+ end
84
+
85
+ def name
86
+ extract_hook_name(request).gsub('-', '_')
87
+ end
88
+
89
+ def update_result
90
+ results_repo.update(result.id, result: 'pass')
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../jwks'
2
+
3
+ module DaVinciCRDTestKit
4
+ module Routes
5
+ class JWKSetEndpointHandler
6
+ def self.call(...)
7
+ new.call(...)
8
+ end
9
+
10
+ def call(_env)
11
+ [200, { 'Content-Type' => 'application/json' }, [JWKS.jwks_json]]
12
+ end
13
+ end
14
+ end
15
+ end