diff --git a/benefits/in_person/forms.py b/benefits/in_person/forms.py
new file mode 100644
index 000000000..f09fabe35
--- /dev/null
+++ b/benefits/in_person/forms.py
@@ -0,0 +1,32 @@
+"""
+The in-person eligibility application: Form definition for the
+in-person eligibility verification flow, in which a
+transit agency employee manually verifies a rider's eligibility.
+"""
+
+from django import forms
+from benefits.routes import routes
+from benefits.core import models
+
+
+class InPersonEligibilityForm(forms.Form):
+ """Form to capture eligibility for in-person verification flow selection."""
+
+ action_url = routes.IN_PERSON_ELIGIBILITY
+ id = "form-flow-selection"
+ method = "POST"
+
+ flow = forms.ChoiceField(label="Choose an eligibility type to qualify this rider.", widget=forms.widgets.RadioSelect)
+ verified = forms.BooleanField(label="I have verified this person’s eligibility for a transit benefit.", required=True)
+
+ cancel_url = routes.ADMIN_INDEX
+
+ def __init__(self, agency: models.TransitAgency, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ flows = agency.enrollment_flows.all()
+
+ self.classes = "checkbox-parent"
+ flow_field = self.fields["flow"]
+ flow_field.choices = [(f.id, f.label) for f in flows]
+ flow_field.widget.attrs.update({"data-custom-validity": "Please choose a transit benefit."})
+ self.use_custom_validity = True
diff --git a/benefits/in_person/templates/in_person/eligibility.html b/benefits/in_person/templates/in_person/eligibility.html
index e69de29bb..1955233af 100644
--- a/benefits/in_person/templates/in_person/eligibility.html
+++ b/benefits/in_person/templates/in_person/eligibility.html
@@ -0,0 +1,30 @@
+{% extends "admin/agency-base.html" %}
+
+{% block title %}
+ Agency dashboard: In-person enrollment
+{% endblock title %}
+
+{% block content %}
+
+
+
+
In-person enrollment
+
+
{% include "core/includes/form.html" with form=form %}
+
+
+ {% url form.cancel_url as url_cancel %}
+
Cancel
+
+
+
+
+
+
+
+{% endblock content %}
diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py
index e821e2b28..88568a175 100644
--- a/benefits/in_person/views.py
+++ b/benefits/in_person/views.py
@@ -1,8 +1,39 @@
+from django.contrib.admin import site as admin_site
from django.template.response import TemplateResponse
+from django.shortcuts import redirect
+from django.urls import reverse
+
+
+from benefits.routes import routes
+from benefits.core import session
+from benefits.core.models import EnrollmentFlow
+
+from benefits.in_person import forms
def eligibility(request):
- return TemplateResponse(request, "in_person/eligibility.html")
+ """View handler for the in-person eligibility flow selection form."""
+
+ agency = session.agency(request)
+ context = {**admin_site.each_context(request), "form": forms.InPersonEligibilityForm(agency=agency)}
+
+ if request.method == "POST":
+ form = forms.InPersonEligibilityForm(data=request.POST, agency=agency)
+
+ if form.is_valid():
+ flow_id = form.cleaned_data.get("flow")
+ flow = EnrollmentFlow.objects.get(id=flow_id)
+ session.update(request, flow=flow)
+
+ in_person_enrollment = reverse(routes.IN_PERSON_ENROLLMENT)
+ response = redirect(in_person_enrollment)
+ else:
+ context["form"] = form
+ response = TemplateResponse(request, "in_person/eligibility.html", context)
+ else:
+ response = TemplateResponse(request, "in_person/eligibility.html", context)
+
+ return response
def enrollment(request):
diff --git a/benefits/routes.py b/benefits/routes.py
index 8daed2a31..f3743af60 100644
--- a/benefits/routes.py
+++ b/benefits/routes.py
@@ -121,6 +121,11 @@ def ENROLLMENT_SYSTEM_ERROR(self):
"""Enrollment error not caused by the user."""
return "enrollment:system_error"
+ @property
+ def ADMIN_INDEX(self):
+ """Admin index page"""
+ return "admin:index"
+
@property
def IN_PERSON_ELIGIBILITY(self):
"""In-person (e.g. agency assisted) eligibility"""
diff --git a/benefits/static/css/admin/styles.css b/benefits/static/css/admin/styles.css
index d0ba275bf..ae33ad12d 100644
--- a/benefits/static/css/admin/styles.css
+++ b/benefits/static/css/admin/styles.css
@@ -2,6 +2,7 @@
/* Buttons */
/* Primary Button: Use all three classes: btn btn-lg btn-primary */
+/* Outline Primary Button: Use all three classes: btn btn-lg btn-outline-primary */
/* Set button width in parent with Bootstrap column */
/* Height: 60px on Desktop; 72 on mobile*/
@@ -15,8 +16,16 @@
}
}
+.btn.btn-lg.btn-outline-primary:not(:hover) {
+ color: var(--primary-color);
+}
+
.btn.btn-lg.btn-primary {
background-color: var(--primary-color);
+}
+
+.btn.btn-lg.btn-primary,
+.btn.btn-lg.btn-outline-primary {
border-color: var(--primary-color);
border-width: 2px;
font-weight: var(--medium-font-weight);
@@ -26,7 +35,8 @@
padding: var(--primary-button-padding);
}
-.btn.btn-lg.btn-primary:hover {
+.btn.btn-lg.btn-primary:hover,
+.btn.btn-lg.btn-outline-primary:hover {
background-color: var(--hover-color);
border-color: var(--hover-color);
}
@@ -63,3 +73,19 @@ html[data-theme="light"],
#logout-form button {
text-transform: unset;
}
+
+.checkbox-parent .form-group:last-of-type .col-12 {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: start;
+ column-gap: 0.5rem;
+ margin-top: 2rem;
+}
+
+.checkbox-parent,
+.checkbox-parent .form-group .col-12,
+.checkbox-parent .form-group .col-12 #id_flow {
+ display: flex;
+ flex-direction: column;
+ row-gap: 1rem;
+}
diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py
index 9aa9e16df..d2179cba5 100644
--- a/tests/pytest/in_person/test_views.py
+++ b/tests/pytest/in_person/test_views.py
@@ -17,6 +17,8 @@ def test_view_not_logged_in(client, viewname):
# admin_client is a fixture from pytest
# https://pytest-django.readthedocs.io/en/latest/helpers.html#admin-client-django-test-client-logged-in-as-admin
+@pytest.mark.django_db
+@pytest.mark.usefixtures("mocked_session_agency")
def test_eligibility_logged_in(admin_client):
path = reverse(routes.IN_PERSON_ELIGIBILITY)
@@ -31,3 +33,27 @@ def test_enrollment_logged_in(admin_client):
response = admin_client.get(path)
assert response.status_code == 200
assert response.template_name == "in_person/enrollment.html"
+
+
+@pytest.mark.django_db
+@pytest.mark.usefixtures("mocked_session_agency")
+def test_confirm_post_valid_form_eligibility_verified(admin_client):
+
+ path = reverse(routes.IN_PERSON_ELIGIBILITY)
+ form_data = {"flow": 1, "verified": True}
+ response = admin_client.post(path, form_data)
+
+ assert response.status_code == 302
+ assert response.url == reverse(routes.IN_PERSON_ENROLLMENT)
+
+
+@pytest.mark.django_db
+@pytest.mark.usefixtures("mocked_session_agency")
+def test_confirm_post_valid_form_eligibility_unverified(admin_client):
+
+ path = reverse(routes.IN_PERSON_ELIGIBILITY)
+ form_data = {"flow": 1, "verified": False}
+ response = admin_client.post(path, form_data)
+
+ assert response.status_code == 200
+ assert response.template_name == "in_person/eligibility.html"