davinci_crd_test_kit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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