Skip to content

Let someone file on behalf of a party they are not - #213

Merged
nonprofittechy merged 8 commits into
mainfrom
207-separate-filer-from-party
Sep 2, 2026
Merged

Let someone file on behalf of a party they are not#213
nonprofittechy merged 8 commits into
mainfrom
207-separate-filer-from-party

Conversation

@nonprofittechy

Copy link
Copy Markdown
Member

The bug

The parties screen refused to continue until the person signed in gave themselves a court party type. Anyone filing for someone else — a parent for a child, a neighbour helping answer an eviction, a friend with the only e-filing account in the house — could not get through it at all.

Being the filer and being a party are different questions, and Tyler already keeps them apart: users is the list of parties a filing is made on behalf of, and lead_contact is whoever filed it. LITEFile was collapsing the two.

What changed

FilingParty.is_filing_party records who the filing is for. The filer's own row keeps the contact record, its party_type is now optional, and an empty one means they are not in the case at all. The role question offers "I am not one of the parties — I am filing for someone else", and then asks which party the filing is for. The payment screen's gate moved with it: it asks whether the filing has a party, not whether the filer is one.

"This is me", wherever the people are. Every listed party can be claimed as yourself — on the party list, on the party-details screen, and (FilingParty.is_self) on the extraction-review screen where the document's names first appear, which is where people actually notice. Claiming adopts that party's court role, folds them into the filer's row (the one with the address the court needs), and files on their behalf.

Claiming a differently-named party asks first. It replaces a person in the case, so it is not called "This is me" — it is called "Replace with me", and it opens a dialog that settles two things: which name the court should see (neither pre-selected — a filer named by a former name wants one answer, a filer fixing a misread name wants the other), and whether they are helping that person rather than being them, with a button that backs out into "I am filing for them" instead. The server refuses the claim without a name answer, so a browser that never ran the dialog cannot skip the question. An organization is never offered, because accounts register as individuals.

Guards against parties nobody added. "Unknown", "N/A" and friends in the names field are the extraction saying it found nobody, not people — dropped on a whole-name comparison, never a substring, because All Unknown Occupants is a real eviction defendant. The same name read onto two sides is one person, listed once. A row started by "add another person" and never named is swept up rather than stranded on the list.

person_type for organizations. The EFSP reads a party as a business only when the entry says so, and LITEFile never sent the field — so a company reached Tyler as a person with an empty PersonSurName and the court rejected the envelope at the fee quote. This one predates the branch; the filer/party work just made it easy to reach.

Notices get an address that can be someone else's. Filing for another person, users[0].email was quietly filled in with the filer's, because Tyler rejects a new case whose first filing party has no email. FilingDraft.notice_email makes that a question — asked only when filing for someone else, offered filled in with their own address, editable to the party's — and the review screen says where notices go, with a link back to change it.

Copy that matches the route taken. With the people settled earlier, the parties screen says the list is there to be checked, not filled in, and it no longer offers "based on this filing, you are likely the Plaintiff" over the top of people the filer already named. A filer who turned AI off gets keyword_document_analysis, which reads a form number and a case number and never a name — so on that route the list is entirely their own typing, and they are told "these are the people you told us about" rather than being credited a reading that never happened.

Migrations

Three, all additive: 0019_filing_party, 0020_notice_email, 0021_party_is_self. 0019 backfills existing drafts (a filer with a party type was the only possible filing party before this), and both filing_parties() and the payload builder fall back to the filer when nothing is marked, so drafts mid-flight keep working.

Testing

  • 651 Python tests (~75 new, mostly in test_filing_on_behalf.py and test_extracted_parties.py) and 42 JS unit tests (12 new in js-tests/filing-payload.test.js). Ruff, ty and ESLint clean.
  • Live probes against the EFSP staging proxy, Kane County, using the codes from a real draft. The PersonSurName fix was confirmed by sending two otherwise identical fee-quote payloads: without person_type422 PersonSurName is required or does not match regular expression; with it → 200 and a fee quote. Worth knowing for the next person who debugs this: without a real tyler_payment_id Tyler fails on PaymentID is empty before it validates names, which masks the error entirely — my first two probe runs "passed" that way.
  • Driven in a real browser (Playwright against the local stack) for the claim dialog and the extraction-review toggle: labels, dialog contents, the refusal to confirm without a name answer, the "I am filing for them instead" escape, one-at-a-time selection, and that a row added by hand keeps the parallel form arrays aligned.
  • @Quinten filed end-to-end through the new workflow successfully, which is what turned up the last two bugs below.

How this got to here

Worth recording, because most of the fixes came from use rather than from tests:

  1. The original block was reproduced with a probe before anything changed — a filer with a complete tenant party still could not pass the step.
  2. The first "add me" work landed on the parties screen. A real session showed the missing action was different: with both required party types already taken by detected people, the only offers were to add yourself as a second plaintiff or to say you were not a party. Claiming an existing row was the thing that was missing.
  3. The 422 was misdiagnosed twice before the payment-ID masking above was understood.
  4. "This is me" silently renamed the party in the case; that was caught in review here, not by tests, and became the confirmation dialog.
  5. Reading a screenshot rather than the HTML caught three rendering bugs the tests were blind to: multi-line {# #} comments rendering as visible page text (Django only treats those as comments on one line), a hint overlapping the buttons in a flex row, and a row grid with fewer columns than the row had controls.
  6. The AI opt-out route was flagged as unhandled, and the screen copy is now conditional on which route produced the list.
  7. Reading my own screenshot again turned up "Replace with me" beside an LLC — meaningless for an individual account, and a second route into the very PersonSurName failure fixed earlier in the branch, since the filer's own row would have become the company.

Issues

Closes #207.

Advances #197 without closing it — a filer who is not a party is now supported and can say who they are filing for, and the account's saved details are offered as an explicit action rather than only defaulted. Still open there: asking at registration whether the account is mainly used for other people's filings, and keeping court-sourced parties read-only in an existing case.

Known gaps

  • The notice email is only asked when filing for someone else. A filer who is a party still gets notices at their account address with no way to redirect them.
  • absorb_filer_duplicates still matches on an exact normalised name, so "Q. Steenhuis" and "Quinten Steenhuis" are two people to it. The claim button is the manual answer to that.

🤖 Generated with Claude Code

https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx

nonprofittechy and others added 7 commits September 1, 2026 20:29
The parties screen refused to continue until the person signed in gave
*themselves* a court party type, so anyone filing for someone else -- a
parent for a child, a neighbour helping answer an eviction -- could not
get through it at all. Being the filer and being a party are different
questions, and Tyler already keeps them apart: `users` is the list of
parties a filing is made on behalf of, and `lead_contact` is whoever
filed it.

FilingParty.is_filing_party records the first of those. The filer's own
row keeps the second, its party_type is now optional, and an empty one
means they are not in the case at all. The role question offers "I am
filing for someone else" alongside the court's party types, and then
asks which party the filing is for.

Also fixes the same assumption one step later: the payment screen sent
a filer with no party type back to the people step, which a filer who
is not a party could never satisfy. It now asks whether the filing has
a party at all, which is the thing that actually has to be settled.

Closes #207 by making "add me as a party" an explicit button rather
than a step nobody could skip:

* on the party list, for a filer who is not yet a party
* on the review page, next to who the filing is on behalf of
* on the add-a-person screen, as "Actually, this party is me" -- which
  drops the blank row instead of writing a second copy of the filer,
  who is already on the draft with a name and address

Drafts answered before the question existed still file as the filer:
the migration backfills them, and both filing_parties() and the payload
builder fall back to the filer when nothing is marked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
Three things that meet on the parties screen.

**"Unknown" is not a person.** A model asked to list the parties will
sometimes answer, in the field where the names go, that it could not
find any -- and "Unknown", "N/A" and "None" became parties the filer
then had to work out how to get rid of. They are dropped now, on a
whole-name comparison and never a substring: "All Unknown Occupants"
is a real defendant in a real eviction, and a guard that reached
inside names would delete them. The same name read onto two sides is
also one person misread rather than two people, and is listed once.

Two other ways an unnamed party appeared: adding a person makes the
row before the form that names them, so leaving that form without
saving stranded a nameless entry on the list, which is now cleared on
the way back in. A nameless row with a party type is left alone --
that is the court's own required-party placeholder, waiting for a name
rather than missing one by accident.

**A caption name matching the filer is a question, not an answer.** It
used to be folded into the filer silently, which was safe while the
filer was always a party. Now that they need not be, deleting a party
because they share a name with the person filing takes a real party
out of the case -- someone filing for a relative they are named after
is exactly the case this screen now exists for. So the fold waits for
a filer who has said they are a party, and until then the match is put
to them as "your document lists Jamie Rivera as the Defendant -- is
this you?", with one button to say yes. It is the strongest suggestion
available (the document said which side they were on) so it replaces
the vaguer case-posture guess, and it stops being offered once they
have answered the role question either way.

**Notices need an address that can be someone else's.** Filing for
another person, `users[0].email` was quietly filled in with the
filer's, because Tyler rejects a new case whose first filing party has
no email. Quietly is the problem: it decided where the court writes
about someone else's case without asking. FilingDraft.notice_email is
that question, asked only of someone filing for a party they are not,
offered filled in with their own address and editable to the party's
or to whoever handles their mail. It reaches the payload as the lead
contact's address and as the filing party's when they have none of
their own, and the review screen says where notices go with a link
back to change it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
…companies

Two things found filing a real Kane County small claim.

**Organizations reached Tyler as people with no surname.** The EFSP
reads a party as a business only when the entry says `person_type`,
and LITEFile has never sent that field, so a company went over as a
PersonType whose PersonSurName was empty -- `partyFromDraft` puts an
organization's one name in `name.first` and leaves `name.last` blank.
The court rejected the whole envelope as far along as the fee quote:
"the court's filing service returned status 422: PersonSurName is
required or does not match regular expression." Confirmed against the
live Kane endpoint with the same case codes: without the field, 422
with that exact message; with it, 200 and a fee quote.

This is older than the filing-party work, but that work made it much
easier to hit -- an eviction or a small claim against a company now
routinely puts the company on a roster the filer never has to open.

**There was no way to say "that detected party is me".** The court's
required party types are routinely all taken by people the document
named, so the only offers on the parties screen were to add yourself
as a *second* plaintiff or to say you were not a party at all. Neither
is what someone means when their own name is already in the caption
under a spelling the name match did not catch, or when they are the
tenant the AI read off the complaint.

Every listed party now carries "This is me", which adopts that party's
court role, folds them into the filer's own row -- the one with the
address the court needs -- and files on their behalf. The same button
is on the party-details screen for a party that already has a name,
where it used to be offered only for a blank row, and the party list
says so for a filer who would otherwise add themselves twice. The
name-match suggestion is now the pre-answered case of this rather than
the only way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
"This is me" quietly took the claimed party's court role and deleted
their row, which meant the name in the case silently became the
name on the account. Two different people can be behind that click:
someone whose caption name is not the one their account carries, and
someone who read "this is me" as "I am helping this person" -- and
the second is the reading the words invite. Only the first is what
the button does, and it does it to the party list the court sees.

So the two cases are no longer offered under the same words. A row
that already carries the filer's name still says "This is me", and
claiming it asks nothing, because nothing changes. A row with someone
else's name says "Replace with me" and opens a confirmation that has
to settle two things before anything happens:

* Which name the court should see -- theirs, or the one already in
  the case. Neither is pre-selected: a filer whose complaint names
  them by a former name wants one, a filer correcting a misread name
  wants the other, and only they know which. The server refuses the
  claim without an answer, so a browser that never ran the dialog
  cannot skip the question either.
* Whether they are *helping* that person rather than being them, with
  a button that backs out into the answer they actually wanted --
  keeping the party on the filing and marking themselves as filing on
  that party's behalf.

Claiming a row nobody has named yet is neither of those and still
asks nothing: there is no name to replace.

Fixes three things found by looking at the rendered page: multi-line
{# #} comments were rendering as visible text (Django only treats
those as comments on one line), the party list's hint was inside the
heading's flex row and overlapped the buttons, and the row grid had
one fewer column than the row now has controls, wrapping the delete
button onto its own line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
The people in a case are first shown two screens before the one that
asks the filer their own role, and that is the moment someone looks at
a name the system read off their complaint and thinks "that is me".
Until now there was nothing to do about it there, and the answer had
to wait for a screen that had forgotten the question was obvious.

Each person on the review screen now carries "This is me".
FilingParty.is_self records it -- on the party, because the filer has
no row of their own until two screens later -- and the parties screen
spends it: the role question comes back already answered and the
duplicate already folded in. When the two names disagree the fold
waits, because replacing one name with another is the same question
wherever it was asked from, and it goes through the same confirmation
as any other claim.

Only one person can be the filer, so choosing a row un-chooses
whichever held it. The answer rides on a hidden value rather than a
checkbox, so an unticked row still posts one and stays aligned with
the names beside it -- the same reason every other field on that
editor posts from every row.

With that answered earlier, the parties screen stops pretending it is
the first time. It says the list is there to be checked rather than
filled in, and it no longer offers "based on this filing, you are
likely the Plaintiff" over the top of people the filer has already
named -- a guess that contradicts a better answer they already gave.

Both routes into that screen are worded for. A filer who turned AI
off gets keyword_document_analysis, which reads a form number and a
case number and never a name, so their party list is entirely their
own typing: they are told "these are the people you told us about",
and only a list that actually came off the document is credited to
having read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
"Replace with me" sat next to Fox River Phone Repair LLC on the party
list, which no individual can truthfully say -- accounts register as
individuals (views.register sends registrationType: INDIVIDUAL), and a
landlord whose LLC is the plaintiff wants to file *for* the company.

It was also a way to break the filing. Claiming a company and keeping
its name would have made the filer's own row an organization, and the
filer's row reaches Tyler through accountUser(), which splits one name
into a first and a last and never says the party is a business -- the
same missing person_type that answered a fee quote with "PersonSurName
is required" a few commits ago, arrived at from the other direction.

Refused in the view as well as hidden on both screens, so it is not
only the button that is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
The claim button was taken off the party list a commit ago but left on
the screen the names first appear on, so a tick on a company was still
reachable -- and led to a refusal two screens later, which is a dead
end rather than an answer. Hidden there too, and ignored by
self_claimed_party in case an older draft carries one.

The hidden value still posts from every row. It is what keeps the
lists the view reads back lined up with the names beside them, so it
is the button that is conditional and never the field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A confirmed bug remains where an organization row can consume the single “is self” selection and block marking the actual person, due to save_reviewed_parties not gating is_self on non-organizations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR updates LITEFile’s “People/Parties” workflow to support a filer who is not themselves a party, while still producing a valid Tyler/EFSP filing payload (including correct filing-party selection, notice email routing, and organization/business handling).

Changes:

  • Add durable model + workflow support for “filing on behalf of” via FilingParty.is_filing_party, optional filer party_type, and a parties-screen flow to select the represented party (plus notice_email).
  • Add “This is me / Replace with me” claiming UX across extraction review, parties list, and party-details, including a confirmation dialog for name-replacement cases.
  • Fix organization payload semantics by sending person_type: "business" for org parties to avoid Tyler surname validation failures.
File summaries
File Description
efile_app/js-tests/filing-payload.test.js Expands JS payload unit tests for filing-on-behalf + org handling.
efile_app/efile/views/review.py Adds review context for “filing for” and notice email display.
efile_app/efile/views/payment.py Gates payment on having at least one filing party (not filer party_type).
efile_app/efile/views/party_details.py Adds claim/replace context for party-details screen.
efile_app/efile/views/parties.py Implements filer-not-a-party flow, claiming, and notice-email capture.
efile_app/efile/views/extraction_review.py Reads “is self” per extracted party row from the POSTed form.
efile_app/efile/utils/ui_text.py Updates role-help copy to mention filing for someone else.
efile_app/efile/tests/test_people_flow.py Updates assertions for the revised role-help copy.
efile_app/efile/tests/test_filing_on_behalf.py New test suite covering filing-on-behalf workflow paths.
efile_app/efile/tests/test_extracted_parties.py Adds tests for placeholder-name filtering, de-dupe, and is-self behavior.
efile_app/efile/templates/efile/review.html Displays “not a party / filing on behalf of …” and notice-email summary.
efile_app/efile/templates/efile/party_details.html Adds claim/replace UI and includes the claim dialog + JS.
efile_app/efile/templates/efile/parties.html Adds “not a party” option, filing-for checklist, claim buttons, hints, and dialog include.
efile_app/efile/templates/efile/extraction_review.html Adds “This is me” toggles (hidden field + button) per extracted row.
efile_app/efile/templates/efile/components/claim_party_dialog.html New reusable dialog for confirming “replace with me” claims.
efile_app/efile/static/js/parties.js Syncs filing-for section visibility + “Add me as a party” shortcut.
efile_app/efile/static/js/filing-payload.js Resolves filing parties distinct from filer; adds notice-email routing and org person_type.
efile_app/efile/static/js/extraction-review.js Implements one-at-a-time “This is me” toggle behavior.
efile_app/efile/static/js/claim-party.js New dialog controller for claim/replace confirmation + “file for them instead” path.
efile_app/efile/static/css/reorganized-flow.css Styles for claim dialog and updated party/extraction review layouts.
efile_app/efile/services/people.py Adds filing-party resolution helpers + claiming logic + blank-row cleanup.
efile_app/efile/services/extracted_parties.py Adds placeholder-name filtering, cross-side de-dupe, and is-self persistence.
efile_app/efile/services/drafts.py Includes notice_email and is_filing_party in serialized case data.
efile_app/efile/models.py Adds notice_email, FilingParty.is_self, and FilingParty.is_filing_party.
efile_app/efile/migrations/0021_party_is_self.py Adds DB field for FilingParty.is_self.
efile_app/efile/migrations/0020_notice_email.py Adds DB field for FilingDraft.notice_email.
efile_app/efile/migrations/0019_filing_party.py Adds DB field for FilingParty.is_filing_party + backfill migration.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread efile_app/efile/services/extracted_parties.py Outdated
Comment thread efile_app/efile/views/parties.py
Review catch. The screens stopped offering the tick on an organization
and the parties screen ignores one, but nothing stopped it being
*written* -- and a name only becomes a company when apply_name runs on
the same submit that ticked it, so the UI cannot be the whole guard.

Two ways that bit. A tick on a company was stored as an answer nothing
downstream could act on, so the filer's answer quietly went nowhere.
And because a company took the one self slot, a real person ticked on
a later row lost the tick to a row that could never use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MLBD8Co7jdvH2ZmtQeEwx
@nonprofittechy
nonprofittechy merged commit b32f736 into main Sep 2, 2026
6 checks passed
@nonprofittechy
nonprofittechy deleted the 207-separate-filer-from-party branch September 2, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an "add me" button to the case review/party addition page?

2 participants