Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,7 @@ test-results/
efile_app/screenshots
efile_app/playwright-report
efile_app/tmp

# Cookie jars left behind by curl-based manual testing
session_cookies.txt
*.cookies
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,10 @@ The Playwright configuration includes several important settings:
npx playwright test
```

- __Run specific tests__:
- __Run one spec__:
```bash
cd efile_app
npx playwright test tests/expert-form-name-change.spec.js
npx playwright test tests/expert-form-order-of-protection.spec.js
npx playwright test tests/expert-form-forfeiture-of-seized-property.spec.js
npx playwright test tests/reorganized-filing-matrix.spec.js
```

- __Run with UI mode__ (interactive):
Expand Down Expand Up @@ -313,17 +311,17 @@ The `test-utils.js` module provides two login methods:

### Available tests

- **`expert-form-name-change.spec.js`**: Tests the complete workflow for filing a name change case
- **`expert-form-order-of-protection.spec.js`**: Tests the complete workflow for filing an order of protection case
- **`expert-form-forfeiture-of-seized-property.spec.js`**: Tests the complete workflow for filing a forfeiture of seized property case

All tests:
1. Use shared login utilities from `test-utils.js`
2. Navigate to the appropriate expert form section
3. Fill out court selection and case details
4. Complete required party information
5. Verify the document upload page loads correctly
6. Take screenshots for visual verification
- **`reorganized-filing-matrix.spec.js`**: Files one envelope per scenario through
the whole workflow -- start a filing, upload a document, confirm the case codes,
answer the people and fees screens, submit -- for both new cases and filings into
an existing case, across a matrix of Illinois courts and case types.

It really files, in the Tyler test EFSP, so it is skipped unless you ask for it:

```bash
cd efile_app
RUN_FILING_MATRIX=1 npx playwright test tests/reorganized-filing-matrix.spec.js
```

Screenshots are saved to `screenshots/` directory and excluded from git via `.gitignore`.

Expand Down
136 changes: 10 additions & 126 deletions efile_app/efile/api/filing_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@

logger = logging.getLogger(__name__)

# TODO(brycew): this file doesn't work in it's current state. Keeping
# around for later refactors, when we inevitably want to start letting users
# handle filings themselves / see current status, etc.


def get_tyler_token(request, jurisdiction=None):
"""Helper method to retrieve Tyler token from various sources"""
Expand Down Expand Up @@ -120,6 +116,16 @@ def get_filings(request):
def convert_filing_data(filing):
data = {}
data["filing_status"] = filing.get("filingStatus").get("filingStatusCode")
# Tyler identifies the same filing two ways: the envelope number a clerk
# quotes over the phone, and the FILINGID the detail endpoint wants. The
# detail endpoint answers 422 to an envelope number, so keep both.
for identifier in filing.get("documentIdentification") or []:
category = ((identifier.get("identificationCategory") or {}).get("value") or {}).get("value", "")
value = (identifier.get("identificationID") or {}).get("value", "")
if str(category).upper() == "FILINGID":
data["filing_id"] = value
elif str(category).upper() == "ENVELOPEID":
data["envelope_id"] = value
data["filing_status_text"] = next(iter(filing.get("filingStatus").get("statusDescriptionText"))).get("value")
data["case_tracking_id"] = filing.get("caseTrackingID").get("value")
data["filed_timestamp"] = filing.get("documentFiledDate").get("dateRepresentation").get("value").get("value")
Expand All @@ -138,42 +144,6 @@ def convert_filing_data(filing):
# TODO(brycew): submitted by?
return data

@staticmethod
@require_http_methods(["POST"])
@csrf_exempt
def create_filing(request):
"""Create a new filing"""
try:
data = json.loads(request.body)

# Validate required fields
required_fields = ["case_category", "case_type", "filing_type", "county"]
missing_fields = [field for field in required_fields if not data.get(field)]

if missing_fields:
return FilingAPIViews.error_response(f"Missing required fields: {', '.join(missing_fields)}")

# API call to create filing
api_url = "https://suffolkefile.com/api/filings"
logger.debug("POST %s payload keys=%s", api_url, list(data.keys()))
response = requests.post(api_url, json=data, timeout=30)
logger.debug(
"Create filing response: status=%s content_type=%s",
response.status_code,
response.headers.get("Content-Type"),
)

if response.status_code == 201:
filing_data = response.json()
return FilingAPIViews.success_response(filing_data, "Filing created successfully")
else:
return FilingAPIViews.error_response("Failed to create filing")

except json.JSONDecodeError:
return FilingAPIViews.error_response("Invalid JSON data")
except Exception as e:
return FilingAPIViews.error_response(f"Error: {str(e)}")

@staticmethod
@require_http_methods(["POST"])
@csrf_exempt
Expand Down Expand Up @@ -249,93 +219,7 @@ def payment_fees(request):
except Exception as e:
return FilingAPIViews.error_response(f"Error: {str(e)}")

@staticmethod
@require_http_methods(["GET"])
def get_filing_detail(request, filing_id):
"""Get details for a specific filing"""
try:
# API call to get filing details
api_url = f"https://suffolkefile.com/api/filings/{filing_id}"
response = requests.get(api_url, timeout=30)
logger.debug(
"Filing detail response: status=%s content_type=%s",
response.status_code,
response.headers.get("Content-Type"),
)

if response.status_code == 200:
filing_data = response.json()
return FilingAPIViews.success_response(filing_data)
elif response.status_code == 404:
return FilingAPIViews.error_response("Filing not found", 404)
else:
return FilingAPIViews.error_response("Failed to fetch filing details")

except Exception as e:
return FilingAPIViews.error_response(f"Error: {str(e)}")

@staticmethod
@require_http_methods(["PUT"])
@csrf_exempt
def update_filing(request, filing_id):
"""Update an existing filing"""
try:
data = json.loads(request.body)

# API call to update filing
api_url = f"https://suffolkefile.com/api/filings/{filing_id}"
logger.debug("PUT %s payload keys=%s", api_url, list(data.keys()))
response = requests.put(api_url, json=data, timeout=30)
logger.debug(
"Update filing response: status=%s content_type=%s",
response.status_code,
response.headers.get("Content-Type"),
)

if response.status_code == 200:
filing_data = response.json()
return FilingAPIViews.success_response(filing_data, "Filing updated successfully")
elif response.status_code == 404:
return FilingAPIViews.error_response("Filing not found", 404)
else:
return FilingAPIViews.error_response("Failed to update filing")

except json.JSONDecodeError:
return FilingAPIViews.error_response("Invalid JSON data")
except Exception as e:
return FilingAPIViews.error_response(f"Error: {str(e)}")

@staticmethod
@require_http_methods(["DELETE"])
@csrf_exempt
def delete_filing(request, filing_id):
"""Delete a filing"""
try:
# API call to delete filing
api_url = f"https://suffolkefile.com/api/filings/{filing_id}"
logger.debug("DELETE %s", api_url)
response = requests.delete(api_url, timeout=30)
logger.debug(
"Delete filing response: status=%s content_type=%s",
response.status_code,
response.headers.get("Content-Type"),
)

if response.status_code == 204:
return FilingAPIViews.success_response({}, "Filing deleted successfully")
elif response.status_code == 404:
return FilingAPIViews.error_response("Filing not found", 404)
else:
return FilingAPIViews.error_response("Failed to delete filing")

except Exception as e:
return FilingAPIViews.error_response(f"Error: {str(e)}")


# Individual view functions for URL mapping
get_filings = FilingAPIViews.get_filings
create_filing = FilingAPIViews.create_filing
payment_fees = FilingAPIViews.payment_fees
get_filing_detail = FilingAPIViews.get_filing_detail
update_filing = FilingAPIViews.update_filing
delete_filing = FilingAPIViews.delete_filing
7 changes: 1 addition & 6 deletions efile_app/efile/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
get_optional_services,
get_party_types,
)
from .filing_views import create_filing, delete_filing, get_filing_detail, get_filings, payment_fees, update_filing
from .filing_views import get_filings, payment_fees
from .s3_upload import (
mock_s3_upload,
simple_s3_upload,
Expand All @@ -40,7 +40,6 @@
path("simple-s3-upload/", simple_s3_upload, name="simple_s3_upload"),
path("mock-s3-upload/", mock_s3_upload, name="mock_s3_upload"),
path("test-s3-connection/", test_s3_connection, name="test_s3_connection"),
# path("api/create-filing/", create_filing, name="create_filing"),
# Dropdown API endpoints
path("dropdowns/case-categories/", get_case_categories, name="case_categories"),
path("dropdowns/case-types/", get_case_types, name="case_types"),
Expand Down Expand Up @@ -68,8 +67,4 @@
path("payment-fees/", payment_fees, name="payment_fees"),
# Filing API endpoints
path("filings/", get_filings, name="get_filings"),
path("filings/create/", create_filing, name="create_filing"),
path("filings/<int:filing_id>/", get_filing_detail, name="filing_detail"),
path("filings/<int:filing_id>/update/", update_filing, name="update_filing"),
path("filings/<int:filing_id>/delete/", delete_filing, name="delete_filing"),
]
31 changes: 31 additions & 0 deletions efile_app/efile/migrations/0013_archived_cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 5.2.5 on 2026-08-18 00:37

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('efile', '0012_filing_plans'),
]

operations = [
migrations.CreateModel(
name='ArchivedCase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('jurisdiction', models.CharField(db_index=True, max_length=40)),
('case_tracking_id', models.CharField(max_length=255)),
('docket_number', models.CharField(blank=True, max_length=255)),
('case_title', models.CharField(blank=True, max_length=500)),
('archived_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='archived_cases', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-archived_at'],
'constraints': [models.UniqueConstraint(fields=('user', 'jurisdiction', 'case_tracking_id'), name='unique_archived_case_per_user')],
},
),
]
38 changes: 38 additions & 0 deletions efile_app/efile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,44 @@ def is_linked_to_a_case(self) -> bool:
return bool(self.case_tracking_id and self.docket_number)


class ArchivedCase(models.Model):
"""A court case the filer has told us to keep out of the way.

"My cases" is built from the court's own filing history, which we do not
own and cannot write to: an attorney with three hundred filings sees all
three hundred, forever. Archiving is therefore ours to remember -- one row
per case a filer has tidied away, and the case itself is untouched. Nothing
is deleted or hidden from the court; the list simply stops leading with it,
and the filer can still ask to see everything.
"""

user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="archived_cases",
)
jurisdiction = models.CharField(max_length=40, db_index=True)
# Tyler's permanent identifier for the case. Docket numbers are the human
# name for it and can be reissued or corrected, so they are display only.
case_tracking_id = models.CharField(max_length=255)
docket_number = models.CharField(max_length=255, blank=True)
case_title = models.CharField(max_length=500, blank=True)

archived_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ["-archived_at"]
constraints = [
models.UniqueConstraint(
fields=["user", "jurisdiction", "case_tracking_id"],
name="unique_archived_case_per_user",
)
]

def __str__(self):
return self.docket_number or self.case_title or f"Archived case #{self.pk}"


class FilingDraft(models.Model):
"""Durable aggregate for a single in-progress or submitted court filing."""

Expand Down
Loading