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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 15:39 +0000
1import logging
3from django.shortcuts import redirect
4from django.urls import reverse
5from django.views.generic import FormView, TemplateView
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
26logger = logging.getLogger(__name__)
29class AdditionalAgenciesView(mixins.CommonContextMixin, DigitalAdditionalAgenciesView):
30 """View handler for showing the list of agencies the customer will be enrolled at (if more than one)."""
32 template_name = "in_person/additional-agencies.html"
35class EligibilityView(mixins.CommonContextMixin, FormView):
36 """CBV for the in-person eligibility flow selection form."""
38 template_name = "in_person/eligibility.html"
39 form_class = forms.InPersonEligibilityForm
41 def dispatch(self, request, *args, **kwargs):
42 """Initialize session state before handling the request."""
44 LittlepaySession(request, reset=True)
45 SwitchioSession(request, reset=True)
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)
54 def get_form_kwargs(self):
55 """Return the keyword arguments for instantiating the form."""
57 kwargs = super().get_form_kwargs()
58 kwargs["agency"] = self.agency
59 return kwargs
61 def form_valid(self, form):
62 """If the form is valid, set enrollment flow, eligible session, and redirect."""
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)
73class LittlepayTokenView(TokenView):
74 """View handler for the enrollment auth token."""
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
81class EnrollmentView(DigitalEnrollmentIndexView):
83 route_origin = routes.IN_PERSON_ENROLLMENT
85 def get_redirect_url(self, *args, **kwargs):
86 route_name = self.agency.in_person_enrollment_index_route
87 return reverse(route_name)
90class LittlepayEnrollmentView(mixins.CommonContextMixin, LittlepayIndexView):
91 """View handler for the in-person enrollment page."""
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"
102 def _get_verified_by(self):
103 return f"{self.request.user.first_name} {self.request.user.last_name}"
106class ReenrollmentErrorView(mixins.CommonContextMixin, AgencySessionRequiredMixin, DigitalReenrollmentErrorView):
107 """View handler for a re-enrollment attempt that is not yet within the re-enrollment window."""
109 template_name = "in_person/enrollment/reenrollment_error.html"
111 def get_context_data(self, **kwargs):
112 context = super().get_context_data(**kwargs)
114 context["flow_label"] = self.flow.label
115 return context
118class RetryView(mixins.CommonContextMixin, DigitalRetryView):
119 """View handler for card verification failure."""
121 template_name = "in_person/enrollment/retry.html"
122 enrollment_method = models.EnrollmentMethods.IN_PERSON
125class SystemErrorView(mixins.CommonContextMixin, DigitalSystemErrorView):
126 """View handler for an enrollment system error."""
128 template_name = "in_person/enrollment/system_error.html"
130 def get_origin_url(self):
131 return reverse(routes.ADMIN_INDEX)
134class ServerErrorView(mixins.CommonContextMixin, AgencySessionRequiredMixin, TemplateView):
135 """View handler for errors caused by a misconfiguration or bad request."""
137 template_name = "in_person/enrollment/server_error.html"
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)
147class SuccessView(mixins.CommonContextMixin, AgencySessionRequiredMixin, DigitalSuccessView):
148 """View handler for the final success page."""
150 template_name = "in_person/enrollment/success.html"
152 def get_context_data(self, **kwargs):
153 context = super().get_context_data(**kwargs)
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 )
167 return context
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
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"
188 def _get_verified_by(self):
189 return f"{self.request.user.first_name} {self.request.user.last_name}"
191 def get_context_data(self, **kwargs):
192 """Add in-person specific context data."""
193 context = super().get_context_data(**kwargs)
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..."
200 context.update({"loading_message": message})
201 return context
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)
209 return super().get(request, *args, **kwargs)