davinci_pas_test_kit 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. checksums.yaml +4 -4
  2. data/config/presets/g33_br_provider.json +13 -0
  3. data/lib/davinci_pas_test_kit/client/abstract_approval_submit_test.rb +28 -17
  4. data/lib/davinci_pas_test_kit/client/abstract_denial_submit_test.rb +26 -16
  5. data/lib/davinci_pas_test_kit/client/abstract_gather_must_support_test.rb +43 -26
  6. data/lib/davinci_pas_test_kit/client/abstract_modification_submit_test.rb +29 -19
  7. data/lib/davinci_pas_test_kit/client/abstract_response_attest.rb +75 -6
  8. data/lib/davinci_pas_test_kit/client/abstract_subscription_create_test.rb +26 -9
  9. data/lib/davinci_pas_test_kit/client/abstract_subscription_update_delete_test.rb +91 -0
  10. data/lib/davinci_pas_test_kit/client/client_bundle_validation_helper.rb +245 -0
  11. data/lib/davinci_pas_test_kit/client/client_input_descriptions.rb +91 -25
  12. data/lib/davinci_pas_test_kit/client/endpoints/claim_endpoint.rb +108 -68
  13. data/lib/davinci_pas_test_kit/client/endpoints/endpoint_session_identification.rb +24 -0
  14. data/lib/davinci_pas_test_kit/client/endpoints/expired_token.rb +40 -0
  15. data/lib/davinci_pas_test_kit/client/endpoints/subscription_create_endpoint.rb +7 -12
  16. data/lib/davinci_pas_test_kit/client/endpoints/subscription_delete_endpoint.rb +13 -0
  17. data/lib/davinci_pas_test_kit/client/endpoints/subscription_read_endpoint.rb +13 -0
  18. data/lib/davinci_pas_test_kit/client/endpoints/subscription_status_endpoint.rb +15 -20
  19. data/lib/davinci_pas_test_kit/client/endpoints/subscription_update_endpoint.rb +13 -0
  20. data/lib/davinci_pas_test_kit/client/endpoints/token_endpoint.rb +16 -0
  21. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/beneficiary/client_inquire_request_must_support_beneficiary_test.rb +3 -3
  22. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/beneficiary/client_inquire_response_must_support_beneficiary_test.rb +3 -3
  23. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/beneficiary/client_submit_request_must_support_beneficiary_test.rb +3 -3
  24. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/beneficiary/client_submit_response_must_support_beneficiary_test.rb +3 -3
  25. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/claim_inquiry/client_inquire_request_must_support_claim_inquiry_test.rb +3 -3
  26. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/claim_update/client_submit_request_must_support_claim_update_test.rb +3 -3
  27. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/claiminquiryresponse/client_inquire_response_must_support_claiminquiryresponse_test.rb +3 -3
  28. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/claimresponse/client_submit_response_must_support_claimresponse_test.rb +3 -3
  29. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/communication_request/client_submit_response_must_support_communication_request_test.rb +3 -3
  30. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/coverage/client_inquire_request_must_support_coverage_test.rb +3 -3
  31. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/coverage/client_submit_request_must_support_coverage_test.rb +3 -3
  32. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/encounter/client_submit_request_must_support_encounter_test.rb +3 -3
  33. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/insurer/client_inquire_request_must_support_insurer_test.rb +3 -3
  34. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/insurer/client_inquire_response_must_support_insurer_test.rb +3 -3
  35. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/insurer/client_submit_request_must_support_insurer_test.rb +3 -3
  36. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/insurer/client_submit_response_must_support_insurer_test.rb +3 -3
  37. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_client_inquire_must_support_group.rb +1 -1
  38. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_client_inquire_response_must_support_group.rb +9 -31
  39. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_client_submit_must_support_group.rb +1 -1
  40. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_client_submit_response_must_support_group.rb +10 -32
  41. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_inquiry_request_bundle/client_inquire_request_must_support_pas_inquiry_request_bundle_test.rb +3 -3
  42. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_inquiry_response_bundle/client_inquire_response_must_support_pas_inquiry_response_bundle_test.rb +3 -3
  43. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_request_bundle/client_submit_request_must_support_pas_request_bundle_test.rb +3 -3
  44. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/pas_response_bundle/client_submit_response_must_support_pas_response_bundle_test.rb +3 -3
  45. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner/client_inquire_request_must_support_practitioner_test.rb +3 -3
  46. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner/client_inquire_response_must_support_practitioner_test.rb +3 -3
  47. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner/client_submit_request_must_support_practitioner_test.rb +3 -3
  48. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner/client_submit_response_must_support_practitioner_test.rb +3 -3
  49. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner_role/client_inquire_request_must_support_practitioner_role_test.rb +3 -3
  50. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner_role/client_inquire_response_must_support_practitioner_role_test.rb +3 -3
  51. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner_role/client_submit_request_must_support_practitioner_role_test.rb +3 -3
  52. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/practitioner_role/client_submit_response_must_support_practitioner_role_test.rb +3 -3
  53. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/requestor/client_inquire_request_must_support_requestor_test.rb +3 -3
  54. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/requestor/client_inquire_response_must_support_requestor_test.rb +3 -3
  55. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/requestor/client_submit_request_must_support_requestor_test.rb +3 -3
  56. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/requestor/client_submit_response_must_support_requestor_test.rb +3 -3
  57. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/subscriber/client_inquire_request_must_support_subscriber_test.rb +3 -3
  58. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/subscriber/client_submit_request_must_support_subscriber_test.rb +3 -3
  59. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/beneficiary/client_inquire_response_must_support_beneficiary_test.rb +4 -3
  60. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/beneficiary/client_submit_response_must_support_beneficiary_test.rb +4 -3
  61. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/claim_inquiry/client_inquire_request_must_support_claim_inquiry_test.rb +3 -3
  62. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/claim_update/client_submit_request_must_support_claim_update_test.rb +3 -3
  63. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/claiminquiryresponse/client_inquire_response_must_support_claiminquiryresponse_test.rb +4 -3
  64. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/claimresponse/client_submit_response_must_support_claimresponse_test.rb +4 -3
  65. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/communication_request/client_submit_response_must_support_communication_request_test.rb +4 -3
  66. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/insurer/client_inquire_response_must_support_insurer_test.rb +4 -3
  67. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/insurer/client_submit_response_must_support_insurer_test.rb +4 -3
  68. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/pas_client_inquire_must_support_group.rb +8 -6
  69. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/pas_client_inquire_response_must_support_group.rb +9 -31
  70. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/pas_client_submit_must_support_group.rb +13 -9
  71. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/pas_client_submit_response_must_support_group.rb +10 -32
  72. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/pas_inquiry_response_bundle/client_inquire_response_must_support_pas_inquiry_response_bundle_test.rb +4 -3
  73. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/pas_response_bundle/client_submit_response_must_support_pas_response_bundle_test.rb +4 -3
  74. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/practitioner/client_inquire_response_must_support_practitioner_test.rb +4 -3
  75. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/practitioner/client_submit_response_must_support_practitioner_test.rb +4 -3
  76. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/practitioner_role/client_inquire_response_must_support_practitioner_role_test.rb +4 -3
  77. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/practitioner_role/client_submit_response_must_support_practitioner_role_test.rb +4 -3
  78. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/requestor/client_inquire_response_must_support_requestor_test.rb +4 -3
  79. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/requestor/client_submit_response_must_support_requestor_test.rb +4 -3
  80. data/lib/davinci_pas_test_kit/client/jobs/send_pas_subscription_notification.rb +64 -3
  81. data/lib/davinci_pas_test_kit/client/pas_client_subscription_pas_conformance_test.rb +4 -3
  82. data/lib/davinci_pas_test_kit/client/registration/abstract_configuration_other_display_test.rb +6 -4
  83. data/lib/davinci_pas_test_kit/client/registration/abstract_configuration_smart_display_test.rb +8 -4
  84. data/lib/davinci_pas_test_kit/client/registration/abstract_configuration_udap_display_test.rb +9 -4
  85. data/lib/davinci_pas_test_kit/client/registration/abstract_other_auth_attest_test.rb +5 -4
  86. data/lib/davinci_pas_test_kit/client/request_count_gating.rb +14 -0
  87. data/lib/davinci_pas_test_kit/client/response_generator.rb +50 -9
  88. data/lib/davinci_pas_test_kit/client/session_identification.rb +26 -7
  89. data/lib/davinci_pas_test_kit/client/subscription_notification_trigger.rb +80 -0
  90. data/lib/davinci_pas_test_kit/client/subscription_setup_requirement.rb +59 -0
  91. data/lib/davinci_pas_test_kit/client/user_input_response.rb +27 -6
  92. data/lib/davinci_pas_test_kit/client/v2.0.1/client_suite.rb +30 -15
  93. data/lib/davinci_pas_test_kit/client/v2.0.1/must_support/pas_client_must_support_request_profiles_test.rb +2 -2
  94. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_approval_group.rb +8 -7
  95. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_auth_smart_group.rb +4 -4
  96. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_auth_udap_group.rb +4 -4
  97. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_denial_group.rb +8 -7
  98. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_must_support_group.rb +38 -52
  99. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_pended_group.rb +28 -22
  100. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_registration_group.rb +2 -2
  101. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_subscription_setup_group.rb +2 -2
  102. data/lib/davinci_pas_test_kit/client/v2.0.1/pas_client_workflows_group.rb +2 -2
  103. data/lib/davinci_pas_test_kit/client/v2.0.1/workflows/pas_client_inquire_request_bundle_validation_test.rb +15 -21
  104. data/lib/davinci_pas_test_kit/client/v2.0.1/workflows/pas_client_inquire_response_bundle_validation_test.rb +29 -29
  105. data/lib/davinci_pas_test_kit/client/v2.0.1/workflows/pas_client_pended_submit_test.rb +49 -40
  106. data/lib/davinci_pas_test_kit/client/v2.0.1/workflows/pas_client_request_bundle_validation_test.rb +15 -21
  107. data/lib/davinci_pas_test_kit/client/v2.0.1/workflows/pas_client_response_bundle_validation_test.rb +29 -29
  108. data/lib/davinci_pas_test_kit/client/v2.2.1/attestations/attestation_instructions.rb +10 -0
  109. data/lib/davinci_pas_test_kit/client/v2.2.1/attestations/client_data_element_expectations_attestation_test.rb +44 -0
  110. data/lib/davinci_pas_test_kit/client/v2.2.1/attestations/client_provider_review_attestation_test.rb +45 -0
  111. data/lib/davinci_pas_test_kit/client/v2.2.1/attestations/client_security_attestation_test.rb +42 -0
  112. data/lib/davinci_pas_test_kit/client/v2.2.1/attestations/client_subscription_error_recovery_attestation_test.rb +42 -0
  113. data/lib/davinci_pas_test_kit/client/v2.2.1/client_suite.rb +64 -12
  114. data/lib/davinci_pas_test_kit/client/v2.2.1/error_handling/pas_client_operation_failure_submit_test.rb +20 -10
  115. data/lib/davinci_pas_test_kit/client/v2.2.1/error_handling/pas_client_operation_outcome_validation_test.rb +11 -5
  116. data/lib/davinci_pas_test_kit/client/v2.2.1/error_handling/pas_client_processing_error_response_validation_test.rb +21 -12
  117. data/lib/davinci_pas_test_kit/client/v2.2.1/error_handling/pas_client_processing_error_submit_test.rb +19 -8
  118. data/lib/davinci_pas_test_kit/client/v2.2.1/must_support/must_support_with_attestation_option.rb +90 -40
  119. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_approval_group.rb +12 -11
  120. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_attestations_group.rb +32 -0
  121. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_auth_smart_group.rb +4 -4
  122. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_auth_udap_group.rb +4 -4
  123. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_claim_updates_group.rb +24 -6
  124. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_denial_group.rb +12 -10
  125. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_modification_group.rb +13 -14
  126. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_must_support_group.rb +55 -59
  127. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_operation_failure_group.rb +8 -5
  128. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_pended_group.rb +53 -41
  129. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_processing_error_group.rb +9 -7
  130. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_registration_group.rb +2 -2
  131. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_subscription_setup_group.rb +9 -4
  132. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_subscription_update_delete_group.rb +57 -0
  133. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_workflows_group.rb +22 -2
  134. data/lib/davinci_pas_test_kit/client/v2.2.1/subscription/pas_client_subscription_create_test.rb +2 -1
  135. data/lib/davinci_pas_test_kit/client/v2.2.1/subscription/pas_client_subscription_put_update_pas_conformance_test.rb +49 -0
  136. data/lib/davinci_pas_test_kit/client/v2.2.1/subscription/pas_client_subscription_update_delete_test.rb +12 -0
  137. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_claim_update_referenced_resources_test.rb +5 -6
  138. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_claim_update_submit_tests.rb +82 -56
  139. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_inquire_request_bundle_validation_test.rb +17 -20
  140. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_inquire_response_bundle_validation_test.rb +59 -27
  141. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_modification_verification_test.rb +0 -3
  142. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_pended_send_notification_test.rb +138 -0
  143. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_pended_submit_test.rb +32 -85
  144. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_request_bundle_validation_test.rb +16 -21
  145. data/lib/davinci_pas_test_kit/client/v2.2.1/workflows/pas_client_response_bundle_validation_test.rb +27 -27
  146. data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/task/metadata.yml +0 -4
  147. data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/metadata.yml +0 -1
  148. data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/task/metadata.yml +0 -4
  149. data/lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb +489 -289
  150. data/lib/davinci_pas_test_kit/cross_suite/pas_constants.rb +7 -0
  151. data/lib/davinci_pas_test_kit/cross_suite/pas_subscription_verification.rb +8 -4
  152. data/lib/davinci_pas_test_kit/cross_suite/referenced_resource_presence_validation.rb +141 -0
  153. data/lib/davinci_pas_test_kit/cross_suite/response_selection_utils.rb +13 -11
  154. data/lib/davinci_pas_test_kit/cross_suite/tags.rb +11 -6
  155. data/lib/davinci_pas_test_kit/cross_suite/validator_configuration.rb +16 -0
  156. data/lib/davinci_pas_test_kit/cross_suite/validator_suppressions.rb +4 -0
  157. data/lib/davinci_pas_test_kit/docs/demo/PAS Client Suite Demonstration.postman_collection.json +3 -3
  158. data/lib/davinci_pas_test_kit/ext/inferno_core/runnable.rb +1 -0
  159. data/lib/davinci_pas_test_kit/generator/client_must_support_group_generator.rb +47 -16
  160. data/lib/davinci_pas_test_kit/generator/must_support_target_profiles.rb +17 -0
  161. data/lib/davinci_pas_test_kit/generator/must_support_test_generator.rb +8 -2
  162. data/lib/davinci_pas_test_kit/generator/profile_metadata.rb +1 -1
  163. data/lib/davinci_pas_test_kit/generator/server_suite_generator.rb +4 -9
  164. data/lib/davinci_pas_test_kit/generator/templates/client_must_support_group.rb.erb +22 -3
  165. data/lib/davinci_pas_test_kit/generator/templates/client_must_support_response_group.rb.erb +7 -16
  166. data/lib/davinci_pas_test_kit/generator/templates/must_support.rb.erb +8 -8
  167. data/lib/davinci_pas_test_kit/generator/templates/server_suite.rb.erb +6 -7
  168. data/lib/davinci_pas_test_kit/generator/use_case_group_generator.rb +5 -5
  169. data/lib/davinci_pas_test_kit/parameters_helper.rb +13 -5
  170. data/lib/davinci_pas_test_kit/pas_notification_conformance_test.rb +2 -2
  171. data/lib/davinci_pas_test_kit/requirements/davinci_pas_test_kit_requirements.csv +14 -7
  172. data/lib/davinci_pas_test_kit/requirements/generated/davinci_pas_client_suite_v201_requirements_coverage.csv +21 -21
  173. data/lib/davinci_pas_test_kit/requirements/generated/davinci_pas_client_suite_v221_requirements_coverage.csv +30 -26
  174. data/lib/davinci_pas_test_kit/requirements/generated/davinci_pas_server_suite_v221_requirements_coverage.csv +12 -6
  175. data/lib/davinci_pas_test_kit/requirements/hl7.fhir.us.davinci-pas_2.2.1_requirements.xlsx +0 -0
  176. data/lib/davinci_pas_test_kit/server/claim_operation_logic.rb +1 -1
  177. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/beneficiary/server_inquire_request_must_support_beneficiary_test.rb +1 -1
  178. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/beneficiary/server_inquire_response_must_support_beneficiary_test.rb +1 -1
  179. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/beneficiary/server_submit_request_must_support_beneficiary_test.rb +1 -1
  180. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/beneficiary/server_submit_response_must_support_beneficiary_test.rb +1 -1
  181. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/claim_inquiry/server_inquire_request_must_support_claim_inquiry_test.rb +1 -1
  182. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/claim_update/server_submit_request_must_support_claim_update_test.rb +1 -1
  183. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/claiminquiryresponse/server_inquire_response_must_support_claiminquiryresponse_test.rb +1 -1
  184. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/claimresponse/server_submit_response_must_support_claimresponse_test.rb +1 -1
  185. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/communication_request/server_submit_response_must_support_communication_request_test.rb +1 -1
  186. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/coverage/server_inquire_request_must_support_coverage_test.rb +1 -1
  187. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/coverage/server_submit_request_must_support_coverage_test.rb +1 -1
  188. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/encounter/server_submit_request_must_support_encounter_test.rb +1 -1
  189. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/insurer/server_inquire_request_must_support_insurer_test.rb +1 -1
  190. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/insurer/server_inquire_response_must_support_insurer_test.rb +1 -1
  191. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/insurer/server_submit_request_must_support_insurer_test.rb +1 -1
  192. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/insurer/server_submit_response_must_support_insurer_test.rb +1 -1
  193. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_inquiry_request_bundle/server_inquire_request_must_support_pas_inquiry_request_bundle_test.rb +1 -1
  194. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_inquiry_response_bundle/server_inquire_response_must_support_pas_inquiry_response_bundle_test.rb +1 -1
  195. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_request_bundle/server_submit_request_must_support_pas_request_bundle_test.rb +1 -1
  196. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_response_bundle/server_submit_response_must_support_pas_response_bundle_test.rb +1 -1
  197. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_server_approval_use_case_group.rb +3 -3
  198. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_server_denial_use_case_group.rb +3 -3
  199. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_server_pended_use_case_group.rb +6 -6
  200. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner/server_inquire_request_must_support_practitioner_test.rb +1 -1
  201. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner/server_inquire_response_must_support_practitioner_test.rb +1 -1
  202. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner/server_submit_request_must_support_practitioner_test.rb +1 -1
  203. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner/server_submit_response_must_support_practitioner_test.rb +1 -1
  204. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner_role/server_inquire_request_must_support_practitioner_role_test.rb +1 -1
  205. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner_role/server_inquire_response_must_support_practitioner_role_test.rb +1 -1
  206. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner_role/server_submit_request_must_support_practitioner_role_test.rb +1 -1
  207. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/practitioner_role/server_submit_response_must_support_practitioner_role_test.rb +1 -1
  208. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/requestor/server_inquire_request_must_support_requestor_test.rb +1 -1
  209. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/requestor/server_inquire_response_must_support_requestor_test.rb +1 -1
  210. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/requestor/server_submit_request_must_support_requestor_test.rb +1 -1
  211. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/requestor/server_submit_response_must_support_requestor_test.rb +1 -1
  212. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/server_suite.rb +6 -7
  213. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/subscriber/server_inquire_request_must_support_subscriber_test.rb +1 -1
  214. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/subscriber/server_submit_request_must_support_subscriber_test.rb +1 -1
  215. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/task/server_inquire_response_must_support_task_test.rb +1 -1
  216. data/lib/davinci_pas_test_kit/server/generated/v2.0.1/task/server_submit_response_must_support_task_test.rb +1 -1
  217. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/beneficiary/server_inquire_request_must_support_beneficiary_test.rb +1 -1
  218. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/beneficiary/server_inquire_response_must_support_beneficiary_test.rb +1 -1
  219. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/beneficiary/server_submit_request_must_support_beneficiary_test.rb +1 -1
  220. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/beneficiary/server_submit_response_must_support_beneficiary_test.rb +1 -1
  221. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/claim_inquiry/server_inquire_request_must_support_claim_inquiry_test.rb +1 -1
  222. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/claim_update/server_submit_request_must_support_claim_update_test.rb +1 -1
  223. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/claiminquiryresponse/server_inquire_response_must_support_claiminquiryresponse_test.rb +1 -1
  224. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/claimresponse/server_submit_response_must_support_claimresponse_test.rb +1 -1
  225. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/communication_request/server_submit_response_must_support_communication_request_test.rb +1 -1
  226. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/coverage/server_inquire_request_must_support_coverage_test.rb +1 -1
  227. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/coverage/server_submit_request_must_support_coverage_test.rb +1 -1
  228. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/encounter/server_submit_request_must_support_encounter_test.rb +1 -1
  229. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/insurer/server_inquire_request_must_support_insurer_test.rb +1 -1
  230. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/insurer/server_inquire_response_must_support_insurer_test.rb +1 -1
  231. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/insurer/server_submit_request_must_support_insurer_test.rb +1 -1
  232. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/insurer/server_submit_response_must_support_insurer_test.rb +1 -1
  233. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_inquiry_request_bundle/server_inquire_request_must_support_pas_inquiry_request_bundle_test.rb +1 -1
  234. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_inquiry_response_bundle/server_inquire_response_must_support_pas_inquiry_response_bundle_test.rb +1 -1
  235. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_request_bundle/server_submit_request_must_support_pas_request_bundle_test.rb +1 -1
  236. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_response_bundle/server_submit_response_must_support_pas_response_bundle_test.rb +1 -1
  237. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_approval_use_case_group.rb +3 -3
  238. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_denial_use_case_group.rb +3 -3
  239. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_pended_use_case_group.rb +4 -4
  240. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner/server_inquire_request_must_support_practitioner_test.rb +1 -1
  241. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner/server_inquire_response_must_support_practitioner_test.rb +1 -1
  242. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner/server_submit_request_must_support_practitioner_test.rb +1 -1
  243. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner/server_submit_response_must_support_practitioner_test.rb +1 -1
  244. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner_role/server_inquire_request_must_support_practitioner_role_test.rb +1 -1
  245. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner_role/server_inquire_response_must_support_practitioner_role_test.rb +1 -1
  246. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner_role/server_submit_request_must_support_practitioner_role_test.rb +1 -1
  247. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/practitioner_role/server_submit_response_must_support_practitioner_role_test.rb +1 -1
  248. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/requestor/server_inquire_request_must_support_requestor_test.rb +1 -1
  249. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/requestor/server_inquire_response_must_support_requestor_test.rb +1 -1
  250. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/requestor/server_submit_request_must_support_requestor_test.rb +1 -1
  251. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/requestor/server_submit_response_must_support_requestor_test.rb +1 -1
  252. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/server_suite.rb +6 -7
  253. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/subscriber/server_inquire_request_must_support_subscriber_test.rb +1 -1
  254. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/subscriber/server_submit_request_must_support_subscriber_test.rb +1 -1
  255. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/task/server_inquire_response_must_support_task_test.rb +1 -1
  256. data/lib/davinci_pas_test_kit/server/generated/v2.2.1/task/server_submit_response_must_support_task_test.rb +1 -1
  257. data/lib/davinci_pas_test_kit/server/pas_claim_response_decision_test.rb +2 -2
  258. data/lib/davinci_pas_test_kit/server/server_request_bundle_validation_test.rb +4 -6
  259. data/lib/davinci_pas_test_kit/server/server_response_bundle_validation_test.rb +4 -6
  260. data/lib/davinci_pas_test_kit/server/v2.0.1/must_support/pas_server_must_support_request_profiles_test.rb +1 -1
  261. data/lib/davinci_pas_test_kit/server/v2.0.1/pas_server_subscription_setup.rb +1 -1
  262. data/lib/davinci_pas_test_kit/server/v2.2.1/must_support/pas_server_must_support_request_profiles_test.rb +1 -1
  263. data/lib/davinci_pas_test_kit/server/v2.2.1/pas_server_claim_updates_group.rb +1 -1
  264. data/lib/davinci_pas_test_kit/server/v2.2.1/pas_server_subscription_setup.rb +1 -1
  265. data/lib/davinci_pas_test_kit/version.rb +2 -2
  266. metadata +31 -13
  267. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/task/client_inquire_response_must_support_task_test.rb +0 -45
  268. data/lib/davinci_pas_test_kit/client/generated/v2.0.1/task/client_submit_response_must_support_task_test.rb +0 -45
  269. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/task/client_inquire_response_must_support_task_test.rb +0 -55
  270. data/lib/davinci_pas_test_kit/client/generated/v2.2.1/task/client_submit_response_must_support_task_test.rb +0 -55
  271. data/lib/davinci_pas_test_kit/client/v2.2.1/pas_client_error_handling_group.rb +0 -20
@@ -4,85 +4,35 @@ require_relative '../parameters_helper'
4
4
  require_relative 'validation_test'
5
5
  require_relative 'pas_constants'
6
6
  require_relative 'pas_datatype_constraints'
7
+ require_relative 'referenced_resource_presence_validation'
8
+ require_relative 'validator_configuration'
7
9
 
8
10
  module DaVinciPASTestKit
9
11
  module PasBundleValidation
10
12
  include DaVinciPASTestKit::ValidationTest
11
13
  include DaVinciPASTestKit::PasDatatypeConstraints
14
+ include DaVinciPASTestKit::ReferencedResourcePresenceValidation
12
15
  include ParametersHelper
13
16
 
14
- US_CORE_VERSION = '6.1.0'
15
- US_CORE_PROFILE_BASE = 'http://hl7.org/fhir/us/core/StructureDefinition'
16
- BASE_R4_PROFILE = :base_r4
17
- CLAIM_ENCOUNTER_EXTENSION_URL = 'http://hl7.org/fhir/5.0/StructureDefinition/extension-Claim.encounter'
18
- LOINC_SYSTEM = 'http://loinc.org'
19
- TERMINOLOGY_CONDITION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/condition-category'
20
- OBSERVATION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/observation-category'
21
- DIAGNOSTIC_REPORT_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v2-0074'
22
-
23
- US_CORE_SINGLE_PROFILE_IDS_BY_RESOURCE = {
24
- 'AllergyIntolerance' => 'us-core-allergyintolerance',
25
- 'CarePlan' => 'us-core-careplan',
26
- 'CareTeam' => 'us-core-careteam',
27
- 'Coverage' => 'us-core-coverage',
28
- 'Device' => 'us-core-implantable-device',
29
- 'DocumentReference' => 'us-core-documentreference',
30
- 'Encounter' => 'us-core-encounter',
31
- 'Goal' => 'us-core-goal',
32
- 'Immunization' => 'us-core-immunization',
33
- 'Location' => 'us-core-location',
34
- 'Medication' => 'us-core-medication',
35
- 'MedicationDispense' => 'us-core-medicationdispense',
36
- 'MedicationRequest' => 'us-core-medicationrequest',
37
- 'Organization' => 'us-core-organization',
38
- 'Patient' => 'us-core-patient',
39
- 'Practitioner' => 'us-core-practitioner',
40
- 'PractitionerRole' => 'us-core-practitionerrole',
41
- 'Procedure' => 'us-core-procedure',
42
- 'Provenance' => 'us-core-provenance',
43
- 'QuestionnaireResponse' => 'us-core-questionnaireresponse',
44
- 'RelatedPerson' => 'us-core-relatedperson',
45
- 'ServiceRequest' => 'us-core-servicerequest',
46
- 'Specimen' => 'us-core-specimen'
47
- }.freeze
48
-
49
- US_CORE_CONDITION_ENCOUNTER_DIAGNOSIS_PROFILE_ID = 'us-core-condition-encounter-diagnosis'
50
- US_CORE_CONDITION_PROBLEMS_HEALTH_CONCERNS_PROFILE_ID = 'us-core-condition-problems-health-concerns'
51
- US_CORE_DIAGNOSTIC_REPORT_LAB_PROFILE_ID = 'us-core-diagnosticreport-lab'
52
- US_CORE_DIAGNOSTIC_REPORT_NOTE_PROFILE_ID = 'us-core-diagnosticreport-note'
53
- US_CORE_OBSERVATION_CLINICAL_RESULT_PROFILE_ID = 'us-core-observation-clinical-result'
54
- US_CORE_OBSERVATION_LAB_PROFILE_ID = 'us-core-observation-lab'
55
- US_CORE_OBSERVATION_SCREENING_ASSESSMENT_PROFILE_ID = 'us-core-observation-screening-assessment'
56
- US_CORE_SIMPLE_OBSERVATION_PROFILE_ID = 'us-core-simple-observation'
57
- US_CORE_SMOKING_STATUS_PROFILE_ID = 'us-core-smokingstatus'
58
- US_CORE_VITAL_SIGNS_PROFILE_ID = 'us-core-vital-signs'
59
-
60
- US_CORE_OBSERVATION_CODE_PROFILE_IDS = {
61
- '11341-5' => 'us-core-observation-occupation',
62
- '86645-9' => 'us-core-observation-pregnancyintent',
63
- '82810-3' => 'us-core-observation-pregnancystatus',
64
- '76690-7' => 'us-core-observation-sexual-orientation',
65
- '8289-1' => 'head-occipital-frontal-circumference-percentile',
66
- '59576-9' => 'pediatric-bmi-for-age',
67
- '77606-2' => 'pediatric-weight-for-height',
68
- '85354-9' => 'us-core-blood-pressure',
69
- '39156-5' => 'us-core-bmi',
70
- '8302-2' => 'us-core-body-height',
71
- '8310-5' => 'us-core-body-temperature',
72
- '29463-7' => 'us-core-body-weight',
73
- '9843-4' => 'us-core-head-circumference',
74
- '8867-4' => 'us-core-heart-rate',
75
- '59408-5' => 'us-core-pulse-oximetry',
76
- '2708-6' => 'us-core-pulse-oximetry',
77
- '9279-1' => 'us-core-respiratory-rate',
78
- '72166-2' => 'us-core-smokingstatus'
79
- }.freeze
80
-
81
- def validation_error_messages
82
- @validation_error_messages ||= []
83
- end
84
-
85
- def perform_bundle_validation(bundle, operation, type, ig_version, request_bundle = nil)
17
+ ###########################################################################
18
+ # Public API
19
+ ###########################################################################
20
+
21
+ # @return [Array<Hash>] The messages (of every severity) found while validating this
22
+ # bundle, each a { type:, message: } hash. Callers that only care about failure should
23
+ # check for a message with type: 'error' rather than just whether this is present -
24
+ # see validation_messages.
25
+ # @param validator [Symbol] the name of the FHIR resource validator to use for profile
26
+ # validation. Use perform_no_custom_extensions_validation for Bundles Inferno sends.
27
+ # @param echoed_from [FHIR::Bundle, String, nil] the request Bundle the response answers. When
28
+ # given, entry resources echoed back from it that carry no extensions beyond those they had in
29
+ # it are validated with the :default validator rather than `validator`, since the extensions
30
+ # came from the system under test.
31
+ def perform_bundle_validation(bundle, operation, type, ig_version, request_bundle = nil, validator: :default,
32
+ echoed_from: nil)
33
+ @validation_messages = []
34
+ @profile_validator = validator
35
+ @echoed_from_bundle = parse_bundle(echoed_from)
86
36
  target_profile = PASConstants.bundle_profile_url_for_operation_and_type(operation, type)
87
37
  request_type = "#{operation}_#{type}"
88
38
  if type == 'request'
@@ -90,66 +40,260 @@ module DaVinciPASTestKit
90
40
  else
91
41
  perform_response_validation(bundle, target_profile, ig_version.delete_prefix('v'), request_type, request_bundle)
92
42
  end
43
+ validation_messages
93
44
  end
94
45
 
95
- def perform_request_validation(bundle, profile_url, version, request_type)
96
- validate_pa_request_payload_structure(bundle, request_type)
97
- validate_resources_conformance_against_profile(bundle, profile_url, version, request_type)
46
+ # Validates a Bundle that Inferno sends to the system under test, which may be tester-provided,
47
+ # using NO_CUSTOM_EXTENSIONS_VALIDATOR so that custom extensions aren't allowed. Resources echoed
48
+ # back from the system under test's Bundle may keep the extensions they had in it.
49
+ # @param type [String] 'request' or 'response' - see perform_bundle_validation.
50
+ # @param echoed_from [FHIR::Bundle, String, nil] the Bundle from the system under test that this
51
+ # one answers, e.g. the request for a response, or nil if there is none - see
52
+ # perform_bundle_validation.
53
+ # @return [Array<Hash>] see perform_bundle_validation.
54
+ def perform_no_custom_extensions_validation(bundle, operation, type, ig_version, echoed_from:)
55
+ perform_bundle_validation(bundle, operation, type, ig_version,
56
+ validator: NO_CUSTOM_EXTENSIONS_VALIDATOR, echoed_from:)
98
57
  end
99
58
 
100
- def perform_response_validation(response_bundle, profile_url, version, request_type, request_bundle = nil)
101
- validate_pa_response_body_structure(response_bundle, request_bundle) if request_type.start_with?('submit')
102
- validate_resources_conformance_against_profile(response_bundle, profile_url, version, request_type)
59
+ # Validates a Parameters resource that Inferno sends to wrap Bundles (e.g., a v2.2.1 $inquire
60
+ # response) against the base Parameters definition, since PAS doesn't profile it, using
61
+ # NO_CUSTOM_EXTENSIONS_VALIDATOR. Issues within the wrapped resources are dropped, since those are
62
+ # validated separately - see perform_no_custom_extensions_validation.
63
+ # @param parameters [FHIR::Parameters]
64
+ # @return [Array<Hash>] the messages found, each a { type:, message: } hash.
65
+ def perform_no_custom_extensions_parameters_validation(parameters)
66
+ response_details = []
67
+ resource_is_valid?(resource: parameters, validator: NO_CUSTOM_EXTENSIONS_VALIDATOR,
68
+ add_messages_to_runnable: false,
69
+ validator_response_details: response_details)
70
+ validator_issues_to_messages(response_details.reject do |issue|
71
+ issue.filtered || issue.location&.match?(/\AParameters\.parameter\[\d+\](\.part\[\d+\])*\.resource/)
72
+ end)
103
73
  end
104
74
 
105
- def validate_pas_bundle_json(json, profile_url, version, request_type, bundle_type, skips: false, message: '')
106
- assert_valid_json(json)
107
- resource = FHIR.from_contents(json)
108
- assert resource.present?, 'Not a FHIR resource'
75
+ ###########################################################################
76
+ # Internal Validation Methods
77
+ ###########################################################################
78
+
79
+ # Every message (of every severity, not just errors) collected while validating the
80
+ # current bundle. Passing-profile messages and per-issue profile-conformance messages
81
+ # (see validate_bundle_entries_against_profiles) may be warnings or info, not just
82
+ # errors, which is why this isn't error-only despite most entries being added via
83
+ # add_validation_error.
84
+ def validation_messages
85
+ @validation_messages ||= []
86
+ end
109
87
 
110
- # For v2.2.1 inquire responses, expect Parameters resource
111
- if version == '2.2.1' && request_type == 'inquire' && bundle_type == 'response_bundle'
112
- if resource.resourceType == 'Parameters'
113
- # Extract and validate each Bundle in the Parameters
114
- bundles = extract_bundles_from_pas_inquiry_response_parameters(resource)
88
+ # Appends a plain error string to validation_messages as a { type:, message: } hash,
89
+ # for the many checks in this file that only ever produce errors (as opposed to
90
+ # validate_bundle_entries_against_profiles, which already has typed messages from the
91
+ # validator and concats them directly).
92
+ def add_validation_error(message)
93
+ validation_messages << { type: 'error', message: }
94
+ end
115
95
 
116
- bundles.each do |bundle|
117
- perform_response_validation(bundle, profile_url, version, request_type)
118
- end
119
- elsif resource.is_a?(FHIR::Bundle)
120
- # Bundle received instead of Parameters - validate it but log an error
121
- validation_error_messages << 'Expected Parameters resource for v2.2.1 inquire response, but received ' \
122
- 'Bundle. The response Bundle should be wrapped in a ' \
123
- 'Parameters resource with a return parameter.'
124
- perform_response_validation(resource, profile_url, version, request_type)
125
- else
126
- assert false,
127
- "Expected Parameters resource for v2.2.1 inquire response, but received #{resource.resourceType}"
96
+ # The name of the FHIR resource validator used for profile validation of the current
97
+ # bundle - see perform_bundle_validation.
98
+ def profile_validator
99
+ @profile_validator || :default
100
+ end
101
+
102
+ # How to validate one entry resource in the current bundle. Resources echoed back from the request
103
+ # (see perform_bundle_validation's echoed_from) may keep the extensions they had in it: those that
104
+ # add no extensions of their own use the :default validator, and the others use the specified
105
+ # validator with the issues it reports about their extensions explained or dropped - see
106
+ # echoed_extension_issue_messages.
107
+ # @param full_url [String] the fullUrl of the entry containing the resource.
108
+ # @param resource [FHIR::Model] the entry resource.
109
+ # @return [Array(Symbol, Array, FHIR::Bundle::Entry)] the name of the validator to use, the
110
+ # [element path, extension] pairs (see extensions_in) of the request resource the resource echoes,
111
+ # and the request entry it echoes. The last two are nil if it doesn't echo one (or the validator is
112
+ # :default, which allows any extension).
113
+ def entry_validation_settings(full_url, resource)
114
+ return [profile_validator, nil, nil] if profile_validator == :default
115
+
116
+ request_entry = echoed_request_entry(full_url, resource)
117
+ return [profile_validator, nil, nil] if request_entry.blank?
118
+
119
+ echoed_extensions = extensions_in(validated_hash(request_entry.resource))
120
+ all_echoed = extensions_in(validated_hash(resource)).all? { |extension| echoed_extensions.include?(extension) }
121
+ [all_echoed ? :default : profile_validator, echoed_extensions, request_entry]
122
+ end
123
+
124
+ def echoed_request_entry(full_url, resource)
125
+ return if @echoed_from_bundle.blank?
126
+
127
+ response_entry = FHIR::Bundle::Entry.new(fullUrl: full_url, resource:)
128
+ @echoed_from_bundle.entry.find { |entry| echoed_resource?(entry, response_entry) }
129
+ end
130
+
131
+ # The resource as the validator receives it (Inferno sends its source_contents), as a hash. Unlike
132
+ # to_hash, this keeps the extensions on primitive elements (e.g., `_birthDate`).
133
+ def validated_hash(resource)
134
+ JSON.parse(resource.source_contents)
135
+ rescue StandardError
136
+ resource.to_hash
137
+ end
138
+
139
+ # An extension is echoed if it appears, with the same content and at the same element path, in the
140
+ # request resource. Paths leave out array indexes so reordered repeating elements still match.
141
+ # Reference values are ignored because Inferno rewrites them when it echoes resources in responses
142
+ # it generates.
143
+ # @return [Array<Array(String, Hash)>] [element path, extension] pairs for every extension (at any
144
+ # depth, including modifierExtensions) in the value.
145
+ def extensions_in(value, path = '')
146
+ case value
147
+ when Hash
148
+ value.flat_map do |key, child|
149
+ child_path = path.empty? ? key : "#{path}.#{key}"
150
+ own = if %w[extension modifierExtension].include?(key)
151
+ Array(child).map { |extension| [child_path, without_references(extension)] }
152
+ else
153
+ []
154
+ end
155
+ own + extensions_in(child, child_path)
128
156
  end
157
+ when Array
158
+ value.flat_map { |child| extensions_in(child, path) }
129
159
  else
130
- # For v2.0.1 or non-inquire operations, expect Bundle resource
131
- assert_resource_type(:bundle, resource: resource)
132
- bundle = resource
160
+ []
161
+ end
162
+ end
133
163
 
134
- if bundle_type == 'request_bundle'
135
- perform_request_validation(bundle, profile_url, version, request_type)
136
- else
137
- perform_response_validation(bundle, profile_url, version, request_type)
138
- end
164
+ def without_references(value)
165
+ case value
166
+ when Hash
167
+ value.except('reference').transform_values { |child| without_references(child) }
168
+ when Array
169
+ value.map { |child| without_references(child) }
170
+ else
171
+ value
172
+ end
173
+ end
174
+
175
+ # An info message identifying an echoed resource, which of its identifying fields matched the request
176
+ # entry it echoes (see echoed_resource?), and the extensions it is allowed to keep because they are
177
+ # unchanged from the request - see entry_validation_settings.
178
+ def echoed_resource_message(full_url, resource, echoed_extensions, request_entry)
179
+ kept_urls = extensions_in(validated_hash(resource))
180
+ .select { |extension| echoed_extensions.include?(extension) }
181
+ .map { |_path, extension| extension['url'] }.uniq
182
+ message = "#{resource.resourceType} #{full_url} echoes the #{resource.resourceType} in the request " \
183
+ "(#{echo_match_details(full_url, resource, request_entry)})"
184
+ message += if kept_urls.present?
185
+ ", so these extensions unchanged from the request are allowed in it: #{kept_urls.join(', ')}."
186
+ else
187
+ '.'
188
+ end
189
+ { type: 'info', message: }
190
+ end
191
+
192
+ # Which identifying fields of an echoed resource match the request entry it echoes and which differ,
193
+ # e.g., "matched by id; fullUrl differs". Fields blank in both are left out.
194
+ def echo_match_details(full_url, resource, request_entry)
195
+ fields = {
196
+ 'fullUrl' => [request_entry.fullUrl, full_url],
197
+ 'id' => [request_entry.resource.id, resource.id],
198
+ 'identifiers' => [identifiers_of(request_entry.resource), identifiers_of(resource)]
199
+ }.reject { |_name, values| values.all?(&:blank?) }
200
+ matched, differing = fields.keys.partition do |name|
201
+ request_value, response_value = fields[name]
202
+ request_value == response_value
203
+ end
204
+ details = "matched by #{matched.to_sentence}"
205
+ details += "; #{differing.to_sentence} #{differing.one? ? 'differs' : 'differ'}" if differing.present?
206
+ details
207
+ end
208
+
209
+ # Messages for the validator issues about an echoed resource. An issue that an extension isn't allowed
210
+ # (see unknown_extension_issue?) is:
211
+ # - dropped if the extension is echoed, i.e., in echoed_extensions.
212
+ # - reworded otherwise, to say whether the request resource has an extension with that url at the
213
+ # same element, but with different content, or doesn't.
214
+ # Other issues are kept as is.
215
+ # @param resource_hash [Hash] the validated resource, as a hash.
216
+ def echoed_extension_issue_messages(issues, resource_hash, echoed_extensions)
217
+ issues.filter_map do |issue|
218
+ path, extension = extension_at_location(resource_hash, issue.location)
219
+ url = extension&.dig('url')
220
+ next validator_issue_message(issue) unless unknown_extension_issue?(issue, url)
221
+ next if echoed_extensions.include?([path, without_references(extension)])
222
+
223
+ details =
224
+ if echoed_extensions.any? { |echoed_path, echoed| echoed_path == path && echoed['url'] == url }
225
+ "echoed extension #{url} contained a different value than in the source resource."
226
+ else
227
+ "extension #{url} in an echoed resource is not in the source resource at this element."
228
+ end
229
+ validator_issue_message(issue, details:)
139
230
  end
231
+ end
232
+
233
+ # True if the issue is the validator's error for an extension it doesn't know, so doesn't allow
234
+ # (Extension_EXT_Unknown_NotHere), as the strict validator reports for custom extensions. Other
235
+ # issues about extensions, e.g., known extensions with invalid values, are reported as is.
236
+ def unknown_extension_issue?(issue, url)
237
+ url.present? && issue.raw_issue['message'] == "The extension #{url} could not be found so is not allowed here"
238
+ end
239
+
240
+ # True if the issue is the :default validator's note that an echoed extension is unknown
241
+ # (Extension_EXT_Unknown) - see echoed_resource_message.
242
+ def echoed_unknown_extension_note?(issue, resource_hash, echoed_extensions)
243
+ path, extension = extension_at_location(resource_hash, issue.location)
244
+ url = extension&.dig('url')
245
+ url.present? && issue.raw_issue['message'] == "Unknown extension #{url}" &&
246
+ echoed_extensions.include?([path, without_references(extension)])
247
+ end
140
248
 
141
- validation_error_messages.each do |msg|
142
- messages << { type: 'error', message: msg }
249
+ # Resolves a validator issue location such as `Patient.name[0].extension[1]` against the resource.
250
+ # Extensions on primitive elements are located by the element's name (e.g.,
251
+ # `Patient.birthDate.extension[0]`) but held in its `_` sibling (e.g., `_birthDate`) in JSON.
252
+ # @return [Array(String, Hash), nil] the extension's index-free element path (as in extensions_in)
253
+ # and the extension, or nil if the location isn't an extension in the resource.
254
+ def extension_at_location(resource_hash, location)
255
+ value = resource_hash
256
+ names = location.to_s.split('.').drop(1).map do |segment|
257
+ name, index = segment.match(/\A(\w+)(?:\[(\d+)\])?\z/)&.captures
258
+ return nil if name.nil? || !value.is_a?(Hash)
259
+
260
+ name = "_#{name}" if primitive_element?(value, name)
261
+ value = value[name]
262
+ value = value[index.to_i] if index.present? && value.is_a?(Array)
263
+ name
143
264
  end
144
- msg = 'Bundle and/or entry resources are not conformant. Check messages for issues found.'
145
- assert validation_error_messages.blank?, msg
146
- rescue Inferno::Exceptions::AssertionException => e
147
- msg = "#{message} #{e.message}".strip
148
- raise e.class, msg unless skips
265
+ return unless %w[extension modifierExtension].include?(names.last) && value.is_a?(Hash)
149
266
 
150
- skip msg
267
+ [names.join('.'), value]
268
+ end
269
+
270
+ # True if the element is a primitive (or repeating primitive) with a `_` sibling holding its
271
+ # extensions, in the JSON object `parent`.
272
+ def primitive_element?(parent, name)
273
+ parent.key?("_#{name}") && Array.wrap(parent[name]).none?(Hash)
274
+ end
275
+
276
+ def parse_bundle(bundle)
277
+ bundle = FHIR.from_contents(bundle) if bundle.is_a?(String)
278
+ bundle if bundle.is_a?(FHIR::Bundle)
279
+ rescue StandardError
280
+ nil
281
+ end
282
+
283
+ def perform_request_validation(bundle, profile_url, version, request_type)
284
+ validate_pa_request_payload_structure(bundle, request_type)
285
+ validate_resources_conformance_against_profile(bundle, profile_url, version, request_type)
286
+ end
287
+
288
+ def perform_response_validation(response_bundle, profile_url, version, request_type, request_bundle = nil)
289
+ validate_pa_response_body_structure(response_bundle, request_bundle) if request_type.start_with?('submit')
290
+ validate_resources_conformance_against_profile(response_bundle, profile_url, version, request_type)
151
291
  end
152
292
 
293
+ ###########################################################################
294
+ # Structure Validation
295
+ ###########################################################################
296
+
153
297
  # Validates the structure of a Prior Authorization (PA) request Bundle.
154
298
  #
155
299
  # @param bundle [FHIR::Bundle] The FHIR Bundle of the PA request.
@@ -167,18 +311,22 @@ module DaVinciPASTestKit
167
311
  base_url = extract_base_url(bundle.entry.first&.fullUrl)
168
312
 
169
313
  check_presence_of_referenced_resources(first_entry, base_url, bundle.entry)
314
+ .each { |msg| add_validation_error(msg) }
170
315
 
171
- if request_type == 'submit'
316
+ # request_type is the compound 'submit_request' / 'inquire_request' built by
317
+ # perform_bundle_validation - start_with? also accepts a bare 'submit', consistent with
318
+ # find_profile_url and validate_resources_conformance_against_profile below.
319
+ if request_type.start_with?('submit')
172
320
  unless first_entry.is_a?(FHIR::Claim)
173
- validation_error_messages << "[Bundle/#{bundle.id}]: The first bundle entry must be a Claim"
321
+ add_validation_error("[Bundle/#{bundle.id}]: The first bundle entry must be a Claim")
174
322
  end
175
323
 
176
324
  validate_uniqueness_of_supporting_info_sequences(first_entry)
177
325
  validate_bundle_entries_full_url(bundle)
178
326
  else
179
- claim_resource = bundle_entry_resources.find { |resource| resource.resourceType == 'Claim' }
327
+ claim_resource = bundle_entry_resources.find { |resource| resource&.resourceType == 'Claim' }
180
328
  if claim_resource.blank?
181
- validation_error_messages << "[Bundle/#{bundle.id}]: Claim must be present for inquiry request"
329
+ add_validation_error("[Bundle/#{bundle.id}]: Claim must be present for inquiry request")
182
330
  end
183
331
 
184
332
  # The inquiry operation must contain a requesting provider organization,
@@ -188,16 +336,15 @@ module DaVinciPASTestKit
188
336
  payer_reference = claim_resource&.insurer&.reference
189
337
 
190
338
  if patient_reference.blank?
191
- validation_error_messages <<
192
- "[Bundle/#{bundle.id}]: The Claim for inquiry operation must reference a patient."
339
+ add_validation_error("[Bundle/#{bundle.id}]: The Claim for inquiry operation must reference a patient.")
193
340
  end
194
341
  if provider_reference.blank?
195
- validation_error_messages << "[Bundle/#{bundle.id}]: The claim for inquiry operation must reference " \
196
- 'a requesting provider organization.'
342
+ add_validation_error("[Bundle/#{bundle.id}]: The claim for inquiry operation must reference " \
343
+ 'a requesting provider organization.')
197
344
  end
198
345
  if payer_reference.blank?
199
- validation_error_messages << "[Bundle/#{bundle.id}]: The Claim for inquiry operation must contain " \
200
- 'a payer organization.'
346
+ add_validation_error("[Bundle/#{bundle.id}]: The Claim for inquiry operation must contain " \
347
+ 'a payer organization.')
201
348
  end
202
349
  end
203
350
  end
@@ -217,14 +364,14 @@ module DaVinciPASTestKit
217
364
  # to ensure the same fullUrl and resource identifiers as in the
218
365
  # request are used.
219
366
  def validate_pa_response_body_structure(pa_response_bundle, pa_request_bundle)
220
- first_entry = pa_response_bundle.entry.first.resource
367
+ first_entry = pa_response_bundle.entry.first&.resource
221
368
  unless first_entry.is_a?(FHIR::ClaimResponse)
222
- validation_error_messages <<
223
- "[Bundle/#{pa_response_bundle.id}]: The first bundle entry must be a ClaimResponse"
369
+ add_validation_error("[Bundle/#{pa_response_bundle.id}]: The first bundle entry must be a ClaimResponse")
224
370
  end
225
371
 
226
372
  base_url = extract_base_url(pa_response_bundle.entry.last&.fullUrl)
227
373
  check_presence_of_referenced_resources(first_entry, base_url, pa_response_bundle.entry)
374
+ .each { |msg| add_validation_error(msg) }
228
375
 
229
376
  validate_echoed_response_resources(pa_response_bundle, pa_request_bundle)
230
377
  end
@@ -246,10 +393,10 @@ module DaVinciPASTestKit
246
393
 
247
394
  next if echoed_resource_identifiers_match?(request_entry, response_entry)
248
395
 
249
- validation_error_messages << resource_present_in_pa_request_and_response_msg(response_resource)
396
+ add_validation_error(resource_present_in_pa_request_and_response_msg(response_resource))
250
397
  end
251
398
  rescue StandardError
252
- validation_error_messages << 'Unable to compare PAS request and response Bundle resources for echoed identifiers.'
399
+ add_validation_error('Unable to compare PAS request and response Bundle resources for echoed identifiers.')
253
400
  end
254
401
 
255
402
  def echoed_resource?(request_entry, response_entry)
@@ -262,7 +409,8 @@ module DaVinciPASTestKit
262
409
  # do not have all matching identifiers. Blank fields do not identify echoed resources.
263
410
  (request_entry.fullUrl.present? && request_entry.fullUrl == response_entry.fullUrl) ||
264
411
  (request_resource.id.present? && request_resource.id == response_resource.id) ||
265
- (request_resource.identifier.present? && request_resource.identifier == response_resource.identifier)
412
+ (identifiers_of(request_resource).present? &&
413
+ identifiers_of(request_resource) == identifiers_of(response_resource))
266
414
  end
267
415
 
268
416
  def echoed_resource_identifiers_match?(request_entry, response_entry)
@@ -271,9 +419,18 @@ module DaVinciPASTestKit
271
419
 
272
420
  request_entry.fullUrl == response_entry.fullUrl &&
273
421
  request_resource.id == response_resource.id &&
274
- request_resource.identifier == response_resource.identifier
422
+ identifiers_of(request_resource) == identifiers_of(response_resource)
423
+ end
424
+
425
+ # Nil for resource types without an identifier element (e.g., Binary, Parameters).
426
+ def identifiers_of(resource)
427
+ resource.identifier if resource.respond_to?(:identifier)
275
428
  end
276
429
 
430
+ ###########################################################################
431
+ # Bundle entry profile validation
432
+ ###########################################################################
433
+
277
434
  # Profile conformance of Prior Authorization (PA) resources.
278
435
  #
279
436
  # @param bundle [FHIR::Bundle] The FHIR bundle representing the PA request/response.
@@ -291,7 +448,7 @@ module DaVinciPASTestKit
291
448
  bundle_entry = bundle.entry
292
449
 
293
450
  root_entry = bundle_entry.find do |entry|
294
- ['Claim', 'ClaimResponse'].include?(entry.resource.resourceType)
451
+ ['Claim', 'ClaimResponse'].include?(entry.resource&.resourceType)
295
452
  end
296
453
 
297
454
  if root_entry.present?
@@ -333,12 +490,21 @@ module DaVinciPASTestKit
333
490
  # Validates bundle resource and each entry in the bundle against its target profiles.
334
491
  # Validation messages are collected per profile rather than logged directly, so a
335
492
  # resource with multiple candidate profiles only reports errors when it fails all of
336
- # them. When a profile passes, only that profile's (non-error) messages are logged.
493
+ # them. When a profile passes, only that profile's (non-error) messages are kept. Either
494
+ # way, messages go into validation_messages rather than straight to the runnable's
495
+ # `messages` - see perform_bundle_validation - so callers see every message this method
496
+ # finds, not just the summary line for a resource that conformed to nothing.
337
497
  # @param version [String] The version of the IG.
338
498
  def validate_bundle_entries_against_profiles(version)
339
499
  bundle_resources_target_profile_map.each do |key, item|
340
500
  resource = item[:resource]
341
501
  base_profile = FHIR::Definitions.resource_definition(resource.resourceType).url
502
+ unless key == 'bundle'
503
+ validator, echoed_extensions, request_entry = entry_validation_settings(key, resource)
504
+ if request_entry.present?
505
+ validation_messages << echoed_resource_message(key, resource, echoed_extensions, request_entry)
506
+ end
507
+ end
342
508
  messages_by_profile = {}
343
509
 
344
510
  success_profile = item[:profile_urls].find do |url|
@@ -346,9 +512,10 @@ module DaVinciPASTestKit
346
512
  if key == 'bundle'
347
513
  collect_bundle_profile_messages(resource, profile_url_for_validation(url, base_profile, version))
348
514
  elsif url == BASE_R4_PROFILE
349
- collect_resource_profile_messages(resource)
515
+ collect_resource_profile_messages(resource, validator:, echoed_extensions:)
350
516
  else
351
- collect_resource_profile_messages(resource, profile_url_for_validation(url, base_profile, version)) +
517
+ collect_resource_profile_messages(resource, profile_url_for_validation(url, base_profile, version),
518
+ validator:, echoed_extensions:) +
352
519
  datatype_constraint_messages(resource, profile_url_without_version(url), version)
353
520
  end
354
521
  messages_by_profile[url] = profile_messages
@@ -356,10 +523,10 @@ module DaVinciPASTestKit
356
523
  end
357
524
 
358
525
  if success_profile
359
- messages.concat(messages_by_profile[success_profile])
526
+ validation_messages.concat(messages_by_profile[success_profile])
360
527
  else
361
- messages_by_profile.each_value { |profile_messages| messages.concat(profile_messages) }
362
- validation_error_messages << generate_non_conformance_message(item)
528
+ messages_by_profile.each_value { |profile_messages| validation_messages.concat(profile_messages) }
529
+ add_validation_error(generate_non_conformance_message(item))
363
530
  end
364
531
  end
365
532
  end
@@ -367,7 +534,17 @@ module DaVinciPASTestKit
367
534
  def profile_url_for_validation(url, base_profile, version)
368
535
  url = url.to_s
369
536
  return url if url.include?('|') || profile_url_without_version(url) == base_profile
370
- return "#{url}|#{US_CORE_VERSION}" if us_core_profile_url?(url)
537
+
538
+ if us_core_profile_url?(url)
539
+ profile_id = url.delete_prefix("#{US_CORE_PROFILE_BASE}/")
540
+ # US Core 3.1.1's single Condition profile was split by category in later versions,
541
+ # so only 3.1.1 defines it.
542
+ return "#{url}|#{PASConstants::US_CORE_VERSION_3_1_1}" if profile_id == US_CORE_CONDITION_PROFILE_ID
543
+
544
+ # A declared US Core profile gets the selected version only when that version defines it;
545
+ # otherwise it stays unversioned so the validator resolves it to a version that does.
546
+ return us_core_profile_available?(profile_id) ? "#{url}|#{us_core_version}" : url
547
+ end
371
548
 
372
549
  "#{url}|#{version}"
373
550
  end
@@ -406,6 +583,12 @@ module DaVinciPASTestKit
406
583
  version.to_s.delete_prefix('v').match?(/\A2\.2(\.|\z)/)
407
584
  end
408
585
 
586
+ # The US Core version whose profiles the fallback validates against: the session's
587
+ # US Core Version suite option when the suite offers it, otherwise 6.1.0.
588
+ def us_core_version
589
+ suite_options[:us_core_version].presence || PASConstants::DEFAULT_US_CORE_VERSION
590
+ end
591
+
409
592
  def us_core_profile_urls_for_resource(resource)
410
593
  profile_ids =
411
594
  case resource.resourceType
@@ -419,10 +602,20 @@ module DaVinciPASTestKit
419
602
  Array(US_CORE_SINGLE_PROFILE_IDS_BY_RESOURCE[resource.resourceType])
420
603
  end
421
604
 
422
- us_core_profile_urls(profile_ids)
605
+ profile_urls = us_core_profile_urls(profile_ids.select { |profile_id| us_core_profile_available?(profile_id) })
606
+ # US Core 3.1.1 has no Simple Observation profile, so the base resource takes its place as the
607
+ # catch-all at the end of the Observation candidates.
608
+ if profile_ids.include?(US_CORE_SIMPLE_OBSERVATION_PROFILE_ID) &&
609
+ !us_core_profile_available?(US_CORE_SIMPLE_OBSERVATION_PROFILE_ID)
610
+ profile_urls << BASE_R4_PROFILE
611
+ end
612
+ profile_urls
423
613
  end
424
614
 
425
615
  def us_core_condition_profile_ids(resource)
616
+ # US Core 3.1.1 has a single Condition profile; later versions split it by category.
617
+ return [US_CORE_CONDITION_PROFILE_ID] if us_core_version == PASConstants::US_CORE_VERSION_3_1_1
618
+
426
619
  if category_code_present?(resource, 'encounter-diagnosis', system: TERMINOLOGY_CONDITION_CATEGORY_SYSTEM)
427
620
  [US_CORE_CONDITION_ENCOUNTER_DIAGNOSIS_PROFILE_ID]
428
621
  else
@@ -439,8 +632,9 @@ module DaVinciPASTestKit
439
632
  end
440
633
 
441
634
  def us_core_observation_profile_ids(resource)
442
- code_profile_id = US_CORE_OBSERVATION_CODE_PROFILE_IDS.find do |code, _profile_id|
443
- codeable_concept_has_code?(resource.code, code, system: LOINC_SYSTEM)
635
+ code_profile_id = US_CORE_OBSERVATION_CODE_PROFILE_IDS.find do |code, profile_id|
636
+ us_core_profile_available?(profile_id) &&
637
+ codeable_concept_has_code?(resource.code, code, system: LOINC_SYSTEM)
444
638
  end&.last
445
639
  return [code_profile_id] if code_profile_id.present?
446
640
 
@@ -463,7 +657,16 @@ module DaVinciPASTestKit
463
657
  end
464
658
 
465
659
  def us_core_profile_urls(profile_ids)
466
- Array(profile_ids).map { |profile_id| "#{US_CORE_PROFILE_BASE}/#{profile_id}|#{US_CORE_VERSION}" }
660
+ Array(profile_ids).map { |profile_id| "#{US_CORE_PROFILE_BASE}/#{profile_id}|#{us_core_version}" }
661
+ end
662
+
663
+ # Whether the selected US Core version defines the profile.
664
+ def us_core_profile_available?(profile_id)
665
+ # The only profile a later supported version dropped; see profile_url_for_validation.
666
+ return us_core_version == PASConstants::US_CORE_VERSION_3_1_1 if profile_id == US_CORE_CONDITION_PROFILE_ID
667
+
668
+ added_in = US_CORE_PROFILE_IDS_ADDED_IN_VERSION.find { |_, profile_ids| profile_ids.include?(profile_id) }&.first
669
+ added_in.nil? || Gem::Version.new(us_core_version) >= Gem::Version.new(added_in)
467
670
  end
468
671
 
469
672
  def profile_url_without_version(url)
@@ -492,13 +695,29 @@ module DaVinciPASTestKit
492
695
  # @param resource [FHIR::Model] The resource to validate.
493
696
  # @param profile_url [String, nil] The profile URL (with version) to validate against,
494
697
  # or nil to validate against the base resource definition.
698
+ # @param validator [Symbol] The name of the FHIR resource validator to use.
699
+ # @param echoed_extensions [Array, nil] Extensions of the request resource the resource echoes, or
700
+ # nil if it doesn't echo one - see entry_validation_settings and echoed_extension_issue_messages.
495
701
  # @return [Array<Hash>] Message hashes for the issues found.
496
- def collect_resource_profile_messages(resource, profile_url = nil)
702
+ def collect_resource_profile_messages(resource, profile_url = nil, validator: profile_validator,
703
+ echoed_extensions: nil)
497
704
  response_details = []
498
- resource_is_valid?(resource:, profile_url:,
705
+ resource_is_valid?(resource:, profile_url:, validator:,
499
706
  add_messages_to_runnable: false,
500
707
  validator_response_details: response_details)
501
- validator_issues_to_messages(response_details.reject(&:filtered))
708
+ issues = response_details.reject(&:filtered)
709
+ return validator_issues_to_messages(issues) if echoed_extensions.nil?
710
+
711
+ resource_hash = validated_hash(resource)
712
+ if validator == :default
713
+ # The :default validator allows every extension, so its only issues to drop are its notes that
714
+ # echoed extensions are unknown, which the echoed resource message already covers.
715
+ return validator_issues_to_messages(issues.reject do |issue|
716
+ echoed_unknown_extension_note?(issue, resource_hash, echoed_extensions)
717
+ end)
718
+ end
719
+
720
+ echoed_extension_issue_messages(issues, resource_hash, echoed_extensions)
502
721
  end
503
722
 
504
723
  # Same as collect_resource_profile_messages for a Bundle, additionally dropping
@@ -509,14 +728,21 @@ module DaVinciPASTestKit
509
728
  # @return [Array<Hash>] Message hashes for the Bundle-structural issues found.
510
729
  def collect_bundle_profile_messages(bundle, profile_url)
511
730
  response_details = []
512
- resource_is_valid?(resource: bundle, profile_url:,
731
+ resource_is_valid?(resource: bundle, profile_url:, validator: profile_validator,
513
732
  add_messages_to_runnable: false,
514
733
  validator_response_details: response_details)
515
734
  validator_issues_to_messages(reject_entry_resource_issues(response_details))
516
735
  end
517
736
 
518
737
  def validator_issues_to_messages(issues)
519
- issues.map { |issue| { type: issue.severity, message: issue.message } }
738
+ issues.map { |issue| validator_issue_message(issue) }
739
+ end
740
+
741
+ # @param details [String, nil] replaces the validator's text, keeping the location the message starts with.
742
+ def validator_issue_message(issue, details: nil)
743
+ message = issue.message
744
+ message = message.delete_suffix(issue.raw_issue['message'].to_s) + details if details.present?
745
+ { type: issue.severity, message: }
520
746
  end
521
747
 
522
748
  # Rejects validator issues that are already filtered or are located at/below
@@ -710,29 +936,6 @@ module DaVinciPASTestKit
710
936
  bundle_map[key]
711
937
  end
712
938
 
713
- def absolute_url(reference, base_url)
714
- return if reference.blank?
715
- return reference if base_url.blank? || reference.starts_with?('urn:uuid:') || URI(reference).absolute?
716
-
717
- "#{base_url}/#{reference}"
718
- end
719
-
720
- # Extracts the base URL from an absolute URL by removing the resource type and ID.
721
- # @param absolute_url [String] The absolute URL.
722
- # @return [String] The base URL, or an empty string if the URL format is not as expected.
723
- def extract_base_url(absolute_url)
724
- return '' if absolute_url.blank?
725
-
726
- uri = URI(absolute_url)
727
- return '' unless uri.scheme && uri.host
728
-
729
- # Split the path segments and remove the last two segments (resource type and id)
730
- path_segments = uri.path.split('/')
731
- base_path = path_segments[0...-2].join('/')
732
-
733
- "#{uri.scheme}://#{uri.host}#{base_path}"
734
- end
735
-
736
939
  # Resource Types to validate in request/ response bundle
737
940
  def find_profile_url(request_type)
738
941
  {
@@ -751,7 +954,7 @@ module DaVinciPASTestKit
751
954
 
752
955
  # Determines the target profile URL for a Claim resource in a submit request bundle.
753
956
  #
754
- # In v2.2.1, profile-pas-request-bundle permits either profile-claim or profile-claim-update.
957
+ # In PAS v2.2.1, profile-pas-request-bundle permits either profile-claim or profile-claim-update.
755
958
  # The structural discriminator is Claim.related: profile-claim-update requires it (must-support)
756
959
  # while profile-claim disallows it (max: 0). For all other versions, profile-claim-update is
757
960
  # used unconditionally.
@@ -785,15 +988,15 @@ module DaVinciPASTestKit
785
988
  is_unique = sequences.uniq.length == sequences.length
786
989
  return if is_unique
787
990
 
788
- validation_error_messages << "[Claim/#{claim.id}]: The sequence element for each supportingInfo entry SHALL be " \
789
- 'unique within the Claim.'
991
+ add_validation_error("[Claim/#{claim.id}]: The sequence element for each supportingInfo entry SHALL be " \
992
+ 'unique within the Claim.')
790
993
  end
791
994
 
792
995
  def validate_bundle_entries_full_url(bundle)
793
996
  msg = "[Bundle/#{bundle.id}]: Bundle.entry.fullUrl values SHALL be a valid url or in the form " \
794
997
  "'urn:uuid:[some guid]'."
795
998
  bundle.entry.each do |entry|
796
- validation_error_messages << msg unless valid_url_or_urn_uuid?(entry.fullUrl)
999
+ add_validation_error(msg) unless valid_url_or_urn_uuid?(entry.fullUrl)
797
1000
  end
798
1001
  end
799
1002
 
@@ -807,119 +1010,6 @@ module DaVinciPASTestKit
807
1010
  string&.match?(url_regex) || string&.match?(urn_uuid_regex)
808
1011
  end
809
1012
 
810
- # This method traverses references within a FHIR resource, ensuring that referenced resources
811
- # are populated in the bundle. It also enforces that a referenced resource appears only once in the bundle,
812
- # as required by the PAS IG.
813
- # @param target_resource [FHIR::Model] The FHIR resource to traverse and validate.
814
- # @param base_url [String] The server base url.
815
- # @param resources_to_match [Array<FHIR:Bundle:Entry] The list of FHIR bundle entries to match references against.
816
- # @param skip_claim_related [Boolean] When true, a Claim's `related` element is not traversed. This is set
817
- # for any Claim reached by following a reference (i.e. a non-primary Claim in a Claim Update chain), whose
818
- # own Claim.related.claim (the grandparent) is deliberately omitted from the Bundle per spec-65/66. The
819
- # primary Claim passed in by the caller keeps `skip_claim_related: false`, so its parent reference - and the
820
- # parent's own referenced resources - are still checked.
821
- def check_presence_of_referenced_resources(target_resource, base_url, resources_to_match,
822
- skip_claim_related: false)
823
- return if target_resource.blank?
824
-
825
- if target_resource.is_a?(FHIR::Reference) && target_resource.reference.present?
826
- ref = target_resource.reference
827
- absolute_ref = absolute_url(ref, base_url)
828
- matching_resources = resources_to_match.find_all { |res| res.fullUrl == absolute_ref }
829
-
830
- if matching_resources.length != 1
831
- validation_error_messages << resource_shall_appear_once_message(absolute_ref,
832
- matching_resources.length)
833
- end
834
-
835
- if matching_resources.length.positive?
836
- # A resource reached by following a reference is an included resource, not the primary Claim
837
- # being validated; if it is itself a Claim Update, its referenced grandparent Claim is omitted.
838
- check_presence_of_referenced_resources(matching_resources.first.resource, base_url, resources_to_match,
839
- skip_claim_related: true)
840
- end
841
- else
842
- target_resource.source_hash.each_key do |attr|
843
- next if claim_response_request_attr?(target_resource, attr)
844
- next if skip_claim_related && claim_related_attr?(target_resource, attr)
845
-
846
- value = target_resource.send(attr.to_sym)
847
- if value.is_a?(FHIR::Model)
848
- check_presence_of_referenced_resources(value, base_url, resources_to_match, skip_claim_related:)
849
- elsif value.is_a?(Array) && value.all?(FHIR::Model)
850
- value.each do |elmt|
851
- check_presence_of_referenced_resources(elmt, base_url, resources_to_match, skip_claim_related:)
852
- end
853
- end
854
- end
855
- end
856
- end
857
-
858
- # ClaimResponse.request is a back-reference to the submitted Claim. The PAS IG response bundle
859
- # profile (profile-pas-response-bundle) has no required Claim entry slice, so the Claim need
860
- # not be present in the response bundle. Skipping here matches the identical guard in
861
- # ResponseGenerator#referenced_entities, which also skips ClaimResponse.request when
862
- # building mock response bundles.
863
- def claim_response_request_attr?(resource, attr)
864
- attr.to_s == 'request' &&
865
- resource.respond_to?(:resourceType) &&
866
- resource.resourceType == 'ClaimResponse'
867
- end
868
-
869
- # Claim.related.claim points to the Claim being updated. For a non-primary Claim in a Claim Update
870
- # chain (one reached by following a reference), that prior Claim is the grandparent, which spec-65/66
871
- # require to be omitted from the Bundle. Used with the `skip_claim_related` flag so the generic
872
- # reference-presence check does not flag the deliberately-omitted grandparent as missing.
873
- def claim_related_attr?(resource, attr)
874
- attr.to_s == 'related' &&
875
- resource.respond_to?(:resourceType) &&
876
- resource.resourceType == 'Claim'
877
- end
878
-
879
- # Extracts resources from a bundle while following "next" links.
880
- #
881
- # @param bundle [FHIR::Bundle] The initial FHIR bundle to extract resources from.
882
- # @param response [Object] The HTTP response object for the bundle retrieval.
883
- # @param reply_handler [Proc] A callback function to handle responses.
884
- # @param max_pages [Integer] The maximum number of pages to process.
885
- #
886
- # This method extracts resources from a FHIR bundle, following "next" links in the bundle
887
- # until the specified maximum number of pages is reached. It collects resources and
888
- # invokes the reply_handler for each response.
889
- def extract_resources_from_bundle(
890
- bundle: nil,
891
- response: nil,
892
- reply_handler: nil,
893
- max_pages: 20
894
- )
895
- page_count = 1
896
- resources = []
897
-
898
- until bundle.nil? || page_count == max_pages
899
- resources += bundle&.entry&.map { |entry| entry&.resource }
900
- next_bundle_link = bundle&.link&.find { |link| link.relation == 'next' }&.url
901
- reply_handler&.call(response)
902
-
903
- break if next_bundle_link.blank?
904
-
905
- page_count += 1
906
- end
907
-
908
- resources
909
- end
910
-
911
- # Generates a message for a resource that appears more than once in a bundle.
912
- #
913
- # @param reference_resource_type [String] The resource type being referenced.
914
- # @param reference_resource_id [String] The resource ID being referenced.
915
- # @param total_matches [Integer] The total number of matches found in the bundle.
916
- #
917
- # This method generates an error message when a referenced resource appears more than once
918
- # in a FHIR bundle, which is not allowed.
919
- def resource_shall_appear_once_message(absolute_ref, total_matches)
920
- " The referenced #{absolute_ref} resource SHALL appear exactly once in the Bundle, but found #{total_matches}."
921
- end
922
-
923
1013
  # Generates a message for a resource present in both the PA request and response bundles.
924
1014
  #
925
1015
  # @param resource [FHIR::Model] The resource present in both bundles.
@@ -930,5 +1020,115 @@ module DaVinciPASTestKit
930
1020
  "Resource #{resource.resourceType}/#{resource.id} is an entry in both the PA Request Bundle and the Response " \
931
1021
  'Bundle, but they do not have the same fullUrl or identifiers'
932
1022
  end
1023
+
1024
+ ###########################################################################
1025
+ # US Core Profile Constants
1026
+ ###########################################################################
1027
+
1028
+ US_CORE_PROFILE_BASE = 'http://hl7.org/fhir/us/core/StructureDefinition'
1029
+ BASE_R4_PROFILE = :base_r4
1030
+ CLAIM_ENCOUNTER_EXTENSION_URL = 'http://hl7.org/fhir/5.0/StructureDefinition/extension-Claim.encounter'
1031
+ LOINC_SYSTEM = 'http://loinc.org'
1032
+ TERMINOLOGY_CONDITION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/condition-category'
1033
+ OBSERVATION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/observation-category'
1034
+ DIAGNOSTIC_REPORT_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v2-0074'
1035
+
1036
+ US_CORE_SINGLE_PROFILE_IDS_BY_RESOURCE = {
1037
+ 'AllergyIntolerance' => 'us-core-allergyintolerance',
1038
+ 'CarePlan' => 'us-core-careplan',
1039
+ 'CareTeam' => 'us-core-careteam',
1040
+ 'Coverage' => 'us-core-coverage',
1041
+ 'Device' => 'us-core-implantable-device',
1042
+ 'DocumentReference' => 'us-core-documentreference',
1043
+ 'Encounter' => 'us-core-encounter',
1044
+ 'Goal' => 'us-core-goal',
1045
+ 'Immunization' => 'us-core-immunization',
1046
+ 'Location' => 'us-core-location',
1047
+ 'Medication' => 'us-core-medication',
1048
+ 'MedicationDispense' => 'us-core-medicationdispense',
1049
+ 'MedicationRequest' => 'us-core-medicationrequest',
1050
+ 'Organization' => 'us-core-organization',
1051
+ 'Patient' => 'us-core-patient',
1052
+ 'Practitioner' => 'us-core-practitioner',
1053
+ 'PractitionerRole' => 'us-core-practitionerrole',
1054
+ 'Procedure' => 'us-core-procedure',
1055
+ 'Provenance' => 'us-core-provenance',
1056
+ 'QuestionnaireResponse' => 'us-core-questionnaireresponse',
1057
+ 'RelatedPerson' => 'us-core-relatedperson',
1058
+ 'ServiceRequest' => 'us-core-servicerequest',
1059
+ 'Specimen' => 'us-core-specimen'
1060
+ }.freeze
1061
+
1062
+ US_CORE_CONDITION_PROFILE_ID = 'us-core-condition'
1063
+ US_CORE_CONDITION_ENCOUNTER_DIAGNOSIS_PROFILE_ID = 'us-core-condition-encounter-diagnosis'
1064
+ US_CORE_CONDITION_PROBLEMS_HEALTH_CONCERNS_PROFILE_ID = 'us-core-condition-problems-health-concerns'
1065
+ US_CORE_DIAGNOSTIC_REPORT_LAB_PROFILE_ID = 'us-core-diagnosticreport-lab'
1066
+ US_CORE_DIAGNOSTIC_REPORT_NOTE_PROFILE_ID = 'us-core-diagnosticreport-note'
1067
+ US_CORE_OBSERVATION_CLINICAL_RESULT_PROFILE_ID = 'us-core-observation-clinical-result'
1068
+ US_CORE_OBSERVATION_LAB_PROFILE_ID = 'us-core-observation-lab'
1069
+ US_CORE_OBSERVATION_SCREENING_ASSESSMENT_PROFILE_ID = 'us-core-observation-screening-assessment'
1070
+ US_CORE_SIMPLE_OBSERVATION_PROFILE_ID = 'us-core-simple-observation'
1071
+ US_CORE_SMOKING_STATUS_PROFILE_ID = 'us-core-smokingstatus'
1072
+ US_CORE_VITAL_SIGNS_PROFILE_ID = 'us-core-vital-signs'
1073
+
1074
+ US_CORE_OBSERVATION_CODE_PROFILE_IDS = {
1075
+ '11341-5' => 'us-core-observation-occupation',
1076
+ '86645-9' => 'us-core-observation-pregnancyintent',
1077
+ '82810-3' => 'us-core-observation-pregnancystatus',
1078
+ '76690-7' => 'us-core-observation-sexual-orientation',
1079
+ '95541-9' => 'us-core-care-experience-preference',
1080
+ '75773-2' => 'us-core-treatment-intervention-preference',
1081
+ '8289-1' => 'head-occipital-frontal-circumference-percentile',
1082
+ '59576-9' => 'pediatric-bmi-for-age',
1083
+ '77606-2' => 'pediatric-weight-for-height',
1084
+ '85354-9' => 'us-core-blood-pressure',
1085
+ '96607-7' => 'us-core-average-blood-pressure',
1086
+ '39156-5' => 'us-core-bmi',
1087
+ '8302-2' => 'us-core-body-height',
1088
+ '8310-5' => 'us-core-body-temperature',
1089
+ '29463-7' => 'us-core-body-weight',
1090
+ '9843-4' => 'us-core-head-circumference',
1091
+ '8867-4' => 'us-core-heart-rate',
1092
+ '59408-5' => 'us-core-pulse-oximetry',
1093
+ '2708-6' => 'us-core-pulse-oximetry',
1094
+ '9279-1' => 'us-core-respiratory-rate',
1095
+ '72166-2' => 'us-core-smokingstatus'
1096
+ }.freeze
1097
+
1098
+ # Fallback profiles that US Core 3.1.1 does not define, keyed by the earliest supported
1099
+ # version that does. Profiles not listed here exist in every supported version.
1100
+ US_CORE_PROFILE_IDS_ADDED_IN_VERSION = {
1101
+ PASConstants::US_CORE_VERSION_6_1_0 => [
1102
+ 'us-core-blood-pressure',
1103
+ 'us-core-bmi',
1104
+ 'us-core-body-height',
1105
+ 'us-core-body-temperature',
1106
+ 'us-core-body-weight',
1107
+ 'us-core-condition-encounter-diagnosis',
1108
+ 'us-core-condition-problems-health-concerns',
1109
+ 'us-core-coverage',
1110
+ 'us-core-head-circumference',
1111
+ 'us-core-heart-rate',
1112
+ 'us-core-medicationdispense',
1113
+ 'us-core-observation-clinical-result',
1114
+ 'us-core-observation-occupation',
1115
+ 'us-core-observation-pregnancyintent',
1116
+ 'us-core-observation-pregnancystatus',
1117
+ 'us-core-observation-screening-assessment',
1118
+ 'us-core-observation-sexual-orientation',
1119
+ 'us-core-questionnaireresponse',
1120
+ 'us-core-relatedperson',
1121
+ 'us-core-respiratory-rate',
1122
+ 'us-core-servicerequest',
1123
+ 'us-core-simple-observation',
1124
+ 'us-core-specimen',
1125
+ 'us-core-vital-signs'
1126
+ ],
1127
+ PASConstants::US_CORE_VERSION_7_0_0 => [
1128
+ 'us-core-average-blood-pressure',
1129
+ 'us-core-care-experience-preference',
1130
+ 'us-core-treatment-intervention-preference'
1131
+ ]
1132
+ }.freeze
933
1133
  end
934
1134
  end