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::CoverageInfoConfigurationTest, V221::CoverageInformationMustSupportTest, V221::CoverageInformationSystemActionReceivedTest, 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

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, #local_reference?, #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

Instance Method Details

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

Returns:

  • (Boolean)


112
113
114
115
116
117
118
119
120
121
122
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 112

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



295
296
297
298
299
300
301
302
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 295

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



209
210
211
212
213
214
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 209

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)


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

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)


97
98
99
100
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 97

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



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 72

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)


91
92
93
94
95
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 91

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

  cards.present? || actions.present?
end

#coverage_info_system_action_type?(action) ⇒ Boolean

Returns:

  • (Boolean)


66
67
68
69
70
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 66

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)


124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 124

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

  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)


150
151
152
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 150

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)


144
145
146
147
148
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 144

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)


154
155
156
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 154

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

#disable_coverage_info_configuration!(request_body) ⇒ Object



102
103
104
105
106
107
108
109
110
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 102

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



137
138
139
140
141
142
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 137

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)


158
159
160
161
162
163
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 158

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



272
273
274
275
276
277
278
279
280
281
282
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 272

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)


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

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)


165
166
167
168
169
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 165

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)


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

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



308
309
310
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 308

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

#identify_action_type(action) ⇒ Object



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

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



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

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



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 251

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)


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

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

#launch_smart_app_response_type?(card) ⇒ Boolean

Returns:

  • (Boolean)


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

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



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

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)


195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 195

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



240
241
242
243
244
245
246
247
248
249
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 240

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)


284
285
286
287
288
289
290
291
292
293
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 284

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



304
305
306
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 304

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

#sorted_cards_from_requests(hooks_requests) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/davinci_crd_test_kit/cross_suite/cards_identification.rb', line 225

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