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
22 changes: 22 additions & 0 deletions docs/docs/partners-courts/jurisdiction-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ court_specific_requirements:
When a filing is rejected by a court clerk, LITEFile automatically surfaces the clerk's phone number and email address directly on the filer's status screen so they know who to call for assistance.
:::

### Other-party address rules

An other party's address is optional by default. Add a `party_address` rule only when a court or filing workflow is known to require it. The rule may be placed under `defaults`, under a case type, or directly under a court in `court_specific_requirements`. More specific layers override the default.

```yaml
defaults:
party_address:
required: false
required_for_party_types: []
required_for_filing_types: []
required_for_services: []

court_specific_requirements:
"example:civil":
party_address:
required_for_filing_types: ["SUMMONS"]
required_for_services: ["PERSONAL_SERVICE"]
reason: "The court needs an address to issue or serve these documents."
```

Set `required: true` when every other party in that layer needs an address. The three `required_for_*` lists may contain a Tyler code or name and are matched without regard to capitalization. A matching party type, filing type, or selected optional service makes the address required. LITEFile also honors an address-required flag from live Tyler party metadata if the code list provides one.

---

## 3. Wording that differs by state
Expand Down
12 changes: 10 additions & 2 deletions efile_app/efile/services/efsp_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

# "al_court_bundle.elements[0].filing_type" -> document 1, field filing_type
_BUNDLE_FIELD = re.compile(r"^al_court_bundle\.elements\[(\d+)\]\.(.+)$")
_OTHER_PARTY_ADDRESS_FIELD = re.compile(r"^other_parties\[(\d+)\]\.address\.(address|city|state|zip)$")

_MAX_RAW_BODY = 300

Expand Down Expand Up @@ -162,6 +163,7 @@ def _describe_var(var, *, missing: bool) -> str:
name = str(var.get("name") or "").strip()
if not name:
return ""
current = str(var.get("currentVal") or "").strip()

match = _BUNDLE_FIELD.match(name)
if match:
Expand All @@ -170,9 +172,15 @@ def _describe_var(var, *, missing: bool) -> str:
else:
where = ""

label = _FIELD_LABELS.get(name, name.replace("_", " "))
current = str(var.get("currentVal") or "").strip()
address_match = _OTHER_PARTY_ADDRESS_FIELD.match(name)
if address_match:
index, field = address_match.groups()
field_label = {"address": "street address", "zip": "ZIP code"}.get(field, field)
if current:
return f"{current!r} is not a {field_label} the court accepts for other party {int(index) + 1}"
return f"{field_label} is required for other party {int(index) + 1}'s mailing address"

label = _FIELD_LABELS.get(name, name.replace("_", " "))
if missing or not current:
return f"no {label} was given{where}"
return f"{current!r} is not a {label} this court accepts{where}"
118 changes: 118 additions & 0 deletions efile_app/efile/services/party_requirements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Determine when the court needs an other party's mailing address."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from efile.utils.config_loader import config_loader


@dataclass(frozen=True)
class AddressRequirement:
required: bool = False
reason: str = ""


_DEFAULT_REASON = "The court requires a mailing address for this party before the filing can continue."


def _values(items) -> set[str]:
return {str(item).strip().casefold() for item in items or [] if str(item).strip()}


def _matches(configured, *actual) -> bool:
wanted = _values(configured)
return bool(wanted.intersection(_values(actual)))


def _metadata_requires_address(party, party_types) -> bool:
if party is None:
return False
for party_type in party_types or []:
if str(party_type.get("code") or "") != str(party.party_type or ""):
continue
return party_type.get("address_required") is True
return False


def party_address_requirement(draft, party=None, *, party_types=None) -> AddressRequirement:
"""Resolve live metadata and layered YAML rules for one other party.

The default is deliberately optional. A state/case/court configuration can
require every address, or only addresses selected by party type, filing
type, or optional-service code. ``get_case_type_config`` has already merged
the base, state, case-type, and court layers before this function reads it.
"""
if draft is None:
return AddressRequirement()

# The filer's own contact address is collected on a separate screen and is
# always part of the EFSP user record. This resolver changes only the rule
# for other parties.
if party is not None and getattr(party, "role", "") == "filer":
return AddressRequirement(True, _DEFAULT_REASON)

if _metadata_requires_address(party, party_types):
return AddressRequirement(True, _DEFAULT_REASON)

jurisdiction = getattr(draft, "jurisdiction", "")
if not jurisdiction:
return AddressRequirement()
case_type = getattr(draft, "case_type_name", "") or getattr(draft, "case_type_code", "") or ""
jurisdiction_config = config_loader.load_jurisdiction_config(jurisdiction) or {}
config = (
config_loader.get_case_type_config(
jurisdiction,
case_type,
court=getattr(draft, "court_code", ""),
)
or {}
)
rule: dict[str, Any] = config_loader._deep_merge(
(jurisdiction_config.get("defaults") or {}).get("party_address") or {},
config.get("party_address") or {},
)
court_config = (jurisdiction_config.get("court_specific_requirements") or {}).get(
getattr(draft, "court_code", ""),
{},
)
rule = config_loader._deep_merge(rule, court_config.get("party_address") or {})
reason = str(rule.get("reason") or _DEFAULT_REASON)

if rule.get("required") is True:
return AddressRequirement(True, reason)

if party is not None and _matches(
rule.get("required_for_party_types"),
party.party_type,
party.party_type_name,
):
return AddressRequirement(True, reason)

document_manager = getattr(draft, "documents", None)
documents = list(document_manager.all()) if hasattr(document_manager, "all") else []
if any(
_matches(rule.get("required_for_filing_types"), document.filing_type_code, document.filing_type_name)
for document in documents
):
return AddressRequirement(True, reason)

selected_services = {str(code) for document in documents for code in (document.requested_optional_services or [])}
selected_services.update(str(code) for code in (getattr(draft, "optional_services", None) or []))
if _matches(rule.get("required_for_services"), *selected_services):
return AddressRequirement(True, reason)

return AddressRequirement()


def address_values(party) -> tuple[str, str, str, str]:
return (party.address_line_1, party.city, party.state, party.zip_code)


def address_is_blank(party) -> bool:
return not any((*address_values(party), party.address_line_2))


def address_is_complete(party) -> bool:
return all(address_values(party))
27 changes: 22 additions & 5 deletions efile_app/efile/services/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from efile.party_sides import PARTY_SIDE_KEYWORDS, PartySide, side_for_party_type_name
from efile.services.document_checklists import party_type_keywords_for_role
from efile.services.extracted_parties import party_display_name
from efile.services.party_requirements import address_is_blank, address_is_complete, party_address_requirement
from efile.utils.config_loader import config_loader
from efile.workflow import ExistingCase

Expand All @@ -24,14 +25,23 @@
_RESPONDING_PARTY_KEYWORDS = PARTY_SIDE_KEYWORDS[PartySide.RESPONDING]


def party_is_complete(party: FilingParty) -> bool:
def party_is_complete(party: FilingParty, *, draft=None, party_types=None) -> bool:
has_name = bool(party.organization_name or (party.first_name and party.last_name))
has_address = bool(party.address_line_1 and party.city and party.state and party.zip_code)
return bool(party.party_type and has_name and has_address)
address_required = party_address_requirement(
draft or getattr(party, "draft", None),
party,
party_types=party_types,
).required
valid_address = address_is_complete(party) or (not address_required and address_is_blank(party))
return bool(party.party_type and has_name and valid_address)


def incomplete_parties(draft: FilingDraft):
return [party for party in FilingParty.objects.filter(draft=draft) if not party_is_complete(party)]
def incomplete_parties(draft: FilingDraft, *, party_types=None):
return [
party
for party in FilingParty.objects.filter(draft=draft)
if not party_is_complete(party, draft=draft, party_types=party_types)
]


def get_party_types(draft: FilingDraft) -> list[dict[str, Any]]:
Expand All @@ -55,6 +65,13 @@ def get_party_types(draft: FilingDraft) -> list[dict[str, Any]]:
"code": str(item.get("code") or ""),
"name": str(item.get("name") or ""),
"required": str(item.get("isrequired", "")).lower() == "true" or item.get("isrequired") is True,
# Tyler does not currently return one of these fields in the
# Illinois staging lists we checked. Preserve support for the live
# metadata rather than forcing a future flag into static YAML.
"address_required": any(
str(item.get(key, "")).lower() == "true"
for key in ("addressrequired", "addressRequired", "partyaddressrequired", "requirespartyaddress")
),
}
for item in data
if isinstance(item, dict) and item.get("code") and item.get("name")
Expand Down
18 changes: 13 additions & 5 deletions efile_app/efile/static/config/base-case-types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ jurisdiction:
contact_address: "Suffolk University Law School LIT Lab, 120 Tremont Street, Boston, MA"

defaults:
# Other-party addresses are optional unless a state/case/court layer, a
# selected filing/service code, or live Tyler metadata says otherwise.
party_address:
required: false
required_for_party_types: []
required_for_filing_types: []
required_for_services: []
reason: "The court requires a mailing address for this party before the filing can continue."
sections:
parties:
title: "Required parties"
Expand Down Expand Up @@ -61,14 +69,14 @@ defaults:
required: true
column_width: "col-6"
- section_title: "Their physical address"
required: true
required: false
conditional_requirements: {}
# Default: show for all courts except those explicitly hidden
fields:
- name: "other_address_line_1"
label: "Street Address"
type: "text"
required: true
required: false
column_width: "col-12"
- name: "other_address_line_2"
label: "Street Address 2"
Expand All @@ -78,17 +86,17 @@ defaults:
- name: "other_address_city"
label: "City"
type: "text"
required: true
required: false
column_width: "col-6"
- name: "other_address_state"
label: "State"
type: "us_state"
required: true
required: false
column_width: "col-6"
- name: "other_address_zip"
label: "Zip Code"
type: "text"
required: true
required: false
column_width: "col-6"
- section_title: "Their contact information"
required: true
Expand Down
34 changes: 34 additions & 0 deletions efile_app/efile/static/css/reorganized-flow.css
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,40 @@
font-weight: 500;
}

.address-choice {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.85rem;
margin-bottom: 1rem;
}

.address-choice .form-check {
margin-bottom: 0;
}

.address-explainer {
background: none;
border: 0;
border-bottom: 1px dotted var(--better-blue);
color: var(--better-blue);
font-size: 0.92rem;
padding: 0;
}

.address-explainer:hover,
.address-explainer:focus-visible {
border-bottom-style: solid;
}

.address-explainer-popover {
max-width: 23rem;
}

.address-explainer-popover .popover-body p:last-child {
margin-bottom: 0;
}

.people-grid {
display: grid;
gap: 1rem;
Expand Down
42 changes: 26 additions & 16 deletions efile_app/efile/static/js/filing-payload.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ const FilingPayload = {
},

partyFromDraft(party) {
const 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"
};
return {
party_type: party.party_type,
name: {
Expand All @@ -58,14 +66,12 @@ const FilingPayload = {
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"
},
// Tyler validates a present address object, even when all its
// values are blank. Omit it when the optional address is wholly
// blank; otherwise staging rejects the blank state as a bad code.
...([address.address, address.unit, address.city, address.state, address.zip].some(Boolean) ? {
address
} : {}),
email: party.email || "",
phone_number: party.phone || "",
is_new: !party.external_party_id
Expand Down Expand Up @@ -152,20 +158,24 @@ const FilingPayload = {
.map((party) => this.partyFromDraft(party));

if (other_parties.length === 0 && caseData.other_first_name && caseData.other_party_type) {
const legacyAddress = {
address: caseData.other_address_line_1 || "",
unit: caseData.other_address_line_2 || "",
city: caseData.other_address_city || "",
state: caseData.other_address_state || "",
zip: caseData.other_address_zip || "",
country: "US"
};
other_parties.push({
party_type: caseData.other_party_type,
name: {
first: caseData.other_first_name,
last: caseData.other_last_name
},
address: {
address: caseData.other_address_line_1,
unit: caseData.other_address_line_2,
city: caseData.other_address_city,
state: caseData.other_address_state,
zip: caseData.other_address_zip,
country: "US"
},
...([legacyAddress.address, legacyAddress.unit, legacyAddress.city, legacyAddress.state, legacyAddress.zip]
.some(Boolean) ? {
address: legacyAddress
} : {}),
email: caseData.other_email,
phone_number: caseData.other_phone_number,
is_new: true,
Expand Down
Loading