1- import os
21import json
2+ import os
3+ import time
4+ from http import HTTPStatus
35
4- from flask import redirect , request , Flask
6+ import requests
57from cms_bluebutton .cms_bluebutton import BlueButton
6-
8+ from flask import Flask , redirect , request
79
810BENE_DENIED_ACCESS = "access_denied"
911FE_MSG_ACCESS_DENIED = "Beneficiary denied app access to their data"
1012ERR_QUERY_EOB = "Error when querying the patient's EOB!"
13+ ERR_QUERY_INSURANCE_CARD = "Error when querying the patient's digital insurance card!"
1114ERR_MISSING_AUTH_CODE = "Response was missing access code!"
1215ERR_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
1421app = Flask (__name__ )
1522bb = BlueButton ()
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
2630auth_data = bb .generate_auth_data ()
2731
3337auth_token = None
3438
3539
36- @app .route (' /api/authorize/authurl' , methods = [' GET' ])
40+ @app .route (" /api/authorize/authurl" , methods = [" GET" ])
3741def 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" ])
4856def 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" ])
107150def 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
113156def 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" ])
121166def 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
134192def 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
142200def 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 )
0 commit comments