Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ hl7.fhir.us.davinci-crd_2.2.1,resp-32,https://hl7.org/fhir/us/davinci-crd/2.2.1/
hl7.fhir.us.davinci-crd_2.2.1,resp-33,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-33,"Where multiple repetitions apply to the same coverage, they **MAY** have the same coverage-assertion-ids and satisfied-pa-ids (if present).",MAY,CRD Server,true,,,"",""
hl7.fhir.us.davinci-crd_2.2.1,resp-35,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-35,"However, CRD servers **SHALL NOT** send a `systemAction` to update the order unless something is new or changed.",SHALL NOT,CRD Server,false,,,"",""
hl7.fhir.us.davinci-crd_2.2.1,resp-36,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-36,CRD servers **SHOULD** take into account the previous decision in deciding how much processing is necessary before returning a response.,SHOULD,CRD Server,false,,,"",""
hl7.fhir.us.davinci-crd_2.2.1,resp-37,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-37,"When returning a `systemAction` to update a resource with the ""Coverage Information"" response type, the resource content **SHALL NOT** make changes to any data elements other than adding or modifying coverage-information extensions.",SHALL NOT,CRD Server,true,,,"",""
hl7.fhir.us.davinci-crd_2.2.1,resp-37,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-37,"When returning a `systemAction` to update a resource with the ""Coverage Information"" response type, the resource content **SHALL NOT** make changes to any data elements other than adding or modifying coverage-information extensions.",SHALL NOT,CRD Server,true,,,"3.2.3.07, 3.3.3.07, 3.4.3.07","crd_server_v221-crd_v221_server_hooks-crd_v221_server_encounter_start-Group03-crd_v221_coverage_info_system_action_validation, crd_server_v221-crd_v221_server_hooks-crd_v221_server_encounter_discharge-Group03-crd_v221_coverage_info_system_action_validation, crd_server_v221-crd_v221_server_hooks-crd_v221_server_order_select-Group03-crd_v221_coverage_info_system_action_validation"
hl7.fhir.us.davinci-crd_2.2.1,resp-38,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-38,"If a *coverage-information* extension indicates the need to collect additional information (via 'doc-needed'), the extension **SHOULD** include a reference to the questionnaire(s) to be completed.",SHOULD,CRD Server,true,,,"",""
hl7.fhir.us.davinci-crd_2.2.1,resp-39,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-39,CRD Servers **SHOULD** access information via the FHIR Rest API as part of CRD and only fall back to data capture via DTR when computable access is not possible.,SHOULD,CRD Server,false,,,"",""
hl7.fhir.us.davinci-crd_2.2.1,resp-42,https://hl7.org/fhir/us/davinci-crd/2.2.1/en/cards.html#ci-c-resp-42,The CRD server **SHOULD** either prompt for the additional needed information using DTR or return a coverage-information extension indicating that the patient is not covered with a reason indicating the issue (e.g. the member could not be found/resolved).,SHOULD,CRD Server,false,,,"",""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
require_relative '../../server_hook_request_validation'
require_relative '../../server_test_helper'
require_relative '../../server_hook_helper'
require_relative 'hook_request_resource_resolution'

module DaVinciCRDTestKit
module V221
class CoverageInformationSystemActionValidationTest < Inferno::Test
include DaVinciCRDTestKit::ServerHookRequestValidation
include DaVinciCRDTestKit::ServerTestHelper
include DaVinciCRDTestKit::ServerHookHelper
include HookRequestResourceResolution

COVERAGE_INFO_EXT_URL = 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information'.freeze

title 'All Coverage Information system actions received are valid'
id :crd_v221_coverage_info_system_action_validation
Expand All @@ -14,6 +20,8 @@ class CoverageInformationSystemActionValidationTest < Inferno::Test
system actions received. It verifies the following for each action:
- The action type is `update`.
- The resource within the action conforms its respective FHIR profile.
- The resource does not change any data elements other than adding or modifying
the `coverage-information` extension.

Additionally, the test examines the `coverage-info` extensions within the resource to ensure that:
- Entries referencing differing coverage have distinct `coverage-assertion-ids` and `satisfied-pa-ids`
Expand All @@ -23,11 +31,13 @@ class CoverageInformationSystemActionValidationTest < Inferno::Test
)

verifies_requirements 'hl7.fhir.us.davinci-crd_2.2.1@resp-32',
'hl7.fhir.us.davinci-crd_2.2.1@resp-37',
'hl7.fhir.us.davinci-crd_2.2.1@resp-47',
'hl7.fhir.us.davinci-crd_2.2.1@resp-48',
'hl7.fhir.us.davinci-crd_2.2.1@resp-52'

input :coverage_info
input :mock_ehr_bundle, optional: true

def find_extension_value(extension, url, *properties)
found_extension = extension.extension.find { |ext| ext.url == url }
Expand All @@ -42,7 +52,7 @@ def find_extension_value(extension, url, *properties)

def extract_and_group_coverage_info(resource)
resource.extension.each_with_object({}) do |extension, grouped_extensions|
next unless extension.url == 'http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information'
next unless extension.url == COVERAGE_INFO_EXT_URL

coverage_key = find_extension_value(extension, 'coverage', 'valueReference', 'reference')
grouped_extensions[coverage_key] ||= []
Expand Down Expand Up @@ -93,6 +103,52 @@ def different_coverage_conformance_error_msg(resource_ref, id_name)
"#{resource_ref}: extensions referencing differing coverage SHALL have distinct #{id_name}."
end

def normalize_value(value)
case value
when Hash
value.transform_values { |child| normalize_value(child) }
when Array
value.map { |child| normalize_value(child) }.sort_by(&:to_json)
else
value
end
end

def strip_coverage_info_extensions(resource_hash)
normalized_hash = resource_hash.deep_dup
return normalized_hash unless normalized_hash['extension'].is_a?(Array)

normalized_hash['extension'] = normalized_hash['extension'].reject do |extension|
extension['url'] == COVERAGE_INFO_EXT_URL
end
normalized_hash.delete('extension') if normalized_hash['extension'].empty?
normalized_hash
end

def verify_only_coverage_info_changed(action)
request = matching_request_for_action(action)
source_resource = find_action_source_resource(action, request)
updated_resource_hash = action['resource']
resource_ref = "#{updated_resource_hash['resourceType']}/#{updated_resource_hash['id']}"
unless source_resource
messages << {
type: 'warning',
message: 'Inferno could not resolve the original source resource for Coverage Information systemAction ' \
"targeting #{resource_ref}, so it could not verify that only coverage-information extensions " \
'were changed.'
}
return
end

source_resource_hash = source_resource.to_hash

source_without_coverage_info = normalize_value(strip_coverage_info_extensions(source_resource_hash))
updated_without_coverage_info = normalize_value(strip_coverage_info_extensions(updated_resource_hash))

assert source_without_coverage_info == updated_without_coverage_info,
"#{resource_ref}: resource content changed outside the coverage-information extension."
end

def coverage_info_system_action_check(coverage_info_system_action)
type = coverage_info_system_action['type']
assert type, '`type` field is missing.'
Expand All @@ -104,9 +160,11 @@ def coverage_info_system_action_check(coverage_info_system_action)

grouped_coverage_info = extract_and_group_coverage_info(resource)
multiple_extensions_conformance_check(grouped_coverage_info, resource)
verify_only_coverage_info_changed(coverage_info_system_action)
end

run do
load_tagged_requests(tested_hook_name)
parsed_coverage_info = parse_json(coverage_info)
error_messages = []
parsed_coverage_info.each do |action|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
module DaVinciCRDTestKit
module V221
module HookRequestResourceResolution
def mock_ehr_bundle_resource
@mock_ehr_bundle_resource ||= JSON.parse(mock_ehr_bundle) if mock_ehr_bundle.present?
rescue JSON::ParserError
nil
end

def matching_request_for_action(action)
requests.find do |request|
response = JSON.parse(request.response_body)
request.status == 200 && Array(response['systemActions']).any? { |candidate| candidate == action }
rescue JSON::ParserError
false
end
end

# Resolve the original resource being updated by a systemAction.
# - appointment-book: look in context.appointments; if the action targets a ServiceRequest,
# follow the Appointment basedOn reference and resolve that from prefetch or mock EHR data
# - order-sign/order-select: look in context.draftOrders
# - other hooks: resolve from prefetch or mock EHR data because context may carry only ids/references
def find_action_source_resource(action, request)
action_resource = action['resource']
return unless action_resource.is_a?(Hash)

target_type = action_resource['resourceType']
target_id = action_resource['id']
request_body = parse_request_body(request)
return unless target_type.present? && target_id.present? && request_body

hook_context_resource(request_body, target_type, target_id) ||
fallback_source_resource(request_body, target_type, target_id)
end

def find_appointment_book_resource(request_body, target_type, target_id)
appointments_bundle = request_body.dig('context', 'appointments')
find_resource_in_bundle(appointments_bundle, target_type, target_id) ||
appointment_book_service_request(request_body, appointments_bundle, target_id)
end

def find_draft_orders_resource(request_body, target_type, target_id)
draft_orders_bundle = request_body.dig('context', 'draftOrders')
find_resource_in_bundle(draft_orders_bundle, target_type, target_id)
end

def fallback_source_resource(request_body, target_type, target_id)
find_resource_in_prefetch(request_body, target_type, target_id) ||
find_resource_in_bundle(mock_ehr_bundle_resource, target_type, target_id)
end

def hook_context_resource(request_body, target_type, target_id)
case tested_hook_name
when 'appointment-book'
find_appointment_book_resource(request_body, target_type, target_id)
when 'order-sign', 'order-select'
find_draft_orders_resource(request_body, target_type, target_id)
end
end

def appointment_book_service_request(request_body, appointments_bundle_hash, target_id)
target_type = 'ServiceRequest'
appointments_bundle = parse_bundle(appointments_bundle_hash)
appointment = (appointments_bundle&.entry || [])
.filter_map(&:resource)
.find { |candidate| appointment_based_on_matches_target?(candidate, target_type, target_id) }
return unless appointment

find_resource_in_prefetch(request_body, target_type, target_id) ||
find_resource_in_bundle(mock_ehr_bundle_resource, target_type, target_id)
end

def appointment_based_on_matches_target?(appointment, target_type, target_id)
Array(appointment.basedOn).any? do |reference|
reference_parts(reference.reference) == [target_type, target_id]
end
end

def find_resource_in_prefetch(request_body, target_type, target_id)
Array(request_body['prefetch']&.values).each do |prefetched_value|
if prefetched_value.is_a?(Hash) &&
prefetched_value['resourceType'] == target_type &&
prefetched_value['id'] == target_id
return FHIR.from_contents(prefetched_value.to_json)
end

resource = find_resource_in_bundle(prefetched_value, target_type, target_id)
return resource if resource
end

nil
end

def find_resource_by_reference(request_body, reference)
target_type, target_id = reference_parts(reference)
return unless target_type.present? && target_id.present?

find_resource_in_prefetch(request_body, target_type, target_id) ||
find_resource_in_bundle(mock_ehr_bundle_resource, target_type, target_id)
end

def find_resource_in_bundle(bundle_hash, target_type, target_id)
bundle = parse_bundle(bundle_hash)
return unless bundle&.entry

bundle.entry
.filter_map(&:resource)
.find { |resource| resource.resourceType == target_type && resource.id == target_id }
end

def reference_parts(reference)
return if reference.blank?

parts = reference.split('/')
return unless parts.length >= 2

[parts[-2], parts[-1]]
end

def parse_bundle(bundle_hash)
bundle = FHIR.from_contents(bundle_hash.to_json)
bundle if bundle.is_a?(FHIR::Bundle)
rescue StandardError
nil
end

def parse_request_body(request)
return unless request&.request_body.present?

JSON.parse(request.request_body)
rescue JSON::ParserError
nil
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,58 +1,35 @@
require_relative 'all_responses_include_coverage_information_test'
require_relative 'hook_request_resource_resolution'

module DaVinciCRDTestKit
module V221
class OrderDispatchCoverageInformationTest < AllResponsesIncludeCoverageInformationTest
include HookRequestResourceResolution

id :crd_v221_order_dispatch_coverage_information

input :invoked_hook

input :mock_ehr_bundle

def resource_matches_reference?(reference, resource)
reference.resource_type == resource.resourceType && reference.reference_id == resource.id
end
def order_resources(hook_call_body)
references = hook_call_body.dig('context', 'dispatchedOrders')
resources = []
unmatched_references = []

def mock_ehr_bundle_resources
FHIR.from_contents(mock_ehr_bundle)
.entry
.map(&:resource)
end
Array(references).each do |reference|
resource = find_resource_by_reference(hook_call_body, reference)

def prefetch_resources(hook_call_body)
(hook_call_body['prefetch'] || {})
.values
.compact
.map { |resource_hash| FHIR.from_contents(resource_hash.to_json) }
.flat_map do |resource|
if resource.is_a? FHIR::Bundle
resource.entry.map(&:resource)
if resource.present?
resources << resource
else
resource
unmatched_references << reference
end
end
end

def order_resources(hook_call_body)
references =
hook_call_body
.dig('context', 'dispatchedOrders')
.map { |reference| FHIR::Reference.new(reference:) }

resources =
(mock_ehr_bundle_resources + prefetch_resources(hook_call_body))
.select do |resource|
references.any? { |reference| resource_matches_reference?(reference, resource) }
end

unmatched_references =
references.reject do |reference|
resources.any? { |resource| resource_matches_reference?(reference, resource) }
end

skip_if unmatched_references.present?,
'The following `dispatchedOrders` are not included in the Mock EHR Data input or prefetch: ' \
"#{unmatched_references.map(&:reference).join(', ')}"
"#{unmatched_references.join(', ')}"

resources
end
Expand Down
Loading