Coverage for benefits / enrollment / views.py: 98%
92 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-22 19:08 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-22 19:08 +0000
1"""
2The enrollment application: view definitions for the benefits enrollment flow.
3"""
5from dataclasses import asdict, dataclass
6import logging
8from django.template.defaultfilters import date
9from django.urls import reverse
10from django.views.generic import RedirectView, TemplateView
12from benefits.core.context.agency import AgencySlug
13from benefits.core.context.flow import SystemName
14from benefits.core.context import formatted_gettext_lazy as _
15from benefits.routes import routes
16from benefits.core import models, session
17from benefits.core.mixins import (
18 AgencySessionRequiredMixin,
19 EligibleSessionRequiredMixin,
20 FlowSessionRequiredMixin,
21 PageViewMixin,
22)
23from . import analytics
26logger = logging.getLogger(__name__)
29class IndexView(AgencySessionRequiredMixin, EligibleSessionRequiredMixin, RedirectView):
30 """CBV for the enrollment landing page."""
32 route_origin = routes.ENROLLMENT_INDEX
34 def get_redirect_url(self, *args, **kwargs):
35 route_name = self.agency.enrollment_index_route
36 return reverse(route_name)
38 def get(self, request, *args, **kwargs):
39 session.update(request, origin=reverse(self.route_origin))
40 return super().get(request, *args, **kwargs)
43class ReenrollmentErrorView(FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
44 """View handler for a re-enrollment attempt that is not yet within the re-enrollment window."""
46 template_name = "enrollment/reenrollment-error.html"
48 def get_context_data(self, **kwargs):
49 context = super().get_context_data(**kwargs)
51 request = self.request
53 flow = self.flow
54 expiry = session.enrollment_expiry(request)
55 reenrollment = session.enrollment_reenrollment(request)
57 if flow.system_name == SystemName.CALFRESH:
58 does_not_expire_until = _("Your CalFresh Cardholder transit benefit does not expire until")
59 reenroll_on = _("You can re-enroll for this benefit beginning on")
60 try_again = _("Please try again then.")
62 context["paragraphs"] = [
63 f"{does_not_expire_until} {date(expiry)}. {reenroll_on} {date(reenrollment)}. {try_again}"
64 ]
66 return context
68 def get(self, request, *args, **kwargs):
69 flow = self.flow
71 if session.logged_in(request) and flow.supports_sign_out:
72 # overwrite origin for a logged in user
73 # if they click the logout button, they are taken to the new route
74 session.update(request, origin=reverse(routes.LOGGED_OUT))
76 return super().get(request, *args, **kwargs)
79class RetryView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
80 """View handler for a recoverable failure condition."""
82 template_name = "enrollment/retry.html"
83 enrollment_method = models.EnrollmentMethods.DIGITAL
85 def dispatch(self, request, *args, **kwargs):
86 # for Littlepay, the Javascript in enrollment_littlepay/index.html sends a form POST to this view
87 # to simplify, we check the method here and process POSTs rather than implementing as a FormView, which requires
88 # instantiating the enrollment.forms.CardTokenizeFailForm, needing additional parameters such as a form id and
89 # action_url, that are already specified in the enrollment_littlepay/views.IndexView
90 #
91 # Switchio doesn't use this view at all
93 # call super().dispatch() first to ensure all mixins are processed (i.e. so we have a self.flow and self.agency)
94 response = super().dispatch(request, *args, **kwargs)
95 if request.method == "POST":
96 enrollment_group = self.flow.group_id
97 transit_processor = self.agency.transit_processor
98 analytics.returned_retry(
99 request,
100 enrollment_group=enrollment_group,
101 transit_processor=transit_processor,
102 enrollment_method=self.enrollment_method,
103 )
104 # TemplateView doesn't implement POST, just return the template via GET
105 return super().get(request, *args, **kwargs)
106 # for other request methods, we don't want/need to serve the retry template since users should only arrive via the form
107 # POST, returning super().dispatch() results in a 200 User Error response
108 return response
111class SystemErrorView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
112 """View handler for an enrollment system error."""
114 template_name = "enrollment/system_error.html"
116 def get_origin_url(self):
117 return self.agency.index_url
119 def get(self, request, *args, **kwargs):
120 # overwrite origin so that CTA takes user to agency index
121 origin_url = self.get_origin_url()
122 session.update(request, origin=origin_url)
123 return super().get(request, *args, **kwargs)
125 def post(self, request, *args, **kwargs):
126 # the Javascript in enrollment_littlepay/index.html and enrollment_switchio/index.html sends a form POST to this view
127 # rather than implementing this view as a FormView, which requires instantiating the
128 # enrollment.forms.CardTokenizeFailForm, we implement post() to simply return the template via get()
129 # we thus avoid interfering with the view's lifecycle and dispatch() method
130 return self.get(request, *args, **kwargs)
133@dataclass
134class EnrollmentSuccess:
135 success_message: str
136 thank_you_message: str
138 def dict(self):
139 return asdict(self)
142class DefaultEnrollmentSuccess(EnrollmentSuccess):
143 def __init__(self, transportation_type):
144 super().__init__(
145 success_message=_(
146 "You were not charged anything today. When boarding {transportation_type}, tap your contactless card and you "
147 "will be charged a reduced fare. You will need to re-enroll if you choose to change the card you use to "
148 "pay for transit service.",
149 transportation_type=transportation_type,
150 ),
151 thank_you_message=_("Thank you for using Cal-ITP Benefits!"),
152 )
155class AgencyCardEnrollmentSuccess(EnrollmentSuccess):
156 def __init__(self, transit_benefit, transportation_type):
157 super().__init__(
158 success_message=_(
159 "Your contactless card is now enrolled in {transit_benefit}. When boarding {transportation_type}, tap this "
160 "card and you will be charged a reduced fare. You will need to re-enroll if you choose to change the card you "
161 "use to pay for transit service.",
162 transit_benefit=transit_benefit,
163 transportation_type=transportation_type,
164 ),
165 thank_you_message=_("You were not charged anything today. Thank you for using Cal-ITP Benefits!"),
166 )
169class SuccessView(PageViewMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
170 """View handler for the final success page."""
172 template_name = "enrollment/success.html"
174 def get_context_data(self, **kwargs):
175 context = super().get_context_data(**kwargs)
177 request = self.request
178 flow = self.flow
180 context = {"redirect_to": request.path}
181 copy = {
182 AgencySlug.CST.value: DefaultEnrollmentSuccess(transportation_type=_("a CST bus")),
183 AgencySlug.EDCTA.value: DefaultEnrollmentSuccess(transportation_type=_("an EDCTA bus")),
184 AgencySlug.MST.value: DefaultEnrollmentSuccess(transportation_type=_("an MST bus")),
185 AgencySlug.NEVCO.value: DefaultEnrollmentSuccess(transportation_type=_("a Nevada County Connects bus")),
186 AgencySlug.RABA.value: DefaultEnrollmentSuccess(transportation_type=_("a RABA bus")),
187 AgencySlug.SACRT.value: DefaultEnrollmentSuccess(transportation_type=_("a SacRT bus")),
188 AgencySlug.SLORTA.value: DefaultEnrollmentSuccess(transportation_type=_("a RTA bus")),
189 AgencySlug.SBMTD.value: DefaultEnrollmentSuccess(transportation_type=_("an SBMTD bus")),
190 AgencySlug.VCTC.value: DefaultEnrollmentSuccess(
191 transportation_type=_("a Ventura County Transportation Commission bus")
192 ),
193 SystemName.AGENCY_CARD.value: AgencyCardEnrollmentSuccess(
194 transit_benefit=_("a CST Agency Card transit benefit"), transportation_type=_("a CST bus")
195 ),
196 SystemName.COURTESY_CARD.value: AgencyCardEnrollmentSuccess(
197 transit_benefit=_("an MST Courtesy Card transit benefit"), transportation_type="an MST bus"
198 ),
199 SystemName.REDUCED_FARE_MOBILITY_ID.value: AgencyCardEnrollmentSuccess(
200 transit_benefit=_("an SBMTD Reduced Fare Mobility ID transit benefit"), transportation_type=_("an SBMTD bus")
201 ),
202 }
204 if flow.uses_api_verification: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 copy_context = copy[flow.system_name].dict()
206 else:
207 copy_context = copy[flow.transit_agency.slug].dict()
209 context.update(copy_context)
211 return context
213 def get(self, request, *args, **kwargs):
214 session.update(request, origin=reverse(routes.ENROLLMENT_SUCCESS))
216 flow = self.flow
218 if session.logged_in(request) and flow.supports_sign_out:
219 # overwrite origin for a logged in user
220 # if they click the logout button, they are taken to the new route
221 session.update(request, origin=reverse(routes.LOGGED_OUT))
223 return super().get(request, *args, **kwargs)