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