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

122 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-07-02 21:44 +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 get_card_types_for_js, 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_pretokenization_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 "card_types": get_card_types_for_js(), 

162 } 

163 

164 match agency.littlepay_config.environment: 

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

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

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

168 case models.Environment.PROD.value: 

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

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

171 case _: 

172 raise ValueError("Unrecognized environment value") 

173 

174 transit_processor_context = dict( 

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

176 ) 

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

178 

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

180 

181 

182def reenrollment_error(request): 

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

184 

185 agency = session.agency(request) 

186 context = { 

187 **admin_site.each_context(request), 

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

189 } 

190 

191 flow = session.flow(request) 

192 context["flow_label"] = flow.label 

193 

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

195 

196 

197def retry(request): 

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

199 # enforce POST-only route for sending analytics 

200 if request.method == "POST": 

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

202 

203 agency = session.agency(request) 

204 context = { 

205 **admin_site.each_context(request), 

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

207 } 

208 

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

210 

211 

212def system_error(request): 

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

214 agency = session.agency(request) 

215 context = { 

216 **admin_site.each_context(request), 

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

218 } 

219 

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

221 

222 

223def server_error(request): 

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

225 agency = session.agency(request) 

226 context = { 

227 **admin_site.each_context(request), 

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

229 } 

230 

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

232 

233 

234def success(request): 

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

236 agency = session.agency(request) 

237 context = { 

238 **admin_site.each_context(request), 

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

240 } 

241 

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