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

1""" 

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

3""" 

4 

5from dataclasses import asdict, dataclass 

6import logging 

7 

8from django.template.defaultfilters import date 

9from django.urls import reverse 

10from django.views.generic import RedirectView, TemplateView 

11 

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 

24 

25 

26logger = logging.getLogger(__name__) 

27 

28 

29class IndexView(AgencySessionRequiredMixin, EligibleSessionRequiredMixin, RedirectView): 

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

31 

32 route_origin = routes.ENROLLMENT_INDEX 

33 

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

35 route_name = self.agency.enrollment_index_route 

36 return reverse(route_name) 

37 

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

39 session.update(request, origin=reverse(self.route_origin)) 

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

41 

42 

43class ReenrollmentErrorView(FlowSessionRequiredMixin, EligibleSessionRequiredMixin, TemplateView): 

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

45 

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

47 

48 def get_context_data(self, **kwargs): 

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

50 

51 request = self.request 

52 

53 flow = self.flow 

54 expiry = session.enrollment_expiry(request) 

55 reenrollment = session.enrollment_reenrollment(request) 

56 

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

61 

62 context["paragraphs"] = [ 

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

64 ] 

65 

66 return context 

67 

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

69 flow = self.flow 

70 

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

75 

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

77 

78 

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

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

81 

82 template_name = "enrollment/retry.html" 

83 enrollment_method = models.EnrollmentMethods.DIGITAL 

84 

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 

92 

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 

109 

110 

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

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

113 

114 template_name = "enrollment/system_error.html" 

115 

116 def get_origin_url(self): 

117 return self.agency.index_url 

118 

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) 

124 

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) 

131 

132 

133@dataclass 

134class EnrollmentSuccess: 

135 success_message: str 

136 thank_you_message: str 

137 

138 def dict(self): 

139 return asdict(self) 

140 

141 

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 ) 

153 

154 

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 ) 

167 

168 

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

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

171 

172 template_name = "enrollment/success.html" 

173 

174 def get_context_data(self, **kwargs): 

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

176 

177 request = self.request 

178 flow = self.flow 

179 

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 } 

203 

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

208 

209 context.update(copy_context) 

210 

211 return context 

212 

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

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

215 

216 flow = self.flow 

217 

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

222 

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