Skip to content

Commit ace5b83

Browse files
committed
preliminary addition of insurance card example
1 parent 428db00 commit ace5b83

5 files changed

Lines changed: 189 additions & 44 deletions

File tree

‎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/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
}

‎server/sample-bluebutton-selenium-config.yml‎

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://server:3001/api/bluebutton/callback/",
6-
"version": 2
6+
"version": 3
77
}

0 commit comments

Comments
 (0)