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
« 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"""
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_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
19from . import analytics, forms, verify
22class IndexView(AgencySessionRequiredMixin, RecaptchaEnabledMixin, FormView):
23 """View handler for the enrollment flow selection form."""
25 template_name = "eligibility/index.html"
26 form_class = forms.EnrollmentFlowSelectionForm
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
34 def get_context_data(self, **kwargs):
35 """Add agency-specific context data."""
36 context = super().get_context_data(**kwargs)
38 agency = self.agency
39 group_agencies = agency.group_agencies()
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
55 context.update({"form_text": [form_text], "previous_url": previous_url})
56 return context
58 def get(self, request, *args, **kwargs):
59 """Initialize session state before handling the request."""
61 session.update(request, eligible=False, origin=self.agency.index_url)
62 session.logout(request)
64 return super().get(request, *args, **kwargs)
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)
72 analytics.selected_flow(self.request, flow)
73 return redirect(routes.ELIGIBILITY_START)
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)
82@dataclass
83class CTAButton:
84 text: str
85 route: str
86 fallback_text: Optional[str] = None
87 extra_classes: Optional[str] = None
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
98 def dict(self):
99 return asdict(self)
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 )
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 )
127class StartView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView):
128 """CBV for the eligibility verification getting started screen."""
130 template_name = "eligibility/start.html"
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)
136 def get_context_data(self, **kwargs):
137 context = super().get_context_data(**kwargs)
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 }
176 context.update(eligibility_start[self.flow.system_name].dict())
177 return context
180class ConfirmView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, RecaptchaEnabledMixin, FormView):
181 """View handler for Eligiblity Confirm form, used only by flows that support Eligibility API verification."""
183 template_name = "eligibility/confirm.html"
185 def get_form_class(self):
186 flow_system_name = self.flow.system_name
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.")
195 return form_class
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)
205 def post(self, request, *args, **kwargs):
206 analytics.started_eligibility(request, self.flow)
207 return super().post(request, *args, **kwargs)
209 def form_valid(self, form):
210 agency = self.agency
211 flow = self.flow
212 request = self.request
214 # make Eligibility Verification request to get the verified confirmation
215 is_verified = verify.eligibility_from_api(flow, form, agency)
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)
229 return redirect(routes.ENROLLMENT_INDEX)
231 def form_invalid(self, form):
232 if recaptcha.has_error(form):
233 messages.error(self.request, "Recaptcha failed. Please try again.")
235 return self.get(self.request)
238@dataclass
239class EligibilityUnverified:
240 headline_text: str
241 body_text: str
242 button_text: str
244 def dict(self):
245 return asdict(self)
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 )
261class UnverifiedView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, TemplateView):
262 """CBV for the unverified eligibility page."""
264 template_name = "eligibility/unverified.html"
266 def get_context_data(self, **kwargs):
267 context = super().get_context_data(**kwargs)
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 }
276 context_object = eligibility_unverified.get(self.flow.system_name)
277 context.update(context_object.dict() if context_object else {})
278 return context
280 def get(self, request, *args, **kwargs):
281 analytics.returned_fail(request, self.flow)
282 return super().get(request, *args, **kwargs)