diff --git a/efile_app/efile/migrations/0030_document_type_confirmed.py b/efile_app/efile/migrations/0030_document_type_confirmed.py new file mode 100644 index 00000000..99d6722b --- /dev/null +++ b/efile_app/efile/migrations/0030_document_type_confirmed.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.17 on 2026-10-01 19:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("efile", "0029_document_preparation_and_preview"), + ] + + operations = [ + migrations.AddField( + model_name="filingdocument", + name="document_type_confirmed", + field=models.BooleanField(default=False), + ), + ] diff --git a/efile_app/efile/models.py b/efile_app/efile/models.py index 1f433cb1..0bf66ed2 100644 --- a/efile_app/efile/models.py +++ b/efile_app/efile/models.py @@ -370,6 +370,10 @@ class Role(models.TextChoices): ) preparation_reviewed_at = models.DateTimeField(null=True, blank=True) + # A blank document type is a completed answer only when Organize checked + # that the court offers no confidentiality choices for this filing type. + document_type_confirmed = models.BooleanField(default=False) + filing_type_code = models.CharField(max_length=100, blank=True) filing_type_name = models.CharField(max_length=255, blank=True) document_type_code = models.CharField(max_length=100, blank=True) diff --git a/efile_app/efile/services/drafts.py b/efile_app/efile/services/drafts.py index da651776..c8db8995 100644 --- a/efile_app/efile/services/drafts.py +++ b/efile_app/efile/services/drafts.py @@ -426,6 +426,10 @@ def _apply_document(doc: FilingDocument, file_obj: dict[str, Any], config: dict[ value = config.get(config_key) if isinstance(value, dict): value = value.get("id") or value.get("code") or "" + if model_field in {"filing_type_code", "document_type_code"} and getattr(doc, model_field) != _as_str( + value + ): + doc.document_type_confirmed = False setattr(doc, model_field, _as_str(value)) if "optional_services" in config: @@ -508,6 +512,11 @@ def write_upload_data( if document.s3_key ] claimed_items = {document.s3_key: document.checklist_item_id for document in previous} + confirmed_types = { + document.s3_key: (document.filing_type_code, document.document_type_code) + for document in previous + if document.document_type_confirmed + } # Handoff provenance and pending corrections are keyed by row id, so # they follow the file to its rebuilt row the same way. previous_ids = {document.s3_key: document.pk for document in previous} @@ -524,6 +533,9 @@ def write_upload_data( _upsert_document(draft, FilingDocument.Role.SUPPORTING, index, file_obj or {}, config or {}) moved = {} for document in FilingDocument.objects.filter(draft=draft, role=FilingDocument.Role.SUPPORTING): + if confirmed_types.get(document.s3_key) == (document.filing_type_code, document.document_type_code): + document.document_type_confirmed = True + document.save(update_fields=["document_type_confirmed", "updated_at"]) if metadata := preparation_metadata.get(document.s3_key): for field, value in metadata.items(): setattr(document, field, value) diff --git a/efile_app/efile/services/filing_path.py b/efile_app/efile/services/filing_path.py index d85cd39a..0a155e67 100644 --- a/efile_app/efile/services/filing_path.py +++ b/efile_app/efile/services/filing_path.py @@ -112,6 +112,7 @@ def change_filing_path(draft: FilingDraft, new_path: str) -> FilingPathChange: chosen.update( filing_type_code="", filing_type_name="", + document_type_confirmed=False, filing_component_code="", filing_component_name="", requested_optional_services=[], diff --git a/efile_app/efile/static/js/document-checks.js b/efile_app/efile/static/js/document-checks.js index 10ecdc04..e495425f 100644 --- a/efile_app/efile/static/js/document-checks.js +++ b/efile_app/efile/static/js/document-checks.js @@ -29,6 +29,8 @@ const DocumentChecks = { body.append("action", action); body.append("document_id", check.dataset.documentId); body.append("preview_fingerprint", check.dataset.fingerprint); + const feeToken = document.getElementById("fee-inputs-token"); + if (feeToken) body.append("fee_inputs_token", JSON.parse(feeToken.textContent)); body.append("csrfmiddlewaretoken", apiUtils.getCSRFToken()); buttons.forEach((element) => { element.disabled = true; diff --git a/efile_app/efile/static/js/payment.js b/efile_app/efile/static/js/payment.js index 722d0925..0fd7169a 100644 --- a/efile_app/efile/static/js/payment.js +++ b/efile_app/efile/static/js/payment.js @@ -66,6 +66,8 @@ const PaymentPage = { action, data }) { + // The check endpoint verified this page's fee token before changing + // documents, so the returned token still matches our caseData. document.getElementById("fee-inputs-token").textContent = JSON.stringify(data.fee_inputs_token); const confirmation = document.getElementById("waiver-upload-confirmation"); if (action === "remove") { diff --git a/efile_app/efile/tests/test_detours.py b/efile_app/efile/tests/test_detours.py index 31915cc0..8150b418 100644 --- a/efile_app/efile/tests/test_detours.py +++ b/efile_app/efile/tests/test_detours.py @@ -11,7 +11,10 @@ from django.urls import reverse from efile.models import FilingDocument, FilingParty +from efile.services.drafts import read_upload_data, write_upload_data +from efile.services.filing_path import change_filing_path from efile.tests.test_review_submit_flow import submission_draft as _submission_draft +from efile.views.organize_documents import _save_document_details from efile.workflow import ( WorkflowStepKey, clean_return_to, @@ -67,12 +70,51 @@ def test_an_empty_document_type_is_a_finished_answer(draft): # Organize saves no document type when the court offers no confidentiality # choices. Treating that as unorganized would send Organize back to itself. draft.documents.update(document_type_code="", document_type_name="") + assert documents_need_organizing(draft) + lead = draft.documents.get() + + with patch("efile.views.organize_documents._court_document_types", return_value=[]): + _save_document_details( + draft, + [{"id": lead.pk, "filing_type": lead.filing_type_code, "document_type": ""}], + lead.pk, + ) assert not documents_need_organizing(draft) assert continue_url(draft, "illinois", "review", WorkflowStepKey.YOUR_INFORMATION) == reverse( "case_review", kwargs=J ) +def test_empty_confidentiality_confirmation_is_reset_when_the_filing_path_changes(draft): + draft.documents.update(document_type_code="", document_type_confirmed=True) + change_filing_path(draft, "existing") + lead = draft.documents.get() + assert not lead.document_type_confirmed + lead.filing_type_code = "automatically-assigned" + lead.save() + assert documents_need_organizing(draft) + + +def test_empty_confidentiality_confirmation_survives_rebuild_only_for_the_same_types(draft): + document = FilingDocument.objects.create( + draft=draft, + role="supporting", + s3_key="supporting.pdf", + filing_type_code="motion", + document_type_confirmed=True, + ) + wire = read_upload_data(draft) + write_upload_data(draft, wire) + document = draft.documents.get(s3_key=document.s3_key) + assert document.document_type_confirmed + assert not documents_need_organizing(draft) + + wire["supporting_documents"][0]["filing_type"] = "different-type" + write_upload_data(draft, wire) + assert not draft.documents.get(s3_key=document.s3_key).document_type_confirmed + assert documents_need_organizing(draft) + + def test_adding_a_person_on_a_handoff_detour_stays_on_the_people_screens(client, draft): response = client.post(step("parties", draft, "handoff"), {"action": "add", "return_to": "handoff"}) assert response.status_code == 302 diff --git a/efile_app/efile/tests/test_document_checks.py b/efile_app/efile/tests/test_document_checks.py index 9db181a9..c8d77e17 100644 --- a/efile_app/efile/tests/test_document_checks.py +++ b/efile_app/efile/tests/test_document_checks.py @@ -1,5 +1,6 @@ """Confirming or removing a newly prepared copy on the page it was added to.""" +import io from unittest.mock import MagicMock, patch import pytest @@ -9,6 +10,7 @@ from efile.models import FilingDocument, FilingDraft from efile.services.document_previews import preview_fingerprint from efile.services.fee_quotes import fee_inputs_token +from efile.tests.pdf_helpers import pdf_bytes from efile.tests.test_review_submit_flow import submission_draft as _submission_draft draft = _submission_draft @@ -49,6 +51,29 @@ def test_confirming_marks_only_that_copy_checked(client, draft, added): assert added.preparation_reviewed_at is not None +@pytest.mark.parametrize("action", ["confirm", "remove"]) +def test_payment_checks_reject_unrelated_fee_input_changes(client, draft, added, action): + token = fee_inputs_token(draft) + # Another tab changes the case, but this document's preview is unchanged. + draft.case_type_code = "another-case-type" + draft.save(update_fields=["case_type_code", "updated_at"]) + response = client.post(checks(draft), confirm(added, action=action, fee_inputs_token=token)) + assert response.status_code == 409 + assert "Reload this page" in response.json()["error"] + assert "fee_inputs_token" not in response.json() + added.refresh_from_db() + assert added.preparation_reviewed_at is None + + +def test_payment_confirmation_returns_only_a_token_matching_the_page(client, draft, added): + token = fee_inputs_token(draft) + response = client.post(checks(draft), confirm(added, fee_inputs_token=token)) + assert response.status_code == 200 + assert response.json()["fee_inputs_token"] == token + added.refresh_from_db() + assert added.preparation_reviewed_at is not None + + def test_a_changed_unprepared_or_foreign_copy_is_not_confirmed(client, draft, added): assert client.post(checks(draft), confirm(added, preview_fingerprint="old")).status_code == 409 FilingDocument.objects.filter(pk=added.pk).update(preparation="") @@ -66,7 +91,7 @@ def test_removing_takes_out_a_supporting_file_and_reprices(client, draft, added, lead = draft.documents.get(role=FilingDocument.Role.LEAD) draft.quoted_fee_total = "100" draft.save() - remove = {"action": "remove"} + remove = {"action": "remove", "fee_inputs_token": fee_inputs_token(draft)} assert client.post(checks(draft), remove | {"document_id": lead.pk}).status_code == 400 storage = MagicMock() storage.delete_file.return_value = {"success": True} @@ -80,6 +105,7 @@ def test_removing_takes_out_a_supporting_file_and_reprices(client, draft, added, draft.refresh_from_db() assert draft.quoted_fee_total == "" assert response.json()["fee_inputs_token"] == fee_inputs_token(draft) + assert response.json()["fee_inputs_token"] != remove["fee_inputs_token"] storage.delete_file.assert_called_with("supporting/exhibit.pdf") @@ -93,3 +119,58 @@ def test_checks_need_a_signed_in_named_filing_and_csrf(client, draft, added): protected = Client(enforce_csrf_checks=True) protected.force_login(draft.user) assert protected.post(checks(draft), confirm(added)).status_code == 403 + + +@pytest.mark.parametrize("page", ["payment", "document_checklist"]) +@pytest.mark.parametrize("method", ["get", "post"]) +@pytest.mark.parametrize("return_to", ["", "review", "handoff"]) +def test_legacy_main_documents_recover_through_preparation(client, draft, page, method, return_to): + lead = draft.documents.get(role="lead") + draft.documents.update(preparation="", preparation_reviewed_at=None, s3_key="legacy/filing.pdf") + url = reverse(page, kwargs={"jurisdiction": draft.jurisdiction}) + f"?draft={draft.pk}" + if return_to: + url += f"&return_to={return_to}" + with patch("efile.views.document_checklist.draft_unavailable_message", return_value=""): + response = getattr(client, method)(url) + assert response.status_code == 302 + assert response.url.partition("?")[0] == reverse("preview_documents", kwargs={"jurisdiction": draft.jurisdiction}) + assert f"draft={draft.pk}" in response.url + if return_to: + assert f"return_to={return_to}" in response.url + + storage = MagicMock() + storage.bucket_name = "private" + storage.s3_client.get_object.side_effect = lambda **_kwargs: {"Body": io.BytesIO(pdf_bytes())} + storage.upload_file.return_value = {"success": True, "key": "prepared/filing.pdf"} + storage.get_public_url.return_value = "https://synthetic.invalid/prepared.pdf" + with patch("efile.views.document_previews.S3UploadHandler", return_value=storage): + prepared = client.get(response.url) + assert prepared.status_code == 200 + lead.refresh_from_db() + assert lead.preparation == "unchanged" + assert lead.preparation_reviewed_at is None + approved = client.post( + response.url, + {"preview_fingerprint": preview_fingerprint(list(draft.documents.all())), "return_to": return_to}, + ) + assert approved.status_code == 302 + lead.refresh_from_db() + assert lead.preparation_reviewed_at is not None + # The original screen is now reachable and the filer can continue there. + with ( + patch("efile.views.document_checklist.draft_unavailable_message", return_value=""), + patch("efile.views.payment.estimate_fees", return_value={}), + ): + assert client.get(url).status_code == 200 + + +@pytest.mark.parametrize("page", ["payment", "document_checklist"]) +def test_prepared_unchecked_files_stay_inline(client, draft, added, page): + url = reverse(page, kwargs={"jurisdiction": draft.jurisdiction}) + f"?draft={draft.pk}" + with ( + patch("efile.views.document_checklist.draft_unavailable_message", return_value=""), + patch("efile.views.payment.estimate_fees", return_value={}), + ): + response = client.get(url) + assert response.status_code == 200 + assert f'data-document-id="{added.pk}"' in response.content.decode() diff --git a/efile_app/efile/tests/test_filing_path_navigation.py b/efile_app/efile/tests/test_filing_path_navigation.py index bff0a136..1e0d7ca6 100644 --- a/efile_app/efile/tests/test_filing_path_navigation.py +++ b/efile_app/efile/tests/test_filing_path_navigation.py @@ -328,6 +328,9 @@ def test_changing_back_to_a_new_case_drops_the_existing_case_it_was_filed_into(s def test_an_unchanged_answer_from_review_still_returns_to_review(signed_in): draft = start_from_menu(signed_in, ExistingCase.NEW) lead = lead_with_evidence(draft, phase="initial", title="Complaint") + # This detour starts after the filer organized the filing for Review. + lead.document_type_code = "public" + lead.save() response = confirm(signed_in, existing_case=ExistingCase.NEW, return_to="review") diff --git a/efile_app/efile/tests/test_filing_plan_actions.py b/efile_app/efile/tests/test_filing_plan_actions.py index 3cfa4dc9..08d2d010 100644 --- a/efile_app/efile/tests/test_filing_plan_actions.py +++ b/efile_app/efile/tests/test_filing_plan_actions.py @@ -6,13 +6,16 @@ there -- and the home the plan lives at between filings. """ +import json from unittest.mock import Mock, patch import pytest +from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from efile.models import FilingDocument, FilingDraft, FilingPlan from efile.services.current_drafts import CURRENT_DRAFT_SESSION_KEY +from efile.services.document_previews import preview_fingerprint from efile.services.drafts import read_upload_data, write_upload_data from efile.services.filing_plans import ( documents_missing_from_envelope, @@ -419,6 +422,72 @@ def test_a_complete_filing_returns_straight_to_review(client, signed_in): assert response.url.partition("?")[0] == reverse("case_review", kwargs={"jurisdiction": "illinois"}) +@pytest.mark.django_db +def test_checklist_upload_with_an_automatic_filing_type_requires_confidentiality(client, signed_in): + def upload(draft, *_args, **_kwargs): + FilingDocument.objects.create( + draft=draft, + role="supporting", + name="waiver.pdf", + preparation="unchanged", + ) + + with ( + patch("efile.views.document_checklist.upload_files", side_effect=upload), + patch("efile.views.document_checklist.filing_type_for_item", return_value=("6529", "Fee waiver")), + ): + attached = client.post( + f"{CHECKLIST_URL}?return_to=review", + { + "action": "attach_item", + "item_id": "fee_waiver", + "document": SimpleUploadedFile("waiver.pdf", b"uploaded copy"), + }, + ) + assert "#document-checks" in attached.url + document = signed_in.documents.get(role="supporting") + assert document.filing_type_code == "6529" + assert document.document_type_code == "" + assert not document.document_type_confirmed + checked = client.post( + reverse("document_checks", kwargs={"jurisdiction": "illinois"}) + f"?draft={signed_in.pk}", + {"action": "confirm", "document_id": document.pk, "preview_fingerprint": preview_fingerprint([document])}, + ) + assert checked.status_code == 200 + continued = client.post(f"{CHECKLIST_URL}?return_to=review", {"return_to": "review"}) + assert continued.url.partition("?")[0] == reverse("organize_documents", kwargs={"jurisdiction": "illinois"}) + assert "return_to=review" in continued.url + + lead = signed_in.documents.get(role="lead") + details = { + "main_document_id": lead.pk, + "return_to": "review", + "documents": [ + {"id": lead.pk, "filing_type": lead.filing_type_code, "document_type": "public"}, + {"id": document.pk, "filing_type": document.filing_type_code, "document_type": ""}, + ], + } + organize = reverse("organize_documents", kwargs={"jurisdiction": "illinois"}) + f"?draft={signed_in.pk}" + with patch("efile.views.organize_documents._court_document_types", return_value=[{"code": "sealed"}]): + refused = client.post(organize, json.dumps(details), content_type="application/json") + assert refused.status_code == 400 + assert "Choose a confidentiality setting" in refused.json()["error"] + document.refresh_from_db() + assert not document.document_type_confirmed + + details["documents"][1].update(document_type="sealed", filing_component="supporting") + with patch("efile.views.organize_documents.draft_unavailable_message", return_value=""): + organized = client.post(organize, json.dumps(details), content_type="application/json") + assert organized.status_code == 200 + assert organized.json()["redirect_url"].partition("?")[0] == reverse( + "case_review", kwargs={"jurisdiction": "illinois"} + ) + document.refresh_from_db() + assert document.document_type_code == "sealed" + assert document.filing_component_code == "supporting" + assert document.document_type_confirmed + + # --- The plan's own home ----------------------------------------------------- diff --git a/efile_app/efile/tests/test_reorganized_start.py b/efile_app/efile/tests/test_reorganized_start.py index 1a815cda..f7f05003 100644 --- a/efile_app/efile/tests/test_reorganized_start.py +++ b/efile_app/efile/tests/test_reorganized_start.py @@ -149,6 +149,7 @@ def test_extraction_review_returns_to_review_when_edited_from_there(client, reor name="petition.pdf", # Organized already, as every document on Review has been. filing_type_code="petition", + document_type_code="public", ) response = client.post( diff --git a/efile_app/efile/views/document_checklist.py b/efile_app/efile/views/document_checklist.py index 20199cd4..3399a72c 100644 --- a/efile_app/efile/views/document_checklist.py +++ b/efile_app/efile/views/document_checklist.py @@ -104,7 +104,10 @@ def _attach_to_item(request, draft, plan, jurisdiction): if code: document.filing_type_code = code document.filing_type_name = name - document.save(update_fields=["filing_type_code", "filing_type_name", "updated_at"]) + document.document_type_confirmed = False + document.save( + update_fields=["filing_type_code", "filing_type_name", "document_type_confirmed", "updated_at"] + ) messages.success(request, f"{label} is in this filing.") # A new file is checked right here, before the filer moves on. @@ -129,6 +132,11 @@ def document_checklist(request, jurisdiction): messages.error(request, "Upload at least one document before checking your filing.") return redirect("upload_documents", jurisdiction=jurisdiction) + if documents.filter(preparation="").exists(): + return redirect( + with_return_to(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_target(request)) + ) + if request.method == "POST" and request.POST.get("action") == "upload": uploaded_files = request.FILES.getlist("documents") if not uploaded_files: @@ -187,8 +195,8 @@ def document_checklist(request, jurisdiction): # # Coming back here from Review to add a document is common now that # the review step names what is missing. Go straight back to Review, - # unless a document still needs a filing type -- organizing is where - # that is chosen, and the court will not take a filing without it. + # unless a document still needs its filing details and confidentiality + # checked on Organize. return_to = return_target(request) draft.current_step = continue_step(draft, return_to, WorkflowStepKey.ORGANIZE_DOCUMENTS) draft.save(update_fields=["current_step", "updated_at"]) diff --git a/efile_app/efile/views/document_checks.py b/efile_app/efile/views/document_checks.py index c1b88be6..85a4a1c8 100644 --- a/efile_app/efile/views/document_checks.py +++ b/efile_app/efile/views/document_checks.py @@ -61,6 +61,14 @@ def document_checks(request, jurisdiction): draft = FilingDraft.objects.select_for_update().get(pk=draft.pk) if draft.status not in ACTIVE_DRAFT_STATUSES: return JsonResponse({"error": "This filing is not available to edit."}, status=409) + inputs_token = request.POST.get("fee_inputs_token") + if inputs_token is not None and inputs_token != fee_inputs_token(draft): + # Payment will adopt the token returned below while keeping its + # page's case data. Only advance it from inputs that still match. + return JsonResponse( + {"error": "This filing changed. Reload this page before checking documents or calculating fees."}, + status=409, + ) document = draft.documents.filter(pk=request.POST.get("document_id") or None).first() if document is None: return JsonResponse( diff --git a/efile_app/efile/views/extraction_review.py b/efile_app/efile/views/extraction_review.py index 14af8d8e..28533997 100644 --- a/efile_app/efile/views/extraction_review.py +++ b/efile_app/efile/views/extraction_review.py @@ -80,9 +80,11 @@ def _set_lead_filing_type(draft, filing_type_code, filing_type_name): lead = FilingDocument.objects.filter(draft=draft, role=FilingDocument.Role.LEAD).first() if lead is None: return + if lead.filing_type_code != filing_type_code: + lead.document_type_confirmed = False lead.filing_type_code = filing_type_code lead.filing_type_name = filing_type_name - lead.save(update_fields=["filing_type_code", "filing_type_name", "updated_at"]) + lead.save(update_fields=["filing_type_code", "filing_type_name", "document_type_confirmed", "updated_at"]) def _case_identity(existing_case, docket_number): diff --git a/efile_app/efile/views/organize_documents.py b/efile_app/efile/views/organize_documents.py index 7343b8ec..f268767b 100644 --- a/efile_app/efile/views/organize_documents.py +++ b/efile_app/efile/views/organize_documents.py @@ -108,6 +108,7 @@ def _save_document_details(draft, document_details, main_document_id): document.filing_type_name = str(item.get("filing_type_name") or "")[:255] document.document_type_code = document_type document.document_type_name = str(item.get("document_type_name") or "")[:255] + document.document_type_confirmed = True document.filing_component_code = str(item.get("filing_component") or "")[:100] document.filing_component_name = str(item.get("filing_component_name") or "")[:255] courtesy_copy_email = str(item.get("courtesy_copy_email") or "").strip()[:254] diff --git a/efile_app/efile/views/payment.py b/efile_app/efile/views/payment.py index 58f6d80b..7fcbd1cf 100644 --- a/efile_app/efile/views/payment.py +++ b/efile_app/efile/views/payment.py @@ -62,6 +62,10 @@ def efile_payment(request, jurisdiction): return redirect(with_return_to(get_step_url(WorkflowStepKey.CASE_QUESTIONS, jurisdiction), return_to)) documents = list(FilingDocument.objects.filter(draft=draft).order_by("role", "sort_order", "pk")) + if any(not document.preparation for document in documents): + # Legacy uploads need preparation before they can be checked inline. + # Preview handles preparation failures and offers replacement/retry. + return redirect(with_return_to(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction), return_to)) unchecked = [document for document in documents if document.preparation_reviewed_at is None] if request.method == "POST" and unchecked: # The page keeps Continue off until these are confirmed; this covers diff --git a/efile_app/efile/workflow.py b/efile_app/efile/workflow.py index 9abdde0d..84b10283 100644 --- a/efile_app/efile/workflow.py +++ b/efile_app/efile/workflow.py @@ -13,6 +13,7 @@ from enum import StrEnum from typing import Any +from django.db.models import Q from django.urls import reverse from django.utils.functional import Promise from django.utils.translation import gettext_lazy as _ @@ -345,14 +346,14 @@ def with_return_to(url: str, return_to: str | None) -> str: def documents_need_organizing(draft: Any) -> bool: """Whether a document has not been through Organize since it changed. - New files arrive without a filing type, a replaced main document needs one - chosen, and switching between a new and an existing case clears them all. - Only the filing type is checked: Organize leaves the document type empty - when the court offers no confidentiality choices, so an empty document - type can be a finished answer, and checking it would loop back here. + Checklist attachments can have a filing type before confidentiality is + answered. A blank document type is valid only after Organize checked that + the court offers no choices, so it must carry that confirmation. """ - return draft.documents.filter(filing_type_code="").exists() + return draft.documents.filter( + Q(filing_type_code="") | Q(document_type_code="", document_type_confirmed=False) + ).exists() def continue_step(draft: Any, return_to: str, default_step: WorkflowStepKey | str) -> WorkflowStepKey: diff --git a/efile_app/js-tests/document-checks.test.js b/efile_app/js-tests/document-checks.test.js index b7ba7d45..3bcf04d2 100644 --- a/efile_app/js-tests/document-checks.test.js +++ b/efile_app/js-tests/document-checks.test.js @@ -4,7 +4,7 @@ const fs = require("node:fs"); const vm = require("node:vm"); const path = require("node:path"); -function harness(fetch, action = "confirm") { +function harness(fetch, action = "confirm", feeToken = null) { const events = []; const status = { textContent: "" @@ -41,6 +41,9 @@ function harness(fetch, action = "confirm") { let click; const context = vm.createContext({ document: { + getElementById: () => feeToken === null ? null : { + textContent: JSON.stringify(feeToken) + }, addEventListener(event, callback) { if (event === "click") click = callback; } @@ -119,6 +122,26 @@ test("removing sends the remove action", async () => { assert.equal(page.events[0].detail.action, "remove"); }); +test("payment checks send the token for the case data the page still holds", async () => { + for (const action of ["confirm", "remove"]) { + let token; + const page = harness(async (_url, options) => { + token = options.body.get("fee_inputs_token"); + return { + ok: false, + json: async () => ({ + error: "This filing changed. Reload this page." + }) + }; + }, action, "page-inputs-token"); + await page.click(); + assert.equal(token, "page-inputs-token"); + assert.equal(page.check.removed, false); + assert.equal(page.events.length, 0); + assert.match(page.status.textContent, /Reload this page/); + } +}); + test("a refused check stays on the page with its reason and can be retried", async () => { const page = harness(async () => ({ ok: false,