From 2e048be02fab8475c2f6e630f756df8e03f642f1 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 28 Mar 2024 23:06:40 +0000 Subject: [PATCH 1/4] feat(models): field configures a re-enrollment error template --- ...gibilitytype_reenrollment_error_template.py | 18 ++++++++++++++++++ benefits/core/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 benefits/core/migrations/0009_eligibilitytype_reenrollment_error_template.py diff --git a/benefits/core/migrations/0009_eligibilitytype_reenrollment_error_template.py b/benefits/core/migrations/0009_eligibilitytype_reenrollment_error_template.py new file mode 100644 index 000000000..d0abd30cf --- /dev/null +++ b/benefits/core/migrations/0009_eligibilitytype_reenrollment_error_template.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-05-01 19:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0008_eligibilityverifier_unverified_template"), + ] + + operations = [ + migrations.AddField( + model_name="eligibilitytype", + name="reenrollment_error_template", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/benefits/core/models.py b/benefits/core/models.py index a2f763c29..ed5f6122e 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -114,6 +114,7 @@ class EligibilityType(models.Model): expiration_days = models.PositiveSmallIntegerField(null=True, blank=True) expiration_reenrollment_days = models.PositiveSmallIntegerField(null=True, blank=True) enrollment_index_template = models.TextField(default="enrollment/index.html") + reenrollment_error_template = models.TextField(null=True, blank=True) def __str__(self): return self.label From bac56cde944bf54dc10fdd5cb2a42e88ca1af03e Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 28 Mar 2024 23:11:52 +0000 Subject: [PATCH 2/4] feat(enrollment): base and CalFresh template for re-enrollment error --- .../reenrollment-error--calfresh.html | 10 ++++++ .../enrollment/reenrollment-error-base.html | 31 +++++++++++++++++++ benefits/locale/en/LC_MESSAGES/django.po | 15 +++++++++ benefits/locale/es/LC_MESSAGES/django.po | 15 +++++++++ 4 files changed, 71 insertions(+) create mode 100644 benefits/enrollment/templates/enrollment/reenrollment-error--calfresh.html create mode 100644 benefits/enrollment/templates/enrollment/reenrollment-error-base.html diff --git a/benefits/enrollment/templates/enrollment/reenrollment-error--calfresh.html b/benefits/enrollment/templates/enrollment/reenrollment-error--calfresh.html new file mode 100644 index 000000000..f9a3391c6 --- /dev/null +++ b/benefits/enrollment/templates/enrollment/reenrollment-error--calfresh.html @@ -0,0 +1,10 @@ +{% extends "enrollment/reenrollment-error-base.html" %} +{% load i18n %} + +{% block error-message %} +

+ {% translate "Your CalFresh Cardholder transit benefit does not expire until" %} {{ enrollment.expires|date }}. + {% translate "You can re-enroll for this benefit beginning on" %} {{ enrollment.reenrollment|date }}. + {% translate "Please try again then." %} +

+{% endblock error-message %} diff --git a/benefits/enrollment/templates/enrollment/reenrollment-error-base.html b/benefits/enrollment/templates/enrollment/reenrollment-error-base.html new file mode 100644 index 000000000..720bc8131 --- /dev/null +++ b/benefits/enrollment/templates/enrollment/reenrollment-error-base.html @@ -0,0 +1,31 @@ +{% extends "core/base.html" %} +{% load i18n %} + +{% block page-title %} + {% translate "Enrollment error" %} +{% endblock page-title %} + +{% block main-content %} + {% if authentication and authentication.sign_out_link_template %} + {% include authentication.sign_out_link_template %} + {% endif %} + +
+

+ {% include "core/includes/icon.html" with name="calendarcheck" %} + {% translate "You are still enrolled in this benefit" %} +

+ +
+
+ {% block error-message %} + {% endblock error-message %} +
+
+ +
+
{% include "core/includes/button--index.html" %}
+
+ +
+{% endblock main-content %} diff --git a/benefits/locale/en/LC_MESSAGES/django.po b/benefits/locale/en/LC_MESSAGES/django.po index 71d0c92be..5c2984fe0 100644 --- a/benefits/locale/en/LC_MESSAGES/django.po +++ b/benefits/locale/en/LC_MESSAGES/django.po @@ -615,6 +615,21 @@ msgstr "" msgid "Enroll" msgstr "" +msgid "Your CalFresh Cardholder transit benefit does not expire until" +msgstr "" + +msgid "You can re-enroll for this benefit beginning on" +msgstr "" + +msgid "Please try again then." +msgstr "" + +msgid "Enrollment error" +msgstr "" + +msgid "You are still enrolled in this benefit" +msgstr "" + msgid "Unable to register card" msgstr "" diff --git a/benefits/locale/es/LC_MESSAGES/django.po b/benefits/locale/es/LC_MESSAGES/django.po index 964c902cf..c8da9dfd8 100644 --- a/benefits/locale/es/LC_MESSAGES/django.po +++ b/benefits/locale/es/LC_MESSAGES/django.po @@ -731,6 +731,21 @@ msgstr "Espere por favor..." msgid "Enroll" msgstr "Inscribir" +msgid "Your CalFresh Cardholder transit benefit does not expire until" +msgstr "" + +msgid "You can re-enroll for this benefit beginning on" +msgstr "" + +msgid "Please try again then." +msgstr "" + +msgid "Enrollment error" +msgstr "" + +msgid "You are still enrolled in this benefit" +msgstr "" + msgid "Unable to register card" msgstr "No se pudo registrar la tarjeta" From 1bbc20cc915f456d5b9e3f1689e0af9c031eadc1 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 28 Mar 2024 23:12:56 +0000 Subject: [PATCH 3/4] feat(enrollment): view handler for re-enrollment error --- benefits/enrollment/urls.py | 1 + benefits/enrollment/views.py | 28 +++++++++++++---- tests/pytest/conftest.py | 1 + tests/pytest/enrollment/test_views.py | 43 +++++++++++++++++++++++---- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/benefits/enrollment/urls.py b/benefits/enrollment/urls.py index 22398bc98..f74eb2452 100644 --- a/benefits/enrollment/urls.py +++ b/benefits/enrollment/urls.py @@ -12,6 +12,7 @@ # /enrollment path("", views.index, name="index"), path("token", views.token, name="token"), + path("reenrollment-error", views.reenrollment_error, name="reenrollment-error"), path("retry", views.retry, name="retry"), path("success", views.success, name="success"), ] diff --git a/benefits/enrollment/views.py b/benefits/enrollment/views.py index 6ea2edf29..b6191e811 100644 --- a/benefits/enrollment/views.py +++ b/benefits/enrollment/views.py @@ -12,16 +12,13 @@ from requests.exceptions import HTTPError from benefits.core import session -from benefits.core.middleware import ( - EligibleSessionRequired, - VerifierSessionRequired, - pageview_decorator, -) +from benefits.core.middleware import EligibleSessionRequired, VerifierSessionRequired, pageview_decorator from benefits.core.views import ROUTE_LOGGED_OUT -from . import analytics, forms +from . import analytics, forms ROUTE_INDEX = "enrollment:index" +ROUTE_REENROLLMENT_ERROR = "enrollment:reenrollment-error" ROUTE_RETRY = "enrollment:retry" ROUTE_SUCCESS = "enrollment:success" ROUTE_TOKEN = "enrollment:token" @@ -122,6 +119,25 @@ def index(request): return TemplateResponse(request, eligibility.enrollment_index_template, context) +@decorator_from_middleware(EligibleSessionRequired) +def reenrollment_error(request): + """View handler for a re-enrollment attempt that is not yet within the re-enrollment window.""" + eligibility = session.eligibility(request) + verifier = session.verifier(request) + + if eligibility.reenrollment_error_template is None: + raise Exception(f"Re-enrollment error with null template on: {eligibility.label}") + + if session.logged_in(request) and verifier.auth_provider.supports_sign_out: + # overwrite origin for a logged in user + # if they click the logout button, they are taken to the new route + session.update(request, origin=reverse(ROUTE_LOGGED_OUT)) + + analytics.returned_error(request, "Re-enrollment error.") + + return TemplateResponse(request, eligibility.reenrollment_error_template) + + @decorator_from_middleware(EligibleSessionRequired) def retry(request): """View handler for a recoverable failure condition.""" diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index c10093d6d..2dfb4aa48 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -139,6 +139,7 @@ def model_EligibilityType_supports_expiration(model_EligibilityType): model_EligibilityType.supports_expiration = True model_EligibilityType.expiration_days = 365 model_EligibilityType.expiration_reenrollment_days = 14 + model_EligibilityType.reenrollment_error_template = "enrollment/reenrollment-error--calfresh.html" model_EligibilityType.save() return model_EligibilityType diff --git a/tests/pytest/enrollment/test_views.py b/tests/pytest/enrollment/test_views.py index fba0b7c53..dfec565db 100644 --- a/tests/pytest/enrollment/test_views.py +++ b/tests/pytest/enrollment/test_views.py @@ -1,24 +1,23 @@ import time +import pytest from django.urls import reverse - from littlepay.api.funding_sources import FundingSourceResponse from requests import HTTPError -import pytest +import benefits.enrollment.views from benefits.core.middleware import TEMPLATE_USER_ERROR from benefits.core.views import ROUTE_LOGGED_OUT from benefits.enrollment.views import ( ROUTE_INDEX, - ROUTE_TOKEN, - ROUTE_SUCCESS, + ROUTE_REENROLLMENT_ERROR, ROUTE_RETRY, + ROUTE_SUCCESS, + ROUTE_TOKEN, TEMPLATE_SUCCESS, TEMPLATE_RETRY, ) -import benefits.enrollment.views - @pytest.fixture def card_tokenize_form_data(): @@ -211,6 +210,38 @@ def test_index_ineligible(client): assert response.template_name == TEMPLATE_USER_ERROR +@pytest.mark.django_db +def test_reenrollment_error_ineligible(client): + path = reverse(ROUTE_REENROLLMENT_ERROR) + + response = client.get(path) + + assert response.status_code == 200 + assert response.template_name == TEMPLATE_USER_ERROR + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier", "mocked_session_eligibility") +def test_reenrollment_error_eligibility_no_error_template(client): + path = reverse(ROUTE_REENROLLMENT_ERROR) + + with pytest.raises(Exception, match="Re-enrollment error with null template"): + client.get(path) + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_verifier") +def test_reenrollment_error(client, model_EligibilityType_supports_expiration, mocked_session_eligibility): + mocked_session_eligibility.return_value = model_EligibilityType_supports_expiration + + path = reverse(ROUTE_REENROLLMENT_ERROR) + + response = client.get(path) + + assert response.status_code == 200 + assert response.template_name == model_EligibilityType_supports_expiration.reenrollment_error_template + + @pytest.mark.django_db def test_retry_ineligible(client): path = reverse(ROUTE_RETRY) From c9d4716d192fafc4cf43ec182affc27adf02beab Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 1 May 2024 19:28:25 +0000 Subject: [PATCH 4/4] fix(models): renenrollment error template required if expiration supported --- benefits/core/models.py | 3 +++ tests/pytest/core/test_models.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/benefits/core/models.py b/benefits/core/models.py index ed5f6122e..959c94451 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -142,6 +142,7 @@ def clean(self): supports_expiration = self.supports_expiration expiration_days = self.expiration_days expiration_reenrollment_days = self.expiration_reenrollment_days + reenrollment_error_template = self.reenrollment_error_template if supports_expiration: errors = {} @@ -150,6 +151,8 @@ def clean(self): errors.update(expiration_days=ValidationError(message)) if expiration_reenrollment_days is None or expiration_reenrollment_days <= 0: errors.update(expiration_reenrollment_days=ValidationError(message)) + if reenrollment_error_template is None: + errors.update(reenrollment_error_template=ValidationError("Required when supports expiration is True.")) if errors: raise ValidationError(errors) diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 798dfeb8b..1cb359fc4 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -210,6 +210,19 @@ def test_EligibilityType_zero_expiration_reenrollment_days(model_EligibilityType ) +@pytest.mark.django_db +def test_EligibilityType_missing_reenrollment_template(model_EligibilityType_supports_expiration): + model_EligibilityType_supports_expiration.reenrollment_error_template = None + model_EligibilityType_supports_expiration.save() + + with pytest.raises(ValidationError) as exception_info: + model_EligibilityType_supports_expiration.full_clean() + + error_dict = exception_info.value.error_dict + assert len(error_dict["reenrollment_error_template"]) == 1 + assert error_dict["reenrollment_error_template"][0].message == "Required when supports expiration is True." + + @pytest.mark.django_db def test_EligibilityType_supports_expiration(model_EligibilityType_supports_expiration): # test will fail if any error is raised