Skip to content

Commit

Permalink
chore: Revert "Revert "Feat: enrollments can expire (#1989)" (#2052)"
Browse files Browse the repository at this point in the history
This reverts commit ea0dbea, reversing
changes made to dd3bd7a.
  • Loading branch information
angela-tran committed Jun 14, 2024
1 parent 8eb0e3f commit 5499915
Show file tree
Hide file tree
Showing 4 changed files with 536 additions and 32 deletions.
107 changes: 95 additions & 12 deletions benefits/enrollment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"""

import logging
from datetime import timedelta

from django.http import JsonResponse
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import decorator_from_middleware
from littlepay.api.client import Client
from requests.exceptions import HTTPError
Expand Down Expand Up @@ -92,7 +94,6 @@ def index(request):
if not form.is_valid():
raise Exception("Invalid card token form")

logger.debug("Read tokenized card")
card_token = form.cleaned_data.get("card_token")

client = Client(
Expand All @@ -104,17 +105,69 @@ 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

try:
client.link_concession_group_funding_source(funding_source_id=funding_source.id, group_id=eligibility.group_id)
group_funding_source = _get_group_funding_source(
client=client, group_id=group_id, funding_source_id=funding_source.id
)

already_enrolled = group_funding_source is not None

if eligibility.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))

if not already_enrolled:
# enroll user with an expiration date, return success
client.link_concession_group_funding_source(
group_id=group_id, funding_source_id=funding_source.id, expiry=session.enrollment_expiry(request)
)
return success(request)
else: # already_enrolled
if group_funding_source.expiry_date is None:
# update expiration of existing enrollment, return success
client.update_concession_group_funding_source_expiry(
group_id=group_id,
funding_source_id=funding_source.id,
expiry=session.enrollment_expiry(request),
)
return success(request)
else:
is_expired = _is_expired(group_funding_source.expiry_date)
is_within_reenrollment_window = _is_within_reenrollment_window(
group_funding_source.expiry_date, session.enrollment_reenrollment(request)
)

if is_expired or is_within_reenrollment_window:
# update expiration of existing enrollment, return success
client.update_concession_group_funding_source_expiry(
group_id=group_id,
funding_source_id=funding_source.id,
expiry=session.enrollment_expiry(request),
)
return success(request)
else:
# re-enrollment error, return enrollment error with expiration and reenrollment_date
return reenrollment_error(request)
else: # eligibility does not support expiration
if not already_enrolled:
# enroll user with no expiration date, return success
client.link_concession_group_funding_source(group_id=group_id, funding_source_id=funding_source.id)
return success(request)
else: # already_enrolled
if group_funding_source.expiry_date is None:
# no action, return success
return success(request)
else:
# remove expiration date, return success
raise NotImplementedError("Removing expiration date is currently not supported")

except HTTPError as e:
# 409 means that customer already belongs to a concession group.
# the response JSON will look like:
# {"errors":[{"detail":"Conflict (409) - Customer already belongs to a concession group."}]}
if e.response.status_code == 409:
analytics.returned_success(request, eligibility.group_id)
return success(request)
elif e.response.status_code >= 500:
if e.response.status_code >= 500:
analytics.returned_error(request, str(e))
sentry_sdk.capture_exception(e)

Expand All @@ -125,9 +178,6 @@ def index(request):
except Exception as e:
analytics.returned_error(request, str(e))
raise e
else:
analytics.returned_success(request, eligibility.group_id)
return success(request)

# GET enrollment index
else:
Expand All @@ -154,6 +204,37 @@ def index(request):
return TemplateResponse(request, eligibility.enrollment_index_template, context)


def _get_group_funding_source(client: Client, group_id, funding_source_id):
group_funding_sources = client.get_concession_group_linked_funding_sources(group_id)
matching_group_funding_source = None
for group_funding_source in group_funding_sources:
if group_funding_source.id == funding_source_id:
matching_group_funding_source = group_funding_source
break

return matching_group_funding_source


def _is_expired(expiry_date):
"""Returns whether the passed in datetime is expired or not."""
return expiry_date <= timezone.now()


def _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date):
"""Returns if we are currently within the reenrollment window."""
return enrollment_reenrollment_date <= timezone.now() < expiry_date


def _calculate_expiry(expiration_days):
"""Returns the expiry datetime, which should be midnight in our configured timezone of the (N + 1)th day from now,
where N is expiration_days."""
default_time_zone = timezone.get_default_timezone()
expiry_date = timezone.localtime(timezone=default_time_zone) + timedelta(days=expiration_days + 1)
expiry_datetime = expiry_date.replace(hour=0, minute=0, second=0, microsecond=0)

return expiry_datetime


@decorator_from_middleware(EligibleSessionRequired)
def reenrollment_error(request):
"""View handler for a re-enrollment attempt that is not yet within the re-enrollment window."""
Expand Down Expand Up @@ -192,6 +273,7 @@ def system_error(request):


@pageview_decorator
@decorator_from_middleware(EligibleSessionRequired)
@decorator_from_middleware(VerifierSessionRequired)
def success(request):
"""View handler for the final success page."""
Expand All @@ -206,4 +288,5 @@ 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)
return TemplateResponse(request, eligibility.enrollment_success_template)
6 changes: 5 additions & 1 deletion benefits/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@ def RUNTIME_ENVIRONMENT():

USE_I18N = True

TIME_ZONE = "UTC"
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-TIME_ZONE
# > Note that this isn’t necessarily the time zone of the server.
# > When USE_TZ is True, this is the default time zone that Django will use to display datetimes in templates
# > and to interpret datetimes entered in forms.
TIME_ZONE = "America/Los_Angeles"
USE_TZ = True

# https://docs.djangoproject.com/en/5.0/topics/i18n/formatting/#creating-custom-format-files
Expand Down
10 changes: 10 additions & 0 deletions tests/pytest/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import create_autospec
from django.contrib.sessions.middleware import SessionMiddleware
from django.middleware.locale import LocaleMiddleware
from django.utils import timezone

import pytest
from pytest_socket import disable_socket
Expand Down Expand Up @@ -261,6 +262,15 @@ def mocked_session_oauth_token(mocker):
return mocker.patch("benefits.core.session.oauth_token", autospec=True, return_value="token")


@pytest.fixture
def mocked_session_enrollment_expiry(mocker):
return mocker.patch(
"benefits.core.session.enrollment_expiry",
autospec=True,
return_value=timezone.make_aware(timezone.datetime(2024, 1, 1), timezone=timezone.get_default_timezone()),
)


@pytest.fixture
def mocked_session_verifier(mocker, model_EligibilityVerifier):
return mocker.patch("benefits.core.session.verifier", autospec=True, return_value=model_EligibilityVerifier)
Expand Down
Loading

0 comments on commit 5499915

Please sign in to comment.