From 7c9a8b51cff0beb326745eb90f35beb0d9b3ba09 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 19 Sep 2024 19:18:55 +0000 Subject: [PATCH 1/6] chore: add needed context to each error view function this ensures the usertools section has what it needs to show up --- benefits/in_person/views.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index e63e40e1f..a399b2201 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -133,19 +133,27 @@ def enrollment(request): def reenrollment_error(request): - return TemplateResponse(request, "in_person/enrollment/reenrollment_error.html") + context = {**admin_site.each_context(request)} + + return TemplateResponse(request, "in_person/enrollment/reenrollment_error.html", context) def retry(request): - return TemplateResponse(request, "in_person/enrollment/retry.html") + context = {**admin_site.each_context(request)} + + return TemplateResponse(request, "in_person/enrollment/retry.html", context) def system_error(request): - return TemplateResponse(request, "in_person/enrollment/system_error.html") + context = {**admin_site.each_context(request)} + + return TemplateResponse(request, "in_person/enrollment/system_error.html", context) def server_error(request): - return TemplateResponse(request, "in_person/enrollment/server_error.html") + context = {**admin_site.each_context(request)} + + return TemplateResponse(request, "in_person/enrollment/server_error.html", context) def success(request): From 6da2574f453059f6af7454d10ae7ec8fcea105e9 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 19 Sep 2024 20:06:41 +0000 Subject: [PATCH 2/6] feat: introduce error base template, implement reenrollment error page --- benefits/in_person/templates/error-base.html | 25 +++++++++++++++++++ .../enrollment/reenrollment_error.html | 16 ++++++++++++ benefits/in_person/views.py | 4 +++ benefits/static/css/admin/styles.css | 13 ++++++++++ .../img/icon/exclamation-circle-fill.svg | 3 +++ tests/pytest/in_person/test_views.py | 3 +++ 6 files changed, 64 insertions(+) create mode 100644 benefits/in_person/templates/error-base.html create mode 100644 benefits/static/img/icon/exclamation-circle-fill.svg diff --git a/benefits/in_person/templates/error-base.html b/benefits/in_person/templates/error-base.html new file mode 100644 index 000000000..fae5a200c --- /dev/null +++ b/benefits/in_person/templates/error-base.html @@ -0,0 +1,25 @@ +{% extends "admin/agency-base.html" %} +{% load static %} + +{% block content %} +
+
+
+

In-person enrollment

+
+
+
+ +
+ {% block error-message %} + {% endblock error-message %} +
+
+
+ {% block cta-buttons %} + {% endblock cta-buttons %} +
+
+
+
+{% endblock content %} diff --git a/benefits/in_person/templates/in_person/enrollment/reenrollment_error.html b/benefits/in_person/templates/in_person/enrollment/reenrollment_error.html index e69de29bb..965293df8 100644 --- a/benefits/in_person/templates/in_person/enrollment/reenrollment_error.html +++ b/benefits/in_person/templates/in_person/enrollment/reenrollment_error.html @@ -0,0 +1,16 @@ +{% extends "error-base.html" %} + +{% block error-message %} +

This person is still enrolled in the {{ flow_label }} benefit.

+ +

+ This rider will enjoy a transit benefit until {{ enrollment.expires|date }}. They can re-enroll for this benefit beginning on {{ enrollment.reenrollment|date }}. Please try again then. +

+{% endblock error-message %} + +{% block cta-buttons %} +
+ {% url routes.ADMIN_INDEX as url_return_to_dashboard %} + Return to dashboard +
+{% endblock cta-buttons %} diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index a399b2201..3a042491b 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -133,8 +133,12 @@ def enrollment(request): def reenrollment_error(request): + """View handler for a re-enrollment attempt that is not yet within the re-enrollment window.""" context = {**admin_site.each_context(request)} + flow = session.flow(request) + context["flow_label"] = flow.label + return TemplateResponse(request, "in_person/enrollment/reenrollment_error.html", context) diff --git a/benefits/static/css/admin/styles.css b/benefits/static/css/admin/styles.css index 0f8602fd4..700dc970b 100644 --- a/benefits/static/css/admin/styles.css +++ b/benefits/static/css/admin/styles.css @@ -125,6 +125,19 @@ iframe.card-collection { min-height: 60vh; } +/* Error Pages */ +.error-icon { + background-color: var(--bs-danger); + mask: url("../../../static/img/icon/exclamation-circle-fill.svg") no-repeat; + -webkit-mask: url("../../../static/img/icon/exclamation-circle-fill.svg") + no-repeat; + + display: inline-block; + width: 64px; + height: 64px; + margin-right: calc(12rem / 16); +} + /* Login Page */ .login #header { padding: 0 !important; diff --git a/benefits/static/img/icon/exclamation-circle-fill.svg b/benefits/static/img/icon/exclamation-circle-fill.svg new file mode 100644 index 000000000..ca0b8ea40 --- /dev/null +++ b/benefits/static/img/icon/exclamation-circle-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index 0bbcd0f32..cc0d38cd4 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -305,11 +305,14 @@ def test_enrollment_post_valid_form_reenrollment_error(mocker, admin_client, car assert response.url == reverse(routes.IN_PERSON_ENROLLMENT_REENROLLMENT_ERROR) +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_flow") def test_reenrollment_error(admin_client): path = reverse(routes.IN_PERSON_ENROLLMENT_REENROLLMENT_ERROR) response = admin_client.get(path) + assert response.status_code == 200 assert response.template_name == "in_person/enrollment/reenrollment_error.html" From 1ba512303deecc597a37b811926ad47e56124ce1 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 20 Sep 2024 18:46:33 +0000 Subject: [PATCH 3/6] feat: implement user enrollment error page, aka the "retry" page --- .../templates/in_person/enrollment/retry.html | 20 +++++++++++++++++++ benefits/in_person/views.py | 1 + tests/pytest/in_person/test_views.py | 1 + 3 files changed, 22 insertions(+) diff --git a/benefits/in_person/templates/in_person/enrollment/retry.html b/benefits/in_person/templates/in_person/enrollment/retry.html index e69de29bb..ca306395b 100644 --- a/benefits/in_person/templates/in_person/enrollment/retry.html +++ b/benefits/in_person/templates/in_person/enrollment/retry.html @@ -0,0 +1,20 @@ +{% extends "error-base.html" %} + +{% block error-message %} +

Card not found.

+ +

+ The card information may not have been entered correctly. Please check the details on your card and try again. +

+{% endblock error-message %} + +{% block cta-buttons %} +
+ {% url routes.ADMIN_INDEX as url_cancel %} + Cancel +
+
+ {% url routes.IN_PERSON_ENROLLMENT as url_try_again %} + Try again +
+{% endblock cta-buttons %} diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index 3a042491b..2c401a8c6 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -143,6 +143,7 @@ def reenrollment_error(request): def retry(request): + """View handler for card verification failure.""" context = {**admin_site.each_context(request)} return TemplateResponse(request, "in_person/enrollment/retry.html", context) diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index cc0d38cd4..dcf49b8f1 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -321,6 +321,7 @@ def test_retry(admin_client): response = admin_client.get(path) + assert response.status_code == 200 assert response.template_name == "in_person/enrollment/retry.html" From 54c60d15b25b04ad2e7eba41112ecc32f813578b Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 20 Sep 2024 18:55:10 +0000 Subject: [PATCH 4/6] feat: implement system error page, send sentry notification add more assertions for system error cases --- .../in_person/enrollment/system_error.html | 14 ++++++++++++++ benefits/in_person/views.py | 2 ++ tests/pytest/in_person/test_views.py | 12 ++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/benefits/in_person/templates/in_person/enrollment/system_error.html b/benefits/in_person/templates/in_person/enrollment/system_error.html index e69de29bb..5a46b86b4 100644 --- a/benefits/in_person/templates/in_person/enrollment/system_error.html +++ b/benefits/in_person/templates/in_person/enrollment/system_error.html @@ -0,0 +1,14 @@ +{% extends "error-base.html" %} + +{% block error-message %} +

The enrollment system isn't working right now.

+ +

Please wait 24 hours and try to enroll again.

+{% endblock error-message %} + +{% block cta-buttons %} +
+ {% url routes.ADMIN_INDEX as url_return_to_dashboard %} + Return to dashboard +
+{% endblock cta-buttons %} diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index 2c401a8c6..97e2304ff 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -93,6 +93,7 @@ def enrollment(request): return redirect(routes.IN_PERSON_ENROLLMENT_SUCCESS) case Status.SYSTEM_ERROR: + sentry_sdk.capture_exception(exception) return redirect(routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR) case Status.EXCEPTION: @@ -150,6 +151,7 @@ def retry(request): def system_error(request): + """View handler for an enrollment system error.""" context = {**admin_site.each_context(request)} return TemplateResponse(request, "in_person/enrollment/system_error.html", context) diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index dcf49b8f1..31da2b353 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -22,6 +22,11 @@ def invalid_form_data(): return {"invalid": "data"} +@pytest.fixture +def mocked_sentry_sdk_module(mocker): + return mocker.patch.object(benefits.in_person.views, "sentry_sdk") + + @pytest.mark.django_db @pytest.mark.parametrize("viewname", [routes.IN_PERSON_ELIGIBILITY, routes.IN_PERSON_ENROLLMENT]) def test_view_not_logged_in(client, viewname): @@ -112,7 +117,7 @@ def test_token_valid(mocker, admin_client): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") -def test_token_system_error(mocker, admin_client): +def test_token_system_error(mocker, admin_client, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) mock_error = {"message": "Mock error message"} @@ -135,6 +140,7 @@ def test_token_system_error(mocker, admin_client): assert "token" not in data assert "redirect" in data assert data["redirect"] == reverse(routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR) + mocked_sentry_sdk_module.capture_exception.assert_called_once() @pytest.mark.django_db @@ -271,7 +277,7 @@ def test_enrollment_post_valid_form_success( @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "model_EnrollmentFlow") -def test_enrollment_post_valid_form_system_error(mocker, admin_client, card_tokenize_form_data): +def test_enrollment_post_valid_form_system_error(mocker, admin_client, card_tokenize_form_data, mocked_sentry_sdk_module): mocker.patch("benefits.in_person.views.enroll", return_value=(Status.SYSTEM_ERROR, None)) path = reverse(routes.IN_PERSON_ENROLLMENT) @@ -279,6 +285,7 @@ def test_enrollment_post_valid_form_system_error(mocker, admin_client, card_toke assert response.status_code == 302 assert response.url == reverse(routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR) + mocked_sentry_sdk_module.capture_exception.assert_called_once() @pytest.mark.django_db @@ -330,6 +337,7 @@ def test_system_error(admin_client): response = admin_client.get(path) + assert response.status_code == 200 assert response.template_name == "in_person/enrollment/system_error.html" From 93535f10790620564d716c10398f46aab5ff1ba0 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 20 Sep 2024 19:03:42 +0000 Subject: [PATCH 5/6] feat: implement server error page, send sentry notification add more assertions for server error cases. add missing assertion for success test. --- .../in_person/enrollment/server_error.html | 16 ++++++++++++++++ benefits/in_person/views.py | 1 + tests/pytest/in_person/test_views.py | 14 ++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/benefits/in_person/templates/in_person/enrollment/server_error.html b/benefits/in_person/templates/in_person/enrollment/server_error.html index e69de29bb..2302d6d3e 100644 --- a/benefits/in_person/templates/in_person/enrollment/server_error.html +++ b/benefits/in_person/templates/in_person/enrollment/server_error.html @@ -0,0 +1,16 @@ +{% extends "error-base.html" %} + +{% block error-message %} +

We're working to fix a problem.

+ +

+ There is a problem with the application configuration, but we're working to fix it. Please try again in a little while. +

+{% endblock error-message %} + +{% block cta-buttons %} +
+ {% url routes.ADMIN_INDEX as url_return_to_dashboard %} + Return to dashboard +
+{% endblock cta-buttons %} diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index 97e2304ff..39c525870 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -97,6 +97,7 @@ def enrollment(request): return redirect(routes.IN_PERSON_ENROLLMENT_SYSTEM_ERROR) case Status.EXCEPTION: + sentry_sdk.capture_exception(exception) return redirect(routes.IN_PERSON_SERVER_ERROR) case Status.REENROLLMENT_ERROR: diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index 31da2b353..75d4df171 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -145,7 +145,7 @@ def test_token_system_error(mocker, admin_client, mocked_sentry_sdk_module): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") -def test_token_http_error_400(mocker, admin_client): +def test_token_http_error_400(mocker, admin_client, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) mock_error = {"message": "Mock error message"} @@ -168,11 +168,12 @@ def test_token_http_error_400(mocker, admin_client): assert "token" not in data assert "redirect" in data assert data["redirect"] == reverse(routes.IN_PERSON_SERVER_ERROR) + mocked_sentry_sdk_module.capture_exception.assert_called_once() @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") -def test_token_misconfigured_client_id(mocker, admin_client): +def test_token_misconfigured_client_id(mocker, admin_client, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) exception = UnsupportedTokenTypeError() @@ -192,11 +193,12 @@ def test_token_misconfigured_client_id(mocker, admin_client): assert "token" not in data assert "redirect" in data assert data["redirect"] == reverse(routes.IN_PERSON_SERVER_ERROR) + mocked_sentry_sdk_module.capture_exception_assert_called_once() @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") -def test_token_connection_error(mocker, admin_client): +def test_token_connection_error(mocker, admin_client, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) exception = ConnectionError() @@ -216,6 +218,7 @@ def test_token_connection_error(mocker, admin_client): assert "token" not in data assert "redirect" in data assert data["redirect"] == reverse(routes.IN_PERSON_SERVER_ERROR) + mocked_sentry_sdk_module.capture_exception_assert_called_once() @pytest.mark.django_db @@ -290,7 +293,7 @@ def test_enrollment_post_valid_form_system_error(mocker, admin_client, card_toke @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "model_EnrollmentFlow") -def test_enrollment_post_valid_form_exception(mocker, admin_client, card_tokenize_form_data): +def test_enrollment_post_valid_form_exception(mocker, admin_client, card_tokenize_form_data, mocked_sentry_sdk_module): mocker.patch("benefits.in_person.views.enroll", return_value=(Status.EXCEPTION, None)) path = reverse(routes.IN_PERSON_ENROLLMENT) @@ -298,6 +301,7 @@ def test_enrollment_post_valid_form_exception(mocker, admin_client, card_tokeniz assert response.status_code == 302 assert response.url == reverse(routes.IN_PERSON_SERVER_ERROR) + mocked_sentry_sdk_module.capture_exception.assert_called_once() @pytest.mark.django_db @@ -346,6 +350,7 @@ def test_server_error(admin_client): response = admin_client.get(path) + assert response.status_code == 200 assert response.template_name == "in_person/enrollment/server_error.html" @@ -354,4 +359,5 @@ def test_success(admin_client): response = admin_client.get(path) + assert response.status_code == 200 assert response.template_name == "in_person/enrollment/success.html" From 8ef7dd137c8dd47449848364eb4a95a9e1002591 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 20 Sep 2024 19:23:28 +0000 Subject: [PATCH 6/6] chore: add missing docstrings for some in_person view functions --- benefits/in_person/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index 39c525870..6a390cde1 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -67,6 +67,7 @@ def token(request): def enrollment(request): + """View handler for the in-person enrollment page.""" # POST back after transit processor form, process card token if request.method == "POST": form = forms.CardTokenizeSuccessForm(request.POST) @@ -159,12 +160,14 @@ def system_error(request): def server_error(request): + """View handler for errors caused by a misconfiguration or bad request.""" context = {**admin_site.each_context(request)} return TemplateResponse(request, "in_person/enrollment/server_error.html", context) def success(request): + """View handler for the final success page.""" context = {**admin_site.each_context(request)} return TemplateResponse(request, "in_person/enrollment/success.html", context)