diff --git a/docs/screenshots/reorganized-flow/12-payment.png b/docs/screenshots/reorganized-flow/12-payment.png new file mode 100644 index 0000000..56731b1 Binary files /dev/null and b/docs/screenshots/reorganized-flow/12-payment.png differ diff --git a/docs/screenshots/reorganized-flow/13-review.png b/docs/screenshots/reorganized-flow/13-review.png new file mode 100644 index 0000000..9c6e4e1 Binary files /dev/null and b/docs/screenshots/reorganized-flow/13-review.png differ diff --git a/docs/screenshots/reorganized-flow/14-confirmation.png b/docs/screenshots/reorganized-flow/14-confirmation.png new file mode 100644 index 0000000..2088f3e Binary files /dev/null and b/docs/screenshots/reorganized-flow/14-confirmation.png differ diff --git a/efile_app/efile/api/auth_views.py b/efile_app/efile/api/auth_views.py index fef01fa..0bbb8cb 100644 --- a/efile_app/efile/api/auth_views.py +++ b/efile_app/efile/api/auth_views.py @@ -262,6 +262,49 @@ def payment_accounts(request): except Exception as e: return AuthAPIViews.error_response(f"Error: {str(e)}") + @staticmethod + @require_http_methods(["GET"]) + def payment_account_types(request): + """List the court's payment account types (e.g. "CC", "WV") with names. + + A payment account only carries its type *code*, not a human-readable + name, so the frontend needs this list to label account types other + than the ones it special-cases (card, waiver). + """ + try: + jurisdiction = get_jurisdiction_from_request(request) + tyler_token = get_tyler_token(request, jurisdiction) + api_key = getattr(settings, "SUFFOLK_EFILE_API_KEY", None) + + headers = { + "Content-Type": "application/json", + "User-Agent": f"{jurisdiction.title()}-eFile-Client/1.0", + "X-API-Key": api_key if api_key else "", + } + if tyler_token: + headers[f"tyler-token-{jurisdiction}"] = tyler_token + + url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/payments/types" + logger.debug("GET %s header keys=%s", url, list(headers.keys())) + api_response = requests.get(url, headers=headers, timeout=10) + + if api_response.status_code == 200: + return AuthAPIViews.success_response(api_response.json()) + elif api_response.status_code == 401: + return AuthAPIViews.success_response([]) + else: + return AuthAPIViews.error_response( + f"Payment account types API returned status {api_response.status_code}: {api_response.text[:200]}", + api_response.status_code, + ) + + except Timeout: + return AuthAPIViews.error_response("Payment account types API request timed out", 408) + except RequestException as e: + return AuthAPIViews.error_response(f"Could not connect to payment account types API: {str(e)}", 503) + except Exception as e: + return AuthAPIViews.error_response(f"Error: {str(e)}") + # Individual view functions for URL mapping user_login = AuthAPIViews.user_login @@ -269,4 +312,5 @@ def payment_accounts(request): user_profile = AuthAPIViews.user_profile external_profile = AuthAPIViews.external_profile payment_accounts = AuthAPIViews.payment_accounts +payment_account_types = AuthAPIViews.payment_account_types tyler_token = AuthAPIViews.tyler_token diff --git a/efile_app/efile/api/urls.py b/efile_app/efile/api/urls.py index 1fbf375..d7157f1 100644 --- a/efile_app/efile/api/urls.py +++ b/efile_app/efile/api/urls.py @@ -6,6 +6,7 @@ from .auth_views import ( external_profile, + payment_account_types, payment_accounts, tyler_token, user_login, @@ -62,6 +63,7 @@ path("auth/tyler-token/", tyler_token, name="tyler_token"), # Payment API endpoints path("payment-accounts/", payment_accounts, name="payment_accounts"), + path("payment-account-types/", payment_account_types, name="payment_account_types"), path("payment-fees/", payment_fees, name="payment_fees"), # Filing API endpoints path("filings/", get_filings, name="get_filings"), diff --git a/efile_app/efile/migrations/0007_complete_workflow_migration.py b/efile_app/efile/migrations/0007_complete_workflow_migration.py new file mode 100644 index 0000000..c8372c7 --- /dev/null +++ b/efile_app/efile/migrations/0007_complete_workflow_migration.py @@ -0,0 +1,54 @@ +from django.db import migrations, models + + +STEP_MAPPINGS = { + "options": "filing_path", + "upload_first": "upload_documents", + "case_information": "extraction_review", + "documents": "organize_documents", +} + + +def migrate_saved_workflow_positions(apps, schema_editor): + FilingDraft = apps.get_model("efile", "FilingDraft") + for old_step, new_step in STEP_MAPPINGS.items(): + FilingDraft.objects.filter(current_step=old_step).update(current_step=new_step) + FilingDraft.objects.exclude(workflow_version=2).update(workflow_version=2) + + +class Migration(migrations.Migration): + dependencies = [("efile", "0006_filingparty_party_type_name")] + + operations = [ + migrations.RunPython(migrate_saved_workflow_positions, migrations.RunPython.noop), + migrations.AlterField( + model_name="filingdraft", + name="current_step", + field=models.CharField( + choices=[ + ("options", "Options"), + ("filing_path", "Filing"), + ("upload_documents", "Upload documents"), + ("extraction_review", "Confirm filing"), + ("case_lookup", "Find your case"), + ("case_confirmation", "Confirm your case"), + ("document_checklist", "Check documents"), + ("organize_documents", "Organize documents"), + ("your_information", "Your information"), + ("parties", "People in this filing"), + ("party_details", "Person details"), + ("case_questions", "Case questions"), + ("payment", "Fees"), + ("review", "Review"), + ("confirmation", "Confirmation"), + ], + default="options", + max_length=64, + ), + ), + migrations.AlterField( + model_name="filingdraft", + name="workflow_version", + field=models.PositiveSmallIntegerField(default=2), + ), + ] diff --git a/efile_app/efile/migrations/0008_merge_document_prep_and_people_migrations.py b/efile_app/efile/migrations/0008_merge_document_prep_and_people_migrations.py new file mode 100644 index 0000000..28525dc --- /dev/null +++ b/efile_app/efile/migrations/0008_merge_document_prep_and_people_migrations.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.5 on 2026-08-11 00:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('efile', '0006_filingdocument_requested_optional_services'), + ('efile', '0007_complete_workflow_migration'), + ] + + operations = [ + ] diff --git a/efile_app/efile/migrations/0009_filingdraft_payment_type_and_quoted_fees.py b/efile_app/efile/migrations/0009_filingdraft_payment_type_and_quoted_fees.py new file mode 100644 index 0000000..75c3b67 --- /dev/null +++ b/efile_app/efile/migrations/0009_filingdraft_payment_type_and_quoted_fees.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("efile", "0008_merge_document_prep_and_people_migrations")] + + operations = [ + migrations.AddField( + model_name="filingdraft", + name="selected_payment_account_type", + field=models.CharField(max_length=50, blank=True), + ), + migrations.AddField( + model_name="filingdraft", + name="quoted_fee_total", + field=models.CharField(max_length=50, blank=True), + ), + migrations.AddField( + model_name="filingdraft", + name="quoted_fee_breakdown", + field=models.JSONField(default=list, blank=True), + ), + ] diff --git a/efile_app/efile/migrations/0010_merge_amount_in_controversy_and_payment_migrations.py b/efile_app/efile/migrations/0010_merge_amount_in_controversy_and_payment_migrations.py new file mode 100644 index 0000000..46522d4 --- /dev/null +++ b/efile_app/efile/migrations/0010_merge_amount_in_controversy_and_payment_migrations.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.5 on 2026-08-11 12:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('efile', '0007_amount_in_controversy'), + ('efile', '0009_filingdraft_payment_type_and_quoted_fees'), + ] + + operations = [ + ] diff --git a/efile_app/efile/models.py b/efile_app/efile/models.py index 0cbc9e8..7ae2422 100644 --- a/efile_app/efile/models.py +++ b/efile_app/efile/models.py @@ -54,7 +54,7 @@ class Status(models.TextChoices): choices=get_workflow_step_choices(), default=WorkflowStepKey.OPTIONS, ) - workflow_version = models.PositiveSmallIntegerField(default=1) + workflow_version = models.PositiveSmallIntegerField(default=2) existing_case = models.CharField( max_length=20, @@ -80,6 +80,13 @@ class Status(models.TextChoices): selected_payment_account_id = models.CharField(max_length=255, blank=True) selected_payment_account_name = models.CharField(max_length=255, blank=True) + # Tyler's paymentAccountTypeCode for the selected account (e.g. "WV" for a fee + # waiver). Drives whether Review shows a fee total or waiver messaging. + selected_payment_account_type = models.CharField(max_length=50, blank=True) + # The fee quote shown on the Payment step, carried forward so Review can + # display the same numbers instead of telling the filer to go look again. + quoted_fee_total = models.CharField(max_length=50, blank=True) + quoted_fee_breakdown = models.JSONField(default=list, blank=True) name_change_reason = models.TextField(blank=True) diff --git a/efile_app/efile/services/current_drafts.py b/efile_app/efile/services/current_drafts.py index a42120a..e2235d4 100644 --- a/efile_app/efile/services/current_drafts.py +++ b/efile_app/efile/services/current_drafts.py @@ -83,7 +83,7 @@ def create_current_draft( jurisdiction: str, *, current_step: WorkflowStepKey | str = WorkflowStepKey.OPTIONS, - workflow_version: int = 1, + workflow_version: int = 2, ) -> FilingDraft: draft = create_draft( user=_authenticated_user(request), @@ -109,7 +109,7 @@ def ensure_current_draft( request, jurisdiction, current_step=current_step or WorkflowStepKey.OPTIONS, - workflow_version=workflow_version or 1, + workflow_version=workflow_version or 2, ) if current_step is not None: set_current_step(draft, current_step) diff --git a/efile_app/efile/services/drafts.py b/efile_app/efile/services/drafts.py index 0840342..0382cb8 100644 --- a/efile_app/efile/services/drafts.py +++ b/efile_app/efile/services/drafts.py @@ -54,7 +54,7 @@ def create_draft( user, jurisdiction: str, current_step: WorkflowStepKey | str = WorkflowStepKey.OPTIONS, - workflow_version: int = 1, + workflow_version: int = 2, ) -> FilingDraft: """Create a durable draft owned by an authenticated user.""" diff --git a/efile_app/efile/static/css/common.css b/efile_app/efile/static/css/common.css index 3354d54..9f94544 100644 --- a/efile_app/efile/static/css/common.css +++ b/efile_app/efile/static/css/common.css @@ -1,10 +1,51 @@ +/* Single source of truth for color across every screen. + Anything user-facing should reference a token from this block rather than + its own hex value, so the app reads as one product instead of a set of + pages that each invented a slightly different blue. */ :root { + /* Brand blues. --better-blue is the one action color: primary buttons, + links, and accents all use it. */ --suffolk-blue: #1e3a5f; --better-blue: #2c5aa0; - --primary-btn-color: #007bff; + --brand-blue-hover: #24497f; + --brand-blue-active: #1e3a5f; + + /* Older pages theme their buttons through this name. Pointing it at the + brand blue keeps them in step with the rest of the app. */ + --primary-btn-color: var(--better-blue); + + /* Text tiers. Every value below clears WCAG AA (4.5:1) on white and on + both tinted surfaces defined here. */ + --text-heading: #1e3a5f; + --text-body: #48576c; + --text-muted: #616c82; + + /* Surfaces */ + --surface-subtle: #f6f9fd; + --surface-accent: #eef5ff; + --light-blue-background: #f8f9fa; + + /* Borders. --border-strong is for edges that outline a control; the + lighter two are decorative dividers. */ + --border-subtle: #e3e8ef; + --border-default: #dce3ec; + --border-accent: #cbdcf1; + --border-strong: #7d8ca3; + + /* Status */ --success: #28a745; + --success-text: #16703a; + --success-surface: #e5f6eb; + --success-border: #cde8d5; --failure: #dc3545; - --light-blue-background: #f8f9fa; + --danger-icon: #b23b3b; + --warning-surface: #fffaf0; + --warning-border: #ead8ad; + + /* Links follow the brand blue rather than Bootstrap's default indigo. */ + --bs-link-color: var(--better-blue); + --bs-link-hover-color: var(--brand-blue-hover); + --bs-link-color-rgb: 44, 90, 160; } body { @@ -26,4 +67,27 @@ h1 { h2 { color: var(--better-blue); font-weight: 500; +} + +/* One primary action color on every screen. Bootstrap themes buttons through + its own custom properties, so setting those keeps the hover, active, and + disabled states in step instead of repainting only the resting state. */ +.btn-primary { + --bs-btn-bg: var(--better-blue); + --bs-btn-border-color: var(--better-blue); + --bs-btn-hover-bg: var(--brand-blue-hover); + --bs-btn-hover-border-color: var(--brand-blue-hover); + --bs-btn-active-bg: var(--brand-blue-active); + --bs-btn-active-border-color: var(--brand-blue-active); + --bs-btn-disabled-bg: var(--better-blue); + --bs-btn-disabled-border-color: var(--better-blue); +} + +.btn-outline-primary { + --bs-btn-color: var(--better-blue); + --bs-btn-border-color: var(--better-blue); + --bs-btn-hover-bg: var(--better-blue); + --bs-btn-hover-border-color: var(--better-blue); + --bs-btn-active-bg: var(--brand-blue-active); + --bs-btn-active-border-color: var(--brand-blue-active); } \ No newline at end of file diff --git a/efile_app/efile/static/css/components/search-dropdown.css b/efile_app/efile/static/css/components/search-dropdown.css index b617459..d093612 100644 --- a/efile_app/efile/static/css/components/search-dropdown.css +++ b/efile_app/efile/static/css/components/search-dropdown.css @@ -13,7 +13,7 @@ .search-dropdown-input:focus { border-color: #86b7fe; outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + box-shadow: 0 0 0 0.25rem rgba(44, 90, 160, 0.25); } .search-dropdown-input:disabled { @@ -93,7 +93,7 @@ .search-dropdown-selected:focus-within { border-color: #86b7fe; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + box-shadow: 0 0 0 0.25rem rgba(44, 90, 160, 0.25); } .search-dropdown-selected .selected-text { diff --git a/efile_app/efile/static/css/confirmation.css b/efile_app/efile/static/css/confirmation.css index 08c76d9..0e1f627 100644 --- a/efile_app/efile/static/css/confirmation.css +++ b/efile_app/efile/static/css/confirmation.css @@ -1,37 +1,84 @@ -.confirmation-container { - max-width: 600px; - margin: 0 auto; - padding: 2rem; +.confirmation-workflow-card { text-align: center; } -.success-icon { - color: var(--success); - font-size: 4rem; - margin-bottom: 1rem; +.confirmation-workflow-card .workflow-lede { + margin-left: auto; + margin-right: auto; } -.success-message { - color: #155724; - font-size: 1.25rem; - margin-bottom: 2rem; +.confirmation-reference { + background: var(--surface-accent); + border: 1px solid var(--border-accent); + border-radius: 12px; + margin: 0 auto 1.25rem; + padding: 1rem; } -.btn-primary.custom-confirm { - background: var(--better-blue); - border-color: var(--better-blue); +.confirmation-reference span, +.confirmation-reference strong { + display: block; } -.btn-primary.custom-confirm:hover { - background: #1e3d6f; - border-color: #1e3d6f; +.confirmation-reference span { + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; } -@media (max-width: 576px) { - .confirmation-container { - padding: 1rem; - } +.confirmation-reference strong { + color: var(--better-blue); + font-size: 1.35rem; + margin-top: 0.25rem; +} + +.confirmation-summary { + border: 1px solid var(--border-default); + border-radius: 12px; + margin: 0; + text-align: left; +} + +.confirmation-summary>div { + display: flex; + justify-content: space-between; + padding: 0.75rem 1rem; +} + +.confirmation-summary>div+div { + border-top: 1px solid var(--border-subtle); +} + +.confirmation-summary dt { + color: var(--text-muted); +} + +.confirmation-summary dd { + color: var(--text-heading); + font-weight: 700; + margin: 0; +} + +.confirmation-next { + text-align: left; +} + +.confirmation-actions { + display: flex; + gap: 0.8rem; + justify-content: center; + margin-top: 1.5rem; +} + +.success-icon { + color: var(--success); + font-size: 4rem; + margin-bottom: 1rem; +} +@media (max-width: 700px) { .success-icon { font-size: 3rem; } diff --git a/efile_app/efile/static/css/expert_form.css b/efile_app/efile/static/css/expert_form.css index 7f73ee6..a0b5e14 100644 --- a/efile_app/efile/static/css/expert_form.css +++ b/efile_app/efile/static/css/expert_form.css @@ -44,7 +44,7 @@ .form-select:focus, .form-control:focus { border-color: var(--primary-btn-color); - box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25); } .btn-outline-secondary { diff --git a/efile_app/efile/static/css/login.css b/efile_app/efile/static/css/login.css index 14068ea..bbe531b 100644 --- a/efile_app/efile/static/css/login.css +++ b/efile_app/efile/static/css/login.css @@ -68,8 +68,8 @@ } .form-control:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25); + border-color: var(--better-blue); + box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25); } .btn-sign-in, @@ -103,7 +103,7 @@ .show-password:hover, .forgot-password:hover { - color: #3b82f6; + color: var(--better-blue); text-decoration: underline; } diff --git a/efile_app/efile/static/css/register.css b/efile_app/efile/static/css/register.css index e769047..bede412 100644 --- a/efile_app/efile/static/css/register.css +++ b/efile_app/efile/static/css/register.css @@ -115,8 +115,8 @@ .form-control:focus, .form-select:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25); + border-color: var(--better-blue); + box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25); } .form-control::placeholder { @@ -175,7 +175,7 @@ } .password-toggle:hover { - color: #3b82f6; + color: var(--better-blue); } .phone-example { diff --git a/efile_app/efile/static/css/reorganized-flow.css b/efile_app/efile/static/css/reorganized-flow.css index 61e5d9f..c7b82c3 100644 --- a/efile_app/efile/static/css/reorganized-flow.css +++ b/efile_app/efile/static/css/reorganized-flow.css @@ -20,7 +20,7 @@ } .workflow-progress__item { - color: #687386; + color: var(--text-muted); flex: 1; font-size: 0.74rem; font-weight: 650; @@ -29,7 +29,7 @@ } .workflow-progress__item::before { - background: #d8dee8; + background: var(--border-subtle); content: ""; height: 2px; left: -50%; @@ -45,7 +45,7 @@ .workflow-progress__marker { align-items: center; background: #fff; - border: 2px solid #bcc5d2; + border: 2px solid var(--border-strong); border-radius: 50%; display: inline-flex; height: 32px; @@ -62,28 +62,28 @@ .workflow-progress__item--complete::before, .workflow-progress__item--current::before { - background: #2c5aa0; + background: var(--better-blue); } .workflow-progress__item--complete .workflow-progress__marker { - background: #2c5aa0; - border-color: #2c5aa0; + background: var(--better-blue); + border-color: var(--better-blue); color: #fff; } .workflow-progress__item--current { - color: #163f75; + color: var(--better-blue); } .workflow-progress__item--current .workflow-progress__marker { - border-color: #2c5aa0; + border-color: var(--better-blue); box-shadow: 0 0 0 4px rgb(44 90 160 / 14%); - color: #163f75; + color: var(--better-blue); } .workflow-card { background: #fff; - border: 1px solid #dce2ea; + border: 1px solid var(--border-default); border-radius: 16px; box-shadow: 0 12px 36px rgb(20 46 82 / 8%); margin: 0 auto; @@ -96,7 +96,7 @@ } .workflow-eyebrow { - color: #2c5aa0; + color: var(--better-blue); font-size: 0.82rem; font-weight: 750; letter-spacing: 0.08em; @@ -105,14 +105,13 @@ } .workflow-card h1 { - color: #172b46; font-size: clamp(1.8rem, 4vw, 2.5rem); letter-spacing: -0.025em; margin-bottom: 0.8rem; } .workflow-lede { - color: #536176; + color: var(--text-body); font-size: 1.05rem; line-height: 1.6; margin-bottom: 2rem; @@ -126,7 +125,7 @@ .choice-card { align-items: center; - border: 2px solid #d9e0e9; + border: 2px solid var(--border-default); border-radius: 12px; cursor: pointer; display: grid; @@ -137,8 +136,8 @@ } .choice-card:has(input:checked) { - background: #f0f6ff; - border-color: #2c5aa0; + background: var(--surface-accent); + border-color: var(--better-blue); box-shadow: 0 0 0 3px rgb(44 90 160 / 10%); } @@ -149,9 +148,9 @@ .choice-card__icon { align-items: center; - background: #eaf1fb; + background: var(--surface-accent); border-radius: 10px; - color: #2c5aa0; + color: var(--better-blue); display: flex; font-size: 1.15rem; height: 44px; @@ -165,17 +164,17 @@ } .choice-card strong { - color: #1d2f49; + color: var(--text-heading); margin-bottom: 0.2rem; } .choice-card small { - color: #647187; + color: var(--text-muted); } .workflow-actions { align-items: center; - border-top: 1px solid #e3e7ed; + border-top: 1px solid var(--border-subtle); display: flex; justify-content: space-between; margin-top: 2rem; @@ -189,7 +188,7 @@ } .requirements-strip { - background: #f5f7fa; + background: var(--surface-subtle); border-radius: 10px; display: flex; flex-wrap: wrap; @@ -199,19 +198,19 @@ } .requirements-strip span { - color: #48576c; + color: var(--text-body); font-size: 0.9rem; } .requirements-strip i { - color: #2c5aa0; + color: var(--better-blue); margin-right: 0.35rem; } .drop-zone { align-items: center; - background: #fbfcfe; - border: 2px dashed #9bacc2; + background: var(--surface-subtle); + border: 2px dashed var(--border-strong); border-radius: 14px; cursor: pointer; display: flex; @@ -225,8 +224,13 @@ .drop-zone--active, .drop-zone:hover { - background: #f0f6ff; - border-color: #2c5aa0; + background: var(--surface-accent); + border-color: var(--better-blue); +} + +.drop-zone:focus-within { + outline: 3px solid var(--better-blue); + outline-offset: 2px; } .drop-zone input { @@ -237,20 +241,20 @@ } .drop-zone__icon { - color: #2c5aa0; + color: var(--better-blue); font-size: 2.2rem; } .drop-zone span:last-child { - color: #687386; + color: var(--text-muted); font-size: 0.9rem; } .upload-state { align-items: center; - background: #eef5ff; + background: var(--surface-accent); border-radius: 10px; - color: #234d84; + color: var(--better-blue); display: grid; gap: 0.15rem 0.8rem; grid-template-columns: auto 1fr; @@ -259,7 +263,7 @@ } .upload-state span { - color: #526a88; + color: var(--text-body); font-size: 0.88rem; grid-column: 2; } @@ -276,7 +280,7 @@ } .document-list h2 { - color: #213957; + color: var(--text-heading); font-size: 1.15rem; font-weight: 700; margin: 0; @@ -284,7 +288,7 @@ .document-row { align-items: center; - border: 1px solid #dfe5ed; + border: 1px solid var(--border-default); border-radius: 10px; display: grid; gap: 0.8rem; @@ -294,7 +298,7 @@ } .document-row__icon { - color: #c53434; + color: var(--danger-icon); font-size: 1.4rem; } @@ -304,7 +308,7 @@ } .document-row__details small { - color: #687386; + color: var(--text-muted); } .status-pill { @@ -315,14 +319,19 @@ } .status-pill--ready { - background: #e5f6eb; - color: #16703a; + background: var(--success-surface); + color: var(--success-text); +} + +.status-pill--lead { + background: var(--surface-accent); + color: var(--better-blue); } .document-list__empty { - border: 1px solid #e1e6ed; + border: 1px solid var(--border-default); border-radius: 10px; - color: #788497; + color: var(--text-muted); padding: 1.5rem; text-align: center; } @@ -342,41 +351,41 @@ } .form-field>span { - color: #263c58; + color: var(--text-heading); display: block; font-weight: 700; margin-bottom: 0.35rem; } .form-field em { - color: #778397; + color: var(--text-muted); font-size: 0.8rem; font-style: normal; font-weight: 500; } .form-field small { - color: #758196; + color: var(--text-muted); display: block; margin-top: 0.3rem; } .review-field>span { - color: #263c58; + color: var(--text-heading); display: block; font-weight: 700; margin-bottom: 0.35rem; } .review-field em { - color: #778397; + color: var(--text-muted); font-size: 0.8rem; font-style: normal; font-weight: 500; } .review-field__hint { - color: #758196; + color: var(--text-muted); display: block; margin-top: 0.3rem; min-height: 1.1em; @@ -384,8 +393,8 @@ .review-field__display { align-items: center; - background: #f3faf5; - border: 1px solid #cde8d5; + background: var(--success-surface); + border: 1px solid var(--success-border); border-radius: 8px; display: flex; gap: 0.6rem; @@ -395,7 +404,7 @@ .review-field__found { align-items: center; - color: #16703a; + color: var(--success-text); display: flex; gap: 0.5rem; min-width: 0; @@ -406,7 +415,7 @@ } .review-field__found strong { - color: #1d2a3d; + color: var(--text-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -420,8 +429,8 @@ } .path-confirmation { - background: #f7f9fc; - border: 1px solid #dfe5ed; + background: var(--surface-subtle); + border: 1px solid var(--border-default); border-radius: 12px; display: grid; gap: 0.7rem; @@ -430,7 +439,7 @@ } .path-confirmation legend { - color: #243a56; + color: var(--text-heading); float: none; font-size: 1rem; font-weight: 750; @@ -441,7 +450,7 @@ .path-confirmation>label { align-items: flex-start; background: #fff; - border: 1px solid #d8e0e9; + border: 1px solid var(--border-default); border-radius: 8px; display: flex; gap: 0.75rem; @@ -449,22 +458,22 @@ } .path-confirmation small { - color: #6d798d; + color: var(--text-muted); display: block; } .help-toggle { background: none; border: 0; - color: #2c5aa0; + color: var(--better-blue); font-weight: 650; justify-self: start; padding: 0.35rem 0; } .help-panel { - background: #fff8df; - border-left: 4px solid #e0ac2e; + background: var(--warning-surface); + border-left: 4px solid var(--warning-border); padding: 1rem; } @@ -479,9 +488,9 @@ .lookup-state { align-items: center; - background: #eef5ff; + background: var(--surface-accent); border-radius: 10px; - color: #234d84; + color: var(--better-blue); display: flex; gap: 0.8rem; margin-top: 1rem; @@ -494,7 +503,7 @@ } .lookup-state small { - color: #61748f; + color: var(--text-body); } .found-heading { @@ -505,9 +514,9 @@ .found-heading__icon { align-items: center; - background: #e2f5e9; + background: var(--success-surface); border-radius: 50%; - color: #18733d; + color: var(--success-text); display: flex; flex: 0 0 auto; height: 46px; @@ -517,7 +526,7 @@ } .case-summary { - border: 1px solid #dce3ec; + border: 1px solid var(--border-default); border-radius: 12px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -526,22 +535,22 @@ } .case-summary>div { - border-top: 1px solid #e3e8ef; + border-top: 1px solid var(--border-subtle); padding: 1rem 1.2rem; } .case-summary>div:nth-child(even):not(.case-summary__primary) { - border-left: 1px solid #e3e8ef; + border-left: 1px solid var(--border-subtle); } .case-summary__primary { - background: #f6f9fd; + background: var(--surface-subtle); border-top: 0 !important; grid-column: 1 / -1; } .case-summary dt { - color: #6b788c; + color: var(--text-muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; @@ -550,7 +559,7 @@ } .case-summary dd { - color: #1c314d; + color: var(--text-heading); font-size: 1rem; font-weight: 650; margin: 0; @@ -565,21 +574,21 @@ } .confirmation-question strong { - color: #263c58; + color: var(--text-heading); font-size: 1.05rem; } .confirmation-question p { - color: #69768a; + color: var(--text-muted); margin: 0.25rem 0 0; } .checklist-guidance { align-items: flex-start; - background: #eef5ff; - border-left: 4px solid #3974ba; + background: var(--surface-accent); + border-left: 4px solid var(--better-blue); border-radius: 8px; - color: #294565; + color: var(--text-heading); display: flex; gap: 0.8rem; margin: 1.5rem 0; @@ -587,7 +596,7 @@ } .checklist-guidance i { - color: #3974ba; + color: var(--better-blue); margin-top: 0.2rem; } @@ -596,7 +605,7 @@ } .checklist-files { - border: 1px solid #dce3ec; + border: 1px solid var(--border-default); border-radius: 12px; overflow: hidden; } @@ -610,14 +619,14 @@ } .checklist-file+.checklist-file { - border-top: 1px solid #e4e9f0; + border-top: 1px solid var(--border-subtle); } .checklist-file__check { align-items: center; - background: #e2f5e9; + background: var(--success-surface); border-radius: 50%; - color: #18733d; + color: var(--success-text); display: flex; font-size: 0.7rem; height: 24px; @@ -626,28 +635,28 @@ } .checklist-file__icon { - color: #b23b3b; + color: var(--danger-icon); font-size: 1.35rem; } .checklist-file small, .checklist-confirmation small { - color: #6e7b8f; + color: var(--text-muted); display: block; } .add-missing-toggle { background: none; border: 0; - color: #2c5aa0; + color: var(--better-blue); font-weight: 700; margin: 1rem 0; padding: 0.4rem 0; } .missing-document-state { - background: #f7f9fc; - border: 1px dashed #bac7d6; + background: var(--surface-subtle); + border: 1px dashed var(--border-strong); border-radius: 10px; margin-bottom: 1.2rem; padding: 1.2rem; @@ -655,8 +664,8 @@ .checklist-confirmation { align-items: flex-start; - background: #fffaf0; - border: 1px solid #ead8ad; + background: var(--warning-surface); + border: 1px solid var(--warning-border); border-radius: 10px; display: flex; gap: 0.8rem; @@ -671,15 +680,15 @@ } .main-document-choice { - background: #f6f9fd; - border: 1px solid #dce3ec; + background: var(--surface-subtle); + border: 1px solid var(--border-default); border-radius: 12px; margin-top: 1.5rem; padding: 1rem; } .main-document-choice legend { - color: #263c58; + color: var(--text-heading); float: none; font-size: 1.05rem; font-weight: 750; @@ -688,24 +697,24 @@ } .main-document-choice>p { - color: #68768a; + color: var(--text-muted); margin: 0.25rem 0 0.8rem; } .main-document-choice small { - color: #6d798d; + color: var(--text-muted); display: block; } .organize-card { - border: 1px solid #dce3ec; + border: 1px solid var(--border-default); border-radius: 12px; overflow: hidden; } .organize-card__header { align-items: center; - background: #f6f9fd; + background: var(--surface-subtle); display: grid; gap: 0.8rem; grid-template-columns: auto 1fr auto; @@ -713,7 +722,7 @@ } .organize-card__icon { - color: #b23b3b; + color: var(--danger-icon); font-size: 1.4rem; } @@ -723,7 +732,7 @@ } .organize-card__position { - color: #66758a; + color: var(--text-muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.03em; @@ -743,12 +752,12 @@ } .certified-copy-details { - border-top: 1px solid #e3e8ef; + border-top: 1px solid var(--border-subtle); padding: 0.8rem 1rem 1rem; } .certified-copy-details summary { - color: #38567a; + color: var(--text-body); cursor: pointer; font-weight: 700; } @@ -760,11 +769,11 @@ } .optional-services-list small { - color: #758196; + color: var(--text-muted); } .optional-service-description { - color: #758196; + color: var(--text-muted); margin: -0.35rem 0 0.35rem 1.6rem; } @@ -777,7 +786,7 @@ } .compact-choice-field legend { - color: #263c58; + color: var(--text-heading); float: none; font-size: 1rem; font-weight: 700; @@ -792,8 +801,8 @@ .compact-choice-list label { align-items: center; - background: #f7f9fc; - border: 1px solid #dce3ec; + background: var(--surface-subtle); + border: 1px solid var(--border-default); border-radius: 8px; display: flex; gap: 0.65rem; @@ -805,20 +814,20 @@ } .people-section { - border-top: 1px solid #e3e8ef; + border-top: 1px solid var(--border-subtle); margin-top: 1.5rem; padding-top: 1.25rem; } .people-section h2, .party-roster h2 { - color: #263c58; + color: var(--text-heading); font-size: 1.05rem; margin-bottom: 1rem; } .people-section h2 em { - color: #778397; + color: var(--text-muted); font-size: 0.8rem; font-style: normal; font-weight: 500; @@ -840,8 +849,8 @@ .your-role-card { align-items: end; - background: #f6f9fd; - border: 1px solid #dce3ec; + background: var(--surface-subtle); + border: 1px solid var(--border-default); border-radius: 12px; display: grid; gap: 1rem; @@ -859,7 +868,7 @@ .your-role-card small, .party-row small { - color: #6d798d; + color: var(--text-muted); display: block; } @@ -878,9 +887,9 @@ .party-avatar { align-items: center; - background: #dfeaff; + background: var(--surface-accent); border-radius: 50%; - color: #285ea8; + color: var(--better-blue); display: flex; flex: 0 0 auto; height: 44px; @@ -908,7 +917,7 @@ } .party-row { - border: 1px solid #dce3ec; + border: 1px solid var(--border-default); border-radius: 10px; display: grid; grid-template-columns: auto 1fr auto auto auto; @@ -924,7 +933,7 @@ } .party-kind legend { - color: #263c58; + color: var(--text-heading); font-size: 0.9rem; font-weight: 700; grid-column: 1 / -1; @@ -933,8 +942,8 @@ } .party-kind label { - background: #f6f9fd; - border: 1px solid #dce3ec; + background: var(--surface-subtle); + border: 1px solid var(--border-default); border-radius: 8px; padding: 0.7rem 1rem; } @@ -946,13 +955,13 @@ } .question-card { - border: 1px solid #dce3ec; + border: 1px solid var(--border-default); border-radius: 12px; padding: 1.1rem; } .question-card legend { - color: #263c58; + color: var(--text-heading); float: none; font-size: 1rem; font-weight: 700; @@ -968,8 +977,8 @@ .question-options label { align-items: center; - background: #f6f9fd; - border: 1px solid #dce3ec; + background: var(--surface-subtle); + border: 1px solid var(--border-default); border-radius: 8px; display: flex; gap: 0.7rem; @@ -977,7 +986,7 @@ } .question-card__hint { - color: #758196; + color: var(--text-muted); font-size: 0.88rem; margin: -0.3rem 0 0.8rem; } diff --git a/efile_app/efile/static/css/review.css b/efile_app/efile/static/css/review.css index c36c520..842b280 100644 --- a/efile_app/efile/static/css/review.css +++ b/efile_app/efile/static/css/review.css @@ -1,338 +1,193 @@ -.review-container { - max-width: 600px; - margin: 0 auto; - padding: 1rem; -} - -.review-section { - background: var(--light-blue-background); - border-radius: 8px; - padding: 1.5rem; - margin-bottom: 1.5rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.review-section h3 { - color: var(--better-blue); - margin-bottom: 1rem; - font-size: 1.3rem; - font-weight: 600; -} - -.review-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 0; - border-bottom: 1px solid #e9ecef; -} - -.review-item input { - max-width: 400px; -} - -.review-item:last-child { - border-bottom: none; +.loading-spinner { + display: none; + text-align: center; + padding: 2rem; } -.review-label { - font-weight: 500; - color: #495057; - flex: 0 0 auto; +.payment-choice-field { + border: 0; + margin: 0; + padding: 0; } -.review-value { - flex: 1; - text-align: right; - color: #212529; - margin-left: 1rem; - word-break: break-word; +.payment-choice-field>legend, +.fee-summary h2 { + color: var(--text-heading); + float: none; + font-size: 1.05rem; + font-weight: 750; + margin-bottom: 0.8rem; + width: auto; } -.review-text { - overflow: hidden; - text-overflow: ellipsis; +.fee-summary { + background: var(--surface-accent); + border: 1px solid var(--border-accent); + border-radius: 12px; + margin-top: 1.25rem; + padding: 1.1rem; } -.review-item>.d-flex.align-items-center.w-100 { - justify-content: flex-end; - gap: 0.75rem; +.fee-summary ul { + margin: 0.75rem 0 0; } -.review-item input.form-control { - flex: 1 1 auto; - min-width: 0; +.review-workflow-card { + max-width: 980px; } -.review-item .edit-btn { - margin-left: 0.75rem; - flex: 0 0 auto; -} - -.edit-btn { - background: #45637d; - color: white; - border: none; - border-radius: 20px; - padding: 0.4rem 1rem; - font-size: 0.85rem; - text-decoration: none; - display: inline-block; - transition: background-color 0.3s; +.review-summary-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.edit-btn:hover { - background: #5a6268; - color: white; +.review-summary-card { + border: 1px solid var(--border-default); + border-radius: 12px; + padding: 1.1rem; } -.document-list { - background: white; - border: 1px solid #dee2e6; - border-radius: 6px; - padding: 1rem; - margin-top: 1rem; +.review-summary-card--wide { + grid-column: 1 / -1; } -.document-item { - display: flex; +.review-summary-card>header { align-items: center; - padding: 0.5rem 0; - border-bottom: 1px solid #e9ecef; -} - -.document-item:last-child { - border-bottom: none; -} - -.document-item i { - color: #dc3545; - margin-right: 0.5rem; - font-size: 1.2rem; + border-bottom: 1px solid var(--border-subtle); + display: flex; + justify-content: space-between; + margin-bottom: 0.9rem; + padding-bottom: 0.7rem; } -.document-name { - font-weight: 500; - width: 62%; - overflow: clip; +.review-summary-card h2 { + color: var(--text-heading); + font-size: 1.05rem; + margin: 0; } -.change-btn { - background: #6c757d; - color: white; - border: none; - border-radius: 20px; - padding: 0.3rem 0.8rem; - font-size: 0.8rem; - margin-left: auto; +.review-summary-card header a { + color: var(--better-blue); + font-size: 0.9rem; + font-weight: 700; } -.change-btn:hover { - background: #5a6268; - color: white; +.review-summary-card dl { + margin: 0; } -.action-buttons { - margin-top: 2rem; - display: flex; +.review-summary-card dl>div { + display: grid; gap: 1rem; - justify-content: center; -} - -.btn-continue { - background: var(--better-blue); - color: white; - border: none; - padding: 0.75rem 2rem; - border-radius: 6px; - font-weight: 600; - font-size: 1rem; -} - -.btn-continue:hover { - background: #1e3d6f; - color: white; + grid-template-columns: minmax(110px, 0.8fr) 1.5fr; + padding: 0.35rem 0; } -.btn-back { - background: #6c757d; - color: white; - border: none; - padding: 0.75rem 2rem; - border-radius: 6px; - font-weight: 600; - font-size: 1rem; -} - -.btn-back:hover { - background: #5a6268; - color: white; +.review-summary-card dt { + color: var(--text-muted); + font-size: 0.85rem; } -.payment-info { - background: #e3f2fd; - border: 1px solid #bbdefb; - border-radius: 6px; - padding: 1rem; - margin-top: 1rem; +.review-summary-card dd { + color: var(--text-heading); + font-weight: 650; + margin: 0; + text-align: right; } -.payment-item { - display: flex; - justify-content: space-between; +.review-document-list article { align-items: center; - padding: 0.25rem 0; -} - -.payment-total { - font-weight: bold; - font-size: 1.1rem; - border-top: 1px solid #90caf9; - padding-top: 0.5rem; - margin-top: 0.5rem; -} - -.loading-spinner { - display: none; - text-align: center; - padding: 2rem; -} - -.error-message { - display: none; - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; - border-radius: 6px; - padding: 1rem; - margin-bottom: 1rem; + display: flex; + gap: 0.8rem; + padding: 0.65rem 0; } -.success-message { - display: none; - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; - border-radius: 6px; - padding: 1rem; - margin-bottom: 1rem; +.review-document-list article+article { + border-top: 1px solid var(--border-subtle); } -/* Payment methods styles moved from review.html */ -.payment-methods-list { - border: 1px solid #e0e0e0; - border-radius: 8px; - overflow: hidden; - width: 100%; +.review-document-list i { + color: var(--danger-icon); + font-size: 1.3rem; } -.payment-method-item { - border-bottom: 1px solid #e0e0e0; - padding: 0; - width: 100%; - box-sizing: border-box; +.review-document-list strong, +.review-document-list small, +.review-party strong, +.review-party small { + display: block; } -.payment-method-item:last-child { - border-bottom: none; +.review-document-list small, +.review-party small { + color: var(--text-muted); } -.payment-method-item .form-check { - margin: 0; - padding: 16px 20px; - display: flex; - align-items: center; - gap: 16px; - width: 100%; - box-sizing: border-box; - min-height: 60px; +.review-person-name { + color: var(--text-heading); + font-weight: 700; + margin-bottom: 0.35rem; } -.payment-method-item .form-check-input { - margin: 0 !important; - margin-left: 0 !important; - margin-right: 0 !important; - flex: 0 0 20px; - width: 20px; - height: 20px; - position: static; +.review-document-tag { + background: var(--surface-accent); + border-radius: 999px; + color: var(--text-body); + display: inline-block; + font-size: 0.78rem; + font-weight: 650; + margin-top: 0.3rem; + padding: 0.15rem 0.55rem; } -.payment-method-details { - display: flex; - flex-direction: row; - align-items: center; - gap: 12px; - flex: 1 1 auto; - min-width: 0; - overflow: hidden; +.review-fee-breakdown { + color: var(--text-muted); + font-size: 0.85rem; + list-style: none; + margin: 0.3rem 0 0; + padding: 0; } -.payment-method-info { +.review-fee-breakdown li { display: flex; - align-items: center; - color: #666; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - gap: 8px; -} - -.payment-method-info i { - margin: 0; - width: 28px; - text-align: center; - color: var(--better-blue); - font-size: 1.1rem; -} - -.payment-method-name { - font-weight: 600; - color: var(--better-blue); - margin-bottom: 0; -} - -.payment-method-holder { - font-size: 0.9rem; - color: #888; + justify-content: space-between; + padding: 0.15rem 0; } -.payment-method-item:hover { - background-color: var(--light-blue-background); +.review-summary-card address { + color: var(--text-body); + line-height: 1.55; } -.no-payment-methods { - text-align: center; - padding: 2rem; +.review-party { + padding: 0.45rem 0; } -.add-payment-method { - text-align: center; +.review-party+.review-party { + border-top: 1px solid var(--border-subtle); } -/* Disclaimer section styling */ -.disclaimer-section { - background: #fff3cd; - border: 1px solid #ffeaa7; - border-radius: 6px; +.review-attestation { + background: var(--warning-surface); + border: 1px solid var(--warning-border); + border-radius: 10px; + margin-top: 1.5rem; padding: 1rem; - margin-top: 2rem; } -.disclaimer-section .form-check-label { - font-size: 0.95rem; - line-height: 1.4; - color: #856404; +.review-attestation label { + align-items: flex-start; + display: flex; + gap: 0.75rem; } -.disclaimer-section .form-check-input { - margin-top: 0.25rem; -} +@media (max-width: 700px) { + .review-summary-grid { + grid-template-columns: 1fr; + } -/* Disabled button styling */ -.btn-continue:disabled { - background: #6c757d !important; - border-color: #6c757d !important; - cursor: not-allowed; - opacity: 0.65; + .review-summary-card--wide { + grid-column: auto; + } } \ No newline at end of file diff --git a/efile_app/efile/static/css/upload.css b/efile_app/efile/static/css/upload.css index 6142255..ba6837e 100644 --- a/efile_app/efile/static/css/upload.css +++ b/efile_app/efile/static/css/upload.css @@ -307,7 +307,7 @@ } .btn-primary:hover { - background-color: #0056b3; + background-color: var(--brand-blue-hover); color: white; } diff --git a/efile_app/efile/static/js/extraction-review.js b/efile_app/efile/static/js/extraction-review.js index cad2c81..e6158ff 100644 --- a/efile_app/efile/static/js/extraction-review.js +++ b/efile_app/efile/static/js/extraction-review.js @@ -40,7 +40,12 @@ field.input = root.querySelector(".review-field__input"); field.valueEl = root.querySelector(".review-field__value"); field.hint = root.querySelector(".review-field__hint"); - root.querySelector(".review-field__edit").addEventListener("click", () => setMode(key, "edit")); + root.querySelector(".review-field__edit").addEventListener("click", () => { + setMode(key, "edit"); + // The Edit button lives inside the display panel that setMode just + // hid, so without this the click would strand focus on . + field.select.focus(); + }); }); function setMode(key, mode) { @@ -126,8 +131,8 @@ await ADVANCE[key](); } else { field.hint.textContent = guesses[field.guessKey] ? - "We found a hint in your document, but couldn't match it to an exact choice below." : - "We couldn't find this in your document."; + "We found a hint in your document, but we could not match it to a choice below." : + "We could not find this in your document."; setMode(key, "edit"); } } diff --git a/efile_app/efile/static/js/filing-payload.js b/efile_app/efile/static/js/filing-payload.js index 2b81443..bbfe9ec 100644 --- a/efile_app/efile/static/js/filing-payload.js +++ b/efile_app/efile/static/js/filing-payload.js @@ -32,13 +32,56 @@ function componentCode(value) { } const FilingPayload = { + userDataFromCaseData(caseData) { + const filer = (caseData.filing_parties || []).find((party) => party.role === "filer") || {}; + const fullName = [filer.first_name, filer.middle_name, filer.last_name] + .filter(Boolean) + .join(" ") || [caseData.petitioner_first_name, caseData.petitioner_last_name].filter(Boolean).join(" "); + return { + fullName, + address: filer.address_line_1 || caseData.petitioner_address || "", + addressLine2: filer.address_line_2 || "", + city: filer.city || "", + state: filer.state || "", + zip: filer.zip_code || "", + email: filer.email || caseData.petitioner_email || "", + phone: filer.phone || caseData.petitioner_phone || "" + }; + }, + + partyFromDraft(party) { + return { + party_type: party.party_type, + name: { + first: party.first_name || party.organization_name || "", + middle: party.middle_name || "", + last: party.last_name || "", + suffix: party.suffix || "" + }, + address: { + address: party.address_line_1 || "", + unit: party.address_line_2 || "", + city: party.city || "", + state: party.state || "", + zip: party.zip_code || "", + country: party.country || "US" + }, + email: party.email || "", + phone_number: party.phone || "", + is_new: !party.external_party_id + }; + }, + buildEFilingData(userData, caseData, uploadData, paymentAccountID) { const nameParts = userData.fullName.split(" "); const firstName = nameParts[0] || ""; const lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ""; const middleName = nameParts.length > 2 ? nameParts.slice(1, -1).join(" ") : ""; - const partyType = caseData.determined_party_type || caseData.petitioner_party_type || caseData.party_type; + const durableParties = caseData.filing_parties || []; + const durableFiler = durableParties.find((party) => party.role === "filer"); + const partyType = durableFiler?.party_type || caseData.determined_party_type || + caseData.petitioner_party_type || caseData.party_type; if (!partyType) { throw new Error('Party type could not be determined. This is required for eFiling.'); @@ -102,9 +145,11 @@ const FilingPayload = { }); } - let other_parties = []; + let other_parties = durableParties + .filter((party) => party.role !== "filer" && party.party_type) + .map((party) => this.partyFromDraft(party)); - if (caseData.other_first_name && caseData.other_party_type) { + if (other_parties.length === 0 && caseData.other_first_name && caseData.other_party_type) { other_parties.push({ party_type: caseData.other_party_type, name: { @@ -137,6 +182,12 @@ const FilingPayload = { al_court_bundle: [], comments_to_clerk: "", tyler_payment_id: paymentAccountID, + // Only sent when a chosen filing type requires it (case_questions + // asks for it in that case); the EFSP rejects the filing outright + // if it's required and missing, so leave it out rather than send 0. + ...(caseData?.amount_in_controversy ? { + amount_in_controversy: caseData.amount_in_controversy + } : {}), lead_contact: { name: { first: firstName, @@ -199,11 +250,7 @@ const FilingPayload = { }, createDocumentBundle(doc, filingType, documentType, filingComponent, users, description, docDescription, cc_email) { - if (cc_email) { - courtesy_copies = [cc_email] - } else { - courtesy_copies = [] - } + const courtesy_copies = cc_email ? [cc_email] : []; return { proxy_enabled: true, filing_type: filingType, @@ -215,7 +262,7 @@ const FilingPayload = { filing_comment: "", courtesy_copies: courtesy_copies, preliminary_copies: [], - filing_parties: users.length === 1 ? ["users[0]"] : ["users[0]", "users[1]"], + filing_parties: users.map((_user, index) => `users[${index}]`), filing_action: "efile", tyler_merge_attachments: false, document_type: documentType, @@ -241,20 +288,25 @@ const FilingPayload = { handleFeesResponse(result) { if (result?.success) { - let htmlStr = ` - Total: $${result.api_response.feesCalculationAmount.value} - - "; + const response = result.api_response || {}; + const infoElem = document.getElementById("paymentInfo"); + infoElem.replaceChildren(); + const total = document.createElement("p"); + const label = document.createElement("strong"); + label.textContent = gettext("Total"); + total.append(label, `: $${response.feesCalculationAmount?.value || "0.00"}`); + infoElem.appendChild(total); - let infoElem = document.getElementById("paymentInfo"); - infoElem.innerHTML = htmlStr; + const fees = (response.allowanceCharge || []).filter((fee) => fee.chargeIndicator?.value); + if (fees.length) { + const list = document.createElement("ul"); + fees.forEach((fee) => { + const item = document.createElement("li"); + item.textContent = `${fee.allowanceChargeReason?.value || gettext("Court fee")}: $${fee.amount?.value || "0.00"}`; + list.appendChild(item); + }); + infoElem.appendChild(list); + } document.getElementById("paymentSection").removeAttribute("hidden"); } else { Messages.showError(result?.error || "An error occurred when calculating fees."); diff --git a/efile_app/efile/static/js/organize-documents.js b/efile_app/efile/static/js/organize-documents.js index f319ecd..873738f 100644 --- a/efile_app/efile/static/js/organize-documents.js +++ b/efile_app/efile/static/js/organize-documents.js @@ -242,11 +242,13 @@ const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "btn btn-link optional-services-toggle"; + toggle.setAttribute("aria-expanded", "false"); const showMoreText = interpolate(ngettext("Show %s more option", "Show %s more options", rest.length), [rest.length]); toggle.textContent = showMoreText; toggle.addEventListener("click", () => { const wasExpanded = !moreContainer.hidden; moreContainer.hidden = wasExpanded; + toggle.setAttribute("aria-expanded", String(!wasExpanded)); toggle.textContent = wasExpanded ? showMoreText : gettext("Show fewer options"); }); diff --git a/efile_app/efile/static/js/parties.js b/efile_app/efile/static/js/parties.js index 2e3f1df..50dbc3d 100644 --- a/efile_app/efile/static/js/parties.js +++ b/efile_app/efile/static/js/parties.js @@ -4,14 +4,15 @@ button.addEventListener("click", () => { const radio = document.querySelector(`input[name="filer_party_type"][value="${button.dataset.value}"]`); + const hint = document.getElementById("party-type-hint"); + if (hint) hint.hidden = true; if (radio) { radio.checked = true; radio.scrollIntoView({ behavior: "smooth", block: "center" }); + radio.focus(); } - const hint = document.getElementById("party-type-hint"); - if (hint) hint.hidden = true; }); })(); \ No newline at end of file diff --git a/efile_app/efile/static/js/payment.js b/efile_app/efile/static/js/payment.js index 068ecdf..b6f38ef 100644 --- a/efile_app/efile/static/js/payment.js +++ b/efile_app/efile/static/js/payment.js @@ -1,409 +1,204 @@ -/** - * Review Page JavaScript - Optimized Version - * Handles review page functionality with improved organization and performance - */ - -// Configuration constants -const CONFIG = { - URLS: { - PROFILE: '/api/auth/profile/', - PAYMENT_ACCOUNTS: '/api/payment-accounts/', - TYLER_TOKEN: '/api/auth/tyler-token/', - SUBMIT_FILING: '/api/submit-final-filing/', - QUERY_FEES: '/api/payment-fees/', - } +const PAYMENT_URLS = { + accounts: "/api/payment-accounts/", + accountTypes: "/api/payment-account-types/", + token: "/api/auth/tyler-token/", + fees: "/api/payment-fees/" }; - -// Utility functions -const Utils = { - getElement(id) { - return document.getElementById(id); - }, - - getElements(selector) { - return document.querySelectorAll(selector); - }, - - parseJSON(elementId) { - const element = this.getElement(elementId); - return element ? JSON.parse(element.textContent) : {}; - }, - - showElement(element) { - if (element) element.style.display = "block"; - }, - - hideElement(element) { - if (element) element.style.display = "none"; - }, - - // URL parameter helper - getURLParam(param) { - return new URLSearchParams(window.location.search).get(param); - }, - - cleanURL() { - window.history.replaceState({}, document.title, window.location.pathname); - } - +const paymentJSON = (id) => { + const element = document.getElementById(id); + return element ? JSON.parse(element.textContent) : {}; }; -// Message handling -const Messages = { - show(type, message) { - const messageDiv = Utils.getElement(type === 'error' ? 'errorMessage' : 'successMessage'); - const textElement = Utils.getElement(type === 'error' ? 'errorText' : 'successText'); - - if (messageDiv && textElement) { - textElement.textContent = message; - Utils.showElement(messageDiv); - messageDiv.scrollIntoView({ - behavior: "smooth", - block: "center" - }); - } +const paymentMessages = { + hide() { + document.getElementById("errorMessage").hidden = true; + document.getElementById("successMessage").hidden = true; }, - showError(message) { - this.show('error', message); + document.getElementById("errorText").textContent = message; + const box = document.getElementById("errorMessage"); + box.hidden = false; + box.scrollIntoView({ + behavior: "smooth", + block: "center" + }); }, - showSuccess(message) { - this.show('success', message); - }, - - hide() { - Utils.hideElement(Utils.getElement('errorMessage')); - Utils.hideElement(Utils.getElement('successMessage')); - } -}; - -// UI Field management -const FieldManager = { - // Consolidated field setting logic - setFieldValue(fieldPrefix, value, displayValue = value) { - const input = Utils.getElement(`${fieldPrefix}Input`); - const text = Utils.getElement(`${fieldPrefix}Text`); - - if (input) input.value = value || ""; - if (text) text.textContent = displayValue || ""; - }, - - getFieldValue(fieldPrefix) { - const input = Utils.getElement(`${fieldPrefix}Input`); - const text = Utils.getElement(`${fieldPrefix}Text`); - const inputVal = input?.value?.trim(); - const textVal = text?.textContent?.trim(); - return (inputVal && inputVal.length > 0) ? inputVal : textVal; - }, - - startEditing(input, text, button, inputId) { - Utils.hideElement(text); - Utils.showElement(input); - input.focus(); - input.select(); - button.textContent = "Save"; - button.onclick = () => this.saveField(input, text, button, inputId); - }, - - saveField(input, text, button, inputId) { - text.textContent = input.value.trim(); - Utils.hideElement(input); - Utils.showElement(text); - button.textContent = "Edit"; - button.onclick = () => this.toggleEdit(inputId, button); + document.getElementById("successText").textContent = message; + document.getElementById("successMessage").hidden = false; } }; +window.Messages = paymentMessages; -// API handlers -const APIHandlers = { - async loadPaymentAccounts() { - const params = { - jurisdiction: apiUtils.getCurrentJurisdiction() - }; - const result = await apiUtils.fetchJSON(CONFIG.URLS.PAYMENT_ACCOUNTS, "GET", params); - - if (result?.success && result.data) { - UIUpdater.updatePaymentMethodsSection(result.data); - let elems = document.querySelectorAll('input[name="paymentMethod"]'); - elems.forEach(e => e.addEventListener("change", () => { - window.queryFees(); - })); - window.queryFees(); - } else { - UIUpdater.showAddNewPaymentMethod(); - } - } +const escapeHTML = (value) => { + const span = document.createElement("span"); + span.textContent = String(value || ""); + return span.innerHTML; }; -// UI updaters -const UIUpdater = { - updatePaymentMethodsSection(paymentAccounts) { - const container = Utils.getElement('paymentMethodsContainer'); - if (!container || !paymentAccounts?.length) { - return this.showAddNewPaymentMethod(); - } - - let html = '
'; +const escapeAttribute = (value) => escapeHTML(value).replaceAll('"', """).replaceAll("'", "'"); - let hasMultipleWaivers = paymentAccounts.filter((account) => account.paymentAccountTypeCode === "WV").length > 1; - paymentAccounts.forEach((account, index) => { - const isDefault = index === 0; - const cardType = account.cardType?.value || "Card"; - const cardLast4 = account.cardLast4 || "****"; - let paymentText = `${cardType} ending in ${cardLast4}`; +const PaymentPage = { + caseData: paymentJSON("case-data"), + // code -> the court's own name for that account type (e.g. "CC" -> "Credit + // Card"), fetched from GetPaymentAccountTypeList. Populated by + // loadAccountTypes(); empty if that call fails, which just means the + // generic fallback in accountLabel() is used instead. + typeDescriptions: {}, - if (account.paymentAccountTypeCode === "WV") { - if (hasMultipleWaivers) { - paymentText = `Payment Waiver (named "${account.accountName}")`; - } else { - paymentText = 'Payment Waiver'; - } - } - - html += `
-
- - -
-
`; - }); - - html += `
-
- -
`; - - container.innerHTML = html; + setFeesState(loading) { + document.getElementById("loadingSpinner").style.display = loading ? "block" : "none"; + document.getElementById("submitButton").disabled = loading || !document.querySelector('input[name="paymentMethod"]:checked'); }, - showAddNewPaymentMethod() { - const container = Utils.getElement('paymentMethodsContainer'); - if (!container) return; - - container.innerHTML = `
-
- - No payment methods found. Please add a payment method to continue. -
- -
`; - } -}; - -// Payment handling -const PaymentHandler = { - async addNewPaymentMethod() { + async loadAccountTypes() { try { - const params = new URLSearchParams({ - jurisdiction: apiUtils.getCurrentJurisdiction(), + const result = await apiUtils.fetchJSON(PAYMENT_URLS.accountTypes, "GET", { + jurisdiction: apiUtils.getCurrentJurisdiction() + }); + const types = result?.success ? result.data : []; + (types || []).forEach((type) => { + if (type.code) this.typeDescriptions[type.code] = type.description || type.code; }); - const authData = await apiUtils.fetchJSON(CONFIG.URLS.TYLER_TOKEN, "GET", params); - - if (!authData?.success || !authData.data?.tyler_token) { - Messages.showError("Authentication failed. Please try again."); - return; - } - - this.redirectToPaymentForm(authData.data); } catch (error) { - console.warn("Create payment error: %o", error); - Messages.showError("Failed to create payment method. Please try again."); + // The court's own type names are a nicety, not a requirement -- + // accountLabel() falls back to the account's own name if this + // list never loads. } }, - redirectToPaymentForm(authData) { - const form = document.createElement("form"); - form.method = "post"; - - const jurisdiction = authData.state || apiUtils.getCurrentJurisdiction(); - form.action = Utils.parseJSON('new-toga-url'); - - let dateStr = new Date().toDateString(); - const fields = [ - ['account_name', `Payment Account made on ${dateStr}`], - ['global', 'false'], - ['type_code', 'CC'], - ['tyler_info', authData.tyler_token], - ['original_url', `${window.location.origin}/jurisdiction/${jurisdiction}/payment/?payment_status=success`], - ['error_url', `${window.location.origin}/jurisdiction/${jurisdiction}/payment/?payment_status=failure`] - ]; - - fields.forEach(([name, value]) => { - const input = document.createElement("input"); - input.type = "hidden"; - input.name = name; - input.value = value; - form.appendChild(input); - }); - - document.body.appendChild(form); - form.submit(); - }, - - handleCallback() { - const status = Utils.getURLParam('payment_status'); - - if (status === 'success') { - Messages.showSuccess(gettext("Payment method added successfully!")); - setTimeout(() => APIHandlers.loadPaymentAccounts(), 1000); - Utils.cleanURL(); - } else if (status === 'failure') { - Messages.showError(gettext("Failed to add payment method. Please try again.")); - Utils.cleanURL(); + // Tyler's payment accounts aren't all cards -- assuming so mislabeled + // waivers, ACH/bank accounts, and firm balances alike as "Card ending in + // ****". Only claim "card" when the account actually carries card data; + // otherwise use the court's own name for the account type, falling back + // to whatever Tyler calls the account if that type list didn't load. + accountLabel(account, waiverCount) { + if (account.paymentAccountTypeCode === "WV") { + return waiverCount > 1 ? `${gettext("Payment waiver")}: ${account.accountName}` : gettext("Payment waiver"); } + if (account.cardLast4) { + return `${account.cardType?.value || gettext("Card")} ${gettext("ending in")} ${account.cardLast4}`; + } + const typeName = this.typeDescriptions[account.paymentAccountTypeCode]; + if (typeName && account.accountName) return `${typeName}: ${account.accountName}`; + return typeName || account.accountName || gettext("Payment account"); }, - calcPaymentCosts() { - - } -}; - -// Navigation -const Navigation = { - goBack() { - window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/upload`; - }, - - async toReview() { - const selectedPaymentMethod = document.querySelector('input[name="paymentMethod"]:checked'); - await apiUtils.saveCaseData({ - "selected_payment_account": selectedPaymentMethod.value, - "selected_payment_account_name": selectedPaymentMethod.getAttribute("fullName") + async loadAccounts() { + const result = await apiUtils.fetchJSON(PAYMENT_URLS.accounts, "GET", { + jurisdiction: apiUtils.getCurrentJurisdiction() }); - - window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/review`; - } -}; - -// Filing submission -const FilingHandler = { - async queryFees() { - document.getElementById("paymentSection").setAttribute("hidden", true); - this.setFeesState(true); - - const userData = await this.collectUserData(); - const selectedPaymentMethod = document.querySelector('input[name="paymentMethod"]:checked'); - if (!selectedPaymentMethod) { - this.setFeesState(false); + const accounts = result?.success ? result.data : []; + const container = document.getElementById("paymentMethodsContainer"); + if (!accounts?.length) { + container.innerHTML = `
${gettext("You have no saved payment methods. Add one to continue.")}
+ `; + document.getElementById("add-payment-method").addEventListener("click", () => this.addAccount()); return; } - + const saved = paymentJSON("selected-payment-account-id"); + const waiverCount = accounts.filter((account) => account.paymentAccountTypeCode === "WV").length; + const rows = accounts.map((account, index) => { + const label = this.accountLabel(account, waiverCount); + const checked = saved ? String(saved) === String(account.paymentAccountID) : index === 0; + return ``; + }).join(""); + container.innerHTML = `
${rows}
+ `; + container.querySelectorAll('input[name="paymentMethod"]').forEach((input) => { + input.addEventListener("change", () => this.selectAndQuote()); + }); + document.getElementById("add-payment-method").addEventListener("click", () => this.addAccount()); + await this.selectAndQuote(); + }, + + async selectAndQuote() { + const selected = document.querySelector('input[name="paymentMethod"]:checked'); + if (!selected) return; + document.getElementById("selected-payment-account").value = selected.value; + document.getElementById("selected-payment-account-name").value = selected.dataset.name; + document.getElementById("selected-payment-account-type").value = selected.dataset.type || ""; + document.getElementById("paymentSection").hidden = true; + document.getElementById("quoted-fee-total").value = ""; + document.getElementById("quoted-fee-breakdown").value = ""; + paymentMessages.hide(); + this.setFeesState(true); try { - const result = await this.processFees(userData, selectedPaymentMethod.value); + const uploadData = await apiUtils.getUploadData(); + const userData = FilingPayload.userDataFromCaseData(this.caseData); + const efileData = this.buildEFilingData(userData, this.caseData, uploadData, selected.value); + const result = await apiUtils.post(PAYMENT_URLS.fees, { + efile_data: efileData, + confirm_submission: true, + payment_account_id: selected.value + }, {}, { + timeout: ApiUtils.FILING_TIMEOUT_MS + }); this.handleFeesResponse(result); + this.storeFeeQuote(result); } catch (error) { - console.warn("Error on submission: %o", error) - // Show what the server said when it said anything: "no party of type - // Plaintiff" is fixable by the filer, "an unexpected error" is not. - Messages.showError(error?.serverMessage || gettext("An unexpected error occurred. Please try again.")); + paymentMessages.showError(error?.serverMessage || gettext("We could not calculate fees. Please try again.")); this.setFeesState(false); } }, - async collectUserData() { - const params = { - jurisdiction: apiUtils.getCurrentJurisdiction() - }; - const data = await apiUtils.fetchJSON(CONFIG.URLS.PROFILE, "GET", params); - - if (data?.success && data.data) { - const profile = data.data; - const fullName = [profile.first_name, profile.last_name].filter(n => n).join(" "); + // Persist the quote Review will later display, so it shows the same + // numbers the filer already saw here instead of sending them back to + // look them up again. + storeFeeQuote(result) { + if (!result?.success) return; + const response = result.api_response || {}; + const fees = (response.allowanceCharge || []) + .filter((fee) => fee.chargeIndicator?.value) + .map((fee) => ({ + label: fee.allowanceChargeReason?.value || gettext("Court fee"), + amount: fee.amount?.value || "0.00" + })); + document.getElementById("quoted-fee-total").value = response.feesCalculationAmount?.value || "0.00"; + document.getElementById("quoted-fee-breakdown").value = JSON.stringify(fees); + }, - // Set all user fields - return { - fullName: fullName, - address: profile.address, - addressLine2: profile.address_line2, - city: profile.city, - state: profile.state, - zip: profile.zip, - email: profile.email, - phone: profile.phone - }; + async addAccount() { + const authData = await apiUtils.fetchJSON(PAYMENT_URLS.token, "GET", { + jurisdiction: apiUtils.getCurrentJurisdiction() + }); + if (!authData?.success || !authData.data?.tyler_token) { + paymentMessages.showError(gettext("We could not verify your account. Please sign in again.")); + return; } - return { - fullName: "", - address: "", - addressLine2: "", - city: "Citytown", - state: "IL", - zip: "", - email: "test@example.com", - phone: "", + const jurisdiction = authData.data.state || apiUtils.getCurrentJurisdiction(); + const form = document.createElement("form"); + form.method = "post"; + form.action = paymentJSON("new-toga-url"); + const fields = { + account_name: `Payment account made on ${new Date().toDateString()}`, + global: "false", + type_code: "CC", + tyler_info: authData.data.tyler_token, + original_url: `${window.location.origin}/jurisdiction/${jurisdiction}/payment/?payment_status=success`, + error_url: `${window.location.origin}/jurisdiction/${jurisdiction}/payment/?payment_status=failure` }; - }, - - setFeesState(isQueryingFees) { - const submitButton = Utils.getElement('submitButton'); - const loadingSpinner = Utils.getElement('loadingSpinner'); - - if (submitButton) submitButton.disabled = isQueryingFees; - if (loadingSpinner) loadingSpinner.style.display = isQueryingFees ? "block" : "none"; - }, - - async processFees(userData, paymentAccountID) { - let [caseData, uploadData] = await Promise.all([ - apiUtils.getCaseData(), - apiUtils.getUploadData() - ]); - - caseData = caseData.data.case_data; - - const efilingData = this.buildEFilingData(userData, caseData, uploadData, paymentAccountID); - - return await apiUtils.post(CONFIG.URLS.QUERY_FEES, { - efile_data: efilingData, - confirm_submission: true, - payment_account_id: paymentAccountID - }, {}, { - timeout: ApiUtils.FILING_TIMEOUT_MS + Object.entries(fields).forEach(([name, value]) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = name; + input.value = value; + form.appendChild(input); }); - } -}; - -// buildEFilingData / addCourtBundles / createDocumentBundle / -// handleSubmissionResult / handleFeesResponse are shared with the other -// filing page. See filing-payload.js -- keep payload changes there so the -// fee quote and the submitted filing cannot drift apart. -Object.assign(FilingHandler, FilingPayload); - -// Main application initialization -const ReviewApp = { - async init() { - await this.loadAllData(); - PaymentHandler.handleCallback(); + document.body.appendChild(form); + form.submit(); }, - async loadAllData() { - // Load other data in parallel - await Promise.all([ - APIHandlers.loadPaymentAccounts() - ]); + async init() { + const status = new URLSearchParams(window.location.search).get("payment_status"); + if (status === "failure") paymentMessages.showError(gettext("The payment method was not added.")); + if (status === "success") paymentMessages.showSuccess(gettext("Payment method added.")); + await this.loadAccountTypes(); + this.loadAccounts().catch(() => paymentMessages.showError(gettext("We could not load payment methods."))); } }; -// Global function exports for HTML onclick handlers -window.goBack = Navigation.goBack; -window.toReview = Navigation.toReview; -window.queryFees = FilingHandler.queryFees.bind(FilingHandler); -window.PaymentHandler = PaymentHandler; -window.Navigation = Navigation; - -// Initialize app when DOM is ready -document.addEventListener("DOMContentLoaded", () => ReviewApp.init()); \ No newline at end of file +Object.assign(PaymentPage, FilingPayload); +document.addEventListener("DOMContentLoaded", () => PaymentPage.init()); \ No newline at end of file diff --git a/efile_app/efile/static/js/review.js b/efile_app/efile/static/js/review.js index 7b4052d..1119138 100644 --- a/efile_app/efile/static/js/review.js +++ b/efile_app/efile/static/js/review.js @@ -1,587 +1,67 @@ -/** - * Review Page JavaScript - Optimized Version - * Handles review page functionality with improved organization and performance - */ +const reviewJSON = (id) => JSON.parse(document.getElementById(id).textContent); -// Configuration constants -const CONFIG = { - VALIDATION: { - EMAIL_REGEX: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - ZIP_REGEX: /^\d{5}(-\d{4})?$/, - PHONE_REGEX: /^\+?\d{7,15}$/ - }, - URLS: { - UPLOAD_DATA: '/api/get-upload-data/', - PROFILE: '/api/auth/profile/', - PAYMENT_ACCOUNTS: '/api/payment-accounts/', - TYLER_TOKEN: '/api/auth/tyler-token/', - SUBMIT_FILING: '/api/submit-final-filing/', - CASE_DATA: '/api/get-case-data/', - QUERY_FEES: '/api/payment-fees/', - } -}; - - -// Utility functions -const Utils = { - getElement(id) { - return document.getElementById(id); - }, - - getElements(selector) { - return document.querySelectorAll(selector); - }, - - parseJSON(elementId) { - const element = this.getElement(elementId); - return element ? JSON.parse(element.textContent) : {}; - }, - - showElement(element) { - if (element) element.style.display = "block"; - }, - - hideElement(element) { - if (element) element.style.display = "none"; - }, - - // Validation helpers - isValidEmail(email) { - return email && CONFIG.VALIDATION.EMAIL_REGEX.test(email); - }, - - isValidZip(zip) { - return zip && CONFIG.VALIDATION.ZIP_REGEX.test(zip); - }, - - isValidPhone(phone) { - if (!phone) return false; - const cleaned = phone.replace(/[\s()-\.]/g, ""); - return CONFIG.VALIDATION.PHONE_REGEX.test(cleaned); - }, -}; - -// Message handling const Messages = { - show(type, message) { - const messageDiv = Utils.getElement(type === 'error' ? 'errorMessage' : 'successMessage'); - const textElement = Utils.getElement(type === 'error' ? 'errorText' : 'successText'); - - if (messageDiv && textElement) { - textElement.textContent = message; - Utils.showElement(messageDiv); - messageDiv.scrollIntoView({ - behavior: "smooth", - block: "center" - }); - } + hide() { + document.getElementById("errorMessage").hidden = true; + document.getElementById("successMessage").hidden = true; }, - showError(message) { - this.show('error', message); + document.getElementById("errorText").textContent = message; + const box = document.getElementById("errorMessage"); + box.hidden = false; + box.scrollIntoView({ + behavior: "smooth", + block: "center" + }); }, - showSuccess(message) { - this.show('success', message); - }, - - hide() { - Utils.hideElement(Utils.getElement('errorMessage')); - Utils.hideElement(Utils.getElement('successMessage')); - } -}; - -// Data management -const DataManager = { - async fetchJSON(url, options = {}) { - try { - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': apiUtils.getCSRFToken(), - ...options.headers - }, - ...options - }); - // Parse the body on failures too. This is the filing-submission call, - // and the API answers a rejection with {success: false, error: "..."} - // naming what the filer has to correct -- a missing required party, - // a document the EFSP could not fetch. Returning null on !ok threw - // that away and left "An error occurred during submission." - // handleSubmissionResult already keys off `success`, so a non-ok body - // routes to the same error branch, now with the reason in it. - return await response.json().catch(() => null); - } catch (error) { - console.error(`Fetch error for ${url}:`, error); - return null; - } - }, - - async getCaseData() { - return Utils.parseJSON('case-data'); - }, - - getFriendlyNames() { - return Utils.parseJSON('friendly-names'); + document.getElementById("successText").textContent = message; + document.getElementById("successMessage").hidden = false; } }; -// UI Field management -const FieldManager = { - // Consolidated field setting logic - setFieldValue(fieldPrefix, value, displayValue = value) { - const input = Utils.getElement(`${fieldPrefix}Input`); - const text = Utils.getElement(`${fieldPrefix}Text`); - - if (input) input.value = value || ""; - if (text) text.textContent = displayValue || ""; - }, - - getFieldValue(fieldPrefix) { - const input = Utils.getElement(`${fieldPrefix}Input`); - const text = Utils.getElement(`${fieldPrefix}Text`); - const inputVal = input?.value?.trim(); - const textVal = text?.textContent?.trim(); - return (inputVal && inputVal.length > 0) ? inputVal : textVal; - }, - - toggleEdit(inputId, button) { - const input = Utils.getElement(inputId); - const text = Utils.getElement(inputId.replace("Input", "Text")); - if (!input || !text || !button) return; - - const isEditing = input.style.display !== "none"; - - if (!isEditing) { - this.startEditing(input, text, button, inputId); - } else { - this.saveField(input, text, button, inputId); - } - }, - - startEditing(input, text, button, inputId) { - Utils.hideElement(text); - Utils.showElement(input); - input.focus(); - input.select(); - button.textContent = "Save"; - button.onclick = () => this.saveField(input, text, button, inputId); - }, - - saveField(input, text, button, inputId) { - text.textContent = input.value.trim(); - Utils.hideElement(input); - Utils.showElement(text); - button.textContent = "Edit"; - button.onclick = () => this.toggleEdit(inputId, button); - } -}; - -// Form validation -const FormValidator = { - validateUserData(userData) { - const { - fullName, - address, - city, - state, - zip, - email, - phone - } = userData; - - const requiredFields = [{ - value: fullName, - name: 'Name' - }, { - value: address, - name: 'Address Line 1' - }, { - value: city, - name: 'City' - }, { - value: state, - name: 'State' - }, { - value: zip, - name: 'ZIP Code' - }, { - value: email, - name: 'Email' - }, { - value: phone, - name: 'Phone' - }]; - - // Check required fields - for (const field of requiredFields) { - if (!field.value) { - return `${field.name} is required.`; - } - } - - // Validate formats - if (!Utils.isValidZip(zip)) { - return "Please enter a valid ZIP code (e.g. 60601 or 60601-1234)."; - } - - if (!Utils.isValidPhone(phone)) { - return "Please enter a valid phone number."; - } - - if (!Utils.isValidEmail(email)) { - return "Please provide a valid email address."; - } - - return null; // No validation errors - } -}; - -// API handlers -const APIHandlers = { - async fetchPartyType() { - const caseData = await DataManager.getCaseData(); - - if (!caseData.court || !caseData.case_type) return; - - const params = { - jurisdiction: apiUtils.getCurrentJurisdiction(), - court: caseData.court, - case_type: caseData.case_type, - existing_case: caseData.existing_case || 'no' - }; - - const result = await apiUtils.getPartyTypes(params); - - if (result?.success) { - console.log('Party types received:', result.party_types); - console.log('Selected party type:', result.selected_party_type); - } else { - console.error('Party type fetch failed:', result?.error); - } - }, - - async loadUserInfo() { - const params = { - jurisdiction: apiUtils.getCurrentJurisdiction() - }; - const data = await apiUtils.fetchJSON(CONFIG.URLS.PROFILE, "GET", params); - - if (data?.success && data.data) { - const profile = data.data; - const fullName = [profile.first_name, profile.last_name].filter(n => n).join(" "); - - // Set all user fields - const fields = [ - ['userName', fullName], - ['userAddressLine1', profile.address], - ['userAddressLine2', profile.address_line2], - ['userCity', profile.city], - ['userState', profile.state], - ['userZip', profile.zip], - ['userEmail', profile.email], - ['userPhone', profile.phone] - ]; - - fields.forEach(([prefix, value]) => FieldManager.setFieldValue(prefix, value)); - } else { - // Set empty values for all fields on failure - ['userName', 'userAddressLine1', 'userAddressLine2', 'userCity', 'userState', 'userZip', 'userEmail', 'userPhone'] - .forEach(prefix => FieldManager.setFieldValue(prefix, "", "Please provide")); - } - }, - - async loadUploadData() { - const data = await apiUtils.getUploadData(); - if (data) { - UIUpdater.updateDocumentsSection(data); - } - }, - - async loadPaymentAccounts() { - try { - const caseData = await DataManager.getCaseData(); - UIUpdater.updatePaymentMethodsSection(caseData.selected_payment_account, caseData.selected_payment_account_name || "Your payment"); - await window.queryFees(); - } catch (ex) { - console.error(ex); - UIUpdater.showAddNewPaymentMethod(); - } - } -}; - -// UI updaters -const UIUpdater = { - updateCaseInfo(caseData, friendlyNames) { - const caseTypeEl = Utils.getElement('caseTypeValue'); - const courtEl = Utils.getElement('courtValue'); - - if (caseTypeEl) { - caseTypeEl.textContent = friendlyNames.case_type || caseData.case_type_name || caseData.case_type || "Not specified"; - } - if (courtEl) { - courtEl.textContent = friendlyNames.court || caseData.court_name || caseData.court || "Not specified"; - } - }, - - updateDocumentsSection(uploadData) { - const container = Utils.getElement('documentsContainer'); - if (!container) return; - - let html = ""; - - // Lead document - if (uploadData.files?.lead) { - const docName = uploadData.files.lead.name.includes("Name Change") ? "Name Change Form" : "Lead Document"; - html += this.createDocumentHTML(docName, uploadData.files.lead.name, 'lead', true); - } else { - html += `
1. Lead document (required)
No document found
`; - } - - // Supporting documents - if (uploadData.files?.supporting?.length > 0) { - html += '
2. Fee waiver (optional)
File or Files
'; - - uploadData.files.supporting.forEach(file => { - html += `
- - ${file.name} - -
`; - }); - - html += '
'; - } - - container.innerHTML = html; - }, - - createDocumentHTML(title, filename, type, required = false) { - return `
-
1. ${title} ${required ? '(required)' : ''}
-
File or Files
-
-
- - ${filename} - -
-
-
`; - }, - - updatePaymentMethodsSection(account, account_name) { - const container = Utils.getElement('paymentMethodsContainer'); - let html = '
'; - - //const cardType = account.cardType?.value || "Card"; - //const cardLast4 = account.cardLast4 || "****"; - //let paymentText = `${cardType} ending in ${cardLast4}`; - - html += `
-
-
- -
-
- ${account_name} -
-
-
-
- Change payment method -
`; - - html += `
`; - - container.innerHTML = html; - }, - - showAddNewPaymentMethod() { - const container = Utils.getElement('paymentMethodsContainer'); - if (!container) return; - - container.innerHTML = `
-
- - No payment methods found. Please go back to add a payment method. -
-
`; - } -}; - -// Navigation -const Navigation = { - goBack() { - window.location.href = `/jurisdiction/${apiUtils.getCurrentJurisdiction()}/payment`; +const FilingHandler = { + setSubmissionState(submitting) { + document.getElementById("loadingSpinner").style.display = submitting ? "block" : "none"; + document.getElementById("submitButton").disabled = submitting || !document.getElementById("confirm-filing").checked; }, - changeDocument(type) { - const jurisdiction = apiUtils.getCurrentJurisdiction(); - if (type === "lead") { - // TODO: still save all of the existing stuff? - window.location.href = `/jurisdiction/${jurisdiction}/upload_first`; - } else { - window.location.href = `/jurisdiction/${jurisdiction}/upload`; - } - } -}; + setFeesState() {}, -// Filing submission -const FilingHandler = { async submitFiling() { - const userData = this.collectUserData(); - const validationError = FormValidator.validateUserData(userData); - - if (validationError) { - Messages.showError(validationError); + if (!document.getElementById("confirm-filing").checked) { + Messages.showError(gettext("Confirm that you reviewed the filing before you submit.")); return; } - - const selectedPaymentMethod = document.getElementById('paymentAccountID'); - if (!selectedPaymentMethod) { - Messages.showError(gettext("Please select a payment method to continue.")); - return; - } - - this.setSubmissionState(true); Messages.hide(); - + this.setSubmissionState(true); try { - const result = await this.processSubmission(userData, selectedPaymentMethod.getAttribute("value")); + const caseData = reviewJSON("case-data"); + const userData = this.userDataFromCaseData(caseData); + const efileData = this.buildEFilingData( + userData, + caseData, + reviewJSON("upload-data"), + reviewJSON("payment-account-id") + ); + const result = await apiUtils.post("/api/submit-final-filing/", { + efile_data: efileData, + confirm_submission: true, + payment_account_id: reviewJSON("payment-account-id") + }, {}, { + timeout: ApiUtils.FILING_TIMEOUT_MS + }); this.handleSubmissionResult(result); } catch (error) { - console.error("Error on submission: %o", error) - // See payment.js: a message the server wrote names something the - // filer can actually correct. - Messages.showError(error?.serverMessage || gettext("An unexpected error occurred. Please try again.")); + Messages.showError(error?.serverMessage || gettext("We could not submit the filing. Please try again.")); this.setSubmissionState(false); } - }, - - async queryFees() { - const userData = await this.collectUserData(); - const selectedPaymentMethod = document.getElementById('paymentAccountID'); - - this.setFeesState(true); - - try { - const result = await this.processFees(userData, selectedPaymentMethod.getAttribute("value")); - this.handleFeesResponse(result); - } catch (error) { - console.error("Error on submission: %o", error) - Messages.showError(error?.serverMessage || gettext("An unexpected error occurred. Please try again.")); - this.setFeesState(false); - } - }, - - async processFees(userData, paymentAccountID) { - let [caseData, uploadData] = await Promise.all([ - apiUtils.getCaseData(), - apiUtils.getUploadData() - ]); - - caseData = caseData.data.case_data; - - const efilingData = this.buildEFilingData(userData, caseData, uploadData, paymentAccountID); - - return await apiUtils.post(CONFIG.URLS.QUERY_FEES, { - efile_data: efilingData, - confirm_submission: true, - payment_account_id: paymentAccountID - }, {}, { - timeout: ApiUtils.FILING_TIMEOUT_MS - }); - }, - - collectUserData() { - return { - fullName: FieldManager.getFieldValue('userName'), - address: FieldManager.getFieldValue('userAddressLine1'), - addressLine2: FieldManager.getFieldValue('userAddressLine2'), - city: FieldManager.getFieldValue('userCity'), - state: FieldManager.getFieldValue('userState'), - zip: FieldManager.getFieldValue('userZip'), - email: FieldManager.getFieldValue('userEmail'), - phone: FieldManager.getFieldValue('userPhone') - }; - }, - - setFeesState(isQueryingFees) { - const submitButton = Utils.getElement('submitButton'); - const loadingSpinner = Utils.getElement('loadingSpinner'); - - if (submitButton) submitButton.disabled = isQueryingFees; - if (loadingSpinner) loadingSpinner.style.display = isQueryingFees ? "block" : "none"; - }, - - setSubmissionState(isSubmitting) { - const submitButton = Utils.getElement('submitButton'); - const loadingSpinner = Utils.getElement('loadingSpinner'); - - if (submitButton) submitButton.disabled = isSubmitting; - if (loadingSpinner) loadingSpinner.style.display = isSubmitting ? "block" : "none"; - }, - - async processSubmission(userData, paymentAccountID) { - let [caseData, uploadData] = await Promise.all([ - apiUtils.getCaseData(), - apiUtils.getUploadData() - ]); - - caseData = caseData.data.case_data; - - const efilingData = this.buildEFilingData(userData, caseData, uploadData, paymentAccountID); - - return await DataManager.fetchJSON(CONFIG.URLS.SUBMIT_FILING, { - method: 'POST', - body: JSON.stringify({ - efile_data: efilingData, - confirm_submission: true, - payment_account_id: paymentAccountID - }) - }); } }; -// buildEFilingData / addCourtBundles / createDocumentBundle / -// handleSubmissionResult / handleFeesResponse are shared with the other -// filing page. See filing-payload.js -- keep payload changes there so the -// fee quote and the submitted filing cannot drift apart. Object.assign(FilingHandler, FilingPayload); - -// Main application initialization -const ReviewApp = { - async init() { - await this.loadAllData(); - APIHandlers.fetchPartyType(); - }, - - async loadAllData() { - const caseData = DataManager.getCaseData(); - const friendlyNames = DataManager.getFriendlyNames(); - - // Update case info immediately if available - if (Object.keys(caseData).length > 0 || Object.keys(friendlyNames).length > 0) { - UIUpdater.updateCaseInfo(caseData, friendlyNames); - } - - // Load other data in parallel - await Promise.all([ - APIHandlers.loadUserInfo(), - APIHandlers.loadUploadData(), - ]); - await APIHandlers.loadPaymentAccounts() - } -}; - -// Global function exports for HTML onclick handlers -window.toggleEdit = FieldManager.toggleEdit.bind(FieldManager); -window.goBack = Navigation.goBack; -window.submitFiling = FilingHandler.submitFiling.bind(FilingHandler); -window.queryFees = FilingHandler.queryFees.bind(FilingHandler); -window.Navigation = Navigation; - -// Initialize app when DOM is ready -document.addEventListener("DOMContentLoaded", () => ReviewApp.init()); \ No newline at end of file +document.addEventListener("DOMContentLoaded", () => { + const confirmation = document.getElementById("confirm-filing"); + confirmation.addEventListener("change", () => FilingHandler.setSubmissionState(false)); + document.getElementById("submitButton").addEventListener("click", () => FilingHandler.submitFiling()); +}); \ No newline at end of file diff --git a/efile_app/efile/static/js/upload-documents.js b/efile_app/efile/static/js/upload-documents.js index f4f3d92..7ff8a93 100644 --- a/efile_app/efile/static/js/upload-documents.js +++ b/efile_app/efile/static/js/upload-documents.js @@ -44,7 +44,7 @@ const analyzingTimer = window.setTimeout(() => { stateTitle.textContent = "Analyzing your first PDF…"; - stateDetail.textContent = "We're looking for the court, case type, and case number."; + stateDetail.textContent = "We are looking for the court, case type, and case number."; }, 900); try { @@ -62,7 +62,7 @@ const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.error || "Upload failed."); stateTitle.textContent = "Your documents are ready"; - stateDetail.textContent = "Review what we found before continuing."; + stateDetail.textContent = "Review what we found before you continue."; window.setTimeout(() => window.location.reload(), 500); } catch (error) { state.hidden = true; diff --git a/efile_app/efile/templates/efile/case_confirmation.html b/efile_app/efile/templates/efile/case_confirmation.html index 030c0be..a872e3b 100644 --- a/efile_app/efile/templates/efile/case_confirmation.html +++ b/efile_app/efile/templates/efile/case_confirmation.html @@ -7,10 +7,10 @@
{% translate "Confirm case" %}
- +

{% translate "Is this your court case?" %}

-

{% translate "Check the details before adding this filing to the case." %}

+

{% translate "Check the details before you add this filing to the case." %}

@@ -65,17 +65,17 @@

{% translate "Is this your court case?" %}

{% csrf_token %}
{% translate "Does this match your case?" %} -

{% translate "Choosing Yes will attach your documents to this case." %}

+

{% translate "If you choose Yes, we will attach your documents to this case." %}

diff --git a/efile_app/efile/templates/efile/case_lookup.html b/efile_app/efile/templates/efile/case_lookup.html index a3b54fd..b15a658 100644 --- a/efile_app/efile/templates/efile/case_lookup.html +++ b/efile_app/efile/templates/efile/case_lookup.html @@ -16,11 +16,15 @@

{% translate "Find your court case" %}

- +
{% translate "Back" %} + href="{% url 'extraction_review' jurisdiction %}"> {% translate "Back" %}
diff --git a/efile_app/efile/templates/efile/case_questions.html b/efile_app/efile/templates/efile/case_questions.html index 02ad180..18d0032 100644 --- a/efile_app/efile/templates/efile/case_questions.html +++ b/efile_app/efile/templates/efile/case_questions.html @@ -17,7 +17,9 @@

{% translate "A few questions about your case" %}

{{ question.label }} - {% if question.required %}{% endif %} + {% if question.required %} + — {% translate "required" %} + {% endif %} {% if question.type == "radio" %}
@@ -52,11 +54,9 @@

{% translate "A few questions about your case" %}

{% endfor %} {% if show_amount_field %}
- - {% translate "Amount in controversy" %} - + {% translate "Amount in controversy" %} — {% translate "required" %}

- {% translate "The dollar amount at stake in this case. The court requires this for the filing type you selected." %} + {% translate "The amount of money involved in this case. The court needs this for the filing type you chose." %}

$ @@ -73,15 +73,15 @@

{% translate "A few questions about your case" %}

{% if return_to == "review" %} {% translate "Back to review" %} + href="{% url 'case_review' jurisdiction %}"> {% translate "Back to review" %} {% else %} {% translate "Back" %} + href="{% url 'parties' jurisdiction %}"> {% translate "Back" %} {% endif %}
diff --git a/efile_app/efile/templates/efile/components/workflow_progress.html b/efile_app/efile/templates/efile/components/workflow_progress.html index 4bf78e7..262130e 100644 --- a/efile_app/efile/templates/efile/components/workflow_progress.html +++ b/efile_app/efile/templates/efile/components/workflow_progress.html @@ -1,3 +1,4 @@ +{% load i18n %}
- +
{% if return_to == "review" %} {% translate "Back to review" %} + href="{% url 'case_review' jurisdiction %}"> {% translate "Back to review" %} {% else %} {% translate "Back" %} + href="{% url 'upload_documents' jurisdiction %}"> {% translate "Back" %} {% endif %}
diff --git a/efile_app/efile/templates/efile/filing_path.html b/efile_app/efile/templates/efile/filing_path.html index 14d248e..ba533c9 100644 --- a/efile_app/efile/templates/efile/filing_path.html +++ b/efile_app/efile/templates/efile/filing_path.html @@ -20,7 +20,7 @@

{% translate "What are you trying to do?" %}

value="new" {% if selected_path == "new" %}checked{% endif %} required /> - + {% translate "Start a new court case" %}{% translate "The court has not assigned a case number yet." %}
{% translate "Back" %} + href="{% url 'efile_options' jurisdiction %}"> {% translate "Back" %}
diff --git a/efile_app/efile/templates/efile/options.html b/efile_app/efile/templates/efile/options.html index 8d6ea74..af1dbdf 100644 --- a/efile_app/efile/templates/efile/options.html +++ b/efile_app/efile/templates/efile/options.html @@ -103,7 +103,7 @@

{% translate "View past filings" %}

}); } else { const resumeUrl = JSON.parse(document.getElementById("resume-url").textContent); - window.location.href = resumeUrl || `/jurisdiction/{{jurisdiction}}/upload_first/`; + window.location.href = resumeUrl || `/jurisdiction/{{jurisdiction}}/filing-path/`; } } @@ -133,7 +133,7 @@

{% translate "View past filings" %}

console.warn('Error creating draft; falling back to session start:', error); await makeNewCaseData(); return { - redirect_url: `/jurisdiction/{{jurisdiction}}/upload_first/?clear_session=true&from_options=true` + redirect_url: `/jurisdiction/{{jurisdiction}}/filing-path/` }; } } diff --git a/efile_app/efile/templates/efile/organize_documents.html b/efile_app/efile/templates/efile/organize_documents.html index f30af5b..9b8275c 100644 --- a/efile_app/efile/templates/efile/organize_documents.html +++ b/efile_app/efile/templates/efile/organize_documents.html @@ -9,20 +9,18 @@
{% translate "Organize documents" %}

{% translate "Organize your documents" %}

- {% translate "Tell the court what each PDF is and whether it should be public or confidential. You can also rename and reorder additional documents." %} + {% translate "Tell the court what each PDF is and if it should be public or confidential. You can also rename and reorder additional documents." %}

- +
{% csrf_token %}
{% translate "Which PDF is the main document?" %} {% if documents|length == 1 %} -

{% translate "This is the only document in this filing, so it's the main document." %}

+

{% translate "This is the only document in this filing, so it is the main document." %}

{% for document in documents %}{% endfor %} {% else %} -

- {% translate "Choose the document that starts or drives this filing, such as a petition. Upload order does not matter." %} -

+

{% translate "Choose the document that starts this filing, like a petition. Upload order does not matter." %}

{% for document in documents %}
{% if not party_types %}

- {% translate "Court party roles could not be loaded. Refresh this page before continuing." %} + {% translate "We could not load the court party roles. Refresh this page before you continue." %}

{% endif %}
{% for item in roster %}
- + {% if item.party.organization_name %} @@ -110,7 +110,7 @@

{% translate "Party list" %}

{% endif %} @@ -120,10 +120,10 @@

{% translate "Party list" %}

{% if return_to == "review" %} {% translate "Back to review" %} + href="{% url 'case_review' jurisdiction %}"> {% translate "Back to review" %} {% else %} {% translate "Back" %} + href="{% url 'your_information' jurisdiction %}"> {% translate "Back" %} {% endif %}
diff --git a/efile_app/efile/templates/efile/party_details.html b/efile_app/efile/templates/efile/party_details.html index 6c28e64..baafd54 100644 --- a/efile_app/efile/templates/efile/party_details.html +++ b/efile_app/efile/templates/efile/party_details.html @@ -14,9 +14,7 @@

{% translate "Add the next party" %} {% endif %}

-

- {% translate "Enter this person's or organization's court role, name, and mailing address." %} -

+

{% translate "Enter the court role, name, and mailing address for this party." %}

{% csrf_token %} @@ -64,7 +62,10 @@

{% translate "Name" %}

@@ -156,14 +161,14 @@

{% translate "Back to party list" %} + href="{% url 'parties' jurisdiction %}{% if return_to %}?return_to={{ return_to }}{% endif %}"> {% translate "Back to party list" %}
diff --git a/efile_app/efile/templates/efile/payment.html b/efile_app/efile/templates/efile/payment.html index f5844ca..0375785 100644 --- a/efile_app/efile/templates/efile/payment.html +++ b/efile_app/efile/templates/efile/payment.html @@ -1,81 +1,72 @@ +{% extends "efile/workflow_base.html" %} {% load static %} {% load i18n %} - - - - - - Review Case Details - - - - - - - {% csrf_token %} - {% include "efile/components/profile_header.html" %} -
-
-

{% translate "Payment information" %}

-

{% translate "Choose how you want to pay the filing fees to the court." %}

-
-

{% translate "Choose your payment method" %}

- - -
-

{% translate "Existing" %}

-
- -
-
- - - -
- - -
-
- - -
- -
-
- Loading... -
-

{% translate "Calculating cost of filing..." %}

-
- -
- - +{% block title %} + {% translate "Payment information" %} +{% endblock title %} +{% block extra_css %} + +{% endblock extra_css %} +{% block workflow_content %} +
+
{% translate "Fees" %}
+

{% translate "Choose how to pay court fees" %}

+

+ {% translate "Choose a saved payment method. We will calculate the court's fees before you continue." %} +

+
+ {% csrf_token %} + + + + + +
+ {% translate "Payment method" %} +
+
+ + {% translate "Loading payment methods…" %}
+
+ + + +
+
+ {% translate "Loading" %} +
+

{% translate "Calculating court fees…" %}

+
+
+ + {% translate "Back" %} + +
-
- {% include "efile/components/footer.html" %} - - - - {{ case_data|json_script:"case-data" }} - {{ new_toga_url|json_script:"new-toga-url" }} - - - - - - - - + + + {{ case_data|json_script:"case-data" }} + {{ new_toga_url|json_script:"new-toga-url" }} + {{ selected_payment_account_id|json_script:"selected-payment-account-id" }} +{% endblock workflow_content %} +{% block extra_js %} + + +{% endblock extra_js %} diff --git a/efile_app/efile/templates/efile/review.html b/efile_app/efile/templates/efile/review.html index 3c11a1d..4f3371a 100644 --- a/efile_app/efile/templates/efile/review.html +++ b/efile_app/efile/templates/efile/review.html @@ -1,259 +1,221 @@ +{% extends "efile/workflow_base.html" %} {% load static %} {% load i18n %} - - - - - - Review Case Details - - - - - - - {% csrf_token %} - {% include "efile/components/profile_header.html" %} -
-
-

{% translate "Review case details" %}

-

- {% translate "Verify the information below for your case. Please edit anything you believe to be incorrect." %} -

- -
-
-

{% translate "Case information" %}

+{% block title %} + {% translate "Review your filing" %} +{% endblock title %} +{% block extra_css %} + +{% endblock extra_css %} +{% block workflow_content %} +
+
{% translate "Review" %}
+

{% translate "Review your filing" %}

+

+ {% translate "Check each section carefully. Use Edit to go back to the screen where you entered it." %} +

+
+
+
+

{% translate "Case" %}

+ {% translate "Edit" %} +
+
+
+
{% translate "Filing path" %}
+
+ {{ draft.get_existing_case_display }} +
-
- Case type: -
- {{ friendly_names.case_type|default:"Loading..." }} - Edit -
+ {% if draft.case_title %} +
+
{% translate "Case name" %}
+
+ {{ draft.case_title }} +
+
+ {% endif %} + {% if draft.docket_number %} +
+
{% translate "Case number" %}
+
+ {{ draft.docket_number }} +
+
+ {% endif %} +
+
{% translate "Court" %}
+
+ {{ draft.court_name|default:draft.court_code }} +
-
- To be filed in: -
- {{ friendly_names.court|default:"Loading..." }} - Edit -
+
+
{% translate "Category" %}
+
+ {{ draft.case_category_name|default:draft.case_category_code }} +
-
- -
-

{% translate "Review your information" %}

- -
- {% translate "Name:" %}* -
- {% translate "Loading..." %} - - -
+
+
{% translate "Case type" %}
+
+ {{ draft.case_type_name|default:draft.case_type_code }} +
-
- {% translate "Address line 1:" %} - * -
- {% translate "Loading..." %} - - -
-
-
- {% translate "Address line 2" %}: -
- {% translate "Loading..." %} - - -
-
-
- {% translate "City:" %}* -
- {% translate "Loading..." %} - - -
-
-
- {% translate "State:" %}* -
- {% translate "Loading..." %} - - -
-
-
- {% translate "ZIP code" %}: * -
- {% translate "Loading..." %} - - -
-
-
- {% translate "Email" %} - * -
- {% translate "Loading..." %} - - -
-
-
- Phone: * -
- Loading... - - -
-
-
- -
-

{% translate "Your documents for filing" %}

-
- -
-
- -

{% translate "Payment method" %}

-
-
- -
-
- - - -

{% translate "Submit my filing" %}

-
-
- - -
-
- -
- - -
-
- - -
- -
-
- Loading... +
+
+
+
+

{% translate "Documents" %}

+ {% translate "Edit" %} +
+
+ {% for document in documents %} +
+ +
+ {{ document.name|default:document.original_filename }} + + {% if document.role == "lead" %} + {% translate "Main document" %} + {% else %} + {% translate "Additional document" %} + {% endif %} + {% if document.document_type_name %}· {{ document.document_type_name }}{% endif %} + + {% if document.filing_type_name %} + {% translate "Filing type:" %} {{ document.filing_type_name }} + {% endif %}
-

{% translate "Processing your filing..." %}

-
- -
- - -
-
+
+ {% endfor %} - {% include "efile/components/footer.html" %} - - - - {{ case_data|json_script:"case-data" }} - {{ new_toga_url|json_script:"new-toga-url" }} - {{ friendly_names|json_script:"friendly-names" }} - - - - - - - - - + +
+
+

{% translate "Your information" %}

+ {% translate "Edit" %} +
+ {% if filer %} +

{{ filer.first_name }} {{ filer.middle_name }} {{ filer.last_name }}

+
+ {{ filer.address_line_1 }} + {% if filer.address_line_2 %}, {{ filer.address_line_2 }}{% endif %} +
+ {{ filer.city }}, {{ filer.state }} {{ filer.zip_code }} +
+ {{ filer.email }} + {% if filer.phone %} +
+ {{ filer.phone }} + {% endif %} +
+ {{ filer.party_type_name|default:filer.party_type }} + {% endif %} +
+
+
+

{% translate "Other people" %}

+ {% translate "Edit" %} +
+ {% for party in parties %} +
+ + {% if party.organization_name %} + {{ party.organization_name }} + {% else %} + {{ party.first_name }} {{ party.middle_name }} {{ party.last_name }} + {% endif %} + + {{ party.party_type_name|default:party.party_type }} +
+ {% empty %} +

{% translate "No additional people are listed." %}

+ {% endfor %} +
+ {% if question_answers %} +
+
+

{% translate "Case questions" %}

+ {% translate "Edit" %} +
+
+ {% for answer in question_answers %} +
+
{{ answer.label }}
+
+ {{ answer.value }} +
+
+ {% endfor %} +
+
+ {% endif %} +
+
+

{% translate "Payment" %}

+ {% translate "Edit" %} +
+

{{ draft.selected_payment_account_name }}

+ {% if draft.selected_payment_account_type == "WV" %} + {% translate "This filing has an approved fee waiver. No payment will be charged." %} + {% elif draft.quoted_fee_total %} +
+
+
{% translate "Total" %}
+
+ ${{ draft.quoted_fee_total }} +
+
+
+ {% if draft.quoted_fee_breakdown %} + + {% endif %} + {% else %} + {% translate "Confirm the fee amount on the Payment step." %} + {% endif %} +
+ + + +
+ +
+
+
+ {% translate "Submitting" %} +
+

{% translate "We are sending your filing to the court. Do not close this page." %}

+
+
+ {% translate "Back" %} + +
+ + {{ case_data|json_script:"case-data" }} + {{ upload_data|json_script:"upload-data" }} + {{ draft.selected_payment_account_id|json_script:"payment-account-id" }} +{% endblock workflow_content %} +{% block extra_js %} + + +{% endblock extra_js %} diff --git a/efile_app/efile/templates/efile/upload_documents.html b/efile_app/efile/templates/efile/upload_documents.html index 2288e19..64ef6bd 100644 --- a/efile_app/efile/templates/efile/upload_documents.html +++ b/efile_app/efile/templates/efile/upload_documents.html @@ -12,9 +12,9 @@

{% translate "Upload your court documents" %}

{% translate "Add the forms and other documents you want to file. We will analyze the first PDF, and you can choose the main document later." %}

- {% translate "PDF only" %} - {% translate "10 MB per file" %} - {% translate "Text must be readable" %} + {% translate "PDF only" %} + {% translate "10 MB per file" %} + {% translate "Text must be readable" %}
{% csrf_token %} @@ -24,7 +24,7 @@

{% translate "Upload your court documents" %}

name="documents" accept=".pdf,application/pdf" multiple /> - + {% translate "Choose PDFs or drag them here" %} {% translate "You can select more than one file." %} @@ -33,7 +33,7 @@

{% translate "Upload your court documents" %}

{% translate "Uploading your documents…" %} {% translate "Keep this page open." %} - + {% endfor %} {% else %}
- +

{% translate "No documents uploaded yet." %}

{% endif %}
{% translate "Back" %} + href="{% url 'filing_path' jurisdiction %}"> {% translate "Back" %} {% translate "Review what we found" %} + href="{% url 'extraction_review' jurisdiction %}" + {% if not has_lead_document %}aria-disabled="true" tabindex="-1"{% endif %}>{% translate "Review what we found" %}
{% endblock workflow_content %} diff --git a/efile_app/efile/templates/efile/your_information.html b/efile_app/efile/templates/efile/your_information.html index 2d6990e..2fc6b26 100644 --- a/efile_app/efile/templates/efile/your_information.html +++ b/efile_app/efile/templates/efile/your_information.html @@ -123,15 +123,15 @@

{% translate "Contact information" %}

{% if return_to == "review" %} {% translate "Back to review" %} + href="{% url 'case_review' jurisdiction %}"> {% translate "Back to review" %} {% else %} {% translate "Back" %} + href="{% url 'organize_documents' jurisdiction %}"> {% translate "Back" %} {% endif %}
diff --git a/efile_app/efile/tests/test_durable_drafts.py b/efile_app/efile/tests/test_durable_drafts.py index 01319c4..7816a1b 100644 --- a/efile_app/efile/tests/test_durable_drafts.py +++ b/efile_app/efile/tests/test_durable_drafts.py @@ -354,11 +354,11 @@ def test_options_page_points_resume_to_draft_workflow_step(client, django_user_m response = client.get(reverse("efile_options", kwargs={"jurisdiction": "illinois"})) assert response.status_code == 200 - assert reverse("upload", kwargs={"jurisdiction": "illinois"}).encode() in response.content + assert reverse("organize_documents", kwargs={"jurisdiction": "illinois"}).encode() in response.content @pytest.mark.django_db -def test_documents_page_returns_to_lead_upload_when_lead_is_missing(client, django_user_model): +def test_legacy_documents_url_redirects_into_reorganized_document_flow(client, django_user_model): user = django_user_model.objects.create_user(username="missing-lead-user", tyler_jurisdiction="illinois") draft = FilingDraft.objects.create(user=user, jurisdiction="illinois") write_case_data(draft, {"court": "cook:cd", "case_type": "Name Change"}) @@ -371,7 +371,7 @@ def test_documents_page_returns_to_lead_upload_when_lead_is_missing(client, djan response = client.get(reverse("upload", kwargs={"jurisdiction": "illinois"})) assert response.status_code == 302 - assert response.url == reverse("upload_first", kwargs={"jurisdiction": "illinois"}) + assert response.url == reverse("organize_documents", kwargs={"jurisdiction": "illinois"}) @pytest.mark.django_db diff --git a/efile_app/efile/tests/test_review_submit_flow.py b/efile_app/efile/tests/test_review_submit_flow.py new file mode 100644 index 0000000..8e267e9 --- /dev/null +++ b/efile_app/efile/tests/test_review_submit_flow.py @@ -0,0 +1,229 @@ +import pytest +from django.urls import reverse + +from efile.models import FilingDocument, FilingDraft, FilingParty +from efile.services.current_drafts import CURRENT_DRAFT_SESSION_KEY +from efile.workflow import WorkflowStepKey + + +@pytest.fixture +def submission_draft(client, django_user_model): + user = django_user_model.objects.create_user(username="review-user", tyler_jurisdiction="illinois") + draft = FilingDraft.objects.create( + user=user, + jurisdiction="illinois", + workflow_version=2, + current_step=WorkflowStepKey.PAYMENT, + existing_case="new", + court_code="cook:law1", + court_name="Circuit Court of Cook County", + case_category_code="civil", + case_category_name="Civil", + case_type_code="contract", + case_type_name="Contract", + document_checklist_acknowledged=True, + ) + FilingDocument.objects.create( + draft=draft, + role=FilingDocument.Role.LEAD, + sort_order=0, + name="Petition.pdf", + filing_type_code="petition", + filing_type_name="Petition", + document_type_code="public", + document_type_name="Public", + filing_component_code="lead", + filing_component_name="Lead document", + ) + FilingParty.objects.create( + draft=draft, + role="filer", + sort_order=0, + party_type="PLA", + party_type_name="Plaintiff/Petitioner", + first_name="Jordan", + last_name="Taylor", + email="jordan@example.com", + address_line_1="123 Main Street", + city="Springfield", + state="IL", + zip_code="62701", + ) + client.force_login(user) + session = client.session + session[CURRENT_DRAFT_SESSION_KEY] = draft.pk + session["jurisdiction"] = "illinois" + session["auth_tokens"] = {"TYLER-TOKEN-ILLINOIS": "test-token"} + session.save() + return draft + + +@pytest.mark.django_db +def test_payment_saves_account_and_advances_durable_step(client, submission_draft): + response = client.post( + reverse("payment", kwargs={"jurisdiction": "illinois"}), + {"selected_payment_account": "pay-123", "selected_payment_account_name": "Card ending in 4242"}, + ) + + assert response.status_code == 302 + assert response.url == reverse("case_review", kwargs={"jurisdiction": "illinois"}) + submission_draft.refresh_from_db() + assert submission_draft.selected_payment_account_id == "pay-123" + assert submission_draft.current_step == WorkflowStepKey.REVIEW + + +class _PaymentAccountTypesResponse: + status_code = 200 + + @staticmethod + def json(): + return [ + {"code": "CC", "description": "Credit Card"}, + {"code": "WV", "description": "Waiver"}, + ] + + +@pytest.mark.django_db +def test_payment_account_types_proxies_the_courts_type_list(client, submission_draft, monkeypatch): + monkeypatch.setattr( + "efile.api.auth_views.requests.get", + lambda *args, **kwargs: _PaymentAccountTypesResponse(), + ) + + response = client.get(reverse("api:payment_account_types"), {"jurisdiction": "illinois"}) + + assert response.status_code == 200 + body = response.json() + assert body["success"] is True + assert {"code": "CC", "description": "Credit Card"} in body["data"] + + +@pytest.mark.django_db +def test_payment_persists_account_type_and_quoted_fees(client, submission_draft): + response = client.post( + reverse("payment", kwargs={"jurisdiction": "illinois"}), + { + "selected_payment_account": "pay-123", + "selected_payment_account_name": "Card ending in 4242", + "selected_payment_account_type": "CC", + "quoted_fee_total": "125.00", + "quoted_fee_breakdown": '[{"label": "Filing fee", "amount": "100.00"}, {"label": "Technology fee", "amount": "25.00"}]', + }, + ) + + assert response.status_code == 302 + submission_draft.refresh_from_db() + assert submission_draft.selected_payment_account_type == "CC" + assert submission_draft.quoted_fee_total == "125.00" + assert submission_draft.quoted_fee_breakdown == [ + {"label": "Filing fee", "amount": "100.00"}, + {"label": "Technology fee", "amount": "25.00"}, + ] + + +@pytest.mark.django_db +def test_payment_tolerates_malformed_fee_breakdown(client, submission_draft): + response = client.post( + reverse("payment", kwargs={"jurisdiction": "illinois"}), + { + "selected_payment_account": "pay-123", + "selected_payment_account_name": "Payment waiver", + "selected_payment_account_type": "WV", + "quoted_fee_breakdown": "not json", + }, + ) + + assert response.status_code == 302 + submission_draft.refresh_from_db() + assert submission_draft.selected_payment_account_type == "WV" + assert submission_draft.quoted_fee_breakdown == [] + + +@pytest.mark.django_db +def test_review_shows_waiver_messaging_instead_of_fee_reference(client, submission_draft): + submission_draft.selected_payment_account_id = "pay-123" + submission_draft.selected_payment_account_name = "Payment waiver" + submission_draft.selected_payment_account_type = "WV" + submission_draft.save( + update_fields=["selected_payment_account_id", "selected_payment_account_name", "selected_payment_account_type"] + ) + + response = client.get(reverse("case_review", kwargs={"jurisdiction": "illinois"})) + + assert response.status_code == 200 + assert b"fee waiver" in response.content + assert b"the previous screen" not in response.content + + +@pytest.mark.django_db +def test_review_shows_previously_calculated_fee_total(client, submission_draft): + submission_draft.selected_payment_account_id = "pay-123" + submission_draft.selected_payment_account_name = "Card ending in 4242" + submission_draft.selected_payment_account_type = "CC" + submission_draft.quoted_fee_total = "125.00" + submission_draft.quoted_fee_breakdown = [{"label": "Filing fee", "amount": "125.00"}] + submission_draft.save( + update_fields=[ + "selected_payment_account_id", + "selected_payment_account_name", + "selected_payment_account_type", + "quoted_fee_total", + "quoted_fee_breakdown", + ] + ) + + response = client.get(reverse("case_review", kwargs={"jurisdiction": "illinois"})) + + assert response.status_code == 200 + assert b"125.00" in response.content + assert b"Filing fee" in response.content + assert b"the previous screen" not in response.content + + +@pytest.mark.django_db +def test_review_uses_new_edit_routes_and_durable_summary(client, submission_draft): + submission_draft.selected_payment_account_id = "pay-123" + submission_draft.selected_payment_account_name = "Card ending in 4242" + submission_draft.save(update_fields=["selected_payment_account_id", "selected_payment_account_name"]) + + response = client.get(reverse("case_review", kwargs={"jurisdiction": "illinois"})) + + assert response.status_code == 200 + assert b"Jordan" in response.content + assert b"Petition.pdf" in response.content + assert b"Filing type:" in response.content + assert b"review-document-tag" in response.content + assert reverse("organize_documents", kwargs={"jurisdiction": "illinois"}).encode() in response.content + assert reverse("your_information", kwargs={"jurisdiction": "illinois"}).encode() in response.content + assert reverse("expert_form", kwargs={"jurisdiction": "illinois"}).encode() not in response.content + assert reverse("upload", kwargs={"jurisdiction": "illinois"}).encode() not in response.content + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("route", "target"), + [ + ("upload_first", "upload_documents"), + ("expert_form", "extraction_review"), + ("upload", "organize_documents"), + ], +) +def test_retired_screen_urls_redirect_forward(client, submission_draft, route, target): + response = client.get(reverse(route, kwargs={"jurisdiction": "illinois"})) + + assert response.status_code == 302 + assert response.url == reverse(target, kwargs={"jurisdiction": "illinois"}) + + +@pytest.mark.django_db +def test_confirmation_uses_saved_submission_reference(client, submission_draft): + submission_draft.mark_submitted({"confirmationNumber": "IL-2026-12345"}) + session = client.session + session["last_submitted_filing_draft_id"] = submission_draft.pk + session.save() + + response = client.get(reverse("filing_confirmation", kwargs={"jurisdiction": "illinois"})) + + assert response.status_code == 200 + assert b"IL-2026-12345" in response.content + assert b"Circuit Court of Cook County" in response.content diff --git a/efile_app/efile/tests/test_workflow.py b/efile_app/efile/tests/test_workflow.py index ac7c548..abfbcb4 100644 --- a/efile_app/efile/tests/test_workflow.py +++ b/efile_app/efile/tests/test_workflow.py @@ -5,7 +5,6 @@ from efile.workflow import ( FILING_WORKFLOW, - LEGACY_WORKFLOW, ExistingCase, WorkflowStepKey, get_next_step, @@ -58,25 +57,12 @@ def test_target_workflow_declares_every_reorganized_screen(): ] -def test_legacy_drafts_keep_the_current_linear_route_during_migration(): - legacy_draft = draft(current_step=WorkflowStepKey.DOCUMENTS, workflow_version=1) +def test_every_draft_uses_the_canonical_workflow_after_migration(): + pre_migration_version = draft(current_step=WorkflowStepKey.PAYMENT, workflow_version=1) - assert get_visible_workflow(legacy_draft) == LEGACY_WORKFLOW - assert get_previous_step(WorkflowStepKey.PAYMENT, legacy_draft).key == WorkflowStepKey.DOCUMENTS - assert get_next_step(WorkflowStepKey.PAYMENT, legacy_draft).key == WorkflowStepKey.REVIEW - - -@pytest.mark.parametrize( - "shared_step", - [ - WorkflowStepKey.OPTIONS, - WorkflowStepKey.PAYMENT, - WorkflowStepKey.REVIEW, - WorkflowStepKey.CONFIRMATION, - ], -) -def test_shared_steps_without_draft_context_default_to_legacy(shared_step): - assert get_visible_workflow(current_step=shared_step) == LEGACY_WORKFLOW + assert get_visible_workflow(pre_migration_version) != () + assert get_previous_step(WorkflowStepKey.PAYMENT, pre_migration_version).key == WorkflowStepKey.PARTIES + assert get_next_step(WorkflowStepKey.PAYMENT, pre_migration_version).key == WorkflowStepKey.REVIEW @pytest.mark.parametrize( @@ -121,8 +107,26 @@ def test_unsure_case_stays_on_extraction_review(): def test_party_details_only_appear_for_incomplete_parties(): - incomplete = SimpleNamespace(first_name="Ada", last_name="", organization_name="") - complete = SimpleNamespace(first_name="Ada", last_name="Lovelace", organization_name="") + incomplete = SimpleNamespace( + party_type="PLA", + first_name="Ada", + last_name="", + organization_name="", + address_line_1="1 Main St", + city="Chicago", + state="IL", + zip_code="60601", + ) + complete = SimpleNamespace( + party_type="PLA", + first_name="Ada", + last_name="Lovelace", + organization_name="", + address_line_1="1 Main St", + city="Chicago", + state="IL", + zip_code="60601", + ) assert WorkflowStepKey.PARTY_DETAILS in keys(get_visible_workflow(draft(parties=[incomplete]))) assert WorkflowStepKey.PARTY_DETAILS not in keys(get_visible_workflow(draft(parties=[complete]))) @@ -143,18 +147,18 @@ def test_get_step_url_reverses_an_available_route(): assert get_step_url(WorkflowStepKey.PAYMENT, "illinois") == expected_url -def test_resume_preserves_legacy_draft_routes(): - expected_url = reverse("upload", kwargs={"jurisdiction": "illinois"}) +def test_resume_maps_legacy_document_step_into_reorganized_flow(): + expected_url = reverse("organize_documents", kwargs={"jurisdiction": "illinois"}) assert get_resume_step_url(WorkflowStepKey.DOCUMENTS, "illinois") == expected_url -def test_resume_skips_options_for_pre_migration_drafts(): - expected_url = reverse("upload_first", kwargs={"jurisdiction": "illinois"}) +def test_resume_skips_options_for_saved_drafts(): + expected_url = reverse("filing_path", kwargs={"jurisdiction": "illinois"}) assert get_resume_step_url(WorkflowStepKey.OPTIONS, "illinois") == expected_url def test_resume_falls_back_for_an_unrecognised_step(): - expected_url = reverse("upload_first", kwargs={"jurisdiction": "illinois"}) + expected_url = reverse("filing_path", kwargs={"jurisdiction": "illinois"}) assert get_resume_step_url("a_step_that_was_removed", "illinois") == expected_url @@ -163,19 +167,21 @@ def test_resume_returns_none_without_a_draft(): def test_workflow_context_uses_draft_branch_and_includes_stage_progress(): - legacy_draft = draft(current_step=WorkflowStepKey.PAYMENT, workflow_version=1) - context = get_workflow_context(WorkflowStepKey.PAYMENT, "illinois", legacy_draft) + current_draft = draft(current_step=WorkflowStepKey.PAYMENT) + context = get_workflow_context(WorkflowStepKey.PAYMENT, "illinois", current_draft) assert context["workflow_current_step"].key == WorkflowStepKey.PAYMENT - assert context["workflow_previous_step"].key == WorkflowStepKey.DOCUMENTS + assert context["workflow_previous_step"].key == WorkflowStepKey.PARTIES assert context["workflow_next_step"].key == WorkflowStepKey.REVIEW - assert context["workflow_previous_url"] == reverse("upload", kwargs={"jurisdiction": "illinois"}) + assert context["workflow_previous_url"] == reverse("parties", kwargs={"jurisdiction": "illinois"}) assert context["workflow_next_url"] == reverse("case_review", kwargs={"jurisdiction": "illinois"}) assert [stage.value for stage in context["workflow_stages"]] == [ "filing", "upload", "confirm_case", + "check_documents", "organize_documents", + "people", "fees", "review", ] diff --git a/efile_app/efile/tests/tests.py b/efile_app/efile/tests/tests.py index 652c078..b1b2194 100644 --- a/efile_app/efile/tests/tests.py +++ b/efile_app/efile/tests/tests.py @@ -408,12 +408,11 @@ def authenticated_client(self, client, db): return client def test_expert_form_page_loads(self, authenticated_client): - """Test that the expert form page loads correctly.""" + """The retired expert form URL safely returns users to the active flow.""" response = authenticated_client.get("/jurisdiction/illinois/expert_form/") - assert response.status_code == 200 - assert b"Case Information" in response.content or b"Expert Form" in response.content - assert b"cascading-dropdowns.js" in response.content or b"dynamic-form-sections.js" in response.content + assert response.status_code == 302 + assert response.url == "/jurisdiction/illinois/options/" @patch("efile.api.dropdown_views.requests.get") def test_complete_dropdown_flow(self, mock_get, authenticated_client): diff --git a/efile_app/efile/urls.py b/efile_app/efile/urls.py index f5ebb12..2809b14 100644 --- a/efile_app/efile/urls.py +++ b/efile_app/efile/urls.py @@ -10,10 +10,10 @@ from .views.confirmation import filing_confirmation from .views.document_checklist import document_checklist from .views.draft_views import create_draft_view, get_current_draft_view -from .views.expert_form import efile_expert_form from .views.extraction_review import extraction_review from .views.filing_path import filing_path from .views.filing_statuses import filing_statuses +from .views.legacy_workflow import legacy_workflow_redirect from .views.login import efile_login, efile_logout, efile_password_reset from .views.options import efile_options from .views.organize_documents import organize_documents @@ -33,9 +33,7 @@ save_upload_first_data, ) from .views.submission import submit_final_filing -from .views.upload import efile_upload from .views.upload_documents import upload_documents -from .views.upload_first import efile_upload_first from .views.your_information import your_information @@ -70,9 +68,24 @@ def jurisdiction_homepage(request, jurisdiction): path("jurisdiction//case-questions/", case_questions, name="case_questions"), path("jurisdiction//drafts/", create_draft_view, name="create_draft"), path("jurisdiction//filing_statuses/", filing_statuses, name="filing_statuses"), - path("jurisdiction//expert_form/", efile_expert_form, name="expert_form"), - path("jurisdiction//upload_first/", efile_upload_first, name="upload_first"), - path("jurisdiction//upload/", efile_upload, name="upload"), + path( + "jurisdiction//expert_form/", + legacy_workflow_redirect, + {"destination": "expert_form"}, + name="expert_form", + ), + path( + "jurisdiction//upload_first/", + legacy_workflow_redirect, + {"destination": "upload_first"}, + name="upload_first", + ), + path( + "jurisdiction//upload/", + legacy_workflow_redirect, + {"destination": "upload"}, + name="upload", + ), path("jurisdiction//payment/", efile_payment, name="payment"), path("jurisdiction//review/", case_review, name="case_review"), path("jurisdiction//filing-confirmation/", filing_confirmation, name="filing_confirmation"), diff --git a/efile_app/efile/views/confirmation.py b/efile_app/efile/views/confirmation.py index 424f2d1..a29e15f 100644 --- a/efile_app/efile/views/confirmation.py +++ b/efile_app/efile/views/confirmation.py @@ -1,24 +1,53 @@ -from django.shortcuts import render +from django.contrib import messages +from django.shortcuts import redirect, render from efile.api.suffolk_api_views import get_tyler_token +from efile.models import FilingDraft from ..workflow import WorkflowStepKey, get_workflow_context +LAST_SUBMITTED_DRAFT_SESSION_KEY = "last_submitted_filing_draft_id" -def filing_confirmation(request, jurisdiction): - """Confirmation page after successful filing submission.""" - # You can add logic here to retrieve filing details from session - # or from database if you're storing submitted filings - is_logged_in = request.user.is_authenticated - if not get_tyler_token(request, jurisdiction): - is_logged_in = False +def _confirmation_number(response): + if isinstance(response, dict): + for key in ("confirmation_number", "confirmationNumber", "filing_id", "filingId", "id"): + if response.get(key): + return str(response[key]) + values = response.values() + elif isinstance(response, list): + values = response + else: + return "" + for value in values: + found = _confirmation_number(value) + if found: + return found + return "" + +def filing_confirmation(request, jurisdiction): + """Show the submitted durable draft and external confirmation reference.""" + if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): + return redirect("efile_login", jurisdiction=jurisdiction) + + submitted = FilingDraft.objects.filter( + user=request.user, + jurisdiction=jurisdiction, + status=FilingDraft.Status.SUBMITTED, + ) + draft_id = request.session.get(LAST_SUBMITTED_DRAFT_SESSION_KEY) + draft = submitted.filter(pk=draft_id).first() if draft_id else None + if draft is None: + draft = submitted.order_by("-submitted_at", "-updated_at").first() + if draft is None: + messages.info(request, "No submitted filing was found for this confirmation page.") + return redirect("filing_statuses", jurisdiction=jurisdiction) context = { - "is_logged_in": is_logged_in, - "page_title": "Filing Confirmation", - "success_message": "Your filing has been successfully submitted!", + "is_logged_in": True, + "page_title": "Filing confirmation", + "draft": draft, + "confirmation_number": _confirmation_number(draft.submission_response), } - context.update(get_workflow_context(WorkflowStepKey.CONFIRMATION, jurisdiction)) - + context.update(get_workflow_context(WorkflowStepKey.CONFIRMATION, jurisdiction, draft)) return render(request, "efile/confirmation.html", context) diff --git a/efile_app/efile/views/legacy_workflow.py b/efile_app/efile/views/legacy_workflow.py index 166e045..ca2753a 100644 --- a/efile_app/efile/views/legacy_workflow.py +++ b/efile_app/efile/views/legacy_workflow.py @@ -1,24 +1,26 @@ +from django.contrib import messages from django.shortcuts import redirect from efile.services.current_drafts import get_current_draft +from efile.workflow import WorkflowStepKey, get_resume_step_url, get_step_url -def legacy_workflow_redirect(request, jurisdiction, destination): - """Temporary bridges replaced screen-by-screen by the stacked migration.""" +def legacy_workflow_redirect(request, jurisdiction, destination=None): + """Send old workflow URLs forward without restoring retired screens.""" draft = get_current_draft(request, jurisdiction=jurisdiction) - if draft is not None and draft.workflow_version != 1: - draft.workflow_version = 1 + if draft is None: + return redirect("efile_options", jurisdiction=jurisdiction) + + if draft.workflow_version != 2: + draft.workflow_version = 2 draft.save(update_fields=["workflow_version", "updated_at"]) - route = { - "case_lookup": "expert_form", - "case_confirmation": "expert_form", - "document_checklist": "expert_form", - "organize_documents": "upload", - "your_information": "expert_form", - "parties": "expert_form", - "party_details": "expert_form", - "case_questions": "expert_form", - }[destination] - return redirect(route, jurisdiction=jurisdiction) + target = { + "upload_first": WorkflowStepKey.UPLOAD_DOCUMENTS, + "expert_form": WorkflowStepKey.EXTRACTION_REVIEW, + "upload": WorkflowStepKey.ORGANIZE_DOCUMENTS, + }.get(destination) + url = get_step_url(target, jurisdiction) if target else get_resume_step_url(draft.current_step, jurisdiction) + messages.info(request, "This filing now uses the updated filing screens.") + return redirect(url or get_step_url(WorkflowStepKey.FILING_PATH, jurisdiction)) diff --git a/efile_app/efile/views/payment.py b/efile_app/efile/views/payment.py index 746a39a..fca982b 100644 --- a/efile_app/efile/views/payment.py +++ b/efile_app/efile/views/payment.py @@ -1,51 +1,79 @@ -import logging +import json from django.conf import settings from django.contrib import messages from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods from efile.api.suffolk_api_views import get_tyler_token +from efile.models import FilingDocument, FilingParty from efile.services.current_drafts import ensure_current_draft -from efile.services.drafts import draft_snapshot +from efile.services.drafts import draft_snapshot, read_case_data -from ..utils.case_data_utils import get_case_data -from ..workflow import WorkflowStepKey, get_workflow_context - -logger = logging.getLogger(__name__) +from ..workflow import WorkflowStepKey, get_step_url, get_workflow_context +@require_http_methods(["GET", "POST"]) def efile_payment(request, jurisdiction): - """Review view for case details before final submission.""" - if not request.user.is_authenticated: - return redirect("efile_login", jurisdiction=jurisdiction) - - if not get_tyler_token(request, jurisdiction): + """Choose a payment account and quote court fees for the durable draft.""" + if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): return redirect("efile_login", jurisdiction=jurisdiction) - # Get case data from session - case_data = get_case_data(request, jurisdiction) - logger.debug("Review view case_data %s", case_data) + draft = ensure_current_draft( + request, + jurisdiction, + current_step=WorkflowStepKey.PAYMENT, + workflow_version=2, + ) + if not draft.court_code or not draft.case_type_code: + messages.error(request, "Confirm the case information before choosing payment.") + return redirect("extraction_review", jurisdiction=jurisdiction) + if not FilingDocument.objects.filter(draft=draft).exists(): + messages.error(request, "Add and organize at least one document before choosing payment.") + return redirect("upload_documents", jurisdiction=jurisdiction) + filer = FilingParty.objects.filter(draft=draft, role="filer").first() + if filer is None or not filer.party_type: + messages.error(request, "Complete the people in this filing before choosing payment.") + return redirect("parties", jurisdiction=jurisdiction) - # Add user email from session if available and not already in case_data - user_email = request.session.get("user_email") - if user_email and not case_data.get("email"): - case_data["email"] = user_email + if request.method == "POST": + account_id = request.POST.get("selected_payment_account", "").strip() + account_name = request.POST.get("selected_payment_account_name", "").strip() + if not account_id: + messages.error(request, "Choose a payment method to continue.") + else: + try: + fee_breakdown = json.loads(request.POST.get("quoted_fee_breakdown") or "[]") + except json.JSONDecodeError: + fee_breakdown = [] + if not isinstance(fee_breakdown, list): + fee_breakdown = [] - # If no case data exists, redirect back to expert form - if not case_data: - messages.error(request, "Please complete the case details first.") - return redirect("expert_form", jurisdiction=jurisdiction) - - filing_draft = ensure_current_draft(request, jurisdiction, current_step=WorkflowStepKey.PAYMENT) - - new_toga_url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/payments/new-toga-account" + draft.selected_payment_account_id = account_id + draft.selected_payment_account_name = account_name or "Selected payment method" + draft.selected_payment_account_type = request.POST.get("selected_payment_account_type", "").strip() + draft.quoted_fee_total = request.POST.get("quoted_fee_total", "").strip() + draft.quoted_fee_breakdown = fee_breakdown + draft.current_step = WorkflowStepKey.REVIEW + draft.save( + update_fields=[ + "selected_payment_account_id", + "selected_payment_account_name", + "selected_payment_account_type", + "quoted_fee_total", + "quoted_fee_breakdown", + "current_step", + "updated_at", + ] + ) + return redirect(get_step_url(WorkflowStepKey.REVIEW, jurisdiction)) context = { "is_logged_in": True, - "new_toga_url": new_toga_url, - "case_data": case_data, - "filing_draft": draft_snapshot(filing_draft), + "new_toga_url": f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/payments/new-toga-account", + "case_data": read_case_data(draft), + "filing_draft": draft_snapshot(draft), + "selected_payment_account_id": draft.selected_payment_account_id, } - context.update(get_workflow_context(WorkflowStepKey.PAYMENT, jurisdiction, filing_draft)) - + context.update(get_workflow_context(WorkflowStepKey.PAYMENT, jurisdiction, draft)) return render(request, "efile/payment.html", context) diff --git a/efile_app/efile/views/review.py b/efile_app/efile/views/review.py index 1753792..48972e9 100644 --- a/efile_app/efile/views/review.py +++ b/efile_app/efile/views/review.py @@ -1,125 +1,50 @@ -import logging - -from django.conf import settings from django.contrib import messages from django.shortcuts import redirect, render from efile.api.suffolk_api_views import get_tyler_token +from efile.models import FilingDocument, FilingParty from efile.services.current_drafts import ensure_current_draft -from efile.services.drafts import draft_snapshot +from efile.services.drafts import draft_snapshot, read_case_data, read_upload_data +from efile.services.people import get_case_questions -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__) - def case_review(request, jurisdiction): - """Review view for case details before final submission.""" - if not request.user.is_authenticated: + """Render a single read-only summary from the durable draft before submit.""" + if not request.user.is_authenticated or not get_tyler_token(request, jurisdiction): return redirect("efile_login", jurisdiction=jurisdiction) - if not get_tyler_token(request, jurisdiction): - return redirect("efile_login", jurisdiction=jurisdiction) - - # Get case data from session - case_data = get_case_data(request, jurisdiction) - logger.debug("Review view case_data %s", case_data) - - # Add user email from session if available and not already in case_data - user_email = request.session.get("user_email") - if user_email and not case_data.get("email"): - case_data["email"] = user_email - - # If no case data exists, redirect back to expert form - if not case_data: - messages.error(request, "Please complete the case details first.") - return redirect("expert_form", jurisdiction=jurisdiction) - - filing_draft = ensure_current_draft(request, jurisdiction, current_step=WorkflowStepKey.REVIEW) - - # Get organized case information - petitioner_info = get_petitioner_info(request, jurisdiction) - name_sought_info = get_name_sought_info(request, jurisdiction) - case_classification = get_case_classification(request, jurisdiction) - - # 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"]) - friendly_case_category = case_data.get("case_category_name", case_classification.get("case_category", "")) - friendly_document_type = case_data.get("document_type_name", case_classification.get("document_type", "")) - - # Organize data for review - review_sections = { - "case_classification": { - "title": "Case Classification", - "items": [ - {"label": "County/Court", "value": friendly_court, "raw": case_classification["court"]}, - { - "label": "Case Category", - "value": friendly_case_category, - "raw": case_classification.get("case_category", ""), - }, - {"label": "Case Type", "value": friendly_case_type, "raw": case_classification["case_type"]}, - {"label": "Filing Type", "value": friendly_filing_type, "raw": case_classification["filing_type"]}, - { - "label": "Document Type", - "value": friendly_document_type, - "raw": case_classification.get("document_type", ""), - }, - ], - }, - "petitioner_info": { - "title": "Petitioner Information", - "items": [ - {"label": "First Name", "value": petitioner_info.get("first_name", "")}, - {"label": "Last Name", "value": petitioner_info.get("last_name", "")}, - {"label": "Address", "value": petitioner_info.get("address", "")}, - {"label": "City", "value": petitioner_info.get("city", "")}, - {"label": "State", "value": petitioner_info.get("state", "")}, - {"label": "Zip Code", "value": petitioner_info.get("zip_code", "")}, - {"label": "Phone", "value": petitioner_info.get("phone", "")}, - {"label": "Email", "value": petitioner_info.get("email", "")}, - ], - }, - } - - # Add name sought info if it's a name change case - if "name change" in friendly_case_type.lower(): - review_sections["name_sought"] = { - "title": "Name Change Details", - "items": [ - {"label": "First Name", "value": name_sought_info.get("first_name", "")}, - {"label": "Last Name", "value": name_sought_info.get("last_name", "")}, - {"label": "Reason for Change", "value": case_data.get("reason_for_name_change", "")}, - ], - } - - # Add optional services if any - optional_services = case_data.get("optional_services", []) - if optional_services: - review_sections["optional_services"] = { - "title": "Optional Services", - "items": [{"label": "Selected Services", "value": ", ".join(optional_services)}], + draft = ensure_current_draft( + request, + jurisdiction, + current_step=WorkflowStepKey.REVIEW, + workflow_version=2, + ) + if not draft.selected_payment_account_id: + messages.error(request, "Choose a payment method before reviewing your filing.") + return redirect("payment", jurisdiction=jurisdiction) + + question_labels = {question["name"]: question["label"] for question in get_case_questions(draft)} + question_answers = [ + { + "label": question_labels.get(key, key.replace("_", " ").title()), + "value": "Yes" if value is True else "No" if value is False else value, } - - new_toga_url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/payments/new-toga-account" - + for key, value in (draft.supplemental_fields or {}).items() + if not key.startswith("_") and value not in (None, "") + ] + parties = FilingParty.objects.filter(draft=draft) context = { "is_logged_in": True, - "new_toga_url": new_toga_url, - "case_data": case_data, - "filing_draft": draft_snapshot(filing_draft), - "review_sections": review_sections, - "friendly_names": { - "case_type": friendly_case_type, - "filing_type": friendly_filing_type, - "court": friendly_court, - "case_category": friendly_case_category, - "document_type": friendly_document_type, - }, + "case_data": read_case_data(draft), + "upload_data": read_upload_data(draft), + "filing_draft": draft_snapshot(draft), + "draft": draft, + "filer": parties.filter(role="filer").first(), + "parties": parties.exclude(role="filer").order_by("sort_order", "created_at"), + "documents": FilingDocument.objects.filter(draft=draft).order_by("role", "sort_order", "created_at"), + "question_answers": question_answers, } - context.update(get_workflow_context(WorkflowStepKey.REVIEW, jurisdiction, filing_draft)) - + context.update(get_workflow_context(WorkflowStepKey.REVIEW, jurisdiction, draft)) return render(request, "efile/review.html", context) diff --git a/efile_app/efile/views/submission.py b/efile_app/efile/views/submission.py index 4e447f8..e919f33 100644 --- a/efile_app/efile/views/submission.py +++ b/efile_app/efile/views/submission.py @@ -10,6 +10,7 @@ from efile.services.current_drafts import clear_current_draft, get_current_draft from efile.services.submission_errors import PRE_SUBMIT_ERROR_CODES +from .confirmation import LAST_SUBMITTED_DRAFT_SESSION_KEY from .session_api import submit_final_filing as legacy_submit_final_filing logger = logging.getLogger(__name__) @@ -90,6 +91,8 @@ def submit_final_filing(request): if response.status_code < 400 and payload.get("success") is True: draft.mark_submitted(payload.get("api_response") or {}) + request.session[LAST_SUBMITTED_DRAFT_SESSION_KEY] = draft.pk + request.session.modified = True clear_current_draft(request) elif _failed_before_external_call(payload) or _confirmed_api_rejection(payload): # Nothing was filed (rejected before the call, or the API refused it), diff --git a/efile_app/efile/workflow.py b/efile_app/efile/workflow.py index 5b89eed..f92cb7a 100644 --- a/efile_app/efile/workflow.py +++ b/efile_app/efile/workflow.py @@ -5,9 +5,8 @@ those decisions here so templates and JavaScript do not each invent their own redirect rules. -``LEGACY_WORKFLOW`` remains temporarily available while the reorganized screens -land in stacked changes. A draft on a legacy step continues through the old flow; -as soon as it enters a reorganized step it uses ``FILING_WORKFLOW``. +Legacy step strings remain recognizable so saved bookmarks can be mapped into +the reorganized flow, but there is now one canonical workflow for every draft. """ from dataclasses import dataclass @@ -95,8 +94,8 @@ class WorkflowStepKey(StrEnum): REVIEW = "review" CONFIRMATION = "confirmation" - # Removed after all screens have migrated. Keeping these values temporarily - # lets saved drafts and each independently reviewable stacked PR keep working. + # Compatibility aliases for pre-migration drafts and URLs. They are not + # exposed as model choices or active workflow steps. UPLOAD_FIRST = "upload_first" CASE_INFORMATION = "case_information" DOCUMENTS = "documents" @@ -153,22 +152,12 @@ class WorkflowStep: WorkflowStep(WorkflowStepKey.CONFIRMATION, "Confirmation", "filing_confirmation", WorkflowStage.REVIEW), ) -LEGACY_WORKFLOW: tuple[WorkflowStep, ...] = ( - WorkflowStep(WorkflowStepKey.OPTIONS, "Options", "efile_options", WorkflowStage.FILING), - WorkflowStep(WorkflowStepKey.UPLOAD_FIRST, "Upload lead document", "upload_first", WorkflowStage.UPLOAD), - WorkflowStep(WorkflowStepKey.CASE_INFORMATION, "Case information", "expert_form", WorkflowStage.CONFIRM_CASE), - WorkflowStep(WorkflowStepKey.DOCUMENTS, "Documents", "upload", WorkflowStage.ORGANIZE_DOCUMENTS), - WorkflowStep(WorkflowStepKey.PAYMENT, "Fees", "payment", WorkflowStage.FEES), - WorkflowStep(WorkflowStepKey.REVIEW, "Review", "case_review", WorkflowStage.REVIEW), - WorkflowStep(WorkflowStepKey.CONFIRMATION, "Confirmation", "filing_confirmation", WorkflowStage.REVIEW), -) +_STEPS_BY_KEY = {step.key: step for step in FILING_WORKFLOW} -_STEPS_BY_KEY = {step.key: step for step in (*FILING_WORKFLOW, *LEGACY_WORKFLOW)} -_LEGACY_KEYS = {step.key for step in LEGACY_WORKFLOW} - { - WorkflowStepKey.OPTIONS, - WorkflowStepKey.PAYMENT, - WorkflowStepKey.REVIEW, - WorkflowStepKey.CONFIRMATION, +LEGACY_STEP_TARGETS = { + WorkflowStepKey.UPLOAD_FIRST: WorkflowStepKey.UPLOAD_DOCUMENTS, + WorkflowStepKey.CASE_INFORMATION: WorkflowStepKey.EXTRACTION_REVIEW, + WorkflowStepKey.DOCUMENTS: WorkflowStepKey.ORGANIZE_DOCUMENTS, } @@ -179,7 +168,7 @@ def get_workflow_steps() -> tuple[WorkflowStep, ...]: def get_workflow_step_choices() -> tuple[tuple[str, str], ...]: - """Return choices for target and temporarily supported legacy draft steps.""" + """Return choices for the canonical reorganized workflow.""" return tuple((step.key.value, step.label) for step in _STEPS_BY_KEY.values()) @@ -202,13 +191,30 @@ def _has_incomplete_parties(draft: Any | None) -> bool: if parties is None: return bool(_draft_value(draft, "has_incomplete_parties", False)) if hasattr(parties, "filter"): - incomplete = Q(organization_name="") & (Q(first_name="") | Q(last_name="")) + incomplete = ( + Q(party_type="") + | (Q(organization_name="") & (Q(first_name="") | Q(last_name=""))) + | Q(address_line_1="") + | Q(city="") + | Q(state="") + | Q(zip_code="") + ) return parties.filter(incomplete).exists() try: party_list = list(parties.all()) except (AttributeError, TypeError): party_list = list(parties) - return any(not (party.organization_name or (party.first_name and party.last_name)) for party in party_list) + return any( + not ( + party.party_type + and (party.organization_name or (party.first_name and party.last_name)) + and party.address_line_1 + and party.city + and party.state + and party.zip_code + ) + for party in party_list + ) def _has_case_questions(draft: Any | None) -> bool: @@ -220,26 +226,6 @@ def _has_case_questions(draft: Any | None) -> bool: return bool((_draft_value(draft, "supplemental_fields", {}) or {}).get("_case_questions_required")) -def _uses_legacy_workflow(current_step: WorkflowStepKey | str | None, draft: Any | None) -> bool: - raw_step = current_step or _draft_value(draft, "current_step") - try: - key = WorkflowStepKey(raw_step) - except (TypeError, ValueError): - return False - if key in _LEGACY_KEYS: - return True - if draft is None and key in { - WorkflowStepKey.OPTIONS, - WorkflowStepKey.PAYMENT, - WorkflowStepKey.REVIEW, - WorkflowStepKey.CONFIRMATION, - }: - return True - if draft is not None: - return int(_draft_value(draft, "workflow_version", 1)) < 2 - return False - - def get_visible_workflow( draft: Any | None = None, *, @@ -247,9 +233,6 @@ def get_visible_workflow( ) -> tuple[WorkflowStep, ...]: """Resolve the screens visible for this draft's branch.""" - if _uses_legacy_workflow(current_step, draft): - return LEGACY_WORKFLOW - existing_case = normalize_existing_case(_draft_value(draft, "existing_case")) current_key = None try: @@ -350,9 +333,10 @@ def get_resume_step_url(current_step: WorkflowStepKey | str | None, jurisdiction try: step_key = WorkflowStepKey(current_step) except ValueError: - step_key = WorkflowStepKey.UPLOAD_FIRST + step_key = WorkflowStepKey.FILING_PATH if step_key == WorkflowStepKey.OPTIONS: - step_key = WorkflowStepKey.UPLOAD_FIRST + step_key = WorkflowStepKey.FILING_PATH + step_key = LEGACY_STEP_TARGETS.get(step_key, step_key) return get_step_url(step_key, jurisdiction) diff --git a/efile_app/js-tests/filing-payload.test.js b/efile_app/js-tests/filing-payload.test.js index 498aaef..b32c679 100644 --- a/efile_app/js-tests/filing-payload.test.js +++ b/efile_app/js-tests/filing-payload.test.js @@ -146,4 +146,101 @@ test("the same module object serves both pages, so payloads cannot drift", () => assert.strictEqual(paymentHandler.buildEFilingData, reviewHandler.buildEFilingData); assert.strictEqual(paymentHandler.addCourtBundles, reviewHandler.addCourtBundles); assert.strictEqual(paymentHandler.createDocumentBundle, reviewHandler.createDocumentBundle); +}); + +test("saved filer information drives the filing contact instead of account profile data", () => { + const handler = makeHandler(); + const caseData = { + filing_parties: [{ + role: "filer", + first_name: "Jordan", + last_name: "Taylor", + address_line_1: "123 Main Street", + city: "Springfield", + state: "IL", + zip_code: "62701", + email: "jordan@example.com", + phone: "217-555-0100" + }] + }; + + assert.deepStrictEqual(handler.userDataFromCaseData(caseData), { + fullName: "Jordan Taylor", + address: "123 Main Street", + addressLine2: "", + city: "Springfield", + state: "IL", + zip: "62701", + email: "jordan@example.com", + phone: "217-555-0100" + }); +}); + +test("amount_in_controversy is sent when the draft has one", () => { + const handler = makeHandler(); + const caseData = { + case_category: "cat", + case_type: "type", + amount_in_controversy: "12500.00", + filing_parties: [{ + role: "filer", + party_type: "PLA", + first_name: "Jordan", + last_name: "Taylor" + }] + }; + const userData = handler.userDataFromCaseData(caseData); + + const result = handler.buildEFilingData(userData, caseData, {}, "pay-1"); + + assert.strictEqual(result.amount_in_controversy, "12500.00"); +}); + +test("amount_in_controversy is omitted (not sent as empty/zero) when the draft has none", () => { + const handler = makeHandler(); + const caseData = { + case_category: "cat", + case_type: "type", + filing_parties: [{ + role: "filer", + party_type: "PLA", + first_name: "Jordan", + last_name: "Taylor" + }] + }; + const userData = handler.userDataFromCaseData(caseData); + + const result = handler.buildEFilingData(userData, caseData, {}, "pay-1"); + + assert.strictEqual("amount_in_controversy" in result, false); +}); + +test("durable non-filer parties are included without collapsing to one legacy party", () => { + const handler = makeHandler(); + const caseData = { + case_category: "cat", + case_type: "type", + filing_parties: [{ + role: "filer", + party_type: "PLA", + first_name: "Jordan", + last_name: "Taylor" + }, { + role: "other", + party_type: "DEF", + first_name: "Alex", + last_name: "Morgan" + }, { + role: "other", + party_type: "DEF", + organization_name: "Example LLC" + }] + }; + const userData = handler.userDataFromCaseData(caseData); + const result = handler.buildEFilingData(userData, caseData, {}, "pay-1"); + + assert.strictEqual(result.users[0].party_type, "PLA"); + assert.strictEqual(result.other_parties.length, 2); + assert.strictEqual(result.other_parties[0].name.first, "Alex"); + assert.strictEqual(result.other_parties[1].name.first, "Example LLC"); }); \ No newline at end of file