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
« 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"""
5from dataclasses import asdict, dataclass
6from typing import Optional
8from django.contrib import messages
9from django.shortcuts import redirect
10from django.urls import reverse
11from django.views.generic import FormView, TemplateView
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
20from . import analytics, forms, verify
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]
28 self.form_text = form_text
30 def dict(self):
31 return dict(form_text=self.form_text)
34class IndexView(AgencySessionRequiredMixin, RecaptchaEnabledMixin, FormView):
35 """View handler for the enrollment flow selection form."""
37 template_name = "eligibility/index.html"
38 form_class = forms.EnrollmentFlowSelectionForm
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
46 def get_context_data(self, **kwargs):
47 """Add agency-specific context data."""
48 context = super().get_context_data(**kwargs)
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 }
115 context.update(eligiblity_index[self.agency.slug].dict())
116 return context
118 def get(self, request, *args, **kwargs):
119 """Initialize session state before handling the request."""
121 session.update(request, eligible=False, origin=self.agency.index_url)
122 session.logout(request)
124 return super().get(request, *args, **kwargs)
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)
132 analytics.selected_flow(self.request, flow)
133 return redirect(routes.ELIGIBILITY_START)
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)
142@dataclass
143class CTAButton:
144 text: str
145 route: str
146 fallback_text: Optional[str] = None
147 extra_classes: Optional[str] = None
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
158 def dict(self):
159 return asdict(self)
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 )
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 )
187class StartView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView):
188 """CBV for the eligibility verification getting started screen."""
190 template_name = "eligibility/start.html"
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)
196 def get_context_data(self, **kwargs):
197 context = super().get_context_data(**kwargs)
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 }
243 context.update(eligibility_start[self.flow.system_name].dict())
244 return context
247class ConfirmView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, RecaptchaEnabledMixin, FormView):
248 """View handler for Eligiblity Confirm form, used only by flows that support Eligibility API verification."""
250 template_name = "eligibility/confirm.html"
252 def get_form_class(self):
253 agency_slug = self.agency.slug
254 flow_system_name = self.flow.system_name
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 )
267 return form_class
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)
277 def post(self, request, *args, **kwargs):
278 analytics.started_eligibility(request, self.flow)
279 return super().post(request, *args, **kwargs)
281 def form_valid(self, form):
282 agency = self.agency
283 flow = self.flow
284 request = self.request
286 # make Eligibility Verification request to get the verified confirmation
287 is_verified = verify.eligibility_from_api(flow, form, agency)
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)
301 return redirect(routes.ENROLLMENT_INDEX)
303 def form_invalid(self, form):
304 if recaptcha.has_error(form):
305 messages.error(self.request, "Recaptcha failed. Please try again.")
307 return self.get(self.request)
310@dataclass
311class EligibilityUnverified:
312 headline_text: str
313 body_text: str
314 button_text: str
316 def dict(self):
317 return asdict(self)
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 )
333class UnverifiedView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView):
334 """CBV for the unverified eligibility page."""
336 template_name = "eligibility/unverified.html"
338 def get_context_data(self, **kwargs):
339 context = super().get_context_data(**kwargs)
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 }
349 context_object = eligibility_unverified.get(self.flow.system_name)
350 context.update(context_object.dict() if context_object else {})
351 return context
353 def get(self, request, *args, **kwargs):
354 analytics.returned_fail(request, self.flow)
355 return super().get(request, *args, **kwargs)