From 4fabc1dd78d605afb36fa9acdc286d2a67cde3e9 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 14 Mar 2024 18:28:14 +0000 Subject: [PATCH 1/4] feat(model): add expiration-related fields to EligibilityType `supports_expiration` serves as a toggle for expiration functionality. the other fields are for configuring the duration before expiration and reenrollment. --- .../0003_eligibilitytype_expiration.py | 28 +++++++++++++++++++ benefits/core/models.py | 3 ++ 2 files changed, 31 insertions(+) create mode 100644 benefits/core/migrations/0003_eligibilitytype_expiration.py diff --git a/benefits/core/migrations/0003_eligibilitytype_expiration.py b/benefits/core/migrations/0003_eligibilitytype_expiration.py new file mode 100644 index 000000000..2cf1a88e4 --- /dev/null +++ b/benefits/core/migrations/0003_eligibilitytype_expiration.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.2 on 2024-03-14 20:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0002_paymentprocessor_backoffice_api"), + ] + + operations = [ + migrations.AddField( + model_name="eligibilitytype", + name="expiration_days", + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="eligibilitytype", + name="expiration_reenrollment_days", + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="eligibilitytype", + name="supports_expiration", + field=models.BooleanField(default=False), + ), + ] diff --git a/benefits/core/models.py b/benefits/core/models.py index 72561c93a..7e807f534 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -109,6 +109,9 @@ class EligibilityType(models.Model): name = models.TextField() label = models.TextField() group_id = models.TextField() + supports_expiration = models.BooleanField(default=False) + expiration_days = models.PositiveSmallIntegerField(null=True, blank=True) + expiration_reenrollment_days = models.PositiveSmallIntegerField(null=True, blank=True) def __str__(self): return self.label From b3d71f6f23f813bb32d95e4381e564a7faefba67 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 14 Mar 2024 21:36:42 +0000 Subject: [PATCH 2/4] feat: validate EligibilityType expiration days and reenrollment days --- benefits/core/models.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/benefits/core/models.py b/benefits/core/models.py index 7e807f534..ec3c3499b 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -7,6 +7,7 @@ import logging from django.conf import settings +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse @@ -135,6 +136,22 @@ def get_names(eligibility_types): eligibility_types = [eligibility_types] return [t.name for t in eligibility_types if isinstance(t, EligibilityType)] + def clean(self): + supports_expiration = self.supports_expiration + expiration_days = self.expiration_days + expiration_reenrollment_days = self.expiration_reenrollment_days + + if supports_expiration: + errors = {} + message = "When support_expiration is True, this value must be greater than 0." + if expiration_days is None or expiration_days <= 0: + 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 errors: + raise ValidationError(errors) + class EligibilityVerifier(models.Model): """An entity that verifies eligibility.""" From af0eb638b5e57a6b0d531e18c2d50a0a79993dc1 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 14 Mar 2024 22:07:32 +0000 Subject: [PATCH 3/4] test(model): supports_expiration being False results in valid object --- tests/pytest/core/test_models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 2069b4d22..99d5027be 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -12,6 +12,14 @@ def mock_requests_get_pem_data(mocker): return mocker.patch("benefits.core.models.requests.get", return_value=mocker.Mock(text="PEM text")) +@pytest.fixture +def model_EligibilityType_does_not_support_expiration(model_EligibilityType): + model_EligibilityType.supports_expiration = False + model_EligibilityType.save() + + return model_EligibilityType + + def test_SecretNameField_init(): field = SecretNameField() @@ -180,6 +188,12 @@ def test_EligibilityVerifier_str(model_EligibilityVerifier): assert str(model_EligibilityVerifier) == model_EligibilityVerifier.name +@pytest.mark.django_db +def test_EligibilityType_supports_expiration_False(model_EligibilityType_does_not_support_expiration): + # test will fail if any error is raised + model_EligibilityType_does_not_support_expiration.full_clean() + + class SampleFormClass: """A class for testing EligibilityVerifier form references.""" From b109035f94e5d1e448089da2446998f8dd20a440 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 14 Mar 2024 22:32:58 +0000 Subject: [PATCH 4/4] test(model): positive values are required if supporting expiration --- tests/pytest/core/test_models.py | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 99d5027be..dbf4f44bd 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.exceptions import ValidationError import pytest @@ -15,6 +16,37 @@ def mock_requests_get_pem_data(mocker): @pytest.fixture def model_EligibilityType_does_not_support_expiration(model_EligibilityType): model_EligibilityType.supports_expiration = False + model_EligibilityType.expiration_days = 0 + model_EligibilityType.save() + + return model_EligibilityType + + +@pytest.fixture +def model_EligibilityType_zero_expiration_days(model_EligibilityType): + model_EligibilityType.supports_expiration = True + model_EligibilityType.expiration_days = 0 + model_EligibilityType.expiration_reenrollment_days = 14 + model_EligibilityType.save() + + return model_EligibilityType + + +@pytest.fixture +def model_EligibilityType_zero_expiration_reenrollment_days(model_EligibilityType): + model_EligibilityType.supports_expiration = True + model_EligibilityType.expiration_days = 14 + model_EligibilityType.expiration_reenrollment_days = 0 + model_EligibilityType.save() + + return model_EligibilityType + + +@pytest.fixture +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.save() return model_EligibilityType @@ -194,6 +226,35 @@ def test_EligibilityType_supports_expiration_False(model_EligibilityType_does_no model_EligibilityType_does_not_support_expiration.full_clean() +@pytest.mark.django_db +def test_EligibilityType_zero_expiration_days(model_EligibilityType_zero_expiration_days): + with pytest.raises(ValidationError) as exception_info: + model_EligibilityType_zero_expiration_days.full_clean() + + error_dict = exception_info.value.error_dict + assert len(error_dict["expiration_days"]) == 1 + assert error_dict["expiration_days"][0].message == "When support_expiration is True, this value must be greater than 0." + + +@pytest.mark.django_db +def test_EligibilityType_zero_expiration_reenrollment_days(model_EligibilityType_zero_expiration_reenrollment_days): + with pytest.raises(ValidationError) as exception_info: + model_EligibilityType_zero_expiration_reenrollment_days.full_clean() + + error_dict = exception_info.value.error_dict + assert len(error_dict["expiration_reenrollment_days"]) == 1 + assert ( + error_dict["expiration_reenrollment_days"][0].message + == "When support_expiration is True, this value must be greater than 0." + ) + + +@pytest.mark.django_db +def test_EligibilityType_supports_expiration(model_EligibilityType_supports_expiration): + # test will fail if any error is raised + model_EligibilityType_supports_expiration.full_clean() + + class SampleFormClass: """A class for testing EligibilityVerifier form references."""