We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
New `User enrollment error` page
![image](https://github.com/cal-itp/benefits/assets/25497886/22e984b8-ecb1-4f25-a9ce-c7a3eed5c5b0)


## Acceptance Criteria

<!-- Remember to consider edge cases -->

- [x] Headline copy updated
- [x] Body copy updated
- [x] 24px spacing between icon, headline, body, and agency info
- [x] 64px spacing between CTA and the thing above it
- [x] Login.gov and non-login.gov variants (with and without the Sign out button)


## Additional context

 - Handles the case of the user entering the wrong card information (e.g. expiration date, security code)
 - Littlepay responds with an error in Hosted Card Tokenization flow (Javascript)



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `benefits/enrollment/views.py`
Content:
```
1 """
2 The enrollment application: view definitions for the benefits enrollment flow.
3 """
4 
5 import logging
6 from datetime import timedelta
7 
8 from django.http import JsonResponse
9 from django.template.response import TemplateResponse
10 from django.urls import reverse
11 from django.utils import timezone
12 from django.utils.decorators import decorator_from_middleware
13 from littlepay.api.client import Client
14 from requests.exceptions import HTTPError
15 
16 from benefits.core import session
17 from benefits.core.middleware import (
18     EligibleSessionRequired,
19     VerifierSessionRequired,
20     pageview_decorator,
21 )
22 from benefits.core.views import ROUTE_LOGGED_OUT
23 from . import analytics, forms
24 
25 
26 ROUTE_INDEX = "enrollment:index"
27 ROUTE_RETRY = "enrollment:retry"
28 ROUTE_SUCCESS = "enrollment:success"
29 ROUTE_TOKEN = "enrollment:token"
30 
31 TEMPLATE_REENROLLMENT_ERROR = "enrollment/reenrollment-error.html"
32 TEMPLATE_RETRY = "enrollment/retry.html"
33 TEMPLATE_SUCCESS = "enrollment/success.html"
34 
35 
36 logger = logging.getLogger(__name__)
37 
38 
39 @decorator_from_middleware(EligibleSessionRequired)
40 def token(request):
41     """View handler for the enrollment auth token."""
42     if not session.enrollment_token_valid(request):
43         agency = session.agency(request)
44         payment_processor = agency.payment_processor
45         client = Client(
46             base_url=payment_processor.api_base_url,
47             client_id=payment_processor.client_id,
48             client_secret=payment_processor.client_secret,
49             audience=payment_processor.audience,
50         )
51         client.oauth.ensure_active_token(client.token)
52         response = client.request_card_tokenization_access()
53         session.update(request, enrollment_token=response.get("access_token"), enrollment_token_exp=response.get("expires_at"))
54 
55     data = {"token": session.enrollment_token(request)}
56 
57     return JsonResponse(data)
58 
59 
60 @decorator_from_middleware(EligibleSessionRequired)
61 def index(request):
62     """View handler for the enrollment landing page."""
63     session.update(request, origin=reverse(ROUTE_INDEX))
64 
65     agency = session.agency(request)
66     eligibility = session.eligibility(request)
67     payment_processor = agency.payment_processor
68 
69     # POST back after payment processor form, process card token
70     if request.method == "POST":
71         form = forms.CardTokenizeSuccessForm(request.POST)
72         if not form.is_valid():
73             raise Exception("Invalid card token form")
74 
75         card_token = form.cleaned_data.get("card_token")
76 
77         client = Client(
78             base_url=payment_processor.api_base_url,
79             client_id=payment_processor.client_id,
80             client_secret=payment_processor.client_secret,
81             audience=payment_processor.audience,
82         )
83         client.oauth.ensure_active_token(client.token)
84 
85         funding_source = client.get_funding_source_by_token(card_token)
86         group_id = eligibility.group_id
87 
88         try:
89             group_funding_source = _get_group_funding_source(
90                 client=client, group_id=group_id, funding_source_id=funding_source.id
91             )
92 
93             already_enrolled = group_funding_source is not None
94 
95             if eligibility.supports_expiration:
96                 # set expiry on session
97                 if already_enrolled and group_funding_source.expiry_date is not None:
98                     session.update(request, enrollment_expiry=group_funding_source.expiry_date)
99                 else:
100                     session.update(request, enrollment_expiry=_calculate_expiry(eligibility.expiration_days))
101 
102                 if not already_enrolled:
103                     # enroll user with an expiration date, return success
104                     client.link_concession_group_funding_source(
105                         group_id=group_id, funding_source_id=funding_source.id, expiry=session.enrollment_expiry(request)
106                     )
107                     return success(request)
108                 else:  # already_enrolled
109                     if group_funding_source.expiry_date is None:
110                         # update expiration of existing enrollment, return success
111                         client.update_concession_group_funding_source_expiry(
112                             group_id=group_id,
113                             funding_source_id=funding_source.id,
114                             expiry=session.enrollment_expiry(request),
115                         )
116                         return success(request)
117                     else:
118                         is_expired = _is_expired(group_funding_source.expiry_date)
119                         is_within_reenrollment_window = _is_within_reenrollment_window(
120                             group_funding_source.expiry_date, session.enrollment_reenrollment(request)
121                         )
122 
123                         if is_expired or is_within_reenrollment_window:
124                             # update expiration of existing enrollment, return success
125                             client.update_concession_group_funding_source_expiry(
126                                 group_id=group_id,
127                                 funding_source_id=funding_source.id,
128                                 expiry=session.enrollment_expiry(request),
129                             )
130                             return success(request)
131                         else:
132                             # re-enrollment error, return enrollment error with expiration and reenrollment_date
133                             return reenrollment_error(request)
134             else:  # eligibility does not support expiration
135                 if not already_enrolled:
136                     # enroll user with no expiration date, return success
137                     client.link_concession_group_funding_source(group_id=group_id, funding_source_id=funding_source.id)
138                     return success(request)
139                 else:  # already_enrolled
140                     if group_funding_source.expiry_date is None:
141                         # no action, return success
142                         return success(request)
143                     else:
144                         # remove expiration date, return success
145                         raise NotImplementedError("Removing expiration date is currently not supported")
146 
147         except HTTPError as e:
148             analytics.returned_error(request, str(e))
149             raise Exception(f"{e}: {e.response.json()}")
150         except Exception as e:
151             analytics.returned_error(request, str(e))
152             raise e
153 
154     # GET enrollment index
155     else:
156         tokenize_retry_form = forms.CardTokenizeFailForm(ROUTE_RETRY)
157         tokenize_success_form = forms.CardTokenizeSuccessForm(auto_id=True, label_suffix="")
158 
159         context = {
160             "forms": [tokenize_retry_form, tokenize_success_form],
161             "cta_button": "tokenize_card",
162             "card_tokenize_env": agency.payment_processor.card_tokenize_env,
163             "card_tokenize_func": agency.payment_processor.card_tokenize_func,
164             "card_tokenize_url": agency.payment_processor.card_tokenize_url,
165             "token_field": "card_token",
166             "form_retry": tokenize_retry_form.id,
167             "form_success": tokenize_success_form.id,
168         }
169 
170         logger.debug(f'card_tokenize_url: {context["card_tokenize_url"]}')
171 
172         return TemplateResponse(request, eligibility.enrollment_index_template, context)
173 
174 
175 def _get_group_funding_source(client: Client, group_id, funding_source_id):
176     group_funding_sources = client.get_concession_group_linked_funding_sources(group_id)
177     matching_group_funding_source = None
178     for group_funding_source in group_funding_sources:
179         if group_funding_source.id == funding_source_id:
180             matching_group_funding_source = group_funding_source
181             break
182 
183     return matching_group_funding_source
184 
185 
186 def _is_expired(expiry_date):
187     """Returns whether the passed in datetime is expired or not."""
188     return expiry_date <= timezone.now()
189 
190 
191 def _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date):
192     """Returns if we are currently within the reenrollment window."""
193     return enrollment_reenrollment_date <= timezone.now() < expiry_date
194 
195 
196 def _calculate_expiry(expiration_days):
197     """Returns the expiry datetime, which should be midnight in our configured timezone of the (N + 1)th day from now,
198     where N is expiration_days."""
199     default_time_zone = timezone.get_default_timezone()
200     expiry_date = timezone.localtime(timezone=default_time_zone) + timedelta(days=expiration_days + 1)
201     expiry_datetime = expiry_date.replace(hour=0, minute=0, second=0, microsecond=0)
202 
203     return expiry_datetime
204 
205 
206 def reenrollment_error(request):
207     """View handler for a re-enrollment attempt that is not yet within the re-enrollment window."""
208     analytics.returned_error(request, "Re-enrollment error")
209     return TemplateResponse(request, TEMPLATE_REENROLLMENT_ERROR)
210 
211 
212 @decorator_from_middleware(EligibleSessionRequired)
213 def retry(request):
214     """View handler for a recoverable failure condition."""
215     if request.method == "POST":
216         analytics.returned_retry(request)
217         form = forms.CardTokenizeFailForm(request.POST)
218         if form.is_valid():
219             return TemplateResponse(request, TEMPLATE_RETRY)
220         else:
221             analytics.returned_error(request, "Invalid retry submission.")
222             raise Exception("Invalid retry submission.")
223     else:
224         analytics.returned_error(request, "This view method only supports POST.")
225         raise Exception("This view method only supports POST.")
226 
227 
228 @pageview_decorator
229 @decorator_from_middleware(EligibleSessionRequired)
230 @decorator_from_middleware(VerifierSessionRequired)
231 def success(request):
232     """View handler for the final success page."""
233     request.path = "/enrollment/success"
234     session.update(request, origin=reverse(ROUTE_SUCCESS))
235 
236     agency = session.agency(request)
237     verifier = session.verifier(request)
238     eligibility = session.eligibility(request)
239 
240     if session.logged_in(request) and verifier.auth_provider.supports_sign_out:
241         # overwrite origin for a logged in user
242         # if they click the logout button, they are taken to the new route
243         session.update(request, origin=reverse(ROUTE_LOGGED_OUT))
244 
245     analytics.returned_success(request, eligibility.group_id)
246     return TemplateResponse(request, agency.enrollment_success_template)
247
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/benefits/enrollment/views.py b/benefits/enrollment/views.py
--- a/benefits/enrollment/views.py
+++ b/benefits/enrollment/views.py
@@ -212,17 +212,8 @@
 @decorator_from_middleware(EligibleSessionRequired)
 def retry(request):
     """View handler for a recoverable failure condition."""
-    if request.method == "POST":
-        analytics.returned_retry(request)
-        form = forms.CardTokenizeFailForm(request.POST)
-        if form.is_valid():
-            return TemplateResponse(request, TEMPLATE_RETRY)
-        else:
-            analytics.returned_error(request, "Invalid retry submission.")
-            raise Exception("Invalid retry submission.")
-    else:
-        analytics.returned_error(request, "This view method only supports POST.")
-        raise Exception("This view method only supports POST.")
+    analytics.returned_retry(request)
+    return TemplateResponse(request, TEMPLATE_RETRY)
 
 
 @pageview_decorator

{"golden_diff": "diff --git a/benefits/enrollment/views.py b/benefits/enrollment/views.py\n--- a/benefits/enrollment/views.py\n+++ b/benefits/enrollment/views.py\n@@ -212,17 +212,8 @@\n @decorator_from_middleware(EligibleSessionRequired)\n def retry(request):\n     \"\"\"View handler for a recoverable failure condition.\"\"\"\n-    if request.method == \"POST\":\n-        analytics.returned_retry(request)\n-        form = forms.CardTokenizeFailForm(request.POST)\n-        if form.is_valid():\n-            return TemplateResponse(request, TEMPLATE_RETRY)\n-        else:\n-            analytics.returned_error(request, \"Invalid retry submission.\")\n-            raise Exception(\"Invalid retry submission.\")\n-    else:\n-        analytics.returned_error(request, \"This view method only supports POST.\")\n-        raise Exception(\"This view method only supports POST.\")\n+    analytics.returned_retry(request)\n+    return TemplateResponse(request, TEMPLATE_RETRY)\n \n \n @pageview_decorator\n", "issue": "New `User enrollment error` page\n![image](https://github.com/cal-itp/benefits/assets/25497886/22e984b8-ecb1-4f25-a9ce-c7a3eed5c5b0)\r\n\r\n\r\n## Acceptance Criteria\r\n\r\n<!-- Remember to consider edge cases -->\r\n\r\n- [x] Headline copy updated\r\n- [x] Body copy updated\r\n- [x] 24px spacing between icon, headline, body, and agency info\r\n- [x] 64px spacing between CTA and the thing above it\r\n- [x] Login.gov and non-login.gov variants (with and without the Sign out button)\r\n\r\n\r\n## Additional context\r\n\r\n - Handles the case of the user entering the wrong card information (e.g. expiration date, security code)\r\n - Littlepay responds with an error in Hosted Card Tokenization flow (Javascript)\r\n\r\n\n", "before_files": [{"content": "\"\"\"\nThe enrollment application: view definitions for the benefits enrollment flow.\n\"\"\"\n\nimport logging\nfrom datetime import timedelta\n\nfrom django.http import JsonResponse\nfrom django.template.response import TemplateResponse\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.decorators import decorator_from_middleware\nfrom littlepay.api.client import Client\nfrom requests.exceptions import HTTPError\n\nfrom benefits.core import session\nfrom benefits.core.middleware import (\n    EligibleSessionRequired,\n    VerifierSessionRequired,\n    pageview_decorator,\n)\nfrom benefits.core.views import ROUTE_LOGGED_OUT\nfrom . import analytics, forms\n\n\nROUTE_INDEX = \"enrollment:index\"\nROUTE_RETRY = \"enrollment:retry\"\nROUTE_SUCCESS = \"enrollment:success\"\nROUTE_TOKEN = \"enrollment:token\"\n\nTEMPLATE_REENROLLMENT_ERROR = \"enrollment/reenrollment-error.html\"\nTEMPLATE_RETRY = \"enrollment/retry.html\"\nTEMPLATE_SUCCESS = \"enrollment/success.html\"\n\n\nlogger = logging.getLogger(__name__)\n\n\n@decorator_from_middleware(EligibleSessionRequired)\ndef token(request):\n    \"\"\"View handler for the enrollment auth token.\"\"\"\n    if not session.enrollment_token_valid(request):\n        agency = session.agency(request)\n        payment_processor = agency.payment_processor\n        client = Client(\n            base_url=payment_processor.api_base_url,\n            client_id=payment_processor.client_id,\n            client_secret=payment_processor.client_secret,\n            audience=payment_processor.audience,\n        )\n        client.oauth.ensure_active_token(client.token)\n        response = client.request_card_tokenization_access()\n        session.update(request, enrollment_token=response.get(\"access_token\"), enrollment_token_exp=response.get(\"expires_at\"))\n\n    data = {\"token\": session.enrollment_token(request)}\n\n    return JsonResponse(data)\n\n\n@decorator_from_middleware(EligibleSessionRequired)\ndef index(request):\n    \"\"\"View handler for the enrollment landing page.\"\"\"\n    session.update(request, origin=reverse(ROUTE_INDEX))\n\n    agency = session.agency(request)\n    eligibility = session.eligibility(request)\n    payment_processor = agency.payment_processor\n\n    # POST back after payment processor form, process card token\n    if request.method == \"POST\":\n        form = forms.CardTokenizeSuccessForm(request.POST)\n        if not form.is_valid():\n            raise Exception(\"Invalid card token form\")\n\n        card_token = form.cleaned_data.get(\"card_token\")\n\n        client = Client(\n            base_url=payment_processor.api_base_url,\n            client_id=payment_processor.client_id,\n            client_secret=payment_processor.client_secret,\n            audience=payment_processor.audience,\n        )\n        client.oauth.ensure_active_token(client.token)\n\n        funding_source = client.get_funding_source_by_token(card_token)\n        group_id = eligibility.group_id\n\n        try:\n            group_funding_source = _get_group_funding_source(\n                client=client, group_id=group_id, funding_source_id=funding_source.id\n            )\n\n            already_enrolled = group_funding_source is not None\n\n            if eligibility.supports_expiration:\n                # set expiry on session\n                if already_enrolled and group_funding_source.expiry_date is not None:\n                    session.update(request, enrollment_expiry=group_funding_source.expiry_date)\n                else:\n                    session.update(request, enrollment_expiry=_calculate_expiry(eligibility.expiration_days))\n\n                if not already_enrolled:\n                    # enroll user with an expiration date, return success\n                    client.link_concession_group_funding_source(\n                        group_id=group_id, funding_source_id=funding_source.id, expiry=session.enrollment_expiry(request)\n                    )\n                    return success(request)\n                else:  # already_enrolled\n                    if group_funding_source.expiry_date is None:\n                        # update expiration of existing enrollment, return success\n                        client.update_concession_group_funding_source_expiry(\n                            group_id=group_id,\n                            funding_source_id=funding_source.id,\n                            expiry=session.enrollment_expiry(request),\n                        )\n                        return success(request)\n                    else:\n                        is_expired = _is_expired(group_funding_source.expiry_date)\n                        is_within_reenrollment_window = _is_within_reenrollment_window(\n                            group_funding_source.expiry_date, session.enrollment_reenrollment(request)\n                        )\n\n                        if is_expired or is_within_reenrollment_window:\n                            # update expiration of existing enrollment, return success\n                            client.update_concession_group_funding_source_expiry(\n                                group_id=group_id,\n                                funding_source_id=funding_source.id,\n                                expiry=session.enrollment_expiry(request),\n                            )\n                            return success(request)\n                        else:\n                            # re-enrollment error, return enrollment error with expiration and reenrollment_date\n                            return reenrollment_error(request)\n            else:  # eligibility does not support expiration\n                if not already_enrolled:\n                    # enroll user with no expiration date, return success\n                    client.link_concession_group_funding_source(group_id=group_id, funding_source_id=funding_source.id)\n                    return success(request)\n                else:  # already_enrolled\n                    if group_funding_source.expiry_date is None:\n                        # no action, return success\n                        return success(request)\n                    else:\n                        # remove expiration date, return success\n                        raise NotImplementedError(\"Removing expiration date is currently not supported\")\n\n        except HTTPError as e:\n            analytics.returned_error(request, str(e))\n            raise Exception(f\"{e}: {e.response.json()}\")\n        except Exception as e:\n            analytics.returned_error(request, str(e))\n            raise e\n\n    # GET enrollment index\n    else:\n        tokenize_retry_form = forms.CardTokenizeFailForm(ROUTE_RETRY)\n        tokenize_success_form = forms.CardTokenizeSuccessForm(auto_id=True, label_suffix=\"\")\n\n        context = {\n            \"forms\": [tokenize_retry_form, tokenize_success_form],\n            \"cta_button\": \"tokenize_card\",\n            \"card_tokenize_env\": agency.payment_processor.card_tokenize_env,\n            \"card_tokenize_func\": agency.payment_processor.card_tokenize_func,\n            \"card_tokenize_url\": agency.payment_processor.card_tokenize_url,\n            \"token_field\": \"card_token\",\n            \"form_retry\": tokenize_retry_form.id,\n            \"form_success\": tokenize_success_form.id,\n        }\n\n        logger.debug(f'card_tokenize_url: {context[\"card_tokenize_url\"]}')\n\n        return TemplateResponse(request, eligibility.enrollment_index_template, context)\n\n\ndef _get_group_funding_source(client: Client, group_id, funding_source_id):\n    group_funding_sources = client.get_concession_group_linked_funding_sources(group_id)\n    matching_group_funding_source = None\n    for group_funding_source in group_funding_sources:\n        if group_funding_source.id == funding_source_id:\n            matching_group_funding_source = group_funding_source\n            break\n\n    return matching_group_funding_source\n\n\ndef _is_expired(expiry_date):\n    \"\"\"Returns whether the passed in datetime is expired or not.\"\"\"\n    return expiry_date <= timezone.now()\n\n\ndef _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date):\n    \"\"\"Returns if we are currently within the reenrollment window.\"\"\"\n    return enrollment_reenrollment_date <= timezone.now() < expiry_date\n\n\ndef _calculate_expiry(expiration_days):\n    \"\"\"Returns the expiry datetime, which should be midnight in our configured timezone of the (N + 1)th day from now,\n    where N is expiration_days.\"\"\"\n    default_time_zone = timezone.get_default_timezone()\n    expiry_date = timezone.localtime(timezone=default_time_zone) + timedelta(days=expiration_days + 1)\n    expiry_datetime = expiry_date.replace(hour=0, minute=0, second=0, microsecond=0)\n\n    return expiry_datetime\n\n\ndef reenrollment_error(request):\n    \"\"\"View handler for a re-enrollment attempt that is not yet within the re-enrollment window.\"\"\"\n    analytics.returned_error(request, \"Re-enrollment error\")\n    return TemplateResponse(request, TEMPLATE_REENROLLMENT_ERROR)\n\n\n@decorator_from_middleware(EligibleSessionRequired)\ndef retry(request):\n    \"\"\"View handler for a recoverable failure condition.\"\"\"\n    if request.method == \"POST\":\n        analytics.returned_retry(request)\n        form = forms.CardTokenizeFailForm(request.POST)\n        if form.is_valid():\n            return TemplateResponse(request, TEMPLATE_RETRY)\n        else:\n            analytics.returned_error(request, \"Invalid retry submission.\")\n            raise Exception(\"Invalid retry submission.\")\n    else:\n        analytics.returned_error(request, \"This view method only supports POST.\")\n        raise Exception(\"This view method only supports POST.\")\n\n\n@pageview_decorator\n@decorator_from_middleware(EligibleSessionRequired)\n@decorator_from_middleware(VerifierSessionRequired)\ndef success(request):\n    \"\"\"View handler for the final success page.\"\"\"\n    request.path = \"/enrollment/success\"\n    session.update(request, origin=reverse(ROUTE_SUCCESS))\n\n    agency = session.agency(request)\n    verifier = session.verifier(request)\n    eligibility = session.eligibility(request)\n\n    if session.logged_in(request) and verifier.auth_provider.supports_sign_out:\n        # overwrite origin for a logged in user\n        # if they click the logout button, they are taken to the new route\n        session.update(request, origin=reverse(ROUTE_LOGGED_OUT))\n\n    analytics.returned_success(request, eligibility.group_id)\n    return TemplateResponse(request, agency.enrollment_success_template)\n", "path": "benefits/enrollment/views.py"}], "after_files": [{"content": "\"\"\"\nThe enrollment application: view definitions for the benefits enrollment flow.\n\"\"\"\n\nimport logging\nfrom datetime import timedelta\n\nfrom django.http import JsonResponse\nfrom django.template.response import TemplateResponse\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.decorators import decorator_from_middleware\nfrom littlepay.api.client import Client\nfrom requests.exceptions import HTTPError\n\nfrom benefits.core import session\nfrom benefits.core.middleware import (\n    EligibleSessionRequired,\n    VerifierSessionRequired,\n    pageview_decorator,\n)\nfrom benefits.core.views import ROUTE_LOGGED_OUT\nfrom . import analytics, forms\n\n\nROUTE_INDEX = \"enrollment:index\"\nROUTE_RETRY = \"enrollment:retry\"\nROUTE_SUCCESS = \"enrollment:success\"\nROUTE_TOKEN = \"enrollment:token\"\n\nTEMPLATE_REENROLLMENT_ERROR = \"enrollment/reenrollment-error.html\"\nTEMPLATE_RETRY = \"enrollment/retry.html\"\nTEMPLATE_SUCCESS = \"enrollment/success.html\"\n\n\nlogger = logging.getLogger(__name__)\n\n\n@decorator_from_middleware(EligibleSessionRequired)\ndef token(request):\n    \"\"\"View handler for the enrollment auth token.\"\"\"\n    if not session.enrollment_token_valid(request):\n        agency = session.agency(request)\n        payment_processor = agency.payment_processor\n        client = Client(\n            base_url=payment_processor.api_base_url,\n            client_id=payment_processor.client_id,\n            client_secret=payment_processor.client_secret,\n            audience=payment_processor.audience,\n        )\n        client.oauth.ensure_active_token(client.token)\n        response = client.request_card_tokenization_access()\n        session.update(request, enrollment_token=response.get(\"access_token\"), enrollment_token_exp=response.get(\"expires_at\"))\n\n    data = {\"token\": session.enrollment_token(request)}\n\n    return JsonResponse(data)\n\n\n@decorator_from_middleware(EligibleSessionRequired)\ndef index(request):\n    \"\"\"View handler for the enrollment landing page.\"\"\"\n    session.update(request, origin=reverse(ROUTE_INDEX))\n\n    agency = session.agency(request)\n    eligibility = session.eligibility(request)\n    payment_processor = agency.payment_processor\n\n    # POST back after payment processor form, process card token\n    if request.method == \"POST\":\n        form = forms.CardTokenizeSuccessForm(request.POST)\n        if not form.is_valid():\n            raise Exception(\"Invalid card token form\")\n\n        card_token = form.cleaned_data.get(\"card_token\")\n\n        client = Client(\n            base_url=payment_processor.api_base_url,\n            client_id=payment_processor.client_id,\n            client_secret=payment_processor.client_secret,\n            audience=payment_processor.audience,\n        )\n        client.oauth.ensure_active_token(client.token)\n\n        funding_source = client.get_funding_source_by_token(card_token)\n        group_id = eligibility.group_id\n\n        try:\n            group_funding_source = _get_group_funding_source(\n                client=client, group_id=group_id, funding_source_id=funding_source.id\n            )\n\n            already_enrolled = group_funding_source is not None\n\n            if eligibility.supports_expiration:\n                # set expiry on session\n                if already_enrolled and group_funding_source.expiry_date is not None:\n                    session.update(request, enrollment_expiry=group_funding_source.expiry_date)\n                else:\n                    session.update(request, enrollment_expiry=_calculate_expiry(eligibility.expiration_days))\n\n                if not already_enrolled:\n                    # enroll user with an expiration date, return success\n                    client.link_concession_group_funding_source(\n                        group_id=group_id, funding_source_id=funding_source.id, expiry=session.enrollment_expiry(request)\n                    )\n                    return success(request)\n                else:  # already_enrolled\n                    if group_funding_source.expiry_date is None:\n                        # update expiration of existing enrollment, return success\n                        client.update_concession_group_funding_source_expiry(\n                            group_id=group_id,\n                            funding_source_id=funding_source.id,\n                            expiry=session.enrollment_expiry(request),\n                        )\n                        return success(request)\n                    else:\n                        is_expired = _is_expired(group_funding_source.expiry_date)\n                        is_within_reenrollment_window = _is_within_reenrollment_window(\n                            group_funding_source.expiry_date, session.enrollment_reenrollment(request)\n                        )\n\n                        if is_expired or is_within_reenrollment_window:\n                            # update expiration of existing enrollment, return success\n                            client.update_concession_group_funding_source_expiry(\n                                group_id=group_id,\n                                funding_source_id=funding_source.id,\n                                expiry=session.enrollment_expiry(request),\n                            )\n                            return success(request)\n                        else:\n                            # re-enrollment error, return enrollment error with expiration and reenrollment_date\n                            return reenrollment_error(request)\n            else:  # eligibility does not support expiration\n                if not already_enrolled:\n                    # enroll user with no expiration date, return success\n                    client.link_concession_group_funding_source(group_id=group_id, funding_source_id=funding_source.id)\n                    return success(request)\n                else:  # already_enrolled\n                    if group_funding_source.expiry_date is None:\n                        # no action, return success\n                        return success(request)\n                    else:\n                        # remove expiration date, return success\n                        raise NotImplementedError(\"Removing expiration date is currently not supported\")\n\n        except HTTPError as e:\n            analytics.returned_error(request, str(e))\n            raise Exception(f\"{e}: {e.response.json()}\")\n        except Exception as e:\n            analytics.returned_error(request, str(e))\n            raise e\n\n    # GET enrollment index\n    else:\n        tokenize_retry_form = forms.CardTokenizeFailForm(ROUTE_RETRY)\n        tokenize_success_form = forms.CardTokenizeSuccessForm(auto_id=True, label_suffix=\"\")\n\n        context = {\n            \"forms\": [tokenize_retry_form, tokenize_success_form],\n            \"cta_button\": \"tokenize_card\",\n            \"card_tokenize_env\": agency.payment_processor.card_tokenize_env,\n            \"card_tokenize_func\": agency.payment_processor.card_tokenize_func,\n            \"card_tokenize_url\": agency.payment_processor.card_tokenize_url,\n            \"token_field\": \"card_token\",\n            \"form_retry\": tokenize_retry_form.id,\n            \"form_success\": tokenize_success_form.id,\n        }\n\n        logger.debug(f'card_tokenize_url: {context[\"card_tokenize_url\"]}')\n\n        return TemplateResponse(request, eligibility.enrollment_index_template, context)\n\n\ndef _get_group_funding_source(client: Client, group_id, funding_source_id):\n    group_funding_sources = client.get_concession_group_linked_funding_sources(group_id)\n    matching_group_funding_source = None\n    for group_funding_source in group_funding_sources:\n        if group_funding_source.id == funding_source_id:\n            matching_group_funding_source = group_funding_source\n            break\n\n    return matching_group_funding_source\n\n\ndef _is_expired(expiry_date):\n    \"\"\"Returns whether the passed in datetime is expired or not.\"\"\"\n    return expiry_date <= timezone.now()\n\n\ndef _is_within_reenrollment_window(expiry_date, enrollment_reenrollment_date):\n    \"\"\"Returns if we are currently within the reenrollment window.\"\"\"\n    return enrollment_reenrollment_date <= timezone.now() < expiry_date\n\n\ndef _calculate_expiry(expiration_days):\n    \"\"\"Returns the expiry datetime, which should be midnight in our configured timezone of the (N + 1)th day from now,\n    where N is expiration_days.\"\"\"\n    default_time_zone = timezone.get_default_timezone()\n    expiry_date = timezone.localtime(timezone=default_time_zone) + timedelta(days=expiration_days + 1)\n    expiry_datetime = expiry_date.replace(hour=0, minute=0, second=0, microsecond=0)\n\n    return expiry_datetime\n\n\ndef reenrollment_error(request):\n    \"\"\"View handler for a re-enrollment attempt that is not yet within the re-enrollment window.\"\"\"\n    analytics.returned_error(request, \"Re-enrollment error\")\n    return TemplateResponse(request, TEMPLATE_REENROLLMENT_ERROR)\n\n\n@decorator_from_middleware(EligibleSessionRequired)\ndef retry(request):\n    \"\"\"View handler for a recoverable failure condition.\"\"\"\n    analytics.returned_retry(request)\n    return TemplateResponse(request, TEMPLATE_RETRY)\n\n\n@pageview_decorator\n@decorator_from_middleware(EligibleSessionRequired)\n@decorator_from_middleware(VerifierSessionRequired)\ndef success(request):\n    \"\"\"View handler for the final success page.\"\"\"\n    request.path = \"/enrollment/success\"\n    session.update(request, origin=reverse(ROUTE_SUCCESS))\n\n    agency = session.agency(request)\n    verifier = session.verifier(request)\n    eligibility = session.eligibility(request)\n\n    if session.logged_in(request) and verifier.auth_provider.supports_sign_out:\n        # overwrite origin for a logged in user\n        # if they click the logout button, they are taken to the new route\n        session.update(request, origin=reverse(ROUTE_LOGGED_OUT))\n\n    analytics.returned_success(request, eligibility.group_id)\n    return TemplateResponse(request, agency.enrollment_success_template)\n", "path": "benefits/enrollment/views.py"}]}