Coverage for benefits / enrollment / views.py: 100%
104 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 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_processors import formatted_gettext_lazy as _
15from benefits.core.mixins import (
16 AgencySessionRequiredMixin,
17 EligibleSessionRequiredMixin,
18 FlowSessionRequiredMixin,
19 PageViewMixin,
20)
21from benefits.core.models import SystemName
22from benefits.routes import routes
24from . import analytics
26logger = logging.getLogger(__name__)
29@dataclass
30class EnrollmentIndex:
31 headline: str = _("Your eligibility is confirmed! You’re almost there.")
32 next_step: str = _("The next step is to enroll the contactless card you will use to tap to ride for a reduced fare.")
33 partner_post_link: str = _(", to enter your contactless card details.")
34 alert_include: Optional[str] = ""
36 def dict(self):
37 return asdict(self)
40@dataclass
41class AgencyCardEnrollmentIndex(EnrollmentIndex):
42 def __init__(self):
43 super().__init__(headline=_("We found your record! Now let’s enroll your contactless card."))
46@dataclass
47class CalFreshEnrollmentIndex(EnrollmentIndex):
48 def __init__(self):
49 super().__init__(
50 next_step=_("The next step is to connect your contactless card to your transit benefit"),
51 partner_post_link=".",
52 alert_include="enrollment/includes/alert-box--warning--calfresh.html",
53 )
56class IndexContextMixin(FlowSessionRequiredMixin):
57 def get_context_data(self, **kwargs) -> dict[str, Any]:
58 enrollment_index = {
59 SystemName.COURTESY_CARD: AgencyCardEnrollmentIndex(),
60 SystemName.REDUCED_FARE_MOBILITY_ID: AgencyCardEnrollmentIndex(),
61 SystemName.CALFRESH: CalFreshEnrollmentIndex(),
62 }
63 context = enrollment_index.get(self.flow.system_name, EnrollmentIndex())
64 return context.dict()
67class IndexView(AgencySessionRequiredMixin, EligibleSessionRequiredMixin, RedirectView):
68 """CBV for the enrollment landing page."""
70 route_origin = routes.ENROLLMENT_INDEX
72 def get_redirect_url(self, *args, **kwargs):
73 route_name = self.agency.enrollment_index_route
74 return reverse(route_name)
76 def get(self, request, *args, **kwargs):
77 flow = session.flow(request)
78 group = models.EnrollmentGroup.objects.get(transit_agency=self.agency, enrollment_flow=flow)
79 session.update(request, origin=reverse(self.route_origin), group=group)
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 agency = self.agency
137 enrollment_group = str(self.group.group_id) # needs to be a string for the API call
138 transit_processor = agency.transit_processor
139 analytics.returned_retry(
140 request,
141 agency=agency,
142 enrollment_group=enrollment_group,
143 transit_processor=transit_processor,
144 enrollment_method=self.enrollment_method,
145 )
146 # TemplateView doesn't implement POST, just return the template via GET
147 return super().get(request, *args, **kwargs)
148 # for other request methods, we don't want/need to serve the retry template since users should only arrive via the form
149 # POST, returning super().dispatch() results in a 200 User Error response
150 return response
153class SystemErrorView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
154 """View handler for an enrollment system error."""
156 template_name = "enrollment/system_error.html"
158 def get_origin_url(self):
159 return self.agency.index_url
161 def get(self, request, *args, **kwargs):
162 # overwrite origin so that CTA takes user to agency index
163 origin_url = self.get_origin_url()
164 session.update(request, origin=origin_url)
165 return super().get(request, *args, **kwargs)
167 def post(self, request, *args, **kwargs):
168 # the Javascript in enrollment_littlepay/index.html and enrollment_switchio/index.html sends a form POST to this view
169 # rather than implementing this view as a FormView, which requires instantiating the
170 # enrollment.forms.CardTokenizeFailForm, we implement post() to simply return the template via get()
171 # we thus avoid interfering with the view's lifecycle and dispatch() method
172 return self.get(request, *args, **kwargs)
175class SuccessView(PageViewMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView):
176 """View handler for the final success page."""
178 template_name = "enrollment/success.html"
180 def get_context_data(self, **kwargs):
181 context = super().get_context_data(**kwargs)
183 agency = self.agency
184 group_agencies = agency.group_agencies()
186 if group_agencies:
187 success_message = _(
188 "You were not charged anything today. When boarding public transit at the following providers, tap this card "
189 "and you will be charged a reduced fare:"
190 )
191 agency_short_names = agency.group_agency_short_names()
192 else:
193 success_message = _(
194 "You were not charged anything today. When boarding public transit provided by {short_name}, tap this "
195 "card and you will be charged a reduced fare.",
196 short_name=self.agency.short_name,
197 )
198 agency_short_names = None
200 context |= {
201 "redirect_to": self.request.path,
202 "success_message": success_message,
203 "agency_short_names": agency_short_names,
204 }
205 return context
207 def get(self, request, *args, **kwargs):
208 session.update(request, origin=reverse(routes.ENROLLMENT_SUCCESS))
210 flow = self.flow
212 if session.logged_in(request) and flow.supports_sign_out:
213 # overwrite origin for a logged in user
214 # if they click the logout button, they are taken to the new route
215 session.update(request, origin=reverse(routes.LOGGED_OUT))
217 return super().get(request, *args, **kwargs)