Coverage for benefits / enrollment / views.py: 98%
114 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 enrollment application: view definitions for the benefits enrollment flow.
3"""
5import logging
6from dataclasses import asdict, dataclass
7from typing import Any, Optional
9from django.template.defaultfilters import date
10from django.urls import reverse
11from django.views.generic import RedirectView, TemplateView
13from benefits.core import models, session
14from benefits.core.context import formatted_gettext_lazy as _
15from benefits.core.context.flow import SystemName
16from benefits.core.mixins import (
17 AgencySessionRequiredMixin,
18 EligibleSessionRequiredMixin,
19 FlowSessionRequiredMixin,
20 PageViewMixin,
21)
22from benefits.core.models import AgencySlug
23from benefits.routes import routes
25from . import analytics
27logger = logging.getLogger(__name__)
30@dataclass
31class EnrollmentIndex:
32 headline: str = _("Your eligibility is confirmed! You’re almost there.")
33 next_step: str = _("The next step is to enroll the contactless card you will use to tap to ride for a reduced fare.")
34 partner_post_link: str = _(", to enter your contactless card details.")
35 alert_include: Optional[str] = ""
37 def dict(self):
38 return asdict(self)
41@dataclass
42class AgencyCardEnrollmentIndex(EnrollmentIndex):
43 def __init__(self):
44 super().__init__(headline=_("We found your record! Now let’s enroll your contactless card."))
47@dataclass
48class CalFreshEnrollmentIndex(EnrollmentIndex):
49 def __init__(self):
50 super().__init__(
51 next_step=_("The next step is to connect your contactless card to your transit benefit"),
52 partner_post_link=".",
53 alert_include="enrollment/includes/alert-box--warning--calfresh.html",
54 )
57class IndexContextMixin(FlowSessionRequiredMixin):
58 def get_context_data(self, **kwargs) -> dict[str, Any]:
59 enrollment_index = {
60 SystemName.AGENCY_CARD: AgencyCardEnrollmentIndex(),
61 SystemName.COURTESY_CARD: AgencyCardEnrollmentIndex(),
62 SystemName.REDUCED_FARE_MOBILITY_ID: AgencyCardEnrollmentIndex(),
63 SystemName.CALFRESH: CalFreshEnrollmentIndex(),
64 }
65 context = enrollment_index.get(self.flow.system_name, EnrollmentIndex())
66 return context.dict()
69class IndexView(AgencySessionRequiredMixin, EligibleSessionRequiredMixin, RedirectView):
70 """CBV for the enrollment landing page."""
72 route_origin = routes.ENROLLMENT_INDEX
74 def get_redirect_url(self, *args, **kwargs):
75 route_name = self.agency.enrollment_index_route
76 return reverse(route_name)
78 def get(self, request, *args, **kwargs):
79 session.update(request, origin=reverse(self.route_origin))
80 return super().get(request, *args, **kwargs)
83class ReenrollmentErrorView(FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
84 """View handler for a re-enrollment attempt that is not yet within the re-enrollment window."""
86 template_name = "enrollment/reenrollment-error.html"
88 def get_context_data(self, **kwargs):
89 context = super().get_context_data(**kwargs)
91 request = self.request
93 flow = self.flow
94 expiry = session.enrollment_expiry(request)
95 reenrollment = session.enrollment_reenrollment(request)
97 if flow.system_name == SystemName.CALFRESH:
98 does_not_expire_until = _("Your CalFresh Cardholder transit benefit does not expire until")
99 reenroll_on = _("You can re-enroll for this benefit beginning on")
100 try_again = _("Please try again then.")
102 context["paragraphs"] = [
103 f"{does_not_expire_until} {date(expiry)}. {reenroll_on} {date(reenrollment)}. {try_again}"
104 ]
106 return context
108 def get(self, request, *args, **kwargs):
109 flow = self.flow
111 if session.logged_in(request) and flow.supports_sign_out:
112 # overwrite origin for a logged in user
113 # if they click the logout button, they are taken to the new route
114 session.update(request, origin=reverse(routes.LOGGED_OUT))
116 return super().get(request, *args, **kwargs)
119class RetryView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
120 """View handler for a recoverable failure condition."""
122 template_name = "enrollment/retry.html"
123 enrollment_method = models.EnrollmentMethods.DIGITAL
125 def dispatch(self, request, *args, **kwargs):
126 # for Littlepay, the Javascript in enrollment_littlepay/index.html sends a form POST to this view
127 # to simplify, we check the method here and process POSTs rather than implementing as a FormView, which requires
128 # instantiating the enrollment.forms.CardTokenizeFailForm, needing additional parameters such as a form id and
129 # action_url, that are already specified in the enrollment_littlepay/views.IndexView
130 #
131 # Switchio doesn't use this view at all
133 # call super().dispatch() first to ensure all mixins are processed (i.e. so we have a self.flow and self.agency)
134 response = super().dispatch(request, *args, **kwargs)
135 if request.method == "POST":
136 enrollment_group = self.flow.group_id
137 transit_processor = self.agency.transit_processor
138 analytics.returned_retry(
139 request,
140 enrollment_group=enrollment_group,
141 transit_processor=transit_processor,
142 enrollment_method=self.enrollment_method,
143 )
144 # TemplateView doesn't implement POST, just return the template via GET
145 return super().get(request, *args, **kwargs)
146 # for other request methods, we don't want/need to serve the retry template since users should only arrive via the form
147 # POST, returning super().dispatch() results in a 200 User Error response
148 return response
151class SystemErrorView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
152 """View handler for an enrollment system error."""
154 template_name = "enrollment/system_error.html"
156 def get_origin_url(self):
157 return self.agency.index_url
159 def get(self, request, *args, **kwargs):
160 # overwrite origin so that CTA takes user to agency index
161 origin_url = self.get_origin_url()
162 session.update(request, origin=origin_url)
163 return super().get(request, *args, **kwargs)
165 def post(self, request, *args, **kwargs):
166 # the Javascript in enrollment_littlepay/index.html and enrollment_switchio/index.html sends a form POST to this view
167 # rather than implementing this view as a FormView, which requires instantiating the
168 # enrollment.forms.CardTokenizeFailForm, we implement post() to simply return the template via get()
169 # we thus avoid interfering with the view's lifecycle and dispatch() method
170 return self.get(request, *args, **kwargs)
173@dataclass
174class EnrollmentSuccess:
175 success_message: str
176 thank_you_message: str
178 def dict(self):
179 return asdict(self)
182class DefaultEnrollmentSuccess(EnrollmentSuccess):
183 def __init__(self, transportation_type):
184 super().__init__(
185 success_message=_(
186 "You were not charged anything today. When boarding {transportation_type}, tap your contactless card and you "
187 "will be charged a reduced fare. You will need to re-enroll if you choose to change the card you use to "
188 "pay for transit service.",
189 transportation_type=transportation_type,
190 ),
191 thank_you_message=_("Thank you for using Cal-ITP Benefits!"),
192 )
195class AgencyCardEnrollmentSuccess(EnrollmentSuccess):
196 def __init__(self, transit_benefit, transportation_type):
197 super().__init__(
198 success_message=_(
199 "Your contactless card is now enrolled in {transit_benefit}. When boarding {transportation_type}, tap this "
200 "card and you will be charged a reduced fare. You will need to re-enroll if you choose to change the card you "
201 "use to pay for transit service.",
202 transit_benefit=transit_benefit,
203 transportation_type=transportation_type,
204 ),
205 thank_you_message=_("You were not charged anything today. Thank you for using Cal-ITP Benefits!"),
206 )
209class SuccessView(PageViewMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
210 """View handler for the final success page."""
212 template_name = "enrollment/success.html"
214 def get_context_data(self, **kwargs):
215 context = super().get_context_data(**kwargs)
217 request = self.request
218 flow = self.flow
220 context = {"redirect_to": request.path}
221 copy = {
222 AgencySlug.CST.value: DefaultEnrollmentSuccess(transportation_type=_("a CST bus")),
223 AgencySlug.EDCTA.value: DefaultEnrollmentSuccess(transportation_type=_("an EDCTA bus")),
224 AgencySlug.MST.value: DefaultEnrollmentSuccess(transportation_type=_("an MST bus")),
225 AgencySlug.NEVCO.value: DefaultEnrollmentSuccess(transportation_type=_("a Nevada County Connects bus")),
226 AgencySlug.RABA.value: DefaultEnrollmentSuccess(transportation_type=_("a RABA bus")),
227 AgencySlug.ROSEVILLE.value: DefaultEnrollmentSuccess(transportation_type=_("a Roseville bus")),
228 AgencySlug.SACRT.value: DefaultEnrollmentSuccess(transportation_type=_("a SacRT bus")),
229 AgencySlug.SLORTA.value: DefaultEnrollmentSuccess(transportation_type=_("a RTA bus")),
230 AgencySlug.SBMTD.value: DefaultEnrollmentSuccess(transportation_type=_("an SBMTD bus")),
231 AgencySlug.VCTC.value: DefaultEnrollmentSuccess(
232 transportation_type=_("a Ventura County Transportation Commission bus")
233 ),
234 SystemName.AGENCY_CARD.value: AgencyCardEnrollmentSuccess(
235 transit_benefit=_("a CST Agency Card transit benefit"), transportation_type=_("a CST bus")
236 ),
237 SystemName.COURTESY_CARD.value: AgencyCardEnrollmentSuccess(
238 transit_benefit=_("an MST Courtesy Card transit benefit"), transportation_type="an MST bus"
239 ),
240 SystemName.REDUCED_FARE_MOBILITY_ID.value: AgencyCardEnrollmentSuccess(
241 transit_benefit=_("an SBMTD Reduced Fare Mobility ID transit benefit"), transportation_type=_("an SBMTD bus")
242 ),
243 }
245 if flow.uses_api_verification: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 copy_context = copy[flow.system_name].dict()
247 else:
248 copy_context = copy[flow.transit_agency.slug].dict()
250 context.update(copy_context)
252 return context
254 def get(self, request, *args, **kwargs):
255 session.update(request, origin=reverse(routes.ENROLLMENT_SUCCESS))
257 flow = self.flow
259 if session.logged_in(request) and flow.supports_sign_out:
260 # overwrite origin for a logged in user
261 # if they click the logout button, they are taken to the new route
262 session.update(request, origin=reverse(routes.LOGGED_OUT))
264 return super().get(request, *args, **kwargs)