diff --git a/apps/accounts/fixtures/scopes.json b/apps/accounts/fixtures/scopes.json index b1a4759e3..df544f4a0 100644 --- a/apps/accounts/fixtures/scopes.json +++ b/apps/accounts/fixtures/scopes.json @@ -80,5 +80,29 @@ "protected_resources": "[[\"POST\", \"/v[12]/o/introspect\"]]", "default": "False" } + }, + { + "model": "capabilities.protectedcapability", + "pk": 7, + "fields": { + "title": "My Medicare partially adjudicated claims.", + "slug": "patient/Claim.read", + "group": 5, + "description": "Claim FHIR Resource", + "protected_resources": "[\n [\n \"POST\",\n \"/v2/fhir/Claim/_search$\"\n ],\n [\n \"GET\",\n \"/v2/fhir/Claim/(?P[^/]+)$\"\n ]\n]", + "default": "True" + } + }, + { + "model": "capabilities.protectedcapability", + "pk": 8, + "fields": { + "title": "My Medicare partially adjudicated claim responses.", + "slug": "patient/ClaimResponse.read", + "group": 5, + "description": "ClaimResponse FHIR Resource", + "protected_resources": "[\n [\n \"POST\",\n \"/v2/fhir/ClaimResponse/_search$\"\n ],\n [\n \"GET\",\n \"/v2/fhir/ClaimResponse/(?P[^/]+)$\"\n ]\n]", + "default": "True" + } } ] diff --git a/apps/authorization/permissions.py b/apps/authorization/permissions.py index 510c2a4d5..739d80753 100644 --- a/apps/authorization/permissions.py +++ b/apps/authorization/permissions.py @@ -32,10 +32,10 @@ def has_object_permission(self, request, view, obj): # Patient resources were taken care of above # Return 404 on error to avoid notifying unauthorized user the object exists - return is_resource_for_patient(obj, request.crosswalk.fhir_id) + return is_resource_for_patient(obj, request.crosswalk.fhir_id, request.crosswalk.user_mbi) -def is_resource_for_patient(obj, patient_id): +def is_resource_for_patient(obj, patient_id, user_mbi): try: if obj['resourceType'] == 'Coverage': reference = obj['beneficiary']['reference'] @@ -51,9 +51,15 @@ def is_resource_for_patient(obj, patient_id): reference_id = obj['id'] if reference_id != patient_id: raise exceptions.NotFound() + elif obj['resourceType'] == 'Claim': + if not _check_mbi(obj, user_mbi): + raise exceptions.NotFound() + elif obj['resourceType'] == 'ClaimResponse': + if not _check_mbi(obj, user_mbi): + raise exceptions.NotFound() elif obj['resourceType'] == 'Bundle': for entry in obj.get('entry', []): - is_resource_for_patient(entry['resource'], patient_id) + is_resource_for_patient(entry['resource'], patient_id, user_mbi) else: raise exceptions.NotFound() @@ -62,3 +68,22 @@ def is_resource_for_patient(obj, patient_id): except Exception: return False return True + + +# helper verify mbi of a claim or claim response resource +def _check_mbi(obj, mbi): + matched = False + try: + if obj['contained']: + for c in obj['contained']: + if c['resourceType'] == 'Patient': + identifiers = c['identifier'] + if len(identifiers) > 0: + if identifiers[0]['value'] == mbi: + matched = True + break + except KeyError as ke: + # log error and return false + print(ke) + pass + return matched diff --git a/apps/capabilities/management/commands/create_blue_button_scopes.py b/apps/capabilities/management/commands/create_blue_button_scopes.py index e984b2201..495cccebb 100644 --- a/apps/capabilities/management/commands/create_blue_button_scopes.py +++ b/apps/capabilities/management/commands/create_blue_button_scopes.py @@ -98,6 +98,42 @@ def create_coverage_capability(group, return c +def create_claim_capability(group, + fhir_prefix, + title="My Medicare partially adjudicated claims."): + c = None + description = "Claim FHIR Resource" + smart_scope_string = "patient/Claim.read" + pr = [] + pr.append(["GET", "%sClaim/" % fhir_prefix]) + pr.append(["GET", "%sClaim/[id]" % fhir_prefix]) + if not ProtectedCapability.objects.filter(slug=smart_scope_string).exists(): + c = ProtectedCapability.objects.create(group=group, + title=title, + description=description, + slug=smart_scope_string, + protected_resources=json.dumps(pr, indent=4)) + return c + + +def create_claimresponse_capability(group, + fhir_prefix, + title="My Medicare partially adjudicated claim responses."): + c = None + description = "ClaimResponse FHIR Resource" + smart_scope_string = "patient/ClaimResponse.read" + pr = [] + pr.append(["GET", "%sClaimResponse/" % fhir_prefix]) + pr.append(["GET", "%sClaimResponse/[id]" % fhir_prefix]) + if not ProtectedCapability.objects.filter(slug=smart_scope_string).exists(): + c = ProtectedCapability.objects.create(group=group, + title=title, + description=description, + slug=smart_scope_string, + protected_resources=json.dumps(pr, indent=4)) + return c + + class Command(BaseCommand): help = 'Create BlueButton Group and Scopes' diff --git a/apps/dot_ext/migrations/0009_internalapplicationlabelsproxy_and_more.py b/apps/dot_ext/migrations/0009_internalapplicationlabelsproxy_and_more.py new file mode 100644 index 000000000..0781d9ff3 --- /dev/null +++ b/apps/dot_ext/migrations/0009_internalapplicationlabelsproxy_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.17 on 2025-02-19 23:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dot_ext', '0008_internalapplicationlabels_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='InternalApplicationLabelsProxy', + fields=[ + ], + options={ + 'verbose_name': 'Internal Category', + 'verbose_name_plural': 'Internal Categories', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('dot_ext.internalapplicationlabels',), + ), + migrations.AlterField( + model_name='application', + name='internal_application_labels', + field=models.ManyToManyField(blank=True, to='dot_ext.internalapplicationlabels'), + ), + ] diff --git a/apps/fhir/bluebutton/constants.py b/apps/fhir/bluebutton/constants.py index 0900bd756..0c8a00fb9 100644 --- a/apps/fhir/bluebutton/constants.py +++ b/apps/fhir/bluebutton/constants.py @@ -1,3 +1,3 @@ -ALLOWED_RESOURCE_TYPES = ['Patient', 'Coverage', 'ExplanationOfBenefit'] +ALLOWED_RESOURCE_TYPES = ['Patient', 'Coverage', 'ExplanationOfBenefit', 'Claim', 'ClaimResponse'] DEFAULT_PAGE_SIZE = 10 MAX_PAGE_SIZE = 50 diff --git a/apps/fhir/bluebutton/migrations/0005_crosswalk__user_mbi.py b/apps/fhir/bluebutton/migrations/0005_crosswalk__user_mbi.py new file mode 100644 index 000000000..7553acd27 --- /dev/null +++ b/apps/fhir/bluebutton/migrations/0005_crosswalk__user_mbi.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-02-19 23:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bluebutton', '0004_createnewapplication_mycredentialingrequest'), + ] + + operations = [ + migrations.AddField( + model_name='crosswalk', + name='_user_mbi', + field=models.CharField(db_column='user_mbi', db_index=True, default=None, max_length=32, null=True, unique=True, verbose_name='User MBI ID'), + ), + ] diff --git a/apps/fhir/bluebutton/models.py b/apps/fhir/bluebutton/models.py index 70597eaca..8fc63ca97 100644 --- a/apps/fhir/bluebutton/models.py +++ b/apps/fhir/bluebutton/models.py @@ -120,6 +120,18 @@ class Crosswalk(models.Model): db_index=True, ) + # This stores the MBI value. + # Can be null for backwards migration compatibility. + _user_mbi = models.CharField( + max_length=32, + verbose_name="User MBI ID", + unique=True, + null=True, + default=None, + db_column="user_mbi", + db_index=True, + ) + objects = models.Manager() # Default manager real_objects = RealCrosswalkManager() # Real bene manager synth_objects = SynthCrosswalkManager() # Synth bene manager @@ -145,6 +157,10 @@ def user_hicn_hash(self): def user_mbi_hash(self): return self._user_mbi_hash + @property + def user_mbi(self): + return self._user_mbi + @user_hicn_hash.setter def user_hicn_hash(self, value): self._user_id_hash = value @@ -153,6 +169,10 @@ def user_hicn_hash(self, value): def user_mbi_hash(self, value): self._user_mbi_hash = value + @user_mbi.setter + def user_mbi(self, value): + self._user_mbi = value + class ArchivedCrosswalk(models.Model): """ diff --git a/apps/fhir/bluebutton/permissions.py b/apps/fhir/bluebutton/permissions.py index bdc45ab4a..a0c35e24d 100644 --- a/apps/fhir/bluebutton/permissions.py +++ b/apps/fhir/bluebutton/permissions.py @@ -52,6 +52,12 @@ def has_object_permission(self, request, view, obj): reference_id = reference.split("/")[1] if reference_id != request.crosswalk.fhir_id: raise exceptions.NotFound() + elif request.resource_type == "Claim": + if not _check_mbi(obj, request.crosswalk.user_mbi): + raise exceptions.NotFound() + elif request.resource_type == "ClaimResponse": + if not _check_mbi(obj, request.crosswalk.user_mbi): + raise exceptions.NotFound() else: reference_id = obj["id"] if reference_id != request.crosswalk.fhir_id: @@ -68,7 +74,6 @@ def has_object_permission(self, request, view, obj): class SearchCrosswalkPermission(HasCrosswalk): def has_object_permission(self, request, view, obj): patient_id = request.crosswalk.fhir_id - if "patient" in request.GET and request.GET["patient"] != patient_id: return False @@ -98,3 +103,22 @@ def has_permission(self, request, view): ) return True + + +# helper verify mbi of a claim or claim response resource +def _check_mbi(obj, mbi): + matched = False + try: + if obj['contained']: + for c in obj['contained']: + if c['resourceType'] == 'Patient': + identifiers = c['identifier'] + if len(identifiers) > 0: + if identifiers[0]['value'] == mbi: + matched = True + break + except KeyError as ke: + # log error and return false + print(ke) + pass + return matched diff --git a/apps/fhir/bluebutton/v2/urls.py b/apps/fhir/bluebutton/v2/urls.py index 2a0584387..f8a091b41 100755 --- a/apps/fhir/bluebutton/v2/urls.py +++ b/apps/fhir/bluebutton/v2/urls.py @@ -5,11 +5,15 @@ ReadViewCoverage, ReadViewExplanationOfBenefit, ReadViewPatient, + ReadViewClaim, + ReadViewClaimResponse, ) from apps.fhir.bluebutton.views.search import ( SearchViewCoverage, SearchViewExplanationOfBenefit, SearchViewPatient, + SearchViewClaim, + SearchViewClaimResponse, ) admin.autodiscover() @@ -51,4 +55,33 @@ SearchViewExplanationOfBenefit.as_view(version=2), name="bb_oauth_fhir_eob_search_v2", ), + # Claim SearchView + re_path( + r"Claim/_search$", + SearchViewClaim.as_view(version=2), + name="bb_oauth_fhir_claim_search", + ), + re_path( + r"ClaimJSON/_search$", + SearchViewClaim.as_view(version=2), + name="bb_oauth_fhir_claimjson_search", + ), + # Claim ReadView + re_path( + r"Claim/(?P[^/]+)", + ReadViewClaim.as_view(version=2), + name="bb_oauth_fhir_claim_read", + ), + # ClaimResponse SearchView + re_path( + r"ClaimResponse/_search$", + SearchViewClaimResponse.as_view(version=2), + name="bb_oauth_fhir_claimresponse_search", + ), + # ClaimResponse ReadView + re_path( + r"ClaimResponse/(?P[^/]+)", + ReadViewClaimResponse.as_view(version=2), + name="bb_oauth_fhir_claimresponse_read", + ), ] diff --git a/apps/fhir/bluebutton/views/generic.py b/apps/fhir/bluebutton/views/generic.py index 1ca0fe675..cdf2222c1 100644 --- a/apps/fhir/bluebutton/views/generic.py +++ b/apps/fhir/bluebutton/views/generic.py @@ -114,11 +114,17 @@ def initial(self, request, resource_type, *args, **kwargs): def get(self, request, resource_type, *args, **kwargs): - out_data = self.fetch_data(request, resource_type, *args, **kwargs) + out_data = self.fetch_data(request, False, resource_type, *args, **kwargs) return Response(out_data) - def fetch_data(self, request, resource_type, *args, **kwargs): + def post(self, request, resource_type, *args, **kwargs): + + out_data = self.fetch_data(request, True, resource_type, *args, **kwargs) + + return Response(out_data) + + def fetch_data(self, request, post_call, resource_type, *args, **kwargs): resource_router = get_resourcerouter(request.crosswalk) target_url = self.build_url(resource_router, @@ -132,15 +138,56 @@ def fetch_data(self, request, resource_type, *args, **kwargs): except voluptuous.error.Invalid as e: raise exceptions.ParseError(detail=e.msg) - logger.debug('Here is the URL to send, %s now add ' - 'GET parameters %s' % (target_url, get_parameters)) + logger.debug('Here is the URL to send, %s and here are the ' + 'parameters %s' % (target_url, get_parameters)) + + if post_call: + # hacky code prep parameters for the POST based search + # for POC: hard code some parameters: + + # isHashed: + # type: string + # description: |- + # Set this flag to false during POST request. Not setting the flag is defaulted to true + # Example: + # - `isHashed=true` + # example: false + # + # mbi required parameter - always obtain from crosswalk (current authorized user) + + # excludeSAMHSA + # description: |- + # The _Substance Abuse and Mental Health Services Administration_ (SAMHSA) + # is the agency within the U.S. Department of HHS that leads public health efforts to + # advance the behavioral health of the nation. + # Setting this flag to _true_, modifies the request to filter out all SAMSHA-related claims from the response. + + # Examples: + # - `excludeSAMHSA=true` + + payload = request.data if request.data else {} + # override mbi with user_mbi from corsswalk + # override samhsa to always exclude + # override includeTaxNumbers to always not to include + payload['mbi'] = request.crosswalk.user_mbi + payload['excludeSAMHSA'] = 'true' + payload['includeTaxNumbers'] = 'false' + payload['isHashed'] = 'false' + # chunk load the search result bundle to avoid large payload unless caller explicitly specified page size etc. + payload['startIndex'] = payload.get('startIndex', 0) + payload['_count'] = payload.get('_count', 10) + req = Request('POST', + target_url, + headers=backend_connection.headers(request, url=target_url), + data=payload) + else: + # Now make the call to the backend API + req = Request('GET', + target_url, + data=get_parameters, + params=get_parameters, + headers=backend_connection.headers(request, url=target_url)) - # Now make the call to the backend API - req = Request('GET', - target_url, - data=get_parameters, - params=get_parameters, - headers=backend_connection.headers(request, url=target_url)) s = Session() # BB2-1544 request header url encode if header value (app name) contains char (>256) @@ -151,6 +198,7 @@ def fetch_data(self, request, resource_type, *args, **kwargs): req.headers["BlueButton-Application"] = quote(req.headers.get("BlueButton-Application")) prepped = s.prepare_request(req) + # Send signal pre_fetch.send_robust(FhirDataView, request=req, auth_request=request, api_ver='v2' if self.version == 2 else 'v1') r = s.send( diff --git a/apps/fhir/bluebutton/views/read.py b/apps/fhir/bluebutton/views/read.py index 2a53206de..3710fd4ed 100644 --- a/apps/fhir/bluebutton/views/read.py +++ b/apps/fhir/bluebutton/views/read.py @@ -48,21 +48,30 @@ def build_url(self, resource_router, resource_type, resource_id, **kwargs): class ReadViewPatient(ReadView): - # Class used for Patient resource def __init__(self, version=1): super().__init__(version) self.resource_type = "Patient" class ReadViewCoverage(ReadView): - # Class used for Patient resource def __init__(self, version=1): super().__init__(version) self.resource_type = "Coverage" class ReadViewExplanationOfBenefit(ReadView): - # Class used for Patient resource def __init__(self, version=1): super().__init__(version) self.resource_type = "ExplanationOfBenefit" + + +class ReadViewClaim(ReadView): + def __init__(self, version=1): + super().__init__(version) + self.resource_type = "Claim" + + +class ReadViewClaimResponse(ReadView): + def __init__(self, version=1): + super().__init__(version) + self.resource_type = "ClaimResponse" diff --git a/apps/fhir/bluebutton/views/search.py b/apps/fhir/bluebutton/views/search.py index 8de3df3c8..058e4028c 100644 --- a/apps/fhir/bluebutton/views/search.py +++ b/apps/fhir/bluebutton/views/search.py @@ -52,6 +52,9 @@ def initial(self, request, *args, **kwargs): def get(self, request, *args, **kwargs): return super().get(request, self.resource_type, *args, **kwargs) + def post(self, request, *args, **kwargs): + return super().post(request, self.resource_type, *args, **kwargs) + def build_url(self, resource_router, resource_type, *args, **kwargs): if resource_router.fhir_url.endswith('v1/fhir/'): # only if called by tests @@ -146,3 +149,49 @@ def filter_parameters(self, request): getattr(self, "QUERY_SCHEMA", {}), extra=REMOVE_EXTRA) return schema(params) + + +class SearchViewClaim(SearchView): + http_method_names = ['post'] + + def __init__(self, version=1): + super().__init__(version) + self.resource_type = "Claim" + + def build_parameters(self, request, *args, **kwargs): + return { + '_format': 'application/json+fhir', + 'beneficiary': 'Patient/' + request.crosswalk.fhir_id, + } + + # hacky code for POC only + def build_url(self, resource_router, resource_type, *args, **kwargs): + if resource_router.fhir_url.endswith('v1/fhir/'): + # only if called by tests + return "{}{}/_search".format(resource_router.fhir_url, resource_type) + else: + return "{}/{}/fhir/{}/_search".format(resource_router.fhir_url, 'v2' if self.version == 2 else 'v1', + resource_type) + + +class SearchViewClaimResponse(SearchView): + http_method_names = ['post'] + + def __init__(self, version=1): + super().__init__(version) + self.resource_type = "ClaimResponse" + + def build_parameters(self, request, *args, **kwargs): + return { + '_format': 'application/json+fhir', + 'beneficiary': 'Patient/' + request.crosswalk.fhir_id, + } + + # hacky code for POC only + def build_url(self, resource_router, resource_type, *args, **kwargs): + if resource_router.fhir_url.endswith('v1/fhir/'): + # only if called by tests + return "{}{}/_search".format(resource_router.fhir_url, resource_type) + else: + return "{}/{}/fhir/{}/_search".format(resource_router.fhir_url, 'v2' if self.version == 2 else 'v1', + resource_type) diff --git a/apps/mymedicare_cb/models.py b/apps/mymedicare_cb/models.py index 43ad2a93d..946e135db 100644 --- a/apps/mymedicare_cb/models.py +++ b/apps/mymedicare_cb/models.py @@ -26,17 +26,6 @@ class BBMyMedicareCallbackCrosswalkUpdateException(APIException): def get_and_update_user(slsx_client: OAuth2ConfigSLSx, request=None): """ Find or create the user associated - with the identity information from the ID provider. - - Args: - Identity parameters passed in from ID provider. - slsx_client = OAuth2ConfigSLSx encapsulates all slsx exchanges and user info values as listed below: - subject = ID provider's sub or username - mbi_hash = Previously hashed mbi - hicn_hash = Previously hashed hicn - first_name - last_name - email request = request from caller to pass along for logging info. Returns: user = The user that was existing or newly created @@ -125,6 +114,7 @@ def get_and_update_user(slsx_client: OAuth2ConfigSLSx, request=None): user.crosswalk.user_id_type = hash_lookup_type user.crosswalk.user_hicn_hash = slsx_client.hicn_hash user.crosswalk.user_mbi_hash = slsx_client.mbi_hash + user.crosswalk.user_mbi = slsx_client.mbi user.crosswalk.save() # Beneficiary has been successfully matched! @@ -231,6 +221,7 @@ def create_beneficiary_record(slsx_client: OAuth2ConfigSLSx, fhir_id=None, user_ user=user, user_hicn_hash=slsx_client.hicn_hash, user_mbi_hash=slsx_client.mbi_hash, + user_mbi=slsx_client.mbi, fhir_id=fhir_id, user_id_type=user_id_type, ) diff --git a/apps/testclient/management/commands/create_test_user_and_application.py b/apps/testclient/management/commands/create_test_user_and_application.py index 5febcf40a..f38a11e8b 100644 --- a/apps/testclient/management/commands/create_test_user_and_application.py +++ b/apps/testclient/management/commands/create_test_user_and_application.py @@ -99,6 +99,8 @@ def create_application(user, group, app, redirect): titles = ["My Medicare and supplemental coverage information.", "My Medicare claim information.", "My general patient and demographic information.", + "My Medicare partially adjudicated claims.", + "My Medicare partially adjudicated claim responses.", "Profile information including name and email." ] diff --git a/apps/testclient/management/commands/create_test_users_and_applications_batch.py b/apps/testclient/management/commands/create_test_users_and_applications_batch.py index 43cbcac46..a180996ea 100755 --- a/apps/testclient/management/commands/create_test_users_and_applications_batch.py +++ b/apps/testclient/management/commands/create_test_users_and_applications_batch.py @@ -199,6 +199,8 @@ def create_dev_users_apps_and_bene_crosswalks( "My Medicare and supplemental coverage information.", "My Medicare claim information.", "My general patient and demographic information.", + "My Medicare partially adjudicated claims.", + "My Medicare partially adjudicated claim responses.", "Profile information including name and email." ] diff --git a/apps/testclient/templates/home.html b/apps/testclient/templates/home.html index d204a257f..b1d264c49 100644 --- a/apps/testclient/templates/home.html +++ b/apps/testclient/templates/home.html @@ -69,6 +69,8 @@

Step 1: Sample Authorization

{% url 'test_eob_v2' as test_eob_url %} {% url 'test_patient_v2' as test_patient_url %} {% url 'test_coverage_v2' as test_coverage_url %} + {% url 'test_claim' as test_claim_url %} + {% url 'test_claimresponse' as test_claimresponse_url %} {% else %} {% url 'authorize_link' as auth_url %} {% url 'test_metadata' as meta_url %} @@ -91,14 +93,27 @@

Step 2: API Calls

Once you've completed step one and have an authorization token, you can click on any of the links below to simulate calls to different endpoints and see the sample data that is delivered in the response.

- + {% if api_ver == 'v2' %} + + {% else %} + + {% endif %}

Additional Resources

diff --git a/apps/testclient/urls.py b/apps/testclient/urls.py index f3afc3009..f8f73a497 100644 --- a/apps/testclient/urls.py +++ b/apps/testclient/urls.py @@ -17,6 +17,8 @@ test_patient, test_patient_v2, test_links, + test_claim, + test_claimresponse, ) urlpatterns = [ @@ -35,6 +37,8 @@ path("PatientV2", test_patient_v2, name="test_patient_v2"), path("CoverageV2", test_coverage_v2, name="test_coverage_v2"), path("userinfoV2", test_userinfo_v2, name="test_userinfo_v2"), + path("ClaimV2", test_claim, name="test_claim"), + path("ClaimResponseV2", test_claimresponse, name="test_claimresponse"), path("metadataV2", test_metadata_v2, name="test_metadata_v2"), path("openidConfigV2", test_openid_config_v2, name="test_openid_config_v2"), ] diff --git a/apps/testclient/views.py b/apps/testclient/views.py index e659a427e..9ae70f639 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -10,6 +10,7 @@ from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import OAuth2Session from rest_framework import status +from urllib.parse import urlparse, parse_qs, urlunparse, urlencode from waffle.decorators import waffle_switch from .utils import test_setup, get_client_secret @@ -29,6 +30,8 @@ "patient": "{}/{}/fhir/Patient/{}?_format=json", "eob": "{}/{}/fhir/ExplanationOfBenefit/?_format=json", "coverage": "{}/{}/fhir/Coverage/?_format=json", + "claim": "{}/{}/fhir/Claim/_search", + "claimresponse": "{}/{}/fhir/ClaimResponse/_search", } @@ -37,18 +40,42 @@ def _get_page_loc(request, fhir_json): total = fhir_json.get('total', 0) - index = int(request.GET.get('startIndex', 0)) + + index = 0 + + if 'startIndex' not in request.GET and request.GET.get('nav_link'): + # hack here for quick POC + # for PACA claim and claim response pagination: + # some how startIndex ends up as part of nav_link, so hack it to extract startIndex + nav_link_parsed = urlparse(request.GET.get('nav_link')) + nav_qps = parse_qs(nav_link_parsed.query) + start_index = nav_qps.get('startIndex', 0) + if isinstance(start_index, list): + start_index = start_index[0] if start_index else 0 + index = int(start_index) + else: + index = int(request.GET.get('startIndex', 0)) + count = int(request.GET.get('_count', 10)) + return "{}/{}".format(index // count + 1, math.ceil(total / count)) def _extract_page_nav(request, fhir_json): + # strip off PHI if any that appears in url as q-param link = fhir_json.get('link', None) nav_list = [] if link is not None: for lnk in link: if lnk.get('url', None) is not None and lnk.get('relation', None) is not None: - nav_list.append({'relation': lnk['relation'], 'nav_link': lnk['url']}) + parsed_url = urlparse(lnk.get('url', None)) + query_params = parse_qs(parsed_url.query) + # for now check mbi (because BFD PACA Claim / ClaimResponse search result nav links contains mbi) + if 'mbi' in query_params: + del query_params['mbi'] + parsed_url = parsed_url._replace(query=urlencode(query_params, doseq=True)) + nav_link_regen = urlunparse(parsed_url) + nav_list.append({'relation': lnk['relation'], 'nav_link': nav_link_regen}) else: nav_list = [] break @@ -63,27 +90,49 @@ def _get_data_json(request, name, params): uri = ENDPOINT_URL_FMT[name].format(*params) - if nav_link is not None: - q_params = [uri] - q_params.append(request.GET.get('_count', 10)) - q_params.append(request.GET.get('startIndex', 0)) + resp = None + + if name == 'claim' or name == 'claimresponse': + # need to handle query parameters to body convert here for claim and claim response + qps = request.GET + nav_link = qps.get('nav_link') + # some how startIndex ends up as part of nav_link, so hack it to extract startIndex + nav_link_parsed = urlparse(nav_link) + nav_qps = parse_qs(nav_link_parsed.query) + start_index = nav_qps.get('startIndex', 0) + + if isinstance(start_index, list): + start_index = start_index[0] + + json = { + '_count': qps.get('_count', 10), + 'startIndex': start_index + } + resp = oas.post(uri, json=json) + else: + if nav_link is not None: + q_params = [uri] + q_params.append(request.GET.get('_count', 10)) + q_params.append(request.GET.get('startIndex', 0)) + + # for now it's either EOB or Coverage, make this more generic later + patient = request.GET.get('patient') - # for now it's either EOB or Coverage, make this more generic later - patient = request.GET.get('patient') + if patient is not None: + q_params.append('patient') + q_params.append(patient) - if patient is not None: - q_params.append('patient') - q_params.append(patient) + beneficiary = request.GET.get('beneficiary') - beneficiary = request.GET.get('beneficiary') + if beneficiary is not None: + q_params.append('beneficiary') + q_params.append(beneficiary) - if beneficiary is not None: - q_params.append('beneficiary') - q_params.append(beneficiary) + uri = NAV_URI_FMT.format(*q_params) - uri = NAV_URI_FMT.format(*q_params) + resp = oas.get(uri) - return oas.get(uri).json() + return resp.json() def _convert_to_json(json_response): @@ -323,6 +372,48 @@ def test_eob(request, version=1): "api_ver": "v2" if version == 2 else "v1"}) +@never_cache +@waffle_switch('enable_testclient') +def test_claim(request): + + if 'token' not in request.session: + return redirect('test_links', permanent=True) + + claims = _get_data_json(request, 'claim', [request.session['resource_uri'], 'v2']) + cnt = claims.get('total', 0) + nav_info = [] if cnt == 0 else _extract_page_nav(request, claims) + + return render(request, RESULTS_PAGE, + {"fhir_json_pretty": json.dumps(claims, indent=3), + "url_name": 'test_claim', + "nav_list": nav_info, "page_loc": _get_page_loc(request, claims), + "response_type": "Bundle of Claim", + "total_resource": claims.get('total', 0), + "api_ver": "v2"}) + + +@never_cache +@waffle_switch('enable_testclient') +def test_claimresponse(request): + + if 'token' not in request.session: + return redirect('test_links', permanent=True) + + claimresponses = _get_data_json(request, 'claimresponse', [request.session['resource_uri'], 'v2']) + + cnt = claimresponses.get('total', 0) + + nav_info = [] if cnt == 0 else _extract_page_nav(request, claimresponses) + + return render(request, RESULTS_PAGE, + {"fhir_json_pretty": json.dumps(claimresponses, indent=3), + "url_name": 'test_claimresponse', + "nav_list": nav_info, "page_loc": _get_page_loc(request, claimresponses), + "response_type": "Bundle of ClaimResponse", + "total_resource": cnt, + "api_ver": "v2"}) + + @never_cache @waffle_switch('enable_testclient') def authorize_link_v2(request): diff --git a/docker-compose/db-env-vars.env b/docker-compose/db-env-vars.env index e221689dd..3066f727c 100644 --- a/docker-compose/db-env-vars.env +++ b/docker-compose/db-env-vars.env @@ -1,3 +1,4 @@ # Shared DB ENV vars file for the "db" service containter. POSTGRES_DB=bluebutton POSTGRES_PASSWORD=toor +PGPORT=5432 diff --git a/static/openapi.yaml b/static/openapi.yaml index e26e00c70..7f0eb1380 100755 --- a/static/openapi.yaml +++ b/static/openapi.yaml @@ -5,11 +5,11 @@ info: description: | To try out the API using Swagger UI, follow these steps: - 1. **Create an Application**: Ensure you have an application set up on [CMS Sandbox](https://sandbox.bluebutton.cms.gov). - 2. **Update Application Settings**: Go to your application settings and add [https://sandbox.bluebutton.cms.gov/docs/oauth2-redirect](https://sandbox.bluebutton.cms.gov/docs/oauth2-redirect) to the "Callback URLs / Redirect URIs" field under "App Details - Required Information". Save the updated application settings. + 1. **Create an Application**: Ensure you have an application set up on [CMS Sandbox](http://localhost:8000/). + 2. **Update Application Settings**: Go to your application settings and add [http://localhost:8000//docs/oauth2-redirect](http://localhost:8000//docs/oauth2-redirect) to the "Callback URLs / Redirect URIs" field under "App Details - Required Information". Save the updated application settings. 3. **Store Client Credentials**: Note down and securely store your client_id and client_secret from your CMS Sandbox application. These credentials will be used later. 4. **Authorize in Swagger UI**: Open this Swagger UI and click on the "Authorize" button. - 5. **Authorize Access**: Within the prompt, input your client_id and client_secret, choose the scopes you want to use in your testing, and click "Authorize" to open a new tab where you will grant consent to your application from a sample beneficiary. In order to log in, you can use a sample beneficiary using the login instructions from [this link](https://sandbox.bluebutton.cms.gov/testclient/authorize-link-v2). + 5. **Authorize Access**: Within the prompt, input your client_id and client_secret, choose the scopes you want to use in your testing, and click "Authorize" to open a new tab where you will grant consent to your application from a sample beneficiary. In order to log in, you can use a sample beneficiary using the login instructions from [this link](http://localhost:8000//testclient/authorize-link-v2). 6. **Complete Consent**: Follow the prompts to complete the consent form. 7. **Return to Swagger UI**: After consenting, you will be redirected back to this Swagger UI. 8. **Explore APIs**: You can now start exploring and testing the APIs by providing the relevant parameters. @@ -404,6 +404,198 @@ paths: description: "Error: Bad Gateway, e.g. An error occurred contacting the FHIR server." tags: - v2 + /v2/fhir/Claim/{id}: + get: + tags: [v2] + summary: 'read-instance: Read Claim instance' + parameters: + - name: id + in: path + description: The Claim resource ID e.g. m-123456, f-45678 + required: true + style: simple + schema: {minimum: 1, type: string} + example: 'm-123' + responses: + '200': + description: Success + content: + application/fhir+json: + schema: {$ref: '#/components/schemas/FHIR-JSON-RESOURCE'} + /v2/fhir/Claim/_search: + post: + tags: [v2] + summary: 'search-type: Search for Claim instances' + description: This is a search type + requestBody: + content: + application/json: + schema: + type: object + properties: + _lastUpdated: + type: string + description: |- + Only satisfy the Search if the Beneficiary's `last_updated` Date falls within a specified _DateRange_. + A _DateRange_ can be defined by providing less than `lt` and/or greater than `gt` values. + This parameter can be included in a request one or more times. + + Inexact timestamps are accepted, but not recommended, since the input will implicitly be converted to use the server's timezone. + + Examples: + - `_lastUpdated=gt2023-01-02T00:00+00:00&_lastUpdated=lt2023-05-01T00:00+00:00` defines a range between two provided dates + - `_lastUpdated=gt2023-01-02T00:00+00:00` defines a range between the provided date and today + - `_lastUpdated=lt2023-05-01T00:00+00:00` defines a range from the earliest available records until the provided date + example: gt2023-01-02 + service-date: + type: string + description: "Only satisfy the search request if a claim's date\ + \ falls within a specified _DateRange_.\nThe _DateRange_ is defined\ + \ by the claim's billing period, specifically the \n`Claim.billablePeriod.end`.\ + \ A _DateRange_ can be further refined by providing \nless than\ + \ `lt` and/or greater than `gt` values. This parameter can be\ + \ included \nin a request one or more times.\n\nExamples:\n -\ + \ `service-date=gt2023-01-02&service-date=lt2023-05-01`\n - `service-date=gt2023-01-02`\n\ + \ - `service-date=lt2023-05-01`" + example: lt2023-05-01 + startIndex: + type: string + description: |- + When fetching a _Bundle Response_ using pagination, this URL parameter represents an offset + (starting point) into the list of elements for the _Request_. + It is optional and defaults to 1 if not supplied. + A value 0 is not allowed and negative indices are not currently supported. + + Example: + - `startIndex=100` + example: 5 + _count: + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 + example: 10 + type: + type: string + description: |- + A list of one or more comma-separated claim types to be included in the request; + within BFD, the claim types represent an _OR_ inclusion logic meaning any claims matching one of the specified + claim types will be checked + + Supported Claim Type values: + - `fiss` + - `mcs` + + Examples: + - `type=fiss,mcs` + - `type=fiss` + example: fiss,mcs + required: true + responses: + '200': + description: Success + content: + application/fhir+json: + schema: {$ref: '#/components/schemas/FHIR-JSON-RESOURCE'} + /v2/fhir/ClaimResponse/{id}: + get: + tags: [v2] + summary: 'read-instance: Read ClaimResponse instance' + parameters: + - name: id + in: path + description: The resource ID + required: true + style: simple + schema: {minimum: 1, type: string} + example: '123' + responses: + '200': + description: Success + content: + application/fhir+json: + schema: {$ref: '#/components/schemas/FHIR-JSON-RESOURCE'} + /v2/fhir/ClaimResponse/_search: + post: + tags: [v2] + summary: 'search-type: Search for ClaimResponse instances' + description: This is a search type + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + _lastUpdated: + type: string + description: |- + Only satisfy the Search if the Claim's `last_updated` Date falls within a specified _DateRange_. + A _DateRange_ can be defined by providing less than `lt` and/or greater than `gt` values. + This parameter can be included in a request one or more times. + + Inexact timestamps are accepted, but not recommended, since the input will implicitly be converted to use the server's timezone. + + Examples: + - `_lastUpdated=gt2023-01-02T00:00+00:00&_lastUpdated=lt2023-05-01T00:00+00:00` defines a range between two provided dates + - `_lastUpdated=gt2023-01-02T00:00+00:00` defines a range between the provided date and today + - `_lastUpdated=lt2023-05-01T00:00+00:00` defines a range from the earliest available records until the provided date + example: gt2023-01-02 + service-date: + type: string + description: |- + Only satisfy the Search request if a claim's Date + falls within a specified _DateRange_. A _DateRange_ can be + defined by providing less than `lt` and/or greater than `gt` values. + This parameter can be included in a request one or more times. + + Examples: + - `service-date=gt2023-01-02&service-date=lt2023-05-01` + - `service-date=gt2023-01-02` + - `service-date=lt2023-05-01` + example: lt2023-05-01 + startIndex: + type: string + description: |- + When fetching a _Bundle Response_ using pagination, this URL parameter represents an offset + (starting point) into the list of elements for the _Request_. + It is optional and defaults to 1 if not supplied. + A value 0 is not allowed and negative indices are not currently supported. + + Example: + - `startIndex=100` + example: 5 + _count: + description: |- + Provides the number of records to be used for pagination. + + Examples: + - `_count=10`: return 10 values. + format: int32 + example: 10 + type: + type: string + description: |- + A list of one or more comma-separated claim types to be included in the request; + within BFD, the claim types represent an _OR_ inclusion logic meaning any claims matching one of the specified + claim types will be checked + + Supported Claim Type values: + - `fiss` + - `mcs` + + Examples: + - `type=fiss,mcs` + - `type=fiss` + example: fiss,mcs + required: true + responses: + '200': + description: Success + content: + application/fhir+json: + schema: {$ref: '#/components/schemas/FHIR-JSON-RESOURCE'} /v1/fhir/metadata: get: @@ -415,6 +607,8 @@ paths: - patient/Patient.read - patient/Coverage.read - patient/ExplanationOfBenefit.read + - patient/Claim.read + - patient/ClaimResponse.read responses: '200': content: @@ -795,20 +989,6 @@ paths: - v1 components: - securitySchemes: - oAuth2Security: - type: oauth2 - description: This API uses OAuth 2 with the implicit grant flow. - flows: - authorizationCode: - authorizationUrl: https://sandbox.bluebutton.cms.gov/v2/o/authorize - tokenUrl: https://sandbox.bluebutton.cms.gov/v2/o/token/ - scopes: - profile: User Profile - patient/Patient.read: Read patient - patient/Coverage.read: Read Patient coverage - patient/ExplanationOfBenefit.read: Read patient explanation of benefit - schemas: FHIR-JSON-RESOURCE: {type: object, description: A FHIR resource} @@ -911,15 +1091,31 @@ components: format: date-time patient: type: string - + + securitySchemes: + oAuth2Security: + type: oauth2 + description: This API uses OAuth 2 with the implicit grant flow. + flows: + authorizationCode: + authorizationUrl: http://localhost:8000//v2/o/authorize + tokenUrl: http://localhost:8000//v2/o/token/ + scopes: + profile: User Profile + patient/Patient.read: Read patient + patient/Coverage.read: Read Patient coverage + patient/ExplanationOfBenefit.read: Read patient explanation of benefit + patient/Claim.read: Read patient partially adjudicated claim + patient/ClaimResponse.read: Read patient partially adjudicated claim response + examples: V1OpenIdConfigurationExample: value: - issuer: 'https://sandbox.bluebutton.cms.gov' - authorization_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/o/authorize/' - revocation_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/o/revoke/' - token_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/o/token/' - userinfo_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/connect/userinfo' + issuer: 'http://localhost:8000/' + authorization_endpoint: 'http://localhost:8000//v1/o/authorize/' + revocation_endpoint: 'http://localhost:8000//v1/o/revoke/' + token_endpoint: 'http://localhost:8000//v1/o/token/' + userinfo_endpoint: 'http://localhost:8000//v1/connect/userinfo' ui_locales_supported: - en-US service_documentation: 'https://bluebutton.cms.gov/developers' @@ -930,15 +1126,15 @@ components: response_types_supported: - code - token - fhir_metadata_uri: 'https://sandbox.bluebutton.cms.gov/v1/fhir/metadata' + fhir_metadata_uri: 'http://localhost:8000//v1/fhir/metadata' V2OpenIdConfigurationExample: value: - issuer: 'https://sandbox.bluebutton.cms.gov' - authorization_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/o/authorize/' - revocation_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/o/revoke/' - token_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/o/token/' - userinfo_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/connect/userinfo' + issuer: 'http://localhost:8000/' + authorization_endpoint: 'http://localhost:8000//v2/o/authorize/' + revocation_endpoint: 'http://localhost:8000//v2/o/revoke/' + token_endpoint: 'http://localhost:8000//v2/o/token/' + userinfo_endpoint: 'http://localhost:8000//v2/connect/userinfo' ui_locales_supported: - en-US service_documentation: 'https://bluebutton.cms.gov/developers' @@ -949,7 +1145,7 @@ components: response_types_supported: - code - token - fhir_metadata_uri: 'https://sandbox.bluebutton.cms.gov/v2/fhir/metadata' + fhir_metadata_uri: 'http://localhost:8000//v2/fhir/metadata' V1FhirMetadataExample: @@ -1334,11 +1530,11 @@ components: http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris extension: - url: token - valueUri: 'https://sandbox.bluebutton.cms.gov/v1/o/token/' + valueUri: 'http://localhost:8000//v1/o/token/' - url: authorize - valueUri: 'https://sandbox.bluebutton.cms.gov/v1/o/authorize/' + valueUri: 'http://localhost:8000//v1/o/authorize/' - url: revoke - valueUri: 'https://sandbox.bluebutton.cms.gov/v1/o/revoke/' + valueUri: 'http://localhost:8000//v1/o/revoke/' V2FhirMetadataExample: value: @@ -1354,7 +1550,7 @@ components: version: 2.141.0 implementation: description: 'gov.cms.bfd:bfd-server-war' - url: 'https://sandbox.bluebutton.cms.gov/v2/fhir' + url: 'http://localhost:8000//v2/fhir' fhirVersion: 4.0.1 format: - application/json @@ -1771,11 +1967,11 @@ components: http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris extension: - url: token - valueUri: 'https://sandbox.bluebutton.cms.gov/v2/o/token/' + valueUri: 'http://localhost:8000//v2/o/token/' - url: authorize - valueUri: 'https://sandbox.bluebutton.cms.gov/v2/o/authorize/' + valueUri: 'http://localhost:8000//v2/o/authorize/' - url: revoke - valueUri: 'https://sandbox.bluebutton.cms.gov/v2/o/revoke/' + valueUri: 'http://localhost:8000//v2/o/revoke/' V1UserInfoExample: value: @@ -1930,13 +2126,13 @@ components: link: - relation: first url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 + http://localhost:8000//v1/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 - relation: last url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 + http://localhost:8000//v1/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 - relation: self url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/Patient/?_count=10&_format=application%2Fjson%2Bfhir&_id=-10000010254647&startIndex=0 + http://localhost:8000//v1/fhir/Patient/?_count=10&_format=application%2Fjson%2Bfhir&_id=-10000010254647&startIndex=0 entry: - resource: resourceType: Patient @@ -1994,13 +2190,13 @@ components: link: - relation: first url: >- - https://sandbox.bluebutton.cms.gov/v2/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 + http://localhost:8000//v2/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 - relation: last url: >- - https://sandbox.bluebutton.cms.gov/v2/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 + http://localhost:8000//v2/fhir/Patient?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&_id=-10000010254647 - relation: self url: >- - https://sandbox.bluebutton.cms.gov/v2/fhir/Patient/?_count=10&_format=application%2Fjson%2Bfhir&_id=-10000010254647&startIndex=0 + http://localhost:8000//v2/fhir/Patient/?_count=10&_format=application%2Fjson%2Bfhir&_id=-10000010254647&startIndex=0 entry: - resource: resourceType: Patient @@ -2088,13 +2284,13 @@ components: link: - relation: first url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 + http://localhost:8000//v1/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 - relation: last url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 + http://localhost:8000//v1/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 - relation: self url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/Coverage/?_count=10&_format=application%2Fjson%2Bfhir&beneficiary=Patient%2F-10000010254647&startIndex=0 + http://localhost:8000//v1/fhir/Coverage/?_count=10&_format=application%2Fjson%2Bfhir&beneficiary=Patient%2F-10000010254647&startIndex=0 entry: - resource: resourceType: Coverage @@ -2902,13 +3098,13 @@ components: link: - relation: first url: >- - https://sandbox.bluebutton.cms.gov/v2/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 + http://localhost:8000//v2/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 - relation: last url: >- - https://sandbox.bluebutton.cms.gov/v2/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 + http://localhost:8000//v2/fhir/Coverage?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&beneficiary=Patient%2F-10000010254647 - relation: self url: >- - https://sandbox.bluebutton.cms.gov/v2/fhir/Coverage/?_count=10&_format=application%2Fjson%2Bfhir&beneficiary=Patient%2F-10000010254647&startIndex=0 + http://localhost:8000//v2/fhir/Coverage/?_count=10&_format=application%2Fjson%2Bfhir&beneficiary=Patient%2F-10000010254647&startIndex=0 entry: - resource: resourceType: Coverage @@ -3978,16 +4174,16 @@ components: link: - relation: first url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&patient=-10000010254647 + http://localhost:8000//v1/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&patient=-10000010254647 - relation: next url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=10&_count=10&patient=-10000010254647 + http://localhost:8000//v1/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=10&_count=10&patient=-10000010254647 - relation: last url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=50&_count=10&patient=-10000010254647 + http://localhost:8000//v1/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=50&_count=10&patient=-10000010254647 - relation: self url: >- - https://sandbox.bluebutton.cms.gov/v1/fhir/ExplanationOfBenefit/?_count=10&_format=application%2Fjson%2Bfhir&patient=-10000010254647&startIndex=0 + http://localhost:8000//v1/fhir/ExplanationOfBenefit/?_count=10&_format=application%2Fjson%2Bfhir&patient=-10000010254647&startIndex=0 entry: - resource: resourceType: ExplanationOfBenefit @@ -18142,13 +18338,13 @@ components: total: 51 link: - relation: first - url: https://sandbox.bluebutton.cms.gov/v2/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&patient=-10000010254647 + url: http://localhost:8000//v2/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=0&_count=10&patient=-10000010254647 - relation: next - url: https://sandbox.bluebutton.cms.gov/v2/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=10&_count=10&patient=-10000010254647 + url: http://localhost:8000//v2/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=10&_count=10&patient=-10000010254647 - relation: last - url: https://sandbox.bluebutton.cms.gov/v2/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=50&_count=10&patient=-10000010254647 + url: http://localhost:8000//v2/fhir/ExplanationOfBenefit?_format=application%2Fjson%2Bfhir&startIndex=50&_count=10&patient=-10000010254647 - relation: self - url: https://sandbox.bluebutton.cms.gov/v2/fhir/ExplanationOfBenefit/?_count=10&_format=application%2Fjson%2Bfhir&patient=-10000010254647&startIndex=0 + url: http://localhost:8000//v2/fhir/ExplanationOfBenefit/?_count=10&_format=application%2Fjson%2Bfhir&patient=-10000010254647&startIndex=0 entry: - resource: resourceType: ExplanationOfBenefit @@ -33905,8 +34101,8 @@ components: servers: - - url: 'https://sandbox.bluebutton.cms.gov/' - description: Sandbox server + - url: 'http://localhost:8000//' + description: Local server variables: {} security: - oAuth2Security: @@ -33914,4 +34110,6 @@ security: - patient/Patient.read - patient/Coverage.read - patient/ExplanationOfBenefit.read + - patient/Claim.read + - patient/ClaimResponse.read - profile