Skip to content

Commit 81cb0a0

Browse files
authored
Merge pull request #182 from CMSgov/brandon/BB2-4349-dic-endpoint
preliminary addition of insurance card example
2 parents 751dff1 + 5e89071 commit 81cb0a0

7 files changed

Lines changed: 204 additions & 47 deletions

File tree

README-bb2-dev.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,17 @@ The overall goals are to:
101101
102102
### Run a local version of sample client that consumes a local version of the SDK
103103
104-
Ensure that in bluebutton-sample-client-python-react/server/Dockerfile, uncomment the following line. Replace the version number (1.0.4 in the example) of the .whl file with what has been generated from the previous build command.
104+
Ensure that in bluebutton-sample-client-python-react/server/Dockerfile, comment out this block to not use the currently published SDK:
105+
106+
```
107+
RUN if [ "$BUILD_DEVELOPMENT" = "True" ]; then \
108+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ cms-bluebutton-sdk; \
109+
else \
110+
pip install cms-bluebutton-sdk; \
111+
fi
112+
```
113+
114+
then uncomment the following line. Replace the version number (1.0.4 in the example) of the .whl file with what has been generated from the previous build command.
105115
106116
```
107117
RUN pip install cms_bluebutton_sdk-1.0.4-py3-none-any.whl
@@ -119,6 +129,7 @@ The overall goals are to:
119129
cd server
120130
unzip -l cms_bluebutton_sdk-1.0.4-py3-none-any.whl
121131
pip install cms_bluebutton_sdk-1.0.4-py3-none-any.whl
132+
cd ..
122133
docker compose up
123134
```
124135

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Header from '../src/components/header';
33
import Patient from '../src/components/patient';
44
import PatientData from './components/patientData';
55
import Records from './components/records';
6+
import InsuranceCard from './components/insuranceCard';
67
import { BrowserRouter as Router} from "react-router-dom";
78
import { TabPanel, Tabs } from '@cmsgov/design-system';
89

@@ -24,6 +25,7 @@ function App() {
2425
</div>
2526
{}
2627
<Records />
28+
<InsuranceCard />
2729
{}
2830
<div>
2931
<div>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useEffect, useState } from 'react';
2+
import * as process from 'process';
3+
4+
export type InsuranceCardInfo = {
5+
beneficiaryName: string,
6+
payerName: string,
7+
memberId: string,
8+
groupNumber: string,
9+
planNumber: string,
10+
effectiveDate: string
11+
}
12+
13+
export type ErrorResponse = {
14+
type: string,
15+
content: string,
16+
}
17+
18+
/*
19+
* DEVELOPER NOTES:
20+
* The digital insurance card is returned from the BB2 `$generate-insurance-card`
21+
* operation (v3-only) as a CARIN Digital Insurance Card (C4DIC) FHIR Bundle
22+
* containing Patient, Coverage, and Organization (payer) resources.
23+
* See https://hl7.org/fhir/us/insurance-card/ for the full resource shapes.
24+
*/
25+
export default function InsuranceCard() {
26+
const [card, setCard] = useState<InsuranceCardInfo>();
27+
const [message, setMessage] = useState<ErrorResponse>();
28+
29+
useEffect(() => {
30+
const test_url = process.env.TEST_APP_API_URL ? process.env.TEST_APP_API_URL : '';
31+
fetch(`${test_url}/api/data/insuranceCard`)
32+
.then(res => res.json())
33+
.then(bundleData => {
34+
console.log('Insurance card response:', JSON.stringify(bundleData, null, 2));
35+
if (bundleData.entry) {
36+
const resources = bundleData.entry.map((entry: any) => entry.resource);
37+
const patient = resources.find((r: any) => r?.resourceType === 'Patient');
38+
const coverage = resources.find((r: any) => r?.resourceType === 'Coverage');
39+
const organizations = resources.filter((r: any) => r?.resourceType === 'Organization');
40+
const payerReference = coverage?.payor?.[0]?.reference?.split('/').pop();
41+
const payer = organizations.find((r: any) => r?.id === payerReference) || organizations[0];
42+
43+
setCard({
44+
beneficiaryName: patient?.name?.[0]?.text
45+
|| [patient?.name?.[0]?.given?.join(' '), patient?.name?.[0]?.family].filter(Boolean).join(' ')
46+
|| 'Unknown',
47+
payerName: payer?.name || 'Unknown',
48+
memberId: coverage?.subscriberId || coverage?.identifier?.[0]?.value || 'Unknown',
49+
groupNumber: coverage?.class?.find((c: any) => c.type?.coding?.[0]?.code === 'group')?.value || 'Unknown',
50+
planNumber: coverage?.class?.find((c: any) => c.type?.coding?.[0]?.code === 'plan')?.value || 'Unknown',
51+
effectiveDate: coverage?.period?.start || 'Unknown',
52+
});
53+
} else if (bundleData.message) {
54+
setMessage({ type: 'error', content: bundleData.message });
55+
}
56+
});
57+
}, []);
58+
59+
if (message) {
60+
return (
61+
<div className='full-width-card'>
62+
<p>{message.content}</p>
63+
</div>
64+
);
65+
}
66+
67+
if (!card) {
68+
return null;
69+
}
70+
71+
return (
72+
<div className="bb-c-card default-card">
73+
<h3>Digital Insurance Card</h3>
74+
<ul>
75+
<li>Beneficiary: {card.beneficiaryName}</li>
76+
<li>Payer: {card.payerName}</li>
77+
<li>Member ID: {card.memberId}</li>
78+
<li>Group Number: {card.groupNumber}</li>
79+
<li>Plan Number: {card.planNumber}</li>
80+
<li>Effective Date: {card.effectiveDate}</li>
81+
</ul>
82+
</div>
83+
);
84+
}

server/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ RUN apt-get update && \
1818
RUN pip install pipenv debugpy
1919

2020
# Install cms_bluebutton_sdk from pypi.org or test.pypi.org
21+
# Comment out this entire block when running a local version of the SDK
2122
RUN if [ "$BUILD_DEVELOPMENT" = "True" ]; then \
2223
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ cms-bluebutton-sdk; \
2324
else \
2425
pip install cms-bluebutton-sdk; \
2526
fi
2627

27-
# If using a local version of the sdk, copy the wheel file and install it
28-
# RUN pip install cms_bluebutton_sdk-1.0.4-py3-none-any.whl
28+
# If using a local version of the sdk, uncomment out this next line and add the proper version
29+
# RUN pip install cms_bluebutton_sdk-1.0.5-py3-none-any.whl
2930
RUN pipenv lock
3031
RUN pip install click
3132
RUN pipenv install --system --deploy --ignore-pipfile

server/app.py

Lines changed: 101 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
import os
21
import json
2+
import os
3+
import time
4+
from http import HTTPStatus
35

4-
from flask import redirect, request, Flask
6+
import requests
57
from cms_bluebutton.cms_bluebutton import BlueButton
6-
8+
from flask import Flask, redirect, request
79

810
BENE_DENIED_ACCESS = "access_denied"
911
FE_MSG_ACCESS_DENIED = "Beneficiary denied app access to their data"
1012
ERR_QUERY_EOB = "Error when querying the patient's EOB!"
13+
ERR_QUERY_INSURANCE_CARD = "Error when querying the patient's digital insurance card!"
1114
ERR_MISSING_AUTH_CODE = "Response was missing access code!"
1215
ERR_MISSING_STATE = "State is required when using PKCE"
16+
ERR_TOKEN_EXCHANGE = "Error when exchanging the authorization code for an access token!"
17+
TOKEN_RETRY_TOTAL = 3
18+
TOKEN_RETRY_BACKOFF_SECONDS = 2
19+
RETRY_ERROR_DENY = [HTTPStatus.TOO_MANY_REQUESTS, HTTPStatus.BAD_REQUEST]
1320

1421
app = Flask(__name__)
1522
bb = BlueButton()
@@ -18,10 +25,7 @@
1825
# with the current logged in app user,
1926
# in real app, this could be the app specific
2027
# account management system
21-
logged_in_user = {
22-
'authToken': None,
23-
'eobData': None
24-
}
28+
logged_in_user = {"authToken": None, "eobData": None, "insuranceCardData": None}
2529

2630
auth_data = bb.generate_auth_data()
2731

@@ -33,51 +37,58 @@
3337
auth_token = None
3438

3539

36-
@app.route('/api/authorize/authurl', methods=['GET'])
40+
@app.route("/api/authorize/authurl", methods=["GET"])
3741
def get_auth_url():
3842
# for SMART App v2 scopes usage: explicitly
3943
# provide query parameter scope=<v2 scopes>
4044
# where <v2 scopes> is space delimited v2 scope specs (url encoded)
4145
# e.g. patient/ExplanationOfBenefit.rs
42-
redirect_url = (bb.generate_authorize_url(auth_data)
43-
+ "&scope=patient%2FExplanationOfBenefit.s")
46+
# patient/Patient.rs is required for the $generate-insurance-card
47+
# operation (v3-only) used to retrieve the digital insurance card
48+
redirect_url = (
49+
bb.generate_authorize_url(auth_data)
50+
+ "&scope=patient%2FExplanationOfBenefit.rs+patient%2FPatient.rs+patient%2FCoverage.rs"
51+
)
4452
return redirect_url
4553

4654

47-
@app.route('/api/bluebutton/callback/', methods=['GET'])
55+
@app.route("/api/bluebutton/callback/", methods=["GET"])
4856
def authorization_callback():
4957
request_query = request.args
5058

51-
if (request_query.get('error') == BENE_DENIED_ACCESS):
59+
if request_query.get("error") == BENE_DENIED_ACCESS:
5260
# clear all cached claims eob data since the bene has denied access
5361
# for the application
5462
clear_bb2_data()
55-
logged_in_user.update({'eobData': {'message': FE_MSG_ACCESS_DENIED}})
63+
logged_in_user.update({"eobData": {"message": FE_MSG_ACCESS_DENIED}})
5664
print(FE_MSG_ACCESS_DENIED)
5765
return redirect(get_fe_redirect_url())
5866

59-
code = request_query.get('code')
67+
code = request_query.get("code")
6068

6169
if code is None:
6270
print(ERR_MISSING_AUTH_CODE)
6371
return redirect(get_fe_redirect_url())
6472

65-
state = request_query.get('state')
73+
state = request_query.get("state")
6674

6775
if state is None:
6876
print(ERR_MISSING_STATE)
6977
return redirect(get_fe_redirect_url())
7078

71-
auth_token = bb.get_authorization_token(auth_data, code, state)
79+
try:
80+
auth_token = get_authorization_token_with_retry(auth_data, code, state)
81+
except Exception as ex:
82+
clear_bb2_data()
83+
logged_in_user.update({"eobData": {"message": ERR_TOKEN_EXCHANGE}})
84+
print(ERR_TOKEN_EXCHANGE)
85+
print(ex)
86+
return redirect(get_fe_redirect_url())
7287

7388
# correlate app user with medicare bene
74-
logged_in_user['authToken'] = auth_token
89+
logged_in_user["authToken"] = auth_token
7590

76-
config = {
77-
"auth_token": auth_token,
78-
"params": {},
79-
"url": "to be overriden"
80-
}
91+
config = {"auth_token": auth_token, "params": {}, "url": "to be overriden"}
8192

8293
try:
8394
# search eob (or other fhir resources: patient, coverage, etc.)
@@ -91,61 +102,109 @@ def authorization_callback():
91102
# 'previous' might present depending on the current page.
92103
# Use bb.get_pages(data, config) to get all the pages
93104

94-
auth_token = eob_data['auth_token']
95-
logged_in_user['authToken'] = auth_token
96-
logged_in_user['eobData'] = eob_data['response'].json()
105+
auth_token = eob_data["auth_token"]
106+
logged_in_user["authToken"] = auth_token
107+
logged_in_user["eobData"] = eob_data["response"].json()
97108
except Exception as ex:
98109
clear_bb2_data()
99-
logged_in_user.update({'eobData': {'message': ERR_QUERY_EOB}})
110+
logged_in_user.update({"eobData": {"message": ERR_QUERY_EOB}})
100111
print(ERR_QUERY_EOB)
101112
print(ex)
113+
return redirect(get_fe_redirect_url())
114+
115+
try:
116+
# fetch the CARIN Digital Insurance Card (C4DIC) FHIR bundle
117+
insurance_card_data = bb.get_insurance_card_data(config)
118+
119+
auth_token = insurance_card_data["auth_token"]
120+
logged_in_user["authToken"] = auth_token
121+
logged_in_user["insuranceCardData"] = insurance_card_data["response"].json()
122+
print(json.dumps(logged_in_user["insuranceCardData"], indent=2))
123+
except Exception as ex:
124+
logged_in_user.update(
125+
{"insuranceCardData": {"message": ERR_QUERY_INSURANCE_CARD}}
126+
)
127+
print(ERR_QUERY_INSURANCE_CARD)
128+
print(ex)
102129

103130
return redirect(get_fe_redirect_url())
104131

105132

106-
@app.route('/api/bluebutton/loadDefaults', methods=['GET'])
133+
def get_authorization_token_with_retry(auth_data, code, state):
134+
for attempt in range(TOKEN_RETRY_TOTAL + 1):
135+
try:
136+
return bb.get_authorization_token(auth_data, code, state)
137+
except requests.exceptions.HTTPError as ex:
138+
status_code = ex.response.status_code if ex.response is not None else None
139+
if attempt == TOKEN_RETRY_TOTAL and status_code not in RETRY_ERROR_DENY:
140+
raise
141+
wait_seconds = TOKEN_RETRY_BACKOFF_SECONDS * (2**attempt)
142+
print(
143+
f"Token endpoint returned {status_code}, retrying in "
144+
f"{wait_seconds}s (attempt {attempt + 1}/{TOKEN_RETRY_TOTAL})"
145+
)
146+
time.sleep(wait_seconds)
147+
148+
149+
@app.route("/api/bluebutton/loadDefaults", methods=["GET"])
107150
def load_default_data():
108151
# TODO: add config var or param to detemine dataset
109-
logged_in_user['eobData'] = load_data_file("Dataset 1", "eobData")
152+
logged_in_user["eobData"] = load_data_file("Dataset 1", "eobData")
110153
return get_fe_redirect_url()
111154

112155

113156
def load_data_file(dataset_name, resource_file_name):
114-
response_file = open("./default_datasets/{}/{}.json".format(dataset_name, resource_file_name), 'r')
157+
response_file = open(
158+
"./default_datasets/{}/{}.json".format(dataset_name, resource_file_name), "r"
159+
)
115160
resource = json.load(response_file)
116161
response_file.close()
117162
return resource
118163

119164

120-
@app.route('/api/data/benefit', methods=['GET'])
165+
@app.route("/api/data/benefit", methods=["GET"])
121166
def get_patient_eob():
122167
"""
123168
* this function is used directly by the front-end to
124169
* retrieve eob data from the logged in user from within the mocked DB
125170
* This would be replaced by a persistence service layer for whatever
126171
* DB you would choose to use
127172
"""
128-
if logged_in_user and logged_in_user.get('eobData'):
129-
return logged_in_user.get('eobData')
173+
if logged_in_user and logged_in_user.get("eobData"):
174+
return logged_in_user.get("eobData")
175+
else:
176+
return {}
177+
178+
179+
@app.route("/api/data/insuranceCard", methods=["GET"])
180+
def get_patient_insurance_card():
181+
"""
182+
* this function is used directly by the front-end to
183+
* retrieve the digital insurance card data for the logged in user
184+
* from within the mocked DB
185+
"""
186+
if logged_in_user and logged_in_user.get("insuranceCardData"):
187+
return logged_in_user.get("insuranceCardData")
130188
else:
131189
return {}
132190

133191

134192
def get_fe_redirect_url():
135-
'''
193+
"""
136194
helper to figure out the correct front end redirect url per context
137-
'''
138-
is_selenium = os.getenv('SELENIUM_TESTS', 'False').lower() in ('true')
139-
return 'http://client:3000' if is_selenium else 'http://localhost:3000'
195+
"""
196+
is_selenium = os.getenv("SELENIUM_TESTS", "False").lower() in ("true")
197+
return "http://client:3000" if is_selenium else "http://localhost:3000"
140198

141199

142200
def clear_bb2_data():
143-
'''
201+
"""
144202
helper to clean up cached result
145-
'''
146-
logged_in_user.update({'authToken': None})
147-
logged_in_user.update({'eobData': {}})
203+
"""
204+
logged_in_user.update({"authToken": None})
205+
logged_in_user.update({"eobData": {}})
206+
logged_in_user.update({"insuranceCardData": {}})
148207

149208

150-
if __name__ == '__main__':
151-
app.run(debug=True, host='0.0.0.0', port=3001)
209+
if __name__ == "__main__":
210+
app.run(debug=True, host="0.0.0.0", port=3001)

server/sample-bluebutton-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"client_id": "<client_id>",
44
"client_secret": "<client_secret>",
55
"callback_url": "http://localhost:3001/api/bluebutton/callback/",
6-
"version": 2
6+
"version": 3
77
}

0 commit comments

Comments
 (0)