Coverage for benefits / in_person / views.py: 98%

116 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-01 15:39 +0000

1import logging 

2 

3from django.shortcuts import redirect 

4from django.urls import reverse 

5from django.views.generic import FormView, TemplateView 

6 

7from benefits.core import models, session 

8from benefits.core.mixins import AgencySessionRequiredMixin 

9from benefits.core.models.transit import TransitAgency 

10from benefits.core.views import AdditionalAgenciesView as DigitalAdditionalAgenciesView 

11from benefits.eligibility import analytics as eligibility_analytics 

12from benefits.enrollment.views import ( 

13 IndexView as DigitalEnrollmentIndexView, 

14 ReenrollmentErrorView as DigitalReenrollmentErrorView, 

15 RetryView as DigitalRetryView, 

16 SuccessView as DigitalSuccessView, 

17 SystemErrorView as DigitalSystemErrorView, 

18) 

19from benefits.enrollment_littlepay.session import Session as LittlepaySession 

20from benefits.enrollment_littlepay.views import IndexView as LittlepayIndexView, TokenView 

21from benefits.enrollment_switchio.session import Session as SwitchioSession 

22from benefits.enrollment_switchio.views import GatewayUrlView, IndexView as SwitchioIndexView 

23from benefits.in_person import forms, mixins 

24from benefits.routes import routes 

25 

26logger = logging.getLogger(__name__) 

27 

28 

29class AdditionalAgenciesView(mixins.CommonContextMixin, DigitalAdditionalAgenciesView): 

30 """View handler for showing the list of agencies the customer will be enrolled at (if more than one).""" 

31 

32 template_name = "in_person/additional-agencies.html" 

33 

34 

35class EligibilityView(mixins.CommonContextMixin, FormView): 

36 """CBV for the in-person eligibility flow selection form.""" 

37 

38 template_name = "in_person/eligibility.html" 

39 form_class = forms.InPersonEligibilityForm 

40 

41 def dispatch(self, request, *args, **kwargs): 

42 """Initialize session state before handling the request.""" 

43 

44 LittlepaySession(request, reset=True) 

45 SwitchioSession(request, reset=True) 

46 

47 agency = session.agency(request) 

48 if not agency: 

49 agency = TransitAgency.for_user(request.user) 

50 session.update(request, agency=agency) 

51 self.agency = agency 

52 return super().dispatch(request, *args, **kwargs) 

53 

54 def get_form_kwargs(self): 

55 """Return the keyword arguments for instantiating the form.""" 

56 

57 kwargs = super().get_form_kwargs() 

58 kwargs["agency"] = self.agency 

59 return kwargs 

60 

61 def form_valid(self, form): 

62 """If the form is valid, set enrollment flow, eligible session, and redirect.""" 

63 

64 flow_id = form.cleaned_data.get("flow") 

65 flow = models.EnrollmentFlow.objects.get(id=flow_id) 

66 session.update(self.request, flow=flow, eligible=True) 

67 eligibility_analytics.selected_flow(self.request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON) 

68 eligibility_analytics.started_eligibility(self.request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON) 

69 eligibility_analytics.returned_success(self.request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON) 

70 return redirect(routes.IN_PERSON_ENROLLMENT) 

71 

72 

73class LittlepayTokenView(TokenView): 

74 """View handler for the enrollment auth token.""" 

75 

76 enrollment_method = models.EnrollmentMethods.IN_PERSON 

77 route_system_error = routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR 

78 route_server_error = routes.IN_PERSON_SERVER_ERROR 

79 

80 

81class EnrollmentView(DigitalEnrollmentIndexView): 

82 

83 route_origin = routes.IN_PERSON_ENROLLMENT 

84 

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

86 route_name = self.agency.in_person_enrollment_index_route 

87 return reverse(route_name) 

88 

89 

90class LittlepayEnrollmentView(mixins.CommonContextMixin, LittlepayIndexView): 

91 """View handler for the in-person enrollment page.""" 

92 

93 enrollment_method = models.EnrollmentMethods.IN_PERSON 

94 route_enrollment_success = routes.IN_PERSON_ENROLLMENT_SUCCESS 

95 route_enrollment_retry = routes.IN_PERSON_ENROLLMENT_RETRY 

96 route_reenrollment_error = routes.IN_PERSON_ENROLLMENT_REENROLLMENT_ERROR 

97 route_server_error = routes.IN_PERSON_SERVER_ERROR 

98 route_system_error = routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR 

99 route_tokenize_success = routes.IN_PERSON_ENROLLMENT_LITTLEPAY_INDEX 

100 template_name = "in_person/enrollment/index_littlepay.html" 

101 

102 def _get_verified_by(self): 

103 return f"{self.request.user.first_name} {self.request.user.last_name}" 

104 

105 

106class ReenrollmentErrorView(mixins.CommonContextMixin, AgencySessionRequiredMixin, DigitalReenrollmentErrorView): 

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

108 

109 template_name = "in_person/enrollment/reenrollment_error.html" 

110 

111 def get_context_data(self, **kwargs): 

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

113 

114 context["flow_label"] = self.flow.label 

115 return context 

116 

117 

118class RetryView(mixins.CommonContextMixin, DigitalRetryView): 

119 """View handler for card verification failure.""" 

120 

121 template_name = "in_person/enrollment/retry.html" 

122 enrollment_method = models.EnrollmentMethods.IN_PERSON 

123 

124 

125class SystemErrorView(mixins.CommonContextMixin, DigitalSystemErrorView): 

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

127 

128 template_name = "in_person/enrollment/system_error.html" 

129 

130 def get_origin_url(self): 

131 return reverse(routes.ADMIN_INDEX) 

132 

133 

134class ServerErrorView(mixins.CommonContextMixin, AgencySessionRequiredMixin, TemplateView): 

135 """View handler for errors caused by a misconfiguration or bad request.""" 

136 

137 template_name = "in_person/enrollment/server_error.html" 

138 

139 def post(self, request, *args, **kwargs): 

140 # the Javascript in in_person/index_littlepay.html sends a form POST to this view 

141 # rather than implementing this view as a FormView, which requires instantiating the 

142 # enrollment.forms.CardTokenizeFailForm, we implement post() to simply return the template via get() 

143 # we thus avoid interfering with the view's lifecycle and dispatch() method 

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

145 

146 

147class SuccessView(mixins.CommonContextMixin, AgencySessionRequiredMixin, DigitalSuccessView): 

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

149 

150 template_name = "in_person/enrollment/success.html" 

151 

152 def get_context_data(self, **kwargs): 

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

154 

155 # Check DigitalSuccessView's agency_short_names to see if we have a group situation 

156 if context["agency_short_names"]: 

157 context["success_message"] = ( 

158 "This rider can now use their contactless card to automatically receive a reduced fare " 

159 "when they tap-to-ride at the following transit providers:" 

160 ) 

161 else: 

162 context["success_message"] = ( 

163 "This rider can now use their contactless card to automatically receive a reduced fare " 

164 "when they tap-to-ride." 

165 ) 

166 

167 return context 

168 

169 

170class SwitchioGatewayUrlView(GatewayUrlView): 

171 enrollment_method = models.EnrollmentMethods.IN_PERSON 

172 route_redirect = routes.IN_PERSON_ENROLLMENT_SWITCHIO_INDEX 

173 route_server_error = routes.IN_PERSON_SERVER_ERROR 

174 route_system_error = routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR 

175 

176 

177class SwitchioEnrollmentIndexView(mixins.CommonContextMixin, SwitchioIndexView): 

178 enrollment_method = models.EnrollmentMethods.IN_PERSON 

179 form_class = forms.CardTokenizeSuccessForm 

180 route_enrollment_success = routes.IN_PERSON_ENROLLMENT_SUCCESS 

181 route_reenrollment_error = routes.IN_PERSON_ENROLLMENT_REENROLLMENT_ERROR 

182 route_retry = routes.IN_PERSON_ENROLLMENT_RETRY 

183 route_server_error = routes.IN_PERSON_SERVER_ERROR 

184 route_system_error = routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR 

185 route_tokenize_success = routes.IN_PERSON_ENROLLMENT_SWITCHIO_INDEX 

186 template_name = "in_person/enrollment/index_switchio.html" 

187 

188 def _get_verified_by(self): 

189 return f"{self.request.user.first_name} {self.request.user.last_name}" 

190 

191 def get_context_data(self, **kwargs): 

192 """Add in-person specific context data.""" 

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

194 

195 if self.request.GET.get("state") == "tokenize": 

196 message = "Registering this contactless card for reduced fares..." 

197 else: 

198 message = "Connecting with payment processor..." 

199 

200 context.update({"loading_message": message}) 

201 return context 

202 

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

204 if request.GET.get("error") == "canceled": 204 ↛ 209line 204 didn't jump to line 209 because the condition on line 204 was always true

205 # the user clicked the "Back" button on the Switchio tokenization gateway 

206 # send them back to the Admin index, similar to the Littlepay cancel button 

207 return redirect(routes.ADMIN_INDEX) 

208 

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