Module: DaVinciPASTestKit::PasBundleValidation
- Includes:
- ParametersHelper, ValidationTest
- Included in:
- DaVinciPASV201::PASClientPendedSubmitTest, DaVinciPASV201::PasClientInquireRequestBundleValidationTest, DaVinciPASV201::PasClientInquireResponseBundleValidationTest, DaVinciPASV201::PasClientRequestBundleValidationTest, DaVinciPASV201::PasClientResponseBundleValidationTest, DaVinciPASV221::PASClientClaimUpdateReferencedResourcesTest, DaVinciPASV221::PASClientPendedSubmitTest, DaVinciPASV221::PASClientProcessingErrorResponseValidationTest, DaVinciPASV221::PasClientInquireRequestBundleValidationTest, DaVinciPASV221::PasClientInquireResponseBundleValidationTest, DaVinciPASV221::PasClientRequestBundleValidationTest, DaVinciPASV221::PasClientResponseBundleValidationTest, ServerRequestBundleValidationTest, ServerResponseBundleValidationTest
- Defined in:
- lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb
Constant Summary collapse
- US_CORE_VERSION =
'6.1.0'- US_CORE_PROFILE_BASE =
'http://hl7.org/fhir/us/core/StructureDefinition'- BASE_R4_PROFILE =
:base_r4- CLAIM_ENCOUNTER_EXTENSION_URL =
'http://hl7.org/fhir/5.0/StructureDefinition/extension-Claim.encounter'- LOINC_SYSTEM =
'http://loinc.org'- TERMINOLOGY_CONDITION_CATEGORY_SYSTEM =
'http://terminology.hl7.org/CodeSystem/condition-category'- OBSERVATION_CATEGORY_SYSTEM =
'http://terminology.hl7.org/CodeSystem/observation-category'- DIAGNOSTIC_REPORT_CATEGORY_SYSTEM =
'http://terminology.hl7.org/CodeSystem/v2-0074'- US_CORE_SINGLE_PROFILE_IDS_BY_RESOURCE =
{ 'AllergyIntolerance' => 'us-core-allergyintolerance', 'CarePlan' => 'us-core-careplan', 'CareTeam' => 'us-core-careteam', 'Coverage' => 'us-core-coverage', 'Device' => 'us-core-implantable-device', 'DocumentReference' => 'us-core-documentreference', 'Encounter' => 'us-core-encounter', 'Goal' => 'us-core-goal', 'Immunization' => 'us-core-immunization', 'Location' => 'us-core-location', 'Medication' => 'us-core-medication', 'MedicationDispense' => 'us-core-medicationdispense', 'MedicationRequest' => 'us-core-medicationrequest', 'Organization' => 'us-core-organization', 'Patient' => 'us-core-patient', 'Practitioner' => 'us-core-practitioner', 'PractitionerRole' => 'us-core-practitionerrole', 'Procedure' => 'us-core-procedure', 'Provenance' => 'us-core-provenance', 'QuestionnaireResponse' => 'us-core-questionnaireresponse', 'RelatedPerson' => 'us-core-relatedperson', 'ServiceRequest' => 'us-core-servicerequest', 'Specimen' => 'us-core-specimen' }.freeze
- US_CORE_CONDITION_ENCOUNTER_DIAGNOSIS_PROFILE_ID =
'us-core-condition-encounter-diagnosis'- US_CORE_CONDITION_PROBLEMS_HEALTH_CONCERNS_PROFILE_ID =
'us-core-condition-problems-health-concerns'- US_CORE_DIAGNOSTIC_REPORT_LAB_PROFILE_ID =
'us-core-diagnosticreport-lab'- US_CORE_DIAGNOSTIC_REPORT_NOTE_PROFILE_ID =
'us-core-diagnosticreport-note'- US_CORE_OBSERVATION_CLINICAL_RESULT_PROFILE_ID =
'us-core-observation-clinical-result'- US_CORE_OBSERVATION_LAB_PROFILE_ID =
'us-core-observation-lab'- US_CORE_OBSERVATION_SCREENING_ASSESSMENT_PROFILE_ID =
'us-core-observation-screening-assessment'- US_CORE_SIMPLE_OBSERVATION_PROFILE_ID =
'us-core-simple-observation'- US_CORE_SMOKING_STATUS_PROFILE_ID =
'us-core-smokingstatus'- US_CORE_VITAL_SIGNS_PROFILE_ID =
'us-core-vital-signs'- US_CORE_OBSERVATION_CODE_PROFILE_IDS =
{ '11341-5' => 'us-core-observation-occupation', '86645-9' => 'us-core-observation-pregnancyintent', '82810-3' => 'us-core-observation-pregnancystatus', '76690-7' => 'us-core-observation-sexual-orientation', '8289-1' => 'head-occipital-frontal-circumference-percentile', '59576-9' => 'pediatric-bmi-for-age', '77606-2' => 'pediatric-weight-for-height', '85354-9' => 'us-core-blood-pressure', '39156-5' => 'us-core-bmi', '8302-2' => 'us-core-body-height', '8310-5' => 'us-core-body-temperature', '29463-7' => 'us-core-body-weight', '9843-4' => 'us-core-head-circumference', '8867-4' => 'us-core-heart-rate', '59408-5' => 'us-core-pulse-oximetry', '2708-6' => 'us-core-pulse-oximetry', '9279-1' => 'us-core-respiratory-rate', '72166-2' => 'us-core-smokingstatus' }.freeze
Instance Method Summary collapse
- #absolute_url(reference, base_url) ⇒ Object
-
#add_declared_profiles(instance, bundle_entry, version) ⇒ Object
Adds declared profiles from an instance (meta.profile) to the resource target profile map.
-
#add_profile_to_instance(instance, profile_url, bundle_entry, version) ⇒ Object
Adds a specific profile URL to an instance in the resource target profile map.
-
#add_resource_target_profile_to_map(resource_full_url, resource, profile_url = nil) ⇒ Object
Adds a resource and its associated profile URL to the resource target profile map.
- #add_us_core_profiles_to_unprofiled_entries(bundle_entry, version) ⇒ Object
- #bundle_entry_map(bundle_entry) ⇒ Object
-
#bundle_profile_valid?(bundle, profile_url) ⇒ Boolean
Validates a Bundle resource against a profile, filtering out entry-resource-level issues that are covered by individual resource validation to avoid duplicate messages.
-
#bundle_resources_target_profile_map ⇒ Hash
Returns a hash map where the keys are resource full URLs and the values are a hash containing the resource object and an array of associated profile URLs.
- #category_code_present?(resource, code, system:) ⇒ Boolean
-
#check_presence_of_referenced_resources(target_resource, base_url, resources_to_match, skip_claim_related: false) ⇒ Object
This method traverses references within a FHIR resource, ensuring that referenced resources are populated in the bundle.
-
#claim_related_attr?(resource, attr) ⇒ Boolean
Claim.related.claim points to the Claim being updated.
-
#claim_response_request_attr?(resource, attr) ⇒ Boolean
ClaimResponse.request is a back-reference to the submitted Claim.
- #codeable_concept_has_code?(codeable_concepts, code, system:) ⇒ Boolean
-
#determine_claim_submit_profile_url(version, claim) ⇒ String
Determines the target profile URL for a Claim resource in a submit request bundle.
-
#extract_base_url(absolute_url) ⇒ String
Extracts the base URL from an absolute URL by removing the resource type and ID.
-
#extract_profiles_to_validate_each_entry(bundle_entry, current_entry, current_entry_profile_url, version) ⇒ Object
Processes each entry in a FHIR bundle to extract resource and possible profiles to validate against.
-
#extract_resources_from_bundle(bundle: nil, response: nil, reply_handler: nil, max_pages: 20) ⇒ Object
Extracts resources from a bundle while following "next" links.
-
#find_profile_url(request_type) ⇒ Object
Resource Types to validate in request/ response bundle.
-
#find_referenced_instance_in_bundle(reference, enclosing_entry_fullurl, bundle_map) ⇒ Object
Finds a referenced instance in a FHIR bundle based on a reference and the full URL of the enclosing entry.
- #generate_non_conformance_message(item) ⇒ Object
-
#handle_claim_profile(reference_elements, current_entry_profile_url) ⇒ Object
Handles the special case for the Claim profile in a FHIR bundle.
-
#metadata_map(version) ⇒ Object
Mapping profile url to metadata.
- #perform_bundle_validation(bundle, operation, type, ig_version) ⇒ Object
- #perform_request_validation(bundle, profile_url, version, request_type) ⇒ Object
- #perform_response_validation(response_bundle, profile_url, version, request_type, request_bundle = nil) ⇒ Object
-
#process_instance_profiles(instance, bundle_entry, reference_element, version) ⇒ Object
Processes the profiles associated with a given instance in a FHIR bundle.
-
#process_reference_element(reference_element, current_entry, bundle_entry, bundle_map, version) ⇒ Object
Processes a given reference element definition from a FHIR bundle entry.
- #profile_url_for_validation(url, base_profile, version) ⇒ Object
- #profile_url_without_version(url) ⇒ Object
-
#reject_entry_resource_issues(issues) ⇒ Array<ValidatorIssue>
Rejects validator issues that are already filtered or are located at/below Bundle.entry.resource, since those are validated individually per resource.
- #reset_bundle_profile_inference_state ⇒ Object
- #resource_full_url_for_entry(entry) ⇒ Object
-
#resource_present_in_pa_request_and_response_msg(resource) ⇒ Object
Generates a message for a resource present in both the PA request and response bundles.
-
#resource_shall_appear_once_message(absolute_ref, total_matches) ⇒ Object
Generates a message for a resource that appears more than once in a bundle.
- #us_core_condition_profile_ids(resource) ⇒ Object
- #us_core_diagnostic_report_profile_ids(resource) ⇒ Object
- #us_core_observation_profile_ids(resource) ⇒ Object
- #us_core_profile_fallback_enabled?(version) ⇒ Boolean
- #us_core_profile_url?(url) ⇒ Boolean
- #us_core_profile_urls(profile_ids) ⇒ Object
- #us_core_profile_urls_for_resource(resource) ⇒ Object
-
#valid_url_or_urn_uuid?(string) ⇒ Boolean
Checks if a string is a valid url or in the form “urn:uuid:[some guid]”.
-
#validate_bundle_entries_against_profiles(version) ⇒ Object
Validates bundle resource and each entry in the bundle against its target profiles.
- #validate_bundle_entries_full_url(bundle) ⇒ Object
-
#validate_pa_request_payload_structure(bundle, request_type) ⇒ Object
Validates the structure of a Prior Authorization (PA) request Bundle.
-
#validate_pa_response_body_structure(pa_response_bundle, _pa_request_bundle) ⇒ Object
Validates the response body structure of a Prior Authorization (PA) response.
- #validate_pas_bundle_json(json, profile_url, version, request_type, bundle_type, skips: false, message: '') ⇒ Object
-
#validate_resources_conformance_against_profile(bundle, profile_url, version, request_type) ⇒ Object
Profile conformance of Prior Authorization (PA) resources.
-
#validate_uniqueness_of_supporting_info_sequences(claim) ⇒ Object
Checks the following requirement: The Claim.supportingInfo.sequence for each entry SHALL be unique within the Claim.
- #validation_error_messages ⇒ Object
Methods included from ParametersHelper
#extract_bundles_from_pas_inquiry_response_parameters
Methods included from ValidationTest
Instance Method Details
#absolute_url(reference, base_url) ⇒ Object
655 656 657 658 659 660 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 655 def absolute_url(reference, base_url) return if reference.blank? return reference if base_url.blank? || reference.starts_with?('urn:uuid:') || URI(reference).absolute? "#{base_url}/#{reference}" end |
#add_declared_profiles(instance, bundle_entry, version) ⇒ Object
Adds declared profiles from an instance (meta.profile) to the resource target profile map. It recursively processes each entry for further profile extraction.
602 603 604 605 606 607 608 609 610 611 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 602 def add_declared_profiles(instance, bundle_entry, version) return unless instance.resource.present? instance.resource.&.profile&.each do |url| next if bundle_resources_target_profile_map[instance.fullUrl][:profile_urls].include?(url) bundle_resources_target_profile_map[instance.fullUrl][:profile_urls] << url extract_profiles_to_validate_each_entry(bundle_entry, instance, url, version) end end |
#add_profile_to_instance(instance, profile_url, bundle_entry, version) ⇒ Object
Adds a specific profile URL to an instance in the resource target profile map. It recursively processes the instance for further profile extraction.
619 620 621 622 623 624 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 619 def add_profile_to_instance(instance, profile_url, bundle_entry, version) return if bundle_resources_target_profile_map[instance.fullUrl][:profile_urls].include?(profile_url) bundle_resources_target_profile_map[instance.fullUrl][:profile_urls] << profile_url extract_profiles_to_validate_each_entry(bundle_entry, instance, profile_url, version) end |
#add_resource_target_profile_to_map(resource_full_url, resource, profile_url = nil) ⇒ Object
Adds a resource and its associated profile URL to the resource target profile map. If the resource is already in the map, it adds the profile URL to the resource's list of profile URLs.
293 294 295 296 297 298 299 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 293 def add_resource_target_profile_to_map(resource_full_url, resource, profile_url = nil) entry = bundle_resources_target_profile_map[resource_full_url] ||= { resource:, profile_urls: [] } return if profile_url.blank? || entry[:profile_urls].include?(profile_url) entry[:profile_urls] << profile_url end |
#add_us_core_profiles_to_unprofiled_entries(bundle_entry, version) ⇒ Object
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 339 def add_us_core_profiles_to_unprofiled_entries(bundle_entry, version) return unless us_core_profile_fallback_enabled?(version) bundle_entry.each do |entry| resource = entry.resource next if resource.blank? resource_full_url = resource_full_url_for_entry(entry) next if bundle_resources_target_profile_map[resource_full_url]&.dig(:profile_urls).present? profile_urls = us_core_profile_urls_for_resource(resource) profile_urls = [BASE_R4_PROFILE] if profile_urls.blank? profile_urls.each do |profile_url| add_resource_target_profile_to_map(resource_full_url, resource, profile_url) end end end |
#bundle_entry_map(bundle_entry) ⇒ Object
637 638 639 640 641 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 637 def bundle_entry_map(bundle_entry) @bundle_entry_map ||= bundle_entry.to_h do |entry| [entry.fullUrl, entry] end end |
#bundle_profile_valid?(bundle, profile_url) ⇒ Boolean
Validates a Bundle resource against a profile, filtering out entry-resource-level issues that are covered by individual resource validation to avoid duplicate messages.
454 455 456 457 458 459 460 461 462 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 454 def bundle_profile_valid?(bundle, profile_url) response_details = [] resource_is_valid?(resource: bundle, profile_url:, add_messages_to_runnable: false, validator_response_details: response_details) bundle_level_issues = reject_entry_resource_issues(response_details) bundle_level_issues.each { |issue| << { type: issue.severity, message: issue. } } bundle_level_issues.none? { |issue| issue.severity == 'error' } end |
#bundle_resources_target_profile_map ⇒ Hash
Returns a hash map where the keys are resource full URLs and the values are a hash containing the resource object and an array of associated profile URLs.
284 285 286 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 284 def bundle_resources_target_profile_map @bundle_resources_target_profile_map ||= {} end |
#category_code_present?(resource, code, system:) ⇒ Boolean
436 437 438 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 436 def category_code_present?(resource, code, system:) codeable_concept_has_code?(resource.category, code, system:) end |
#check_presence_of_referenced_resources(target_resource, base_url, resources_to_match, skip_claim_related: false) ⇒ Object
This method traverses references within a FHIR resource, ensuring that referenced resources are populated in the bundle. It also enforces that a referenced resource appears only once in the bundle, as required by the PAS IG.
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 763 def check_presence_of_referenced_resources(target_resource, base_url, resources_to_match, skip_claim_related: false) return if target_resource.blank? if target_resource.is_a?(FHIR::Reference) && target_resource.reference.present? ref = target_resource.reference absolute_ref = absolute_url(ref, base_url) matching_resources = resources_to_match.find_all { |res| res.fullUrl == absolute_ref } if matching_resources.length != 1 << (absolute_ref, matching_resources.length) end if matching_resources.length.positive? # A resource reached by following a reference is an included resource, not the primary Claim # being validated; if it is itself a Claim Update, its referenced grandparent Claim is omitted. check_presence_of_referenced_resources(matching_resources.first.resource, base_url, resources_to_match, skip_claim_related: true) end else target_resource.source_hash.each_key do |attr| next if claim_response_request_attr?(target_resource, attr) next if && (target_resource, attr) value = target_resource.send(attr.to_sym) if value.is_a?(FHIR::Model) check_presence_of_referenced_resources(value, base_url, resources_to_match, skip_claim_related:) elsif value.is_a?(Array) && value.all?(FHIR::Model) value.each do |elmt| check_presence_of_referenced_resources(elmt, base_url, resources_to_match, skip_claim_related:) end end end end end |
#claim_related_attr?(resource, attr) ⇒ Boolean
Claim.related.claim points to the Claim being updated. For a non-primary Claim in a Claim Update
chain (one reached by following a reference), that prior Claim is the grandparent, which spec-65/66
require to be omitted from the Bundle. Used with the skip_claim_related flag so the generic
reference-presence check does not flag the deliberately-omitted grandparent as missing.
815 816 817 818 819 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 815 def (resource, attr) attr.to_s == 'related' && resource.respond_to?(:resourceType) && resource.resourceType == 'Claim' end |
#claim_response_request_attr?(resource, attr) ⇒ Boolean
ClaimResponse.request is a back-reference to the submitted Claim. The PAS IG response bundle profile (profile-pas-response-bundle) has no required Claim entry slice, so the Claim need not be present in the response bundle. Skipping here matches the identical guard in ResponseGenerator#referenced_entities, which also skips ClaimResponse.request when building mock response bundles.
805 806 807 808 809 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 805 def claim_response_request_attr?(resource, attr) attr.to_s == 'request' && resource.respond_to?(:resourceType) && resource.resourceType == 'ClaimResponse' end |
#codeable_concept_has_code?(codeable_concepts, code, system:) ⇒ Boolean
440 441 442 443 444 445 446 447 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 440 def codeable_concept_has_code?(codeable_concepts, code, system:) systems = Array(system).compact Array(codeable_concepts).compact.any? do |codeable_concept| Array(codeable_concept&.coding).compact.any? do |coding| coding.code == code && systems.include?(coding.system) end end end |
#determine_claim_submit_profile_url(version, claim) ⇒ String
Determines the target profile URL for a Claim resource in a submit request bundle.
In v2.2.1, profile-pas-request-bundle permits either profile-claim or profile-claim-update. The structural discriminator is Claim.related: profile-claim-update requires it (must-support) while profile-claim disallows it (max: 0). For all other versions, profile-claim-update is used unconditionally.
704 705 706 707 708 709 710 711 712 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 704 def determine_claim_submit_profile_url(version, claim) return PASConstants::CLAIM_PROFILE unless version == '2.2.1' if claim..blank? PASConstants::CLAIM_PROFILE_FIRST_SUBMIT else PASConstants::CLAIM_PROFILE end end |
#extract_base_url(absolute_url) ⇒ String
Extracts the base URL from an absolute URL by removing the resource type and ID.
665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 665 def extract_base_url(absolute_url) return '' if absolute_url.blank? uri = URI(absolute_url) return '' unless uri.scheme && uri.host # Split the path segments and remove the last two segments (resource type and id) path_segments = uri.path.split('/') base_path = path_segments[0...-2].join('/') "#{uri.scheme}://#{uri.host}#{base_path}" end |
#extract_profiles_to_validate_each_entry(bundle_entry, current_entry, current_entry_profile_url, version) ⇒ Object
Processes each entry in a FHIR bundle to extract resource and possible profiles to validate against. It recursively evaluates referenced instances and their profiles, expanding the validation scope.
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 487 def extract_profiles_to_validate_each_entry(bundle_entry, current_entry, current_entry_profile_url, version) return if current_entry.blank? # NOTE: the IG does not have the metadata for us-core profiles. = ("v#{version}")[profile_url_without_version(current_entry_profile_url)] return if .blank? bundle_map = bundle_entry_map(bundle_entry) reference_elements = .references.dup # Special handling for Claim submit profile claim_submit_profile_urls = [ PASConstants::CLAIM_PROFILE, PASConstants::CLAIM_PROFILE_FIRST_SUBMIT ] if claim_submit_profile_urls.include?(current_entry_profile_url) handle_claim_profile(reference_elements, current_entry_profile_url) end reference_elements.each do |reference_element| process_reference_element(reference_element, current_entry, bundle_entry, bundle_map, version) end end |
#extract_resources_from_bundle(bundle: nil, response: nil, reply_handler: nil, max_pages: 20) ⇒ Object
Extracts resources from a bundle while following "next" links.
This method extracts resources from a FHIR bundle, following "next" links in the bundle until the specified maximum number of pages is reached. It collects resources and invokes the reply_handler for each response.
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 831 def extract_resources_from_bundle( bundle: nil, response: nil, reply_handler: nil, max_pages: 20 ) page_count = 1 resources = [] until bundle.nil? || page_count == max_pages resources += bundle&.entry&.map { |entry| entry&.resource } next_bundle_link = bundle&.link&.find { |link| link.relation == 'next' }&.url reply_handler&.call(response) break if next_bundle_link.blank? page_count += 1 end resources end |
#find_profile_url(request_type) ⇒ Object
Resource Types to validate in request/ response bundle
679 680 681 682 683 684 685 686 687 688 689 690 691 692 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 679 def find_profile_url(request_type) { 'Claim' => if request_type.start_with?('submit') PASConstants::CLAIM_PROFILE else PASConstants::CLAIM_INQUIRY_PROFILE end, 'ClaimResponse' => if %w[submit submit_response].include?(request_type) PASConstants::CLAIM_RESPONSE_PROFILE else PASConstants::CLAIM_INQUIRY_RESPONSE_PROFILE end } end |
#find_referenced_instance_in_bundle(reference, enclosing_entry_fullurl, bundle_map) ⇒ Object
Finds a referenced instance in a FHIR bundle based on a reference and the full URL of the enclosing entry.
648 649 650 651 652 653 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 648 def find_referenced_instance_in_bundle(reference, enclosing_entry_fullurl, bundle_map) base_url = extract_base_url(enclosing_entry_fullurl) key = absolute_url(reference, base_url) bundle_map[key] end |
#generate_non_conformance_message(item) ⇒ Object
476 477 478 479 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 476 def (item) "#{item[:resource].resourceType}/#{item[:resource].id} is not conformant to any of the " \ "target profiles: #{item[:profile_urls]}." end |
#handle_claim_profile(reference_elements, current_entry_profile_url) ⇒ Object
Handles the special case for the Claim profile in a FHIR bundle. It adds missing reference elements for the Claim profile. Claim.item.extension:requestedService value is a reference, but somehow not included in the metadata references.
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 517 def handle_claim_profile(reference_elements, current_entry_profile_url) claim_submit_profile_urls = [ PASConstants::CLAIM_PROFILE, PASConstants::CLAIM_PROFILE_FIRST_SUBMIT ] return unless claim_submit_profile_urls.include?(current_entry_profile_url) claim_ref_element = { path: 'Claim.item.extension.value', profiles: [ 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-medicationrequest', 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-servicerequest', 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-devicerequest', 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-nutritionorder' ] } # This temporary traversal handling may be replaced later by shared metadata-driven navigation logic. claim_encounter_ref_element = { path: "Claim.extension.where(url = '#{CLAIM_ENCOUNTER_EXTENSION_URL}').value", profiles: [ 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-encounter' ] } reference_elements << claim_ref_element reference_elements << claim_encounter_ref_element end |
#metadata_map(version) ⇒ Object
Mapping profile url to metadata
627 628 629 630 631 632 633 634 635 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 627 def (version) @metadata ||= {} @metadata[version] ||= YAML.load_file(File.join(__dir__, "generated/#{version}/metadata.yml"), aliases: true) @metadata_map ||= {} @metadata_map[version] ||= @metadata[version][:profiles].to_h do || [[:profile_url], Generator::ProfileMetadata.new()] end end |
#perform_bundle_validation(bundle, operation, type, ig_version) ⇒ Object
83 84 85 86 87 88 89 90 91 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 83 def perform_bundle_validation(bundle, operation, type, ig_version) target_profile = PASConstants.bundle_profile_url_for_operation_and_type(operation, type) request_type = "#{operation}_#{type}" if type == 'request' perform_request_validation(bundle, target_profile, ig_version.delete_prefix('v'), request_type) else perform_response_validation(bundle, target_profile, ig_version.delete_prefix('v'), request_type) end end |
#perform_request_validation(bundle, profile_url, version, request_type) ⇒ Object
93 94 95 96 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 93 def perform_request_validation(bundle, profile_url, version, request_type) validate_pa_request_payload_structure(bundle, request_type) validate_resources_conformance_against_profile(bundle, profile_url, version, request_type) end |
#perform_response_validation(response_bundle, profile_url, version, request_type, request_bundle = nil) ⇒ Object
98 99 100 101 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 98 def perform_response_validation(response_bundle, profile_url, version, request_type, request_bundle = nil) validate_pa_response_body_structure(response_bundle, request_bundle) if request_type == 'submit' validate_resources_conformance_against_profile(response_bundle, profile_url, version, request_type) end |
#process_instance_profiles(instance, bundle_entry, reference_element, version) ⇒ Object
Processes the profiles associated with a given instance in a FHIR bundle. It adds the instance's profiles to the resource target profile map and handles recursive profile extraction. The profiles collected here are possible profiles the given instance may conform to. The conformance validation will ensure that the resource is conformant to at least one of the target profiles.
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 576 def process_instance_profiles(instance, bundle_entry, reference_element, version) add_resource_target_profile_to_map(instance.fullUrl, instance.resource) # Add the declared profile conformance add_declared_profiles(instance, bundle_entry, version) reference_element[:profiles].each do |profile_url| # NOTE: the IG does not have the metadata for us-core profiles. = ("v#{version}")[profile_url_without_version(profile_url)] resource_type = instance.resource.resourceType next unless &.resource == resource_type || profile_url.include?(resource_type) add_profile_to_instance(instance, profile_url, bundle_entry, version) # NOTE: The algorithm assumes OR semantics for profile conformance, where the resource needs to conform to # at least one of the collected profiles. However, it may not cover all scenarios, such as cases # where AND semantics are required for multiple profile conformance. Also, it does not address complex # situations where profile requirements may conflict or have dependencies across referenced instances. # Therefore, this algorithm may not provide complete validation for all scenarios, and additional checks may be # necessary depending on the use case. end end |
#process_reference_element(reference_element, current_entry, bundle_entry, bundle_map, version) ⇒ Object
Processes a given reference element definition from a FHIR bundle entry. It evaluates FHIRPath expressions and processes each referenced instance and its profiles.
553 554 555 556 557 558 559 560 561 562 563 564 565 566 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 553 def process_reference_element(reference_element, current_entry, bundle_entry, bundle_map, version) fhirpath_result = evaluate_fhirpath(resource: current_entry.resource, path: reference_element[:path]) reference_element_values = fhirpath_result.filter_map do |entry| entry['element']&.reference if entry['type'] == 'Reference' end referenced_instances = reference_element_values.filter_map do |value| find_referenced_instance_in_bundle(value, current_entry.fullUrl, bundle_map) end referenced_instances.each do |instance| process_instance_profiles(instance, bundle_entry, reference_element, version) end end |
#profile_url_for_validation(url, base_profile, version) ⇒ Object
326 327 328 329 330 331 332 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 326 def profile_url_for_validation(url, base_profile, version) url = url.to_s return url if url.include?('|') || profile_url_without_version(url) == base_profile return "#{url}|#{US_CORE_VERSION}" if us_core_profile_url?(url) "#{url}|#{version}" end |
#profile_url_without_version(url) ⇒ Object
428 429 430 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 428 def profile_url_without_version(url) url.to_s.split('|', 2).first end |
#reject_entry_resource_issues(issues) ⇒ Array<ValidatorIssue>
Rejects validator issues that are already filtered or are located at/below Bundle.entry.resource, since those are validated individually per resource. Entry resources that receive direct PAS or US Core target profiles are validated individually after this filter.
470 471 472 473 474 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 470 def reject_entry_resource_issues(issues) issues.reject do |issue| issue.filtered || issue.location&.match?(/\ABundle\.entry\[\d+\]\.resource/) end end |
#reset_bundle_profile_inference_state ⇒ Object
334 335 336 337 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 334 def reset_bundle_profile_inference_state bundle_resources_target_profile_map.clear @bundle_entry_map = nil end |
#resource_full_url_for_entry(entry) ⇒ Object
358 359 360 361 362 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 358 def resource_full_url_for_entry(entry) resource = entry.resource entry.fullUrl.presence || "#{resource.resourceType}/#{resource.id}" end |
#resource_present_in_pa_request_and_response_msg(resource) ⇒ Object
Generates a message for a resource present in both the PA request and response bundles.
This method generates an error message when a resource appears in both the PA request and response bundles but does not have the same fullUrl or identifiers.
871 872 873 874 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 871 def resource_present_in_pa_request_and_response_msg(resource) "Resource #{resource.resourceType}/#{resource.id} is an entry in both the PA Request Bundle and the Response " \ 'Bundle, but they do not have the same fullUrl or identifiers' end |
#resource_shall_appear_once_message(absolute_ref, total_matches) ⇒ Object
Generates a message for a resource that appears more than once in a bundle.
This method generates an error message when a referenced resource appears more than once in a FHIR bundle, which is not allowed.
861 862 863 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 861 def (absolute_ref, total_matches) " The referenced #{absolute_ref} resource SHALL appear exactly once in the Bundle, but found #{total_matches}." end |
#us_core_condition_profile_ids(resource) ⇒ Object
384 385 386 387 388 389 390 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 384 def us_core_condition_profile_ids(resource) if category_code_present?(resource, 'encounter-diagnosis', system: TERMINOLOGY_CONDITION_CATEGORY_SYSTEM) [US_CORE_CONDITION_ENCOUNTER_DIAGNOSIS_PROFILE_ID] else [US_CORE_CONDITION_PROBLEMS_HEALTH_CONCERNS_PROFILE_ID] end end |
#us_core_diagnostic_report_profile_ids(resource) ⇒ Object
392 393 394 395 396 397 398 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 392 def us_core_diagnostic_report_profile_ids(resource) if category_code_present?(resource, 'LAB', system: DIAGNOSTIC_REPORT_CATEGORY_SYSTEM) [US_CORE_DIAGNOSTIC_REPORT_LAB_PROFILE_ID] else [US_CORE_DIAGNOSTIC_REPORT_NOTE_PROFILE_ID] end end |
#us_core_observation_profile_ids(resource) ⇒ Object
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 400 def us_core_observation_profile_ids(resource) code_profile_id = US_CORE_OBSERVATION_CODE_PROFILE_IDS.find do |code, _profile_id| codeable_concept_has_code?(resource.code, code, system: LOINC_SYSTEM) end&.last return [code_profile_id] if code_profile_id.present? profile_ids = [] profile_ids << US_CORE_SMOKING_STATUS_PROFILE_ID if category_code_present?(resource, 'social-history', system: OBSERVATION_CATEGORY_SYSTEM) profile_ids << US_CORE_VITAL_SIGNS_PROFILE_ID if category_code_present?(resource, 'vital-signs', system: OBSERVATION_CATEGORY_SYSTEM) if category_code_present?(resource, 'survey', system: OBSERVATION_CATEGORY_SYSTEM) profile_ids << US_CORE_OBSERVATION_SCREENING_ASSESSMENT_PROFILE_ID end # Keep candidate profiles most-specific first because validation stops at the first conformant profile. profile_ids << if category_code_present?(resource, 'laboratory', system: OBSERVATION_CATEGORY_SYSTEM) US_CORE_OBSERVATION_LAB_PROFILE_ID else US_CORE_OBSERVATION_CLINICAL_RESULT_PROFILE_ID end profile_ids << US_CORE_SIMPLE_OBSERVATION_PROFILE_ID profile_ids.uniq end |
#us_core_profile_fallback_enabled?(version) ⇒ Boolean
364 365 366 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 364 def us_core_profile_fallback_enabled?(version) version.to_s.delete_prefix('v').match?(/\A2\.2(\.|\z)/) end |
#us_core_profile_url?(url) ⇒ Boolean
432 433 434 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 432 def us_core_profile_url?(url) profile_url_without_version(url).start_with?("#{US_CORE_PROFILE_BASE}/") end |
#us_core_profile_urls(profile_ids) ⇒ Object
424 425 426 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 424 def us_core_profile_urls(profile_ids) Array(profile_ids).map { |profile_id| "#{US_CORE_PROFILE_BASE}/#{profile_id}|#{US_CORE_VERSION}" } end |
#us_core_profile_urls_for_resource(resource) ⇒ Object
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 368 def us_core_profile_urls_for_resource(resource) profile_ids = case resource.resourceType when 'Condition' us_core_condition_profile_ids(resource) when 'DiagnosticReport' us_core_diagnostic_report_profile_ids(resource) when 'Observation' us_core_observation_profile_ids(resource) else Array(US_CORE_SINGLE_PROFILE_IDS_BY_RESOURCE[resource.resourceType]) end us_core_profile_urls(profile_ids) end |
#valid_url_or_urn_uuid?(string) ⇒ Boolean
Checks if a string is a valid url or in the form “urn:uuid:[some guid]”
745 746 747 748 749 750 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 745 def valid_url_or_urn_uuid?(string) url_regex = /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ urn_uuid_regex = /\Aurn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i string&.match?(url_regex) || string&.match?(urn_uuid_regex) end |
#validate_bundle_entries_against_profiles(version) ⇒ Object
Validates bundle resource and each entry in the bundle against its target profiles. It logs a message for each conformant entry and collects error messages for non-conformant entries. Asserts that there are no validation errors.
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 305 def validate_bundle_entries_against_profiles(version) bundle_resources_target_profile_map.each do |key, item| resource = item[:resource] base_profile = FHIR::Definitions.resource_definition(resource.resourceType).url success_profile = item[:profile_urls].find do |url| if key == 'bundle' profile_to_validate = profile_url_for_validation(url, base_profile, version) bundle_profile_valid?(resource, profile_to_validate) elsif url == BASE_R4_PROFILE resource_is_valid?(resource:) else profile_to_validate = profile_url_for_validation(url, base_profile, version) resource_is_valid?(resource:, profile_url: profile_to_validate) end end << (item) unless success_profile end end |
#validate_bundle_entries_full_url(bundle) ⇒ Object
734 735 736 737 738 739 740 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 734 def validate_bundle_entries_full_url(bundle) msg = "[Bundle/#{bundle.id}]: Bundle.entry.fullUrl values SHALL be a valid url or in the form " \ "'urn:uuid:[some guid]'." bundle.entry.each do |entry| << msg unless valid_url_or_urn_uuid?(entry.fullUrl) end end |
#validate_pa_request_payload_structure(bundle, request_type) ⇒ Object
Validates the structure of a Prior Authorization (PA) request Bundle.
This method performs various checks on the PA request payload, including validating the FHIR bundle structure, checking the resource type, and validating the resources referenced in the Claim resource are included in the bundle. It ensures that the first entry in the Bundle is a Claim resource and additional entries are populated with referenced resources, following the traversal of references. Duplicate resources are handled as required (appearing only once in the bundle entry).
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 162 def validate_pa_request_payload_structure(bundle, request_type) bundle_entry_resources = bundle.entry.map(&:resource) first_entry = bundle_entry_resources.first base_url = extract_base_url(bundle.entry.first&.fullUrl) check_presence_of_referenced_resources(first_entry, base_url, bundle.entry) if request_type == 'submit' unless first_entry.is_a?(FHIR::Claim) << "[Bundle/#{bundle.id}]: The first bundle entry must be a Claim" end validate_uniqueness_of_supporting_info_sequences(first_entry) validate_bundle_entries_full_url(bundle) else claim_resource = bundle_entry_resources.find { |resource| resource.resourceType == 'Claim' } if claim_resource.blank? << "[Bundle/#{bundle.id}]: Claim must be present for inquiry request" end # The inquiry operation must contain a requesting provider organization, # a payer organization, and a patient for the inquiry patient_reference = claim_resource&.patient&.reference provider_reference = claim_resource&.provider&.reference payer_reference = claim_resource&.insurer&.reference if patient_reference.blank? << "[Bundle/#{bundle.id}]: The Claim for inquiry operation must reference a patient." end if provider_reference.blank? << "[Bundle/#{bundle.id}]: The claim for inquiry operation must reference " \ 'a requesting provider organization.' end if payer_reference.blank? << "[Bundle/#{bundle.id}]: The Claim for inquiry operation must contain " \ 'a payer organization.' end end end |
#validate_pa_response_body_structure(pa_response_bundle, _pa_request_bundle) ⇒ Object
Validates the response body structure of a Prior Authorization (PA) response.
This method performs validation of the PA response bundle structure. It follows the PAS IG requirement that the FHIR Bundle generated from the response starts with a ClaimResponse entry. For response of $submit request: Additional Bundle entries are populated with resources referenced by the ClaimResponse or descendant references, ensuring that only one resource is created for a given combination of content. Resources echoed back from the request are validated to ensure the same fullUrl and resource identifiers as in the request are used.
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 217 def validate_pa_response_body_structure(pa_response_bundle, _pa_request_bundle) first_entry = pa_response_bundle.entry.first.resource unless first_entry.is_a?(FHIR::ClaimResponse) << "[Bundle/#{pa_response_bundle.id}]: The first bundle entry must be a ClaimResponse" end base_url = extract_base_url(pa_response_bundle.entry.last&.fullUrl) check_presence_of_referenced_resources(first_entry, base_url, pa_response_bundle.entry) # Testing: When echoing back resources that are the same as were present in the prior authorization request, # the system SHALL ensure that the same fullUrl and resource identifiers are used in the response as appeared # in the request # pa_request_bundle = FHIR.from_contents(pa_request_bundle) # pa_response_bundle.entry.each do |entry| # res = entry.resource # request_entry = pa_request_bundle.entry.find do |ent| # ent.resource.resourceType == res.resourceType && ent.resource.id == res.id # end # next unless request_entry.present? # assert( # request_entry.fullUrl == entry.fullUrl && request_entry.resource.identifier == res.identifier, # resource_present_in_pa_request_and_response_msg(res) # ) # end end |
#validate_pas_bundle_json(json, profile_url, version, request_type, bundle_type, skips: false, message: '') ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 103 def validate_pas_bundle_json(json, profile_url, version, request_type, bundle_type, skips: false, message: '') assert_valid_json(json) resource = FHIR.from_contents(json) assert resource.present?, 'Not a FHIR resource' # For v2.2.1 inquire responses, expect Parameters resource if version == '2.2.1' && request_type == 'inquire' && bundle_type == 'response_bundle' if resource.resourceType == 'Parameters' # Extract and validate each Bundle in the Parameters bundles = extract_bundles_from_pas_inquiry_response_parameters(resource) bundles.each do |bundle| perform_response_validation(bundle, profile_url, version, request_type) end elsif resource.is_a?(FHIR::Bundle) # Bundle received instead of Parameters - validate it but log an error << 'Expected Parameters resource for v2.2.1 inquire response, but received ' \ 'Bundle. The response Bundle should be wrapped in a ' \ 'Parameters resource with a return parameter.' perform_response_validation(resource, profile_url, version, request_type) else assert false, "Expected Parameters resource for v2.2.1 inquire response, but received #{resource.resourceType}" end else # For v2.0.1 or non-inquire operations, expect Bundle resource assert_resource_type(:bundle, resource: resource) bundle = resource if bundle_type == 'request_bundle' perform_request_validation(bundle, profile_url, version, request_type) else perform_response_validation(bundle, profile_url, version, request_type) end end .each do |msg| << { type: 'error', message: msg } end msg = 'Bundle and/or entry resources are not conformant. Check messages for issues found.' assert .blank?, msg rescue Inferno::Exceptions::AssertionException => e msg = "#{} #{e.}".strip raise e.class, msg unless skips skip msg end |
#validate_resources_conformance_against_profile(bundle, profile_url, version, request_type) ⇒ Object
Profile conformance of Prior Authorization (PA) resources.
This method performs conformance validation on the PA bundle and bundle entries. The request/response bundle and includes resources are validated against their respective profile.
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 255 def validate_resources_conformance_against_profile(bundle, profile_url, version, request_type) reset_bundle_profile_inference_state add_resource_target_profile_to_map('bundle', bundle, profile_url) bundle_entry = bundle.entry root_entry = bundle_entry.find do |entry| ['Claim', 'ClaimResponse'].include?(entry.resource.resourceType) end if root_entry.present? root_resource_profile_url = if %w[submit submit_request].include?(request_type) && root_entry.resource.resourceType == 'Claim' determine_claim_submit_profile_url(version, root_entry.resource) else find_profile_url(request_type)[root_entry.resource.resourceType] end add_resource_target_profile_to_map(root_entry.fullUrl, root_entry.resource, root_resource_profile_url) extract_profiles_to_validate_each_entry(bundle_entry, root_entry, root_resource_profile_url, version) end add_us_core_profiles_to_unprofiled_entries(bundle_entry, version) validate_bundle_entries_against_profiles(version) end |
#validate_uniqueness_of_supporting_info_sequences(claim) ⇒ Object
Checks the following requirement: The Claim.supportingInfo.sequence for each entry SHALL be unique within the Claim.
Since the cardinality for Claim.supportingInfo is 0..*, we will check the uniqueness if Array not empty.
720 721 722 723 724 725 726 727 728 729 730 731 732 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 720 def validate_uniqueness_of_supporting_info_sequences(claim) return unless claim.is_a?(FHIR::Claim) supporting_info = claim.supportingInfo return unless supporting_info.present? sequences = supporting_info.map(&:sequence) is_unique = sequences.uniq.length == sequences.length return if is_unique << "[Claim/#{claim.id}]: The sequence element for each supportingInfo entry SHALL be " \ 'unique within the Claim.' end |
#validation_error_messages ⇒ Object
79 80 81 |
# File 'lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb', line 79 def @validation_error_messages ||= [] end |