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/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/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/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 e63e40e1f..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) @@ -93,9 +94,11 @@ 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: + sentry_sdk.capture_exception(exception) return redirect(routes.IN_PERSON_SERVER_ERROR) case Status.REENROLLMENT_ERROR: @@ -133,22 +136,38 @@ def enrollment(request): def reenrollment_error(request): - return TemplateResponse(request, "in_person/enrollment/reenrollment_error.html") + """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) def retry(request): - return TemplateResponse(request, "in_person/enrollment/retry.html") + """View handler for card verification failure.""" + 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") + """View handler for an enrollment system error.""" + 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") + """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) 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..75d4df171 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,11 +140,12 @@ 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 @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"} @@ -162,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() @@ -186,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() @@ -210,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 @@ -271,7 +280,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,11 +288,12 @@ 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 @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) @@ -291,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 @@ -305,11 +316,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" @@ -318,6 +332,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" @@ -326,6 +341,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" @@ -334,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" @@ -342,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"