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..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 @@ -109,6 +110,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 @@ -132,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.""" diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 2069b4d22..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 @@ -12,6 +13,45 @@ 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.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 + + def test_SecretNameField_init(): field = SecretNameField() @@ -180,6 +220,41 @@ 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() + + +@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."""