davinci_pas_test_kit 0.15.1 → 0.15.2
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.
- checksums.yaml +4 -4
- data/config/presets/pas_client_example.json +2 -2
- data/config/presets/pas_client_v201_run_against_pas_server.json +2 -2
- data/config/presets/pas_client_v221_run_against_pas_server.json +3 -3
- data/config/presets/pas_ri.json +6 -6
- data/config/presets/pas_server_v201_run_against_pas_client.json.erb +6 -6
- data/config/presets/pas_server_v221_run_against_pas_client.json.erb +10 -10
- data/lib/davinci_pas_test_kit/client/abstract_gather_must_support_test.rb +2 -48
- data/lib/davinci_pas_test_kit/client/client_input_descriptions.rb +29 -0
- data/lib/davinci_pas_test_kit/client/endpoints/claim_endpoint.rb +25 -9
- data/lib/davinci_pas_test_kit/client/user_input_response.rb +25 -6
- data/lib/davinci_pas_test_kit/client/v2.0.1/client_suite.rb +1 -1
- data/lib/davinci_pas_test_kit/client/v2.2.1/client_suite.rb +1 -1
- data/lib/davinci_pas_test_kit/cross_suite/fhirpath_utils.rb +13 -3
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/beneficiary/metadata.yml +4 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/claim_inquiry/metadata.yml +15 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/claim_update/metadata.yml +17 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/claiminquiryresponse/metadata.yml +15 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/claimresponse/metadata.yml +15 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/communication_request/metadata.yml +4 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/coverage/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/device_request/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/encounter/metadata.yml +3 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/insurer/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/medication_request/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/metadata.yml +143 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/nutrition_order/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/pas_inquiry_request_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/pas_inquiry_response_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/pas_request_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/pas_response_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/practitioner/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/practitioner_role/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/requestor/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/service_request/metadata.yml +3 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/subscriber/metadata.yml +4 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.0.1/task/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/beneficiary/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/claim_inquiry/metadata.yml +23 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/claim_update/metadata.yml +31 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/claiminquiryresponse/metadata.yml +42 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/claimresponse/metadata.yml +42 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/communication_request/metadata.yml +4 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/coverage/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/device_request/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/encounter/metadata.yml +3 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/insurer/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/medication_request/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/metadata.yml +265 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/nutrition_order/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/pas_inquiry_request_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/pas_inquiry_response_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/pas_request_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/pas_response_bundle/metadata.yml +2 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/practitioner/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/practitioner_role/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/requestor/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/service_request/metadata.yml +3 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/subscriber/metadata.yml +1 -0
- data/lib/davinci_pas_test_kit/cross_suite/generated/v2.2.1/task/metadata.yml +4 -0
- data/lib/davinci_pas_test_kit/cross_suite/pas_bundle_validation.rb +97 -39
- data/lib/davinci_pas_test_kit/cross_suite/pas_constants.rb +6 -0
- data/lib/davinci_pas_test_kit/cross_suite/pas_datatype_constraints.rb +166 -0
- data/lib/davinci_pas_test_kit/cross_suite/response_selection_utils.rb +26 -10
- data/lib/davinci_pas_test_kit/cross_suite/validator_suppressions.rb +206 -146
- data/lib/davinci_pas_test_kit/docs/demo/PAS Client Suite Demonstration.postman_collection.json +4 -4
- data/lib/davinci_pas_test_kit/generator/must_support_test_generator.rb +6 -0
- data/lib/davinci_pas_test_kit/generator/pas_must_support_metadata_extractor.rb +1 -2
- data/lib/davinci_pas_test_kit/generator/server_must_support_group_generator.rb +23 -0
- data/lib/davinci_pas_test_kit/generator/server_suite_generator.rb +6 -0
- data/lib/davinci_pas_test_kit/generator/templates/must_support.rb.erb +3 -2
- data/lib/davinci_pas_test_kit/generator/templates/server_must_support_group.rb.erb +22 -0
- data/lib/davinci_pas_test_kit/generator/templates/server_suite.rb.erb +4 -5
- data/lib/davinci_pas_test_kit/generator/templates/use_case_group.rb.erb +3 -0
- data/lib/davinci_pas_test_kit/generator/use_case_group_generator.rb +14 -8
- data/lib/davinci_pas_test_kit/requirements/generated/davinci_pas_server_suite_v221_requirements_coverage.csv +28 -28
- data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_server_approval_use_case_group.rb +1 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_server_denial_use_case_group.rb +1 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.0.1/pas_server_pended_use_case_group.rb +1 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.0.1/server_suite.rb +4 -5
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/claiminquiryresponse/server_inquire_response_must_support_claiminquiryresponse_test.rb +2 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/claimresponse/server_submit_response_must_support_claimresponse_test.rb +2 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_approval_use_case_group.rb +2 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_denial_use_case_group.rb +2 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_must_support_group.rb +12 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/pas_server_pended_use_case_group.rb +5 -0
- data/lib/davinci_pas_test_kit/server/generated/v2.2.1/server_suite.rb +4 -5
- data/lib/davinci_pas_test_kit/server/server_response_bundle_validation_test.rb +14 -5
- data/lib/davinci_pas_test_kit/server/v2.2.1/claim_inquiry/server_claim_inquiry_response_tests.rb +89 -0
- data/lib/davinci_pas_test_kit/server/v2.2.1/pas_error_group.rb +2 -0
- data/lib/davinci_pas_test_kit/server/v2.2.1/pas_server_claim_inquiry_responses_group.rb +17 -0
- data/lib/davinci_pas_test_kit/server/v2.2.1/pas_server_no_custom_extensions_test.rb +241 -0
- data/lib/davinci_pas_test_kit/server/v2.2.1/pas_server_subscription_capability_statement_test.rb +40 -0
- data/lib/davinci_pas_test_kit/server/v2.2.1/pas_server_subscription_setup.rb +14 -9
- data/lib/davinci_pas_test_kit/server/v2.2.1/server_replaced_pa_inquire_reference_input_test.rb +63 -0
- data/lib/davinci_pas_test_kit/server/v2.2.1/server_replaced_pa_inquire_reference_test.rb +109 -0
- data/lib/davinci_pas_test_kit/version.rb +2 -2
- metadata +9 -2
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
:must_supports:
|
|
19
19
|
:extensions:
|
|
20
20
|
- :id: NutritionOrder.extension:coverage
|
|
21
|
+
:slice_name: coverage
|
|
21
22
|
:path: extension
|
|
22
23
|
:url: http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information
|
|
23
24
|
:modifier_extension: false
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
- :path: oralDiet.type
|
|
35
36
|
- :path: enteralFormula
|
|
36
37
|
- :path: enteralFormula.baseFormulaType
|
|
38
|
+
:recursive_elements: []
|
|
37
39
|
:mandatory_elements:
|
|
38
40
|
- NutritionOrder.status
|
|
39
41
|
- NutritionOrder.intent
|
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
:must_supports:
|
|
18
18
|
:extensions:
|
|
19
19
|
- :id: ServiceRequest.extension:serviceCodeEnd
|
|
20
|
+
:slice_name: serviceCodeEnd
|
|
20
21
|
:path: extension
|
|
21
22
|
:url: http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-productOrServiceCodeEnd
|
|
22
23
|
:modifier_extension: false
|
|
23
24
|
- :id: ServiceRequest.extension:coverage
|
|
25
|
+
:slice_name: coverage
|
|
24
26
|
:path: extension
|
|
25
27
|
:url: http://hl7.org/fhir/us/davinci-crd/StructureDefinition/ext-coverage-information
|
|
26
28
|
:modifier_extension: false
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
:target_profiles:
|
|
37
39
|
- http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-beneficiary|2.2.1
|
|
38
40
|
- :path: occurrence[x]
|
|
41
|
+
:recursive_elements: []
|
|
39
42
|
:mandatory_elements:
|
|
40
43
|
- ServiceRequest.status
|
|
41
44
|
- ServiceRequest.intent
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
:must_supports:
|
|
19
19
|
:extensions:
|
|
20
20
|
- :id: Task.input:AttachmentsNeeded.extension:paLineNumber
|
|
21
|
+
:slice_name: paLineNumber
|
|
21
22
|
:path: input.extension
|
|
22
23
|
:url: http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-serviceLineNumber
|
|
23
24
|
:modifier_extension: false
|
|
24
25
|
- :id: Task.input:AttachmentsNeeded.extension:contentModifier
|
|
26
|
+
:slice_name: contentModifier
|
|
25
27
|
:path: input.extension
|
|
26
28
|
:url: http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-contentModifier
|
|
27
29
|
:modifier_extension: false
|
|
28
30
|
- :id: Task.input:QuestionnaireContext.extension:paLineNumber
|
|
31
|
+
:slice_name: paLineNumber
|
|
29
32
|
:path: input.extension
|
|
30
33
|
:url: http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-serviceLineNumber
|
|
31
34
|
:modifier_extension: false
|
|
@@ -86,6 +89,7 @@
|
|
|
86
89
|
- :path: input:AttachmentsNeeded.value[x]
|
|
87
90
|
- :path: input:QuestionnaireContext.type
|
|
88
91
|
- :path: input:QuestionnaireContext.value[x]
|
|
92
|
+
:recursive_elements: []
|
|
89
93
|
:mandatory_elements:
|
|
90
94
|
- Task.identifier
|
|
91
95
|
- Task.status
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
require_relative '../parameters_helper'
|
|
4
4
|
require_relative 'validation_test'
|
|
5
5
|
require_relative 'pas_constants'
|
|
6
|
+
require_relative 'pas_datatype_constraints'
|
|
6
7
|
|
|
7
8
|
module DaVinciPASTestKit
|
|
8
9
|
module PasBundleValidation
|
|
9
10
|
include DaVinciPASTestKit::ValidationTest
|
|
11
|
+
include DaVinciPASTestKit::PasDatatypeConstraints
|
|
10
12
|
include ParametersHelper
|
|
11
13
|
|
|
12
14
|
US_CORE_VERSION = '6.1.0'
|
|
@@ -80,13 +82,13 @@ module DaVinciPASTestKit
|
|
|
80
82
|
@validation_error_messages ||= []
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
def perform_bundle_validation(bundle, operation, type, ig_version)
|
|
85
|
+
def perform_bundle_validation(bundle, operation, type, ig_version, request_bundle = nil)
|
|
84
86
|
target_profile = PASConstants.bundle_profile_url_for_operation_and_type(operation, type)
|
|
85
87
|
request_type = "#{operation}_#{type}"
|
|
86
88
|
if type == 'request'
|
|
87
89
|
perform_request_validation(bundle, target_profile, ig_version.delete_prefix('v'), request_type)
|
|
88
90
|
else
|
|
89
|
-
perform_response_validation(bundle, target_profile, ig_version.delete_prefix('v'), request_type)
|
|
91
|
+
perform_response_validation(bundle, target_profile, ig_version.delete_prefix('v'), request_type, request_bundle)
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
|
|
@@ -96,7 +98,7 @@ module DaVinciPASTestKit
|
|
|
96
98
|
end
|
|
97
99
|
|
|
98
100
|
def perform_response_validation(response_bundle, profile_url, version, request_type, request_bundle = nil)
|
|
99
|
-
validate_pa_response_body_structure(response_bundle, request_bundle) if request_type
|
|
101
|
+
validate_pa_response_body_structure(response_bundle, request_bundle) if request_type.start_with?('submit')
|
|
100
102
|
validate_resources_conformance_against_profile(response_bundle, profile_url, version, request_type)
|
|
101
103
|
end
|
|
102
104
|
|
|
@@ -203,7 +205,7 @@ module DaVinciPASTestKit
|
|
|
203
205
|
# Validates the response body structure of a Prior Authorization (PA) response.
|
|
204
206
|
#
|
|
205
207
|
# @param pa_response_bundle [FHIR::Bundle] The FHIR bundle representing the PA response.
|
|
206
|
-
# @param pa_request_bundle [String] The
|
|
208
|
+
# @param pa_request_bundle [FHIR::Bundle, String, nil] The submitted PA request Bundle, either parsed or JSON.
|
|
207
209
|
#
|
|
208
210
|
# This method performs validation of the PA response bundle structure.
|
|
209
211
|
# It follows the PAS IG requirement that the FHIR Bundle generated
|
|
@@ -214,7 +216,7 @@ module DaVinciPASTestKit
|
|
|
214
216
|
# a given combination of content. Resources echoed back from the request are validated
|
|
215
217
|
# to ensure the same fullUrl and resource identifiers as in the
|
|
216
218
|
# request are used.
|
|
217
|
-
def validate_pa_response_body_structure(pa_response_bundle,
|
|
219
|
+
def validate_pa_response_body_structure(pa_response_bundle, pa_request_bundle)
|
|
218
220
|
first_entry = pa_response_bundle.entry.first.resource
|
|
219
221
|
unless first_entry.is_a?(FHIR::ClaimResponse)
|
|
220
222
|
validation_error_messages <<
|
|
@@ -224,22 +226,52 @@ module DaVinciPASTestKit
|
|
|
224
226
|
base_url = extract_base_url(pa_response_bundle.entry.last&.fullUrl)
|
|
225
227
|
check_presence_of_referenced_resources(first_entry, base_url, pa_response_bundle.entry)
|
|
226
228
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
validate_echoed_response_resources(pa_response_bundle, pa_request_bundle)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def validate_echoed_response_resources(pa_response_bundle, pa_request_bundle)
|
|
233
|
+
return if pa_request_bundle.blank?
|
|
234
|
+
|
|
235
|
+
request_bundle = pa_request_bundle.is_a?(FHIR::Bundle) ? pa_request_bundle : FHIR.from_contents(pa_request_bundle)
|
|
236
|
+
return unless request_bundle.is_a?(FHIR::Bundle)
|
|
237
|
+
|
|
238
|
+
pa_response_bundle.entry.each do |response_entry|
|
|
239
|
+
response_resource = response_entry.resource
|
|
240
|
+
next if response_resource.blank?
|
|
241
|
+
|
|
242
|
+
request_entry = request_bundle.entry.find do |entry|
|
|
243
|
+
echoed_resource?(entry, response_entry)
|
|
244
|
+
end
|
|
245
|
+
next if request_entry.blank?
|
|
246
|
+
|
|
247
|
+
next if echoed_resource_identifiers_match?(request_entry, response_entry)
|
|
237
248
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
validation_error_messages << resource_present_in_pa_request_and_response_msg(response_resource)
|
|
250
|
+
end
|
|
251
|
+
rescue StandardError
|
|
252
|
+
validation_error_messages << 'Unable to compare PAS request and response Bundle resources for echoed identifiers.'
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def echoed_resource?(request_entry, response_entry)
|
|
256
|
+
request_resource = request_entry.resource
|
|
257
|
+
response_resource = response_entry.resource
|
|
258
|
+
return false if request_resource.blank? || response_resource.blank?
|
|
259
|
+
return false unless request_resource.resourceType == response_resource.resourceType
|
|
260
|
+
|
|
261
|
+
# True if any identifying field is present on both sides and matches, so we can later report if any
|
|
262
|
+
# do not have all matching identifiers. Blank fields do not identify echoed resources.
|
|
263
|
+
(request_entry.fullUrl.present? && request_entry.fullUrl == response_entry.fullUrl) ||
|
|
264
|
+
(request_resource.id.present? && request_resource.id == response_resource.id) ||
|
|
265
|
+
(request_resource.identifier.present? && request_resource.identifier == response_resource.identifier)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def echoed_resource_identifiers_match?(request_entry, response_entry)
|
|
269
|
+
request_resource = request_entry.resource
|
|
270
|
+
response_resource = response_entry.resource
|
|
271
|
+
|
|
272
|
+
request_entry.fullUrl == response_entry.fullUrl &&
|
|
273
|
+
request_resource.id == response_resource.id &&
|
|
274
|
+
request_resource.identifier == response_resource.identifier
|
|
243
275
|
end
|
|
244
276
|
|
|
245
277
|
# Profile conformance of Prior Authorization (PA) resources.
|
|
@@ -299,27 +331,36 @@ module DaVinciPASTestKit
|
|
|
299
331
|
end
|
|
300
332
|
|
|
301
333
|
# Validates bundle resource and each entry in the bundle against its target profiles.
|
|
302
|
-
#
|
|
303
|
-
#
|
|
334
|
+
# Validation messages are collected per profile rather than logged directly, so a
|
|
335
|
+
# 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.
|
|
304
337
|
# @param version [String] The version of the IG.
|
|
305
338
|
def validate_bundle_entries_against_profiles(version)
|
|
306
339
|
bundle_resources_target_profile_map.each do |key, item|
|
|
307
340
|
resource = item[:resource]
|
|
308
341
|
base_profile = FHIR::Definitions.resource_definition(resource.resourceType).url
|
|
342
|
+
messages_by_profile = {}
|
|
309
343
|
|
|
310
344
|
success_profile = item[:profile_urls].find do |url|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
profile_messages =
|
|
346
|
+
if key == 'bundle'
|
|
347
|
+
collect_bundle_profile_messages(resource, profile_url_for_validation(url, base_profile, version))
|
|
348
|
+
elsif url == BASE_R4_PROFILE
|
|
349
|
+
collect_resource_profile_messages(resource)
|
|
350
|
+
else
|
|
351
|
+
collect_resource_profile_messages(resource, profile_url_for_validation(url, base_profile, version)) +
|
|
352
|
+
datatype_constraint_messages(resource, profile_url_without_version(url), version)
|
|
353
|
+
end
|
|
354
|
+
messages_by_profile[url] = profile_messages
|
|
355
|
+
profile_messages.none? { |message| message[:type] == 'error' }
|
|
320
356
|
end
|
|
321
357
|
|
|
322
|
-
|
|
358
|
+
if success_profile
|
|
359
|
+
messages.concat(messages_by_profile[success_profile])
|
|
360
|
+
else
|
|
361
|
+
messages_by_profile.each_value { |profile_messages| messages.concat(profile_messages) }
|
|
362
|
+
validation_error_messages << generate_non_conformance_message(item)
|
|
363
|
+
end
|
|
323
364
|
end
|
|
324
365
|
end
|
|
325
366
|
|
|
@@ -446,19 +487,36 @@ module DaVinciPASTestKit
|
|
|
446
487
|
end
|
|
447
488
|
end
|
|
448
489
|
|
|
449
|
-
# Validates a
|
|
450
|
-
#
|
|
490
|
+
# Validates a resource against a profile without logging, returning the unfiltered
|
|
491
|
+
# issues as message hashes for the caller to log once profile selection is resolved.
|
|
492
|
+
# @param resource [FHIR::Model] The resource to validate.
|
|
493
|
+
# @param profile_url [String, nil] The profile URL (with version) to validate against,
|
|
494
|
+
# or nil to validate against the base resource definition.
|
|
495
|
+
# @return [Array<Hash>] Message hashes for the issues found.
|
|
496
|
+
def collect_resource_profile_messages(resource, profile_url = nil)
|
|
497
|
+
response_details = []
|
|
498
|
+
resource_is_valid?(resource:, profile_url:,
|
|
499
|
+
add_messages_to_runnable: false,
|
|
500
|
+
validator_response_details: response_details)
|
|
501
|
+
validator_issues_to_messages(response_details.reject(&:filtered))
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Same as collect_resource_profile_messages for a Bundle, additionally dropping
|
|
505
|
+
# entry-resource-level issues that are covered by individual resource validation
|
|
506
|
+
# to avoid duplicate messages.
|
|
451
507
|
# @param bundle [FHIR::Bundle] The Bundle resource to validate.
|
|
452
508
|
# @param profile_url [String] The profile URL (with version) to validate against.
|
|
453
|
-
# @return [
|
|
454
|
-
def
|
|
509
|
+
# @return [Array<Hash>] Message hashes for the Bundle-structural issues found.
|
|
510
|
+
def collect_bundle_profile_messages(bundle, profile_url)
|
|
455
511
|
response_details = []
|
|
456
512
|
resource_is_valid?(resource: bundle, profile_url:,
|
|
457
513
|
add_messages_to_runnable: false,
|
|
458
514
|
validator_response_details: response_details)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
515
|
+
validator_issues_to_messages(reject_entry_resource_issues(response_details))
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def validator_issues_to_messages(issues)
|
|
519
|
+
issues.map { |issue| { type: issue.severity, message: issue.message } }
|
|
462
520
|
end
|
|
463
521
|
|
|
464
522
|
# Rejects validator issues that are already filtered or are located at/below
|
|
@@ -5,6 +5,7 @@ module DaVinciPASTestKit
|
|
|
5
5
|
CLAIM_RESPONSE_PROFILE = 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claimresponse'.freeze
|
|
6
6
|
CLAIM_INQUIRY_PROFILE = 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claim-inquiry'.freeze
|
|
7
7
|
CLAIM_INQUIRY_RESPONSE_PROFILE = 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claiminquiryresponse'.freeze
|
|
8
|
+
DATA_ABSENT_REASON_EXTENSION_URL = 'http://hl7.org/fhir/StructureDefinition/data-absent-reason'.freeze
|
|
8
9
|
|
|
9
10
|
BUNDLE_PROFILES_FOR_OPERATION_TYPE = {
|
|
10
11
|
submit_request: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-pas-request-bundle',
|
|
@@ -13,6 +14,11 @@ module DaVinciPASTestKit
|
|
|
13
14
|
inquire_response: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-pas-inquiry-response-bundle'
|
|
14
15
|
}.freeze
|
|
15
16
|
|
|
17
|
+
REFERENCE_NUMBER_EXTENSIONS = {
|
|
18
|
+
authorization_number: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-authorizationNumber',
|
|
19
|
+
administration_reference_number: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-administrationReferenceNumber'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
16
22
|
def self.bundle_profile_url_for_operation_and_type(operation, type)
|
|
17
23
|
BUNDLE_PROFILES_FOR_OPERATION_TYPE[:"#{operation}_#{type}"]
|
|
18
24
|
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require_relative 'pas_constants'
|
|
2
|
+
|
|
3
|
+
module DaVinciPASTestKit
|
|
4
|
+
# Checks for the narrative-only constraints on the PAS Timing and Quantity data type
|
|
5
|
+
# profiles introduced in v2.2 (prof-1, prof-2, prof-3). These constraints have no
|
|
6
|
+
# FHIRPath expression in the StructureDefinitions, so the FHIR validator cannot
|
|
7
|
+
# enforce them and they are verified here instead.
|
|
8
|
+
# See https://hl7.org/fhir/us/davinci-pas/STU2.2/StructureDefinition-profile-datatype-timing.html
|
|
9
|
+
# and https://hl7.org/fhir/us/davinci-pas/STU2.2/StructureDefinition-profile-quantity.html
|
|
10
|
+
module PasDatatypeConstraints
|
|
11
|
+
PAS_STRUCTURE_DEFINITION_BASE = 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition'.freeze
|
|
12
|
+
|
|
13
|
+
TIMING_CALENDAR_PATTERN_EXTENSION_URL = "#{PAS_STRUCTURE_DEFINITION_BASE}/extension-timingcalendarpattern".freeze
|
|
14
|
+
TIMING_DELIVERY_PATTERN_EXTENSION_URL = "#{PAS_STRUCTURE_DEFINITION_BASE}/extension-timingdeliverypattern".freeze
|
|
15
|
+
ITEM_AUTHORIZED_DETAIL_EXTENSION_URL = "#{PAS_STRUCTURE_DEFINITION_BASE}/extension-itemAuthorizedDetail".freeze
|
|
16
|
+
|
|
17
|
+
# Quantity.code is bound (required) to the X12 673 "quantity qualifier" value set,
|
|
18
|
+
# which the terminology validator cannot expand. Requiring this system when a code
|
|
19
|
+
# is present is the machine-checkable part of prof-3.
|
|
20
|
+
X12_QUANTITY_UNITS_SYSTEM = 'https://codesystem.x12.org/005010/673'.freeze
|
|
21
|
+
|
|
22
|
+
# Profiles that apply profile-datatype-timing to elements of their resource.
|
|
23
|
+
TIMING_CONSTRAINED_PROFILE_URLS = [
|
|
24
|
+
"#{PAS_STRUCTURE_DEFINITION_BASE}/profile-devicerequest",
|
|
25
|
+
"#{PAS_STRUCTURE_DEFINITION_BASE}/profile-medicationrequest",
|
|
26
|
+
"#{PAS_STRUCTURE_DEFINITION_BASE}/profile-servicerequest"
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
# Profiles that apply profile-quantity to elements of their resource, directly or
|
|
30
|
+
# inherited from profile-claim-base (Claim.item.quantity) and
|
|
31
|
+
# profile-claimresponse-base (ClaimResponse.addItem.quantity and the
|
|
32
|
+
# itemAuthorizedDetail quantity sub-extension).
|
|
33
|
+
QUANTITY_CONSTRAINED_PROFILE_URLS = [
|
|
34
|
+
PASConstants::CLAIM_PROFILE_FIRST_SUBMIT,
|
|
35
|
+
PASConstants::CLAIM_PROFILE,
|
|
36
|
+
PASConstants::CLAIM_INQUIRY_PROFILE,
|
|
37
|
+
PASConstants::CLAIM_RESPONSE_PROFILE,
|
|
38
|
+
PASConstants::CLAIM_INQUIRY_RESPONSE_PROFILE,
|
|
39
|
+
"#{PAS_STRUCTURE_DEFINITION_BASE}/profile-servicerequest"
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
# Evaluates the PAS data type profile constraints for a resource against a profile.
|
|
43
|
+
# @param resource [FHIR::Model] The resource being validated.
|
|
44
|
+
# @param profile_url [String] The canonical (unversioned) profile URL being validated against.
|
|
45
|
+
# @param version [String] The IG version.
|
|
46
|
+
# @return [Array<Hash>] Error message hashes for each constraint violation found.
|
|
47
|
+
def datatype_constraint_messages(resource, profile_url, version)
|
|
48
|
+
return [] unless pas_datatype_constraints_enabled?(version)
|
|
49
|
+
|
|
50
|
+
constraint_messages = []
|
|
51
|
+
if TIMING_CONSTRAINED_PROFILE_URLS.include?(profile_url)
|
|
52
|
+
profiled_timing_elements(resource).each do |timing, location|
|
|
53
|
+
constraint_messages << timing_prof1_message(resource, timing, location)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
if QUANTITY_CONSTRAINED_PROFILE_URLS.include?(profile_url)
|
|
57
|
+
profiled_quantity_elements(resource).each do |quantity, location|
|
|
58
|
+
constraint_messages << quantity_prof2_message(resource, quantity, location)
|
|
59
|
+
constraint_messages << quantity_prof3_message(resource, quantity, location)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
constraint_messages.compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The Timing and Quantity data type profiles were introduced in PAS v2.2.
|
|
66
|
+
def pas_datatype_constraints_enabled?(version)
|
|
67
|
+
version.to_s.delete_prefix('v').match?(/\A2\.2(\.|\z)/)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Timing instances the PAS profiles constrain, paired with their element locations.
|
|
73
|
+
def profiled_timing_elements(resource)
|
|
74
|
+
case resource.resourceType
|
|
75
|
+
when 'DeviceRequest', 'ServiceRequest'
|
|
76
|
+
timing = resource.occurrenceTiming
|
|
77
|
+
timing.present? ? [[timing, "#{resource.resourceType}.occurrenceTiming"]] : []
|
|
78
|
+
when 'MedicationRequest'
|
|
79
|
+
Array(resource.dosageInstruction).each_with_index.filter_map do |dosage, index|
|
|
80
|
+
[dosage.timing, "MedicationRequest.dosageInstruction[#{index}].timing"] if dosage.timing.present?
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Quantity instances the PAS profiles constrain, paired with their element locations.
|
|
88
|
+
def profiled_quantity_elements(resource)
|
|
89
|
+
case resource.resourceType
|
|
90
|
+
when 'Claim'
|
|
91
|
+
Array(resource.item).each_with_index.filter_map do |item, index|
|
|
92
|
+
[item.quantity, "Claim.item[#{index}].quantity"] if item.quantity.present?
|
|
93
|
+
end
|
|
94
|
+
when 'ClaimResponse'
|
|
95
|
+
claim_response_quantity_elements(resource)
|
|
96
|
+
when 'ServiceRequest'
|
|
97
|
+
quantity = resource.quantityQuantity
|
|
98
|
+
quantity.present? ? [[quantity, 'ServiceRequest.quantityQuantity']] : []
|
|
99
|
+
else
|
|
100
|
+
[]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def claim_response_quantity_elements(resource)
|
|
105
|
+
add_item_quantities = Array(resource.addItem).each_with_index.filter_map do |add_item, index|
|
|
106
|
+
[add_item.quantity, "ClaimResponse.addItem[#{index}].quantity"] if add_item.quantity.present?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
authorized_detail_quantities = Array(resource.item).each_with_index.flat_map do |item, index|
|
|
110
|
+
Array(item.extension)
|
|
111
|
+
.select { |extension| extension.url == ITEM_AUTHORIZED_DETAIL_EXTENSION_URL }
|
|
112
|
+
.filter_map do |extension|
|
|
113
|
+
quantity = Array(extension.extension).find { |sub| sub.url == 'quantity' }&.valueQuantity
|
|
114
|
+
if quantity.present?
|
|
115
|
+
[quantity, "ClaimResponse.item[#{index}].extension:authorizedItemDetail.extension:quantity.valueQuantity"]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
add_item_quantities + authorized_detail_quantities
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# prof-1: Timings SHALL have at least one of: a count, a frequency and period and
|
|
124
|
+
# periodUnit (with optional frequencyMax and periodMax), a calendarPattern, or a
|
|
125
|
+
# deliveryPattern.
|
|
126
|
+
def timing_prof1_message(resource, timing, location)
|
|
127
|
+
repeat = timing.repeat
|
|
128
|
+
return if repeat&.count.present?
|
|
129
|
+
return if repeat&.frequency.present? && repeat.period.present? && repeat.periodUnit.present?
|
|
130
|
+
return if extension_present?(timing, TIMING_CALENDAR_PATTERN_EXTENSION_URL)
|
|
131
|
+
return if extension_present?(timing, TIMING_DELIVERY_PATTERN_EXTENSION_URL)
|
|
132
|
+
|
|
133
|
+
datatype_constraint_message(resource, location,
|
|
134
|
+
'Timings SHALL have at least one of: a count, a frequency and period and ' \
|
|
135
|
+
'periodUnit (with optional frequencyMax and periodMax), a calendarPattern, ' \
|
|
136
|
+
'or a deliveryPattern (prof-1)')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# prof-2: Quantities SHALL have a value and either a unit or a code.
|
|
140
|
+
def quantity_prof2_message(resource, quantity, location)
|
|
141
|
+
return if quantity.value.present? && (quantity.unit.present? || quantity.code.present?)
|
|
142
|
+
|
|
143
|
+
datatype_constraint_message(resource, location,
|
|
144
|
+
'Quantities SHALL have a value and either a unit or a code (prof-2)')
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# prof-3: If a quantity code is present, it SHALL use the X12 quantity units.
|
|
148
|
+
def quantity_prof3_message(resource, quantity, location)
|
|
149
|
+
return if quantity.code.blank? || quantity.system == X12_QUANTITY_UNITS_SYSTEM
|
|
150
|
+
|
|
151
|
+
datatype_constraint_message(resource, location,
|
|
152
|
+
'If a quantity code is present, it SHALL use the X12 quantity units ' \
|
|
153
|
+
"(system #{X12_QUANTITY_UNITS_SYSTEM}) (prof-3)")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def extension_present?(element, url)
|
|
157
|
+
Array(element.extension).any? { |extension| extension.url == url }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Mirrors the "ResourceType/id: location: message" format of validator messages.
|
|
161
|
+
def datatype_constraint_message(resource, location, description)
|
|
162
|
+
resource_label = resource.id.present? ? "#{resource.resourceType}/#{resource.id}" : resource.resourceType
|
|
163
|
+
{ type: 'error', message: "#{resource_label}: #{location}: #{description}" }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -32,10 +32,10 @@ module DaVinciPASTestKit
|
|
|
32
32
|
criteria.is_a?(Hash) ? criteria : {}
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def include_entity?(entity, request_fhir_obj,
|
|
35
|
+
def include_entity?(entity, request_fhir_obj, request_number)
|
|
36
36
|
criteria = entity_criteria(entity)
|
|
37
37
|
return false if criteria[REQUEST_RANGE_KEY].present? &&
|
|
38
|
-
!
|
|
38
|
+
!ranges_cover_value?(request_number, criteria[REQUEST_RANGE_KEY].to_s)
|
|
39
39
|
return false if criteria[FHIRPATH_KEY].present? &&
|
|
40
40
|
!request_meets_inclusion_criteria?(criteria[FHIRPATH_KEY], request_fhir_obj)
|
|
41
41
|
|
|
@@ -55,26 +55,23 @@ module DaVinciPASTestKit
|
|
|
55
55
|
# Request index-based selection criteria
|
|
56
56
|
# ***********************************************************************
|
|
57
57
|
|
|
58
|
-
def request_meets_request_range_criteria?(ranges, operation)
|
|
59
|
-
ranges_cover_value?(count_previous_successful_requests(operation) + 1, ranges.to_s)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
58
|
def count_previous_successful_requests(operation)
|
|
63
59
|
requests_repo = Inferno::Repositories::Requests.new
|
|
64
60
|
previous_requests = requests_repo.requests_for_result(result.id)
|
|
65
61
|
previous_requests.count { |req| req.url.include?(operation) && req.status == 200 }
|
|
66
62
|
end
|
|
67
63
|
|
|
64
|
+
# An invalid range comes from tester input, so it is reported as a warning on the
|
|
65
|
+
# test and treated as unmet rather than raised, leaving the candidate unselected.
|
|
68
66
|
def ranges_cover_value?(value, ranges_string)
|
|
69
67
|
unless /\A(\d+(-\d+)?,)*\d+(-\d+)?\z/.match?(ranges_string)
|
|
70
|
-
raise ArgumentError,
|
|
71
|
-
"Invalid range string: #{ranges_string.inspect}"
|
|
68
|
+
raise ArgumentError, 'expected comma-separated numbers or ranges such as "1-2,4"'
|
|
72
69
|
end
|
|
73
70
|
|
|
74
71
|
ranges_string.split(',').any? do |part|
|
|
75
72
|
if part.include?('-')
|
|
76
73
|
low, high = part.split('-').map(&:to_i)
|
|
77
|
-
raise ArgumentError, "
|
|
74
|
+
raise ArgumentError, "#{part.inspect} is inverted" if low > high
|
|
78
75
|
|
|
79
76
|
(low..high).cover?(value)
|
|
80
77
|
else
|
|
@@ -82,7 +79,26 @@ module DaVinciPASTestKit
|
|
|
82
79
|
end
|
|
83
80
|
end
|
|
84
81
|
rescue ArgumentError => e
|
|
85
|
-
|
|
82
|
+
add_result_warning(
|
|
83
|
+
"Invalid requestRange criteria #{ranges_string.inspect}. The corresponding Bundle was not selected " \
|
|
84
|
+
"(#{e.message})."
|
|
85
|
+
)
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ***********************************************************************
|
|
90
|
+
# Tester-facing warnings
|
|
91
|
+
# ***********************************************************************
|
|
92
|
+
|
|
93
|
+
# Records a warning on the result of the waiting test, where the tester will see it,
|
|
94
|
+
# since problems with response selection stem from tester-provided input. The same
|
|
95
|
+
# input is evaluated for every incoming request, so a warning identical to one the
|
|
96
|
+
# result already has is not recorded again.
|
|
97
|
+
def add_result_warning(message)
|
|
98
|
+
messages_repo = Inferno::Repositories::Messages.new
|
|
99
|
+
return if messages_repo.messages_for_result(result.id).any? { |existing| existing.message == message }
|
|
100
|
+
|
|
101
|
+
messages_repo.create(result_id: result.id, type: 'warning', message:)
|
|
86
102
|
end
|
|
87
103
|
end
|
|
88
104
|
end
|