From 042801f8a28d0b4736b331daa7353ae91631d9da Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Tue, 13 Aug 2024 15:36:40 +0000 Subject: [PATCH 1/5] refactor(model): move fields from EligibilityType to EnrollmentFlow --- benefits/core/admin.py | 22 +-- ...refactor_EligibilityType_EnrollmentFlow.py | 105 ++++++++++ benefits/core/migrations/local_fixtures.json | 73 ++----- benefits/core/models.py | 131 +++++-------- benefits/oauth/middleware.py | 2 +- tests/pytest/conftest.py | 104 +++++----- tests/pytest/core/test_models.py | 180 +++--------------- 7 files changed, 245 insertions(+), 372 deletions(-) create mode 100644 benefits/core/migrations/0022_refactor_EligibilityType_EnrollmentFlow.py diff --git a/benefits/core/admin.py b/benefits/core/admin.py index 8d8a60a4f..017594e04 100644 --- a/benefits/core/admin.py +++ b/benefits/core/admin.py @@ -41,25 +41,6 @@ def get_readonly_fields(self, request, obj=None): return super().get_readonly_fields(request, obj) -@admin.register(models.EligibilityType) -class EligibilityTypeAdmin(admin.ModelAdmin): # pragma: no cover - def get_exclude(self, request, obj=None): - if not request.user.is_superuser: - return [] - else: - return super().get_exclude(request, obj) - - def get_readonly_fields(self, request, obj=None): - if not request.user.is_superuser: - return [ - "enrollment_index_template", - "reenrollment_error_template", - "enrollment_success_template", - ] - else: - return super().get_readonly_fields(request, obj) - - @admin.register(models.EnrollmentFlow) class SortableEnrollmentFlowAdmin(SortableAdminMixin, admin.ModelAdmin): # pragma: no cover def get_exclude(self, request, obj=None): @@ -86,6 +67,9 @@ def get_readonly_fields(self, request, obj=None): "help_template", "selection_label_template", "claims_scheme_override", + "enrollment_index_template", + "reenrollment_error_template", + "enrollment_success_template", ] else: return super().get_readonly_fields(request, obj) diff --git a/benefits/core/migrations/0022_refactor_EligibilityType_EnrollmentFlow.py b/benefits/core/migrations/0022_refactor_EligibilityType_EnrollmentFlow.py new file mode 100644 index 000000000..069804306 --- /dev/null +++ b/benefits/core/migrations/0022_refactor_EligibilityType_EnrollmentFlow.py @@ -0,0 +1,105 @@ +# Generated by Django 5.0.6 on 2024-08-14 16:14 + +from django.db import migrations, models + + +def migrate_data(apps, schema_editor): + EnrollmentFlow = apps.get_model("core", "EnrollmentFlow") + + for flow in EnrollmentFlow.objects.all(): + if flow.eligibility_type is not None: + flow.name = flow.eligibility_type.name + flow.label = flow.eligibility_type.label + flow.group_id = flow.eligibility_type.group_id + flow.supports_expiration = flow.eligibility_type.supports_expiration + flow.expiration_days = flow.eligibility_type.expiration_days + flow.expiration_reenrollment_days = flow.eligibility_type.expiration_reenrollment_days + flow.enrollment_index_template = flow.eligibility_type.enrollment_index_template + flow.enrollment_error_template = flow.eligibility_type.reenrollment_error_template + flow.enrollment_success_template = flow.eligibility_type.enrollment_success_template + flow.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0021_rename_eligibilityverifier_enrollmentflow"), + ] + + operations = [ + migrations.AddField( + model_name="enrollmentflow", + name="enrollment_index_template", + field=models.TextField( + default="enrollment/index.html", + help_text="Template for the Eligibility Confirmation page (which is the index of the enrollment Django app)", + ), + ), + migrations.AddField( + model_name="enrollmentflow", + name="enrollment_success_template", + field=models.TextField( + default="enrollment/success.html", + help_text="Template for a successful enrollment associated with the enrollment flow", + ), + ), + migrations.AddField( + model_name="enrollmentflow", + name="expiration_days", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + help_text="If the enrollment supports expiration, number of days before the eligibility expires", + ), + ), + migrations.AddField( + model_name="enrollmentflow", + name="expiration_reenrollment_days", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + help_text="If the enrollment supports expiration, number of days preceding the expiration date during which a user can re-enroll in the eligibilty", # noqa: E501 + ), + ), + migrations.AddField( + model_name="enrollmentflow", + name="group_id", + field=models.TextField(null=True, help_text="Reference to the TransitProcessor group for user enrollment"), + ), + migrations.AddField( + model_name="enrollmentflow", + name="label", + field=models.TextField( + null=True, help_text="A human readable label, not shown to end-users. Used as the display text in Admin." + ), + ), + migrations.AddField( + model_name="enrollmentflow", + name="reenrollment_error_template", + field=models.TextField( + blank=True, null=True, help_text="Template for a re-enrollment error associated with the enrollment flow" + ), + ), + migrations.AddField( + model_name="enrollmentflow", + name="supports_expiration", + field=models.BooleanField(default=False, help_text="Indicates if the enrollment expires or does not expire"), + ), + migrations.RunPython(migrate_data), + migrations.RenameField( + model_name="enrollmentflow", + old_name="name", + new_name="system_name", + ), + migrations.RemoveField( + model_name="enrollmentflow", + name="eligibility_type", + ), + migrations.RemoveField( + model_name="transitagency", + name="eligibility_types", + ), + migrations.DeleteModel( + name="EligibilityType", + ), + ] diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 936b76c22..f0d516813 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -39,57 +39,14 @@ } }, { - "model": "core.eligibilitytype", + "model": "core.enrollmentflow", "pk": 1, "fields": { - "name": "senior", + "system_name": "senior", "label": "(CST) Senior Discount", "group_id": "group123", - "enrollment_success_template": "enrollment/success--cst.html" - } - }, - { - "model": "core.eligibilitytype", - "pk": 2, - "fields": { - "name": "veteran", - "label": "(CST) Veteran Discount", - "group_id": "group123", - "enrollment_success_template": "enrollment/success--cst.html" - } - }, - { - "model": "core.eligibilitytype", - "pk": 3, - "fields": { - "name": "agency_card", - "label": "(CST) Agency Card Discount", - "group_id": "group123", - "enrollment_index_template": "enrollment/index--agency-card.html", - "enrollment_success_template": "enrollment/success--cst-agency-card.html" - } - }, - { - "model": "core.eligibilitytype", - "pk": 4, - "fields": { - "name": "calfresh", - "label": "CalFresh", - "group_id": "group123", - "supports_expiration": "True", - "expiration_days": 5, - "expiration_reenrollment_days": 3, - "reenrollment_error_template": "enrollment/reenrollment-error--calfresh.html", - "enrollment_success_template": "enrollment/success--cst.html" - } - }, - { - "model": "core.enrollmentflow", - "pk": 1, - "fields": { - "name": "(CST) oauth claims via Login.gov", + "enrollment_success_template": "enrollment/success--cst.html", "display_order": 1, - "eligibility_type": 1, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--senior.html", "eligibility_start_template": "eligibility/start--senior.html", @@ -101,9 +58,11 @@ "model": "core.enrollmentflow", "pk": 2, "fields": { - "name": "(CST) VA.gov - veteran", + "system_name": "veteran", + "label": "(CST) Veteran Discount", + "group_id": "group123", + "enrollment_success_template": "enrollment/success--cst.html", "display_order": 3, - "eligibility_type": 2, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--veteran.html", "eligibility_start_template": "eligibility/start--veteran.html", @@ -115,12 +74,15 @@ "model": "core.enrollmentflow", "pk": 3, "fields": { - "name": "(CST) eligibility server flow", + "system_name": "agency_card", + "label": "(CST) Agency Card Discount", + "group_id": "group123", + "enrollment_index_template": "enrollment/index--agency-card.html", + "enrollment_success_template": "enrollment/success--cst-agency-card.html", "display_order": 4, "eligibility_api_url": "http://server:8000/verify", "eligibility_api_auth_header": "X-Server-API-Key", "eligibility_api_auth_key_secret_name": "agency-card-flow-api-auth-key", - "eligibility_type": 3, "eligibility_api_public_key": 1, "eligibility_api_jwe_cek_enc": "A256CBC-HS512", "eligibility_api_jwe_encryption_alg": "RSA-OAEP", @@ -136,9 +98,15 @@ "model": "core.enrollmentflow", "pk": 4, "fields": { - "name": "(CST) CalFresh oauth claims via Login.gov", + "system_name": "calfresh", + "label": "CalFresh", + "group_id": "group123", + "supports_expiration": "True", + "expiration_days": 5, + "expiration_reenrollment_days": 3, + "reenrollment_error_template": "enrollment/reenrollment-error--calfresh.html", + "enrollment_success_template": "enrollment/success--cst.html", "display_order": 2, - "eligibility_type": 4, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--calfresh.html", "eligibility_start_template": "eligibility/start--calfresh.html", @@ -163,7 +131,6 @@ "pk": 1, "fields": { "active": true, - "eligibility_types": [1, 2, 3, 4], "enrollment_flows": [1, 2, 3, 4], "slug": "cst", "short_name": "CST (local)", diff --git a/benefits/core/models.py b/benefits/core/models.py index ff1b3543d..17df9d76a 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -100,71 +100,14 @@ def __str__(self) -> str: return self.client_name -class EligibilityType(models.Model): - """A single conditional eligibility type.""" - - id = models.AutoField(primary_key=True) - 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) - enrollment_index_template = models.TextField(default="enrollment/index.html") - reenrollment_error_template = models.TextField(null=True, blank=True) - enrollment_success_template = models.TextField(default="enrollment/success.html") - - def __str__(self): - return self.label - - @staticmethod - def get(id): - """Get an EligibilityType instance by its id.""" - logger.debug(f"Get {EligibilityType.__name__} by id: {id}") - return EligibilityType.objects.get(pk=id) - - @staticmethod - def get_many(ids): - """Get a list of EligibilityType instances from a list of ids.""" - logger.debug(f"Get {EligibilityType.__name__} list by ids: {ids}") - return EligibilityType.objects.filter(id__in=ids) - - @staticmethod - def get_names(eligibility_types): - """Convert a list of EligibilityType to a list of their names""" - if isinstance(eligibility_types, EligibilityType): - 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 - reenrollment_error_template = self.reenrollment_error_template - - 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 reenrollment_error_template is None: - errors.update(reenrollment_error_template=ValidationError("Required when supports expiration is True.")) - - if errors: - raise ValidationError(errors) - - class EnrollmentFlow(models.Model): """Represents a user journey through the Benefits app for a single eligibility type.""" id = models.AutoField(primary_key=True) - name = models.TextField( + system_name = models.TextField( help_text="Primary internal system name for this EnrollmentFlow instance, e.g. in analytics and Eligibility API requests." # noqa: 501 ) display_order = models.PositiveSmallIntegerField(default=0, blank=False, null=False) - eligibility_type = models.ForeignKey(EligibilityType, on_delete=models.PROTECT) claims_provider = models.ForeignKey( ClaimsProvider, on_delete=models.PROTECT, @@ -243,12 +186,38 @@ class EnrollmentFlow(models.Model): blank=True, help_text="Path to a Django template that defines the help text for this enrollment flow, used in building the dynamic help page for an agency", # noqa: E501 ) + label = models.TextField( + null=True, + help_text="A human readable label, not shown to end-users. Used as the display text in Admin.", + ) + group_id = models.TextField(null=True, help_text="Reference to the TransitProcessor group for user enrollment") + supports_expiration = models.BooleanField( + default=False, help_text="Indicates if the enrollment expires or does not expire" + ) + expiration_days = models.PositiveSmallIntegerField( + null=True, blank=True, help_text="If the enrollment supports expiration, number of days before the eligibility expires" + ) + expiration_reenrollment_days = models.PositiveSmallIntegerField( + null=True, + blank=True, + help_text="If the enrollment supports expiration, number of days preceding the expiration date during which a user can re-enroll in the eligibilty", # noqa: E501 + ) + enrollment_index_template = models.TextField( + default="enrollment/index.html", + help_text="Template for the Eligibility Confirmation page (which is the index of the enrollment Django app)", + ) + reenrollment_error_template = models.TextField( + null=True, blank=True, help_text="Template for a re-enrollment error associated with the enrollment flow" + ) + enrollment_success_template = models.TextField( + default="enrollment/success.html", help_text="Template for a successful enrollment associated with the enrollment flow" + ) class Meta: ordering = ["display_order"] def __str__(self): - return self.name + return self.label @property def eligibility_api_auth_key(self): @@ -284,6 +253,25 @@ def by_id(id): logger.debug(f"Get {EnrollmentFlow.__name__} by id: {id}") return EnrollmentFlow.objects.get(id=id) + 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 = {} + 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 reenrollment_error_template is None: + errors.update(reenrollment_error_template=ValidationError("Required when supports expiration is True.")) + + if errors: + raise ValidationError(errors) + @property def claims_scheme(self): if not self.claims_scheme_override: @@ -314,7 +302,6 @@ class TransitAgency(models.Model): id = models.AutoField(primary_key=True) active = models.BooleanField(default=False, help_text="Determines if this Agency is enabled for users") - eligibility_types = models.ManyToManyField(EligibilityType) enrollment_flows = models.ManyToManyField(EnrollmentFlow) slug = models.TextField(help_text="Used for URL navigation for this agency, e.g. the agency homepage url is /{slug}") short_name = models.TextField(help_text="The user-facing short name for this agency. Often an uppercase acronym.") @@ -356,30 +343,6 @@ class TransitAgency(models.Model): def __str__(self): return self.long_name - def get_type_id(self, name): - """Get the id of the EligibilityType identified by the given name for this agency.""" - eligibility = self.eligibility_types.all().filter(name=name) - if eligibility.count() == 1: - return eligibility[0].id - else: - raise Exception("name does not correspond to a single eligibility type for agency") - - def supports_type(self, eligibility_type): - """True if the eligibility_type is one of this agency's types. False otherwise.""" - return isinstance(eligibility_type, EligibilityType) and eligibility_type in self.eligibility_types.all() - - def types_to_verify(self, flow: EnrollmentFlow): - """List of eligibility types to verify for this agency.""" - # compute set intersection of agency and flow type ids - agency_types = set(self.eligibility_types.values_list("id", flat=True)) - flow_types = {flow.eligibility_type.id} - supported_types = list(agency_types & flow_types) - return EligibilityType.get_many(supported_types) - - def type_names_to_verify(self, flow: EnrollmentFlow): - """List of names of the eligibility types to check for this agency.""" - return EligibilityType.get_names(self.types_to_verify(flow)) - @property def index_url(self): """Public-facing URL to the TransitAgency's landing page.""" diff --git a/benefits/oauth/middleware.py b/benefits/oauth/middleware.py index 6fc30acb6..a96a907c5 100644 --- a/benefits/oauth/middleware.py +++ b/benefits/oauth/middleware.py @@ -30,7 +30,7 @@ def process_request(self, request): elif not (flow.eligibility_api_url or flow.eligibility_form_class): # the chosen flow doesn't have Eligibility API config OR claims provider config # this is likely a misconfiguration on the backend, not a user error - message = f"Flow with no API or claims config: {flow.name} (id={flow.id})" + message = f"Flow with no API or claims config: {flow.system_name} (id={flow.id})" analytics.error(request, message=message, operation=request.path) sentry_sdk.capture_exception(Exception(message)) return redirect(ROUTE_SYSTEM_ERROR) diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 7c8ef3e40..17a6e5a0d 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -7,7 +7,7 @@ from pytest_socket import disable_socket from benefits.core import session -from benefits.core.models import ClaimsProvider, EligibilityType, EnrollmentFlow, TransitProcessor, PemData, TransitAgency +from benefits.core.models import ClaimsProvider, EnrollmentFlow, TransitProcessor, PemData, TransitAgency def pytest_runtest_setup(): @@ -69,86 +69,76 @@ def model_ClaimsProvider_no_sign_out(model_ClaimsProvider): @pytest.fixture -def model_EligibilityType(): - eligibility = EligibilityType.objects.create( - name="test", - label="Test Eligibility Type", - group_id="1234", +def model_EnrollmentFlow(model_PemData, model_ClaimsProvider): + flow = EnrollmentFlow.objects.create( + system_name="Test Flow", + eligibility_api_url="https://example.com/verify", + eligibility_api_auth_header="X-API-AUTH", + eligibility_api_auth_key_secret_name="secret-key", + eligibility_api_public_key=model_PemData, + selection_label_template="eligibility/includes/selection-label.html", + claims_provider=model_ClaimsProvider, + claims_scheme_override="", + label="Test flow label", + group_id="group123", enrollment_success_template="enrollment/success.html", ) - return eligibility - - -@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 + return flow @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() +def model_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow): + model_EnrollmentFlow.claims_scope = "scope" + model_EnrollmentFlow.claims_claim = "claim" + model_EnrollmentFlow.save() - return model_EligibilityType + return model_EnrollmentFlow @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() +def model_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow): + model_EnrollmentFlow.claims_scheme_override = "scheme" + model_EnrollmentFlow.save() - return model_EligibilityType + return model_EnrollmentFlow @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.reenrollment_error_template = "enrollment/reenrollment-error--calfresh.html" - model_EligibilityType.save() +def model_EnrollmentFlow_does_not_support_expiration(model_EnrollmentFlow): + model_EnrollmentFlow.supports_expiration = False + model_EnrollmentFlow.expiration_days = 0 + model_EnrollmentFlow.save() - return model_EligibilityType + return model_EnrollmentFlow @pytest.fixture -def model_EnrollmentFlow(model_PemData, model_EligibilityType, model_ClaimsProvider): - flow = EnrollmentFlow.objects.create( - name="Test Flow", - eligibility_api_url="https://example.com/verify", - eligibility_api_auth_header="X-API-AUTH", - eligibility_api_auth_key_secret_name="secret-key", - eligibility_type=model_EligibilityType, - eligibility_api_public_key=model_PemData, - selection_label_template="eligibility/includes/selection-label.html", - claims_provider=model_ClaimsProvider, - claims_scheme_override="", - ) +def model_EnrollmentFlow_zero_expiration_days(model_EnrollmentFlow): + model_EnrollmentFlow.supports_expiration = True + model_EnrollmentFlow.expiration_days = 0 + model_EnrollmentFlow.expiration_reenrollment_days = 14 + model_EnrollmentFlow.save() - return flow + return model_EnrollmentFlow @pytest.fixture -def model_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow): - model_EnrollmentFlow.claims_scope = "scope" - model_EnrollmentFlow.claims_claim = "claim" +def model_EnrollmentFlow_zero_expiration_reenrollment_days(model_EnrollmentFlow): + model_EnrollmentFlow.supports_expiration = True + model_EnrollmentFlow.expiration_days = 14 + model_EnrollmentFlow.expiration_reenrollment_days = 0 model_EnrollmentFlow.save() return model_EnrollmentFlow @pytest.fixture -def model_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow): - model_EnrollmentFlow.claims_scheme_override = "scheme" +def model_EnrollmentFlow_supports_expiration(model_EnrollmentFlow): + model_EnrollmentFlow.supports_expiration = True + model_EnrollmentFlow.expiration_days = 365 + model_EnrollmentFlow.expiration_reenrollment_days = 14 + model_EnrollmentFlow.reenrollment_error_template = "enrollment/reenrollment-error--calfresh.html" model_EnrollmentFlow.save() return model_EnrollmentFlow @@ -168,7 +158,7 @@ def model_TransitProcessor(): @pytest.fixture -def model_TransitAgency(model_PemData, model_EligibilityType, model_EnrollmentFlow, model_TransitProcessor): +def model_TransitAgency(model_PemData, model_EnrollmentFlow, model_TransitProcessor): agency = TransitAgency.objects.create( slug="test", short_name="TEST", @@ -189,7 +179,6 @@ def model_TransitAgency(model_PemData, model_EligibilityType, model_EnrollmentFl ) # add many-to-many relationships after creation, need ID on both sides - agency.eligibility_types.add(model_EligibilityType) agency.enrollment_flows.add(model_EnrollmentFlow) agency.save() @@ -230,9 +219,8 @@ def mocked_session_agency(mocker, model_TransitAgency): @pytest.fixture -def mocked_session_eligibility(mocker, model_EligibilityType): - mocker.patch("benefits.core.session.eligible", autospec=True, return_value=True) - return mocker.patch("benefits.core.session.eligibility", autospec=True, return_value=model_EligibilityType) +def mocked_session_eligible(mocker): + return mocker.patch("benefits.core.session.eligible", autospec=True, return_value=True) @pytest.fixture diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 54e46e8d9..21ffa56c1 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -3,7 +3,7 @@ import pytest -from benefits.core.models import SecretNameField, EligibilityType, EnrollmentFlow, TransitAgency +from benefits.core.models import SecretNameField, EnrollmentFlow, TransitAgency import benefits.secrets @@ -111,76 +111,21 @@ def test_model_ClaimsProvider_no_sign_out(model_ClaimsProvider_no_sign_out): assert not model_ClaimsProvider_no_sign_out.supports_sign_out -@pytest.mark.django_db -def test_model_EligibilityType_str(model_EligibilityType): - assert str(model_EligibilityType) == model_EligibilityType.label - - -@pytest.mark.django_db -def test_EligibilityType_get_matching(model_EligibilityType): - eligibility = EligibilityType.get(model_EligibilityType.id) - - assert eligibility == model_EligibilityType - - -@pytest.mark.django_db -def test_EligibilityType_get_nonmatching(): - with pytest.raises(EligibilityType.DoesNotExist): - EligibilityType.get(99999) - - -@pytest.mark.django_db -def test_EligibilityType_get_many_matching(model_EligibilityType): - new_type = EligibilityType.get(model_EligibilityType.id) - new_type.pk = None - new_type.save() - - result = EligibilityType.get_many([model_EligibilityType.id, new_type.id]) - - assert len(result) == 2 - assert model_EligibilityType in result - assert new_type in result - - -@pytest.mark.django_db -def test_EligibilityType_get_many_nonmatching(): - result = EligibilityType.get_many([99998, 99999]) - - assert len(result) == 0 - - -@pytest.mark.django_db -def test_EligibilityType_get_many_somematching(model_EligibilityType): - result = EligibilityType.get_many([model_EligibilityType.id, 99999]) - - assert len(result) == 1 - assert model_EligibilityType in result - - -@pytest.mark.django_db -def test_EligibilityType_get_names(model_EligibilityType): - expected = [model_EligibilityType.name] - - result = EligibilityType.get_names([model_EligibilityType]) - - assert result == expected - - @pytest.mark.django_db def test_EnrollmentFlow_str(model_EnrollmentFlow): - assert str(model_EnrollmentFlow) == model_EnrollmentFlow.name + assert str(model_EnrollmentFlow) == model_EnrollmentFlow.label @pytest.mark.django_db -def test_EligibilityType_supports_expiration_False(model_EligibilityType_does_not_support_expiration): +def test_EnrollmentFlow_supports_expiration_False(model_EnrollmentFlow_does_not_support_expiration): # test will fail if any error is raised - model_EligibilityType_does_not_support_expiration.full_clean() + model_EnrollmentFlow_does_not_support_expiration.full_clean() @pytest.mark.django_db -def test_EligibilityType_zero_expiration_days(model_EligibilityType_zero_expiration_days): +def test_EnrollmentFlow_zero_expiration_days(model_EnrollmentFlow_zero_expiration_days): with pytest.raises(ValidationError) as exception_info: - model_EligibilityType_zero_expiration_days.full_clean() + model_EnrollmentFlow_zero_expiration_days.full_clean() error_dict = exception_info.value.error_dict assert len(error_dict["expiration_days"]) == 1 @@ -188,9 +133,9 @@ def test_EligibilityType_zero_expiration_days(model_EligibilityType_zero_expirat @pytest.mark.django_db -def test_EligibilityType_zero_expiration_reenrollment_days(model_EligibilityType_zero_expiration_reenrollment_days): +def test_EnrollmentFlow_zero_expiration_reenrollment_days(model_EnrollmentFlow_zero_expiration_reenrollment_days): with pytest.raises(ValidationError) as exception_info: - model_EligibilityType_zero_expiration_reenrollment_days.full_clean() + model_EnrollmentFlow_zero_expiration_reenrollment_days.full_clean() error_dict = exception_info.value.error_dict assert len(error_dict["expiration_reenrollment_days"]) == 1 @@ -201,12 +146,12 @@ 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() +def test_EnrollmentFlow_missing_reenrollment_template(model_EnrollmentFlow_supports_expiration): + model_EnrollmentFlow_supports_expiration.reenrollment_error_template = None + model_EnrollmentFlow_supports_expiration.save() with pytest.raises(ValidationError) as exception_info: - model_EligibilityType_supports_expiration.full_clean() + model_EnrollmentFlow_supports_expiration.full_clean() error_dict = exception_info.value.error_dict assert len(error_dict["reenrollment_error_template"]) == 1 @@ -214,28 +159,28 @@ def test_EligibilityType_missing_reenrollment_template(model_EligibilityType_sup @pytest.mark.django_db -def test_EligibilityType_supports_expiration(model_EligibilityType_supports_expiration): +def test_EnrollmentFlow_supports_expiration(model_EnrollmentFlow_supports_expiration): # test will fail if any error is raised - model_EligibilityType_supports_expiration.full_clean() + model_EnrollmentFlow_supports_expiration.full_clean() @pytest.mark.django_db -def test_EligibilityType_enrollment_index_template(): - new_eligibility_type = EligibilityType.objects.create() +def test_EnrollmentFlow_enrollment_index_template(): + new_flow = EnrollmentFlow.objects.create() - assert new_eligibility_type.enrollment_index_template == "enrollment/index.html" + assert new_flow.enrollment_index_template == "enrollment/index.html" - new_eligibility_type.enrollment_index_template = "test/enrollment.html" - new_eligibility_type.save() + new_flow.enrollment_index_template = "test/enrollment.html" + new_flow.save() - assert new_eligibility_type.enrollment_index_template == "test/enrollment.html" + assert new_flow.enrollment_index_template == "test/enrollment.html" @pytest.mark.django_db -def test_EligibilityType_enrollment_success_template(): - new_eligibility_type = EligibilityType.objects.create() +def test_EnrollmentFlow_enrollment_success_template(): + new_flow = EnrollmentFlow.objects.create() - assert new_eligibility_type.enrollment_success_template == "enrollment/success.html" + assert new_flow.enrollment_success_template == "enrollment/success.html" class SampleFormClass: @@ -334,85 +279,6 @@ def test_TransitAgency_str(model_TransitAgency): assert str(model_TransitAgency) == model_TransitAgency.long_name -@pytest.mark.django_db -def test_TransitAgency_get_type_id_matching(model_TransitAgency): - eligibility = model_TransitAgency.eligibility_types.first() - result = model_TransitAgency.get_type_id(eligibility.name) - - assert result == eligibility.id - - -@pytest.mark.django_db -def test_TransitAgency_get_type_id_manymatching(model_TransitAgency): - eligibility = model_TransitAgency.eligibility_types.first() - new_eligibility = EligibilityType.get(eligibility.id) - new_eligibility.pk = None - new_eligibility.save() - model_TransitAgency.eligibility_types.add(new_eligibility) - - with pytest.raises(Exception, match=r"name"): - model_TransitAgency.get_type_id(eligibility.name) - - -@pytest.mark.django_db -def test_TransitAgency_get_type_id_nonmatching(model_TransitAgency): - with pytest.raises(Exception, match=r"name"): - model_TransitAgency.get_type_id("something") - - -@pytest.mark.django_db -def test_TransitAgency_supports_type_matching(model_TransitAgency): - eligibility = model_TransitAgency.eligibility_types.first() - - assert model_TransitAgency.supports_type(eligibility) - - -@pytest.mark.django_db -def test_TransitAgency_supports_type_nonmatching(model_TransitAgency): - eligibility = model_TransitAgency.eligibility_types.first() - new_eligibility = EligibilityType.get(eligibility.id) - new_eligibility.pk = None - new_eligibility.save() - - assert not model_TransitAgency.supports_type(new_eligibility) - - -@pytest.mark.django_db -def test_TransitAgency_supports_type_wrongtype(model_TransitAgency): - eligibility = model_TransitAgency.eligibility_types.first() - - assert not model_TransitAgency.supports_type(eligibility.name) - - -@pytest.mark.django_db -def test_TransitAgency_types_to_verify(model_TransitAgency): - eligibility = model_TransitAgency.eligibility_types.first() - new_eligibility = EligibilityType.get(eligibility.id) - new_eligibility.pk = None - new_eligibility.save() - - assert eligibility != new_eligibility - - model_TransitAgency.eligibility_types.add(new_eligibility) - assert model_TransitAgency.eligibility_types.count() == 2 - - flow = model_TransitAgency.enrollment_flows.first() - assert flow.eligibility_type == eligibility - - result = model_TransitAgency.types_to_verify(flow) - assert len(result) == 1 - assert eligibility in result - - -@pytest.mark.django_db -def test_TransitAgency_type_names_to_verify(model_TransitAgency, model_EnrollmentFlow): - expected = [t.name for t in model_TransitAgency.types_to_verify(model_EnrollmentFlow)] - - result = model_TransitAgency.type_names_to_verify(model_EnrollmentFlow) - - assert result == expected - - @pytest.mark.django_db def test_TransitAgency_index_url(model_TransitAgency): result = model_TransitAgency.index_url From 2c56e255bde85acd08ece8b849acaf4efdc06929 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Tue, 13 Aug 2024 18:53:42 +0000 Subject: [PATCH 2/5] refactor(session): update how session determines if user is eligible --- benefits/core/context_processors.py | 4 +- benefits/core/session.py | 40 ++--- benefits/eligibility/views.py | 51 +++---- benefits/enrollment/views.py | 22 ++- tests/pytest/core/test_context_processors.py | 9 +- tests/pytest/core/test_session.py | 49 ++---- tests/pytest/eligibility/test_views.py | 12 +- tests/pytest/enrollment/test_views.py | 150 +++++++++---------- 8 files changed, 143 insertions(+), 194 deletions(-) diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 47b65e257..f556ff08f 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -71,14 +71,14 @@ def debug(request): def enrollment(request): """Context processor adds enrollment information to request context.""" - eligibility = session.eligibility(request) + flow = session.flow(request) expiry = session.enrollment_expiry(request) reenrollment = session.enrollment_reenrollment(request) data = { "expires": expiry, "reenrollment": reenrollment, - "supports_expiration": eligibility.supports_expiration if eligibility else False, + "supports_expiration": flow.supports_expiration if flow else False, } return {"enrollment": data} diff --git a/benefits/core/session.py b/benefits/core/session.py index a8bc5b359..5639a5987 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -19,7 +19,7 @@ _AGENCY = "agency" _DEBUG = "debug" _DID = "did" -_ELIGIBILITY = "eligibility" +_ELIGIBLE = "eligibility" _ENROLLMENT_TOKEN = "enrollment_token" _ENROLLMENT_TOKEN_EXP = "enrollment_token_expiry" _ENROLLMENT_EXP = "enrollment_expiry" @@ -53,7 +53,7 @@ def context_dict(request): _DEBUG: debug(request), _DID: did(request), _FLOW: flow(request), - _ELIGIBILITY: eligibility(request), + _ELIGIBLE: eligible(request), _ENROLLMENT_EXP: enrollment_expiry(request), _ENROLLMENT_TOKEN: enrollment_token(request), _ENROLLMENT_TOKEN_EXP: enrollment_token_expiry(request), @@ -89,18 +89,9 @@ def did(request): return str(d) -def eligibility(request): - """Get the confirmed models.EligibilityType from the request's session, or None""" - eligibility = request.session.get(_ELIGIBILITY) - if eligibility: - return models.EligibilityType.get(eligibility) - else: - return None - - def eligible(request): - """True if the request's session is configured with an active agency and has confirmed eligibility. False otherwise.""" - return active_agency(request) and agency(request).supports_type(eligibility(request)) + """True if the request's session has confirmed eligibility. False otherwise.""" + return request.session.get(_ELIGIBLE) def enrollment_expiry(request): @@ -115,10 +106,10 @@ def enrollment_expiry(request): def enrollment_reenrollment(request): """Get the reenrollment date for a user's enrollment from session, or None.""" expiry = enrollment_expiry(request) - elig = eligibility(request) + enrollment_flow = flow(request) - if elig and elig.supports_expiration and expiry: - return expiry - timedelta(days=elig.expiration_reenrollment_days) + if enrollment_flow and enrollment_flow.supports_expiration and expiry: + return expiry - timedelta(days=enrollment_flow.expiration_reenrollment_days) else: return None @@ -179,7 +170,7 @@ def reset(request): logger.debug("Reset session") request.session[_AGENCY] = None request.session[_FLOW] = None - request.session[_ELIGIBILITY] = None + request.session[_ELIGIBLE] = False request.session[_ORIGIN] = reverse("core:index") request.session[_ENROLLMENT_EXP] = None request.session[_ENROLLMENT_TOKEN] = None @@ -239,7 +230,7 @@ def update( agency=None, debug=None, flow=None, - eligibility_types=None, + eligible=None, enrollment_expiry=None, enrollment_token=None, enrollment_token_exp=None, @@ -252,17 +243,8 @@ def update( request.session[_AGENCY] = agency.id if debug is not None: request.session[_DEBUG] = debug - if eligibility_types is not None and isinstance(eligibility_types, list): - if len(eligibility_types) > 1: - raise NotImplementedError("Multiple eligibilities are not supported at this time.") - elif len(eligibility_types) == 1: - # get the eligibility corresponding to the session's agency - a = models.TransitAgency.by_id(request.session[_AGENCY]) - t = str(eligibility_types[0]).strip() - request.session[_ELIGIBILITY] = a.get_type_id(t) - else: - # empty list, clear session eligibility - request.session[_ELIGIBILITY] = None + if eligible is not None: + request.session[_ELIGIBLE] = bool(eligible) if isinstance(enrollment_expiry, datetime): if enrollment_expiry.tzinfo is None or enrollment_expiry.tzinfo.utcoffset(enrollment_expiry) is None: # this is a naive datetime instance, update tzinfo for UTC diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py index dc20a3754..347f81b30 100644 --- a/benefits/eligibility/views.py +++ b/benefits/eligibility/views.py @@ -35,9 +35,9 @@ def index(request, agency=None): if agency is None: return TemplateResponse(request, "200-user-error.html") else: - session.update(request, eligibility_types=[], origin=agency.index_url) + session.update(request, eligible=False, origin=agency.index_url) else: - session.update(request, agency=agency, eligibility_types=[], origin=reverse(ROUTE_CORE_INDEX)) + session.update(request, agency=agency, eligible=False, origin=reverse(ROUTE_CORE_INDEX)) # clear any prior OAuth token as the user is choosing their desired flow # this may or may not require OAuth, with a different set of scope/claims than what is already stored @@ -53,8 +53,7 @@ def index(request, agency=None): flow = EnrollmentFlow.objects.get(id=flow_id) session.update(request, flow=flow) - types_to_verify = agency.type_names_to_verify(flow) - analytics.selected_verifier(request, types_to_verify) + analytics.selected_verifier(request, flow.system_name) eligibility_start = reverse(ROUTE_START) response = redirect(eligibility_start) @@ -74,7 +73,7 @@ def index(request, agency=None): @decorator_from_middleware(FlowSessionRequired) def start(request): """View handler for the eligibility verification getting started screen.""" - session.update(request, eligibility_types=[], origin=reverse(ROUTE_START)) + session.update(request, eligible=False, origin=reverse(ROUTE_START)) flow = session.flow(request) @@ -90,22 +89,21 @@ def confirm(request): # GET from an already verified user, no need to verify again if request.method == "GET" and session.eligible(request): - eligibility = session.eligibility(request) - return verified(request, [eligibility.name]) + return verified(request) unverified_view = reverse(ROUTE_UNVERIFIED) agency = session.agency(request) flow = session.flow(request) - types_to_verify = agency.type_names_to_verify(flow) # GET for OAuth verification if request.method == "GET" and flow.uses_claims_verification: - analytics.started_eligibility(request, types_to_verify) + analytics.started_eligibility(request, flow.system_name) - verified_types = verify.eligibility_from_oauth(flow, session.oauth_claim(request), agency) - if verified_types: - return verified(request, verified_types) + is_verified = verify.eligibility_from_oauth(flow, session.oauth_claim(request), agency) + + if is_verified: + return verified(request) else: return redirect(unverified_view) @@ -120,7 +118,7 @@ def confirm(request): return TemplateResponse(request, TEMPLATE_CONFIRM, context) # POST form submission, process form data, make Eligibility Verification API call elif request.method == "POST": - analytics.started_eligibility(request, types_to_verify) + analytics.started_eligibility(request, flow.system_name) form = flow.eligibility_form_instance(data=request.POST) # form was not valid, allow for correction/resubmission @@ -130,30 +128,31 @@ def confirm(request): context["form"] = form return TemplateResponse(request, TEMPLATE_CONFIRM, context) - # form is valid, make Eligibility Verification request to get the verified types - verified_types = verify.eligibility_from_api(flow, form, agency) + # form is valid, make Eligibility Verification request to get the verified confirmation + is_verified = verify.eligibility_from_api(flow, form, agency) # form was not valid, allow for correction/resubmission - if verified_types is None: - analytics.returned_error(request, types_to_verify, form.errors) + if is_verified is None: + analytics.returned_error(request, flow.system_name, form.errors) context["form"] = form return TemplateResponse(request, TEMPLATE_CONFIRM, context) - # no types were verified - elif len(verified_types) == 0: + # no type was verified + elif not is_verified: return redirect(unverified_view) - # type(s) were verified + # type was verified else: - return verified(request, verified_types) + return verified(request) @decorator_from_middleware(AgencySessionRequired) @decorator_from_middleware(LoginRequired) -def verified(request, verified_types): +def verified(request): """View handler for the verified eligibility page.""" - analytics.returned_success(request, verified_types) + flow = session.flow(request) + analytics.returned_success(request, flow.system_name) - session.update(request, eligibility_types=verified_types) + session.update(request, eligible=True) return redirect(ROUTE_ENROLLMENT) @@ -163,10 +162,8 @@ def verified(request, verified_types): def unverified(request): """View handler for the unverified eligibility page.""" - agency = session.agency(request) flow = session.flow(request) - types_to_verify = agency.type_names_to_verify(flow) - analytics.returned_fail(request, types_to_verify) + analytics.returned_fail(request, flow.system_name) return TemplateResponse(request, flow.eligibility_unverified_template) diff --git a/benefits/enrollment/views.py b/benefits/enrollment/views.py index e0f231d8e..217c64bcf 100644 --- a/benefits/enrollment/views.py +++ b/benefits/enrollment/views.py @@ -84,7 +84,7 @@ def index(request): session.update(request, origin=reverse(ROUTE_INDEX)) agency = session.agency(request) - eligibility = session.eligibility(request) + flow = session.flow(request) # POST back after transit processor form, process card token if request.method == "POST": @@ -103,7 +103,7 @@ def index(request): client.oauth.ensure_active_token(client.token) funding_source = client.get_funding_source_by_token(card_token) - group_id = eligibility.group_id + group_id = flow.group_id try: group_funding_source = _get_group_funding_source( @@ -112,12 +112,12 @@ def index(request): already_enrolled = group_funding_source is not None - if eligibility.supports_expiration: + if flow.supports_expiration: # set expiry on session if already_enrolled and group_funding_source.expiry_date is not None: session.update(request, enrollment_expiry=group_funding_source.expiry_date) else: - session.update(request, enrollment_expiry=_calculate_expiry(eligibility.expiration_days)) + session.update(request, enrollment_expiry=_calculate_expiry(flow.expiration_days)) if not already_enrolled: # enroll user with an expiration date, return success @@ -203,7 +203,7 @@ def index(request): logger.debug(f'card_tokenize_url: {context["card_tokenize_url"]}') - return TemplateResponse(request, eligibility.enrollment_index_template, context) + return TemplateResponse(request, flow.enrollment_index_template, context) def _get_group_funding_source(client: Client, group_id, funding_source_id): @@ -240,11 +240,10 @@ def _calculate_expiry(expiration_days): @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) flow = session.flow(request) - if eligibility.reenrollment_error_template is None: - raise Exception(f"Re-enrollment error with null template on: {eligibility.label}") + if flow.reenrollment_error_template is None: + raise Exception(f"Re-enrollment error with null template on: {flow}") if session.logged_in(request) and flow.claims_provider.supports_sign_out: # overwrite origin for a logged in user @@ -253,7 +252,7 @@ def reenrollment_error(request): analytics.returned_error(request, "Re-enrollment error.") - return TemplateResponse(request, eligibility.reenrollment_error_template) + return TemplateResponse(request, flow.reenrollment_error_template) @decorator_from_middleware(EligibleSessionRequired) @@ -282,7 +281,6 @@ def success(request): request.path = "/enrollment/success" session.update(request, origin=reverse(ROUTE_SUCCESS)) - eligibility = session.eligibility(request) flow = session.flow(request) if session.logged_in(request) and flow.claims_provider.supports_sign_out: @@ -290,6 +288,6 @@ def success(request): # if they click the logout button, they are taken to the new route session.update(request, origin=reverse(ROUTE_LOGGED_OUT)) - analytics.returned_success(request, eligibility.group_id) + analytics.returned_success(request, flow.group_id) context = {"redirect_to": request.path} - return TemplateResponse(request, eligibility.enrollment_success_template, context) + return TemplateResponse(request, flow.enrollment_success_template, context) diff --git a/tests/pytest/core/test_context_processors.py b/tests/pytest/core/test_context_processors.py index 1733146ce..c00c07be6 100644 --- a/tests/pytest/core/test_context_processors.py +++ b/tests/pytest/core/test_context_processors.py @@ -22,17 +22,14 @@ def test_enrollment_default(app_request): @pytest.mark.django_db -def test_enrollment_expiration(app_request, model_EligibilityType_supports_expiration, model_TransitAgency): - model_TransitAgency.eligibility_types.add(model_EligibilityType_supports_expiration) - model_TransitAgency.save() +def test_enrollment_expiration(app_request, model_EnrollmentFlow_supports_expiration): expiry = datetime.now(tz=timezone.utc) - reenrollment = expiry - timedelta(days=model_EligibilityType_supports_expiration.expiration_reenrollment_days) + reenrollment = expiry - timedelta(days=model_EnrollmentFlow_supports_expiration.expiration_reenrollment_days) session.update( app_request, - agency=model_TransitAgency, - eligibility_types=[model_EligibilityType_supports_expiration.name], + flow=model_EnrollmentFlow_supports_expiration, enrollment_expiry=expiry, ) diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index 2602f85a5..2cf044c60 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -51,26 +51,21 @@ def test_did_default(app_request, mocker): assert did != "None" -@pytest.mark.django_db -def test_eligibility_default(app_request): - assert session.eligibility(app_request) is None - - @pytest.mark.django_db def test_eligibile_False(app_request): agency = models.TransitAgency.objects.first() - eligibility = [] + eligible = False - session.update(app_request, agency=agency, eligibility_types=eligibility) + session.update(app_request, agency=agency, eligible=eligible) assert not session.eligible(app_request) @pytest.mark.django_db def test_eligibile_True(model_TransitAgency, app_request): - eligibility = model_TransitAgency.eligibility_types.first() + eligible = True - session.update(app_request, agency=model_TransitAgency, eligibility_types=[eligibility.name]) + session.update(app_request, agency=model_TransitAgency, eligible=eligible) assert session.eligible(app_request) @@ -118,17 +113,15 @@ def test_enrollment_expiry_datetime_timezone_naive(app_request): @pytest.mark.django_db -def test_enrollment_reenrollment(app_request, model_EligibilityType_supports_expiration, model_TransitAgency): - model_TransitAgency.eligibility_types.add(model_EligibilityType_supports_expiration) - model_TransitAgency.save() +def test_enrollment_reenrollment(app_request, model_EnrollmentFlow_supports_expiration): expiry = datetime.now(tz=timezone.utc) - expected_reenrollment = expiry - timedelta(days=model_EligibilityType_supports_expiration.expiration_reenrollment_days) + expected_reenrollment = expiry - timedelta(days=model_EnrollmentFlow_supports_expiration.expiration_reenrollment_days) session.update( app_request, - agency=model_TransitAgency, - eligibility_types=[model_EligibilityType_supports_expiration.name], + flow=model_EnrollmentFlow_supports_expiration, + eligible=True, enrollment_expiry=expiry, ) @@ -251,11 +244,11 @@ def test_reset_agency(model_TransitAgency, app_request): @pytest.mark.django_db def test_reset_eligibility(app_request): - app_request.session[session._ELIGIBILITY] = ["type1"] + app_request.session[session._ELIGIBLE] = ["type1"] session.reset(app_request) - assert session.eligibility(app_request) is None + assert session.eligible(app_request) is False @pytest.mark.django_db @@ -409,25 +402,11 @@ def test_update_debug_True(app_request): @pytest.mark.django_db -def test_update_eligibility_empty(app_request): - session.update(app_request, eligibility_types=[]) - - assert session.eligibility(app_request) is None - - -@pytest.mark.django_db -def test_update_eligibility_many(model_EligibilityType, app_request): - with pytest.raises(NotImplementedError): - assert session.update(app_request, eligibility_types=[model_EligibilityType, model_EligibilityType]) - - -@pytest.mark.django_db -def test_update_eligibility_single(model_TransitAgency, app_request): - eligibility = model_TransitAgency.eligibility_types.first() - - session.update(app_request, agency=model_TransitAgency, eligibility_types=[eligibility.name]) +@pytest.mark.parametrize("argument, result", [(True, True), (False, False)]) +def test_update_eligible(app_request, argument, result): + session.update(app_request, eligible=argument) - assert session.eligibility(app_request) == eligibility + assert session.eligible(app_request) is result @pytest.mark.django_db diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py index 611e6a4ee..04a3b292f 100644 --- a/tests/pytest/eligibility/test_views.py +++ b/tests/pytest/eligibility/test_views.py @@ -224,7 +224,7 @@ def test_confirm_get_unverified(mocker, client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility", "mocked_session_flow") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible", "mocked_session_flow") def test_confirm_get_verified(client, mocked_session_update): path = reverse(ROUTE_CONFIRM) response = client.get(path) @@ -236,8 +236,8 @@ def test_confirm_get_verified(client, mocked_session_update): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow_uses_claims_verification", "mocked_session_oauth_token") -def test_confirm_get_oauth_verified(mocker, client, model_EligibilityType, mocked_session_update, mocked_analytics_module): - mocker.patch("benefits.eligibility.verify.eligibility_from_oauth", return_value=[model_EligibilityType]) +def test_confirm_get_oauth_verified(mocker, client, mocked_session_update, mocked_analytics_module): + mocker.patch("benefits.eligibility.verify.eligibility_from_oauth", return_value=True) path = reverse(ROUTE_CONFIRM) response = client.get(path) @@ -318,10 +318,10 @@ def test_confirm_post_valid_form_eligibility_unverified(mocker, client, form_dat @pytest.mark.django_db @pytest.mark.usefixtures("mocked_eligibility_auth_request", "model_EnrollmentFlow_with_form_class") def test_confirm_post_valid_form_eligibility_verified( - mocker, client, form_data, mocked_session_eligibility, mocked_session_update, mocked_analytics_module + mocker, client, form_data, mocked_session_eligible, mocked_session_update, mocked_analytics_module ): - eligibility = mocked_session_eligibility.return_value - mocker.patch("benefits.eligibility.verify.eligibility_from_api", return_value=[eligibility]) + eligible = mocked_session_eligible.return_value + mocker.patch("benefits.eligibility.verify.eligibility_from_api", return_value=eligible) path = reverse(ROUTE_CONFIRM) response = client.post(path, form_data) diff --git a/tests/pytest/enrollment/test_views.py b/tests/pytest/enrollment/test_views.py index 666b69748..495eb3e59 100644 --- a/tests/pytest/enrollment/test_views.py +++ b/tests/pytest/enrollment/test_views.py @@ -97,7 +97,7 @@ def test_token_ineligible(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_token_refresh(mocker, client): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) @@ -119,7 +119,7 @@ def test_token_refresh(mocker, client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_token_valid(mocker, client): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=True) mocker.patch("benefits.core.session.enrollment_token", return_value="enrollment_token") @@ -134,7 +134,7 @@ def test_token_valid(mocker, client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_token_http_error_500(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) @@ -162,7 +162,7 @@ def test_token_http_error_500(mocker, client, mocked_analytics_module, mocked_se @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_token_http_error_400(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) @@ -190,7 +190,7 @@ def test_token_http_error_400(mocker, client, mocked_analytics_module, mocked_se @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_token_misconfigured_client_id(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) @@ -212,7 +212,7 @@ def test_token_misconfigured_client_id(mocker, client, mocked_analytics_module, @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_token_connection_error(mocker, client, mocked_analytics_module, mocked_sentry_sdk_module): mocker.patch("benefits.core.session.enrollment_token_valid", return_value=False) @@ -234,13 +234,13 @@ def test_token_connection_error(mocker, client, mocked_analytics_module, mocked_ @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") -def test_index_eligible_get(client, model_EligibilityType): +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") +def test_index_eligible_get(client, model_EnrollmentFlow): path = reverse(ROUTE_INDEX) response = client.get(path) assert response.status_code == 200 - assert response.template_name == model_EligibilityType.enrollment_index_template + assert response.template_name == model_EnrollmentFlow.enrollment_index_template assert "forms" in response.context_data assert "cta_button" in response.context_data assert "card_tokenize_env" in response.context_data @@ -253,7 +253,7 @@ def test_index_eligible_get(client, model_EligibilityType): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") @pytest.mark.parametrize("LANGUAGE_CODE, overlay_language", [("en", "en"), ("es", "es-419"), ("unsupported", "en")]) def test_index_eligible_get_changed_language(client, LANGUAGE_CODE, overlay_language): path = reverse(ROUTE_INDEX) @@ -264,7 +264,7 @@ def test_index_eligible_get_changed_language(client, LANGUAGE_CODE, overlay_lang @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_invalid_form(client, invalid_form_data): path = reverse(ROUTE_INDEX) @@ -274,12 +274,12 @@ def test_index_eligible_post_invalid_form(client, invalid_form_data): @pytest.mark.django_db @pytest.mark.parametrize("status_code", [500, 501, 502, 503, 504]) -@pytest.mark.usefixtures("mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_http_error_500( mocker, client, mocked_session_agency, - model_EligibilityType, + model_EnrollmentFlow_does_not_support_expiration, mocked_analytics_module, mocked_sentry_sdk_module, card_tokenize_form_data, @@ -287,7 +287,7 @@ def test_index_eligible_post_valid_form_http_error_500( ): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.agency.return_value = mocked_session_agency.return_value - mock_session.eligibility.return_value = model_EligibilityType + mock_session.flow.return_value = model_EnrollmentFlow_does_not_support_expiration mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value @@ -310,7 +310,7 @@ def test_index_eligible_post_valid_form_http_error_500( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_http_error_400(mocker, client, card_tokenize_form_data): mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value @@ -328,7 +328,7 @@ def test_index_eligible_post_valid_form_http_error_400(mocker, client, card_toke @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_failure(mocker, client, card_tokenize_form_data): mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value @@ -341,7 +341,7 @@ def test_index_eligible_post_valid_form_failure(mocker, client, card_tokenize_fo @pytest.mark.django_db -@pytest.mark.usefixtures("model_EligibilityType") +@pytest.mark.usefixtures("model_EnrollmentFlow") def test_get_group_funding_sources_funding_source_not_enrolled_yet(mocker, mocked_funding_source): mock_client = mocker.Mock() mock_client.get_concession_group_linked_funding_sources.return_value = [] @@ -352,7 +352,7 @@ def test_get_group_funding_sources_funding_source_not_enrolled_yet(mocker, mocke @pytest.mark.django_db -@pytest.mark.usefixtures("model_EligibilityType") +@pytest.mark.usefixtures("model_EnrollmentFlow") def test_get_group_funding_sources_funding_source_already_enrolled( mocker, mocked_funding_source, mocked_group_funding_source_no_expiry ): @@ -365,13 +365,13 @@ def test_get_group_funding_sources_funding_source_already_enrolled( @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_does_not_support_expiration_customer_already_enrolled_no_expiry( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_does_not_support_expiration, + model_EnrollmentFlow_does_not_support_expiration, mocked_funding_source, mocked_group_funding_source_no_expiry, ): @@ -385,21 +385,19 @@ def test_index_eligible_post_valid_form_success_does_not_support_expiration_cust response = client.post(path, card_tokenize_form_data) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_does_not_support_expiration.enrollment_success_template + assert response.template_name == model_EnrollmentFlow_does_not_support_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert ( - model_EligibilityType_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args - ) + assert model_EnrollmentFlow_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_does_not_support_expiration_no_expiry( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_does_not_support_expiration, + model_EnrollmentFlow_does_not_support_expiration, mocked_funding_source, ): mock_client_cls = mocker.patch("benefits.enrollment.views.Client") @@ -410,14 +408,12 @@ def test_index_eligible_post_valid_form_success_does_not_support_expiration_no_e response = client.post(path, card_tokenize_form_data) mock_client.link_concession_group_funding_source.assert_called_once_with( - funding_source_id=mocked_funding_source.id, group_id=model_EligibilityType_does_not_support_expiration.group_id + funding_source_id=mocked_funding_source.id, group_id=model_EnrollmentFlow_does_not_support_expiration.group_id ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_does_not_support_expiration.enrollment_success_template + assert response.template_name == model_EnrollmentFlow_does_not_support_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert ( - model_EligibilityType_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args - ) + assert model_EnrollmentFlow_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args def test_calculate_expiry(): @@ -447,13 +443,13 @@ def test_calculate_expiry_specific_date(mocker): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_supports_expiration( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_supports_expiration, + model_EnrollmentFlow_supports_expiration, mocked_funding_source, mocked_session_enrollment_expiry, ): @@ -466,23 +462,23 @@ def test_index_eligible_post_valid_form_success_supports_expiration( mock_client.link_concession_group_funding_source.assert_called_once_with( funding_source_id=mocked_funding_source.id, - group_id=model_EligibilityType_supports_expiration.group_id, + group_id=model_EnrollmentFlow_supports_expiration.group_id, expiry=mocked_session_enrollment_expiry.return_value, ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + assert response.template_name == model_EnrollmentFlow_supports_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + assert model_EnrollmentFlow_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_supports_expiration_no_expiry( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_supports_expiration, + model_EnrollmentFlow_supports_expiration, mocked_funding_source, mocked_group_funding_source_no_expiry, mocked_session_enrollment_expiry, @@ -498,13 +494,13 @@ def test_index_eligible_post_valid_form_success_supports_expiration_no_expiry( mock_client.update_concession_group_funding_source_expiry.assert_called_once_with( funding_source_id=mocked_funding_source.id, - group_id=model_EligibilityType_supports_expiration.group_id, + group_id=model_EnrollmentFlow_supports_expiration.group_id, expiry=mocked_session_enrollment_expiry.return_value, ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + assert response.template_name == model_EnrollmentFlow_supports_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + assert model_EnrollmentFlow_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args def test_is_expired_expiry_date_is_in_the_past(mocker): @@ -544,13 +540,13 @@ def test_is_expired_expiry_date_equals_now(mocker): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_supports_expiration_is_expired( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_supports_expiration, + model_EnrollmentFlow_supports_expiration, mocked_funding_source, mocked_group_funding_source_with_expiry, mocked_session_enrollment_expiry, @@ -569,13 +565,13 @@ def test_index_eligible_post_valid_form_success_supports_expiration_is_expired( mock_client.update_concession_group_funding_source_expiry.assert_called_once_with( funding_source_id=mocked_funding_source.id, - group_id=model_EligibilityType_supports_expiration.group_id, + group_id=model_EnrollmentFlow_supports_expiration.group_id, expiry=mocked_session_enrollment_expiry.return_value, ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + assert response.template_name == model_EnrollmentFlow_supports_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + assert model_EnrollmentFlow_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args def test_is_within_enrollment_window_True(mocker): @@ -654,13 +650,13 @@ def test_is_within_enrollment_window_equal_expiry_date(mocker): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_supports_expiration_is_within_reenrollment_window( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_supports_expiration, + model_EnrollmentFlow_supports_expiration, mocked_funding_source, mocked_group_funding_source_with_expiry, mocked_session_enrollment_expiry, @@ -679,17 +675,17 @@ def test_index_eligible_post_valid_form_success_supports_expiration_is_within_re mock_client.update_concession_group_funding_source_expiry.assert_called_once_with( funding_source_id=mocked_funding_source.id, - group_id=model_EligibilityType_supports_expiration.group_id, + group_id=model_EnrollmentFlow_supports_expiration.group_id, expiry=mocked_session_enrollment_expiry.return_value, ) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_supports_expiration.enrollment_success_template + assert response.template_name == model_EnrollmentFlow_supports_expiration.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() - assert model_EligibilityType_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + assert model_EnrollmentFlow_supports_expiration.group_id in mocked_analytics_module.returned_success.call_args.args @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_supports_expiration_is_not_expired_yet( mocker, client, @@ -697,7 +693,7 @@ def test_index_eligible_post_valid_form_success_supports_expiration_is_not_expir mocked_analytics_module, mocked_funding_source, mocked_group_funding_source_with_expiry, - model_EligibilityType_supports_expiration, + model_EnrollmentFlow_supports_expiration, ): mock_client_cls = mocker.patch("benefits.enrollment.views.Client") mock_client = mock_client_cls.return_value @@ -713,18 +709,18 @@ def test_index_eligible_post_valid_form_success_supports_expiration_is_not_expir response = client.post(path, card_tokenize_form_data) assert response.status_code == 200 - assert response.template_name == model_EligibilityType_supports_expiration.reenrollment_error_template + assert response.template_name == model_EnrollmentFlow_supports_expiration.reenrollment_error_template mocked_analytics_module.returned_error.assert_called_once() @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_index_eligible_post_valid_form_success_does_not_support_expiration_has_expiration_date( mocker, client, card_tokenize_form_data, mocked_analytics_module, - model_EligibilityType_does_not_support_expiration, + model_EnrollmentFlow_does_not_support_expiration, mocked_funding_source, mocked_group_funding_source_with_expiry, ): @@ -743,14 +739,14 @@ def test_index_eligible_post_valid_form_success_does_not_support_expiration_has_ # # mock_client.link_concession_group_funding_source.assert_called_once_with( # funding_source_id=mocked_funding_source.id, - # group_id=model_EligibilityType_does_not_support_expiration.group_id, + # group_id=model_EnrollmentFlow_does_not_support_expiration.group_id, # expiry_date=None, # ) # assert response.status_code == 200 - # assert response.template_name == model_EligibilityType_does_not_support_expiration.enrollment_success_template + # assert response.template_name == model_EnrollmentFlow_does_not_support_expiration.enrollment_success_template # mocked_analytics_module.returned_success.assert_called_once() # assert ( - # model_EligibilityType_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args + # model_EnrollmentFlow_does_not_support_expiration.group_id in mocked_analytics_module.returned_success.call_args.args # ) @@ -775,7 +771,7 @@ def test_reenrollment_error_ineligible(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow", "mocked_session_eligible") def test_reenrollment_error_eligibility_no_error_template(client): path = reverse(ROUTE_REENROLLMENT_ERROR) @@ -785,15 +781,15 @@ def test_reenrollment_error_eligibility_no_error_template(client): @pytest.mark.django_db @pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow") -def test_reenrollment_error(client, model_EligibilityType_supports_expiration, mocked_session_eligibility): - mocked_session_eligibility.return_value = model_EligibilityType_supports_expiration +def test_reenrollment_error(client, model_EnrollmentFlow_supports_expiration, mocked_session_eligible): + mocked_session_eligible.return_value = model_EnrollmentFlow_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 + assert response.template_name == model_EnrollmentFlow_supports_expiration.reenrollment_error_template @pytest.mark.django_db @@ -807,7 +803,7 @@ def test_retry_ineligible(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_retry_get(client, mocked_analytics_module): path = reverse(ROUTE_RETRY) response = client.get(path) @@ -818,7 +814,7 @@ def test_retry_get(client, mocked_analytics_module): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_eligible") def test_retry_valid_form(client, mocked_analytics_module): path = reverse(ROUTE_RETRY) response = client.post(path) @@ -839,51 +835,51 @@ def test_success_no_flow(client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_session_eligibility") -def test_success_authentication_logged_in(mocker, client, model_TransitAgency, model_EligibilityType, mocked_analytics_module): +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_session_eligible") +def test_success_authentication_logged_in(mocker, client, model_TransitAgency, model_EnrollmentFlow, mocked_analytics_module): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.logged_in.return_value = True mock_session.agency.return_value = model_TransitAgency - mock_session.eligibility.return_value = model_EligibilityType + mock_session.flow.return_value = model_EnrollmentFlow path = reverse(ROUTE_SUCCESS) response = client.get(path) assert response.status_code == 200 - assert response.template_name == model_EligibilityType.enrollment_success_template + assert response.template_name == model_EnrollmentFlow.enrollment_success_template assert {"origin": reverse(ROUTE_LOGGED_OUT)} in mock_session.update.call_args mocked_analytics_module.returned_success.assert_called_once() @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_session_eligibility") +@pytest.mark.usefixtures("mocked_session_flow_uses_claims_verification", "mocked_session_eligible") def test_success_authentication_not_logged_in( - mocker, client, model_TransitAgency, model_EligibilityType, mocked_analytics_module + mocker, client, model_TransitAgency, model_EnrollmentFlow, mocked_analytics_module ): mock_session = mocker.patch("benefits.enrollment.views.session") mock_session.logged_in.return_value = False mock_session.agency.return_value = model_TransitAgency - mock_session.eligibility.return_value = model_EligibilityType + mock_session.flow.return_value = model_EnrollmentFlow path = reverse(ROUTE_SUCCESS) response = client.get(path) assert response.status_code == 200 - assert response.template_name == model_EligibilityType.enrollment_success_template + assert response.template_name == model_EnrollmentFlow.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() @pytest.mark.django_db @pytest.mark.usefixtures( - "mocked_session_agency", "mocked_session_flow_does_not_use_claims_verification", "mocked_session_eligibility" + "mocked_session_agency", "mocked_session_flow_does_not_use_claims_verification", "mocked_session_eligible" ) -def test_success_no_authentication(mocker, client, model_EligibilityType, mocked_analytics_module): +def test_success_no_authentication(mocker, client, model_EnrollmentFlow, mocked_analytics_module): mock_session = mocker.patch("benefits.enrollment.views.session") - mock_session.eligibility.return_value = model_EligibilityType + mock_session.flow.return_value = model_EnrollmentFlow path = reverse(ROUTE_SUCCESS) response = client.get(path) assert response.status_code == 200 - assert response.template_name == model_EligibilityType.enrollment_success_template + assert response.template_name == model_EnrollmentFlow.enrollment_success_template mocked_analytics_module.returned_success.assert_called_once() From 11681b0436b6386da060a0f1079b567f396ba338 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Wed, 14 Aug 2024 15:10:35 +0000 Subject: [PATCH 3/5] refactor(analytics): update getting eligibility_types --- benefits/core/analytics.py | 6 ++---- benefits/eligibility/analytics.py | 4 +++- tests/pytest/core/test_analytics.py | 1 - tests/pytest/eligibility/test_analytics.py | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/benefits/core/analytics.py b/benefits/core/analytics.py index a58a610a4..2da962934 100644 --- a/benefits/core/analytics.py +++ b/benefits/core/analytics.py @@ -13,7 +13,6 @@ import requests from benefits import VERSION -from benefits.core.models import EligibilityType from . import session @@ -46,9 +45,8 @@ def __init__(self, request, event_type, **kwargs): agency = session.agency(request) agency_name = agency.long_name if agency else None flow = session.flow(request) - verifier_name = flow.name if flow else None - eligibility_types = session.eligibility(request) - eligibility_types = EligibilityType.get_names(eligibility_types) if eligibility_types else None + verifier_name = flow.system_name if flow else None + eligibility_types = [flow.system_name] if flow else None self.update_event_properties( path=request.path, diff --git a/benefits/eligibility/analytics.py b/benefits/eligibility/analytics.py index 44ca4d982..8f19ad6fe 100644 --- a/benefits/eligibility/analytics.py +++ b/benefits/eligibility/analytics.py @@ -8,8 +8,10 @@ class EligibilityEvent(core.Event): """Base analytics event for eligibility verification.""" - def __init__(self, request, event_type, eligibility_types): + def __init__(self, request, event_type, flow_name): super().__init__(request, event_type) + # pass a (converted from string to list) flow_name to preserve analytics reporting + eligibility_types = [flow_name] # overwrite core.Event eligibility_types self.update_event_properties(eligibility_types=eligibility_types) self.update_user_properties(eligibility_types=eligibility_types) diff --git a/tests/pytest/core/test_analytics.py b/tests/pytest/core/test_analytics.py index 193736f43..fcc57f8b0 100644 --- a/tests/pytest/core/test_analytics.py +++ b/tests/pytest/core/test_analytics.py @@ -55,7 +55,6 @@ def test_Event_reads_session(app_request, mocker): session_spy.agency.assert_called_once_with(app_request) session_spy.did.assert_called_once_with(app_request) - session_spy.eligibility.assert_called_once_with(app_request) session_spy.language.assert_called_once_with(app_request) session_spy.start.assert_called_once_with(app_request) session_spy.uid.assert_called_once_with(app_request) diff --git a/tests/pytest/eligibility/test_analytics.py b/tests/pytest/eligibility/test_analytics.py index 18028a79d..b91b67e9d 100644 --- a/tests/pytest/eligibility/test_analytics.py +++ b/tests/pytest/eligibility/test_analytics.py @@ -4,12 +4,12 @@ @pytest.mark.django_db -def test_EligibilityEvent_overwrites_eligibility_types(app_request, mocker): +def test_EligibilityEvent_overwrites_eligibility_types(app_request, mocker, model_EnrollmentFlow): key, type1, type2 = "eligibility_types", "type1", "type2" - mocker.patch("benefits.core.analytics.session.eligibility", return_value=[type1]) - mocker.patch("benefits.core.analytics.EligibilityType.get_names", return_value=[type1]) + model_EnrollmentFlow.system_name = type1 + mocker.patch("benefits.core.analytics.session.flow", return_value=model_EnrollmentFlow) - event = EligibilityEvent(app_request, "event_type", [type2]) + event = EligibilityEvent(app_request, "event_type", type2) # event_properties should have been overwritten assert key in event.event_properties From bc53989058676ae8902b76630991a6adba7405f5 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Thu, 15 Aug 2024 13:39:32 +0000 Subject: [PATCH 4/5] refactor(verify): return a boolean instead of an eligibility_type --- benefits/eligibility/verify.py | 13 +++++++------ tests/pytest/eligibility/test_verify.py | 23 ++++++++++------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/benefits/eligibility/verify.py b/benefits/eligibility/verify.py index f2402ca44..e0808b75d 100644 --- a/benefits/eligibility/verify.py +++ b/benefits/eligibility/verify.py @@ -21,18 +21,19 @@ def eligibility_from_api(flow: models.EnrollmentFlow, form, agency: models.Trans timeout=settings.REQUESTS_TIMEOUT, ) - response = client.verify(sub, name, agency.type_names_to_verify(flow)) + response = client.verify(sub, name, [flow.system_name]) if response.error and any(response.error): return None - elif any(response.eligibility): - return list(response.eligibility) + # response.eligibility is a single item list containing the type of eligibility we are trying to verify, e.g. ["senior"] + elif flow.system_name in response.eligibility: + return True else: - return [] + return False def eligibility_from_oauth(flow: models.EnrollmentFlow, oauth_claim, agency: models.TransitAgency): if flow.uses_claims_verification and flow.claims_claim == oauth_claim: - return agency.type_names_to_verify(flow) + return True else: - return [] + return False diff --git a/tests/pytest/eligibility/test_verify.py b/tests/pytest/eligibility/test_verify.py index 8800504c3..746e988f5 100644 --- a/tests/pytest/eligibility/test_verify.py +++ b/tests/pytest/eligibility/test_verify.py @@ -27,13 +27,13 @@ def test_eligibility_from_api_error(mocker, model_TransitAgency, model_Enrollmen @pytest.mark.django_db def test_eligibility_from_api_verified_types(mocker, model_TransitAgency, model_EnrollmentFlow, mock_api_client_verify, form): - verified_types = ["type1", "type2"] + verified_types = ["Test Flow"] api_response = mocker.Mock(eligibility=verified_types, error=None) mock_api_client_verify.return_value = api_response response = eligibility_from_api(model_EnrollmentFlow, form, model_TransitAgency) - assert response == verified_types + assert response is True @pytest.mark.django_db @@ -46,7 +46,7 @@ def test_eligibility_from_api_no_verified_types( response = eligibility_from_api(model_EnrollmentFlow, form, model_TransitAgency) - assert response == verified_types + assert response is False @pytest.mark.django_db @@ -56,9 +56,9 @@ def test_eligibility_from_oauth_does_not_use_claims_verification( # mocked_session_flow_does_not_use_claims_verification is Mocked version of the session.flow() function flow = mocked_session_flow_does_not_use_claims_verification.return_value - types = eligibility_from_oauth(flow, "claim", model_TransitAgency) + response = eligibility_from_oauth(flow, "claim", model_TransitAgency) - assert types == [] + assert response is False @pytest.mark.django_db @@ -67,20 +67,17 @@ def test_eligibility_from_oauth_claim_mismatch(mocked_session_flow_uses_claims_v flow = mocked_session_flow_uses_claims_verification.return_value flow.claims_claim = "claim" - types = eligibility_from_oauth(flow, "some_other_claim", model_TransitAgency) + response = eligibility_from_oauth(flow, "some_other_claim", model_TransitAgency) - assert types == [] + assert response is False @pytest.mark.django_db -def test_eligibility_from_oauth_claim_match( - mocked_session_flow_uses_claims_verification, model_EligibilityType, model_TransitAgency -): +def test_eligibility_from_oauth_claim_match(mocked_session_flow_uses_claims_verification, model_TransitAgency): # mocked_session_flow_uses_claims_verification is Mocked version of the session.flow() function flow = mocked_session_flow_uses_claims_verification.return_value flow.claims_claim = "claim" - flow.eligibility_type = model_EligibilityType - types = eligibility_from_oauth(flow, "claim", model_TransitAgency) + response = eligibility_from_oauth(flow, "claim", model_TransitAgency) - assert types == [model_EligibilityType.name] + assert response is True From 903f76f7bf932cf401dba9e78887ee6b2da5c1b6 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Fri, 16 Aug 2024 17:57:54 +0000 Subject: [PATCH 5/5] docs: remove references to EligibilityType --- docs/configuration/transit-agency.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/configuration/transit-agency.md b/docs/configuration/transit-agency.md index 680baffb5..78801ce94 100644 --- a/docs/configuration/transit-agency.md +++ b/docs/configuration/transit-agency.md @@ -6,7 +6,6 @@ Then, the following steps are done by the Cal-ITP team to configure a new transi Note that a `TransitAgency` model requires: -- a list of supported `EligibilityType`s - a list of `EnrollmentFlows`s available to the agency's users - a `TransitProcessor` for enrolling the user's contactless card for discounts - an `info_url` and `phone` for users to contact customer service @@ -30,9 +29,9 @@ For development and testing, only a Littlepay customer group is needed since the ### Steps 1. Cal-ITP uses the transit agency's Littlepay merchant ID to create a customer group in the Littlepay QA environment for each type of eligibility (e.g. senior). -1. For each group that's created, a group ID will be returned and should be set as the `group_id` on a new `EligibilityType` in the Benefits database. (See [Configuration data](../data/) for more on loading the database.) +1. For each group that's created, a group ID will be returned and should be set as the `group_id` on a new `EnrollmentFlow` in the Benefits database. (See [Configuration data](../data/) for more on loading the database.) 1. Cal-ITP creates a new `EnrollmentFlow` in the database for each supported eligibility type. This will require configuration for either [API](https://docs.calitp.org/eligibility-api/specification/)-based verification or verification through an [OAuth Open ID Connect claims provider](../oauth/) (e.g. sandbox Login.gov) -- either way, this resource should be meant for testing. -1. Cal-ITP creates a new `TransitAgency` in the database and associates it with the new `EligibilityType`s and `EnrollmentFlow`s as well as the existing Littlepay `TransitProcessor`. +1. Cal-ITP creates a new `TransitAgency` in the database and associates it with the new `EnrollmentFlow`s as well as the existing Littlepay `TransitProcessor`. ## Configuration for production validation @@ -44,8 +43,8 @@ For production validation, both a customer group and discount product are needed 1. Transit agency staff takes a screenshot of the discount product in the Merchant Portal, making sure the browser URL is visible, and sends that to Cal-ITP. 1. Cal-ITP creates a customer group **for testing purposes** in production Littlepay. 1. Cal-ITP associates the group with the product. -1. Cal-ITP creates a new `EligibilityType` **for testing purposes** in the Benefits database and sets the `group_id` to the ID of the newly-created group. -1. Cal-ITP creates a new `EnrollmentFlow` with configuration **for a testing environment** to ensure successful eligibility verification. (For example, use sandbox Login.gov instead of production Login.gov.) +1. Cal-ITP creates a new `EnrollmentFlow` **for testing purposes** in the Benefits database and sets the `group_id` to the ID of the newly-created group. +1. Cal-ITP configures the newly created `EnrollmentFlow` **for a testing environment** to ensure successful eligibility verification. (For example, use sandbox Login.gov instead of production Login.gov.) 1. Cal-ITP creates a new `TransitProcessor` **for testing purposes** with configuration for production Littlepay. 1. Cal-ITP updates the existing `TransitAgency` (created [previously](#configuration-for-development-and-testing)) with associations to the eligibility types, enrollment flows, and transit processor that were just created for testing. @@ -66,8 +65,8 @@ Once production validation is done, the transit agency can be added to the produ 1. Cal-ITP creates a customer group **for production use** in production Littlepay. 1. Cal-ITP associates the group with the discount product created [previously during production validation](#configuration-for-production-validation). -1. Cal-ITP sets that group's ID as the `group_id` for a new `EligibilityType` in the Benefits database. -1. Cal-ITP creates a new `EnrollmentFlow` with configuration for the **production** eligibility verification system. +1. Cal-ITP sets that group's ID as the `group_id` for a new `EnrollmentFlow` in the Benefits database. +1. Cal-ITP configures the newly created `EnrollmentFlow` for the **production** eligibility verification system. 1. Cal-ITP creates a new `TransitAgency` in the database with proper associations to eligibility types, enrollment flows, and transit processor. ### Cleanup @@ -76,4 +75,4 @@ At this point, the customer group that was created in production Littlepay for t 1. Remove the association between the test customer group and discount product. 1. Delete the test customer group. -1. Remove temporary `EligibilityType`s, `EnrollmentFlow`s, and `TransitProcessor` that were [created](#steps_1) in the Benefits test environment. +1. Remove temporary `EnrollmentFlow`s and `TransitProcessor` that were [created](#steps_1) in the Benefits test environment.