Module: DaVinciCRDTestKit::CardsIdentification

Includes:
HookRequestFieldValidation, SuggestionActionsValidation
Included in:
HookRequestEndpoint, Jobs::InvokeHook, ResponseLogicalModelValidation, V201::AdditionalOrdersValidationTest, V201::ClientCardDisplayAttest, V201::ClientCardMustSupportCoverageInformationTest, V201::ClientCardMustSupportExternalReferenceTest, V201::ClientCardMustSupportInstructionsTest, V201::CoverageInformationSystemActionReceivedTest, V201::CreateOrUpdateCoverageInfoResponseValidationTest, V201::ExternalReferenceCardValidationTest, V201::FormCompletionResponseValidationTest, V201::InstructionsCardReceivedTest, V201::LaunchSmartAppCardValidationTest, V201::ProposeAlternateRequestCardValidationTest, V221::AdditionalOrdersValidationTest, V221::AllResponsesIncludeCoverageInformationTest, V221::ClientCardDisplayAttest, V221::ClientCardMustSupportCoverageInformationTest, V221::ClientCoverageInfoUpdateTest, V221::ClientHookResponseSupportCoverageInformationTest, V221::ClientUnknownContentAttestationTest, V221::CoverageInfoConfigurationTest, V221::CoverageInformationCardAbsenceTest, V221::CoverageInformationMustSupportTest, V221::CoverageInformationSystemActionValidationTest, V221::CreateOrUpdateCoverageInfoResponseValidationTest, V221::ExternalReferenceCardValidationTest, V221::FormCompletionResponseValidationTest, V221::InstructionsCardReceivedTest, V221::LaunchSmartAppCardValidationTest, V221::ProposeAlternateRequestCardValidationTest, V221::UnknownCDSHooksElementsTest, V221::UnknownConfigurationTest, V221::UnknownContextTest
Defined in:
lib/davinci_crd_test_kit/cross_suite/cards_identification.rb

Constant Summary collapse

COVERAGE_INFO_EXT_URL =
'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information'.freeze
ADDITIONAL_ORDERS_RESPONSE_TYPE =
'companions_prerequisites'.freeze
COVERAGE_INFORMATION_RESPONSE_TYPE =
'coverage_information'.freeze
CREATE_OR_UPDATE_COVERAGE_RESPONSE_TYPE =
'create_update_coverage_info'.freeze
EXTERNAL_REFERENCE_RESPONSE_TYPE =
'external_reference'.freeze
FORM_COMPLETION_RESPONSE_TYPE =
'request_form_completion'.freeze
INSTRUCTIONS_RESPONSE_TYPE =
'instructions'.freeze
LAUNCH_SMART_APP_RESPONSE_TYPE =
'launch_smart_app'.freeze
PROPOSE_ALTERNATIVE_REQUEST_RESPONSE_TYPE =
'propose_alternate_request'.freeze
COVERAGE_INFO_CONFIGURATION_CODE =
'coverage-info'.freeze
ADDITIONAL_ORDERS_EXPECTED_RESOURCE_TYPES =
%w[
  CommunicationRequest Device DeviceRequest Medication
  MedicationRequest NutritionOrder ServiceRequest
  VisionPrescription
].freeze
PROPOSE_ALTERNATIVE_REQUEST_EXPECTED_RESOURCE_TYPES =
%w[
  CommunicationRequest Device DeviceRequest Encounter Medication
  MedicationRequest NutritionOrder ServiceRequest
  VisionPrescription
].freeze
COVERAGE_INFO_EXPECTED_RESOURCE_TYPES =
%w[
  Appointment CommunicationRequest DeviceRequest Encounter MedicationRequest
  NutritionOrder ServiceRequest VisionPrescription
].freeze

Constants included from ProfilesAndResourceTypes

ProfilesAndResourceTypes::ORDER_OR_ENCOUNTER_RESOURCE_CLASSES, ProfilesAndResourceTypes::ORDER_RESOURCE_CLASSES, ProfilesAndResourceTypes::ORDER_RESOURCE_TYPES

Constants included from RequestsLogicalModelValidation

RequestsLogicalModelValidation::CRD_CDS_HOOK_REQUEST_MODEL_URL, RequestsLogicalModelValidation::PERFORMER_ALLOWED_RESOURCE_TYPES, RequestsLogicalModelValidation::USER_ID_ALLOWED_RESOURCE_TYPES

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HookRequestFieldValidation

#hook_request_context_check, #hook_request_optional_fields_check, #hook_request_prefetch_check, #hook_request_required_fields_check, #json_parse, #no_error_validation

Methods included from ProfilesAndResourceTypes

#structure_definition_map, #structure_definition_map_v201, #structure_definition_map_v221

Methods included from ServerBaseURLs

#client_fhir_base_url, #fhir_url, #instance_url, #search_url

Methods included from BaseURLs

#inferno_base_url, #resume_fail_url, #resume_pass_url

Methods included from RequestsLogicalModelValidation

#validate_request_against_logical_model

Methods included from LogicalModelsOverrideHelper

#allowed_resource_type?, #check_appointment_conformance, #check_order_like_resource_conformance, #check_resource_conformance_to_coverage_profile, #check_resource_conformance_to_order_or_encounter_profile, #check_resource_conformance_to_order_profile, #check_resource_conformance_to_questionnaire_task_profile, #check_resource_type_and_validate, #filter_logical_model_extension_issues, #local_reference?, #logical_model_extension_issue?, #manually_check_appointment_validation_errors, #parse_action_resource, #primary_performer_type?, #referenced_resource_present_in_bundle?, #reject_resource_issues, #resolved_participant_patient_slice_issue?, #resolved_participant_primary_performer_slice_issue?

Methods included from SuggestionActionsValidation

#action_fields_validation, #action_resource_type_check, #actions_check

Class Method Details

.humanize_response_type(response_type) ⇒ Object



38
39
40
41
42
43
44
45
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 38

def self.humanize_response_type(response_type)
  response_type.split('_')
    .map(&:capitalize)
    .join(' ')
    .sub('Smart', 'SMART')
    .sub('Create Update', 'Create/Update')
    .sub('Companions Prerequisites', 'Companions/Prerequisites')
end

Instance Method Details

#additional_orders_response_type?(card, expected_resource_types: ADDITIONAL_ORDERS_EXPECTED_RESOURCE_TYPES) ⇒ Boolean

Returns:

  • (Boolean)


129
130
131
132
133
134
135
136
137
138
139
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 129

def additional_orders_response_type?(card, expected_resource_types: ADDITIONAL_ORDERS_EXPECTED_RESOURCE_TYPES)
  return false if card['suggestions'].blank?

  card['suggestions'].all? do |suggestion|
    actions = suggestion['actions']
    actions&.all? do |action|
      action['type'] == 'create' &&
        (expected_resource_types.blank? || action_resource_type_check(action, expected_resource_types))
    end
  end
end

#cache_sorted_cards(requests, sorted_cards) ⇒ Object



313
314
315
316
317
318
319
320
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 313

def cache_sorted_cards(requests, sorted_cards)
  scratch['sorted_cards'] = {
    'hook_instances' => hook_instances_from_requests(requests),
    'data' => sorted_cards
  }
rescue JSON::ParserError
  nil
end

#check_action_type(actions, action_type, expected_resource_types) ⇒ Object



227
228
229
230
231
232
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 227

def check_action_type(actions, action_type, expected_resource_types)
  actions&.any? do |action|
    action['type'] == action_type &&
      (expected_resource_types.blank? || action_resource_type_check(action, expected_resource_types))
  end
end

#coverage_info_card_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
72
73
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 69

def coverage_info_card_type?(card)
  return false unless card.respond_to?(:dig)

  card.dig('source', 'topic', 'code') == COVERAGE_INFO_CONFIGURATION_CODE
end

#coverage_info_configuration_disabled?(request_body) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
117
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 114

def coverage_info_configuration_disabled?(request_body)
  request_body.is_a?(Hash) &&
    request_body.dig('extension', 'davinci-crd.configuration', COVERAGE_INFO_CONFIGURATION_CODE) == false
end

#coverage_info_content(response_body) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 81

def coverage_info_content(response_body)
  return [[], []] unless response_body.is_a?(Hash)

  cards =
    if response_body['cards'].is_a?(Array)
      response_body['cards'].select { |card| coverage_info_card_type?(card) }
    else
      []
    end
  actions =
    if response_body['systemActions'].is_a?(Array)
      response_body['systemActions'].select { |action| coverage_info_system_action_type?(action) }
    else
      []
    end

  [cards, actions]
end

#coverage_info_response?(response_body) ⇒ Boolean

Returns:

  • (Boolean)


108
109
110
111
112
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 108

def coverage_info_response?(response_body)
  cards, actions = coverage_info_content(response_body)

  cards.present? || actions.present?
end

#coverage_info_system_action_response?(response_body) ⇒ Boolean

Returns:

  • (Boolean)


100
101
102
103
104
105
106
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 100

def coverage_info_system_action_response?(response_body)
  return false unless response_body.is_a?(Hash)

  return false unless response_body['systemActions'].is_a?(Array)

  response_body['systemActions'].any? { |action| coverage_information_response_type?(action) }
end

#coverage_info_system_action_type?(action) ⇒ Boolean

Returns:

  • (Boolean)


75
76
77
78
79
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 75

def coverage_info_system_action_type?(action)
  return false unless action.respond_to?(:[])

  [COVERAGE_INFORMATION_RESPONSE_TYPE, FORM_COMPLETION_RESPONSE_TYPE].include?(identify_action_type(action))
end

#coverage_information_response_type?(action) ⇒ Boolean

rubocop:disable Metrics/CyclomaticComplexity

Returns:

  • (Boolean)


141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 141

def coverage_information_response_type?(action) # rubocop:disable Metrics/CyclomaticComplexity
  return false unless action.respond_to?(:[])
  return false unless action['type'] == 'update'

  resource = action['resource']&.to_hash
  return false unless COVERAGE_INFO_EXPECTED_RESOURCE_TYPES.include? resource&.dig('resourceType')

  unless resource&.dig('extension')&.any? { |extension| extension_url(extension) == COVERAGE_INFO_EXT_URL }
    return false
  end

  true
end

#create_or_update_coverage_action_response_type?(action) ⇒ Boolean

Returns:

  • (Boolean)


168
169
170
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 168

def create_or_update_coverage_action_response_type?(action)
  ['create', 'update'].include?(action['type']) && action_resource_type_check(action, ['Coverage'])
end

#create_or_update_coverage_card_response_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


162
163
164
165
166
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 162

def create_or_update_coverage_card_response_type?(card)
  card['suggestions']&.one? && card['suggestions'].first['actions']&.one? do |action|
    create_or_update_coverage_action_response_type?(action)
  end
end

#create_questionnaire_action_response_type?(action) ⇒ Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 172

def create_questionnaire_action_response_type?(action)
  action['type'] == 'create' && action_resource_type_check(action, ['Questionnaire'])
end

#disable_coverage_info_configuration!(request_body) ⇒ Object



119
120
121
122
123
124
125
126
127
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 119

def disable_coverage_info_configuration!(request_body)
  request_body['extension'] = {} unless request_body['extension'].is_a?(Hash)
  unless request_body['extension']['davinci-crd.configuration'].is_a?(Hash)
    request_body['extension']['davinci-crd.configuration'] = {}
  end
  request_body['extension']['davinci-crd.configuration'][COVERAGE_INFO_CONFIGURATION_CODE] = false

  request_body
end

#extension_url(extension) ⇒ Object



155
156
157
158
159
160
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 155

def extension_url(extension)
  return extension.url if extension.respond_to?(:url)
  return extension['url'] if extension.respond_to?(:[])

  nil
end

#external_reference_response_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


176
177
178
179
180
181
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 176

def external_reference_response_type?(card)
  links = card['links']
  return false if links.blank?

  links.all? { |link| link['type'] == 'absolute' }
end

#extract_coverage_information_extensions(sorted_cards) ⇒ Object



290
291
292
293
294
295
296
297
298
299
300
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 290

def extract_coverage_information_extensions(sorted_cards)
  coverage_information_extensions = []

  sorted_cards['actions'][COVERAGE_INFORMATION_RESPONSE_TYPE].each do |coverage_information_action|
    coverage_information_action['resource']['extension'].each do |extension|
      coverage_information_extensions << extension if extension['url'] == COVERAGE_INFO_EXT_URL
    end
  end

  coverage_information_extensions.map { |ext| FHIR::Extension.new(ext) }
end

#form_completion_action_response_type?(action) ⇒ Boolean

Returns:

  • (Boolean)


189
190
191
192
193
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 189

def form_completion_action_response_type?(action)
  action['type'] == 'create' &&
    action_resource_type_check(action, ['Task']) &&
    form_completion_task_questionnaire?(action['resource'])
end

#form_completion_card_response_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
187
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 183

def form_completion_card_response_type?(card)
  card['suggestions']&.all? do |suggestion|
    suggestion['actions']&.one? { |action| form_completion_action_response_type?(action) }
  end
end

#form_completion_task_questionnaire?(task_action) ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
198
199
200
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 195

def form_completion_task_questionnaire?(task_action)
  task_action.dig('code', 'coding')&.any? { |code| code['code'] == 'complete-questionnaire' } &&
    task_action['input']&.any? do |input|
      input.dig('type', 'text') == 'questionnaire' && valid_url?(input['valueCanonical'])
    end
end

#hook_instances_from_requests(requests) ⇒ Object



326
327
328
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 326

def hook_instances_from_requests(requests)
  requests.map { |request| JSON.parse(request.request_body)&.dig('hookInstance') }.compact
end

#identify_action_type(action) ⇒ Object



60
61
62
63
64
65
66
67
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 60

def identify_action_type(action)
  return nil unless action['type'].present? # card, not an action
  return COVERAGE_INFORMATION_RESPONSE_TYPE if coverage_information_response_type?(action)
  return CREATE_OR_UPDATE_COVERAGE_RESPONSE_TYPE if create_or_update_coverage_action_response_type?(action)
  return FORM_COMPLETION_RESPONSE_TYPE if form_completion_action_response_type?(action)

  nil
end

#identify_card_type(card) ⇒ Object

rubocop:disable Metrics/CyclomaticComplexity



47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 47

def identify_card_type(card) # rubocop:disable Metrics/CyclomaticComplexity
  return nil if card['type'].present? # action, not a card
  return ADDITIONAL_ORDERS_RESPONSE_TYPE if additional_orders_response_type?(card)
  return CREATE_OR_UPDATE_COVERAGE_RESPONSE_TYPE if create_or_update_coverage_card_response_type?(card)
  return EXTERNAL_REFERENCE_RESPONSE_TYPE if external_reference_response_type?(card)
  return FORM_COMPLETION_RESPONSE_TYPE if form_completion_card_response_type?(card)
  return INSTRUCTIONS_RESPONSE_TYPE if instructions_response_type?(card)
  return LAUNCH_SMART_APP_RESPONSE_TYPE if launch_smart_app_response_type?(card)
  return PROPOSE_ALTERNATIVE_REQUEST_RESPONSE_TYPE if propose_alternative_request_response_type?(card)

  nil
end

#initialize_sorted_cards_hashObject



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 269

def initialize_sorted_cards_hash
  {
    'cards' => {
      ADDITIONAL_ORDERS_RESPONSE_TYPE => [],
      CREATE_OR_UPDATE_COVERAGE_RESPONSE_TYPE => [],
      EXTERNAL_REFERENCE_RESPONSE_TYPE => [],
      FORM_COMPLETION_RESPONSE_TYPE => [],
      INSTRUCTIONS_RESPONSE_TYPE => [],
      LAUNCH_SMART_APP_RESPONSE_TYPE => [],
      PROPOSE_ALTERNATIVE_REQUEST_RESPONSE_TYPE => [],
      nil => [] # unknown type
    },
    'actions' => {
      COVERAGE_INFORMATION_RESPONSE_TYPE => [],
      CREATE_OR_UPDATE_COVERAGE_RESPONSE_TYPE => [],
      FORM_COMPLETION_RESPONSE_TYPE => [],
      nil => [] # unknown type
    }
  }
end

#instructions_response_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


202
203
204
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 202

def instructions_response_type?(card)
  card['links'].blank? && card['suggestions'].blank?
end

#launch_smart_app_response_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


206
207
208
209
210
211
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 206

def launch_smart_app_response_type?(card)
  links = card['links']
  return false if links.blank?

  links.all? { |link| link['type'] == 'smart' }
end

#list_card_types_in_requests(hooks_requests) ⇒ Object



234
235
236
237
238
239
240
241
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 234

def list_card_types_in_requests(hooks_requests)
  sorted_cards = sorted_cards_from_requests(hooks_requests)

  present_card_types = sorted_cards['cards'].select { |key, value| key.present? && value.present? }.keys
  present_action_types = sorted_cards['actions'].select { |key, value| key.present? && value.present? }.keys

  present_card_types.map { |type| "#{type}_card" } + present_action_types.map { |type| "#{type}_action" }
end

#propose_alternative_request_response_type?(card, expected_resource_types: PROPOSE_ALTERNATIVE_REQUEST_EXPECTED_RESOURCE_TYPES) ⇒ Boolean

Returns:

  • (Boolean)


213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 213

def propose_alternative_request_response_type?(
  card,
  expected_resource_types: PROPOSE_ALTERNATIVE_REQUEST_EXPECTED_RESOURCE_TYPES
)
  card['suggestions']&.any? do |suggestion|
    actions = suggestion['actions']
    has_update = check_action_type(actions, 'update', expected_resource_types)
    has_delete = check_action_type(actions, 'delete', expected_resource_types)
    has_create = check_action_type(actions, 'create', expected_resource_types)

    has_update || (has_delete && has_create)
  end
end

#sort_card_types_from_request(request, sorted_cards) ⇒ Object



258
259
260
261
262
263
264
265
266
267
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 258

def sort_card_types_from_request(request, sorted_cards)
  response_body = JSON.parse(request.response_body)
  response_body['cards']&.each do |card|
    sorted_cards['cards'][identify_card_type(card)] << card
  end

  response_body['systemActions']&.each do |action|
    sorted_cards['actions'][identify_action_type(action)] << action
  end
end

#sorted_cards_cached?(requests) ⇒ Boolean

Returns:

  • (Boolean)


302
303
304
305
306
307
308
309
310
311
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 302

def sorted_cards_cached?(requests)
  return false unless scratch['sorted_cards'].present?
  return false unless scratch['sorted_cards']['hook_instances'].length == requests.length

  requests.all? do |request|
    scratch['sorted_cards']['hook_instances'].include?(JSON.parse(request.request_body)&.dig('hookInstance'))
  rescue JSON::ParserError
    false
  end
end

#sorted_cards_from_cacheObject



322
323
324
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 322

def sorted_cards_from_cache
  scratch['sorted_cards']['data']
end

#sorted_cards_from_requests(hooks_requests) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 243

def sorted_cards_from_requests(hooks_requests)
  return sorted_cards_from_cache if sorted_cards_cached?(hooks_requests)

  sorted_cards = initialize_sorted_cards_hash

  hooks_requests.each do |request|
    sort_card_types_from_request(request, sorted_cards)
  rescue JSON::ParserError
    next
  end

  cache_sorted_cards(hooks_requests, sorted_cards)
  sorted_cards
end