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

1""" 

2The enrollment application: view definitions for the benefits enrollment flow. 

3""" 

4 

5import logging 

6from dataclasses import asdict, dataclass 

7from typing import Any, Optional 

8 

9from django.template.defaultfilters import date 

10from django.urls import reverse 

11from django.views.generic import RedirectView, TemplateView 

12 

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 

23 

24from . import analytics 

25 

26logger = logging.getLogger(__name__) 

27 

28 

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] = "" 

35 

36 def dict(self): 

37 return asdict(self) 

38 

39 

40@dataclass 

41class AgencyCardEnrollmentIndex(EnrollmentIndex): 

42 def __init__(self): 

43 super().__init__(headline=_("We found your record! Now let’s enroll your contactless card.")) 

44 

45 

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 ) 

54 

55 

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() 

65 

66 

67class IndexView(AgencySessionRequiredMixin, EligibleSessionRequiredMixin, RedirectView): 

68 """CBV for the enrollment landing page.""" 

69 

70 route_origin = routes.ENROLLMENT_INDEX 

71 

72 def get_redirect_url(self, *args, **kwargs): 

73 route_name = self.agency.enrollment_index_route 

74 return reverse(route_name) 

75 

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) 

81 

82 

83class ReenrollmentErrorView(FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView): 

84 """View handler for a re-enrollment attempt that is not yet within the re-enrollment window.""" 

85 

86 template_name = "enrollment/reenrollment-error.html" 

87 

88 def get_context_data(self, **kwargs): 

89 context = super().get_context_data(**kwargs) 

90 

91 request = self.request 

92 

93 flow = self.flow 

94 expiry = session.enrollment_expiry(request) 

95 reenrollment = session.enrollment_reenrollment(request) 

96 

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.") 

101 

102 context["paragraphs"] = [ 

103 f"{does_not_expire_until} {date(expiry)}. {reenroll_on} {date(reenrollment)}. {try_again}" 

104 ] 

105 

106 return context 

107 

108 def get(self, request, *args, **kwargs): 

109 flow = self.flow 

110 

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)) 

115 

116 return super().get(request, *args, **kwargs) 

117 

118 

119class RetryView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView): 

120 """View handler for a recoverable failure condition.""" 

121 

122 template_name = "enrollment/retry.html" 

123 enrollment_method = models.EnrollmentMethods.DIGITAL 

124 

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 

132 

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 

151 

152 

153class SystemErrorView(AgencySessionRequiredMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView): 

154 """View handler for an enrollment system error.""" 

155 

156 template_name = "enrollment/system_error.html" 

157 

158 def get_origin_url(self): 

159 return self.agency.index_url 

160 

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) 

166 

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) 

173 

174 

175class SuccessView(PageViewMixin, FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView): 

176 """View handler for the final success page.""" 

177 

178 template_name = "enrollment/success.html" 

179 

180 def get_context_data(self, **kwargs): 

181 context = super().get_context_data(**kwargs) 

182 

183 agency = self.agency 

184 group_agencies = agency.group_agencies() 

185 

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 

199 

200 context |= { 

201 "redirect_to": self.request.path, 

202 "success_message": success_message, 

203 "agency_short_names": agency_short_names, 

204 } 

205 return context 

206 

207 def get(self, request, *args, **kwargs): 

208 session.update(request, origin=reverse(routes.ENROLLMENT_SUCCESS)) 

209 

210 flow = self.flow 

211 

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)) 

216 

217 return super().get(request, *args, **kwargs)