davinci_crd_test_kit 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/davinci_crd_test_kit/card_responses/companions_prerequisites.json +58 -0
- data/lib/davinci_crd_test_kit/card_responses/create_update_coverage_information.json +20 -0
- data/lib/davinci_crd_test_kit/card_responses/external_reference.json +21 -0
- data/lib/davinci_crd_test_kit/card_responses/instructions.json +14 -0
- data/lib/davinci_crd_test_kit/card_responses/launch_smart_app.json +21 -0
- data/lib/davinci_crd_test_kit/card_responses/propose_alternate_request.json +71 -0
- data/lib/davinci_crd_test_kit/card_responses/request_form_completion.json +227 -0
- data/lib/davinci_crd_test_kit/cards_validation.rb +234 -0
- data/lib/davinci_crd_test_kit/client_fhir_api_group.rb +762 -0
- data/lib/davinci_crd_test_kit/client_hook_request_validation.rb +15 -0
- data/lib/davinci_crd_test_kit/client_hooks_group.rb +706 -0
- data/lib/davinci_crd_test_kit/client_tests/appointment_book_receive_request_test.rb +71 -0
- data/lib/davinci_crd_test_kit/client_tests/client_display_cards_attest.rb +48 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_create_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_read_test.rb +39 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_search_test.rb +232 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_update_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/client_fhir_api_validation_test.rb +60 -0
- data/lib/davinci_crd_test_kit/client_tests/decode_auth_token_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/encounter_discharge_receive_request_test.rb +68 -0
- data/lib/davinci_crd_test_kit/client_tests/encounter_start_receive_request_test.rb +68 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_optional_fields_test.rb +41 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_required_fields_test.rb +40 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_context_test.rb +63 -0
- data/lib/davinci_crd_test_kit/client_tests/hook_request_valid_prefetch_test.rb +151 -0
- data/lib/davinci_crd_test_kit/client_tests/order_dispatch_receive_request_test.rb +79 -0
- data/lib/davinci_crd_test_kit/client_tests/order_select_receive_request_test.rb +76 -0
- data/lib/davinci_crd_test_kit/client_tests/order_sign_receive_request_test.rb +79 -0
- data/lib/davinci_crd_test_kit/client_tests/retrieve_jwks_test.rb +65 -0
- data/lib/davinci_crd_test_kit/client_tests/token_header_test.rb +34 -0
- data/lib/davinci_crd_test_kit/client_tests/token_payload_test.rb +61 -0
- data/lib/davinci_crd_test_kit/crd_client_suite.rb +156 -0
- data/lib/davinci_crd_test_kit/crd_jwks.json +59 -0
- data/lib/davinci_crd_test_kit/crd_options.rb +9 -0
- data/lib/davinci_crd_test_kit/crd_server_suite.rb +115 -0
- data/lib/davinci_crd_test_kit/ext/inferno_core/runnable.rb +22 -0
- data/lib/davinci_crd_test_kit/hook_request_field_validation.rb +410 -0
- data/lib/davinci_crd_test_kit/jwks.rb +25 -0
- data/lib/davinci_crd_test_kit/jwt_helper.rb +74 -0
- data/lib/davinci_crd_test_kit/mock_service_response.rb +421 -0
- data/lib/davinci_crd_test_kit/routes/cds-services.json +74 -0
- data/lib/davinci_crd_test_kit/routes/cds_services_discovery_handler.rb +18 -0
- data/lib/davinci_crd_test_kit/routes/hook_request_endpoint.rb +93 -0
- data/lib/davinci_crd_test_kit/routes/jwk_set_endpoint_handler.rb +15 -0
- data/lib/davinci_crd_test_kit/server_appointment_book_group.rb +173 -0
- data/lib/davinci_crd_test_kit/server_discovery_group.rb +59 -0
- data/lib/davinci_crd_test_kit/server_encounter_discharge_group.rb +144 -0
- data/lib/davinci_crd_test_kit/server_encounter_start_group.rb +144 -0
- data/lib/davinci_crd_test_kit/server_hook_request_validation.rb +15 -0
- data/lib/davinci_crd_test_kit/server_hooks_group.rb +69 -0
- data/lib/davinci_crd_test_kit/server_order_dispatch_group.rb +173 -0
- data/lib/davinci_crd_test_kit/server_order_select_group.rb +169 -0
- data/lib/davinci_crd_test_kit/server_order_sign_group.rb +198 -0
- data/lib/davinci_crd_test_kit/server_required_card_response_validation_group.rb +23 -0
- data/lib/davinci_crd_test_kit/server_tests/additional_orders_validation_test.rb +70 -0
- data/lib/davinci_crd_test_kit/server_tests/card_optional_fields_validation_test.rb +47 -0
- data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_across_hooks_validation_test.rb +32 -0
- data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_received_test.rb +58 -0
- data/lib/davinci_crd_test_kit/server_tests/coverage_information_system_action_validation_test.rb +121 -0
- data/lib/davinci_crd_test_kit/server_tests/create_or_update_coverage_info_response_validation_test.rb +72 -0
- data/lib/davinci_crd_test_kit/server_tests/discovery_endpoint_test.rb +88 -0
- data/lib/davinci_crd_test_kit/server_tests/discovery_services_validation_test.rb +65 -0
- data/lib/davinci_crd_test_kit/server_tests/external_reference_card_across_hooks_validation_test.rb +28 -0
- data/lib/davinci_crd_test_kit/server_tests/external_reference_card_validation_test.rb +36 -0
- data/lib/davinci_crd_test_kit/server_tests/form_completion_response_validation_test.rb +79 -0
- data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_across_hooks_test.rb +25 -0
- data/lib/davinci_crd_test_kit/server_tests/instructions_card_received_test.rb +28 -0
- data/lib/davinci_crd_test_kit/server_tests/launch_smart_app_card_validation_test.rb +38 -0
- data/lib/davinci_crd_test_kit/server_tests/propose_alternate_request_card_validation_test.rb +65 -0
- data/lib/davinci_crd_test_kit/server_tests/service_call_test.rb +86 -0
- data/lib/davinci_crd_test_kit/server_tests/service_request_context_validation_test.rb +30 -0
- data/lib/davinci_crd_test_kit/server_tests/service_request_optional_fields_validation_test.rb +41 -0
- data/lib/davinci_crd_test_kit/server_tests/service_request_required_fields_validation_test.rb +43 -0
- data/lib/davinci_crd_test_kit/server_tests/service_response_validation_test.rb +82 -0
- data/lib/davinci_crd_test_kit/suggestion_actions_validation.rb +123 -0
- data/lib/davinci_crd_test_kit/tags.rb +8 -0
- data/lib/davinci_crd_test_kit/test_helper.rb +23 -0
- data/lib/davinci_crd_test_kit/urls.rb +52 -0
- data/lib/davinci_crd_test_kit/version.rb +3 -0
- data/lib/davinci_crd_test_kit.rb +2 -0
- 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
|