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

131 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-01 15:39 +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_processors import formatted_gettext_lazy as _ 

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

16from benefits.core.models import EnrollmentFlow, SystemName 

17from benefits.routes import routes 

18 

19from . import analytics, forms, verify 

20 

21 

22class IndexView(AgencySessionRequiredMixin, RecaptchaEnabledMixin, FormView): 

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

24 

25 template_name = "eligibility/index.html" 

26 form_class = forms.EnrollmentFlowSelectionForm 

27 

28 def get_form_kwargs(self): 

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

30 kwargs = super().get_form_kwargs() 

31 kwargs["agency"] = self.agency 

32 return kwargs 

33 

34 def get_context_data(self, **kwargs): 

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

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

37 

38 agency = self.agency 

39 group_agencies = agency.group_agencies() 

40 

41 if group_agencies: 

42 form_text = _( 

43 "Cal-ITP doesn’t save any of your information. {short_name} and " 

44 "nearby transit providers offer reduced fares to riders who qualify.", 

45 short_name=agency.short_name, 

46 ) 

47 previous_url = routes.ADDITIONAL_AGENCIES 

48 else: 

49 form_text = _( 

50 "Cal-ITP doesn’t save any of your information. {short_name} offers reduced fares to riders who qualify.", 

51 short_name=agency.short_name, 

52 ) 

53 previous_url = routes.INDEX 

54 

55 context.update({"form_text": [form_text], "previous_url": previous_url}) 

56 return context 

57 

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

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

60 

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

62 session.logout(request) 

63 

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

65 

66 def form_valid(self, form): 

67 """If the form is valid, set enrollment flow.""" 

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

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

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

71 

72 analytics.selected_flow(self.request, flow) 

73 return redirect(routes.ELIGIBILITY_START) 

74 

75 def form_invalid(self, form): 

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

77 if recaptcha.has_error(form): 

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

79 return super().form_invalid(form) 

80 

81 

82@dataclass 

83class CTAButton: 

84 text: str 

85 route: str 

86 fallback_text: Optional[str] = None 

87 extra_classes: Optional[str] = None 

88 

89 

90@dataclass 

91class EligibilityStart: 

92 page_title: str 

93 headline_text: str 

94 call_to_action_button: CTAButton 

95 eligibility_item_headline: Optional[str] = None 

96 eligibility_item_body: Optional[str] = None 

97 

98 def dict(self): 

99 return asdict(self) 

100 

101 

102class LoginGovEligibilityStart(EligibilityStart): 

103 def __init__(self, page_title, headline_text): 

104 super().__init__( 

105 page_title=page_title, 

106 headline_text=headline_text, 

107 call_to_action_button=CTAButton( 

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

109 fallback_text="Login.gov", 

110 route=routes.OAUTH_LOGIN, 

111 extra_classes="login", 

112 ), 

113 ) 

114 

115 

116class AgencyCardEligibilityStart(EligibilityStart): 

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

118 super().__init__( 

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

120 headline_text=headline_text, 

121 eligibility_item_headline=eligibility_item_headline, 

122 eligibility_item_body=eligibility_item_body, 

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

124 ) 

125 

126 

127class StartView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView): 

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

129 

130 template_name = "eligibility/start.html" 

131 

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

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

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

135 

136 def get_context_data(self, **kwargs): 

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

138 

139 eligibility_start = { 

140 SystemName.CALFRESH.value: LoginGovEligibilityStart( 

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

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

143 ), 

144 SystemName.COURTESY_CARD.value: AgencyCardEligibilityStart( 

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

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

147 eligibility_item_body=_( 

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

149 ), 

150 ), 

151 SystemName.MEDICARE.value: EligibilityStart( 

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

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

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

155 eligibility_item_body=_( 

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

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

158 ), 

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

160 ), 

161 SystemName.OLDER_ADULT.value: LoginGovEligibilityStart( 

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

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

164 ), 

165 SystemName.REDUCED_FARE_MOBILITY_ID.value: AgencyCardEligibilityStart( 

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

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

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

169 ), 

170 SystemName.VETERAN.value: LoginGovEligibilityStart( 

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

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

173 ), 

174 } 

175 

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

177 return context 

178 

179 

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

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

182 

183 template_name = "eligibility/confirm.html" 

184 

185 def get_form_class(self): 

186 flow_system_name = self.flow.system_name 

187 

188 if flow_system_name == SystemName.COURTESY_CARD: 

189 form_class = forms.MSTCourtesyCard 

190 elif flow_system_name == SystemName.REDUCED_FARE_MOBILITY_ID: 

191 form_class = forms.SBMTDMobilityPass 

192 else: 

193 raise ValueError(f"The {flow_system_name} flow does not support Eligibility API verification.") 

194 

195 return form_class 

196 

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

198 if not session.eligible(request): 

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

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

201 else: 

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

203 return redirect(routes.ENROLLMENT_INDEX) 

204 

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

206 analytics.started_eligibility(request, self.flow) 

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

208 

209 def form_valid(self, form): 

210 agency = self.agency 

211 flow = self.flow 

212 request = self.request 

213 

214 # make Eligibility Verification request to get the verified confirmation 

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

216 

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

218 if is_verified is None: 

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

220 return self.form_invalid(form) 

221 # Eligibility API returned that no type was verified 

222 elif not is_verified: 

223 return redirect(routes.ELIGIBILITY_UNVERIFIED) 

224 # Eligibility API returned that type was verified 

225 else: 

226 session.update(request, eligible=True) 

227 analytics.returned_success(request, flow) 

228 

229 return redirect(routes.ENROLLMENT_INDEX) 

230 

231 def form_invalid(self, form): 

232 if recaptcha.has_error(form): 

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

234 

235 return self.get(self.request) 

236 

237 

238@dataclass 

239class EligibilityUnverified: 

240 headline_text: str 

241 body_text: str 

242 button_text: str 

243 

244 def dict(self): 

245 return asdict(self) 

246 

247 

248class AgencyCardEligibilityUnverified(EligibilityUnverified): 

249 def __init__(self, agency_card): 

250 super().__init__( 

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

252 body_text=_( 

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

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

255 agency_card=agency_card, 

256 ), 

257 button_text=_("Try again"), 

258 ) 

259 

260 

261class UnverifiedView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView): 

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

263 

264 template_name = "eligibility/unverified.html" 

265 

266 def get_context_data(self, **kwargs): 

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

268 

269 eligibility_unverified = { 

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

271 SystemName.REDUCED_FARE_MOBILITY_ID.value: AgencyCardEligibilityUnverified( 

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

273 ), 

274 } 

275 

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

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

278 return context 

279 

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

281 analytics.returned_fail(request, self.flow) 

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