Coverage for benefits / eligibility / views.py: 96%

136 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-13 19:35 +0000

1""" 

2The eligibility application: view definitions for the eligibility verification flow. 

3""" 

4 

5from dataclasses import asdict, dataclass 

6from typing import Optional 

7 

8from django.contrib import messages 

9from django.shortcuts import redirect 

10from django.urls import reverse 

11from django.views.generic import FormView, TemplateView 

12 

13from benefits.core import recaptcha, session 

14from benefits.core.context import formatted_gettext_lazy as _ 

15from benefits.core.context.flow import SystemName 

16from benefits.core.mixins import AgencySessionRequiredMixin, FlowSessionRequiredMixin, RecaptchaEnabledMixin 

17from benefits.core.models import AgencySlug, EnrollmentFlow 

18from benefits.routes import routes 

19 

20from . import analytics, forms, verify 

21 

22 

23class EligibilityIndex: 

24 def __init__(self, form_text): 

25 if not isinstance(form_text, list): 25 ↛ 28line 25 didn't jump to line 28 because the condition on line 25 was always true

26 form_text = [form_text] 

27 

28 self.form_text = form_text 

29 

30 def dict(self): 

31 return dict(form_text=self.form_text) 

32 

33 

34class IndexView(AgencySessionRequiredMixin, RecaptchaEnabledMixin, FormView): 

35 """View handler for the enrollment flow selection form.""" 

36 

37 template_name = "eligibility/index.html" 

38 form_class = forms.EnrollmentFlowSelectionForm 

39 

40 def get_form_kwargs(self): 

41 """Return the keyword arguments for instantiating the form.""" 

42 kwargs = super().get_form_kwargs() 

43 kwargs["agency"] = self.agency 

44 return kwargs 

45 

46 def get_context_data(self, **kwargs): 

47 """Add agency-specific context data.""" 

48 context = super().get_context_data(**kwargs) 

49 

50 eligiblity_index = { 

51 AgencySlug.CST.value: EligibilityIndex( 

52 form_text=_( 

53 "Cal-ITP doesn’t save any of your information. " 

54 "All CST transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

55 ) 

56 ), 

57 AgencySlug.EDCTA.value: EligibilityIndex( 

58 form_text=_( 

59 "Cal-ITP doesn’t save any of your information. " 

60 "All EDCTA transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

61 ), 

62 ), 

63 AgencySlug.MST.value: EligibilityIndex( 

64 form_text=_( 

65 "Cal-ITP doesn’t save any of your information. " 

66 "All MST transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

67 ) 

68 ), 

69 AgencySlug.NEVCO.value: EligibilityIndex( 

70 form_text=_( 

71 "Cal-ITP doesn’t save any of your information. " 

72 "All Nevada County Connects transit benefits reduce fares " 

73 "by 50%% for bus service on fixed routes.".replace("%%", "%") 

74 ) 

75 ), 

76 AgencySlug.RABA.value: EligibilityIndex( 

77 form_text=_( 

78 "Cal-ITP doesn’t save any of your information. " 

79 "All RABA transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

80 ) 

81 ), 

82 AgencySlug.ROSEVILLE.value: EligibilityIndex( 

83 form_text=_( 

84 "Cal-ITP doesn’t save any of your information. " 

85 "All Roseville transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

86 ) 

87 ), 

88 AgencySlug.SACRT.value: EligibilityIndex( 

89 form_text=_( 

90 "Cal-ITP doesn’t save any of your information. " 

91 "All SacRT transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

92 ) 

93 ), 

94 AgencySlug.SBMTD.value: EligibilityIndex( 

95 form_text=_( 

96 "Cal-ITP doesn’t save any of your information. " 

97 "All SBMTD transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

98 ) 

99 ), 

100 AgencySlug.SLORTA.value: EligibilityIndex( 

101 form_text=_( 

102 "Cal-ITP doesn’t save any of your information. " 

103 "All RTA transit benefits reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

104 ) 

105 ), 

106 AgencySlug.VCTC.value: EligibilityIndex( 

107 form_text=_( 

108 "Cal-ITP doesn’t save any of your information. " 

109 "All Ventura County Transportation Commission transit benefits " 

110 "reduce fares by 50%% for bus service on fixed routes.".replace("%%", "%") 

111 ) 

112 ), 

113 } 

114 

115 context.update(eligiblity_index[self.agency.slug].dict()) 

116 return context 

117 

118 def get(self, request, *args, **kwargs): 

119 """Initialize session state before handling the request.""" 

120 

121 session.update(request, eligible=False, origin=self.agency.index_url) 

122 session.logout(request) 

123 

124 return super().get(request, *args, **kwargs) 

125 

126 def form_valid(self, form): 

127 """If the form is valid, set enrollment flow and redirect.""" 

128 flow_id = form.cleaned_data.get("flow") 

129 flow = EnrollmentFlow.objects.get(id=flow_id) 

130 session.update(self.request, flow=flow) 

131 

132 analytics.selected_flow(self.request, flow) 

133 return redirect(routes.ELIGIBILITY_START) 

134 

135 def form_invalid(self, form): 

136 """If the form is invalid, display error messages.""" 

137 if recaptcha.has_error(form): 

138 messages.error(self.request, "Recaptcha failed. Please try again.") 

139 return super().form_invalid(form) 

140 

141 

142@dataclass 

143class CTAButton: 

144 text: str 

145 route: str 

146 fallback_text: Optional[str] = None 

147 extra_classes: Optional[str] = None 

148 

149 

150@dataclass 

151class EligibilityStart: 

152 page_title: str 

153 headline_text: str 

154 call_to_action_button: CTAButton 

155 eligibility_item_headline: Optional[str] = None 

156 eligibility_item_body: Optional[str] = None 

157 

158 def dict(self): 

159 return asdict(self) 

160 

161 

162class LoginGovEligibilityStart(EligibilityStart): 

163 def __init__(self, page_title, headline_text): 

164 super().__init__( 

165 page_title=page_title, 

166 headline_text=headline_text, 

167 call_to_action_button=CTAButton( 

168 text=_("Get started with"), 

169 fallback_text="Login.gov", 

170 route=routes.OAUTH_LOGIN, 

171 extra_classes="login", 

172 ), 

173 ) 

174 

175 

176class AgencyCardEligibilityStart(EligibilityStart): 

177 def __init__(self, headline_text, eligibility_item_headline, eligibility_item_body): 

178 super().__init__( 

179 page_title=_("Agency card overview"), 

180 headline_text=headline_text, 

181 eligibility_item_headline=eligibility_item_headline, 

182 eligibility_item_body=eligibility_item_body, 

183 call_to_action_button=CTAButton(text=_("Continue"), route=routes.ELIGIBILITY_CONFIRM), 

184 ) 

185 

186 

187class StartView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView): 

188 """CBV for the eligibility verification getting started screen.""" 

189 

190 template_name = "eligibility/start.html" 

191 

192 def get(self, request, *args, **kwargs): 

193 session.update(request, eligible=False, origin=reverse(routes.ELIGIBILITY_START)) 

194 return super().get(request, *args, **kwargs) 

195 

196 def get_context_data(self, **kwargs): 

197 context = super().get_context_data(**kwargs) 

198 

199 eligibility_start = { 

200 SystemName.AGENCY_CARD.value: AgencyCardEligibilityStart( 

201 headline_text=_("You selected an Agency Card transit benefit."), 

202 eligibility_item_headline=_("Your current Agency Card number"), 

203 eligibility_item_body=_( 

204 "You do not need to have your physical CST Agency Card, but you will need to know the number." 

205 ), 

206 ), 

207 SystemName.CALFRESH.value: LoginGovEligibilityStart( 

208 page_title=_("CalFresh benefit overview"), 

209 headline_text=_("You selected a CalFresh Cardholder transit benefit."), 

210 ), 

211 SystemName.COURTESY_CARD.value: AgencyCardEligibilityStart( 

212 headline_text=_("You selected a Courtesy Card transit benefit."), 

213 eligibility_item_headline=_("Your current Courtesy Card number"), 

214 eligibility_item_body=_( 

215 "You do not need to have your physical MST Courtesy Card, but you will need to know the number." 

216 ), 

217 ), 

218 SystemName.MEDICARE.value: EligibilityStart( 

219 page_title=_("Medicare benefit overview"), 

220 headline_text=_("You selected a Medicare Cardholder transit benefit."), 

221 eligibility_item_headline=_("An online account with Medicare.gov"), 

222 eligibility_item_body=_( 

223 "If you do not have an account you will be able to create one using your red, white, and blue Medicare " 

224 "card. We use your Medicare.gov account to verify you qualify." 

225 ), 

226 call_to_action_button=CTAButton(text=_("Continue to Medicare.gov"), route=routes.OAUTH_LOGIN), 

227 ), 

228 SystemName.OLDER_ADULT.value: LoginGovEligibilityStart( 

229 page_title=_("Older Adult benefit overview"), 

230 headline_text=_("You selected an Older Adult transit benefit."), 

231 ), 

232 SystemName.REDUCED_FARE_MOBILITY_ID.value: AgencyCardEligibilityStart( 

233 headline_text=_("You selected a Reduced Fare Mobility ID transit benefit."), 

234 eligibility_item_headline=_("Your current Reduced Fare Mobility ID number"), 

235 eligibility_item_body=_("You do not need to have your physical card, but you will need to know the number."), 

236 ), 

237 SystemName.VETERAN.value: LoginGovEligibilityStart( 

238 page_title=_("Veterans benefit overview"), 

239 headline_text=_("You selected a Veteran transit benefit."), 

240 ), 

241 } 

242 

243 context.update(eligibility_start[self.flow.system_name].dict()) 

244 return context 

245 

246 

247class ConfirmView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, RecaptchaEnabledMixin, FormView): 

248 """View handler for Eligiblity Confirm form, used only by flows that support Eligibility API verification.""" 

249 

250 template_name = "eligibility/confirm.html" 

251 

252 def get_form_class(self): 

253 agency_slug = self.agency.slug 

254 flow_system_name = self.flow.system_name 

255 

256 if agency_slug == AgencySlug.CST and flow_system_name == SystemName.AGENCY_CARD: 

257 form_class = forms.CSTAgencyCard 

258 elif agency_slug == AgencySlug.MST and flow_system_name == SystemName.COURTESY_CARD: 

259 form_class = forms.MSTCourtesyCard 

260 elif agency_slug == AgencySlug.SBMTD and flow_system_name == SystemName.REDUCED_FARE_MOBILITY_ID: 

261 form_class = forms.SBMTDMobilityPass 

262 else: 

263 raise ValueError( 

264 f"This agency/flow combination does not support Eligibility API verification: {agency_slug}, {flow_system_name}" # noqa 

265 ) 

266 

267 return form_class 

268 

269 def get(self, request, *args, **kwargs): 

270 if not session.eligible(request): 

271 session.update(request, origin=reverse(routes.ELIGIBILITY_CONFIRM)) 

272 return super().get(request, *args, **kwargs) 

273 else: 

274 # an already verified user, no need to verify again 

275 return redirect(routes.ENROLLMENT_INDEX) 

276 

277 def post(self, request, *args, **kwargs): 

278 analytics.started_eligibility(request, self.flow) 

279 return super().post(request, *args, **kwargs) 

280 

281 def form_valid(self, form): 

282 agency = self.agency 

283 flow = self.flow 

284 request = self.request 

285 

286 # make Eligibility Verification request to get the verified confirmation 

287 is_verified = verify.eligibility_from_api(flow, form, agency) 

288 

289 # Eligibility API returned errors (so eligibility is unknown), allow for correction/resubmission 

290 if is_verified is None: 

291 analytics.returned_error(request, flow, form.errors) 

292 return self.form_invalid(form) 

293 # Eligibility API returned that no type was verified 

294 elif not is_verified: 

295 return redirect(routes.ELIGIBILITY_UNVERIFIED) 

296 # Eligibility API returned that type was verified 

297 else: 

298 session.update(request, eligible=True) 

299 analytics.returned_success(request, flow) 

300 

301 return redirect(routes.ENROLLMENT_INDEX) 

302 

303 def form_invalid(self, form): 

304 if recaptcha.has_error(form): 

305 messages.error(self.request, "Recaptcha failed. Please try again.") 

306 

307 return self.get(self.request) 

308 

309 

310@dataclass 

311class EligibilityUnverified: 

312 headline_text: str 

313 body_text: str 

314 button_text: str 

315 

316 def dict(self): 

317 return asdict(self) 

318 

319 

320class AgencyCardEligibilityUnverified(EligibilityUnverified): 

321 def __init__(self, agency_card): 

322 super().__init__( 

323 headline_text=_("Your card information may not have been entered correctly."), 

324 body_text=_( 

325 "The number and last name must be entered exactly as they appear on your {agency_card}. " 

326 "Please check your card and try again, or contact your transit agency for help.", 

327 agency_card=agency_card, 

328 ), 

329 button_text=_("Try again"), 

330 ) 

331 

332 

333class UnverifiedView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView): 

334 """CBV for the unverified eligibility page.""" 

335 

336 template_name = "eligibility/unverified.html" 

337 

338 def get_context_data(self, **kwargs): 

339 context = super().get_context_data(**kwargs) 

340 

341 eligibility_unverified = { 

342 SystemName.AGENCY_CARD.value: AgencyCardEligibilityUnverified(agency_card=_("CST Agency Card")), 

343 SystemName.COURTESY_CARD.value: AgencyCardEligibilityUnverified(agency_card=_("MST Courtesy Card")), 

344 SystemName.REDUCED_FARE_MOBILITY_ID.value: AgencyCardEligibilityUnverified( 

345 agency_card=_("SBMTD Reduced Fare Mobility ID card") 

346 ), 

347 } 

348 

349 context_object = eligibility_unverified.get(self.flow.system_name) 

350 context.update(context_object.dict() if context_object else {}) 

351 return context 

352 

353 def get(self, request, *args, **kwargs): 

354 analytics.returned_fail(request, self.flow) 

355 return super().get(request, *args, **kwargs)