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

122 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-06 20:07 +0000

1import logging 

2 

3from django.contrib.admin import site as admin_site 

4from django.http import JsonResponse 

5from django.template.response import TemplateResponse 

6from django.shortcuts import redirect 

7from django.urls import reverse 

8import sentry_sdk 

9 

10 

11from benefits.routes import routes 

12from benefits.core import models, session 

13from benefits.eligibility import analytics as eligibility_analytics 

14from benefits.enrollment import analytics as enrollment_analytics 

15from benefits.enrollment.enrollment import Status 

16from benefits.enrollment_littlepay.enrollment import request_card_tokenization_access, enroll 

17from benefits.enrollment_littlepay.session import Session as LittlepaySession 

18 

19from benefits.in_person import forms 

20 

21logger = logging.getLogger(__name__) 

22 

23 

24def eligibility(request): 

25 """View handler for the in-person eligibility flow selection form.""" 

26 

27 agency = session.agency(request) 

28 context = { 

29 **admin_site.each_context(request), 

30 "form": forms.InPersonEligibilityForm(agency=agency), 

31 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

32 } 

33 

34 if request.method == "POST": 

35 form = forms.InPersonEligibilityForm(data=request.POST, agency=agency) 

36 

37 if form.is_valid(): 

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

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

40 session.update(request, flow=flow) 

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

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

43 

44 in_person_enrollment = reverse(routes.IN_PERSON_ENROLLMENT) 

45 response = redirect(in_person_enrollment) 

46 else: 

47 context["form"] = form 

48 response = TemplateResponse(request, "in_person/eligibility.html", context) 

49 else: 

50 response = TemplateResponse(request, "in_person/eligibility.html", context) 

51 

52 return response 

53 

54 

55def token(request): 

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

57 session = LittlepaySession(request) 

58 

59 if not session.access_token_valid(): 

60 response = request_card_tokenization_access(request) 

61 

62 if response.status is Status.SUCCESS: 

63 session.access_token = response.access_token 

64 session.access_token_expiry = response.expires_at 

65 elif response.status is Status.SYSTEM_ERROR or response.status is Status.EXCEPTION: 65 ↛ 80line 65 didn't jump to line 80 because the condition on line 65 was always true

66 logger.debug("Error occurred while requesting access token", exc_info=response.exception) 

67 sentry_sdk.capture_exception(response.exception) 

68 enrollment_analytics.failed_access_token_request( 

69 request, response.status_code, enrollment_method=models.EnrollmentMethods.IN_PERSON 

70 ) 

71 

72 if response.status is Status.SYSTEM_ERROR: 

73 redirect = reverse(routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR) 

74 else: 

75 redirect = reverse(routes.IN_PERSON_SERVER_ERROR) 

76 

77 data = {"redirect": redirect} 

78 return JsonResponse(data) 

79 

80 data = {"token": session.access_token} 

81 

82 return JsonResponse(data) 

83 

84 

85def enrollment(request): 

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

87 # POST back after transit processor form, process card token 

88 if request.method == "POST": 

89 form = forms.CardTokenizeSuccessForm(request.POST) 

90 if not form.is_valid(): 

91 raise Exception("Invalid card token form") 

92 

93 flow = session.flow(request) 

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

95 card_token = form.cleaned_data.get("card_token") 

96 status, exception = enroll(request, card_token) 

97 

98 match (status): 

99 case Status.SUCCESS: 

100 agency = session.agency(request) 

101 expiry = session.enrollment_expiry(request) 

102 verified_by = f"{request.user.first_name} {request.user.last_name}" 

103 event = models.EnrollmentEvent.objects.create( 

104 transit_agency=agency, 

105 enrollment_flow=flow, 

106 enrollment_method=models.EnrollmentMethods.IN_PERSON, 

107 verified_by=verified_by, 

108 expiration_datetime=expiry, 

109 ) 

110 event.save() 

111 enrollment_analytics.returned_success( 

112 request, flow.group_id, enrollment_method=models.EnrollmentMethods.IN_PERSON 

113 ) 

114 return redirect(routes.IN_PERSON_ENROLLMENT_SUCCESS) 

115 

116 case Status.SYSTEM_ERROR: 

117 enrollment_analytics.returned_error( 

118 request, str(exception), enrollment_method=models.EnrollmentMethods.IN_PERSON 

119 ) 

120 sentry_sdk.capture_exception(exception) 

121 return redirect(routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR) 

122 

123 case Status.EXCEPTION: 

124 enrollment_analytics.returned_error( 

125 request, str(exception), enrollment_method=models.EnrollmentMethods.IN_PERSON 

126 ) 

127 sentry_sdk.capture_exception(exception) 

128 return redirect(routes.IN_PERSON_SERVER_ERROR) 

129 

130 case Status.REENROLLMENT_ERROR: 130 ↛ exitline 130 didn't return from function 'enrollment' because the pattern on line 130 always matched

131 enrollment_analytics.returned_error( 

132 request, "Re-enrollment error.", enrollment_method=models.EnrollmentMethods.IN_PERSON 

133 ) 

134 return redirect(routes.IN_PERSON_ENROLLMENT_REENROLLMENT_ERROR) 

135 # GET enrollment index 

136 else: 

137 agency = session.agency(request) 

138 

139 tokenize_retry_form = forms.CardTokenizeFailForm(routes.IN_PERSON_ENROLLMENT_RETRY, "form-card-tokenize-fail-retry") 

140 tokenize_server_error_form = forms.CardTokenizeFailForm( 

141 routes.IN_PERSON_SERVER_ERROR, "form-card-tokenize-fail-server-error" 

142 ) 

143 tokenize_system_error_form = forms.CardTokenizeFailForm( 

144 routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR, "form-card-tokenize-fail-system-error" 

145 ) 

146 tokenize_success_form = forms.CardTokenizeSuccessForm( 

147 action_url=routes.IN_PERSON_ENROLLMENT, auto_id=True, label_suffix="" 

148 ) 

149 

150 context = { 

151 **admin_site.each_context(request), 

152 "forms": [tokenize_retry_form, tokenize_server_error_form, tokenize_system_error_form, tokenize_success_form], 

153 "cta_button": "tokenize_card", 

154 "enrollment_method": models.EnrollmentMethods.IN_PERSON, 

155 "token_field": "card_token", 

156 "form_retry": tokenize_retry_form.id, 

157 "form_server_error": tokenize_server_error_form.id, 

158 "form_success": tokenize_success_form.id, 

159 "form_system_error": tokenize_system_error_form.id, 

160 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

161 } 

162 

163 match agency.littlepay_config.environment: 

164 case models.Environment.QA.value: 164 ↛ 167line 164 didn't jump to line 167 because the pattern on line 164 always matched

165 url = "https://verify.qa.littlepay.com/assets/js/littlepay.min.js" 

166 card_tokenize_env = "https://verify.qa.littlepay.com" 

167 case models.Environment.PROD.value: 

168 url = "https://verify.littlepay.com/assets/js/littlepay.min.js" 

169 card_tokenize_env = "https://verify.littlepay.com" 

170 case _: 

171 raise ValueError("Unrecognized environment value") 

172 

173 transit_processor_context = dict( 

174 name="Littlepay", website="https://littlepay.com", card_tokenize_url=url, card_tokenize_env=card_tokenize_env 

175 ) 

176 context.update({"transit_processor": transit_processor_context}) 

177 

178 return TemplateResponse(request, "in_person/enrollment/index.html", context) 

179 

180 

181def reenrollment_error(request): 

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

183 

184 agency = session.agency(request) 

185 context = { 

186 **admin_site.each_context(request), 

187 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

188 } 

189 

190 flow = session.flow(request) 

191 context["flow_label"] = flow.label 

192 

193 return TemplateResponse(request, "in_person/enrollment/reenrollment_error.html", context) 

194 

195 

196def retry(request): 

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

198 # enforce POST-only route for sending analytics 

199 if request.method == "POST": 

200 enrollment_analytics.returned_retry(request, enrollment_method=models.EnrollmentMethods.IN_PERSON) 

201 

202 agency = session.agency(request) 

203 context = { 

204 **admin_site.each_context(request), 

205 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

206 } 

207 

208 return TemplateResponse(request, "in_person/enrollment/retry.html", context) 

209 

210 

211def system_error(request): 

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

213 agency = session.agency(request) 

214 context = { 

215 **admin_site.each_context(request), 

216 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

217 } 

218 

219 return TemplateResponse(request, "in_person/enrollment/system_error.html", context) 

220 

221 

222def server_error(request): 

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

224 agency = session.agency(request) 

225 context = { 

226 **admin_site.each_context(request), 

227 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

228 } 

229 

230 return TemplateResponse(request, "in_person/enrollment/server_error.html", context) 

231 

232 

233def success(request): 

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

235 agency = session.agency(request) 

236 context = { 

237 **admin_site.each_context(request), 

238 "title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}", 

239 } 

240 

241 return TemplateResponse(request, "in_person/enrollment/success.html", context)