davinci_pas_test_kit 0.11.1 → 0.12.1
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 +30 -0
- data/config/presets/pas_ri.json +69 -0
- data/lib/davinci_pas_test_kit/client_suite.rb +28 -2
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_approval_submit_test.rb +29 -1
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_denial_submit_test.rb +27 -4
- data/lib/davinci_pas_test_kit/{generated/v2.0.1/client_tests/client_pended_pas_inquiry_request_bundle_validation_test.rb → custom_groups/v2.0.1/client_tests/pas_client_inquire_request_bundle_validation_test.rb} +22 -20
- data/lib/davinci_pas_test_kit/{generated/v2.0.1/client_tests/client_denial_pas_response_bundle_validation_test.rb → custom_groups/v2.0.1/client_tests/pas_client_inquire_response_bundle_validation_test.rb} +34 -21
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_pended_submit_test.rb +124 -5
- data/lib/davinci_pas_test_kit/{generated/v2.0.1/client_tests/client_pas_request_bundle_validation_test.rb → custom_groups/v2.0.1/client_tests/pas_client_request_bundle_validation_test.rb} +22 -20
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/{pas_client_approval_submit_response_attest.rb → pas_client_response_attest.rb} +26 -9
- data/lib/davinci_pas_test_kit/{generated/v2.0.1/client_tests/client_pended_pas_response_bundle_validation_test.rb → custom_groups/v2.0.1/client_tests/pas_client_response_bundle_validation_test.rb} +44 -20
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_subscription_create_test.rb +49 -0
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_subscription_pas_conformance_test.rb +48 -0
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/pas_client_approval_group.rb +21 -9
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/pas_client_authentication_group.rb +2 -2
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/pas_client_denial_group.rb +21 -22
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/pas_client_pended_group.rb +97 -31
- data/lib/davinci_pas_test_kit/docs/PAS Requirements Interpretation.xlsx +0 -0
- data/lib/davinci_pas_test_kit/docs/client_suite_description_v201.md +213 -72
- data/lib/davinci_pas_test_kit/endpoints/claim_endpoint.rb +85 -134
- data/lib/davinci_pas_test_kit/endpoints/subscription_create_endpoint.rb +96 -0
- data/lib/davinci_pas_test_kit/endpoints/subscription_status_endpoint.rb +90 -0
- data/lib/davinci_pas_test_kit/fhir_resource_navigation.rb +3 -3
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_inquiry_request_bundle/metadata.yml +0 -2
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_inquiry_request_bundle/server_pas_inquiry_request_bundle_validation_test.rb +3 -1
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_inquiry_response_bundle/server_pas_inquiry_response_bundle_validation_test.rb +2 -1
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_request_bundle/metadata.yml +0 -2
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_request_bundle/server_pas_request_bundle_validation_test.rb +3 -1
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_response_bundle/metadata.yml +0 -4
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_response_bundle/server_pas_response_bundle_validation_test.rb +2 -1
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_server_denial_use_case_group.rb +1 -1
- data/lib/davinci_pas_test_kit/generated/v2.0.1/pas_server_pended_use_case_group.rb +6 -5
- data/lib/davinci_pas_test_kit/generated/v2.0.1/server_suite.rb +5 -2
- data/lib/davinci_pas_test_kit/generator/group_generator.rb +9 -8
- data/lib/davinci_pas_test_kit/generator/group_metadata_extractor.rb +7 -3
- data/lib/davinci_pas_test_kit/generator/templates/group.rb.erb +129 -0
- data/lib/davinci_pas_test_kit/generator/templates/must_support.rb.erb +73 -0
- data/lib/davinci_pas_test_kit/generator/templates/operation.rb.erb +62 -0
- data/lib/davinci_pas_test_kit/generator/templates/resource_list.rb.erb +13 -0
- data/lib/davinci_pas_test_kit/generator/templates/suite.rb.erb +92 -0
- data/lib/davinci_pas_test_kit/generator/templates/validation.rb.erb +98 -0
- data/lib/davinci_pas_test_kit/generator/validation_test_generator.rb +19 -56
- data/lib/davinci_pas_test_kit/generator/value_extractor.rb +4 -1
- data/lib/davinci_pas_test_kit/generator.rb +1 -1
- data/lib/davinci_pas_test_kit/igs/davinci_pas_2.0.1.tgz +0 -0
- data/lib/davinci_pas_test_kit/jobs/send_pas_subscription_notification.rb +136 -0
- data/lib/davinci_pas_test_kit/jobs/send_subscription_handshake.rb +139 -0
- data/lib/davinci_pas_test_kit/metadata.rb +87 -0
- data/lib/davinci_pas_test_kit/pas_bundle_validation.rb +8 -7
- data/lib/davinci_pas_test_kit/response_generator.rb +397 -0
- data/lib/davinci_pas_test_kit/tags.rb +9 -0
- data/lib/davinci_pas_test_kit/urls.rb +8 -0
- data/lib/davinci_pas_test_kit/user_input_response.rb +11 -8
- data/lib/davinci_pas_test_kit/validation_test.rb +0 -1
- data/lib/davinci_pas_test_kit/version.rb +2 -1
- data/lib/davinci_pas_test_kit.rb +1 -0
- metadata +44 -15
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_denial_submit_response_attest.rb +0 -38
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_pended_inquire_response_attest.rb +0 -39
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_pended_inquire_test.rb +0 -35
- data/lib/davinci_pas_test_kit/custom_groups/v2.0.1/client_tests/pas_client_pended_submit_response_attest.rb +0 -39
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../tags'
|
4
|
+
require 'subscriptions_test_kit'
|
5
|
+
|
6
|
+
module DaVinciPASTestKit
|
7
|
+
module Jobs
|
8
|
+
class SendSubscriptionHandshake
|
9
|
+
include Sidekiq::Job
|
10
|
+
include SubscriptionsTestKit::SubscriptionsR5BackportR4Client::SubscriptionSimulationUtils
|
11
|
+
|
12
|
+
sidekiq_options retry: false
|
13
|
+
|
14
|
+
def perform(test_run_id, test_session_id, result_id, subscription_id, subscription_url, client_endpoint,
|
15
|
+
bearer_token, notification_json, test_run_identifier, test_suite_base_url)
|
16
|
+
@test_run_id = test_run_id
|
17
|
+
@test_session_id = test_session_id
|
18
|
+
@result_id = result_id
|
19
|
+
@subscription_id = subscription_id
|
20
|
+
@subscription_url = subscription_url
|
21
|
+
@client_endpoint = client_endpoint
|
22
|
+
@bearer_token = bearer_token
|
23
|
+
@notification_json = notification_json.present? ? notification_json : default_notification_base_json
|
24
|
+
@test_run_identifier = test_run_identifier
|
25
|
+
@test_suite_base_url = test_suite_base_url
|
26
|
+
|
27
|
+
await_subscription_creation
|
28
|
+
sleep 1
|
29
|
+
return unless test_still_waiting?
|
30
|
+
|
31
|
+
send_handshake_notification
|
32
|
+
test_suite_connection.get(RESUME_PASS_PATH.delete_prefix('/'), { token: @test_run_identifier })
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_notification_base_json
|
36
|
+
FHIR::Bundle.new(
|
37
|
+
timestamp: Time.now.utc.iso8601,
|
38
|
+
type: 'history',
|
39
|
+
entry: [
|
40
|
+
FHIR::Bundle::Entry.new(fullUrl: "urn:uuid:#{SecureRandom.uuid}",
|
41
|
+
resource: FHIR.from_contents(default_handshake_parameters_base_json))
|
42
|
+
]
|
43
|
+
).to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
def default_handshake_parameters_base_json
|
47
|
+
'{ "parameter": [ { "name": "subscription", "valueReference": { "reference": "replace_with_subscription_ref" } }, { "name": "topic", "valueCanonical": "replace_with_topic_canonical" }, { "name": "status", "valueCode": "requested" }, { "name": "type", "valueCode": "handshake" }, { "name": "events-since-subscription-start", "valueString": "0" } ], "resourceType": "Parameters" }' # rubocop:disable Layout/LineLength
|
48
|
+
end
|
49
|
+
|
50
|
+
def requests_repo
|
51
|
+
@requests_repo ||= Inferno::Repositories::Requests.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def results_repo
|
55
|
+
@results_repo ||= Inferno::Repositories::Results.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def subscription
|
59
|
+
@subscription ||= find_subscription(@test_session_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def headers
|
63
|
+
@headers ||= subscription_headers.merge(content_type_header).merge(authorization_header)
|
64
|
+
end
|
65
|
+
|
66
|
+
def rest_hook_connection
|
67
|
+
@rest_hook_connection ||= Faraday.new(url: @client_endpoint, request: { open_timeout: 30 }, headers:)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_suite_connection
|
71
|
+
@test_suite_connection ||= Faraday.new(@test_suite_base_url)
|
72
|
+
end
|
73
|
+
|
74
|
+
def content_type_header
|
75
|
+
@content_type_header ||= { 'Content-Type' => actual_mime_type(subscription) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def subscription_headers
|
79
|
+
return {} unless subscription.present?
|
80
|
+
|
81
|
+
@subscription_headers ||= subscription.channel&.header&.each_with_object({}) do |header, hash|
|
82
|
+
header_name, header_value = header.split(': ', 2)
|
83
|
+
hash[header_name] = header_value
|
84
|
+
end || {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def subscription_topic
|
88
|
+
@subscription_topic ||= subscription&.criteria
|
89
|
+
end
|
90
|
+
|
91
|
+
def authorization_header
|
92
|
+
@authorization_header ||= @bearer_token.present? ? { 'Authorization' => "Bearer #{@bearer_token}" } : {}
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_still_waiting?
|
96
|
+
results_repo.find_waiting_result(test_run_id: @test_run_id)
|
97
|
+
end
|
98
|
+
|
99
|
+
def await_subscription_creation
|
100
|
+
sleep 0.5 until subscription.present?
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_handshake_notification
|
104
|
+
handshake_json = derive_handshake_notification(@notification_json, @subscription_url,
|
105
|
+
subscription_topic).to_json
|
106
|
+
response = send_notification(handshake_json)
|
107
|
+
persist_notification_request(response, [REST_HOOK_HANDSHAKE_NOTIFICATION_TAG])
|
108
|
+
response
|
109
|
+
end
|
110
|
+
|
111
|
+
def send_notification(request_body)
|
112
|
+
rest_hook_connection.post('', request_body)
|
113
|
+
rescue Faraday::Error => e
|
114
|
+
# Warning: This is a hack. If there is an error with the request such that we never get a response, we have
|
115
|
+
# no clean way to persist that information for the Inferno test to check later. The solution here
|
116
|
+
# is to persist the request anyway with a status of nil, using the error message as response body
|
117
|
+
Faraday::Response.new(response_body: e.message, url: rest_hook_connection.url_prefix.to_s)
|
118
|
+
end
|
119
|
+
|
120
|
+
def persist_notification_request(response, tags)
|
121
|
+
inferno_request_headers = headers.map { |name, value| { name:, value: } }
|
122
|
+
inferno_response_headers = response.headers&.map { |name, value| { name:, value: } }
|
123
|
+
requests_repo.create(
|
124
|
+
verb: 'POST',
|
125
|
+
url: response.env.url.to_s,
|
126
|
+
direction: 'outgoing',
|
127
|
+
status: response.status,
|
128
|
+
request_body: response.env.request_body,
|
129
|
+
response_body: response.env.response_body,
|
130
|
+
test_session_id: @test_session_id,
|
131
|
+
result_id: @result_id,
|
132
|
+
request_headers: inferno_request_headers,
|
133
|
+
response_headers: inferno_response_headers,
|
134
|
+
tags:
|
135
|
+
)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'version'
|
2
|
+
|
3
|
+
module DaVinciPASTestKit
|
4
|
+
class Metadata < Inferno::TestKit
|
5
|
+
id :davinci_pas_test_kit
|
6
|
+
title 'Da Vinci Prior Authorization Support (PAS) Test Kit'
|
7
|
+
description <<~DESCRIPTION
|
8
|
+
The Da Vinci Prior Authorization Support (PAS) Test Kit validates
|
9
|
+
the conformance of both PAS client and server implementations to
|
10
|
+
[version 2.0.1 of the Da Vinci PAS Implementation
|
11
|
+
Guide](https://hl7.org/fhir/us/davinci-pas/STU2/).
|
12
|
+
|
13
|
+
<!-- break -->
|
14
|
+
|
15
|
+
To validate the behavior of a system Inferno will act as the partner to the
|
16
|
+
system under test:
|
17
|
+
- **When testing a client**: Inferno will act as a server, awaiting requests
|
18
|
+
from the client under test, returning appropriate responses, and validating
|
19
|
+
the conformance of the client's requests and its ability to handle the
|
20
|
+
responses appropriately.
|
21
|
+
- **When testing a server**: Inferno will act as a client, making requests
|
22
|
+
against the server under test and validating the conformance and
|
23
|
+
appropriateness of the server's responses.
|
24
|
+
|
25
|
+
The test suites for both PAS clients and servers follow the same basic outline,
|
26
|
+
each testing:
|
27
|
+
|
28
|
+
- The implementation's ability to use PAS-defined APIs to participate in the
|
29
|
+
submission of and decision on a prior authorization request, including:
|
30
|
+
- Approval of a prior authorization request.
|
31
|
+
- Denial of a prior authorization request.
|
32
|
+
- Pending of a prior authorization request and a subsequent final decision.
|
33
|
+
- The implementation ability to provide and handle data covering the full scope
|
34
|
+
of PAS must support requirements on:
|
35
|
+
- Prior authorization submissions.
|
36
|
+
- Prior authorization inquiries.
|
37
|
+
|
38
|
+
The Da Vinci PAS Test Kit is built using the [Inferno
|
39
|
+
Framework](https://inferno-framework.github.io/). The Inferno Framework is
|
40
|
+
designed for reuse and aims to make it easier to build test kits for any
|
41
|
+
FHIR-based data exchange.
|
42
|
+
|
43
|
+
## Known Limitations
|
44
|
+
|
45
|
+
The following areas of the IG are not fully tested in this draft version of the test kit:
|
46
|
+
|
47
|
+
- Private X12 details including value set expansions
|
48
|
+
- The use of Subscriptions to alert clients of updates to pended requests
|
49
|
+
- Prior Authorization update workflows
|
50
|
+
- Requests for additional information handled through the CDex framework
|
51
|
+
- PDF, CDA, and JPG attachments
|
52
|
+
- US Core profile support for supporting information
|
53
|
+
- Server inquiry matching and subsetting logic
|
54
|
+
- Server inquiry requests from non-submitting systems
|
55
|
+
- Server collection of metrics
|
56
|
+
- Client handling of responses containing all PAS-defined profiles and must support elements
|
57
|
+
- Client handling of situations that requiring manual review of the client system,
|
58
|
+
e.g., the requirement that clinicians can update details of the prior authorization
|
59
|
+
request before submitting them
|
60
|
+
|
61
|
+
For additional details on these and other areas where the tests may not align with
|
62
|
+
the IGs requirements, see documentation in the test kit source code ([client](https://github.com/inferno-framework/davinci-pas-test-kit/blob/main/lib/davinci_pas_test_kit/docs/client_suite_description_v201.md#testing-limitations), [server](https://github.com/inferno-framework/davinci-pas-test-kit/blob/main/lib/davinci_pas_test_kit/docs/server_suite_description_v201.md#testing-limitations)), and [this requirements analysis
|
63
|
+
spreadsheet](https://github.com/inferno-framework/davinci-pas-test-kit/blob/main/lib/davinci_pas_test_kit/docs/PAS%20Requirements%20Interpretation.xlsx).
|
64
|
+
|
65
|
+
### Known IG Issues
|
66
|
+
|
67
|
+
Through testing with this test kit, issues have been identified in the version of the PAS
|
68
|
+
specification that this test kit tests against which cause false failures. The full list
|
69
|
+
of known issues can be found on the [repository's issues page with the 'source ig issue'
|
70
|
+
lable](https://github.com/inferno-framework/davinci-pas-test-kit/labels/source%20ig%20issue).
|
71
|
+
|
72
|
+
## Reporting Issues
|
73
|
+
|
74
|
+
Please report any issues with this set of tests in the [GitHub
|
75
|
+
Issues](https://github.com/inferno-framework/davinci-pas-test-kit/issues)
|
76
|
+
section of the
|
77
|
+
[open source code repository](https://github.com/inferno-framework/davinci-pas-test-kit).
|
78
|
+
DESCRIPTION
|
79
|
+
suite_ids [:davinci_pas_server_suite_v201, :davinci_pas_client_suite_v201]
|
80
|
+
tags ['Da Vinci', 'PAS']
|
81
|
+
last_updated LAST_UPDATED
|
82
|
+
version VERSION
|
83
|
+
maturity 'Low'
|
84
|
+
authors ['Inferno Team']
|
85
|
+
repo 'https://github.com/inferno-framework/davinci-pas-test-kit'
|
86
|
+
end
|
87
|
+
end
|
@@ -171,7 +171,7 @@ module DaVinciPASTestKit
|
|
171
171
|
bundle_entry = bundle.entry
|
172
172
|
|
173
173
|
root_entry = bundle_entry.find do |entry|
|
174
|
-
|
174
|
+
['Claim', 'ClaimResponse'].include?(entry.resource.resourceType)
|
175
175
|
end
|
176
176
|
|
177
177
|
if root_entry.present?
|
@@ -331,7 +331,9 @@ module DaVinciPASTestKit
|
|
331
331
|
# @param bundle_entry [Array] The bundle.entry contents.
|
332
332
|
# @param version [String] The IG version.
|
333
333
|
def add_declared_profiles(instance, bundle_entry, version)
|
334
|
-
instance.resource
|
334
|
+
return unless instance.resource.present?
|
335
|
+
|
336
|
+
instance.resource.meta&.profile&.each do |url|
|
335
337
|
next if bundle_resources_target_profile_map[instance.fullUrl][:profile_urls].include?(url)
|
336
338
|
|
337
339
|
bundle_resources_target_profile_map[instance.fullUrl][:profile_urls] << url
|
@@ -462,11 +464,10 @@ module DaVinciPASTestKit
|
|
462
464
|
return if ref.blank?
|
463
465
|
|
464
466
|
absolute_ref = absolute_url(ref, base_url)
|
465
|
-
resource_type, resource_id = ref.split('/')
|
466
467
|
matching_resources = resources_to_match.find_all { |res| res.fullUrl == absolute_ref }
|
467
468
|
|
468
469
|
if matching_resources.length != 1
|
469
|
-
validation_error_messages << resource_shall_appear_once_message(
|
470
|
+
validation_error_messages << resource_shall_appear_once_message(absolute_ref,
|
470
471
|
matching_resources.length)
|
471
472
|
end
|
472
473
|
|
@@ -525,10 +526,10 @@ module DaVinciPASTestKit
|
|
525
526
|
#
|
526
527
|
# This method generates an error message when a referenced resource appears more than once
|
527
528
|
# in a FHIR bundle, which is not allowed.
|
528
|
-
def resource_shall_appear_once_message(
|
529
|
+
def resource_shall_appear_once_message(absolute_ref, total_matches)
|
529
530
|
"
|
530
|
-
The referenced #{
|
531
|
-
SHALL
|
531
|
+
The referenced #{absolute_ref} resource
|
532
|
+
SHALL appear exactly once in the Bundle, but found #{total_matches}.
|
532
533
|
"
|
533
534
|
end
|
534
535
|
|
@@ -0,0 +1,397 @@
|
|
1
|
+
module DaVinciPASTestKit
|
2
|
+
module ResponseGenerator
|
3
|
+
def mock_id_only_notification_bundle(submit_response, subscription_reference, subscription_topic)
|
4
|
+
notification_timestamp = Time.now.utc
|
5
|
+
mock_notification_bundle = build_mock_notification_bundle(notification_timestamp, subscription_reference,
|
6
|
+
subscription_topic, submit_response, 'id-only', nil)
|
7
|
+
mock_notification_bundle.to_json
|
8
|
+
end
|
9
|
+
|
10
|
+
def mock_full_resource_notification_bundle(submit_response, subscription_reference, subscription_topic, decision)
|
11
|
+
notification_timestamp = Time.now.utc
|
12
|
+
mock_notification_bundle = build_mock_notification_bundle(notification_timestamp, subscription_reference,
|
13
|
+
subscription_topic, submit_response, 'full-resource',
|
14
|
+
decision)
|
15
|
+
mock_notification_bundle.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
def mock_response_bundle(request_bundle, operation, decision, claim_response_uuid = nil)
|
19
|
+
mocked_timestamp = Time.now.utc
|
20
|
+
build_mock_response_bundle(request_bundle, operation, decision, mocked_timestamp, claim_response_uuid)&.to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
# update things that tester cannot get right themselves
|
24
|
+
# - timestamps on the Bundle and ClaimResponse (can't predict the processing time)
|
25
|
+
# - reference to the submitted Claim (may not have control of created id). NOTE: this is likely
|
26
|
+
# incomplete - when the Claim is included, there are other things that
|
27
|
+
# need to be in the Bundle that may also not be controlled
|
28
|
+
def update_tester_provided_response(user_inputted_response, claim_full_url)
|
29
|
+
response_bundle = FHIR.from_contents(user_inputted_response)
|
30
|
+
return user_inputted_response unless response_bundle.present?
|
31
|
+
|
32
|
+
now = Time.now.utc
|
33
|
+
response_bundle.timestamp = now.iso8601 if response_bundle&.timestamp.present?
|
34
|
+
claim_response_entry = response_bundle&.entry&.find { |e| e&.resource&.resourceType == 'ClaimResponse' }
|
35
|
+
if claim_response_entry.present?
|
36
|
+
claim_response_entry.resource.created = now.iso8601 if claim_response_entry.resource.created
|
37
|
+
if claim_response_entry.resource.request.present? && claim_full_url.present?
|
38
|
+
claim_response_entry.resource.request.reference = claim_full_url
|
39
|
+
end
|
40
|
+
end
|
41
|
+
response_bundle.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
# update things that tester cannot get right themselves
|
45
|
+
# - reference to the ClaimResponse in the case that it is generated by Inferno.
|
46
|
+
# If the tester provided the ClaimResponse, they are responsible for aligning
|
47
|
+
# the notification with it.
|
48
|
+
# - notification timestamps
|
49
|
+
def update_tester_provided_notification(user_inputted_notification, generated_claim_response_uuid)
|
50
|
+
now = Time.now.utc
|
51
|
+
|
52
|
+
notification_bundle = FHIR.from_contents(user_inputted_notification)
|
53
|
+
subscription_status_entry = notification_bundle&.entry&.find { |e| e.resource&.resourceType == 'Parameters' }
|
54
|
+
subscription_status_resource = subscription_status_entry&.resource
|
55
|
+
event_parameter = subscription_status_resource&.parameter&.find { |p| p.name == 'notification-event' }
|
56
|
+
|
57
|
+
if generated_claim_response_uuid.present?
|
58
|
+
claim_response_full_url = "urn:uuid:#{generated_claim_response_uuid}"
|
59
|
+
focus_part = event_parameter&.part&.find { |pt| pt.name == 'focus' }
|
60
|
+
if focus_part.present?
|
61
|
+
existing_claim_response_reference = focus_part.valueReference&.reference
|
62
|
+
focus_part.valueReference.reference = claim_response_full_url
|
63
|
+
update_tester_provided_notification_claim_response_entry(notification_bundle,
|
64
|
+
generated_claim_response_uuid,
|
65
|
+
claim_response_full_url,
|
66
|
+
existing_claim_response_reference)
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
timestamp_part = event_parameter&.part&.find { |pt| pt.name == 'timestamp' }
|
72
|
+
timestamp_part.valueInstant = now.iso8601 if timestamp_part.present?
|
73
|
+
notification_bundle.timestamp = now.iso8601 if notification_bundle.timestamp.present?
|
74
|
+
|
75
|
+
notification_bundle.to_json
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# if there's a ClaimResponse entry (full-resource or id-only without the resource contents)
|
81
|
+
# then update the fullUrl and id
|
82
|
+
def update_tester_provided_notification_claim_response_entry(notification_bundle, claim_response_id,
|
83
|
+
claim_response_full_url,
|
84
|
+
existing_claim_response_reference)
|
85
|
+
claim_response_entry = notification_bundle&.entry&.find { |e| e.resource&.resourceType == 'ClaimResponse' }
|
86
|
+
if claim_response_entry.blank? && existing_claim_response_reference.present?
|
87
|
+
claim_response_entry = notification_bundle&.entry&.find { |e| e.fullUrl == existing_claim_response_reference }
|
88
|
+
end
|
89
|
+
return unless claim_response_entry.present?
|
90
|
+
|
91
|
+
claim_response_entry.fullUrl = claim_response_full_url
|
92
|
+
return unless claim_response_entry.resource.present?
|
93
|
+
|
94
|
+
claim_response_entry.resource.id = claim_response_id
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_mock_notification_bundle(notification_timestamp, subscription_reference, subscription_topic,
|
98
|
+
submit_response, type, decision)
|
99
|
+
submit_bundle = FHIR.from_contents(submit_response)
|
100
|
+
claim_response_full_url = claim_response_full_url_from_submit_response_bundle(submit_bundle)
|
101
|
+
mock_notification_bundle = FHIR::Bundle.new(
|
102
|
+
id: SecureRandom.uuid,
|
103
|
+
timestamp: notification_timestamp.iso8601,
|
104
|
+
type: 'history'
|
105
|
+
)
|
106
|
+
|
107
|
+
additional_context_references =
|
108
|
+
if type == 'full-resource' && submit_bundle.present? && submit_bundle.is_a?(FHIR::Bundle)
|
109
|
+
submit_bundle.entry.reject { |entry| entry.resource&.resourceType == 'ClaimResponse' }.map(&:fullUrl).compact
|
110
|
+
else
|
111
|
+
[]
|
112
|
+
end
|
113
|
+
mock_notification_status = build_mock_notification_status(notification_timestamp, subscription_reference,
|
114
|
+
subscription_topic, claim_response_full_url,
|
115
|
+
additional_context_references)
|
116
|
+
mock_notification_bundle.entry << build_mock_notification_status_entry(mock_notification_status,
|
117
|
+
subscription_reference)
|
118
|
+
if type == 'full-resource' && submit_bundle.present? && submit_bundle.is_a?(FHIR::Bundle)
|
119
|
+
fhir_base_url = extract_fhir_base_url(subscription_reference)
|
120
|
+
submit_bundle.entry.each do |entry|
|
121
|
+
update_claim_response_decisions(entry.resource, decision) if entry.resource.resourceType == 'ClaimResponse'
|
122
|
+
entry.request = FHIR::Bundle::Entry::Request.new(
|
123
|
+
method: 'POST',
|
124
|
+
url: "#{fhir_base_url}/#{entry.resource.resourceType}"
|
125
|
+
)
|
126
|
+
entry.response = FHIR::Bundle::Entry::Response.new(
|
127
|
+
status: '201'
|
128
|
+
)
|
129
|
+
mock_notification_bundle.entry << entry
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
mock_notification_bundle
|
134
|
+
end
|
135
|
+
|
136
|
+
def update_claim_response_decisions(claim_response, decision)
|
137
|
+
claim_response&.item&.each do |item|
|
138
|
+
item.adjudication.each do |adjudication|
|
139
|
+
review_action_extension = adjudication.extension.find do |ext|
|
140
|
+
ext.url == 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewAction'
|
141
|
+
end
|
142
|
+
review_code_extension = review_action_extension&.extension&.find do |ext|
|
143
|
+
ext.url == 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewActionCode'
|
144
|
+
end
|
145
|
+
next unless review_code_extension.present?
|
146
|
+
|
147
|
+
review_code_extension.valueCodeableConcept = FHIR::CodeableConcept.new(
|
148
|
+
coding: [
|
149
|
+
get_review_action_code(decision)
|
150
|
+
]
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def build_mock_response_bundle(request_bundle, operation, decision, timestamp, claim_response_uuid = nil)
|
157
|
+
claim_entry = request_bundle&.entry&.find { |e| e&.resource&.resourceType == 'Claim' }
|
158
|
+
claim_full_url = claim_entry&.fullUrl
|
159
|
+
return nil if claim_entry.blank? || claim_full_url.blank?
|
160
|
+
|
161
|
+
root_url = extract_fhir_base_url(claim_full_url)
|
162
|
+
mocked_claim_response = build_mock_claim_response(claim_entry.resource, request_bundle, root_url, operation,
|
163
|
+
decision, timestamp, claim_response_uuid)
|
164
|
+
build_mock_bundle(mocked_claim_response, request_bundle, root_url, operation, timestamp)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Note that references from the claim to other resources in the bundle need to be changed to absolute URLs
|
168
|
+
# if they are relative, because the ClaimResponse's fullUrl is a urn:uuid
|
169
|
+
#
|
170
|
+
# @private
|
171
|
+
def build_mock_claim_response(claim, request_bundle, root_url, operation, decision, timestamp,
|
172
|
+
claim_response_uuid = nil)
|
173
|
+
claim_response_uuid = SecureRandom.uuid if claim_response_uuid.blank?
|
174
|
+
return FHIR::ClaimResponse.new(id: claim_response_uuid) if claim.blank?
|
175
|
+
|
176
|
+
FHIR::ClaimResponse.new(
|
177
|
+
id: claim_response_uuid,
|
178
|
+
meta: FHIR::Meta.new(profile: if operation == 'submit'
|
179
|
+
'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claimresponse'
|
180
|
+
else
|
181
|
+
'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claiminquiryresponse'
|
182
|
+
end),
|
183
|
+
identifier: claim.identifier,
|
184
|
+
type: claim.type,
|
185
|
+
status: claim.status,
|
186
|
+
use: claim.use,
|
187
|
+
patient: absolute_reference(claim.patient, request_bundle.entry, root_url),
|
188
|
+
created: timestamp.iso8601,
|
189
|
+
insurer: absolute_reference(claim.insurer, request_bundle.entry, root_url),
|
190
|
+
requestor: absolute_reference(claim.provider, request_bundle.entry, root_url),
|
191
|
+
outcome: 'complete',
|
192
|
+
item: claim.item.map do |item|
|
193
|
+
FHIR::ClaimResponse::Item.new(
|
194
|
+
extension: [
|
195
|
+
FHIR::Extension.new(
|
196
|
+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-itemPreAuthIssueDate',
|
197
|
+
valueDate: timestamp.strftime('%Y-%m-%d')
|
198
|
+
),
|
199
|
+
FHIR::Extension.new(
|
200
|
+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-itemPreAuthPeriod',
|
201
|
+
valuePeriod: FHIR::Period.new(start: timestamp.strftime('%Y-%m-%d'),
|
202
|
+
end: (timestamp + 1.month).strftime('%Y-%m-%d'))
|
203
|
+
)
|
204
|
+
],
|
205
|
+
itemSequence: item.sequence,
|
206
|
+
adjudication: [
|
207
|
+
FHIR::ClaimResponse::Item::Adjudication.new(
|
208
|
+
extension: [
|
209
|
+
FHIR::Extension.new(
|
210
|
+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewAction',
|
211
|
+
extension: [
|
212
|
+
FHIR::Extension.new(
|
213
|
+
url: 'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewActionCode',
|
214
|
+
valueCodeableConcept: FHIR::CodeableConcept.new(
|
215
|
+
coding: [
|
216
|
+
get_review_action_code(decision)
|
217
|
+
]
|
218
|
+
)
|
219
|
+
)
|
220
|
+
]
|
221
|
+
)
|
222
|
+
],
|
223
|
+
category: FHIR::CodeableConcept.new(
|
224
|
+
coding: [
|
225
|
+
FHIR::Coding.new(system: 'http://terminology.hl7.org/CodeSystem/adjudication', code: 'submitted')
|
226
|
+
]
|
227
|
+
)
|
228
|
+
)
|
229
|
+
]
|
230
|
+
)
|
231
|
+
end
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
def build_mock_bundle(claim_response, request_bundle, root_url, operation, timestamp)
|
236
|
+
response_bundle = FHIR::Bundle.new(
|
237
|
+
id: SecureRandom.uuid,
|
238
|
+
meta: FHIR::Meta.new(profile: if operation == 'submit'
|
239
|
+
'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-pas-response-bundle'
|
240
|
+
else
|
241
|
+
'http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-pas-inquiry-response-bundle'
|
242
|
+
end),
|
243
|
+
timestamp: timestamp.iso8601,
|
244
|
+
type: 'collection',
|
245
|
+
entry: [
|
246
|
+
FHIR::Bundle::Entry.new(fullUrl: "urn:uuid:#{claim_response.id}",
|
247
|
+
resource: claim_response)
|
248
|
+
]
|
249
|
+
)
|
250
|
+
response_bundle.entry.concat(referenced_entities(claim_response, request_bundle.entry, root_url))
|
251
|
+
response_bundle
|
252
|
+
end
|
253
|
+
|
254
|
+
def build_mock_notification_status_entry(status_parameters, subscription_reference)
|
255
|
+
FHIR::Bundle::Entry.new(fullUrl: "urn:uuid:#{SecureRandom.uuid}",
|
256
|
+
resource: status_parameters,
|
257
|
+
request: FHIR::Bundle::Entry::Request.new(
|
258
|
+
method: 'GET',
|
259
|
+
url: "#{subscription_reference}/$status"
|
260
|
+
),
|
261
|
+
response: FHIR::Bundle::Entry::Response.new(
|
262
|
+
status: '200'
|
263
|
+
))
|
264
|
+
end
|
265
|
+
|
266
|
+
def build_mock_notification_status(timestamp, subscription_reference, subscription_topic, claim_response_reference,
|
267
|
+
additional_context_references)
|
268
|
+
status_parameters = FHIR::Parameters.new
|
269
|
+
status_parameters.parameter << FHIR::Parameters::Parameter.new(
|
270
|
+
name: 'subscription',
|
271
|
+
valueReference: FHIR::Reference.new(
|
272
|
+
reference: subscription_reference
|
273
|
+
)
|
274
|
+
)
|
275
|
+
status_parameters.parameter << FHIR::Parameters::Parameter.new(
|
276
|
+
name: 'topic',
|
277
|
+
valueCanonical: subscription_topic
|
278
|
+
)
|
279
|
+
status_parameters.parameter << FHIR::Parameters::Parameter.new(
|
280
|
+
name: 'status',
|
281
|
+
valueCode: 'active'
|
282
|
+
)
|
283
|
+
status_parameters.parameter << FHIR::Parameters::Parameter.new(
|
284
|
+
name: 'type',
|
285
|
+
valueCode: 'event-notification'
|
286
|
+
)
|
287
|
+
status_parameters.parameter << FHIR::Parameters::Parameter.new(
|
288
|
+
name: 'events-since-subscription-start',
|
289
|
+
valueString: '1'
|
290
|
+
)
|
291
|
+
if claim_response_reference.present?
|
292
|
+
event = FHIR::Parameters::Parameter.new(
|
293
|
+
name: 'notification-event'
|
294
|
+
)
|
295
|
+
status_parameters.parameter << event
|
296
|
+
event.part << FHIR::Parameters::Parameter.new(
|
297
|
+
name: 'event-number',
|
298
|
+
valueString: '1'
|
299
|
+
)
|
300
|
+
event.part << FHIR::Parameters::Parameter.new(
|
301
|
+
name: 'timestamp',
|
302
|
+
valueInstant: timestamp.iso8601
|
303
|
+
)
|
304
|
+
event.part << FHIR::Parameters::Parameter.new(
|
305
|
+
name: 'focus',
|
306
|
+
valueReference: FHIR::Reference.new(
|
307
|
+
reference: claim_response_reference
|
308
|
+
)
|
309
|
+
)
|
310
|
+
additional_context_references&.each do |reference|
|
311
|
+
event.part << FHIR::Parameters::Parameter.new(
|
312
|
+
name: 'additional-context',
|
313
|
+
valueReference: FHIR::Reference.new(
|
314
|
+
reference:
|
315
|
+
)
|
316
|
+
)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
status_parameters
|
320
|
+
end
|
321
|
+
|
322
|
+
def get_review_action_code(decision)
|
323
|
+
case decision
|
324
|
+
when :denial
|
325
|
+
code = 'A3'
|
326
|
+
display = 'Not Certified'
|
327
|
+
when :pended
|
328
|
+
code = 'A4'
|
329
|
+
display = 'Pending'
|
330
|
+
else # approval or no workflow
|
331
|
+
code = 'A1'
|
332
|
+
display = 'Certified in total'
|
333
|
+
end
|
334
|
+
FHIR::Coding.new(
|
335
|
+
system: 'https://codesystem.x12.org/005010/306',
|
336
|
+
code:,
|
337
|
+
display:
|
338
|
+
)
|
339
|
+
end
|
340
|
+
|
341
|
+
def absolute_reference(ref, entries, root_url)
|
342
|
+
url = find_matching_entry(ref&.reference, entries, root_url)&.fullUrl
|
343
|
+
ref.reference = url if url
|
344
|
+
ref
|
345
|
+
end
|
346
|
+
|
347
|
+
def referenced_entities(resource, entries, root_url)
|
348
|
+
matches = []
|
349
|
+
attributes = resource&.source_hash&.keys
|
350
|
+
attributes.each do |attr|
|
351
|
+
value = resource.send(attr.to_sym)
|
352
|
+
if value.is_a?(FHIR::Reference) && value.reference.present?
|
353
|
+
match = find_matching_entry(value.reference, entries, root_url)
|
354
|
+
if match.present? && matches.none?(match)
|
355
|
+
value.reference = match.fullUrl
|
356
|
+
matches.concat([match], referenced_entities(match.resource, entries, root_url))
|
357
|
+
end
|
358
|
+
elsif value.is_a?(Array) && value.all? { |elmt| elmt.is_a?(FHIR::Model) }
|
359
|
+
value.each { |val| matches.concat(referenced_entities(val, entries, root_url)) }
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
matches
|
364
|
+
end
|
365
|
+
|
366
|
+
def find_matching_entry(ref, entries, root_url = '')
|
367
|
+
ref = "#{root_url}/#{ref}" if relative_reference?(ref) && root_url&.present?
|
368
|
+
|
369
|
+
entries&.find { |entry| entry&.fullUrl == ref }
|
370
|
+
end
|
371
|
+
|
372
|
+
def relative_reference?(ref)
|
373
|
+
ref&.count('/') == 1
|
374
|
+
end
|
375
|
+
|
376
|
+
# Drop the last two segments of a URL, i.e. the resource type and ID of a FHIR resource
|
377
|
+
# e.g. http://example.org/fhir/Patient/123 -> http://example.org/fhir
|
378
|
+
def extract_fhir_base_url(url)
|
379
|
+
return unless url&.start_with?('http://', 'https://')
|
380
|
+
|
381
|
+
# Drop everything after the second to last '/', ignoring a trailing slash
|
382
|
+
url.sub(%r{/[^/]*/[^/]*(/)?\z}, '')
|
383
|
+
end
|
384
|
+
|
385
|
+
def claim_response_full_url_from_submit_response_bundle(submit_bundle)
|
386
|
+
claim_response_entry = submit_bundle&.entry&.find { |e| e&.resource&.resourceType == 'ClaimResponse' }
|
387
|
+
|
388
|
+
if claim_response_entry.present?
|
389
|
+
return claim_response_entry.fullUrl unless claim_response_entry.fullUrl.blank?
|
390
|
+
unless claim_response_entry.resource.blank? || claim_response_entry.resource.id.blank?
|
391
|
+
return claim_response_entry.resource.id
|
392
|
+
end
|
393
|
+
end
|
394
|
+
"urn:uuid:#{SecureRandom.uuid}"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|