diff --git a/efile_app/efile/tests/test_workflow.py b/efile_app/efile/tests/test_workflow.py new file mode 100644 index 0000000..98437bd --- /dev/null +++ b/efile_app/efile/tests/test_workflow.py @@ -0,0 +1,90 @@ +import pytest +from django.urls import reverse + +from efile.workflow import ( + FILING_WORKFLOW, + WorkflowStepKey, + get_next_step, + get_previous_step, + get_step, + get_step_url, + get_workflow_context, + get_workflow_steps, +) + +EXPECTED_WORKFLOW_KEYS = [ + WorkflowStepKey.OPTIONS, + WorkflowStepKey.UPLOAD_FIRST, + WorkflowStepKey.CASE_INFORMATION, + WorkflowStepKey.DOCUMENTS, + WorkflowStepKey.PAYMENT, + WorkflowStepKey.REVIEW, + WorkflowStepKey.CONFIRMATION, +] + + +@pytest.mark.parametrize( + ("step_key", "label"), + [ + (WorkflowStepKey.OPTIONS, "Options"), + (WorkflowStepKey.UPLOAD_FIRST, "Upload lead document"), + (WorkflowStepKey.CASE_INFORMATION, "Case information"), + (WorkflowStepKey.DOCUMENTS, "Documents"), + (WorkflowStepKey.PAYMENT, "Payment"), + (WorkflowStepKey.REVIEW, "Review"), + (WorkflowStepKey.CONFIRMATION, "Confirmation"), + ], +) +def test_get_step_returns_registered_step(step_key, label): + step = get_step(step_key) + + assert step.key == step_key + assert step.label == label + + +def test_get_workflow_steps_returns_ordered_workflow(): + assert get_workflow_steps() == FILING_WORKFLOW + assert [step.key for step in get_workflow_steps()] == EXPECTED_WORKFLOW_KEYS + + +def test_get_step_raises_key_error_for_invalid_step(): + with pytest.raises(KeyError): + get_step("invalid_step") + + +def test_get_previous_step_returns_none_for_first_step(): + assert get_previous_step(WorkflowStepKey.OPTIONS) is None + + +def test_get_previous_step_returns_prior_step(): + previous_step = get_previous_step(WorkflowStepKey.CASE_INFORMATION) + + assert previous_step.key == WorkflowStepKey.UPLOAD_FIRST + + +def test_get_next_step_returns_following_step(): + next_step = get_next_step(WorkflowStepKey.CASE_INFORMATION) + + assert next_step.key == WorkflowStepKey.DOCUMENTS + + +def test_get_next_step_returns_none_for_last_step(): + assert get_next_step(WorkflowStepKey.CONFIRMATION) is None + + +def test_get_step_url_reverses_workflow_route(): + expected_url = reverse("payment", kwargs={"jurisdiction": "illinois"}) + + assert get_step_url(WorkflowStepKey.PAYMENT, "illinois") == expected_url + + +def test_get_workflow_context_includes_current_previous_and_next_urls(): + context = get_workflow_context(WorkflowStepKey.PAYMENT, "illinois") + previous_url = reverse("upload", kwargs={"jurisdiction": "illinois"}) + next_url = reverse("case_review", kwargs={"jurisdiction": "illinois"}) + + assert context["workflow_current_step"].key == WorkflowStepKey.PAYMENT + assert context["workflow_previous_step"].key == WorkflowStepKey.DOCUMENTS + assert context["workflow_next_step"].key == WorkflowStepKey.REVIEW + assert context["workflow_previous_url"] == previous_url + assert context["workflow_next_url"] == next_url diff --git a/efile_app/efile/views/confirmation.py b/efile_app/efile/views/confirmation.py index 597e27a..424f2d1 100644 --- a/efile_app/efile/views/confirmation.py +++ b/efile_app/efile/views/confirmation.py @@ -2,6 +2,8 @@ from efile.api.suffolk_api_views import get_tyler_token +from ..workflow import WorkflowStepKey, get_workflow_context + def filing_confirmation(request, jurisdiction): """Confirmation page after successful filing submission.""" @@ -17,5 +19,6 @@ def filing_confirmation(request, jurisdiction): "page_title": "Filing Confirmation", "success_message": "Your filing has been successfully submitted!", } + context.update(get_workflow_context(WorkflowStepKey.CONFIRMATION, jurisdiction)) return render(request, "efile/confirmation.html", context) diff --git a/efile_app/efile/views/expert_form.py b/efile_app/efile/views/expert_form.py index 64ecbf5..96dfd99 100644 --- a/efile_app/efile/views/expert_form.py +++ b/efile_app/efile/views/expert_form.py @@ -5,6 +5,7 @@ from efile.api.suffolk_api_views import get_tyler_token from ..utils.case_data_utils import get_upload_data +from ..workflow import WorkflowStepKey, get_workflow_context logger = logging.getLogger(__name__) @@ -59,5 +60,6 @@ def efile_expert_form(request, jurisdiction): "missing_required_fields": not has_all_required, "missing_party_info": has_all_required and not has_party_info, } + context.update(get_workflow_context(WorkflowStepKey.CASE_INFORMATION, jurisdiction)) return render(request, "efile/expert_form.html", context) diff --git a/efile_app/efile/views/options.py b/efile_app/efile/views/options.py index f2c996d..f6d6182 100644 --- a/efile_app/efile/views/options.py +++ b/efile_app/efile/views/options.py @@ -3,6 +3,7 @@ from efile.api.suffolk_api_views import get_tyler_token from ..utils.case_data_utils import get_case_data +from ..workflow import WorkflowStepKey, get_workflow_context def efile_options(request, jurisdiction): @@ -17,14 +18,12 @@ def efile_options(request, jurisdiction): if not get_tyler_token(request, jurisdiction): is_logged_in = False - is_logged_in = request.user.is_authenticated - if not get_tyler_token(request, jurisdiction): - is_logged_in = False # Pass case data to template for display context = { "is_logged_in": is_logged_in, "case_data": case_data, "has_case_data": bool(case_data), } + context.update(get_workflow_context(WorkflowStepKey.OPTIONS, jurisdiction)) return render(request, "efile/options.html", context) diff --git a/efile_app/efile/views/payment.py b/efile_app/efile/views/payment.py index c6d15d9..8462887 100644 --- a/efile_app/efile/views/payment.py +++ b/efile_app/efile/views/payment.py @@ -7,6 +7,7 @@ from efile.api.suffolk_api_views import get_tyler_token from ..utils.case_data_utils import get_case_data +from ..workflow import WorkflowStepKey, get_workflow_context logger = logging.getLogger(__name__) @@ -37,5 +38,6 @@ def efile_payment(request, jurisdiction): "new_toga_url": new_toga_url, "case_data": case_data, } + context.update(get_workflow_context(WorkflowStepKey.PAYMENT, jurisdiction)) return render(request, "efile/payment.html", context) diff --git a/efile_app/efile/views/review.py b/efile_app/efile/views/review.py index 1b386e8..d980aee 100644 --- a/efile_app/efile/views/review.py +++ b/efile_app/efile/views/review.py @@ -7,6 +7,7 @@ from efile.api.suffolk_api_views import get_tyler_token from ..utils.case_data_utils import get_case_classification, get_case_data, get_name_sought_info, get_petitioner_info +from ..workflow import WorkflowStepKey, get_workflow_context logger = logging.getLogger(__name__) @@ -112,5 +113,6 @@ def case_review(request, jurisdiction): "document_type": friendly_document_type, }, } + context.update(get_workflow_context(WorkflowStepKey.REVIEW, jurisdiction)) return render(request, "efile/review.html", context) diff --git a/efile_app/efile/views/upload.py b/efile_app/efile/views/upload.py index 7e51641..3393cb5 100644 --- a/efile_app/efile/views/upload.py +++ b/efile_app/efile/views/upload.py @@ -13,6 +13,7 @@ get_petitioner_info, get_upload_data, ) +from ..workflow import WorkflowStepKey, get_workflow_context logger = logging.getLogger(__name__) @@ -20,24 +21,19 @@ def efile_upload(request, jurisdiction): """Upload view for document submission and filing creation.""" - # Check if user is authenticated first if not request.user.is_authenticated: return redirect("efile_login", jurisdiction=jurisdiction) - # Get case data from session case_data = get_case_data(request) - # If no case data exists, redirect back to options page if not case_data: messages.error(request, gettext("Please complete the case details first.")) return redirect("efile_options", jurisdiction=jurisdiction) - # Get organized case information petitioner_info = get_petitioner_info(request) name_sought_info = get_name_sought_info(request) case_classification = get_case_classification(request) - # Use friendly names if available, otherwise fallback to raw values friendly_case_type = case_data.get("case_type_name", case_classification["case_type"]) friendly_filing_type = case_data.get("filing_type_name", case_classification["filing_type"]) friendly_court = case_data.get("court_name", case_classification["court"]) @@ -62,5 +58,6 @@ def efile_upload(request, jurisdiction): "filing_type_raw": case_classification["filing_type"], "court_raw": case_classification["court"], } + context.update(get_workflow_context(WorkflowStepKey.DOCUMENTS, jurisdiction)) return render(request, "efile/upload.html", context) diff --git a/efile_app/efile/views/upload_first.py b/efile_app/efile/views/upload_first.py index 24c5858..1a63de6 100644 --- a/efile_app/efile/views/upload_first.py +++ b/efile_app/efile/views/upload_first.py @@ -12,6 +12,7 @@ get_upload_data, ) from ..utils.django_helpers import flush_cache_stay_logged_in +from ..workflow import WorkflowStepKey, get_workflow_context logger = logging.getLogger(__name__) @@ -55,5 +56,6 @@ def efile_upload_first(request, jurisdiction): "name_sought_info": name_sought_info, "case_classification": case_classification, } + context.update(get_workflow_context(WorkflowStepKey.UPLOAD_FIRST, jurisdiction)) return render(request, "efile/upload_first.html", context) diff --git a/efile_app/efile/workflow.py b/efile_app/efile/workflow.py new file mode 100644 index 0000000..d2445d2 --- /dev/null +++ b/efile_app/efile/workflow.py @@ -0,0 +1,115 @@ +"""Central filing workflow registry. + +Use FILING_WORKFLOW as the single high-level map of the filing flow. + +To add a step: +1. Add a WorkflowStepKey member for the new step. +2. Add the URL route and view. +3. Add a WorkflowStep entry in the desired position below. +4. Add get_workflow_context(WorkflowStepKey.YOUR_STEP, jurisdiction) to that view's context. +5. Update any navigation copy that mentions the surrounding steps. +6. Update efile/tests/test_workflow.py. + +To rearrange steps: +1. Reorder FILING_WORKFLOW. +2. Update affected labels, navigation copy, and workflow tests. + +This registry is intentionally linear for now. Future branching should be added +here after the durable filing draft model exists as the workflow state source. +""" + +from dataclasses import dataclass +from enum import StrEnum + +from django.urls import reverse + + +class WorkflowStepKey(StrEnum): + """Stable identifiers for filing workflow steps.""" + + OPTIONS = "options" + UPLOAD_FIRST = "upload_first" + CASE_INFORMATION = "case_information" + DOCUMENTS = "documents" + PAYMENT = "payment" + REVIEW = "review" + CONFIRMATION = "confirmation" + + +@dataclass(frozen=True) +class WorkflowStep: + """A single screen in the filing workflow.""" + + key: WorkflowStepKey + label: str + url_name: str + + +FILING_WORKFLOW: tuple[WorkflowStep, ...] = ( + WorkflowStep(WorkflowStepKey.OPTIONS, "Options", "efile_options"), + WorkflowStep(WorkflowStepKey.UPLOAD_FIRST, "Upload lead document", "upload_first"), + WorkflowStep(WorkflowStepKey.CASE_INFORMATION, "Case information", "expert_form"), + WorkflowStep(WorkflowStepKey.DOCUMENTS, "Documents", "upload"), + WorkflowStep(WorkflowStepKey.PAYMENT, "Payment", "payment"), + WorkflowStep(WorkflowStepKey.REVIEW, "Review", "case_review"), + WorkflowStep(WorkflowStepKey.CONFIRMATION, "Confirmation", "filing_confirmation"), +) + + +def get_workflow_steps() -> tuple[WorkflowStep, ...]: + return FILING_WORKFLOW + + +def get_step(step_key: WorkflowStepKey | str) -> WorkflowStep: + try: + return next(step for step in FILING_WORKFLOW if step.key == step_key) + except StopIteration as exc: + raise KeyError(f"Unknown workflow step: {step_key}") from exc + + +def get_step_index(step_key: WorkflowStepKey | str) -> int: + for index, step in enumerate(FILING_WORKFLOW): + if step.key == step_key: + return index + raise KeyError(f"Unknown workflow step: {step_key}") + + +def get_previous_step(step_key: WorkflowStepKey | str) -> WorkflowStep | None: + index = get_step_index(step_key) + if index == 0: + return None + return FILING_WORKFLOW[index - 1] + + +def get_next_step(step_key: WorkflowStepKey | str) -> WorkflowStep | None: + index = get_step_index(step_key) + try: + return FILING_WORKFLOW[index + 1] + except IndexError: + return None + + +def get_step_url(step_key: WorkflowStepKey | str, jurisdiction: str) -> str: + step = get_step(step_key) + return reverse(step.url_name, kwargs={"jurisdiction": jurisdiction}) + + +def get_workflow_context(current_step: WorkflowStepKey | str, jurisdiction: str) -> dict: + previous_step = get_previous_step(current_step) + next_step = get_next_step(current_step) + previous_url = None + next_url = None + + if previous_step: + previous_url = get_step_url(previous_step.key, jurisdiction) + if next_step: + next_url = get_step_url(next_step.key, jurisdiction) + + return { + "workflow_steps": get_workflow_steps(), + "workflow_current_step": get_step(current_step), + "workflow_previous_step": previous_step, + "workflow_next_step": next_step, + "workflow_previous_url": previous_url, + "workflow_next_url": next_url, + }